@poppinss/multiparty 2.0.1 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +4 -1
  2. package/index.js +82 -26
  3. package/package.json +3 -6
  4. package/HISTORY.md +0 -255
package/README.md CHANGED
@@ -84,6 +84,9 @@ Creates a new form. Options:
84
84
  * `maxFilesSize` - Only relevant when `autoFiles` is `true`. Limits the
85
85
  total bytes accepted for all files combined. If this value is exceeded,
86
86
  an `error` event is emitted. The default is `Infinity`.
87
+ * `maxHeaderSize` - Limits the size of header names and values for each part.
88
+ If this value is exceeded, an `error` event is emitted. The default size is
89
+ 16KB.
87
90
  * `autoFields` - Enables `field` events and disables `part` events for fields.
88
91
  This is automatically set to `true` if you add a `field` listener.
89
92
  * `autoFiles` - Enables `file` events and disables `part` events for files.
@@ -271,5 +274,5 @@ stream uploads to disk for you.
271
274
 
272
275
  [npm-version-image]: https://img.shields.io/npm/v/@poppinss/multiparty?style=for-the-badge
273
276
 
274
- [gh-workflow-image]: https://img.shields.io/github/workflow/status/poppinss/multiparty/test?style=for-the-badge
277
+ [gh-workflow-image]: https://img.shields.io/github/actions/workflow/status/poppinss/multiparty/test.yml?style=for-the-badge
275
278
  [gh-workflow-url]: https://github.com/poppinss/multiparty/actions/workflows/test.yml "Github action"
package/index.js CHANGED
@@ -9,13 +9,12 @@
9
9
  'use strict'
10
10
 
11
11
  var createError = require('http-errors')
12
- var uid = require('uid-safe')
13
12
  var stream = require('stream');
14
13
  var util = require('util');
15
14
  var fs = require('fs');
16
15
  var path = require('path');
17
16
  var os = require('os');
18
- var Buffer = require('safe-buffer').Buffer
17
+ var crypto = require('crypto');
19
18
  var StringDecoder = require('string_decoder').StringDecoder;
20
19
 
21
20
  var START = 0;
@@ -40,14 +39,28 @@ var A = 97;
40
39
  var Z = 122;
41
40
 
42
41
  var CONTENT_TYPE_RE = /^multipart\/(?:form-data|related)(?:;|$)/i;
43
- var CONTENT_TYPE_PARAM_RE = /;\s*([^=]+)=(?:"([^"]+)"|([^;]+))/gi;
42
+ var CONTENT_TYPE_BOUNDARY_RE = /;[ \t]*boundary=(?:"([^"]*)"|([^;]+))/gi;
44
43
  var FILE_EXT_RE = /(\.[_\-a-zA-Z0-9]{0,16})[\S\s]*/;
45
- var FILENAME_PARAM_RE = /\bfilename=(?:"(.*?)"|([!#$%&'*+.0-9A-Z^_`a-z|~-]+))($|; )/i
44
+ var FILENAME_PARAM_RE = /;[ \t]*filename=(?:"((?:[^"]|"(?![ \t]*(?:;|$)))*)"|([^;]+))/i
46
45
  var LAST_BOUNDARY_SUFFIX_LEN = 4; // --\r\n
47
- var NAME_PARAM_RE = /\bname=(?:"([^"]+)"|([!#$%&'*+.0-9A-Z^_`a-z|~-]+))/i
46
+ var NAME_PARAM_RE = /;[ \t]*name=(?:"([^"]*)"|([^;]+))/i
48
47
 
49
48
  exports.Form = Form;
50
49
 
50
+ /**
51
+ * Generates a URL safe random string of a given size
52
+ */
53
+ function randomUid(size) {
54
+ var bits = (size + 1) * 6
55
+ var buffer = crypto.randomBytes(Math.ceil(bits / 8))
56
+ return Buffer.from(buffer)
57
+ .toString('base64')
58
+ .replace(/\+/g, '-')
59
+ .replace(/\//g, '_')
60
+ .replace(/=/g, '')
61
+ .slice(0, size)
62
+ }
63
+
51
64
  util.inherits(Form, stream.Writable);
52
65
  function Form(options) {
53
66
  var opts = options || {}
@@ -62,6 +75,7 @@ function Form(options) {
62
75
  self.maxFields = opts.maxFields || 1000
63
76
  self.maxFieldsSize = opts.maxFieldsSize || 2 * 1024 * 1024
64
77
  self.maxFilesSize = opts.maxFilesSize || Infinity
78
+ self.maxHeaderSize = opts.maxHeaderSize || 16 * 1024
65
79
  self.uploadDir = opts.uploadDir || os.tmpdir()
66
80
  self.encoding = opts.encoding || 'utf8'
67
81
 
@@ -127,12 +141,24 @@ Form.prototype.parse = function(req, cb) {
127
141
  });
128
142
  });
129
143
  self.on('field', function(name, value) {
130
- var fieldsArray = fields[name] || (fields[name] = []);
131
- fieldsArray.push(value);
144
+ var fieldsArray = Object.prototype.hasOwnProperty.call(fields, name)
145
+ ? fields[name]
146
+ : undefined;
147
+ if (Array.isArray(fieldsArray)) {
148
+ fieldsArray.push(value);
149
+ } else {
150
+ fields[name] = [value]
151
+ }
132
152
  });
133
153
  self.on('file', function(name, file) {
134
- var filesArray = files[name] || (files[name] = []);
135
- filesArray.push(file);
154
+ var filesArray = Object.prototype.hasOwnProperty.call(files, name)
155
+ ? files[name]
156
+ : undefined;
157
+ if (Array.isArray(filesArray)) {
158
+ filesArray.push(file);
159
+ } else {
160
+ files[name] = [file]
161
+ }
136
162
  });
137
163
  self.on('close', function() {
138
164
  end(function() {
@@ -172,10 +198,9 @@ Form.prototype.parse = function(req, cb) {
172
198
  }
173
199
 
174
200
  var boundary;
175
- CONTENT_TYPE_PARAM_RE.lastIndex = m.index + m[0].length - 1;
176
- while ((m = CONTENT_TYPE_PARAM_RE.exec(contentType))) {
177
- if (m[1].toLowerCase() !== 'boundary') continue;
178
- boundary = m[2] || m[3];
201
+ CONTENT_TYPE_BOUNDARY_RE.lastIndex = m.index + m[0].length - 1;
202
+ while ((m = CONTENT_TYPE_BOUNDARY_RE.exec(contentType))) {
203
+ boundary = m[1] || m[2];
179
204
  break;
180
205
  }
181
206
 
@@ -292,6 +317,7 @@ Form.prototype._write = function(buffer, encoding, cb) {
292
317
  return;
293
318
  }
294
319
  self.onParseHeaderField(buffer.slice(self.headerFieldMark, i));
320
+ if (self.error) return;
295
321
  self.headerFieldMark = null;
296
322
  state = HEADER_VALUE_START;
297
323
  break;
@@ -312,6 +338,7 @@ Form.prototype._write = function(buffer, encoding, cb) {
312
338
  case HEADER_VALUE:
313
339
  if (c === CR) {
314
340
  self.onParseHeaderValue(buffer.slice(self.headerValueMark, i));
341
+ if (self.error) return;
315
342
  self.headerValueMark = null;
316
343
  self.onParseHeaderEnd();
317
344
  state = HEADER_VALUE_ALMOST_DONE;
@@ -447,10 +474,20 @@ Form.prototype.onParsePartBegin = function() {
447
474
  }
448
475
 
449
476
  Form.prototype.onParseHeaderField = function(b) {
477
+ this.partHeadersSize += b.length;
478
+ if (this.partHeadersSize > this.maxHeaderSize) {
479
+ this.handleError(createError(413, 'maxHeaderSize ' + this.maxHeaderSize + ' exceeded'));
480
+ return;
481
+ }
450
482
  this.headerField += this.headerFieldDecoder.write(b);
451
483
  }
452
484
 
453
485
  Form.prototype.onParseHeaderValue = function(b) {
486
+ this.partHeadersSize += b.length;
487
+ if (this.partHeadersSize > this.maxHeaderSize) {
488
+ this.handleError(createError(413, 'maxHeaderSize ' + this.maxHeaderSize + ' exceeded'));
489
+ return;
490
+ }
454
491
  this.headerValue += this.headerValueDecoder.write(b);
455
492
  }
456
493
 
@@ -476,7 +513,13 @@ Form.prototype.onParseHeaderEnd = function() {
476
513
 
477
514
  Form.prototype.onParsePartData = function(b) {
478
515
  if (this.partTransferEncoding === 'base64') {
479
- this.backpressure = ! this.destStream.write(b.toString('ascii'), 'base64');
516
+ // Count base64 characters, not whitespace, and retain incomplete groups.
517
+ var data = this.partRemainder + b.toString('binary').replace(/[\t\n\v\f\r ]/g, '');
518
+ var length = data.length - (data.length % 4);
519
+ this.partRemainder = data.slice(length);
520
+ if (length > 0) {
521
+ this.backpressure = ! this.destStream.write(data.slice(0, length), this.partTransferEncoding);
522
+ }
480
523
  } else {
481
524
  this.backpressure = ! this.destStream.write(b);
482
525
  }
@@ -486,8 +529,10 @@ Form.prototype.onParsePartEnd = function() {
486
529
  if (this.destStream) {
487
530
  flushWriteCbs(this);
488
531
  var s = this.destStream;
532
+ var remainder = this.partRemainder;
533
+ var encoding = this.partTransferEncoding;
489
534
  process.nextTick(function() {
490
- s.end();
535
+ s.end(remainder, encoding);
491
536
  });
492
537
  }
493
538
  clearPartVars(this);
@@ -742,15 +787,16 @@ function handleField(self, fieldStream) {
742
787
  self.handleError(err);
743
788
  });
744
789
  fieldStream.on('readable', function() {
745
- var buffer = fieldStream.read();
746
- if (!buffer) return;
790
+ var buffer;
791
+ while ((buffer = fieldStream.read()) !== null) {
792
+ self.totalFieldSize += buffer.length;
793
+ if (self.totalFieldSize > self.maxFieldsSize) {
794
+ self.handleError(createError(413, 'maxFieldsSize ' + self.maxFieldsSize + ' exceeded'));
795
+ return;
796
+ }
747
797
 
748
- self.totalFieldSize += buffer.length;
749
- if (self.totalFieldSize > self.maxFieldsSize) {
750
- self.handleError(createError(413, 'maxFieldsSize ' + self.maxFieldsSize + ' exceeded'));
751
- return;
798
+ value += decoder.write(buffer);
752
799
  }
753
- value += decoder.write(buffer);
754
800
  });
755
801
 
756
802
  fieldStream.on('end', function() {
@@ -783,9 +829,11 @@ function hasListeners (emitter, type) {
783
829
 
784
830
  function clearPartVars(self) {
785
831
  self.partHeaders = {};
832
+ self.partHeadersSize = 0;
786
833
  self.partName = null;
787
834
  self.partFilename = null;
788
835
  self.partTransferEncoding = 'binary';
836
+ self.partRemainder = '';
789
837
  self.destStream = null;
790
838
 
791
839
  self.headerFieldDecoder = new StringDecoder(self.encoding);
@@ -819,16 +867,16 @@ function setUpParser(self, boundary) {
819
867
 
820
868
  function uploadPath(baseDir, filename) {
821
869
  var ext = path.extname(filename).replace(FILE_EXT_RE, '$1');
822
- var name = uid.sync(18) + ext
870
+ var name = randomUid(18) + ext
823
871
  return path.join(baseDir, name);
824
872
  }
825
873
 
826
874
  function parseFilename(headerValue) {
827
875
  var m = FILENAME_PARAM_RE.exec(headerValue)
828
876
  if (!m) {
829
- m = headerValue.match(/\bfilename\*=utf-8''(.*?)($|; )/i)
877
+ m = headerValue.match(/;[ \t]*filename\*=utf-8''([^;]*)/i)
830
878
  if (m) {
831
- m[1] = decodeURI(m[1]);
879
+ m[1] = tryDecode(m[1]);
832
880
  } else {
833
881
  return;
834
882
  }
@@ -836,7 +884,7 @@ function parseFilename(headerValue) {
836
884
 
837
885
  var filename = m[1] || m[2] || '';
838
886
  filename = filename.replace(/%22|\\"/g, '"');
839
- filename = filename.replace(/&#([\d]{4});/g, function(m, code) {
887
+ filename = filename.replace(/&#([0-9]{1,5});/g, function(m, code) {
840
888
  return String.fromCharCode(code);
841
889
  });
842
890
  return filename.substr(filename.lastIndexOf('\\') + 1);
@@ -845,3 +893,11 @@ function parseFilename(headerValue) {
845
893
  function lower(c) {
846
894
  return c | 0x20;
847
895
  }
896
+
897
+ function tryDecode(value) {
898
+ try {
899
+ return decodeURIComponent(value);
900
+ } catch (e) {
901
+ return undefined;
902
+ }
903
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poppinss/multiparty",
3
- "version": "2.0.1",
3
+ "version": "3.0.1",
4
4
  "description": "multipart/form-data parser which supports streaming",
5
5
  "main": "index.js",
6
6
  "contributors": [
@@ -16,15 +16,13 @@
16
16
  "s3"
17
17
  ],
18
18
  "dependencies": {
19
- "http-errors": "^2.0.0",
20
- "safe-buffer": "5.2.1",
21
- "uid-safe": "2.1.5"
19
+ "http-errors": "^2.0.0"
22
20
  },
23
21
  "devDependencies": {
24
22
  "eslint": "^8.9.0",
25
23
  "eslint-plugin-markdown": "2.2.1",
26
24
  "mkdirp": "^1.0.4",
27
- "mocha": "^9.2.1",
25
+ "mocha": "^10.8.2",
28
26
  "nyc": "15.1.0",
29
27
  "pend": "1.2.0",
30
28
  "require-all": "3.0.0",
@@ -32,7 +30,6 @@
32
30
  "superagent": "^7.1.1"
33
31
  },
34
32
  "files": [
35
- "HISTORY.md",
36
33
  "LICENSE",
37
34
  "README.md",
38
35
  "index.js"
package/HISTORY.md DELETED
@@ -1,255 +0,0 @@
1
- unreleased
2
- ==========
3
-
4
- * Fix form parsing when no `part` event listener added
5
-
6
- 4.2.3 / 2022-01-20
7
- ==================
8
-
9
- * Fix handling of unquoted values in `Content-Disposition`
10
- * deps: http-errors@~1.8.1
11
- - deps: toidentifier@1.0.1
12
-
13
- 4.2.2 / 2020-07-27
14
- ==================
15
-
16
- * Fix empty files on Node.js 14.x
17
- * Fix form emitting aborted error after close
18
- * Replace `fd-slicer` module with internal transform stream
19
- * deps: http-errors@~1.8.0
20
- - Fix error creating objects in some environments
21
- - deps: inherits@2.0.4
22
- - deps: setprototypeof@1.2.0
23
- * deps: safe-buffer@5.2.1
24
-
25
- 4.2.1 / 2018-08-12
26
- ==================
27
-
28
- * Use `uid-safe` module to for temp file names
29
- * deps: fd-slicer@1.1.0
30
- * deps: http-errors@~1.7.0
31
-
32
- 4.2.0 / 2018-07-30
33
- ==================
34
-
35
- * Use `http-errors` for raised errors
36
- * Use `random-bytes` module for polyfill
37
- * perf: remove parameter reassignment
38
-
39
- 4.1.4 / 2018-05-11
40
- ==================
41
-
42
- * Fix file extension filtering stopping on certain whitespace characters
43
- * Use `safe-buffer` for improved API safety
44
- * perf: enable strict mode
45
-
46
- 4.1.3 / 2017-01-22
47
- ==================
48
-
49
- * Use `os.tmpdir()` instead of `os.tmpDir()`
50
- * deps: fd-slicer@1.0.1
51
-
52
- 4.1.2 / 2015-05-09
53
- ==================
54
-
55
- * Do not emit error on part prior to emitting part
56
- * Fix filename with quotes truncating from certain clients
57
-
58
- 4.1.1 / 2015-01-18
59
- ==================
60
-
61
- * Do not clobber existing temporary files
62
-
63
- 4.1.0 / 2014-12-04
64
- ==================
65
-
66
- * Add `statusCode` field to HTTP-related errors
67
- * deps: fd-slicer@1.0.0
68
-
69
- 4.0.0 / 2014-10-14
70
- ==================
71
-
72
- * `part` events for fields no longer fire if `autoFields` is on
73
- * `part` events for files no longer fire if `autoFiles` is on
74
- * `field`, `file`, and `part` events are guaranteed to emit in the
75
- correct order - the order that the user places the parts in the
76
- request. Each `part` `end` event is guaranteed to emit before the
77
- next `part` event is emitted.
78
- * Drop Node.js 0.8.x support
79
- * Improve random temp file names
80
- - Now using 18 bytes of randomness instead of 8.
81
- * More robust `maxFilesSize` implementation
82
- - Before it was possible for race conditions to cause more than
83
- `maxFilesSize` bytes to get written to disk. That is now fixed.
84
- * Now `part` objects emit `error` events
85
- - This makes streaming work better since the part stream will emit
86
- an error when it is no longer streaming.
87
- * Remove support for generating the hash digest of a part
88
- - If you want this, do it in your own code.
89
- * Remove undocumented `ws` property from `file` objects
90
- * Require the close boundary
91
- - This makes multiparty more RFC-compliant and makes some invalid
92
- requests which used to work, now emit an error instead.
93
-
94
- 3.3.2 / 2014-08-07
95
- ==================
96
-
97
- * Do not invoke callback after close
98
- * Share callback ending logic between error and close
99
-
100
- 3.3.1 / 2014-07-22
101
- ==================
102
-
103
- * Remove problematic test fixtures
104
-
105
- 3.3.0 / 2014-07-03
106
- ==================
107
-
108
- * Always emit close after all parts ended
109
-
110
- 3.2.10 / 2014-07-03
111
- ===================
112
-
113
- * Fix callback hang in node.js 0.8 on errors
114
- * Remove execute bit from files
115
-
116
- 3.2.9 / 2014-06-16
117
- ==================
118
-
119
- * Fix attaching error listeners directly after form.parse
120
- * Fix to not synchronously invoke callback to form.parse on error
121
-
122
- 3.2.8 / 2014-06-01
123
- ==================
124
-
125
- * Fix developer accidentally corrupting data
126
- * Fix handling epilogue in a separate chunk
127
- * Fix initial check errors to use supplied callback
128
-
129
- 3.2.7 / 2014-05-26
130
- ==================
131
-
132
- * Fix errors hanging responses in callback-style
133
-
134
- 3.2.6 / 2014-05-13
135
- ==================
136
-
137
- * Fix `maxFields` to error on field after max
138
-
139
- 3.2.5 / 2014-05-11
140
- ==================
141
-
142
- * Support boundary containing equal sign
143
-
144
- 3.2.4 / 2014-03-26
145
- ==================
146
-
147
- * Keep `part.byteCount` undefined in chunked encoding
148
- * Fix temp files not always cleaned up
149
-
150
- 3.2.3 / 2014-02-20
151
- ==================
152
-
153
- * Improve parsing boundary attribute from `Content-Type`
154
-
155
- 3.2.2 / 2014-01-29
156
- ==================
157
-
158
- * Fix error on empty payloads
159
-
160
- 3.2.1 / 2014-01-27
161
- ==================
162
-
163
- * Fix `maxFilesSize` overcalculation bug
164
-
165
- 3.2.0 / 2014-01-17
166
- ==================
167
-
168
- * Add `maxFilesSize` for `autoFiles`
169
-
170
- 3.1.2 / 2014-01-13
171
- ==================
172
-
173
- * Fix incorrectly using `autoFields` value for `autoFiles`
174
-
175
- 3.1.1 / 2013-12-13
176
- ==================
177
-
178
- * Fix not emitting `close` after all part `end` events
179
-
180
- 3.1.0 / 2013-11-10
181
- ==================
182
-
183
- * Support UTF-8 filename in `Content-Disposition`
184
-
185
- 3.0.0 / 2013-10-25
186
- ==================
187
-
188
- * `form.parse` callback API changed in a compatibility-breaking manner
189
-
190
- 2.2.0 / 2013-10-15
191
- ==================
192
-
193
- * Add callback API to support multiple files with same field name
194
- * Fix assertion crash when max field count is exceeded
195
- * Fix assertion crash when client aborts an invalid request
196
- * Fix assertion crash when `EMFILE` occurrs
197
- * Switch from assertions to only `error` events
198
- * Unpipe the request when an error occurs to save resources
199
- * Update readable-stream to ~1.1.9
200
-
201
- 2.1.9 / 2013-10-06
202
- ==================
203
-
204
- * relax `Content-Type` detection regex
205
-
206
- 2.1.8 / 2013-08-26
207
- ==================
208
-
209
- * Replace deprecated `Buffer.write()`
210
-
211
- 2.1.7 / 2013-05-23
212
- ==================
213
-
214
- * Add repository field to package.json
215
-
216
- 2.1.6 / 2013-04-30
217
- ==================
218
-
219
- * Expose `hash` as an option to `Form`
220
-
221
- 2.1.5 / 2013-04-10
222
- ==================
223
-
224
- * Fix possible `close` event before all temp files are done
225
-
226
- 2.1.4 / 2013-04-09
227
- ==================
228
-
229
- * Fix crash for invalid requests
230
-
231
- 2.1.3 / 2013-04-09
232
- ==================
233
-
234
- * Add `file.size`
235
-
236
- 2.1.2 / 2013-04-08
237
- ==================
238
-
239
- * Add proper backpressure support
240
-
241
- 2.1.1 / 2013-04-05
242
- ==================
243
-
244
- * Add `part.byteCount` and `part.byteOffset`
245
- * Fix uploads larger than 2KB
246
-
247
- 2.1.0 / 2013-04-04
248
- ==================
249
-
250
- * Complete rewrite. See README for changes and new API.
251
-
252
- 2.0.0 / 2013-04-02
253
- ==================
254
-
255
- * Fork and rewrite from `formidable`