multipart.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. /* eslint-disable no-underscore-dangle */
  2. import { Stream } from 'node:stream';
  3. import MultipartParser from '../parsers/Multipart.js';
  4. import * as errors from '../FormidableError.js';
  5. import FormidableError from '../FormidableError.js';
  6. export const multipartType = 'multipart';
  7. // the `options` is also available through the `options` / `formidable.options`
  8. export default function plugin(formidable, options) {
  9. // the `this` context is always formidable, as the first argument of a plugin
  10. // but this allows us to customize/test each plugin
  11. /* istanbul ignore next */
  12. const self = this || formidable;
  13. // NOTE: we (currently) support both multipart/form-data and multipart/related
  14. const multipart = /multipart/i.test(self.headers['content-type']);
  15. if (multipart) {
  16. const m = self.headers['content-type'].match(
  17. /boundary=(?:"([^"]+)"|([^;]+))/i,
  18. );
  19. if (m) {
  20. const initMultipart = createInitMultipart(m[1] || m[2]);
  21. initMultipart.call(self, self, options); // lgtm [js/superfluous-trailing-arguments]
  22. } else {
  23. const err = new FormidableError(
  24. 'bad content-type header, no multipart boundary',
  25. errors.missingMultipartBoundary,
  26. 400,
  27. );
  28. self._error(err);
  29. }
  30. }
  31. return self;
  32. }
  33. // Note that it's a good practice (but it's up to you) to use the `this.options` instead
  34. // of the passed `options` (second) param, because when you decide
  35. // to test the plugin you can pass custom `this` context to it (and so `this.options`)
  36. function createInitMultipart(boundary) {
  37. return function initMultipart() {
  38. this.type = multipartType;
  39. const parser = new MultipartParser(this.options);
  40. let headerField;
  41. let headerValue;
  42. let part;
  43. parser.initWithBoundary(boundary);
  44. // eslint-disable-next-line max-statements, consistent-return
  45. parser.on('data', async ({ name, buffer, start, end }) => {
  46. if (name === 'partBegin') {
  47. part = new Stream();
  48. part.readable = true;
  49. part.headers = {};
  50. part.name = null;
  51. part.originalFilename = null;
  52. part.mimetype = null;
  53. part.transferEncoding = this.options.encoding;
  54. part.transferBuffer = '';
  55. headerField = '';
  56. headerValue = '';
  57. } else if (name === 'headerField') {
  58. headerField += buffer.toString(this.options.encoding, start, end);
  59. } else if (name === 'headerValue') {
  60. headerValue += buffer.toString(this.options.encoding, start, end);
  61. } else if (name === 'headerEnd') {
  62. headerField = headerField.toLowerCase();
  63. part.headers[headerField] = headerValue;
  64. // matches either a quoted-string or a token (RFC 2616 section 19.5.1)
  65. const m = headerValue.match(
  66. // eslint-disable-next-line no-useless-escape
  67. /\bname=("([^"]*)"|([^\(\)<>@,;:\\"\/\[\]\?=\{\}\s\t/]+))/i,
  68. );
  69. if (headerField === 'content-disposition') {
  70. if (m) {
  71. part.name = m[2] || m[3] || '';
  72. }
  73. part.originalFilename = this._getFileName(headerValue);
  74. } else if (headerField === 'content-type') {
  75. part.mimetype = headerValue;
  76. } else if (headerField === 'content-transfer-encoding') {
  77. part.transferEncoding = headerValue.toLowerCase();
  78. }
  79. headerField = '';
  80. headerValue = '';
  81. } else if (name === 'headersEnd') {
  82. switch (part.transferEncoding) {
  83. case 'binary':
  84. case '7bit':
  85. case '8bit':
  86. case 'utf-8': {
  87. const dataPropagation = (ctx) => {
  88. if (ctx.name === 'partData') {
  89. part.emit('data', ctx.buffer.slice(ctx.start, ctx.end));
  90. }
  91. };
  92. const dataStopPropagation = (ctx) => {
  93. if (ctx.name === 'partEnd') {
  94. part.emit('end');
  95. parser.off('data', dataPropagation);
  96. parser.off('data', dataStopPropagation);
  97. }
  98. };
  99. parser.on('data', dataPropagation);
  100. parser.on('data', dataStopPropagation);
  101. break;
  102. }
  103. case 'base64': {
  104. const dataPropagation = (ctx) => {
  105. if (ctx.name === 'partData') {
  106. part.transferBuffer += ctx.buffer
  107. .slice(ctx.start, ctx.end)
  108. .toString('ascii');
  109. /*
  110. four bytes (chars) in base64 converts to three bytes in binary
  111. encoding. So we should always work with a number of bytes that
  112. can be divided by 4, it will result in a number of bytes that
  113. can be divided vy 3.
  114. */
  115. const offset = parseInt(part.transferBuffer.length / 4, 10) * 4;
  116. part.emit(
  117. 'data',
  118. Buffer.from(
  119. part.transferBuffer.substring(0, offset),
  120. 'base64',
  121. ),
  122. );
  123. part.transferBuffer = part.transferBuffer.substring(offset);
  124. }
  125. };
  126. const dataStopPropagation = (ctx) => {
  127. if (ctx.name === 'partEnd') {
  128. part.emit('data', Buffer.from(part.transferBuffer, 'base64'));
  129. part.emit('end');
  130. parser.off('data', dataPropagation);
  131. parser.off('data', dataStopPropagation);
  132. }
  133. };
  134. parser.on('data', dataPropagation);
  135. parser.on('data', dataStopPropagation);
  136. break;
  137. }
  138. default:
  139. return this._error(
  140. new FormidableError(
  141. 'unknown transfer-encoding',
  142. errors.unknownTransferEncoding,
  143. 501,
  144. ),
  145. );
  146. }
  147. this._parser.pause();
  148. await this.onPart(part);
  149. this._parser.resume();
  150. } else if (name === 'end') {
  151. this.ended = true;
  152. this._maybeEnd();
  153. }
  154. });
  155. this._parser = parser;
  156. };
  157. }