multipart.js 5.8 KB

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