From e7c20e1052faf803b779338695c446e58b1b70a1 Mon Sep 17 00:00:00 2001 From: jakedt Date: Mon, 31 Mar 2014 15:40:24 -0400 Subject: [PATCH 1/4] Add tarball support to the builder and pull github code as a tarball. --- endpoints/trigger.py | 24 +++++++++++++----------- workers/dockerfilebuild.py | 17 +++++++++++++++-- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/endpoints/trigger.py b/endpoints/trigger.py index 873f51c9b..d5573a513 100644 --- a/endpoints/trigger.py +++ b/endpoints/trigger.py @@ -1,7 +1,7 @@ import logging import io import os.path -import zipfile +import tarfile from github import Github, UnknownObjectException, GithubException from tempfile import SpooledTemporaryFile @@ -16,7 +16,7 @@ client = app.config['HTTPCLIENT'] logger = logging.getLogger(__name__) -ZIPBALL = 'application/zip' +TARBALL_MIME = 'application/gzip' CHUNK_SIZE = 512 * 1024 @@ -222,19 +222,22 @@ class GithubBuildTrigger(BuildTrigger): @staticmethod def _prepare_build(config, repo, commit_sha, build_name, ref): # Prepare the download and upload URLs - archive_link = repo.get_archive_link('zipball', commit_sha) + archive_link = repo.get_archive_link('tarball', commit_sha) download_archive = client.get(archive_link, stream=True) - zipball_subdir = '' - with SpooledTemporaryFile(CHUNK_SIZE) as zipball: + tarball_subdir = '' + with SpooledTemporaryFile(CHUNK_SIZE) as tarball: for chunk in download_archive.iter_content(CHUNK_SIZE): - zipball.write(chunk) + tarball.write(chunk) + + # Seek to position 0 to make tarfile happy + tarball.seek(0) # Pull out the name of the subdir that GitHub generated - with zipfile.ZipFile(zipball) as archive: - zipball_subdir = archive.namelist()[0] + with tarfile.open(fileobj=tarball) as archive: + tarball_subdir = archive.getnames()[0] - dockerfile_id = user_files.store_file(zipball, ZIPBALL) + dockerfile_id = user_files.store_file(tarball, TARBALL_MIME) logger.debug('Successfully prepared job') @@ -247,7 +250,7 @@ class GithubBuildTrigger(BuildTrigger): # compute the subdir repo_subdir = config['subdir'] - joined_subdir = os.path.join(zipball_subdir, repo_subdir) + joined_subdir = os.path.join(tarball_subdir, repo_subdir) logger.debug('Final subdir: %s' % joined_subdir) return dockerfile_id, list(tags), build_name, joined_subdir @@ -280,7 +283,6 @@ class GithubBuildTrigger(BuildTrigger): def manual_start(self, auth_token, config): source = config['build_source'] - subdir = config['subdir'] gh_client = self._get_client(auth_token) repo = gh_client.get_repo(source) diff --git a/workers/dockerfilebuild.py b/workers/dockerfilebuild.py index 7e75fc429..f44642ff2 100644 --- a/workers/dockerfilebuild.py +++ b/workers/dockerfilebuild.py @@ -6,6 +6,7 @@ import requests import re import json import shutil +import tarfile from docker import Client, APIError from tempfile import TemporaryFile, mkdtemp @@ -290,6 +291,8 @@ class DockerfileBuildWorker(Worker): 'application/x-zip-compressed': DockerfileBuildWorker.__prepare_zip, 'text/plain': DockerfileBuildWorker.__prepare_dockerfile, 'application/octet-stream': DockerfileBuildWorker.__prepare_dockerfile, + 'application/x-tar': DockerfileBuildWorker.__prepare_tarball, + 'application/gzip': DockerfileBuildWorker.__prepare_tarball, } @staticmethod @@ -298,7 +301,7 @@ class DockerfileBuildWorker(Worker): # Save the zip file to temp somewhere with TemporaryFile() as zip_file: - zip_file.write(request_file.content) + zip_file.write(request_file.raw) to_extract = ZipFile(zip_file) to_extract.extractall(build_dir) @@ -313,6 +316,16 @@ class DockerfileBuildWorker(Worker): return build_dir + @staticmethod + def __prepare_tarball(request_file): + build_dir = mkdtemp(prefix='docker-build-') + + # Save the zip file to temp somewhere + with tarfile.open(mode='r|*', fileobj=request_file.raw) as tar_stream: + tar_stream.extractall(build_dir) + + return build_dir + def process_queue_item(self, job_details): repository_build = model.get_repository_build(job_details['namespace'], job_details['repository'], @@ -336,7 +349,7 @@ class DockerfileBuildWorker(Worker): repo)) log_appender(start_msg) - docker_resource = requests.get(resource_url) + docker_resource = requests.get(resource_url, stream=True) c_type = docker_resource.headers['content-type'] filetype_msg = ('Request to build type: %s with repo: %s and tags: %s' % From 35f69b9f5b4a9646505d4bb7aeadee41b8007978 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 1 Apr 2014 00:23:53 -0400 Subject: [PATCH 2/4] Add support for .tar.gz build packs in the build package viewer --- data/userfiles.py | 8 +- endpoints/common.py | 2 +- initdb.py | 6 + static/js/app.js | 143 +++++++++++++++++-- static/js/controllers.js | 144 +++++++++---------- static/lib/multifile.js | 214 +++++++++++++++++++++++++++++ static/lib/zlib.js | 51 +++++++ static/partials/build-package.html | 5 - templates/index.html | 2 + test/data/test.db | Bin 194560 -> 536576 bytes 10 files changed, 480 insertions(+), 95 deletions(-) create mode 100644 static/lib/multifile.js create mode 100644 static/lib/zlib.js diff --git a/data/userfiles.py b/data/userfiles.py index cc314a47f..c01e23568 100644 --- a/data/userfiles.py +++ b/data/userfiles.py @@ -55,11 +55,15 @@ class UserRequestFiles(object): return file_id - def get_file_url(self, file_id, expires_in=300): + def get_file_url(self, file_id, expires_in=300, mime_type=None): self._initialize_s3() full_key = os.path.join(self._prefix, file_id) k = Key(self._bucket, full_key) - return k.generate_url(expires_in) + headers = None + if mime_type: + headers={'Content-Type': mime_type} + + return k.generate_url(expires_in, headers=headers) def get_file_checksum(self, file_id): self._initialize_s3() diff --git a/endpoints/common.py b/endpoints/common.py index 4a8c03eb9..df233dfba 100644 --- a/endpoints/common.py +++ b/endpoints/common.py @@ -91,7 +91,7 @@ def random_string(): def render_page_template(name, **kwargs): resp = make_response(render_template(name, route_data=json.dumps(get_route_data()), - cache_buster=random_string(), **kwargs)) + cache_buster='foobarbaz', **kwargs)) resp.headers['X-FRAME-OPTIONS'] = 'DENY' return resp diff --git a/initdb.py b/initdb.py index a4b1709f0..0f6696b7d 100644 --- a/initdb.py +++ b/initdb.py @@ -349,6 +349,12 @@ def populate_database(): build.uuid = 'deadbeef-dead-beef-dead-beefdeadbeef' build.save() + build2 = model.create_repository_build(building, token, job_config, + '68daeebd-a5b9-457f-80a0-4363b882f8ea', + 'build-name', trigger) + build2.uuid = 'deadpork-dead-pork-dead-porkdeadpork' + build2.save() + org = model.create_organization('buynlarge', 'quay@devtable.com', new_user_1) org.stripe_id = TEST_STRIPE_ID diff --git a/static/js/app.js b/static/js/app.js index 87f73c724..302ffeaea 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -185,24 +185,147 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu return service; }]); - $provide.factory('UtilService', ['$sanitize', function($sanitize) { - var utilService = {}; - utilService.textToSafeHtml = function(text) { - var adjusted = text.replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); + $provide.factory('DataFileService', [function() { + var dataFileService = {}; - return $sanitize(adjusted); + dataFileService.getName_ = function(filePath) { + var parts = filePath.split('/'); + return parts[parts.length - 1]; }; + dataFileService.tryAsZip_ = function(buf, success, failure) { + var zip = null; + var zipFiles = null; + try { + var zip = new JSZip(buf); + zipFiles = zip.files; + } catch (e) { + failure(); + return; + } + + var files = []; + for (var filePath in zipFiles) { + if (zipFiles.hasOwnProperty(filePath)) { + files.push({ + 'name': dataFileService.getName_(filePath), + 'path': filePath, + 'canRead': true, + 'toBlob': (function(fp) { + return function() { + return new Blob([zip.file(fp).asArrayBuffer()]); + }; + }(filePath)) + }); + } + } + + success(files); + }; + + dataFileService.tryAsTarGz_ = function(buf, success, failure) { + var gunzip = new Zlib.Gunzip(buf); + var plain = gunzip.decompress(); + + var handler = new MultiFile(); + handler.files = []; + handler.processTarChunks(dataFileService.arrayToString(plain), 0); + if (!handler.files.length) { + failure(); + return; + } + + var files = []; + for (var i = 0; i < handler.files.length; ++i) { + var currentFile = handler.files[i]; + files.push({ + 'name': dataFileService.getName_(currentFile.filename), + 'path': currentFile.filename, + 'canRead': true, + 'toBlob': (function(currentFile) { + return function() { + return new Blob([currentFile.data], {type: 'application/octet-binary'}); + }; + }(currentFile)) + }); + } + success(files); + }; + + dataFileService.blobToString = function(blob, callback) { + var reader = new FileReader(); + reader.onload = function(event){ + callback(reader.result); + }; + reader.readAsText(blob); + }; + + dataFileService.arrayToString = function(buf) { + return String.fromCharCode.apply(null, new Uint16Array(buf)); + }; + + dataFileService.readDataArrayAsPossibleArchive = function(buf, success, failure) { + dataFileService.tryAsZip_(buf, success, function() { + dataFileService.tryAsTarGz_(buf, success, failure); + }); + }; + + dataFileService.downloadDataFileAsArrayBuffer = function($scope, url, progress, error, loaded) { + var request = new XMLHttpRequest(); + request.open('GET', url, true); + request.responseType = 'arraybuffer'; + + request.onprogress = function(e) { + $scope.$apply(function() { + var percentLoaded; + if (e.lengthComputable) { + progress(e.loaded / e.total); + } + }); + }; + + request.onerror = function() { + $scope.$apply(function() { + error(); + }); + }; + + request.onload = function() { + if (this.status == 200) { + $scope.$apply(function() { + var uint8array = new Uint8Array(request.response); + loaded(uint8array); + }); + return; + } + }; + + request.send(); + }; + + return dataFileService; + }]); + + + $provide.factory('UtilService', ['$sanitize', function($sanitize) { + var utilService = {}; + + utilService.textToSafeHtml = function(text) { + var adjusted = text.replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + + return $sanitize(adjusted); + }; + return utilService; }]); - $provide.factory('TriggerDescriptionBuilder', ['UtilService', '$sanitize', function(UtilService, $sanitize) { + $provide.factory('TriggerDescriptionBuilder', ['UtilService', '$sanitize', function(UtilService, $sanitize) { var builderService = {}; builderService.getDescription = function(name, config) { diff --git a/static/js/controllers.js b/static/js/controllers.js index b74071928..c2249773c 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -778,7 +778,7 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi loadViewInfo(); } -function BuildPackageCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $location, $timeout) { +function BuildPackageCtrl($scope, Restangular, ApiService, DataFileService, $routeParams, $rootScope, $location, $timeout) { var namespace = $routeParams.namespace; var name = $routeParams.name; var buildid = $routeParams.buildid; @@ -800,100 +800,90 @@ function BuildPackageCtrl($scope, Restangular, ApiService, $routeParams, $rootSc }, 10); }; - $scope.downloadForUser = function() { - var blob = $scope.zip.generate({type:"blob"}); - saveAs(blob, $scope.repobuild['display_name'] + '.zip'); + var determineDockerfilePath = function() { + var dockerfilePath = 'Dockerfile'; + if ($scope.repobuild['job_config']) { + var dockerfileFolder = ($scope.repobuild['job_config']['build_subdir'] || ''); + if (dockerfileFolder[0] == '/') { + dockerfileFolder = dockerfileFolder.substr(1); + } + if (dockerfileFolder && dockerfileFolder[dockerfileFolder.length - 1] != '/') { + dockerfileFolder += '/'; + } + dockerfilePath = dockerfileFolder + 'Dockerfile'; + } + return dockerfilePath; }; - var processBuildPack = function(response) { - // Try to load as a zip file. - var zipFiles = null; - var zip = null; - try { - var zip = new JSZip(response); - zipFiles = zip.files; - } catch (e) { - } - - // Find the Dockerfile in the zip file. If there isn't any zip file, then the response - // itself (should) be the Dockerfile. - if (zipFiles && Object.keys(zipFiles).length) { - // Load the dockerfile contents. - var dockerfilePath = 'Dockerfile'; - if ($scope.repobuild['job_config']) { - var dockerfileFolder = ($scope.repobuild['job_config']['build_subdir'] || ''); - if (dockerfileFolder[0] == '/') { - dockerfileFolder = dockerfileFolder.substr(1); - } - if (dockerfileFolder && dockerfileFolder[dockerfileFolder.length - 1] != '/') { - dockerfileFolder += '/'; - } - - dockerfilePath = dockerfileFolder + 'Dockerfile'; - } + var processBuildPack = function(uint8array) { + var archiveread = function(files) { + var getpath = function(file) { + return file.path; + }; - var dockerfile = zip.file(dockerfilePath); - if (dockerfile) { - $scope.dockerFileContents = dockerfile.asText(); - $scope.dockerFilePath = dockerfilePath; - } + var findFile = function(path) { + for (var i = 0; i < files.length; ++i) { + var file = files[i]; + if (file.path == path) { + return file; + } + } + return null; + }; - // Build the zip file tree. - $scope.zip = zip; - $scope.tree = new FileTree(Object.keys(zipFiles)); + $scope.tree = new FileTree($.map(files, getpath)); $($scope.tree).bind('fileClicked', function(e) { - var file = zip.file(e.path); - if (file) { - var blob = new Blob([file.asArrayBuffer()]); - saveAs(blob, file.name); + var file = findFile(e.path); + if (file && file.canRead) { + saveAs(file.toBlob(), file.name); } - }); - } else { - $scope.dockerFileContents = response; - $scope.dockerFilePath = 'Dockerfile'; - } + }); - $scope.loaded = true; + var dockerfilePath = determineDockerfilePath(); + var dockerfile = findFile(dockerfilePath); + if (dockerfile && dockerfile.canRead) { + DataFileService.blobToString(dockerfile.toBlob(), function(result) { + $scope.$apply(function() { + $scope.dockerFilePath = dockerfilePath; + $scope.dockerFileContents = result; + }); + }); + } + + $scope.loaded = true; + }; + + var notarchive = function() { + $scope.dockerFileContents = DataFileService.arrayToString(uint8array); + $scope.loaded = true; + }; + + DataFileService.readDataArrayAsPossibleArchive(uint8array, archiveread, notarchive); }; - + var downloadBuildPack = function(url) { $scope.downloadProgress = 0; $scope.downloading = true; - startDownload(url); }; var startDownload = function(url) { - var request = new XMLHttpRequest(); - request.open('GET', url, true); - if (request.overrideMimeType) { - request.overrideMimeType('text/plain; charset=x-user-defined'); - } - request.onprogress = function(e) { - $scope.$apply(function() { - var percentLoaded; - if (e.lengthComputable) { - $scope.downloadProgress = (e.loaded / e.total) * 100; - } - }); + var onprogress = function(p) { + $scope.downloadProgress = p * 100; }; - request.onerror = function() { - $scope.$apply(function() { - $scope.downloading = false; - $scope.downloadError = true; - }); + + var onerror = function() { + $scope.downloading = false; + $scope.downloadError = true; }; - request.onreadystatechange = function() { - var state = request.readyState; - if (state == 4) { - $scope.$apply(function() { - $scope.downloading = false; - processBuildPack(request.responseText); - }); - return; - } + + var onloaded = function(uint8array) { + $scope.downloading = false; + processBuildPack(uint8array); }; - request.send(); + + DataFileService.downloadDataFileAsArrayBuffer($scope, url, + onprogress, onerror, onloaded); }; var getBuildInfo = function() { diff --git a/static/lib/multifile.js b/static/lib/multifile.js new file mode 100644 index 000000000..02d1e006b --- /dev/null +++ b/static/lib/multifile.js @@ -0,0 +1,214 @@ +/* MultiFile - A JavaScript library to load multiple files from + tar archives and json_packed files (see http://gist.github.com/407595) + + Example: Loading multiple images from a tarball. + + MultiFile.load('images.tar', function(xhr) { + this.files.forEach(function(f) { + var e = document.createElement('div'); + document.body.appendChild(e); + var p = document.createElement('p'); + p.appendChild(document.createTextNode(f.filename + " (" + f.length + " bytes)")); + e.appendChild(p); + var img = new Image(); + img.src = f.toDataURL(); + e.appendChild(img); + }); + }); + + Example 2: Streaming images from a tarball. + + MultiFile.stream('images.tar', function(f) { + var e = document.createElement('div'); + document.body.appendChild(e); + var p = document.createElement('p'); + p.appendChild(document.createTextNode(f.filename + " (" + f.length + " bytes)")); + e.appendChild(p); + var img = new Image(); + img.src = f.toDataURL(); + e.appendChild(img); + }); + + +Copyright (c) 2010 Ilmari Heikkinen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +MultiFile = function(){}; + +// Load and parse archive, calls onload after loading all files. +MultiFile.load = function(url, onload) { + var o = new MultiFile(); + o.onload = onload; + o.load(url); + return o; +} + +// Streams an archive from the given url, calling onstream after loading each file in archive. +// Calls onload after loading all files. +MultiFile.stream = function(url, onstream, onload) { + var o = new MultiFile(); + o.onload = onload; + o.onstream = onstream; + o.load(url); + return o; +} +MultiFile.prototype = { + onerror : null, + onload : null, + onstream : null, + + load : function(url) { + var xhr = new XMLHttpRequest(); + var self = this; + var offset = 0; + this.files = []; + var isTar = (/\.tar(\?.*)?$/i).test(url); + xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + if (xhr.status == 200 || xhr.status == 0) { + if (isTar) + offset = self.processTarChunks(xhr.responseText, offset); + else + self.processJSONChunks(xhr.responseText); + if (self.onload) + self.onload(xhr); + } else { + if (self.onerror) + self.onerror(xhr); + } + } else if (xhr.readyState == 3) { + if (xhr.status == 200 || xhr.status == 0) { + if (isTar) + offset = self.processTarChunks(xhr.responseText, offset); + else + self.processJSONChunks(xhr.responseText); + } + } + }; + xhr.open("GET", url, true); + xhr.overrideMimeType("text/plain; charset=x-user-defined"); + xhr.setRequestHeader("Content-Type", "text/plain"); + xhr.send(null); + }, + + onerror : function(xhr) { + alert("Error: "+xhr.status); + }, + + parseJSON : function(text) { + this.processJSONChunks(text); + }, + processJSONChunks : function(text) { + if (this.files.length == 0) { // processing headers + var idx = text.indexOf('\n'); + if (idx >= 0) { // got header + this.files = JSON.parse(text.substring(0,idx)); + this.files.forEach(function(f) { f.offset += idx + 1; }) + } + } + if (this.files.length > 0) { // processing data + var f = null; + var idx=0; + for (idx=0; idx= offset + 512) { + var header = this.files.length == 0 ? null : this.files[this.files.length-1]; + if (header && header.data == null) { + if (offset + header.length <= responseText.length) { + header.data = responseText.substring(offset, offset+header.length); + header.toDataURL = this.__toDataURL; + offset += 512 * Math.ceil(header.length / 512); + if (this.onstream) + this.onstream(header); + } else { // not loaded yet + break; + } + } else { + var header = this.parseTarHeader(responseText, offset); + if (header.length > 0 || header.filename != '') { + this.files.push(header); + offset += 512; + header.offset = offset; + } else { // empty header, stop processing + offset = responseText.length; + } + } + } + return offset; + }, + parseTarHeader : function(text, offset) { + var i = offset || 0; + var h = {}; + h.filename = text.substring(i, i+=100).split("\0", 1)[0]; + h.mode = text.substring(i, i+=8).split("\0", 1)[0]; + h.uid = text.substring(i, i+=8).split("\0", 1)[0]; + h.gid = text.substring(i, i+=8).split("\0", 1)[0]; + h.length = this.parseTarNumber(text.substring(i, i+=12)); + h.lastModified = text.substring(i, i+=12).split("\0", 1)[0]; + h.checkSum = text.substring(i, i+=8).split("\0", 1)[0]; + h.fileType = text.substring(i, i+=1).split("\0", 1)[0]; + h.linkName = text.substring(i, i+=100).split("\0", 1)[0]; + return h; + }, + + parseTarNumber : function(text) { + return parseInt('0'+text.replace(/[^\d]/g, ''), 8); + }, + + __toDataURL : function() { + if (this.data.substring(0,40).match(/^data:[^\/]+\/[^,]+,/)) { + return this.data; + } else if (MultiFile.prototype.cleanHighByte(this.data.substring(0,10)).match(/\377\330\377\340..JFIF/)) { + return 'data:image/jpeg;base64,'+btoa(MultiFile.prototype.cleanHighByte(this.data)); + } else if (MultiFile.prototype.cleanHighByte(this.data.substring(0,6)) == "\211PNG\r\n") { + return 'data:image/png;base64,'+btoa(MultiFile.prototype.cleanHighByte(this.data)); + } else if (MultiFile.prototype.cleanHighByte(this.data.substring(0,6)).match(/GIF8[79]a/)) { + return 'data:image/gif;base64,'+btoa(MultiFile.prototype.cleanHighByte(this.data)); + } else { + throw("toDataURL: I don't know how to handle " + this.filename); + } + } +} + diff --git a/static/lib/zlib.js b/static/lib/zlib.js new file mode 100644 index 000000000..7fe6bdbcd --- /dev/null +++ b/static/lib/zlib.js @@ -0,0 +1,51 @@ +/** @license zlib.js 2012 - imaya [ https://github.com/imaya/zlib.js ] The MIT License */(function() {'use strict';function q(b){throw b;}var t=void 0,u=!0,aa=this;function A(b,a){var c=b.split("."),d=aa;!(c[0]in d)&&d.execScript&&d.execScript("var "+c[0]);for(var e;c.length&&(e=c.shift());)!c.length&&a!==t?d[e]=a:d=d[e]?d[e]:d[e]={}};var B="undefined"!==typeof Uint8Array&&"undefined"!==typeof Uint16Array&&"undefined"!==typeof Uint32Array&&"undefined"!==typeof DataView;function F(b,a){this.index="number"===typeof a?a:0;this.m=0;this.buffer=b instanceof(B?Uint8Array:Array)?b:new (B?Uint8Array:Array)(32768);2*this.buffer.length<=this.index&&q(Error("invalid index"));this.buffer.length<=this.index&&this.f()}F.prototype.f=function(){var b=this.buffer,a,c=b.length,d=new (B?Uint8Array:Array)(c<<1);if(B)d.set(b);else for(a=0;a>>8&255]<<16|H[b>>>16&255]<<8|H[b>>>24&255])>>32-a:H[b]>>8-a);if(8>a+f)g=g<>a-k-1&1,8===++f&&(f=0,d[e++]=H[g],g=0,e===d.length&&(d=this.f()));d[e]=g;this.buffer=d;this.m=f;this.index=e};F.prototype.finish=function(){var b=this.buffer,a=this.index,c;0ca;++ca){for(var K=ca,da=K,ea=7,K=K>>>1;K;K>>>=1)da<<=1,da|=K&1,--ea;ba[ca]=(da<>>0}var H=ba;function ja(b,a,c){var d,e="number"===typeof a?a:a=0,f="number"===typeof c?c:b.length;d=-1;for(e=f&7;e--;++a)d=d>>>8^O[(d^b[a])&255];for(e=f>>3;e--;a+=8)d=d>>>8^O[(d^b[a])&255],d=d>>>8^O[(d^b[a+1])&255],d=d>>>8^O[(d^b[a+2])&255],d=d>>>8^O[(d^b[a+3])&255],d=d>>>8^O[(d^b[a+4])&255],d=d>>>8^O[(d^b[a+5])&255],d=d>>>8^O[(d^b[a+6])&255],d=d>>>8^O[(d^b[a+7])&255];return(d^4294967295)>>>0} +var ka=[0,1996959894,3993919788,2567524794,124634137,1886057615,3915621685,2657392035,249268274,2044508324,3772115230,2547177864,162941995,2125561021,3887607047,2428444049,498536548,1789927666,4089016648,2227061214,450548861,1843258603,4107580753,2211677639,325883990,1684777152,4251122042,2321926636,335633487,1661365465,4195302755,2366115317,997073096,1281953886,3579855332,2724688242,1006888145,1258607687,3524101629,2768942443,901097722,1119000684,3686517206,2898065728,853044451,1172266101,3705015759, +2882616665,651767980,1373503546,3369554304,3218104598,565507253,1454621731,3485111705,3099436303,671266974,1594198024,3322730930,2970347812,795835527,1483230225,3244367275,3060149565,1994146192,31158534,2563907772,4023717930,1907459465,112637215,2680153253,3904427059,2013776290,251722036,2517215374,3775830040,2137656763,141376813,2439277719,3865271297,1802195444,476864866,2238001368,4066508878,1812370925,453092731,2181625025,4111451223,1706088902,314042704,2344532202,4240017532,1658658271,366619977, +2362670323,4224994405,1303535960,984961486,2747007092,3569037538,1256170817,1037604311,2765210733,3554079995,1131014506,879679996,2909243462,3663771856,1141124467,855842277,2852801631,3708648649,1342533948,654459306,3188396048,3373015174,1466479909,544179635,3110523913,3462522015,1591671054,702138776,2966460450,3352799412,1504918807,783551873,3082640443,3233442989,3988292384,2596254646,62317068,1957810842,3939845945,2647816111,81470997,1943803523,3814918930,2489596804,225274430,2053790376,3826175755, +2466906013,167816743,2097651377,4027552580,2265490386,503444072,1762050814,4150417245,2154129355,426522225,1852507879,4275313526,2312317920,282753626,1742555852,4189708143,2394877945,397917763,1622183637,3604390888,2714866558,953729732,1340076626,3518719985,2797360999,1068828381,1219638859,3624741850,2936675148,906185462,1090812512,3747672003,2825379669,829329135,1181335161,3412177804,3160834842,628085408,1382605366,3423369109,3138078467,570562233,1426400815,3317316542,2998733608,733239954,1555261956, +3268935591,3050360625,752459403,1541320221,2607071920,3965973030,1969922972,40735498,2617837225,3943577151,1913087877,83908371,2512341634,3803740692,2075208622,213261112,2463272603,3855990285,2094854071,198958881,2262029012,4057260610,1759359992,534414190,2176718541,4139329115,1873836001,414664567,2282248934,4279200368,1711684554,285281116,2405801727,4167216745,1634467795,376229701,2685067896,3608007406,1308918612,956543938,2808555105,3495958263,1231636301,1047427035,2932959818,3654703836,1088359270, +936918E3,2847714899,3736837829,1202900863,817233897,3183342108,3401237130,1404277552,615818150,3134207493,3453421203,1423857449,601450431,3009837614,3294710456,1567103746,711928724,3020668471,3272380065,1510334235,755167117],O=B?new Uint32Array(ka):ka;function P(){}P.prototype.getName=function(){return this.name};P.prototype.getData=function(){return this.data};P.prototype.Y=function(){return this.Z};A("Zlib.GunzipMember",P);A("Zlib.GunzipMember.prototype.getName",P.prototype.getName);A("Zlib.GunzipMember.prototype.getData",P.prototype.getData);A("Zlib.GunzipMember.prototype.getMtime",P.prototype.Y);function la(b){this.buffer=new (B?Uint16Array:Array)(2*b);this.length=0}la.prototype.getParent=function(b){return 2*((b-2)/4|0)};la.prototype.push=function(b,a){var c,d,e=this.buffer,f;c=this.length;e[this.length++]=a;for(e[this.length++]=b;0e[d])f=e[c],e[c]=e[d],e[d]=f,f=e[c+1],e[c+1]=e[d+1],e[d+1]=f,c=d;else break;return this.length}; +la.prototype.pop=function(){var b,a,c=this.buffer,d,e,f;a=c[0];b=c[1];this.length-=2;c[0]=c[this.length];c[1]=c[this.length+1];for(f=0;;){e=2*f+2;if(e>=this.length)break;e+2c[e]&&(e+=2);if(c[e]>c[f])d=c[f],c[f]=c[e],c[e]=d,d=c[f+1],c[f+1]=c[e+1],c[e+1]=d;else break;f=e}return{index:b,value:a,length:this.length}};function ma(b){var a=b.length,c=0,d=Number.POSITIVE_INFINITY,e,f,g,k,h,l,s,p,m,n;for(p=0;pc&&(c=b[p]),b[p]>=1;n=g<<16|p;for(m=l;mS;S++)switch(u){case 143>=S:sa.push([S+48,8]);break;case 255>=S:sa.push([S-144+400,9]);break;case 279>=S:sa.push([S-256+0,7]);break;case 287>=S:sa.push([S-280+192,8]);break;default:q("invalid literal: "+S)} +na.prototype.g=function(){var b,a,c,d,e=this.input;switch(this.k){case 0:c=0;for(d=e.length;c>>8&255;m[n++]=l&255;m[n++]=l>>>8&255;if(B)m.set(f,n),n+=f.length,m=m.subarray(0,n);else{s=0;for(p=f.length;sz)for(;0< +z--;)I[G++]=0,M[0]++;else for(;0z?z:138,D>z-3&&D=D?(I[G++]=17,I[G++]=D-3,M[17]++):(I[G++]=18,I[G++]=D-11,M[18]++),z-=D;else if(I[G++]=J[w],M[J[w]]++,z--,3>z)for(;0z?z:6,D>z-3&&DC;C++)wa[C]=oa[pb[C]];for(Z=19;4=a:return[265,a-11,1];case 14>=a:return[266,a-13,1];case 16>=a:return[267,a-15,1];case 18>=a:return[268,a-17,1];case 22>=a:return[269,a-19,2];case 26>=a:return[270,a-23,2];case 30>=a:return[271,a-27,2];case 34>=a:return[272, +a-31,2];case 42>=a:return[273,a-35,3];case 50>=a:return[274,a-43,3];case 58>=a:return[275,a-51,3];case 66>=a:return[276,a-59,3];case 82>=a:return[277,a-67,4];case 98>=a:return[278,a-83,4];case 114>=a:return[279,a-99,4];case 130>=a:return[280,a-115,4];case 162>=a:return[281,a-131,5];case 194>=a:return[282,a-163,5];case 226>=a:return[283,a-195,5];case 257>=a:return[284,a-227,5];case 258===a:return[285,a-258,0];default:q("invalid length: "+a)}}var a=[],c,d;for(c=3;258>=c;c++)d=b(c),a[c]=d[2]<<24|d[1]<< +16|d[0];return a}(),Aa=B?new Uint32Array(ya):ya; +function ta(b,a){function c(a,c){var b=a.Q,d=[],e=0,f;f=Aa[a.length];d[e++]=f&65535;d[e++]=f>>16&255;d[e++]=f>>24;var g;switch(u){case 1===b:g=[0,b-1,0];break;case 2===b:g=[1,b-2,0];break;case 3===b:g=[2,b-3,0];break;case 4===b:g=[3,b-4,0];break;case 6>=b:g=[4,b-5,1];break;case 8>=b:g=[5,b-7,1];break;case 12>=b:g=[6,b-9,2];break;case 16>=b:g=[7,b-13,2];break;case 24>=b:g=[8,b-17,3];break;case 32>=b:g=[9,b-25,3];break;case 48>=b:g=[10,b-33,4];break;case 64>=b:g=[11,b-49,4];break;case 96>=b:g=[12,b- +65,5];break;case 128>=b:g=[13,b-97,5];break;case 192>=b:g=[14,b-129,6];break;case 256>=b:g=[15,b-193,6];break;case 384>=b:g=[16,b-257,7];break;case 512>=b:g=[17,b-385,7];break;case 768>=b:g=[18,b-513,8];break;case 1024>=b:g=[19,b-769,8];break;case 1536>=b:g=[20,b-1025,9];break;case 2048>=b:g=[21,b-1537,9];break;case 3072>=b:g=[22,b-2049,10];break;case 4096>=b:g=[23,b-3073,10];break;case 6144>=b:g=[24,b-4097,11];break;case 8192>=b:g=[25,b-6145,11];break;case 12288>=b:g=[26,b-8193,12];break;case 16384>= +b:g=[27,b-12289,12];break;case 24576>=b:g=[28,b-16385,13];break;case 32768>=b:g=[29,b-24577,13];break;default:q("invalid distance")}f=g;d[e++]=f[0];d[e++]=f[1];d[e++]=f[2];var h,k;h=0;for(k=d.length;h=f;)v[f++]=0;for(f=0;29>=f;)x[f++]=0}v[256]=1;d=0;for(e=a.length;d=e){p&&c(p,-1);f=0;for(g=e-d;fg&&a+gf&&(e=d,f=g);if(258===g)break}return new xa(f,a-e)} +function ua(b,a){var c=b.length,d=new la(572),e=new (B?Uint8Array:Array)(c),f,g,k,h,l;if(!B)for(h=0;h2*e[n-1]+f[n]&&(e[n]=2*e[n-1]+f[n]),k[n]=Array(e[n]),h[n]=Array(e[n]);for(m=0;mb[m]?(k[n][r]=v,h[n][r]=a,x+=2):(k[n][r]=b[m],h[n][r]=m,++m);l[n]=0;1===f[n]&&d(n)}return g} +function va(b){var a=new (B?Uint16Array:Array)(b.length),c=[],d=[],e=0,f,g,k,h;f=0;for(g=b.length;f>>=1}return a};function Da(b,a){this.input=b;this.b=this.c=0;this.i={};a&&(a.flags&&(this.i=a.flags),"string"===typeof a.filename&&(this.filename=a.filename),"string"===typeof a.comment&&(this.A=a.comment),a.deflateOptions&&(this.l=a.deflateOptions));this.l||(this.l={})} +Da.prototype.g=function(){var b,a,c,d,e,f,g,k,h=new (B?Uint8Array:Array)(32768),l=0,s=this.input,p=this.c,m=this.filename,n=this.A;h[l++]=31;h[l++]=139;h[l++]=8;b=0;this.i.fname&&(b|=Ea);this.i.fcomment&&(b|=Fa);this.i.fhcrc&&(b|=Ga);h[l++]=b;a=(Date.now?Date.now():+new Date)/1E3|0;h[l++]=a&255;h[l++]=a>>>8&255;h[l++]=a>>>16&255;h[l++]=a>>>24&255;h[l++]=0;h[l++]=Ha;if(this.i.fname!==t){g=0;for(k=m.length;g>>8&255),h[l++]=f&255;h[l++]=0}if(this.i.comment){g= +0;for(k=n.length;g>>8&255),h[l++]=f&255;h[l++]=0}this.i.fhcrc&&(c=ja(h,0,l)&65535,h[l++]=c&255,h[l++]=c>>>8&255);this.l.outputBuffer=h;this.l.outputIndex=l;e=new na(s,this.l);h=e.g();l=e.b;B&&(l+8>h.buffer.byteLength?(this.a=new Uint8Array(l+8),this.a.set(new Uint8Array(h.buffer)),h=this.a):h=new Uint8Array(h.buffer));d=ja(s,t,t);h[l++]=d&255;h[l++]=d>>>8&255;h[l++]=d>>>16&255;h[l++]=d>>>24&255;k=s.length;h[l++]=k&255;h[l++]=k>>>8&255;h[l++]=k>>>16&255;h[l++]= +k>>>24&255;this.c=p;B&&l>>=1;switch(b){case 0:var a=this.input,c=this.c,d=this.a,e=this.b,f=a.length,g=t,k=t,h=d.length,l=t;this.e=this.j=0;c+1>=f&&q(Error("invalid uncompressed block header: LEN"));g=a[c++]|a[c++]<<8;c+1>=f&&q(Error("invalid uncompressed block header: NLEN"));k=a[c++]|a[c++]<<8;g===~k&&q(Error("invalid uncompressed block header: length verify"));c+g>a.length&&q(Error("input buffer is broken"));switch(this.r){case Ja:for(;e+g>d.length;){l= +h-e;g-=l;if(B)d.set(a.subarray(c,c+l),e),e+=l,c+=l;else for(;l--;)d[e++]=a[c++];this.b=e;d=this.f();e=this.b}break;case Ia:for(;e+g>d.length;)d=this.f({F:2});break;default:q(Error("invalid inflate mode"))}if(B)d.set(a.subarray(c,c+g),e),e+=g,c+=g;else for(;g--;)d[e++]=a[c++];this.c=c;this.b=e;this.a=d;break;case 1:this.s(Za,$a);break;case 2:ab(this);break;default:q(Error("unknown BTYPE: "+b))}}return this.B()}; +var bb=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],cb=B?new Uint16Array(bb):bb,db=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,258,258],eb=B?new Uint16Array(db):db,fb=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0],gb=B?new Uint8Array(fb):fb,hb=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577],ib=B?new Uint16Array(hb):hb,jb=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10, +10,11,11,12,12,13,13],kb=B?new Uint8Array(jb):jb,lb=new (B?Uint8Array:Array)(288),V,mb;V=0;for(mb=lb.length;V=V?8:255>=V?9:279>=V?7:8;var Za=ma(lb),nb=new (B?Uint8Array:Array)(30),ob,qb;ob=0;for(qb=nb.length;ob=g&&q(Error("input buffer is broken")),c|=e[f++]<>>a;b.e=d-a;b.c=f;return k} +function rb(b,a){for(var c=b.j,d=b.e,e=b.input,f=b.c,g=e.length,k=a[0],h=a[1],l,s;d=g);)c|=e[f++]<>>16;b.j=c>>s;b.e=d-s;b.c=f;return l&65535} +function ab(b){function a(a,b,c){var d,e=this.J,f,g;for(g=0;gf)d>=e&&(this.b=d,c=this.f(),d=this.b),c[d++]=f;else{g=f-257;h=eb[g];0=e&&(this.b=d,c=this.f(),d=this.b);for(;h--;)c[d]=c[d++-k]}for(;8<=this.e;)this.e-=8,this.c--;this.b=d}; +T.prototype.T=function(b,a){var c=this.a,d=this.b;this.C=b;for(var e=c.length,f,g,k,h;256!==(f=rb(this,b));)if(256>f)d>=e&&(c=this.f(),e=c.length),c[d++]=f;else{g=f-257;h=eb[g];0e&&(c=this.f(),e=c.length);for(;h--;)c[d]=c[d++-k]}for(;8<=this.e;)this.e-=8,this.c--;this.b=d}; +T.prototype.f=function(){var b=new (B?Uint8Array:Array)(this.b-32768),a=this.b-32768,c,d,e=this.a;if(B)b.set(e.subarray(32768,b.length));else{c=0;for(d=b.length;cc;++c)e[c]=e[a+c];this.b=32768;return e}; +T.prototype.U=function(b){var a,c=this.input.length/this.c+1|0,d,e,f,g=this.input,k=this.a;b&&("number"===typeof b.F&&(c=b.F),"number"===typeof b.P&&(c+=b.P));2>c?(d=(g.length-this.c)/this.C[2],f=258*(d/2)|0,e=fa&&(this.a.length=a),b=this.a);return this.buffer=b};function sb(b){this.input=b;this.c=0;this.t=[];this.D=!1}sb.prototype.X=function(){this.D||this.h();return this.t.slice()}; +sb.prototype.h=function(){for(var b=this.input.length;this.c>>0;ja(e,t,t)!==s&&q(Error("invalid CRC-32 checksum: 0x"+ja(e,t,t).toString(16)+ +" / 0x"+s.toString(16)));a.da=c=(p[m++]|p[m++]<<8|p[m++]<<16|p[m++]<<24)>>>0;(e.length&4294967295)!==c&&q(Error("invalid input size: "+(e.length&4294967295)+" / "+c));this.t.push(a);this.c=m}this.D=u;var n=this.t,r,v,x=0,Q=0,y;r=0;for(v=n.length;r>>0;b=a}for(var e=1,f=0,g=b.length,k,h=0;0>>0};function ub(b,a){var c,d;this.input=b;this.c=0;if(a||!(a={}))a.index&&(this.c=a.index),a.verify&&(this.$=a.verify);c=b[this.c++];d=b[this.c++];switch(c&15){case vb:this.method=vb;break;default:q(Error("unsupported compression method"))}0!==((c<<8)+d)%31&&q(Error("invalid fcheck flag:"+((c<<8)+d)%31));d&32&&q(Error("fdict flag is not supported"));this.L=new T(b,{index:this.c,bufferSize:a.bufferSize,bufferType:a.bufferType,resize:a.resize})} +ub.prototype.h=function(){var b=this.input,a,c;a=this.L.h();this.c=this.L.c;this.$&&(c=(b[this.c++]<<24|b[this.c++]<<16|b[this.c++]<<8|b[this.c++])>>>0,c!==tb(a)&&q(Error("invalid adler-32 checksum")));return a};var vb=8;function wb(b,a){this.input=b;this.a=new (B?Uint8Array:Array)(32768);this.k=W.o;var c={},d;if((a||!(a={}))&&"number"===typeof a.compressionType)this.k=a.compressionType;for(d in a)c[d]=a[d];c.outputBuffer=this.a;this.K=new na(this.input,c)}var W=ra; +wb.prototype.g=function(){var b,a,c,d,e,f,g,k=0;g=this.a;b=vb;switch(b){case vb:a=Math.LOG2E*Math.log(32768)-8;break;default:q(Error("invalid compression method"))}c=a<<4|b;g[k++]=c;switch(b){case vb:switch(this.k){case W.NONE:e=0;break;case W.v:e=1;break;case W.o:e=2;break;default:q(Error("unsupported compression type"))}break;default:q(Error("invalid compression method"))}d=e<<6|0;g[k++]=d|31-(256*c+d)%31;f=tb(this.input);this.K.b=k;g=this.K.g();k=g.length;B&&(g=new Uint8Array(g.buffer),g.length<= +k+4&&(this.a=new Uint8Array(g.length+4),this.a.set(g),g=this.a),g=g.subarray(0,k+4));g[k++]=f>>24&255;g[k++]=f>>16&255;g[k++]=f>>8&255;g[k++]=f&255;return g};function xb(b,a){var c,d,e,f;if(Object.keys)c=Object.keys(a);else for(d in c=[],e=0,a)c[e++]=d;e=0;for(f=c.length;e -
- - - -
diff --git a/templates/index.html b/templates/index.html index 8fe543e64..83b87457f 100644 --- a/templates/index.html +++ b/templates/index.html @@ -34,6 +34,8 @@ + + {% endblock %} diff --git a/test/data/test.db b/test/data/test.db index a69b3b9fbd4e166d21996bd23e461ea3433d1c61..38f50c490f72f993dcc9c52e7ebfbbd5e6fd9cfa 100644 GIT binary patch delta 17190 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 Date: Tue, 1 Apr 2014 13:00:26 -0400 Subject: [PATCH 3/4] Make sure the TAR import system handles TAR paths with local directory references --- auth/auth_context.py | 3 ++- initdb.py | 6 ++++++ static/js/app.js | 21 +++++++++++++++++++-- test/data/test.db | Bin 536576 -> 536576 bytes 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/auth/auth_context.py b/auth/auth_context.py index 2aad14685..b97ffa02d 100644 --- a/auth/auth_context.py +++ b/auth/auth_context.py @@ -20,7 +20,8 @@ def get_authenticated_user(): set_authenticated_user(loaded) user = loaded - logger.debug('Returning authenticated user: %s', user.username) + if user: + logger.debug('Returning authenticated user: %s', user.username) return user diff --git a/initdb.py b/initdb.py index 0f6696b7d..fdb3f8e01 100644 --- a/initdb.py +++ b/initdb.py @@ -355,6 +355,12 @@ def populate_database(): build2.uuid = 'deadpork-dead-pork-dead-porkdeadpork' build2.save() + build3 = model.create_repository_build(building, token, job_config, + 'f49d07f9-93da-474d-ad5f-c852107c3892', + 'build-name', trigger) + build3.uuid = 'deadduck-dead-duck-dead-duckdeadduck' + build3.save() + org = model.create_organization('buynlarge', 'quay@devtable.com', new_user_1) org.stripe_id = TEST_STRIPE_ID diff --git a/static/js/app.js b/static/js/app.js index 302ffeaea..57a012a44 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -225,6 +225,19 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu }; dataFileService.tryAsTarGz_ = function(buf, success, failure) { + var collapsePath = function(originalPath) { + // Tar files can contain entries of the form './', so we need to collapse + // those paths down. + var parts = originalPath.split('/'); + for (var i = parts.length - 1; i >= 0; i--) { + var part = parts[i]; + if (part == '.') { + parts.splice(i, 1); + } + } + return parts.join('/'); + }; + var gunzip = new Zlib.Gunzip(buf); var plain = gunzip.decompress(); @@ -239,9 +252,13 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu var files = []; for (var i = 0; i < handler.files.length; ++i) { var currentFile = handler.files[i]; + var path = collapsePath(currentFile.filename); + + if (path == '') { continue; } + files.push({ - 'name': dataFileService.getName_(currentFile.filename), - 'path': currentFile.filename, + 'name': dataFileService.getName_(path), + 'path': path, 'canRead': true, 'toBlob': (function(currentFile) { return function() { diff --git a/test/data/test.db b/test/data/test.db index 38f50c490f72f993dcc9c52e7ebfbbd5e6fd9cfa..af529991cb856f0d98193c9fe3ed4a0bbf456a3a 100644 GIT binary patch delta 5482 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 From 7c44932c87929aa7d80c9e4a1ca472b9f7bb6f5f Mon Sep 17 00:00:00 2001 From: jakedt Date: Tue, 1 Apr 2014 13:46:41 -0400 Subject: [PATCH 4/4] Use safer tar extraction. Handle error messages in the build process more intelligently. --- util/safetar.py | 52 ++++++++++++++++++++++++++++++++++++++ workers/dockerfilebuild.py | 13 +++++++--- 2 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 util/safetar.py diff --git a/util/safetar.py b/util/safetar.py new file mode 100644 index 000000000..ce9119054 --- /dev/null +++ b/util/safetar.py @@ -0,0 +1,52 @@ +import os.path +import logging +import copy +import operator + + +logger = logging.getLogger(__name__) + + +def safe_extractall(tar, path): + """Extract all members from the archive to the current working + directory and set owner, modification time and permissions on + directories afterwards. `path' specifies a different directory + to extract to. `members' is optional and must be a subset of the + list returned by getmembers(). + """ + abs_extract_path = os.path.abspath(path) + def is_member_safe(member): + """ Returns True if the member is safe, False otherwise. """ + if member.issym(): + link_tar_path = os.path.dirname(member.path) + abs_link_path = os.path.abspath(os.path.join(path, link_tar_path, member.linkpath)) + if not abs_link_path.startswith(abs_extract_path): + logger.warning('Filtering symlink outside of extract dir: %s', member.linkpath) + return False + elif member.isblk(): + logger.warning('Filtering block device from tarfile: %s', member.path) + return False + + return True + + directories = [] + + for tarinfo in tar: + if is_member_safe(tarinfo): + if tarinfo.isdir(): + # Extract directories with a safe mode. + directories.append(tarinfo) + tarinfo = copy.copy(tarinfo) + tarinfo.mode = 0700 + tar.extract(tarinfo, path) + + # Reverse sort directories. + directories.sort(key=operator.attrgetter('name')) + directories.reverse() + + # Set correct owner, mtime and filemode on directories. + for tarinfo in directories: + dirpath = os.path.join(path, tarinfo.name) + tar.chown(tarinfo, dirpath) + tar.utime(tarinfo, dirpath) + tar.chmod(tarinfo, dirpath) diff --git a/workers/dockerfilebuild.py b/workers/dockerfilebuild.py index f44642ff2..9055d0152 100644 --- a/workers/dockerfilebuild.py +++ b/workers/dockerfilebuild.py @@ -17,6 +17,7 @@ from data.queue import dockerfile_build_queue from data import model from workers.worker import Worker from app import app +from util.safetar import safe_extractall root_logger = logging.getLogger('') @@ -152,8 +153,14 @@ class DockerfileBuildContext(object): for status in build_status: fully_unwrapped = "" if isinstance(status, dict): - if len(status) > 0: - fully_unwrapped = status.values()[0] + keys_to_extract = ['error', 'status', 'stream'] + for key in keys_to_extract: + if key in status: + fully_unwrapped = status[key] + break + + if not fully_unwrapped: + logger.debug('Status dict did not have any extractable keys and was: %s', status) elif isinstance(status, basestring): fully_unwrapped = status @@ -322,7 +329,7 @@ class DockerfileBuildWorker(Worker): # Save the zip file to temp somewhere with tarfile.open(mode='r|*', fileobj=request_file.raw) as tar_stream: - tar_stream.extractall(build_dir) + safe_extractall(tar_stream, build_dir) return build_dir