Spaces:
Runtime error
Runtime error
| if (global.GENTLY) require = GENTLY.hijack(require); | |
| var crypto = require('crypto'); | |
| var fs = require('fs'); | |
| var util = require('util'), | |
| path = require('path'), | |
| File = require('./file'), | |
| MultipartParser = require('./multipart_parser').MultipartParser, | |
| QuerystringParser = require('./querystring_parser').QuerystringParser, | |
| OctetParser = require('./octet_parser').OctetParser, | |
| JSONParser = require('./json_parser').JSONParser, | |
| StringDecoder = require('string_decoder').StringDecoder, | |
| EventEmitter = require('events').EventEmitter, | |
| Stream = require('stream').Stream, | |
| os = require('os'); | |
| function IncomingForm(opts) { | |
| if (!(this instanceof IncomingForm)) return new IncomingForm(opts); | |
| EventEmitter.call(this); | |
| opts=opts||{}; | |
| this.error = null; | |
| this.ended = false; | |
| this.maxFields = opts.maxFields || 1000; | |
| 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 && os.tmpdir()) || os.tmpDir(); | |
| this.encoding = opts.encoding || 'utf-8'; | |
| this.headers = null; | |
| this.type = null; | |
| this.hash = opts.hash || false; | |
| this.multiples = opts.multiples || false; | |
| this.bytesReceived = null; | |
| this.bytesExpected = null; | |
| this._parser = null; | |
| this._flushing = 0; | |
| this._fieldsSize = 0; | |
| this._fileSize = 0; | |
| this.openedFiles = []; | |
| return this; | |
| } | |
| util.inherits(IncomingForm, EventEmitter); | |
| exports.IncomingForm = IncomingForm; | |
| IncomingForm.prototype.parse = function(req, cb) { | |
| this.pause = function() { | |
| try { | |
| req.pause(); | |
| } catch (err) { | |
| // the stream was destroyed | |
| if (!this.ended) { | |
| // before it was completed, crash & burn | |
| this._error(err); | |
| } | |
| return false; | |
| } | |
| return true; | |
| }; | |
| this.resume = function() { | |
| try { | |
| req.resume(); | |
| } catch (err) { | |
| // the stream was destroyed | |
| if (!this.ended) { | |
| // before it was completed, crash & burn | |
| this._error(err); | |
| } | |
| return false; | |
| } | |
| 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) { | |
| self._error(err); | |
| }) | |
| .on('aborted', function() { | |
| self.emit('aborted'); | |
| self._error(new Error('Request aborted')); | |
| }) | |
| .on('data', function(buffer) { | |
| self.write(buffer); | |
| }) | |
| .on('end', function() { | |
| if (self.error) { | |
| return; | |
| } | |
| var err = self._parser.end(); | |
| if (err) { | |
| self._error(err); | |
| } | |
| }); | |
| return this; | |
| }; | |
| IncomingForm.prototype.writeHeaders = function(headers) { | |
| this.headers = headers; | |
| this._parseContentLength(); | |
| this._parseContentType(); | |
| }; | |
| IncomingForm.prototype.write = function(buffer) { | |
| if (this.error) { | |
| return; | |
| } | |
| if (!this._parser) { | |
| this._error(new Error('uninitialized parser')); | |
| return; | |
| } | |
| this.bytesReceived += buffer.length; | |
| this.emit('progress', this.bytesReceived, this.bytesExpected); | |
| var bytesParsed = this._parser.write(buffer); | |
| if (bytesParsed !== buffer.length) { | |
| this._error(new Error('parser error, '+bytesParsed+' of '+buffer.length+' bytes parsed')); | |
| } | |
| return bytesParsed; | |
| }; | |
| IncomingForm.prototype.pause = function() { | |
| // this does nothing, unless overwritten in IncomingForm.parse | |
| return false; | |
| }; | |
| IncomingForm.prototype.resume = function() { | |
| // this does nothing, unless overwritten in IncomingForm.parse | |
| return false; | |
| }; | |
| IncomingForm.prototype.onPart = function(part) { | |
| // this method can be overwritten by the user | |
| this.handlePart(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); | |
| part.on('data', function(buffer) { | |
| self._fieldsSize += buffer.length; | |
| if (self._fieldsSize > self.maxFieldsSize) { | |
| self._error(new Error('maxFieldsSize exceeded, received '+self._fieldsSize+' bytes of field data')); | |
| return; | |
| } | |
| value += decoder.write(buffer); | |
| }); | |
| part.on('end', function() { | |
| self.emit('field', part.name, value); | |
| }); | |
| return; | |
| } | |
| this._flushing++; | |
| var file = new File({ | |
| path: this._uploadPath(part.filename), | |
| name: part.filename, | |
| type: part.mime, | |
| hash: self.hash | |
| }); | |
| this.emit('fileBegin', part.name, file); | |
| file.open(); | |
| 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(); | |
| }); | |
| }); | |
| part.on('end', function() { | |
| file.end(function() { | |
| self._flushing--; | |
| self.emit('file', part.name, file); | |
| self._maybeEnd(); | |
| }); | |
| }); | |
| }; | |
| function dummyParser(self) { | |
| return { | |
| end: function () { | |
| self.ended = true; | |
| self._maybeEnd(); | |
| return null; | |
| } | |
| }; | |
| } | |
| IncomingForm.prototype._parseContentType = function() { | |
| if (this.bytesExpected === 0) { | |
| this._parser = dummyParser(this); | |
| return; | |
| } | |
| if (!this.headers['content-type']) { | |
| this._error(new Error('bad content-type header, no content-type')); | |
| return; | |
| } | |
| if (this.headers['content-type'].match(/octet-stream/i)) { | |
| this._initOctetStream(); | |
| return; | |
| } | |
| if (this.headers['content-type'].match(/urlencoded/i)) { | |
| this._initUrlencoded(); | |
| return; | |
| } | |
| if (this.headers['content-type'].match(/multipart/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')); | |
| } | |
| return; | |
| } | |
| if (this.headers['content-type'].match(/json/i)) { | |
| this._initJSONencoded(); | |
| return; | |
| } | |
| this._error(new Error('bad content-type header, unknown content-type: '+this.headers['content-type'])); | |
| }; | |
| IncomingForm.prototype._error = function(err) { | |
| if (this.error || this.ended) { | |
| return; | |
| } | |
| this.error = err; | |
| this.emit('error', err); | |
| if (Array.isArray(this.openedFiles)) { | |
| this.openedFiles.forEach(function(file) { | |
| file._writeStream.destroy(); | |
| setTimeout(fs.unlink, 0, file.path, function(error) { }); | |
| }); | |
| } | |
| }; | |
| IncomingForm.prototype._parseContentLength = function() { | |
| this.bytesReceived = 0; | |
| if (this.headers['content-length']) { | |
| 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); | |
| } | |
| }; | |
| IncomingForm.prototype._newParser = function() { | |
| return new MultipartParser(); | |
| }; | |
| IncomingForm.prototype._initMultipart = function(boundary) { | |
| this.type = 'multipart'; | |
| var parser = new MultipartParser(), | |
| self = this, | |
| headerField, | |
| headerValue, | |
| part; | |
| parser.initWithBoundary(boundary); | |
| parser.onPartBegin = function() { | |
| part = new Stream(); | |
| part.readable = true; | |
| part.headers = {}; | |
| part.name = null; | |
| part.filename = null; | |
| part.mime = null; | |
| part.transferEncoding = 'binary'; | |
| part.transferBuffer = ''; | |
| headerField = ''; | |
| headerValue = ''; | |
| }; | |
| parser.onHeaderField = function(b, start, end) { | |
| headerField += b.toString(self.encoding, start, end); | |
| }; | |
| parser.onHeaderValue = function(b, start, end) { | |
| headerValue += b.toString(self.encoding, start, end); | |
| }; | |
| parser.onHeaderEnd = function() { | |
| headerField = headerField.toLowerCase(); | |
| part.headers[headerField] = headerValue; | |
| // 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) { | |
| part.name = m[2] || m[3] || ''; | |
| } | |
| part.filename = self._fileName(headerValue); | |
| } else if (headerField == 'content-type') { | |
| part.mime = headerValue; | |
| } else if (headerField == 'content-transfer-encoding') { | |
| part.transferEncoding = headerValue.toLowerCase(); | |
| } | |
| headerField = ''; | |
| headerValue = ''; | |
| }; | |
| parser.onHeadersEnd = function() { | |
| switch(part.transferEncoding){ | |
| case 'binary': | |
| case '7bit': | |
| case '8bit': | |
| parser.onPartData = function(b, start, end) { | |
| part.emit('data', b.slice(start, end)); | |
| }; | |
| parser.onPartEnd = function() { | |
| part.emit('end'); | |
| }; | |
| break; | |
| case 'base64': | |
| parser.onPartData = function(b, start, end) { | |
| part.transferBuffer += b.slice(start, end).toString('ascii'); | |
| /* | |
| four bytes (chars) in base64 converts to three bytes in binary | |
| encoding. So we should always work with a number of bytes that | |
| 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, 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('end'); | |
| }; | |
| break; | |
| default: | |
| return self._error(new Error('unknown transfer-encoding')); | |
| } | |
| self.onPart(part); | |
| }; | |
| parser.onEnd = function() { | |
| self.ended = true; | |
| self._maybeEnd(); | |
| }; | |
| this._parser = parser; | |
| }; | |
| IncomingForm.prototype._fileName = function(headerValue) { | |
| // 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 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); | |
| }); | |
| return filename; | |
| }; | |
| IncomingForm.prototype._initUrlencoded = function() { | |
| this.type = 'urlencoded'; | |
| var parser = new QuerystringParser(this.maxFields) | |
| , self = this; | |
| parser.onField = function(key, val) { | |
| self.emit('field', key, val); | |
| }; | |
| parser.onEnd = function() { | |
| self.ended = true; | |
| self._maybeEnd(); | |
| }; | |
| this._parser = parser; | |
| }; | |
| IncomingForm.prototype._initOctetStream = function() { | |
| this.type = 'octet-stream'; | |
| var filename = this.headers['x-file-name']; | |
| var mime = this.headers['content-type']; | |
| var file = new File({ | |
| path: this._uploadPath(filename), | |
| name: filename, | |
| type: mime | |
| }); | |
| this.emit('fileBegin', filename, file); | |
| file.open(); | |
| this.openedFiles.push(file); | |
| this._flushing++; | |
| var self = this; | |
| self._parser = new OctetParser(); | |
| //Keep track of writes that haven't finished so we don't emit the file before it's done being written | |
| var outstandingWrites = 0; | |
| self._parser.on('data', function(buffer){ | |
| self.pause(); | |
| outstandingWrites++; | |
| file.write(buffer, function() { | |
| outstandingWrites--; | |
| self.resume(); | |
| if(self.ended){ | |
| self._parser.emit('doneWritingFile'); | |
| } | |
| }); | |
| }); | |
| self._parser.on('end', function(){ | |
| self._flushing--; | |
| self.ended = true; | |
| var done = function(){ | |
| file.end(function() { | |
| self.emit('file', 'file', file); | |
| self._maybeEnd(); | |
| }); | |
| }; | |
| if(outstandingWrites === 0){ | |
| done(); | |
| } else { | |
| self._parser.once('doneWritingFile', done); | |
| } | |
| }); | |
| }; | |
| IncomingForm.prototype._initJSONencoded = function() { | |
| this.type = 'json'; | |
| var parser = new JSONParser(this) | |
| , self = this; | |
| parser.onField = function(key, val) { | |
| self.emit('field', key, val); | |
| }; | |
| parser.onEnd = function() { | |
| self.ended = true; | |
| self._maybeEnd(); | |
| }; | |
| this._parser = parser; | |
| }; | |
| IncomingForm.prototype._uploadPath = function(filename) { | |
| 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]+).*/i, '$1'); | |
| name += ext; | |
| } | |
| return path.join(this.uploadDir, name); | |
| }; | |
| IncomingForm.prototype._maybeEnd = function() { | |
| if (!this.ended || this._flushing || this.error) { | |
| return; | |
| } | |
| this.emit('end'); | |
| }; | |