1 | exports.Form = Form;
|
2 |
|
3 | var 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 |
|
12 | var 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 |
|
34 | var CONTENT_TYPE_RE = /^multipart\/(form-data|related);\s*boundary=(?:"([^"]+)"|([^;]+))$/i;
|
35 | var FILE_EXT_RE = /(\.[_\-a-zA-Z0-9]{0,16}).*/;
|
36 | var LAST_BOUNDARY_SUFFIX_LEN = 4;
|
37 |
|
38 | util.inherits(Form, stream.Writable);
|
39 | function 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 |
|
81 | Form.prototype.parse = function(req, cb) {
|
82 | var self = this;
|
83 |
|
84 |
|
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 |
|
142 |
|
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 |
|
160 | Form.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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
264 | case PART_DATA:
|
265 | prevIndex = index;
|
266 |
|
267 | if (index === 0) {
|
268 |
|
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 |
|
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 |
|
316 |
|
317 | lookbehind[index-1] = c;
|
318 | } else if (prevIndex > 0) {
|
319 |
|
320 |
|
321 | self.onParsePartData(lookbehind.slice(0, prevIndex));
|
322 | prevIndex = 0;
|
323 | self.partDataMark = i;
|
324 |
|
325 |
|
326 |
|
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 |
|
376 | Form.prototype.onParsePartBegin = function() {
|
377 | clearPartVars(this);
|
378 | }
|
379 |
|
380 | Form.prototype.onParseHeaderField = function(b) {
|
381 | this.headerField += this.headerFieldDecoder.write(b);
|
382 | }
|
383 |
|
384 | Form.prototype.onParseHeaderValue = function(b) {
|
385 | this.headerValue += this.headerValueDecoder.write(b);
|
386 | }
|
387 |
|
388 | Form.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 |
|
408 | Form.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 |
|
416 | Form.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 |
|
427 | Form.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 |
|
468 | function flushWriteCbs(self) {
|
469 | self.writeCbs.forEach(function(cb) {
|
470 | process.nextTick(cb);
|
471 | });
|
472 | self.writeCbs = [];
|
473 | self.backpressure = false;
|
474 | }
|
475 |
|
476 | function 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 |
|
487 | function beginFlush(self) {
|
488 | self.flushing += 1;
|
489 | }
|
490 |
|
491 | function endFlush(self) {
|
492 | self.flushing -= 1;
|
493 | maybeClose(self);
|
494 | }
|
495 |
|
496 | function maybeClose(self) {
|
497 | if (!self.flushing && self.finished && !self.error) {
|
498 | process.nextTick(function() {
|
499 | self.emit('close');
|
500 | });
|
501 | }
|
502 | }
|
503 |
|
504 | function 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 |
|
516 | function 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 |
|
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 |
|
563 | function 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 |
|
586 | function 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 |
|
599 | function 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 |
|
626 | function 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 |
|
633 | function 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 |
|
653 | function lower(c) {
|
654 | return c | 0x20;
|
655 | }
|
656 |
|