diff --git a/.travis.yml b/.travis.yml index 429726c..4db16a0 100755 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ language: node_js node_js: - "12" + - "14" - "node" sudo: false diff --git a/lib/index.js b/lib/index.js index 6a164d2..962d65d 100755 --- a/lib/index.js +++ b/lib/index.js @@ -57,7 +57,7 @@ exports.Dispenser = internals.Dispenser = class extends Stream.Writable { constructor(options) { - super(); + super({ autoDestroy: false }); Hoek.assert(options !== null && typeof options === 'object', 'options must be an object'); const settings = Hoek.applyToDefaults(internals.defaults, options); @@ -147,227 +147,219 @@ exports.Dispenser = internals.Dispenser = class extends Stream.Writable { req.once('aborted', onReqAborted); }); } -}; + _write(buffer, encoding, next) { -internals.Dispenser.prototype._write = function (buffer, encoding, next) { + if (this._error) { + return next(); + } - if (this._error) { + this._parts.write(buffer); return next(); } - this._parts.write(buffer); - return next(); -}; - + _emit(...args) { -internals.Dispenser.prototype._emit = function (...args) { + if (this._error) { + return; + } - if (this._error) { - return; + this.emit(...args); } - this.emit(...args); -}; - - -internals.Dispenser.prototype._abort = function (err) { + _abort(err) { - this._emit('error', err); - this._error = err; -}; + this._emit('error', err); + this._error = err; + } + _onPartEnd() { -internals.Dispenser.prototype._onPartEnd = function () { + this._lines.flush(); - this._lines.flush(); + if (this._state === internals.state.preamble) { + if (this._held) { + const last = this._held.length - 1; - if (this._state === internals.state.preamble) { - if (this._held) { - const last = this._held.length - 1; + if (this._held[last] !== '\n' || + this._held[last - 1] !== '\r') { - if (this._held[last] !== '\n' || - this._held[last - 1] !== '\r') { + return this._abort(Boom.badRequest('Preamble missing CRLF terminator')); + } - return this._abort(Boom.badRequest('Preamble missing CRLF terminator')); + this._emit('preamble', this._held.slice(0, -2)); + this._held = ''; } - this._emit('preamble', this._held.slice(0, -2)); - this._held = ''; + this._parts.needle(Buffer.from('\r\n--' + this._boundary)); // CRLF no longer optional } - this._parts.needle(Buffer.from('\r\n--' + this._boundary)); // CRLF no longer optional - } - - this._state = internals.state.boundary; + this._state = internals.state.boundary; - if (this._stream) { - this._stream.end(); - this._stream = null; - } - else if (this._name) { - this._emit('field', this._name, this._held); - this._name = ''; - this._held = ''; + if (this._stream) { + this._stream.end(); + this._stream = null; + } + else if (this._name) { + this._emit('field', this._name, this._held); + this._name = ''; + this._held = ''; + } } -}; - -internals.Dispenser.prototype._onPart = function (chunk) { + _onPart(chunk) { - if (this._state === internals.state.preamble) { - this._held = this._held + chunk.toString(); - } - else if (this._state === internals.state.payload) { - if (this._stream) { - this._stream.write(chunk); // Stream payload + if (this._state === internals.state.preamble) { + this._held = this._held + chunk.toString(); + } + else if (this._state === internals.state.payload) { + if (this._stream) { + this._stream.write(chunk); // Stream payload + } + else { + this._held = this._held + chunk.toString(); + } } else { - this._held = this._held + chunk.toString(); + this._lines.write(chunk); // Look for boundary } } - else { - this._lines.write(chunk); // Look for boundary - } -}; + _onLineEnd() { -internals.Dispenser.prototype._onLineEnd = function () { + // Boundary whitespace - // Boundary whitespace - - if (this._state === internals.state.boundary) { - if (this._held) { - this._held = this._held.replace(/[\t ]/g, ''); // trim() removes new lines + if (this._state === internals.state.boundary) { if (this._held) { - if (this._held === '--') { - this._state = internals.state.epilogue; - this._held = ''; + this._held = this._held.replace(/[\t ]/g, ''); // trim() removes new lines + if (this._held) { + if (this._held === '--') { + this._state = internals.state.epilogue; + this._held = ''; - return; - } + return; + } - return this._abort(Boom.badRequest('Only white space allowed after boundary')); + return this._abort(Boom.badRequest('Only white space allowed after boundary')); + } } - } - this._state = internals.state.header; + this._state = internals.state.header; - return; - } + return; + } + + // Part headers - // Part headers + if (this._state === internals.state.header) { - if (this._state === internals.state.header) { + // Header - // Header + if (this._held) { - if (this._held) { + // Header continuation - // Header continuation + if (this._held[0] === ' ' || + this._held[0] === '\t') { - if (this._held[0] === ' ' || - this._held[0] === '\t') { + if (!this._pendingHeader) { + return this._abort(Boom.badRequest('Invalid header continuation without valid declaration on previous line')); + } - if (!this._pendingHeader) { - return this._abort(Boom.badRequest('Invalid header continuation without valid declaration on previous line')); + this._pendingHeader = this._pendingHeader + ' ' + this._held.slice(1); // Drop tab + this._held = ''; + return; } - this._pendingHeader = this._pendingHeader + ' ' + this._held.slice(1); // Drop tab + // Start of new header + + this._flushHeader(); + this._pendingHeader = this._held; this._held = ''; + return; } - // Start of new header + // End of headers this._flushHeader(); - this._pendingHeader = this._held; - this._held = ''; - - return; - } - // End of headers + this._state = internals.state.payload; - this._flushHeader(); + let disposition; - this._state = internals.state.payload; - - let disposition; + try { + disposition = Content.disposition(this._headers['content-disposition']); + } + catch (err) { + return this._abort(err); + } - try { - disposition = Content.disposition(this._headers['content-disposition']); - } - catch (err) { - return this._abort(err); - } + if (disposition.filename !== undefined) { + const stream = new Stream.PassThrough(); + const transferEncoding = this._headers['content-transfer-encoding']; - if (disposition.filename !== undefined) { - const stream = new Stream.PassThrough(); - const transferEncoding = this._headers['content-transfer-encoding']; + if (transferEncoding && + transferEncoding.toLowerCase() === 'base64') { - if (transferEncoding && - transferEncoding.toLowerCase() === 'base64') { + this._stream = new B64.Decoder(); + this._stream.pipe(stream); + } + else { + this._stream = stream; + } - this._stream = new B64.Decoder(); - this._stream.pipe(stream); + stream.name = disposition.name; + stream.filename = disposition.filename; + stream.headers = this._headers; + this._headers = {}; + this._emit('part', stream); } else { - this._stream = stream; + this._name = disposition.name; } - stream.name = disposition.name; - stream.filename = disposition.filename; - stream.headers = this._headers; - this._headers = {}; - this._emit('part', stream); - } - else { - this._name = disposition.name; + this._lines.flush(); + return; } - this._lines.flush(); - return; - } - - // Epilogue - - this._held = this._held + '\r\n'; // Put the new line back -}; + // Epilogue + this._held = this._held + '\r\n'; // Put the new line back + } -internals.Dispenser.prototype._onLine = function (chunk) { + _onLine(chunk) { - if (this._stream) { - this._stream.write(chunk); // Stream payload - } - else { - this._held = this._held + chunk.toString(); // Reading header or field + if (this._stream) { + this._stream.write(chunk); // Stream payload + } + else { + this._held = this._held + chunk.toString(); // Reading header or field + } } -}; + _flushHeader() { -internals.Dispenser.prototype._flushHeader = function () { + if (!this._pendingHeader) { + return; + } - if (!this._pendingHeader) { - return; - } + const sep = this._pendingHeader.indexOf(':'); - const sep = this._pendingHeader.indexOf(':'); + if (sep === -1) { + return this._abort(Boom.badRequest('Invalid header missing colon separator')); + } - if (sep === -1) { - return this._abort(Boom.badRequest('Invalid header missing colon separator')); - } + if (!sep) { + return this._abort(Boom.badRequest('Invalid header missing field name')); + } - if (!sep) { - return this._abort(Boom.badRequest('Invalid header missing field name')); - } + const name = this._pendingHeader.slice(0, sep).toLowerCase(); + if (name === '__proto__') { + return this._abort(Boom.badRequest('Invalid header')); + } - const name = this._pendingHeader.slice(0, sep).toLowerCase(); - if (name === '__proto__') { - return this._abort(Boom.badRequest('Invalid header')); + this._headers[name] = this._pendingHeader.slice(sep + 1).trim(); + this._pendingHeader = ''; } - - this._headers[name] = this._pendingHeader.slice(sep + 1).trim(); - this._pendingHeader = ''; };