UNPKG

strata

Version:

A modular, streaming HTTP server

408 lines (348 loc) 9.64 kB
var path = require('path'); var fs = require('fs'); var File = require('./file'); var Part = require('./part'); module.exports = Parser; // This parser is modified from the one in the node-formidable // project, written by Felix Geisendörfer. MIT licensed. var s = 0; var S = { START: s++, START_BOUNDARY: s++, HEADER_FIELD_START: s++, HEADER_FIELD: s++, HEADER_VALUE_START: s++, HEADER_VALUE: s++, HEADER_VALUE_ALMOST_DONE: s++, HEADERS_ALMOST_DONE: s++, PART_DATA_START: s++, PART_DATA: s++, PART_END: s++, END: s++ }; var f = 1; var F = { PART_BOUNDARY: f, LAST_BOUNDARY: f *= 2 }; var LF = 10; var CR = 13; var SPACE = 32; var HYPHEN = 45; var COLON = 58; function Parser(boundary, tmpdir, prefix) { tmpdir = tmpdir || '/tmp'; prefix = prefix || ''; if (!fs.existsSync(tmpdir)) { throw new Error('Temporary directory "' + tmpdir + '" does not exist'); } if (!fs.statSync(tmpdir).isDirectory()) { throw new Error('"' + tmpdir + '" is not a directory'); } this.boundary = new Buffer(boundary.length + 4); this.boundary.write('\r\n--', 'ascii', 0); this.boundary.write(boundary, 'ascii', 4); this.tmpdir = tmpdir; this.prefix = prefix; this.lookBehind = new Buffer(this.boundary.length + 8); this.state = S.START; this.boundaryChars = {}; var i = this.boundary.length; while (i) { this.boundaryChars[this.boundary[--i]] = true; } this.index = null; this.flags = 0; this._done = false; // Are we done with writes? this._flushing = 0; // How many files are still being flushed to disk? } Parser.prototype.tmpfile = function () { var name = this.prefix; for (var i = 0; i < 32; ++i) { name += Math.round(Math.random() * 16).toString(16); } return path.join(this.tmpdir, name); }; Parser.prototype.write = function (buffer) { var self = this, bufferLength = buffer.length, prevIndex = this.index, index = this.index, state = this.state, flags = this.flags, lookBehind = this.lookBehind, boundary = this.boundary, boundaryChars = this.boundaryChars, boundaryLength = boundary.length, boundaryEnd = boundaryLength - 1, c, cl; for (var i = 0; i < bufferLength; ++i) { c = buffer[i]; switch (state) { case S.START: index = 0; state = S.START_BOUNDARY; // fall through case S.START_BOUNDARY: if (index == boundaryLength - 2) { if (c != CR) { return i; } index++; break; } else if (index == boundaryLength - 1) { if (c != LF) { return i; } index = 0; this._callback('partBegin'); state = S.HEADER_FIELD_START; break; } if (c != boundary[index + 2]) { return i; } index++; break; case S.HEADER_FIELD_START: state = S.HEADER_FIELD; this._mark('headerName', i); index = 0; // fall through case S.HEADER_FIELD: if (c == CR) { this._clear('headerName'); state = S.HEADERS_ALMOST_DONE; break; } index++; if (c == HYPHEN) { break; } if (c == COLON) { if (index == 1) { // empty header field return i; } this._dataCallback('headerName', buffer, true, i); state = S.HEADER_VALUE_START; break; } cl = c | 0x20; // lower-case if (cl < 97 || cl > 122) { // not between "a" and "z" return i; } break; case S.HEADER_VALUE_START: if (c == SPACE) { break; } this._mark('headerValue', i); state = S.HEADER_VALUE; // fall through case S.HEADER_VALUE: if (c == CR) { this._dataCallback('headerValue', buffer, true, i); this._callback('headerEnd'); state = S.HEADER_VALUE_ALMOST_DONE; } break; case S.HEADER_VALUE_ALMOST_DONE: if (c != LF) { return i; } state = S.HEADER_FIELD_START; break; case S.HEADERS_ALMOST_DONE: if (c != LF) { return i; } this._callback('headersEnd'); state = S.PART_DATA_START; break; case S.PART_DATA_START: state = S.PART_DATA this._mark('partData', i); // fall through case S.PART_DATA: prevIndex = index; if (index == 0) { // boyer-moore derrived algorithm to safely skip non-boundary data i += boundaryEnd; while (i < bufferLength && !(buffer[i] in boundaryChars)) { i += boundaryLength; } i -= boundaryEnd; c = buffer[i]; } if (index < boundaryLength) { if (boundary[index] == c) { if (index == 0) { this._dataCallback('partData', buffer, true, i); } index++; } else { index = 0; } } else if (index == boundaryLength) { index++; if (c == CR) { // CR = part boundary flags |= F.PART_BOUNDARY; } else if (c == HYPHEN) { // HYPHEN = end boundary flags |= F.LAST_BOUNDARY; } else { index = 0; } } else if (index - 1 == boundaryLength) { if (flags & F.PART_BOUNDARY) { index = 0; if (c == LF) { // unset the PART_BOUNDARY flag flags &= ~F.PART_BOUNDARY; this._callback('partEnd'); this._callback('partBegin'); state = S.HEADER_FIELD_START; break; } } else if (flags & F.LAST_BOUNDARY) { if (c == HYPHEN) { this._callback('partEnd'); // this._callback('end'); state = S.END; } else { index = 0; } } else { index = 0; } } if (index > 0) { // when matching a possible boundary, keep a lookBehind // reference in case it turns out to be a false lead lookBehind[index - 1] = c; } else if (prevIndex > 0) { // if our boundary turned out to be rubbish, the captured // lookBehind belongs to partData this._callback('partData', lookBehind, 0, prevIndex); prevIndex = 0; this._mark('partData', i); // reconsider the current character even so it interrupted the // sequence it could be the beginning of a new sequence i--; } break; case S.END: break; default: return i; } } this._dataCallback('headerName', buffer); this._dataCallback('headerValue', buffer); this._dataCallback('partData', buffer); this.index = index; this.state = state; this.flags = flags; return bufferLength; }; Parser.prototype._mark = function (name, i) { this[name + 'Mark'] = i; }; Parser.prototype._clear = function (name) { delete this[name + 'Mark']; }; Parser.prototype._callback = function (name, buffer, start, end) { if (start !== undefined && start === end) { return; } var prop = 'on' + name.substr(0, 1).toUpperCase() + name.substr(1); if (prop in this) { this[prop](buffer, start, end); } }; Parser.prototype._dataCallback = function (name, buffer, clear, i) { var prop = name + 'Mark'; if (prop in this) { if (!clear) { this._callback(name, buffer, this[prop], buffer.length); this[prop] = 0; } else { this._callback(name, buffer, this[prop], i); delete this[prop]; } } }; Parser.prototype.onPartBegin = function () { this._part = new Part(); this._headerName = ''; this._headerValue = ''; }; Parser.prototype.onHeaderName = function (buffer, start, end) { this._headerName += buffer.toString('utf8', start, end); }; Parser.prototype.onHeaderValue = function (buffer, start, end) { this._headerValue += buffer.toString('utf8', start, end); }; Parser.prototype.onHeaderEnd = function () { this._part.headers[this._headerName.toLowerCase()] = this._headerValue; this._headerName = ''; this._headerValue = ''; }; Parser.prototype.onHeadersEnd = function () { this.onPart(this._part); }; Parser.prototype.onPartData = function (buffer, start, end) { this._part.write(buffer.slice(start, end)); }; Parser.prototype.onPartEnd = function () { this._part.end(); }; Parser.prototype.end = function () { if (this.state !== S.END) { throw new Error('Stream ended unexpectedly (state: ' + this.state + ')'); } this._done = true; this._maybeEnd(); }; Parser.prototype.onPart = function (part) { var filename = part.filename, self = this; if (filename) { this._flushing += 1; var file = new File(this.tmpfile(), part.type, filename); this.onFile(file); file.on('end', function () { self.onParam(part.name, file); self._flushing -= 1; self._maybeEnd(); }); part.on('data', function (buffer) { file.write(buffer); }); part.on('end', function () { file.end(); }); } else { var content = ''; part.on('data', function (buffer) { content += buffer.toString('utf8'); }); part.on('end', function () { self.onParam(part.name, content); }); } }; Parser.prototype._maybeEnd = function () { // Make sure that we're both done with the input (i.e. Parser.end was // called) AND that we're done flushing all files to disk. if (!this._done || this._flushing) { return; } this.onEnd(); }; Parser.prototype.onFile = function (file) {}; Parser.prototype.onParam = function (name, value) {}; Parser.prototype.onEnd = function () {};