multipart.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653
  1. 'use strict';
  2. const { Readable, Writable } = require('stream');
  3. const StreamSearch = require('streamsearch');
  4. const {
  5. basename,
  6. convertToUTF8,
  7. getDecoder,
  8. parseContentType,
  9. parseDisposition,
  10. } = require('../utils.js');
  11. const BUF_CRLF = Buffer.from('\r\n');
  12. const BUF_CR = Buffer.from('\r');
  13. const BUF_DASH = Buffer.from('-');
  14. function noop() {}
  15. const MAX_HEADER_PAIRS = 2000; // From node
  16. const MAX_HEADER_SIZE = 16 * 1024; // From node (its default value)
  17. const HPARSER_NAME = 0;
  18. const HPARSER_PRE_OWS = 1;
  19. const HPARSER_VALUE = 2;
  20. class HeaderParser {
  21. constructor(cb) {
  22. this.header = Object.create(null);
  23. this.pairCount = 0;
  24. this.byteCount = 0;
  25. this.state = HPARSER_NAME;
  26. this.name = '';
  27. this.value = '';
  28. this.crlf = 0;
  29. this.cb = cb;
  30. }
  31. reset() {
  32. this.header = Object.create(null);
  33. this.pairCount = 0;
  34. this.byteCount = 0;
  35. this.state = HPARSER_NAME;
  36. this.name = '';
  37. this.value = '';
  38. this.crlf = 0;
  39. }
  40. push(chunk, pos, end) {
  41. let start = pos;
  42. while (pos < end) {
  43. switch (this.state) {
  44. case HPARSER_NAME: {
  45. let done = false;
  46. for (; pos < end; ++pos) {
  47. if (this.byteCount === MAX_HEADER_SIZE)
  48. return -1;
  49. ++this.byteCount;
  50. const code = chunk[pos];
  51. if (TOKEN[code] !== 1) {
  52. if (code !== 58/* ':' */)
  53. return -1;
  54. this.name += chunk.latin1Slice(start, pos);
  55. if (this.name.length === 0)
  56. return -1;
  57. ++pos;
  58. done = true;
  59. this.state = HPARSER_PRE_OWS;
  60. break;
  61. }
  62. }
  63. if (!done) {
  64. this.name += chunk.latin1Slice(start, pos);
  65. break;
  66. }
  67. // FALLTHROUGH
  68. }
  69. case HPARSER_PRE_OWS: {
  70. // Skip optional whitespace
  71. let done = false;
  72. for (; pos < end; ++pos) {
  73. if (this.byteCount === MAX_HEADER_SIZE)
  74. return -1;
  75. ++this.byteCount;
  76. const code = chunk[pos];
  77. if (code !== 32/* ' ' */ && code !== 9/* '\t' */) {
  78. start = pos;
  79. done = true;
  80. this.state = HPARSER_VALUE;
  81. break;
  82. }
  83. }
  84. if (!done)
  85. break;
  86. // FALLTHROUGH
  87. }
  88. case HPARSER_VALUE:
  89. switch (this.crlf) {
  90. case 0: // Nothing yet
  91. for (; pos < end; ++pos) {
  92. if (this.byteCount === MAX_HEADER_SIZE)
  93. return -1;
  94. ++this.byteCount;
  95. const code = chunk[pos];
  96. if (FIELD_VCHAR[code] !== 1) {
  97. if (code !== 13/* '\r' */)
  98. return -1;
  99. ++this.crlf;
  100. break;
  101. }
  102. }
  103. this.value += chunk.latin1Slice(start, pos++);
  104. break;
  105. case 1: // Received CR
  106. if (this.byteCount === MAX_HEADER_SIZE)
  107. return -1;
  108. ++this.byteCount;
  109. if (chunk[pos++] !== 10/* '\n' */)
  110. return -1;
  111. ++this.crlf;
  112. break;
  113. case 2: { // Received CR LF
  114. if (this.byteCount === MAX_HEADER_SIZE)
  115. return -1;
  116. ++this.byteCount;
  117. const code = chunk[pos];
  118. if (code === 32/* ' ' */ || code === 9/* '\t' */) {
  119. // Folded value
  120. start = pos;
  121. this.crlf = 0;
  122. } else {
  123. if (++this.pairCount < MAX_HEADER_PAIRS) {
  124. this.name = this.name.toLowerCase();
  125. if (this.header[this.name] === undefined)
  126. this.header[this.name] = [this.value];
  127. else
  128. this.header[this.name].push(this.value);
  129. }
  130. if (code === 13/* '\r' */) {
  131. ++this.crlf;
  132. ++pos;
  133. } else {
  134. // Assume start of next header field name
  135. start = pos;
  136. this.crlf = 0;
  137. this.state = HPARSER_NAME;
  138. this.name = '';
  139. this.value = '';
  140. }
  141. }
  142. break;
  143. }
  144. case 3: { // Received CR LF CR
  145. if (this.byteCount === MAX_HEADER_SIZE)
  146. return -1;
  147. ++this.byteCount;
  148. if (chunk[pos++] !== 10/* '\n' */)
  149. return -1;
  150. // End of header
  151. const header = this.header;
  152. this.reset();
  153. this.cb(header);
  154. return pos;
  155. }
  156. }
  157. break;
  158. }
  159. }
  160. return pos;
  161. }
  162. }
  163. class FileStream extends Readable {
  164. constructor(opts, owner) {
  165. super(opts);
  166. this.truncated = false;
  167. this._readcb = null;
  168. this.once('end', () => {
  169. // We need to make sure that we call any outstanding _writecb() that is
  170. // associated with this file so that processing of the rest of the form
  171. // can continue. This may not happen if the file stream ends right after
  172. // backpressure kicks in, so we force it here.
  173. this._read();
  174. if (--owner._fileEndsLeft === 0 && owner._finalcb) {
  175. const cb = owner._finalcb;
  176. owner._finalcb = null;
  177. // Make sure other 'end' event handlers get a chance to be executed
  178. // before busboy's 'finish' event is emitted
  179. process.nextTick(cb);
  180. }
  181. });
  182. }
  183. _read(n) {
  184. const cb = this._readcb;
  185. if (cb) {
  186. this._readcb = null;
  187. cb();
  188. }
  189. }
  190. }
  191. const ignoreData = {
  192. push: (chunk, pos) => {},
  193. destroy: () => {},
  194. };
  195. function callAndUnsetCb(self, err) {
  196. const cb = self._writecb;
  197. self._writecb = null;
  198. if (err)
  199. self.destroy(err);
  200. else if (cb)
  201. cb();
  202. }
  203. function nullDecoder(val, hint) {
  204. return val;
  205. }
  206. class Multipart extends Writable {
  207. constructor(cfg) {
  208. const streamOpts = {
  209. autoDestroy: true,
  210. emitClose: true,
  211. highWaterMark: (typeof cfg.highWaterMark === 'number'
  212. ? cfg.highWaterMark
  213. : undefined),
  214. };
  215. super(streamOpts);
  216. if (!cfg.conType.params || typeof cfg.conType.params.boundary !== 'string')
  217. throw new Error('Multipart: Boundary not found');
  218. const boundary = cfg.conType.params.boundary;
  219. const paramDecoder = (typeof cfg.defParamCharset === 'string'
  220. && cfg.defParamCharset
  221. ? getDecoder(cfg.defParamCharset)
  222. : nullDecoder);
  223. const defCharset = (cfg.defCharset || 'utf8');
  224. const preservePath = cfg.preservePath;
  225. const fileOpts = {
  226. autoDestroy: true,
  227. emitClose: true,
  228. highWaterMark: (typeof cfg.fileHwm === 'number'
  229. ? cfg.fileHwm
  230. : undefined),
  231. };
  232. const limits = cfg.limits;
  233. const fieldSizeLimit = (limits && typeof limits.fieldSize === 'number'
  234. ? limits.fieldSize
  235. : 1 * 1024 * 1024);
  236. const fileSizeLimit = (limits && typeof limits.fileSize === 'number'
  237. ? limits.fileSize
  238. : Infinity);
  239. const filesLimit = (limits && typeof limits.files === 'number'
  240. ? limits.files
  241. : Infinity);
  242. const fieldsLimit = (limits && typeof limits.fields === 'number'
  243. ? limits.fields
  244. : Infinity);
  245. const partsLimit = (limits && typeof limits.parts === 'number'
  246. ? limits.parts
  247. : Infinity);
  248. let parts = -1; // Account for initial boundary
  249. let fields = 0;
  250. let files = 0;
  251. let skipPart = false;
  252. this._fileEndsLeft = 0;
  253. this._fileStream = undefined;
  254. this._complete = false;
  255. let fileSize = 0;
  256. let field;
  257. let fieldSize = 0;
  258. let partCharset;
  259. let partEncoding;
  260. let partType;
  261. let partName;
  262. let partTruncated = false;
  263. let hitFilesLimit = false;
  264. let hitFieldsLimit = false;
  265. this._hparser = null;
  266. const hparser = new HeaderParser((header) => {
  267. this._hparser = null;
  268. skipPart = false;
  269. partType = 'text/plain';
  270. partCharset = defCharset;
  271. partEncoding = '7bit';
  272. partName = undefined;
  273. partTruncated = false;
  274. let filename;
  275. if (!header['content-disposition']) {
  276. skipPart = true;
  277. return;
  278. }
  279. const disp = parseDisposition(header['content-disposition'][0],
  280. paramDecoder);
  281. if (!disp || disp.type !== 'form-data') {
  282. skipPart = true;
  283. return;
  284. }
  285. if (disp.params) {
  286. if (disp.params.name)
  287. partName = disp.params.name;
  288. if (disp.params['filename*'])
  289. filename = disp.params['filename*'];
  290. else if (disp.params.filename)
  291. filename = disp.params.filename;
  292. if (filename !== undefined && !preservePath)
  293. filename = basename(filename);
  294. }
  295. if (header['content-type']) {
  296. const conType = parseContentType(header['content-type'][0]);
  297. if (conType) {
  298. partType = `${conType.type}/${conType.subtype}`;
  299. if (conType.params && typeof conType.params.charset === 'string')
  300. partCharset = conType.params.charset.toLowerCase();
  301. }
  302. }
  303. if (header['content-transfer-encoding'])
  304. partEncoding = header['content-transfer-encoding'][0].toLowerCase();
  305. if (partType === 'application/octet-stream' || filename !== undefined) {
  306. // File
  307. if (files === filesLimit) {
  308. if (!hitFilesLimit) {
  309. hitFilesLimit = true;
  310. this.emit('filesLimit');
  311. }
  312. skipPart = true;
  313. return;
  314. }
  315. ++files;
  316. if (this.listenerCount('file') === 0) {
  317. skipPart = true;
  318. return;
  319. }
  320. fileSize = 0;
  321. this._fileStream = new FileStream(fileOpts, this);
  322. ++this._fileEndsLeft;
  323. this.emit(
  324. 'file',
  325. partName,
  326. this._fileStream,
  327. { filename,
  328. encoding: partEncoding,
  329. mimeType: partType }
  330. );
  331. } else {
  332. // Non-file
  333. if (fields === fieldsLimit) {
  334. if (!hitFieldsLimit) {
  335. hitFieldsLimit = true;
  336. this.emit('fieldsLimit');
  337. }
  338. skipPart = true;
  339. return;
  340. }
  341. ++fields;
  342. if (this.listenerCount('field') === 0) {
  343. skipPart = true;
  344. return;
  345. }
  346. field = [];
  347. fieldSize = 0;
  348. }
  349. });
  350. let matchPostBoundary = 0;
  351. const ssCb = (isMatch, data, start, end, isDataSafe) => {
  352. retrydata:
  353. while (data) {
  354. if (this._hparser !== null) {
  355. const ret = this._hparser.push(data, start, end);
  356. if (ret === -1) {
  357. this._hparser = null;
  358. hparser.reset();
  359. this.emit('error', new Error('Malformed part header'));
  360. break;
  361. }
  362. start = ret;
  363. }
  364. if (start === end)
  365. break;
  366. if (matchPostBoundary !== 0) {
  367. if (matchPostBoundary === 1) {
  368. switch (data[start]) {
  369. case 45: // '-'
  370. // Try matching '--' after boundary
  371. matchPostBoundary = 2;
  372. ++start;
  373. break;
  374. case 13: // '\r'
  375. // Try matching CR LF before header
  376. matchPostBoundary = 3;
  377. ++start;
  378. break;
  379. default:
  380. matchPostBoundary = 0;
  381. }
  382. if (start === end)
  383. return;
  384. }
  385. if (matchPostBoundary === 2) {
  386. matchPostBoundary = 0;
  387. if (data[start] === 45/* '-' */) {
  388. // End of multipart data
  389. this._complete = true;
  390. this._bparser = ignoreData;
  391. return;
  392. }
  393. // We saw something other than '-', so put the dash we consumed
  394. // "back"
  395. const writecb = this._writecb;
  396. this._writecb = noop;
  397. ssCb(false, BUF_DASH, 0, 1, false);
  398. this._writecb = writecb;
  399. } else if (matchPostBoundary === 3) {
  400. matchPostBoundary = 0;
  401. if (data[start] === 10/* '\n' */) {
  402. ++start;
  403. if (parts >= partsLimit)
  404. break;
  405. // Prepare the header parser
  406. this._hparser = hparser;
  407. if (start === end)
  408. break;
  409. // Process the remaining data as a header
  410. continue retrydata;
  411. } else {
  412. // We saw something other than LF, so put the CR we consumed
  413. // "back"
  414. const writecb = this._writecb;
  415. this._writecb = noop;
  416. ssCb(false, BUF_CR, 0, 1, false);
  417. this._writecb = writecb;
  418. }
  419. }
  420. }
  421. if (!skipPart) {
  422. if (this._fileStream) {
  423. let chunk;
  424. const actualLen = Math.min(end - start, fileSizeLimit - fileSize);
  425. if (!isDataSafe) {
  426. chunk = Buffer.allocUnsafe(actualLen);
  427. data.copy(chunk, 0, start, start + actualLen);
  428. } else {
  429. chunk = data.slice(start, start + actualLen);
  430. }
  431. fileSize += chunk.length;
  432. if (fileSize === fileSizeLimit) {
  433. if (chunk.length > 0)
  434. this._fileStream.push(chunk);
  435. this._fileStream.emit('limit');
  436. this._fileStream.truncated = true;
  437. skipPart = true;
  438. } else if (!this._fileStream.push(chunk)) {
  439. if (this._writecb)
  440. this._fileStream._readcb = this._writecb;
  441. this._writecb = null;
  442. }
  443. } else if (field !== undefined) {
  444. let chunk;
  445. const actualLen = Math.min(
  446. end - start,
  447. fieldSizeLimit - fieldSize
  448. );
  449. if (!isDataSafe) {
  450. chunk = Buffer.allocUnsafe(actualLen);
  451. data.copy(chunk, 0, start, start + actualLen);
  452. } else {
  453. chunk = data.slice(start, start + actualLen);
  454. }
  455. fieldSize += actualLen;
  456. field.push(chunk);
  457. if (fieldSize === fieldSizeLimit) {
  458. skipPart = true;
  459. partTruncated = true;
  460. }
  461. }
  462. }
  463. break;
  464. }
  465. if (isMatch) {
  466. matchPostBoundary = 1;
  467. if (this._fileStream) {
  468. // End the active file stream if the previous part was a file
  469. this._fileStream.push(null);
  470. this._fileStream = null;
  471. } else if (field !== undefined) {
  472. let data;
  473. switch (field.length) {
  474. case 0:
  475. data = '';
  476. break;
  477. case 1:
  478. data = convertToUTF8(field[0], partCharset, 0);
  479. break;
  480. default:
  481. data = convertToUTF8(
  482. Buffer.concat(field, fieldSize),
  483. partCharset,
  484. 0
  485. );
  486. }
  487. field = undefined;
  488. fieldSize = 0;
  489. this.emit(
  490. 'field',
  491. partName,
  492. data,
  493. { nameTruncated: false,
  494. valueTruncated: partTruncated,
  495. encoding: partEncoding,
  496. mimeType: partType }
  497. );
  498. }
  499. if (++parts === partsLimit)
  500. this.emit('partsLimit');
  501. }
  502. };
  503. this._bparser = new StreamSearch(`\r\n--${boundary}`, ssCb);
  504. this._writecb = null;
  505. this._finalcb = null;
  506. // Just in case there is no preamble
  507. this.write(BUF_CRLF);
  508. }
  509. static detect(conType) {
  510. return (conType.type === 'multipart' && conType.subtype === 'form-data');
  511. }
  512. _write(chunk, enc, cb) {
  513. this._writecb = cb;
  514. this._bparser.push(chunk, 0);
  515. if (this._writecb)
  516. callAndUnsetCb(this);
  517. }
  518. _destroy(err, cb) {
  519. this._hparser = null;
  520. this._bparser = ignoreData;
  521. if (!err)
  522. err = checkEndState(this);
  523. const fileStream = this._fileStream;
  524. if (fileStream) {
  525. this._fileStream = null;
  526. fileStream.destroy(err);
  527. }
  528. cb(err);
  529. }
  530. _final(cb) {
  531. this._bparser.destroy();
  532. if (!this._complete)
  533. return cb(new Error('Unexpected end of form'));
  534. if (this._fileEndsLeft)
  535. this._finalcb = finalcb.bind(null, this, cb);
  536. else
  537. finalcb(this, cb);
  538. }
  539. }
  540. function finalcb(self, cb, err) {
  541. if (err)
  542. return cb(err);
  543. err = checkEndState(self);
  544. cb(err);
  545. }
  546. function checkEndState(self) {
  547. if (self._hparser)
  548. return new Error('Malformed part header');
  549. const fileStream = self._fileStream;
  550. if (fileStream) {
  551. self._fileStream = null;
  552. fileStream.destroy(new Error('Unexpected end of file'));
  553. }
  554. if (!self._complete)
  555. return new Error('Unexpected end of form');
  556. }
  557. const TOKEN = [
  558. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  559. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  560. 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0,
  561. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
  562. 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  563. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1,
  564. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  565. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0,
  566. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  567. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  568. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  569. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  570. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  571. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  572. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  573. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  574. ];
  575. const FIELD_VCHAR = [
  576. 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0,
  577. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  578. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  579. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  580. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  581. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  582. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  583. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
  584. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  585. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  586. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  587. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  588. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  589. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  590. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  591. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  592. ];
  593. module.exports = Multipart;