UNPKG

18 kBJavaScriptView Raw
1exports.Form = Form;
2
3var stream = require('readable-stream')
4 , util = require('util')
5 , fs = require('fs')
6 , crypto = require('crypto')
7 , path = require('path')
8 , os = require('os')
9 , StringDecoder = require('string_decoder').StringDecoder
10 , StreamCounter = require('stream-counter')
11
12var START = 0
13 , START_BOUNDARY = 1
14 , HEADER_FIELD_START = 2
15 , HEADER_FIELD = 3
16 , HEADER_VALUE_START = 4
17 , HEADER_VALUE = 5
18 , HEADER_VALUE_ALMOST_DONE = 6
19 , HEADERS_ALMOST_DONE = 7
20 , PART_DATA_START = 8
21 , PART_DATA = 9
22 , PART_END = 10
23 , CLOSE_BOUNDARY = 11
24 , END = 12
25
26 , LF = 10
27 , CR = 13
28 , SPACE = 32
29 , HYPHEN = 45
30 , COLON = 58
31 , A = 97
32 , Z = 122
33
34var CONTENT_TYPE_RE = /^multipart\/(form-data|related);\s*boundary=(?:"([^"]+)"|([^;]+))$/i;
35var FILE_EXT_RE = /(\.[_\-a-zA-Z0-9]{0,16}).*/;
36var LAST_BOUNDARY_SUFFIX_LEN = 4; // --\r\n
37
38util.inherits(Form, stream.Writable);
39function Form(options) {
40 var self = this;
41 stream.Writable.call(self);
42
43 options = options || {};
44
45 self.error = null;
46 self.finished = false;
47
48 self.autoFields = !!options.autoFields;
49 self.autoFiles = !!options.autoFiles;
50
51 self.maxFields = options.maxFields || 1000;
52 self.maxFieldsSize = options.maxFieldsSize || 2 * 1024 * 1024;
53 self.maxFilesSize = options.maxFilesSize || Infinity;
54 self.uploadDir = options.uploadDir || os.tmpDir();
55 self.encoding = options.encoding || 'utf8';
56 self.hash = options.hash || false;
57
58 self.bytesReceived = 0;
59 self.bytesExpected = null;
60
61 self.openedFiles = [];
62 self.totalFieldSize = 0;
63 self.totalFieldCount = 0;
64 self.totalFileSize = 0;
65 self.flushing = 0;
66
67 self.backpressure = false;
68 self.writeCbs = [];
69
70 if (options.boundary) setUpParser(self, options.boundary);
71
72 self.on('newListener', function(eventName) {
73 if (eventName === 'file') {
74 self.autoFiles = true;
75 } else if (eventName === 'field') {
76 self.autoFields = true;
77 }
78 });
79}
80
81Form.prototype.parse = function(req, cb) {
82 var self = this;
83
84 // if the user supplies a callback, this implies autoFields and autoFiles
85 if (cb) {
86 self.autoFields = true;
87 self.autoFiles = true;
88 }
89
90 self.handleError = handleError;
91 self.bytesExpected = getBytesExpected(req.headers);
92
93 req.on('error', handleError);
94 req.on('aborted', onReqAborted);
95
96 var contentType = req.headers['content-type'];
97 if (!contentType) {
98 handleError(new Error('missing content-type header'));
99 return;
100 }
101
102 var m = contentType.match(CONTENT_TYPE_RE);
103 if (!m) {
104 handleError(new Error('unrecognized content-type: ' + contentType));
105 return;
106 }
107 var boundary = m[2] || m[3];
108 setUpParser(self, boundary);
109 req.pipe(self);
110
111 if (cb) {
112 var fields = {};
113 var files = {};
114 self.on('error', function(err) {
115 cb(err);
116 });
117 self.on('field', function(name, value) {
118 var fieldsArray = fields[name] || (fields[name] = []);
119 fieldsArray.push(value);
120 });
121 self.on('file', function(name, file) {
122 var filesArray = files[name] || (files[name] = []);
123 filesArray.push(file);
124 });
125 self.on('close', function() {
126 cb(null, fields, files);
127 });
128 }
129
130 function onReqAborted() {
131 self.emit('aborted');
132 handleError(new Error("Request aborted"));
133 }
134
135 function handleError(err) {
136 var first = !self.error;
137 if (first) {
138 self.error = err;
139 req.removeListener('aborted', onReqAborted);
140
141 // welp. 0.8 doesn't support unpipe, too bad so sad.
142 // let's drop support for 0.8 soon.
143 if (req.unpipe) {
144 req.unpipe(self);
145 }
146 }
147
148 self.openedFiles.forEach(function(file) {
149 destroyFile(self, file);
150 });
151 self.openedFiles = [];
152
153 if (first) {
154 self.emit('error', err);
155 }
156 }
157
158};
159
160Form.prototype._write = function(buffer, encoding, cb) {
161 var self = this
162 , i = 0
163 , len = buffer.length
164 , prevIndex = self.index
165 , index = self.index
166 , state = self.state
167 , lookbehind = self.lookbehind
168 , boundary = self.boundary
169 , boundaryChars = self.boundaryChars
170 , boundaryLength = self.boundary.length
171 , boundaryEnd = boundaryLength - 1
172 , bufferLength = buffer.length
173 , c
174 , cl
175
176 for (i = 0; i < len; i++) {
177 c = buffer[i];
178 switch (state) {
179 case START:
180 index = 0;
181 state = START_BOUNDARY;
182 /* falls through */
183 case START_BOUNDARY:
184 if (index === boundaryLength - 2 && c == HYPHEN) {
185 index = 1;
186 state = CLOSE_BOUNDARY;
187 break;
188 } else if (index === boundaryLength - 2) {
189 if (c !== CR) return self.handleError(new Error("Expected CR Received " + c));
190 index++;
191 break;
192 } else if (index === boundaryLength - 1) {
193 if (c !== LF) return self.handleError(new Error("Expected LF Received " + c));
194 index = 0;
195 self.onParsePartBegin();
196 state = HEADER_FIELD_START;
197 break;
198 }
199
200 if (c !== boundary[index+2]) index = -2;
201 if (c === boundary[index+2]) index++;
202 break;
203 case HEADER_FIELD_START:
204 state = HEADER_FIELD;
205 self.headerFieldMark = i;
206 index = 0;
207 /* falls through */
208 case HEADER_FIELD:
209 if (c === CR) {
210 self.headerFieldMark = null;
211 state = HEADERS_ALMOST_DONE;
212 break;
213 }
214
215 index++;
216 if (c === HYPHEN) break;
217
218 if (c === COLON) {
219 if (index === 1) {
220 // empty header field
221 self.handleError(new Error("Empty header field"));
222 return;
223 }
224 self.onParseHeaderField(buffer.slice(self.headerFieldMark, i));
225 self.headerFieldMark = null;
226 state = HEADER_VALUE_START;
227 break;
228 }
229
230 cl = lower(c);
231 if (cl < A || cl > Z) {
232 self.handleError(new Error("Expected alphabetic character, received " + c));
233 return;
234 }
235 break;
236 case HEADER_VALUE_START:
237 if (c === SPACE) break;
238
239 self.headerValueMark = i;
240 state = HEADER_VALUE;
241 /* falls through */
242 case HEADER_VALUE:
243 if (c === CR) {
244 self.onParseHeaderValue(buffer.slice(self.headerValueMark, i));
245 self.headerValueMark = null;
246 self.onParseHeaderEnd();
247 state = HEADER_VALUE_ALMOST_DONE;
248 }
249 break;
250 case HEADER_VALUE_ALMOST_DONE:
251 if (c !== LF) return self.handleError(new Error("Expected LF Received " + c));
252 state = HEADER_FIELD_START;
253 break;
254 case HEADERS_ALMOST_DONE:
255 if (c !== LF) return self.handleError(new Error("Expected LF Received " + c));
256 var err = self.onParseHeadersEnd(i + 1);
257 if (err) return self.handleError(err);
258 state = PART_DATA_START;
259 break;
260 case PART_DATA_START:
261 state = PART_DATA;
262 self.partDataMark = i;
263 /* falls through */
264 case PART_DATA:
265 prevIndex = index;
266
267 if (index === 0) {
268 // boyer-moore derrived algorithm to safely skip non-boundary data
269 i += boundaryEnd;
270 while (i < bufferLength && !(buffer[i] in boundaryChars)) {
271 i += boundaryLength;
272 }
273 i -= boundaryEnd;
274 c = buffer[i];
275 }
276
277 if (index < boundaryLength) {
278 if (boundary[index] === c) {
279 if (index === 0) {
280 self.onParsePartData(buffer.slice(self.partDataMark, i));
281 self.partDataMark = null;
282 }
283 index++;
284 } else {
285 index = 0;
286 }
287 } else if (index === boundaryLength) {
288 index++;
289 if (c === CR) {
290 // CR = part boundary
291 self.partBoundaryFlag = true;
292 } else if (c === HYPHEN) {
293 index = 1;
294 state = CLOSE_BOUNDARY;
295 break;
296 } else {
297 index = 0;
298 }
299 } else if (index - 1 === boundaryLength) {
300 if (self.partBoundaryFlag) {
301 index = 0;
302 if (c === LF) {
303 self.partBoundaryFlag = false;
304 self.onParsePartEnd();
305 self.onParsePartBegin();
306 state = HEADER_FIELD_START;
307 break;
308 }
309 } else {
310 index = 0;
311 }
312 }
313
314 if (index > 0) {
315 // when matching a possible boundary, keep a lookbehind reference
316 // in case it turns out to be a false lead
317 lookbehind[index-1] = c;
318 } else if (prevIndex > 0) {
319 // if our boundary turned out to be rubbish, the captured lookbehind
320 // belongs to partData
321 self.onParsePartData(lookbehind.slice(0, prevIndex));
322 prevIndex = 0;
323 self.partDataMark = i;
324
325 // reconsider the current character even so it interrupted the sequence
326 // it could be the beginning of a new sequence
327 i--;
328 }
329
330 break;
331 case CLOSE_BOUNDARY:
332 if (c !== HYPHEN) return self.handleError(new Error("Expected HYPHEN Received " + c));
333 if (index === 1) {
334 self.onParsePartEnd();
335 self.end();
336 state = END;
337 } else if (index > 1) {
338 return self.handleError(new Error("Parser has invalid state."));
339 }
340 index++;
341 break;
342 case END:
343 break;
344 default:
345 self.handleError(new Error("Parser has invalid state."));
346 return;
347 }
348 }
349
350 if (self.headerFieldMark != null) {
351 self.onParseHeaderField(buffer.slice(self.headerFieldMark));
352 self.headerFieldMark = 0;
353 }
354 if (self.headerValueMark != null) {
355 self.onParseHeaderValue(buffer.slice(self.headerValueMark));
356 self.headerValueMark = 0;
357 }
358 if (self.partDataMark != null) {
359 self.onParsePartData(buffer.slice(self.partDataMark));
360 self.partDataMark = 0;
361 }
362
363 self.index = index;
364 self.state = state;
365
366 self.bytesReceived += buffer.length;
367 self.emit('progress', self.bytesReceived, self.bytesExpected);
368
369 if (self.backpressure) {
370 self.writeCbs.push(cb);
371 } else {
372 cb();
373 }
374};
375
376Form.prototype.onParsePartBegin = function() {
377 clearPartVars(this);
378}
379
380Form.prototype.onParseHeaderField = function(b) {
381 this.headerField += this.headerFieldDecoder.write(b);
382}
383
384Form.prototype.onParseHeaderValue = function(b) {
385 this.headerValue += this.headerValueDecoder.write(b);
386}
387
388Form.prototype.onParseHeaderEnd = function() {
389 this.headerField = this.headerField.toLowerCase();
390 this.partHeaders[this.headerField] = this.headerValue;
391
392 var m;
393 if (this.headerField === 'content-disposition') {
394 if (m = this.headerValue.match(/\bname="([^"]+)"/i)) {
395 this.partName = m[1];
396 }
397 this.partFilename = parseFilename(this.headerValue);
398 } else if (this.headerField === 'content-transfer-encoding') {
399 this.partTransferEncoding = this.headerValue.toLowerCase();
400 }
401
402 this.headerFieldDecoder = new StringDecoder(this.encoding);
403 this.headerField = '';
404 this.headerValueDecoder = new StringDecoder(this.encoding);
405 this.headerValue = '';
406}
407
408Form.prototype.onParsePartData = function(b) {
409 if (this.partTransferEncoding === 'base64') {
410 this.backpressure = ! this.destStream.write(b.toString('ascii'), 'base64');
411 } else {
412 this.backpressure = ! this.destStream.write(b);
413 }
414}
415
416Form.prototype.onParsePartEnd = function() {
417 if (this.destStream) {
418 flushWriteCbs(this);
419 var s = this.destStream;
420 process.nextTick(function() {
421 s.end();
422 });
423 }
424 clearPartVars(this);
425}
426
427Form.prototype.onParseHeadersEnd = function(offset) {
428 var self = this;
429 switch(self.partTransferEncoding){
430 case 'binary':
431 case '7bit':
432 case '8bit':
433 self.partTransferEncoding = 'binary';
434 break;
435
436 case 'base64': break;
437 default:
438 return new Error("unknown transfer-encoding: " + self.partTransferEncoding);
439 }
440
441 self.totalFieldCount += 1;
442 if (self.totalFieldCount >= self.maxFields) {
443 return new Error("maxFields " + self.maxFields + " exceeded.");
444 }
445
446 self.destStream = new stream.PassThrough();
447 self.destStream.on('drain', function() {
448 flushWriteCbs(self);
449 });
450 self.destStream.headers = self.partHeaders;
451 self.destStream.name = self.partName;
452 self.destStream.filename = self.partFilename;
453 self.destStream.byteOffset = self.bytesReceived + offset;
454 var partContentLength = self.destStream.headers['content-length'];
455 self.destStream.byteCount = partContentLength ?
456 parseInt(partContentLength, 10) :
457 (self.bytesExpected - self.destStream.byteOffset -
458 self.boundary.length - LAST_BOUNDARY_SUFFIX_LEN);
459
460 self.emit('part', self.destStream);
461 if (self.destStream.filename == null && self.autoFields) {
462 handleField(self, self.destStream);
463 } else if (self.destStream.filename != null && self.autoFiles) {
464 handleFile(self, self.destStream);
465 }
466}
467
468function flushWriteCbs(self) {
469 self.writeCbs.forEach(function(cb) {
470 process.nextTick(cb);
471 });
472 self.writeCbs = [];
473 self.backpressure = false;
474}
475
476function getBytesExpected(headers) {
477 var contentLength = headers['content-length'];
478 if (contentLength) {
479 return parseInt(contentLength, 10);
480 } else if (headers['transfer-encoding'] == null) {
481 return 0;
482 } else {
483 return null;
484 }
485}
486
487function beginFlush(self) {
488 self.flushing += 1;
489}
490
491function endFlush(self) {
492 self.flushing -= 1;
493 maybeClose(self);
494}
495
496function maybeClose(self) {
497 if (!self.flushing && self.finished && !self.error) {
498 process.nextTick(function() {
499 self.emit('close');
500 });
501 }
502}
503
504function destroyFile(self, file) {
505 if (!file.ws) return;
506 file.ws.destroy();
507 file.ws.removeAllListeners('close');
508 if (typeof file.ws.fd !== 'number') return;
509 file.ws.on('close', function() {
510 fs.unlink(file.path, function(err) {
511 if (!self.error) self.handleError(err);
512 });
513 });
514}
515
516function handleFile(self, fileStream) {
517 beginFlush(self);
518 var file = {
519 fieldName: fileStream.name,
520 originalFilename: fileStream.filename,
521 path: uploadPath(self.uploadDir, fileStream.filename),
522 headers: fileStream.headers,
523 };
524 file.ws = fs.createWriteStream(file.path);
525 self.openedFiles.push(file);
526 fileStream.pipe(file.ws);
527 var counter = new StreamCounter();
528 var seenBytes = 0;
529 fileStream.pipe(counter);
530 var hashWorkaroundStream
531 , hash = null;
532 if (self.hash) {
533 // workaround stream because https://github.com/joyent/node/issues/5216
534 hashWorkaroundStream = stream.Writable();
535 hash = crypto.createHash(self.hash);
536 hashWorkaroundStream._write = function(buffer, encoding, callback) {
537 hash.update(buffer);
538 callback();
539 };
540 fileStream.pipe(hashWorkaroundStream);
541 }
542 counter.on('progress', function() {
543 var deltaBytes = counter.bytes - seenBytes;
544 seenBytes += deltaBytes;
545 self.totalFileSize += deltaBytes;
546 if (self.totalFileSize > self.maxFilesSize) {
547 if (hashWorkaroundStream) fileStream.unpipe(hashWorkaroundStream);
548 fileStream.unpipe(counter);
549 self.handleError(new Error("maxFilesSize " + self.maxFilesSize + " exceeded"));
550 }
551 });
552 file.ws.on('error', function(err) {
553 if (!self.error) self.handleError(err);
554 });
555 file.ws.on('close', function() {
556 if (hash) file.hash = hash.digest('hex');
557 file.size = counter.bytes;
558 self.emit('file', fileStream.name, file);
559 endFlush(self);
560 });
561}
562
563function handleField(self, fieldStream) {
564 var value = '';
565 var decoder = new StringDecoder(self.encoding);
566
567 beginFlush(self);
568 fieldStream.on('readable', function() {
569 var buffer = fieldStream.read();
570 if (!buffer) return;
571
572 self.totalFieldSize += buffer.length;
573 if (self.totalFieldSize > self.maxFieldsSize) {
574 self.handleError(new Error("maxFieldsSize " + self.maxFieldsSize + " exceeded"));
575 return;
576 }
577 value += decoder.write(buffer);
578 });
579
580 fieldStream.on('end', function() {
581 self.emit('field', fieldStream.name, value);
582 endFlush(self);
583 });
584}
585
586function clearPartVars(self) {
587 self.partHeaders = {};
588 self.partName = null;
589 self.partFilename = null;
590 self.partTransferEncoding = 'binary';
591 self.destStream = null;
592
593 self.headerFieldDecoder = new StringDecoder(self.encoding);
594 self.headerField = "";
595 self.headerValueDecoder = new StringDecoder(self.encoding);
596 self.headerValue = "";
597}
598
599function setUpParser(self, boundary) {
600 self.boundary = new Buffer(boundary.length + 4);
601 self.boundary.write('\r\n--', 0, boundary.length + 4, 'ascii');
602 self.boundary.write(boundary, 4, boundary.length, 'ascii');
603 self.lookbehind = new Buffer(self.boundary.length + 8);
604 self.state = START;
605 self.boundaryChars = {};
606 for (var i = 0; i < self.boundary.length; i++) {
607 self.boundaryChars[self.boundary[i]] = true;
608 }
609
610 self.index = null;
611 self.partBoundaryFlag = false;
612
613 self.on('finish', function() {
614 if ((self.state === HEADER_FIELD_START && self.index === 0) ||
615 (self.state === PART_DATA && self.index === self.boundary.length))
616 {
617 self.onParsePartEnd();
618 } else if (self.state !== END) {
619 self.handleError(new Error('stream ended unexpectedly'));
620 }
621 self.finished = true;
622 maybeClose(self);
623 });
624}
625
626function uploadPath(baseDir, filename) {
627 var ext = path.extname(filename).replace(FILE_EXT_RE, '$1');
628 var name = process.pid + '-' +
629 (Math.random() * 0x100000000 + 1).toString(36) + ext;
630 return path.join(baseDir, name);
631}
632
633function parseFilename(headerValue) {
634 var m = headerValue.match(/\bfilename="(.*?)"($|; )/i);
635 if (!m) {
636 m = headerValue.match(/\bfilename\*=utf-8\'\'(.*?)($|; )/i);
637 if (m) {
638 m[1] = decodeURI(m[1]);
639 }
640 else {
641 return;
642 }
643 }
644
645 var filename = m[1].substr(m[1].lastIndexOf('\\') + 1);
646 filename = filename.replace(/%22/g, '"');
647 filename = filename.replace(/&#([\d]{4});/g, function(m, code) {
648 return String.fromCharCode(code);
649 });
650 return filename;
651}
652
653function lower(c) {
654 return c | 0x20;
655}
656