You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					485 lines
				
				9.7 KiB
			
		
		
			
		
	
	
					485 lines
				
				9.7 KiB
			| 
											3 years ago
										 | /** | ||
|  |  * Parted (https://github.com/chjj/parted)
 | ||
|  |  * A streaming multipart state parser. | ||
|  |  * Copyright (c) 2011, Christopher Jeffrey. (MIT Licensed) | ||
|  |  */ | ||
|  | 
 | ||
|  | var fs = require('fs') | ||
|  |   , path = require('path') | ||
|  |   , EventEmitter = require('events').EventEmitter | ||
|  |   , StringDecoder = require('string_decoder').StringDecoder | ||
|  |   , set = require('qs').set | ||
|  |   , each = Array.prototype.forEach; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Character Constants | ||
|  |  */ | ||
|  | 
 | ||
|  | var DASH = '-'.charCodeAt(0) | ||
|  |   , CR = '\r'.charCodeAt(0) | ||
|  |   , LF = '\n'.charCodeAt(0) | ||
|  |   , COLON = ':'.charCodeAt(0) | ||
|  |   , SPACE = ' '.charCodeAt(0); | ||
|  | 
 | ||
|  | /** | ||
|  |  * Parser | ||
|  |  */ | ||
|  | 
 | ||
|  | var Parser = function(type, options) { | ||
|  |   if (!(this instanceof Parser)) { | ||
|  |     return new Parser(type, options); | ||
|  |   } | ||
|  | 
 | ||
|  |   EventEmitter.call(this); | ||
|  | 
 | ||
|  |   this.writable = true; | ||
|  |   this.readable = true; | ||
|  | 
 | ||
|  |   this.options = options || {}; | ||
|  | 
 | ||
|  |   var key = grab(type, 'boundary'); | ||
|  |   if (!key) { | ||
|  |     return this._error('No boundary key found.'); | ||
|  |   } | ||
|  | 
 | ||
|  |   this.key = new Buffer('\r\n--' + key); | ||
|  | 
 | ||
|  |   this._key = {}; | ||
|  |   each.call(this.key, function(ch) { | ||
|  |     this._key[ch] = true; | ||
|  |   }, this); | ||
|  | 
 | ||
|  |   this.state = 'start'; | ||
|  |   this.pending = 0; | ||
|  |   this.written = 0; | ||
|  |   this.writtenDisk = 0; | ||
|  |   this.buff = new Buffer(200); | ||
|  | 
 | ||
|  |   this.preamble = true; | ||
|  |   this.epilogue = false; | ||
|  | 
 | ||
|  |   this._reset(); | ||
|  | }; | ||
|  | 
 | ||
|  | Parser.prototype.__proto__ = EventEmitter.prototype; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Parsing | ||
|  |  */ | ||
|  | 
 | ||
|  | Parser.prototype.write = function(data) { | ||
|  |   if (!this.writable | ||
|  |       || this.epilogue) return; | ||
|  | 
 | ||
|  |   try { | ||
|  |     this._parse(data); | ||
|  |   } catch (e) { | ||
|  |     this._error(e); | ||
|  |   } | ||
|  | 
 | ||
|  |   return true; | ||
|  | }; | ||
|  | 
 | ||
|  | Parser.prototype.end = function(data) { | ||
|  |   if (!this.writable) return; | ||
|  | 
 | ||
|  |   if (data) this.write(data); | ||
|  | 
 | ||
|  |   if (!this.epilogue) { | ||
|  |     return this._error('Message underflow.'); | ||
|  |   } | ||
|  | 
 | ||
|  |   return true; | ||
|  | }; | ||
|  | 
 | ||
|  | Parser.prototype._parse = function(data) { | ||
|  |   var i = 0 | ||
|  |     , len = data.length | ||
|  |     , buff = this.buff | ||
|  |     , key = this.key | ||
|  |     , ch | ||
|  |     , val | ||
|  |     , j; | ||
|  | 
 | ||
|  |   for (; i < len; i++) { | ||
|  |     if (this.pos >= 200) { | ||
|  |       return this._error('Potential buffer overflow.'); | ||
|  |     } | ||
|  | 
 | ||
|  |     ch = data[i]; | ||
|  | 
 | ||
|  |     switch (this.state) { | ||
|  |       case 'start': | ||
|  |         switch (ch) { | ||
|  |           case DASH: | ||
|  |             this.pos = 3; | ||
|  |             this.state = 'key'; | ||
|  |             break; | ||
|  |           default: | ||
|  |             break; | ||
|  |         } | ||
|  |         break; | ||
|  |       case 'key': | ||
|  |         if (this.pos === key.length) { | ||
|  |           this.state = 'key_end'; | ||
|  |           i--; | ||
|  |         } else if (ch !== key[this.pos]) { | ||
|  |           if (this.preamble) { | ||
|  |             this.state = 'start'; | ||
|  |             i--; | ||
|  |           } else { | ||
|  |             this.state = 'body'; | ||
|  |             val = this.pos - i; | ||
|  |             if (val > 0) { | ||
|  |               this._write(key.slice(0, val)); | ||
|  |             } | ||
|  |             i--; | ||
|  |           } | ||
|  |         } else { | ||
|  |           this.pos++; | ||
|  |         } | ||
|  |         break; | ||
|  |       case 'key_end': | ||
|  |         switch (ch) { | ||
|  |           case CR: | ||
|  |             this.state = 'key_line_end'; | ||
|  |             break; | ||
|  |           case DASH: | ||
|  |             this.state = 'key_dash_end'; | ||
|  |             break; | ||
|  |           default: | ||
|  |             return this._error('Expected CR or DASH.'); | ||
|  |         } | ||
|  |         break; | ||
|  |       case 'key_line_end': | ||
|  |         switch (ch) { | ||
|  |           case LF: | ||
|  |             if (this.preamble) { | ||
|  |               this.preamble = false; | ||
|  |             } else { | ||
|  |               this._finish(); | ||
|  |             } | ||
|  |             this.state = 'header_name'; | ||
|  |             this.pos = 0; | ||
|  |             break; | ||
|  |           default: | ||
|  |             return this._error('Expected CR.'); | ||
|  |         } | ||
|  |         break; | ||
|  |       case 'key_dash_end': | ||
|  |         switch (ch) { | ||
|  |           case DASH: | ||
|  |             this.epilogue = true; | ||
|  |             this._finish(); | ||
|  |             return; | ||
|  |           default: | ||
|  |             return this._error('Expected DASH.'); | ||
|  |         } | ||
|  |         break; | ||
|  |       case 'header_name': | ||
|  |         switch (ch) { | ||
|  |           case COLON: | ||
|  |             this.header = buff.toString('ascii', 0, this.pos); | ||
|  |             this.pos = 0; | ||
|  |             this.state = 'header_val'; | ||
|  |             break; | ||
|  |           default: | ||
|  |             buff[this.pos++] = ch | 32; | ||
|  |             break; | ||
|  |         } | ||
|  |         break; | ||
|  |       case 'header_val': | ||
|  |         switch (ch) { | ||
|  |           case CR: | ||
|  |             this.state = 'header_val_end'; | ||
|  |             break; | ||
|  |           case SPACE: | ||
|  |             if (this.pos === 0) { | ||
|  |               break; | ||
|  |             } | ||
|  |             ; // FALL-THROUGH
 | ||
|  |           default: | ||
|  |             buff[this.pos++] = ch; | ||
|  |             break; | ||
|  |         } | ||
|  |         break; | ||
|  |       case 'header_val_end': | ||
|  |         switch (ch) { | ||
|  |           case LF: | ||
|  |             val = buff.toString('ascii', 0, this.pos); | ||
|  |             this._header(this.header, val); | ||
|  |             this.pos = 0; | ||
|  |             this.state = 'header_end'; | ||
|  |             break; | ||
|  |           default: | ||
|  |             return this._error('Expected LF.'); | ||
|  |         } | ||
|  |         break; | ||
|  |       case 'header_end': | ||
|  |         switch (ch) { | ||
|  |           case CR: | ||
|  |             this.state = 'head_end'; | ||
|  |             break; | ||
|  |           default: | ||
|  |             this.state = 'header_name'; | ||
|  |             i--; | ||
|  |             break; | ||
|  |         } | ||
|  |         break; | ||
|  |       case 'head_end': | ||
|  |         switch (ch) { | ||
|  |           case LF: | ||
|  |             this.state = 'body'; | ||
|  |             i++; | ||
|  |             if (i >= len) return; | ||
|  |             data = data.slice(i); | ||
|  |             i = -1; | ||
|  |             len = data.length; | ||
|  |             break; | ||
|  |           default: | ||
|  |             return this._error('Expected LF.'); | ||
|  |         } | ||
|  |         break; | ||
|  |       case 'body': | ||
|  |         switch (ch) { | ||
|  |           case CR: | ||
|  |             if (i > 0) { | ||
|  |               this._write(data.slice(0, i)); | ||
|  |             } | ||
|  |             this.pos = 1; | ||
|  |             this.state = 'key'; | ||
|  |             data = data.slice(i); | ||
|  |             i = 0; | ||
|  |             len = data.length; | ||
|  |             break; | ||
|  |           default: | ||
|  |             // boyer-moore-like algorithm
 | ||
|  |             // at felixge's suggestion
 | ||
|  |             while ((j = i + key.length - 1) < len) { | ||
|  |               if (this._key[data[j]]) break; | ||
|  |               i = j; | ||
|  |             } | ||
|  |             break; | ||
|  |         } | ||
|  |         break; | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   if (this.state === 'body') { | ||
|  |     this._write(data); | ||
|  |   } | ||
|  | }; | ||
|  | 
 | ||
|  | Parser.prototype._header = function(name, val) { | ||
|  |   /*if (name === 'content-disposition') { | ||
|  |     this.field = grab(val, 'name'); | ||
|  |     this.file = grab(val, 'filename'); | ||
|  | 
 | ||
|  |     if (this.file) { | ||
|  |       this.data = stream(this.file, this.options.path); | ||
|  |     } else { | ||
|  |       this.decode = new StringDecoder('utf8'); | ||
|  |       this.data = ''; | ||
|  |     } | ||
|  |   }*/ | ||
|  | 
 | ||
|  |   return this.emit('header', name, val); | ||
|  | }; | ||
|  | 
 | ||
|  | Parser.prototype._write = function(data) { | ||
|  |   /*if (this.data == null) { | ||
|  |     return this._error('No disposition.'); | ||
|  |   } | ||
|  | 
 | ||
|  |   if (this.file) { | ||
|  |     this.data.write(data); | ||
|  |     this.writtenDisk += data.length; | ||
|  |   } else { | ||
|  |     this.data += this.decode.write(data); | ||
|  |     this.written += data.length; | ||
|  |   }*/ | ||
|  | 
 | ||
|  |   this.emit('data', data); | ||
|  | }; | ||
|  | 
 | ||
|  | Parser.prototype._reset = function() { | ||
|  |   this.pos = 0; | ||
|  |   this.decode = null; | ||
|  |   this.field = null; | ||
|  |   this.data = null; | ||
|  |   this.file = null; | ||
|  |   this.header = null; | ||
|  | }; | ||
|  | 
 | ||
|  | Parser.prototype._error = function(err) { | ||
|  |   this.destroy(); | ||
|  |   this.emit('error', typeof err === 'string' | ||
|  |     ? new Error(err) | ||
|  |     : err); | ||
|  | }; | ||
|  | 
 | ||
|  | Parser.prototype.destroy = function(err) { | ||
|  |   this.writable = false; | ||
|  |   this.readable = false; | ||
|  |   this._reset(); | ||
|  | }; | ||
|  | 
 | ||
|  | Parser.prototype._finish = function() { | ||
|  |   var self = this | ||
|  |     , field = this.field | ||
|  |     , data = this.data | ||
|  |     , file = this.file | ||
|  |     , part; | ||
|  | 
 | ||
|  |   this.pending++; | ||
|  | 
 | ||
|  |   this._reset(); | ||
|  | 
 | ||
|  |   if (data && data.path) { | ||
|  |     part = data.path; | ||
|  |     data.end(next); | ||
|  |   } else { | ||
|  |     part = data; | ||
|  |     next(); | ||
|  |   } | ||
|  | 
 | ||
|  |   function next() { | ||
|  |     if (!self.readable) return; | ||
|  | 
 | ||
|  |     self.pending--; | ||
|  | 
 | ||
|  |     self.emit('part', field, part); | ||
|  | 
 | ||
|  |     if (data && data.path) { | ||
|  |       self.emit('file', field, part, file); | ||
|  |     } | ||
|  | 
 | ||
|  |     if (self.epilogue && !self.pending) { | ||
|  |       self.emit('end'); | ||
|  |       self.destroy(); | ||
|  |     } | ||
|  |   } | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Uploads | ||
|  |  */ | ||
|  | 
 | ||
|  | Parser.root = process.platform === 'win32' | ||
|  |   ? 'C:/Temp' | ||
|  |   : '/tmp'; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Middleware | ||
|  |  */ | ||
|  | 
 | ||
|  | Parser.middleware = function(options) { | ||
|  |   options = options || {}; | ||
|  |   return function(req, res, next) { | ||
|  |     if (options.ensureBody) { | ||
|  |       req.body = {}; | ||
|  |     } | ||
|  | 
 | ||
|  |     if (req.method === 'GET' | ||
|  |         || req.method === 'HEAD' | ||
|  |         || req._multipart) return next(); | ||
|  | 
 | ||
|  |     req._multipart = true; | ||
|  | 
 | ||
|  |     var type = req.headers['content-type']; | ||
|  | 
 | ||
|  |     if (type) type = type.split(';')[0].trim().toLowerCase(); | ||
|  | 
 | ||
|  |     if (type === 'multipart/form-data') { | ||
|  |       Parser.handle(req, res, next, options); | ||
|  |     } else { | ||
|  |       next(); | ||
|  |     } | ||
|  |   }; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Handler | ||
|  |  */ | ||
|  | 
 | ||
|  | Parser.handle = function(req, res, next, options) { | ||
|  |   var parser = new Parser(req.headers['content-type'], options) | ||
|  |     , diskLimit = options.diskLimit | ||
|  |     , limit = options.limit | ||
|  |     , parts = {} | ||
|  |     , files = {}; | ||
|  | 
 | ||
|  |   parser.on('error', function(err) { | ||
|  |     req.destroy(); | ||
|  |     next(err); | ||
|  |   }); | ||
|  | 
 | ||
|  |   parser.on('part', function(field, part) { | ||
|  |     set(parts, field, part); | ||
|  |   }); | ||
|  | 
 | ||
|  |   parser.on('file', function(field, path, name) { | ||
|  |     set(files, field, { | ||
|  |       path: path, | ||
|  |       name: name, | ||
|  |       toString: function() { | ||
|  |         return path; | ||
|  |       } | ||
|  |     }); | ||
|  |   }); | ||
|  | 
 | ||
|  |   parser.on('data', function() { | ||
|  |     if (this.writtenDisk > diskLimit || this.written > limit) { | ||
|  |       this.emit('error', new Error('Overflow.')); | ||
|  |       this.destroy(); | ||
|  |     } | ||
|  |   }); | ||
|  | 
 | ||
|  |   parser.on('end', next); | ||
|  | 
 | ||
|  |   req.body = parts; | ||
|  |   req.files = files; | ||
|  |   req.pipe(parser); | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Helpers | ||
|  |  */ | ||
|  | 
 | ||
|  | var isWindows = process.platform === 'win32'; | ||
|  | 
 | ||
|  | var stream = function(name, dir) { | ||
|  |   var ext = path.extname(name) || '' | ||
|  |     , name = path.basename(name, ext) || '' | ||
|  |     , dir = dir || Parser.root | ||
|  |     , tag; | ||
|  | 
 | ||
|  |   tag = Math.random().toString(36).substring(2); | ||
|  | 
 | ||
|  |   name = name.substring(0, 200) + '.' + tag; | ||
|  |   name = path.join(dir, name) + ext.substring(0, 6); | ||
|  |   name = name.replace(/\0/g, ''); | ||
|  | 
 | ||
|  |   if (isWindows) { | ||
|  |     name = name.replace(/[:*<>|"?]/g, ''); | ||
|  |   } | ||
|  | 
 | ||
|  |   return fs.createWriteStream(name); | ||
|  | }; | ||
|  | 
 | ||
|  | var grab = function(str, name) { | ||
|  |   if (!str) return; | ||
|  | 
 | ||
|  |   var rx = new RegExp('\\b' + name + '\\s*=\\s*("[^"]+"|\'[^\']+\'|[^;,]+)', 'i') | ||
|  |     , cap = rx.exec(str); | ||
|  | 
 | ||
|  |   if (cap) { | ||
|  |     return cap[1].trim().replace(/^['"]|['"]$/g, ''); | ||
|  |   } | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Expose | ||
|  |  */ | ||
|  | 
 | ||
|  | module.exports = Parser; |