Control: tags -1 - moreinfo > Please unblock package node-formidable > > [...] > > node-formidable hasn't been updated for years. Testing version isn't > > compatible with our nodejs but this was not seen since there was no > > real tests (see https://bugs.debian.org/924589). > > > > I upgraded it and added full tests (build and autopkgtest). > > > > node-formidable reverse dependencies: > > - node-superagent: > > +- node-supertest (no reverse dependencies) > > > > node-formidable bug does not affect these packages since the failing > > functions are not used in it, that's why I decrease bug severity to > > important. I tested the 2 reverse dependencies with both old and new > > node-formidable with success (sbuild and autopkgtest). > > > > debdiff is big since I embedded some node modules (only for tests, > > they are not installed). Upstream changes are not so big. I updated > > also debian/* files and added examples (and tests of course). > > > > I think it's low risky to update node-formidable. However I leave > > you > > appreciate if it is opportune. > > > Hi all, > > when enabling upstream test in 1.2.1-1, it fails only under debci LXC > environment. After many tries, I fixed this in 1.2.1-2 (was a test > loop: > test/run.js tried to test itself). So all tests are OK now. If you > think > it is opportune to unblock node-formidable,
Hi all, here is a partial diff that shows changes on installed files. Cheers, Xavier
diff --git a/lib/file.js b/lib/file.js index 1e7184d..50d34c0 100644 --- a/lib/file.js +++ b/lib/file.js @@ -1,7 +1,7 @@ if (global.GENTLY) require = GENTLY.hijack(require); var util = require('util'), - WriteStream = require('fs').WriteStream, + fs = require('fs'), EventEmitter = require('events').EventEmitter, crypto = require('crypto'); @@ -23,17 +23,19 @@ function File(properties) { if(typeof this.hash === 'string') { this.hash = crypto.createHash(properties.hash); + } else { + this.hash = null; } } module.exports = File; util.inherits(File, EventEmitter); File.prototype.open = function() { - this._writeStream = new WriteStream(this.path); + this._writeStream = new fs.WriteStream(this.path); }; File.prototype.toJSON = function() { - return { + var json = { size: this.size, path: this.path, name: this.name, @@ -43,16 +45,23 @@ File.prototype.toJSON = function() { filename: this.filename, mime: this.mime }; + if (this.hash && this.hash != "") { + json.hash = this.hash; + } + return json; }; File.prototype.write = function(buffer, cb) { var self = this; + if (self.hash) { + self.hash.update(buffer); + } + + if (this._writeStream.closed) { + return cb(); + } + this._writeStream.write(buffer, function() { - if (self.hash) { - if (self.hash.hasOwnProperty('update')) { - self.hash.update(buffer); - } - } self.lastModifiedDate = new Date(); self.size += buffer.length; self.emit('progress', self.size); @@ -62,10 +71,10 @@ File.prototype.write = function(buffer, cb) { File.prototype.end = function(cb) { var self = this; + if (self.hash) { + self.hash = self.hash.digest('hex'); + } this._writeStream.end(function() { - if(self.hash) { - self.hash = self.hash.digest('hex'); - } self.emit('end'); cb(); }); diff --git a/lib/incoming_form.js b/lib/incoming_form.js index 291236d..dbd920b 100644 --- a/lib/incoming_form.js +++ b/lib/incoming_form.js @@ -1,5 +1,6 @@ if (global.GENTLY) require = GENTLY.hijack(require); +var crypto = require('crypto'); var fs = require('fs'); var util = require('util'), path = require('path'), @@ -23,13 +24,15 @@ function IncomingForm(opts) { this.ended = false; this.maxFields = opts.maxFields || 1000; - this.maxFieldsSize = opts.maxFieldsSize || 2 * 1024 * 1024; + this.maxFieldsSize = opts.maxFieldsSize || 20 * 1024 * 1024; + this.maxFileSize = opts.maxFileSize || 200 * 1024 * 1024; this.keepExtensions = opts.keepExtensions || false; - this.uploadDir = opts.uploadDir || os.tmpDir(); + this.uploadDir = opts.uploadDir || (os.tmpdir && os.tmpdir()) || os.tmpDir(); this.encoding = opts.encoding || 'utf-8'; this.headers = null; this.type = null; - this.hash = false; + this.hash = opts.hash || false; + this.multiples = opts.multiples || false; this.bytesReceived = null; this.bytesExpected = null; @@ -37,10 +40,11 @@ function IncomingForm(opts) { this._parser = null; this._flushing = 0; this._fieldsSize = 0; + this._fileSize = 0; this.openedFiles = []; return this; -}; +} util.inherits(IncomingForm, EventEmitter); exports.IncomingForm = IncomingForm; @@ -74,6 +78,40 @@ IncomingForm.prototype.parse = function(req, cb) { return true; }; + // Setup callback first, so we don't miss anything from data events emitted + // immediately. + if (cb) { + var fields = {}, files = {}; + this + .on('field', function(name, value) { + fields[name] = value; + }) + .on('file', function(name, file) { + if (this.multiples) { + if (files[name]) { + if (!Array.isArray(files[name])) { + files[name] = [files[name]]; + } + files[name].push(file); + } else { + files[name] = file; + } + } else { + files[name] = file; + } + }) + .on('error', function(err) { + cb(err, fields, files); + }) + .on('end', function() { + cb(null, fields, files); + }); + } + + // Parse headers and setup the parser, ready to start listening for data. + this.writeHeaders(req.headers); + + // Start listening for data. var self = this; req .on('error', function(err) { @@ -97,25 +135,6 @@ IncomingForm.prototype.parse = function(req, cb) { } }); - if (cb) { - var fields = {}, files = {}; - this - .on('field', function(name, value) { - fields[name] = value; - }) - .on('file', function(name, file) { - files[name] = file; - }) - .on('error', function(err) { - cb(err, fields, files); - }) - .on('end', function() { - cb(null, fields, files); - }); - } - - this.writeHeaders(req.headers); - return this; }; @@ -126,8 +145,11 @@ IncomingForm.prototype.writeHeaders = function(headers) { }; IncomingForm.prototype.write = function(buffer) { + if (this.error) { + return; + } if (!this._parser) { - this._error(new Error('unintialized parser')); + this._error(new Error('uninitialized parser')); return; } @@ -160,6 +182,7 @@ IncomingForm.prototype.onPart = function(part) { IncomingForm.prototype.handlePart = function(part) { var self = this; + // This MUST check exactly for undefined. You can not change it to !part.filename. if (part.filename === undefined) { var value = '' , decoder = new StringDecoder(this.encoding); @@ -194,6 +217,14 @@ IncomingForm.prototype.handlePart = function(part) { this.openedFiles.push(file); part.on('data', function(buffer) { + self._fileSize += buffer.length; + if (self._fileSize > self.maxFileSize) { + self._error(new Error('maxFileSize exceeded, received '+self._fileSize+' bytes of file data')); + return; + } + if (buffer.length == 0) { + return; + } self.pause(); file.write(buffer, function() { self.resume(); @@ -241,8 +272,8 @@ IncomingForm.prototype._parseContentType = function() { } if (this.headers['content-type'].match(/multipart/i)) { - var m; - if (m = this.headers['content-type'].match(/boundary=(?:"([^"]+)"|([^;]+))/i)) { + var m = this.headers['content-type'].match(/boundary=(?:"([^"]+)"|([^;]+))/i); + if (m) { this._initMultipart(m[1] || m[2]); } else { this._error(new Error('bad content-type header, no multipart boundary')); @@ -264,21 +295,25 @@ IncomingForm.prototype._error = function(err) { } this.error = err; - this.pause(); this.emit('error', err); if (Array.isArray(this.openedFiles)) { this.openedFiles.forEach(function(file) { file._writeStream.destroy(); - setTimeout(fs.unlink, 0, file.path); + setTimeout(fs.unlink, 0, file.path, function(error) { }); }); } }; IncomingForm.prototype._parseContentLength = function() { + this.bytesReceived = 0; if (this.headers['content-length']) { - this.bytesReceived = 0; this.bytesExpected = parseInt(this.headers['content-length'], 10); + } else if (this.headers['transfer-encoding'] === undefined) { + this.bytesExpected = 0; + } + + if (this.bytesExpected !== null) { this.emit('progress', this.bytesReceived, this.bytesExpected); } }; @@ -325,10 +360,11 @@ IncomingForm.prototype._initMultipart = function(boundary) { headerField = headerField.toLowerCase(); part.headers[headerField] = headerValue; - var m; + // matches either a quoted-string or a token (RFC 2616 section 19.5.1) + var m = headerValue.match(/\bname=("([^"]*)"|([^\(\)<>@,;:\\"\/\[\]\?=\{\}\s\t/]+))/i); if (headerField == 'content-disposition') { - if (m = headerValue.match(/\bname="([^"]+)"/i)) { - part.name = m[1]; + if (m) { + part.name = m[2] || m[3] || ''; } part.filename = self._fileName(headerValue); @@ -366,13 +402,13 @@ IncomingForm.prototype._initMultipart = function(boundary) { can be divided by 4, it will result in a number of buytes that can be divided vy 3. */ - var offset = parseInt(part.transferBuffer.length / 4) * 4; - part.emit('data', new Buffer(part.transferBuffer.substring(0, offset), 'base64')) + var offset = parseInt(part.transferBuffer.length / 4, 10) * 4; + part.emit('data', new Buffer(part.transferBuffer.substring(0, offset), 'base64')); part.transferBuffer = part.transferBuffer.substring(offset); }; parser.onPartEnd = function() { - part.emit('data', new Buffer(part.transferBuffer, 'base64')) + part.emit('data', new Buffer(part.transferBuffer, 'base64')); part.emit('end'); }; break; @@ -394,10 +430,12 @@ IncomingForm.prototype._initMultipart = function(boundary) { }; IncomingForm.prototype._fileName = function(headerValue) { - var m = headerValue.match(/\bfilename="(.*?)"($|; )/i); + // matches either a quoted-string or a token (RFC 2616 section 19.5.1) + var m = headerValue.match(/\bfilename=("(.*?)"|([^\(\)<>@,;:\\"\/\[\]\?=\{\}\s\t/]+))($|;\s)/i); if (!m) return; - var filename = m[1].substr(m[1].lastIndexOf('\\') + 1); + var match = m[2] || m[3] || ''; + var filename = match.substr(match.lastIndexOf('\\') + 1); filename = filename.replace(/%22/g, '"'); filename = filename.replace(/&#([\d]{4});/g, function(m, code) { return String.fromCharCode(code); @@ -434,10 +472,9 @@ IncomingForm.prototype._initOctetStream = function() { type: mime }); - file.open(); - this.emit('fileBegin', filename, file); - + file.open(); + this.openedFiles.push(file); this._flushing++; var self = this; @@ -466,8 +503,10 @@ IncomingForm.prototype._initOctetStream = function() { self.ended = true; var done = function(){ - self.emit('file', 'file', file); - self._maybeEnd(); + file.end(function() { + self.emit('file', 'file', file); + self._maybeEnd(); + }); }; if(outstandingWrites === 0){ @@ -481,16 +520,12 @@ IncomingForm.prototype._initOctetStream = function() { IncomingForm.prototype._initJSONencoded = function() { this.type = 'json'; - var parser = new JSONParser() + var parser = new JSONParser(this) , self = this; - if (this.bytesExpected) { - parser.initWithLength(this.bytesExpected); - } - parser.onField = function(key, val) { self.emit('field', key, val); - } + }; parser.onEnd = function() { self.ended = true; @@ -501,14 +536,12 @@ IncomingForm.prototype._initJSONencoded = function() { }; IncomingForm.prototype._uploadPath = function(filename) { - var name = ''; - for (var i = 0; i < 32; i++) { - name += Math.floor(Math.random() * 16).toString(16); - } + var buf = crypto.randomBytes(16); + var name = 'upload_' + buf.toString('hex'); if (this.keepExtensions) { var ext = path.extname(filename); - ext = ext.replace(/(\.[a-z0-9]+).*/, '$1'); + ext = ext.replace(/(\.[a-z0-9]+).*/i, '$1'); name += ext; } @@ -523,4 +556,3 @@ IncomingForm.prototype._maybeEnd = function() { this.emit('end'); }; - diff --git a/lib/json_parser.js b/lib/json_parser.js index 6ce966b..28a23ba 100644 --- a/lib/json_parser.js +++ b/lib/json_parser.js @@ -1,35 +1,30 @@ if (global.GENTLY) require = GENTLY.hijack(require); -var Buffer = require('buffer').Buffer +var Buffer = require('buffer').Buffer; -function JSONParser() { - this.data = new Buffer(''); +function JSONParser(parent) { + this.parent = parent; + this.chunks = []; this.bytesWritten = 0; -}; -exports.JSONParser = JSONParser; - -JSONParser.prototype.initWithLength = function(length) { - this.data = new Buffer(length); } +exports.JSONParser = JSONParser; JSONParser.prototype.write = function(buffer) { - if (this.data.length >= this.bytesWritten + buffer.length) { - buffer.copy(this.data, this.bytesWritten); - } else { - this.data = Buffer.concat([this.data, buffer]); - } this.bytesWritten += buffer.length; + this.chunks.push(buffer); return buffer.length; -} +}; JSONParser.prototype.end = function() { try { - var fields = JSON.parse(this.data.toString('utf8')) + var fields = JSON.parse(Buffer.concat(this.chunks)); for (var field in fields) { this.onField(field, fields[field]); } - } catch (e) {} + } catch (e) { + this.parent.emit('error', e); + } this.data = null; this.onEnd(); -} \ No newline at end of file +}; diff --git a/lib/multipart_parser.js b/lib/multipart_parser.js index 98a6856..36de2b0 100644 --- a/lib/multipart_parser.js +++ b/lib/multipart_parser.js @@ -46,7 +46,7 @@ function MultipartParser() { this.index = null; this.flags = 0; -}; +} exports.MultipartParser = MultipartParser; MultipartParser.stateToString = function(stateNumber) { @@ -58,8 +58,8 @@ MultipartParser.stateToString = function(stateNumber) { MultipartParser.prototype.initWithBoundary = function(str) { this.boundary = new Buffer(str.length+4); - this.boundary.write('\r\n--', 'ascii', 0); - this.boundary.write(str, 'ascii', 4); + this.boundary.write('\r\n--', 0); + this.boundary.write(str, 4); this.lookbehind = new Buffer(this.boundary.length+8); this.state = S.START; @@ -127,18 +127,25 @@ MultipartParser.prototype.write = function(buffer) { state = S.START_BOUNDARY; case S.START_BOUNDARY: if (index == boundary.length - 2) { - if (c != CR) { + if (c == HYPHEN) { + flags |= F.LAST_BOUNDARY; + } else if (c != CR) { return i; } index++; break; } else if (index - 1 == boundary.length - 2) { - if (c != LF) { + if (flags & F.LAST_BOUNDARY && c == HYPHEN){ + callback('end'); + state = S.END; + flags = 0; + } else if (!(flags & F.LAST_BOUNDARY) && c == LF) { + index = 0; + callback('partBegin'); + state = S.HEADER_FIELD_START; + } else { return i; } - index = 0; - callback('partBegin'); - state = S.HEADER_FIELD_START; break; } @@ -214,7 +221,7 @@ MultipartParser.prototype.write = function(buffer) { case S.PART_DATA: prevIndex = index; - if (index == 0) { + if (index === 0) { // boyer-moore derrived algorithm to safely skip non-boundary data i += boundaryEnd; while (i < bufferLength && !(buffer[i] in boundaryChars)) { @@ -226,7 +233,7 @@ MultipartParser.prototype.write = function(buffer) { if (index < boundary.length) { if (boundary[index] == c) { - if (index == 0) { + if (index === 0) { dataCallback('partData', true); } index++; @@ -260,6 +267,7 @@ MultipartParser.prototype.write = function(buffer) { callback('partEnd'); callback('end'); state = S.END; + flags = 0; } else { index = 0; } @@ -310,7 +318,7 @@ MultipartParser.prototype.end = function() { self[callbackSymbol](); } }; - if ((this.state == S.HEADER_FIELD_START && this.index == 0) || + if ((this.state == S.HEADER_FIELD_START && this.index === 0) || (this.state == S.PART_DATA && this.index == this.boundary.length)) { callback(this, 'partEnd'); callback(this, 'end');