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
/**
|
|
* 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 = Buffer.allocUnsafe('\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 = Buffer.allocUnsafe(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;
|
|
|