123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653 |
- 'use strict';
- const { Readable, Writable } = require('stream');
- const StreamSearch = require('streamsearch');
- const {
- basename,
- convertToUTF8,
- getDecoder,
- parseContentType,
- parseDisposition,
- } = require('../utils.js');
- const BUF_CRLF = Buffer.from('\r\n');
- const BUF_CR = Buffer.from('\r');
- const BUF_DASH = Buffer.from('-');
- function noop() {}
- const MAX_HEADER_PAIRS = 2000; // From node
- const MAX_HEADER_SIZE = 16 * 1024; // From node (its default value)
- const HPARSER_NAME = 0;
- const HPARSER_PRE_OWS = 1;
- const HPARSER_VALUE = 2;
- class HeaderParser {
- constructor(cb) {
- this.header = Object.create(null);
- this.pairCount = 0;
- this.byteCount = 0;
- this.state = HPARSER_NAME;
- this.name = '';
- this.value = '';
- this.crlf = 0;
- this.cb = cb;
- }
- reset() {
- this.header = Object.create(null);
- this.pairCount = 0;
- this.byteCount = 0;
- this.state = HPARSER_NAME;
- this.name = '';
- this.value = '';
- this.crlf = 0;
- }
- push(chunk, pos, end) {
- let start = pos;
- while (pos < end) {
- switch (this.state) {
- case HPARSER_NAME: {
- let done = false;
- for (; pos < end; ++pos) {
- if (this.byteCount === MAX_HEADER_SIZE)
- return -1;
- ++this.byteCount;
- const code = chunk[pos];
- if (TOKEN[code] !== 1) {
- if (code !== 58/* ':' */)
- return -1;
- this.name += chunk.latin1Slice(start, pos);
- if (this.name.length === 0)
- return -1;
- ++pos;
- done = true;
- this.state = HPARSER_PRE_OWS;
- break;
- }
- }
- if (!done) {
- this.name += chunk.latin1Slice(start, pos);
- break;
- }
- // FALLTHROUGH
- }
- case HPARSER_PRE_OWS: {
- // Skip optional whitespace
- let done = false;
- for (; pos < end; ++pos) {
- if (this.byteCount === MAX_HEADER_SIZE)
- return -1;
- ++this.byteCount;
- const code = chunk[pos];
- if (code !== 32/* ' ' */ && code !== 9/* '\t' */) {
- start = pos;
- done = true;
- this.state = HPARSER_VALUE;
- break;
- }
- }
- if (!done)
- break;
- // FALLTHROUGH
- }
- case HPARSER_VALUE:
- switch (this.crlf) {
- case 0: // Nothing yet
- for (; pos < end; ++pos) {
- if (this.byteCount === MAX_HEADER_SIZE)
- return -1;
- ++this.byteCount;
- const code = chunk[pos];
- if (FIELD_VCHAR[code] !== 1) {
- if (code !== 13/* '\r' */)
- return -1;
- ++this.crlf;
- break;
- }
- }
- this.value += chunk.latin1Slice(start, pos++);
- break;
- case 1: // Received CR
- if (this.byteCount === MAX_HEADER_SIZE)
- return -1;
- ++this.byteCount;
- if (chunk[pos++] !== 10/* '\n' */)
- return -1;
- ++this.crlf;
- break;
- case 2: { // Received CR LF
- if (this.byteCount === MAX_HEADER_SIZE)
- return -1;
- ++this.byteCount;
- const code = chunk[pos];
- if (code === 32/* ' ' */ || code === 9/* '\t' */) {
- // Folded value
- start = pos;
- this.crlf = 0;
- } else {
- if (++this.pairCount < MAX_HEADER_PAIRS) {
- this.name = this.name.toLowerCase();
- if (this.header[this.name] === undefined)
- this.header[this.name] = [this.value];
- else
- this.header[this.name].push(this.value);
- }
- if (code === 13/* '\r' */) {
- ++this.crlf;
- ++pos;
- } else {
- // Assume start of next header field name
- start = pos;
- this.crlf = 0;
- this.state = HPARSER_NAME;
- this.name = '';
- this.value = '';
- }
- }
- break;
- }
- case 3: { // Received CR LF CR
- if (this.byteCount === MAX_HEADER_SIZE)
- return -1;
- ++this.byteCount;
- if (chunk[pos++] !== 10/* '\n' */)
- return -1;
- // End of header
- const header = this.header;
- this.reset();
- this.cb(header);
- return pos;
- }
- }
- break;
- }
- }
- return pos;
- }
- }
- class FileStream extends Readable {
- constructor(opts, owner) {
- super(opts);
- this.truncated = false;
- this._readcb = null;
- this.once('end', () => {
- // We need to make sure that we call any outstanding _writecb() that is
- // associated with this file so that processing of the rest of the form
- // can continue. This may not happen if the file stream ends right after
- // backpressure kicks in, so we force it here.
- this._read();
- if (--owner._fileEndsLeft === 0 && owner._finalcb) {
- const cb = owner._finalcb;
- owner._finalcb = null;
- // Make sure other 'end' event handlers get a chance to be executed
- // before busboy's 'finish' event is emitted
- process.nextTick(cb);
- }
- });
- }
- _read(n) {
- const cb = this._readcb;
- if (cb) {
- this._readcb = null;
- cb();
- }
- }
- }
- const ignoreData = {
- push: (chunk, pos) => {},
- destroy: () => {},
- };
- function callAndUnsetCb(self, err) {
- const cb = self._writecb;
- self._writecb = null;
- if (err)
- self.destroy(err);
- else if (cb)
- cb();
- }
- function nullDecoder(val, hint) {
- return val;
- }
- class Multipart extends Writable {
- constructor(cfg) {
- const streamOpts = {
- autoDestroy: true,
- emitClose: true,
- highWaterMark: (typeof cfg.highWaterMark === 'number'
- ? cfg.highWaterMark
- : undefined),
- };
- super(streamOpts);
- if (!cfg.conType.params || typeof cfg.conType.params.boundary !== 'string')
- throw new Error('Multipart: Boundary not found');
- const boundary = cfg.conType.params.boundary;
- const paramDecoder = (typeof cfg.defParamCharset === 'string'
- && cfg.defParamCharset
- ? getDecoder(cfg.defParamCharset)
- : nullDecoder);
- const defCharset = (cfg.defCharset || 'utf8');
- const preservePath = cfg.preservePath;
- const fileOpts = {
- autoDestroy: true,
- emitClose: true,
- highWaterMark: (typeof cfg.fileHwm === 'number'
- ? cfg.fileHwm
- : undefined),
- };
- const limits = cfg.limits;
- const fieldSizeLimit = (limits && typeof limits.fieldSize === 'number'
- ? limits.fieldSize
- : 1 * 1024 * 1024);
- const fileSizeLimit = (limits && typeof limits.fileSize === 'number'
- ? limits.fileSize
- : Infinity);
- const filesLimit = (limits && typeof limits.files === 'number'
- ? limits.files
- : Infinity);
- const fieldsLimit = (limits && typeof limits.fields === 'number'
- ? limits.fields
- : Infinity);
- const partsLimit = (limits && typeof limits.parts === 'number'
- ? limits.parts
- : Infinity);
- let parts = -1; // Account for initial boundary
- let fields = 0;
- let files = 0;
- let skipPart = false;
- this._fileEndsLeft = 0;
- this._fileStream = undefined;
- this._complete = false;
- let fileSize = 0;
- let field;
- let fieldSize = 0;
- let partCharset;
- let partEncoding;
- let partType;
- let partName;
- let partTruncated = false;
- let hitFilesLimit = false;
- let hitFieldsLimit = false;
- this._hparser = null;
- const hparser = new HeaderParser((header) => {
- this._hparser = null;
- skipPart = false;
- partType = 'text/plain';
- partCharset = defCharset;
- partEncoding = '7bit';
- partName = undefined;
- partTruncated = false;
- let filename;
- if (!header['content-disposition']) {
- skipPart = true;
- return;
- }
- const disp = parseDisposition(header['content-disposition'][0],
- paramDecoder);
- if (!disp || disp.type !== 'form-data') {
- skipPart = true;
- return;
- }
- if (disp.params) {
- if (disp.params.name)
- partName = disp.params.name;
- if (disp.params['filename*'])
- filename = disp.params['filename*'];
- else if (disp.params.filename)
- filename = disp.params.filename;
- if (filename !== undefined && !preservePath)
- filename = basename(filename);
- }
- if (header['content-type']) {
- const conType = parseContentType(header['content-type'][0]);
- if (conType) {
- partType = `${conType.type}/${conType.subtype}`;
- if (conType.params && typeof conType.params.charset === 'string')
- partCharset = conType.params.charset.toLowerCase();
- }
- }
- if (header['content-transfer-encoding'])
- partEncoding = header['content-transfer-encoding'][0].toLowerCase();
- if (partType === 'application/octet-stream' || filename !== undefined) {
- // File
- if (files === filesLimit) {
- if (!hitFilesLimit) {
- hitFilesLimit = true;
- this.emit('filesLimit');
- }
- skipPart = true;
- return;
- }
- ++files;
- if (this.listenerCount('file') === 0) {
- skipPart = true;
- return;
- }
- fileSize = 0;
- this._fileStream = new FileStream(fileOpts, this);
- ++this._fileEndsLeft;
- this.emit(
- 'file',
- partName,
- this._fileStream,
- { filename,
- encoding: partEncoding,
- mimeType: partType }
- );
- } else {
- // Non-file
- if (fields === fieldsLimit) {
- if (!hitFieldsLimit) {
- hitFieldsLimit = true;
- this.emit('fieldsLimit');
- }
- skipPart = true;
- return;
- }
- ++fields;
- if (this.listenerCount('field') === 0) {
- skipPart = true;
- return;
- }
- field = [];
- fieldSize = 0;
- }
- });
- let matchPostBoundary = 0;
- const ssCb = (isMatch, data, start, end, isDataSafe) => {
- retrydata:
- while (data) {
- if (this._hparser !== null) {
- const ret = this._hparser.push(data, start, end);
- if (ret === -1) {
- this._hparser = null;
- hparser.reset();
- this.emit('error', new Error('Malformed part header'));
- break;
- }
- start = ret;
- }
- if (start === end)
- break;
- if (matchPostBoundary !== 0) {
- if (matchPostBoundary === 1) {
- switch (data[start]) {
- case 45: // '-'
- // Try matching '--' after boundary
- matchPostBoundary = 2;
- ++start;
- break;
- case 13: // '\r'
- // Try matching CR LF before header
- matchPostBoundary = 3;
- ++start;
- break;
- default:
- matchPostBoundary = 0;
- }
- if (start === end)
- return;
- }
- if (matchPostBoundary === 2) {
- matchPostBoundary = 0;
- if (data[start] === 45/* '-' */) {
- // End of multipart data
- this._complete = true;
- this._bparser = ignoreData;
- return;
- }
- // We saw something other than '-', so put the dash we consumed
- // "back"
- const writecb = this._writecb;
- this._writecb = noop;
- ssCb(false, BUF_DASH, 0, 1, false);
- this._writecb = writecb;
- } else if (matchPostBoundary === 3) {
- matchPostBoundary = 0;
- if (data[start] === 10/* '\n' */) {
- ++start;
- if (parts >= partsLimit)
- break;
- // Prepare the header parser
- this._hparser = hparser;
- if (start === end)
- break;
- // Process the remaining data as a header
- continue retrydata;
- } else {
- // We saw something other than LF, so put the CR we consumed
- // "back"
- const writecb = this._writecb;
- this._writecb = noop;
- ssCb(false, BUF_CR, 0, 1, false);
- this._writecb = writecb;
- }
- }
- }
- if (!skipPart) {
- if (this._fileStream) {
- let chunk;
- const actualLen = Math.min(end - start, fileSizeLimit - fileSize);
- if (!isDataSafe) {
- chunk = Buffer.allocUnsafe(actualLen);
- data.copy(chunk, 0, start, start + actualLen);
- } else {
- chunk = data.slice(start, start + actualLen);
- }
- fileSize += chunk.length;
- if (fileSize === fileSizeLimit) {
- if (chunk.length > 0)
- this._fileStream.push(chunk);
- this._fileStream.emit('limit');
- this._fileStream.truncated = true;
- skipPart = true;
- } else if (!this._fileStream.push(chunk)) {
- if (this._writecb)
- this._fileStream._readcb = this._writecb;
- this._writecb = null;
- }
- } else if (field !== undefined) {
- let chunk;
- const actualLen = Math.min(
- end - start,
- fieldSizeLimit - fieldSize
- );
- if (!isDataSafe) {
- chunk = Buffer.allocUnsafe(actualLen);
- data.copy(chunk, 0, start, start + actualLen);
- } else {
- chunk = data.slice(start, start + actualLen);
- }
- fieldSize += actualLen;
- field.push(chunk);
- if (fieldSize === fieldSizeLimit) {
- skipPart = true;
- partTruncated = true;
- }
- }
- }
- break;
- }
- if (isMatch) {
- matchPostBoundary = 1;
- if (this._fileStream) {
- // End the active file stream if the previous part was a file
- this._fileStream.push(null);
- this._fileStream = null;
- } else if (field !== undefined) {
- let data;
- switch (field.length) {
- case 0:
- data = '';
- break;
- case 1:
- data = convertToUTF8(field[0], partCharset, 0);
- break;
- default:
- data = convertToUTF8(
- Buffer.concat(field, fieldSize),
- partCharset,
- 0
- );
- }
- field = undefined;
- fieldSize = 0;
- this.emit(
- 'field',
- partName,
- data,
- { nameTruncated: false,
- valueTruncated: partTruncated,
- encoding: partEncoding,
- mimeType: partType }
- );
- }
- if (++parts === partsLimit)
- this.emit('partsLimit');
- }
- };
- this._bparser = new StreamSearch(`\r\n--${boundary}`, ssCb);
- this._writecb = null;
- this._finalcb = null;
- // Just in case there is no preamble
- this.write(BUF_CRLF);
- }
- static detect(conType) {
- return (conType.type === 'multipart' && conType.subtype === 'form-data');
- }
- _write(chunk, enc, cb) {
- this._writecb = cb;
- this._bparser.push(chunk, 0);
- if (this._writecb)
- callAndUnsetCb(this);
- }
- _destroy(err, cb) {
- this._hparser = null;
- this._bparser = ignoreData;
- if (!err)
- err = checkEndState(this);
- const fileStream = this._fileStream;
- if (fileStream) {
- this._fileStream = null;
- fileStream.destroy(err);
- }
- cb(err);
- }
- _final(cb) {
- this._bparser.destroy();
- if (!this._complete)
- return cb(new Error('Unexpected end of form'));
- if (this._fileEndsLeft)
- this._finalcb = finalcb.bind(null, this, cb);
- else
- finalcb(this, cb);
- }
- }
- function finalcb(self, cb, err) {
- if (err)
- return cb(err);
- err = checkEndState(self);
- cb(err);
- }
- function checkEndState(self) {
- if (self._hparser)
- return new Error('Malformed part header');
- const fileStream = self._fileStream;
- if (fileStream) {
- self._fileStream = null;
- fileStream.destroy(new Error('Unexpected end of file'));
- }
- if (!self._complete)
- return new Error('Unexpected end of form');
- }
- const TOKEN = [
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0,
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
- 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1,
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- ];
- const FIELD_VCHAR = [
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
- ];
- module.exports = Multipart;
|