nxkit
Version:
This is a collection of tools, independent of any other libraries
792 lines (791 loc) • 27.6 kB
JavaScript
;
/* ***** BEGIN LICENSE BLOCK *****
* Distributed under the BSD license:
*
* Copyright (c) 2015, xuewen.chu
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of xuewen.chu nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL xuewen.chu BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* ***** END LICENSE BLOCK ***** */
Object.defineProperty(exports, "__esModule", { value: true });
const event_1 = require("./event");
const fs = require("fs");
const Path = require("path");
const string_decoder_1 = require("string_decoder");
const querystring = require("querystring");
const crypto = require("crypto");
const request_1 = require("./request");
const xml_1 = require("./xml");
var STATUS;
(function (STATUS) {
STATUS[STATUS["PARSER_UNINITIALIZED"] = 0] = "PARSER_UNINITIALIZED";
STATUS[STATUS["START"] = 1] = "START";
STATUS[STATUS["START_BOUNDARY"] = 2] = "START_BOUNDARY";
STATUS[STATUS["HEADER_FIELD_START"] = 3] = "HEADER_FIELD_START";
STATUS[STATUS["HEADER_FIELD"] = 4] = "HEADER_FIELD";
STATUS[STATUS["HEADER_VALUE_START"] = 5] = "HEADER_VALUE_START";
STATUS[STATUS["HEADER_VALUE"] = 6] = "HEADER_VALUE";
STATUS[STATUS["HEADER_VALUE_ALMOST_DONE"] = 7] = "HEADER_VALUE_ALMOST_DONE";
STATUS[STATUS["HEADERS_ALMOST_DONE"] = 8] = "HEADERS_ALMOST_DONE";
STATUS[STATUS["PART_DATA_START"] = 9] = "PART_DATA_START";
STATUS[STATUS["PART_DATA"] = 10] = "PART_DATA";
STATUS[STATUS["PART_END"] = 11] = "PART_END";
STATUS[STATUS["END"] = 12] = "END";
})(STATUS = exports.STATUS || (exports.STATUS = {}));
;
const S = STATUS;
var f = 1;
var F;
(function (F) {
F[F["PART_BOUNDARY"] = f] = "PART_BOUNDARY";
F[F["LAST_BOUNDARY"] = f *= 2] = "LAST_BOUNDARY";
})(F || (F = {}));
const LF = 10, CR = 13, SPACE = 32, HYPHEN = 45, COLON = 58, A = 97, Z = 122, lower = function (c) {
return c | 0x20;
};
// This is a buffering parser, not quite as nice as the multipart one.
// If I find time I'll rewrite this to be fully streaming as well
class QuerystringParser {
/**
* constructor function
* @constructor
*/
constructor(type) {
this.buffers = [];
this.onField = () => { };
this.onEnd = () => { };
this.type = type;
}
write(buffer) {
this.buffers.push(buffer);
return buffer.length;
}
end() {
var buffer = Buffer.concat(this.buffers).toString('utf8');
if (this.type == 'json') {
var data = {};
buffer = buffer.trim();
if (buffer) {
try {
data = request_1.parseJSON(buffer);
}
catch (err) {
console.error(buffer, err);
}
}
this.onEnd(data);
}
else if (this.type == 'xml') {
var doc = new xml_1.default();
var r = doc.load(buffer + '');
this.onEnd({ body: doc });
}
else {
var fields = querystring.parse(buffer);
for (var field in fields) {
this.onField(field, fields[field]);
}
this.onEnd(fields);
}
return undefined;
}
}
class File {
constructor(path, name, type) {
this._writeStream = null;
this._path = '';
this._name = '';
this._size = 0;
this._lastModifiedDate = 0;
this.onProgress = new event_1.EventNoticer('Progress', this);
this.onEnd = new event_1.EventNoticer('End', this);
this._path = path;
this._name = name;
this._type = type;
}
get size() {
return this._size;
}
// @todo Next release: Show error messages when accessing these
get length() {
return this.size;
}
get filename() {
return this._name;
}
get pathname() {
return this._path;
}
get mime() {
return this._type;
}
get lastModifiedDate() {
return this._lastModifiedDate;
}
_open() {
this._writeStream = fs.createWriteStream(this._path);
}
write(buffer, cb) {
var self = this;
if (!self._writeStream)
self._open();
self._writeStream.write(buffer, function () {
self._lastModifiedDate = Date.now();
self._size += buffer.length;
self.onProgress.trigger(self.size);
cb();
});
}
end(cb) {
if (this._writeStream) {
this._writeStream.end(() => {
this.onEnd.trigger({});
cb();
});
}
else {
this._path = '';
this.onEnd.trigger({});
cb();
}
}
}
exports.File = File;
class MultipartParser {
constructor(boundary) {
this.boundaryChars = {};
this.state = S.PARSER_UNINITIALIZED;
this.flags = 0;
this.index = 0;
this._mark = {};
this.state = S.PARSER_UNINITIALIZED;
this.boundary = Buffer.alloc(boundary.length + 4);
this.boundary.write('\r\n--', 0, 'ascii');
this.boundary.write(boundary, 4, 'ascii');
this.lookbehind = Buffer.alloc(this.boundary.length + 8);
this.state = S.START;
this.boundaryChars = {};
for (var i = 0; i < this.boundary.length; i++) {
this.boundaryChars[this.boundary[i]] = true;
}
}
write(buffer) {
var self = this, i = 0, len = buffer.length, prevIndex = this.index, index = this.index, state = this.state, flags = this.flags, lookbehind = this.lookbehind, boundary = this.boundary, boundaryChars = this.boundaryChars, boundaryLength = this.boundary.length, boundaryEnd = boundaryLength - 1, bufferLength = buffer.length, c, cl, _mark = this._mark, mark = function (name) {
_mark[name + 'Mark'] = i;
}, clear = function (name) {
delete _mark[name + 'Mark'];
}, callback = function (name, buffer, start, end) {
if (start !== undefined && start === end) {
return;
}
var callbackSymbol = 'on' + name.substr(0, 1).toUpperCase() + name.substr(1);
if (callbackSymbol in self) {
self[callbackSymbol](buffer, start, end);
}
}, dataCallback = function (name, clear) {
var markSymbol = name + 'Mark';
if (!(markSymbol in _mark)) {
return;
}
if (clear) {
callback(name, buffer, _mark[markSymbol], i);
delete _mark[markSymbol];
}
else {
callback(name, buffer, _mark[markSymbol], buffer.length);
_mark[markSymbol] = 0;
}
};
for (i = 0; i < len; i++) {
c = buffer[i];
switch (state) {
case S.PARSER_UNINITIALIZED:
return i;
case S.START:
index = 0;
state = S.START_BOUNDARY;
case S.START_BOUNDARY:
if (index == boundary.length - 2) {
if (c != CR) {
return i;
}
index++;
break;
}
else if (index - 1 == boundary.length - 2) {
if (c != LF) {
return i;
}
index = 0;
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;
mark('headerField');
index = 0;
case S.HEADER_FIELD:
if (c == CR) {
clear('headerField');
state = S.HEADERS_ALMOST_DONE;
break;
}
index++;
if (c == HYPHEN) {
break;
}
if (c == COLON) {
if (index == 1) {
// empty header field
return i;
}
dataCallback('headerField', true);
state = S.HEADER_VALUE_START;
break;
}
cl = lower(c);
if (cl < A || cl > Z) {
return i;
}
break;
case S.HEADER_VALUE_START:
if (c == SPACE) {
break;
}
mark('headerValue');
state = S.HEADER_VALUE;
case S.HEADER_VALUE:
if (c == CR) {
dataCallback('headerValue', true);
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;
}
callback('headersEnd');
state = S.PART_DATA_START;
break;
case S.PART_DATA_START:
state = S.PART_DATA;
mark('partData');
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 < boundary.length) {
if (boundary[index] == c) {
if (index == 0) {
dataCallback('partData', true);
}
index++;
}
else {
index = 0;
}
}
else if (index == boundary.length) {
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 == boundary.length) {
if (flags & F.PART_BOUNDARY) {
index = 0;
if (c == LF) {
// unset the PART_BOUNDARY flag
flags &= ~F.PART_BOUNDARY;
callback('partEnd');
callback('partBegin');
state = S.HEADER_FIELD_START;
break;
}
}
else if (flags & F.LAST_BOUNDARY) {
if (c == HYPHEN) {
callback('partEnd');
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
callback('partData', lookbehind, 0, prevIndex);
prevIndex = 0;
mark('partData');
// 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;
}
}
dataCallback('headerField');
dataCallback('headerValue');
dataCallback('partData');
this.index = index;
this.state = state;
this.flags = flags;
return len;
}
end() {
if (this.state != S.END) {
return new Error('MultipartParser.end(): stream ended unexpectedly: ' + this.explain());
}
}
explain() {
return 'state = ' + stateToString(this.state);
}
}
class Part {
constructor() {
this.headers = {};
this.name = '';
this.filename = '';
this.mime = '';
this.headerField = '';
this.headerValue = '';
this.onData = new event_1.EventNoticer('Data', this);
this.onEnd = new event_1.EventNoticer('End', this);
}
}
// ----------------------- IncomingForm -----------------------
var temp_dir = '';
var dirs = ['/tmp', process.cwd()];
if (process.env.TMP) {
dirs.unshift(process.env.TMP);
}
for (var dir of dirs) {
var isDirectory = false;
try {
isDirectory = fs.statSync(dir).isDirectory();
}
catch (e) { }
if (isDirectory) {
temp_dir = dir;
break;
}
}
class IncomingForm {
/**
* constructor function
* @param {HttpService}
* @constructor
*/
constructor(service) {
this._parser = null;
this._flushing = 0;
this._fields_size = 0;
this._error = null;
this._ended = false;
/**
* default size 2MB
* @type {Number}
*/
this.maxFieldsSize = 5 * 1024 * 1024;
/**
* default size 5MB
* @type {Number}
*/
this.maxFilesSize = 5 * 1024 * 1024;
/**
* verifyFileMime 'js|jpg|jpeg|png' default as '*' ...
* @type {String}
*/
this.verifyFileMime = '*';
/**
* is use file upload, default not upload
* @type {Boolean}
*/
this.isUpload = false;
this.fields = {};
this.files = {};
this.keepExtensions = false;
this.uploadDir = '';
this.encoding = 'utf-8';
this.headers = {};
this.type = '';
this.bytesReceived = 0;
this.bytesExpected = 0;
this.onAborted = new event_1.EventNoticer('Aborted', this);
this.onProgress = new event_1.EventNoticer('Progress', this);
this.onField = new event_1.EventNoticer('Field', this);
this.onFileBegin = new event_1.EventNoticer('FileBegin', this);
this.onFile = new event_1.EventNoticer('File', this);
this.onError = new event_1.EventNoticer('Error', this);
this.onEnd = new event_1.EventNoticer('End', this);
this.hash = crypto.createHash(service.server.formHash || 'md5');
this.uploadDir = service.server.temp;
this._service = service;
this.maxFieldsSize = this._service.server.maxFormDataSize;
this.maxFilesSize = this._service.server.maxUploadFileSize;
}
get ended() {
return this._ended;
}
_canceled() {
for (var files of Object.values(this.files)) {
for (var file of files) {
fs.unlink(file.pathname, e => { });
}
}
}
/**
* parse
*/
parse() {
var self = this;
var req = this._service.request;
req.on('error', function (err) {
self._throwError(err);
});
req.on('aborted', function () {
self._canceled();
self.onAborted.trigger({});
});
req.on('data', function (buffer) {
self.write(buffer);
});
req.on('end', function () {
if (self._error)
return;
var err = self._parser.end();
if (err) {
self._throwError(err);
}
});
this.headers = req.headers;
this._parseContentLength();
this._parseContentType();
}
write(buffer) {
if (!this._parser) {
this._throwError(new Error('unintialized parser'));
return;
}
this.hash.update(buffer);
this.bytesReceived += buffer.length;
this.onProgress.trigger({
bytesReceived: this.bytesReceived,
bytesExpected: this.bytesExpected,
});
var bytesParsed = this._parser.write(buffer);
if (bytesParsed !== buffer.length) {
this._throwError(new Error('parser error, ' +
bytesParsed + ' of ' +
buffer.length + ' bytes parsed'));
}
return bytesParsed;
}
pause() {
try {
this._service.request.pause();
}
catch (err) {
// the stream was destroyed
if (!this._ended) {
// before it was completed, crash & burn
this._throwError(err);
}
return false;
}
return true;
}
resume() {
try {
this._service.request.resume();
}
catch (err) {
// the stream was destroyed
if (!this._ended) {
// before it was completed, crash & burn
this._throwError(err);
}
return false;
}
return true;
}
onpart(part) {
// this method can be overwritten by the user
this.handle_part(part);
}
handle_part(part) {
var self = this;
if (part.filename === undefined) {
var value = '';
var decoder = new string_decoder_1.StringDecoder(this.encoding);
part.onData.on(function (e) {
var buffer = e.data;
self._fields_size += buffer.length;
if (self._fields_size > self.maxFieldsSize) {
self._throwError(new Error('maxFieldsSize exceeded, received ' + self._fields_size + ' bytes of field data'));
return;
}
value += decoder.write(buffer);
});
part.onEnd.on(function () {
self._fields_size = 0;
self.fields[part.name] = value;
self.onField.trigger({ name: part.name, value: value });
});
return;
}
if (!this.isUpload) {
return this._throwError(new Error('Does not allow file uploads'));
}
this._flushing++;
var file = new File(this._uploadPath(part.filename), part.filename, part.mime);
if (this.verifyFileMime != '*' && !new RegExp('\.(' + this.verifyFileMime + ')$', 'i').test(part.filename)) {
return this._throwError(new Error('File mime error'));
}
this.onFileBegin.trigger({ name: part.name, file: file });
part.onData.on(function (e) {
var buffer = e.data;
self.pause();
self._fields_size += buffer.length;
if (self._fields_size > self.maxFilesSize) { // limit
file.end(function () {
self._throwError(new Error('maxFilesSize exceeded, received ' + self._fields_size + ' bytes of field data'));
});
return;
}
file.write(buffer, function () {
self.resume();
});
});
part.onEnd.on(function () {
self._fields_size = 0;
file.end(function () {
self._flushing--;
var files = self.files[part.name];
if (!files)
self.files[part.name] = files = [];
files.push(file);
self.onFile.trigger({ name: part.name, file: file });
self._maybeEnd();
});
});
}
_parseContentType() {
var type = this.headers['content-type'];
if (type && type.match(/multipart/i)) {
var m;
if (m = type.match(/boundary=(?:"([^"]+)"|([^;]+))/i)) {
this._initMultipart(m[1] || m[2]);
}
else {
this._throwError(new Error('bad content-type header, no multipart boundary'));
}
}
else {
this._initUrlencodedOrJsonOrXml(type);
}
}
_throwError(err) {
if (this._error) {
return;
}
this._canceled();
this._error = err;
this._service.request.socket.end(); //close socket connect
this.onError.trigger(err);
}
_parseContentLength() {
if (this.headers['content-length']) {
this.bytesReceived = 0;
this.bytesExpected = parseInt(this.headers['content-length'], 10);
}
}
_fileName(headerValue) {
var m = headerValue.match(/filename="(.*?)"($|; )/i);
if (!m)
return;
var filename = m[1].substr(m[1].lastIndexOf('\\') + 1);
filename = filename.replace(/%22/g, '"');
filename = filename.replace(/&#([\d]{4});/g, function (m, code) {
return String.fromCharCode(code);
});
return filename;
}
_initMultipart(boundary) {
this.type = 'multipart';
var parser = new MultipartParser(boundary);
var self = this;
var headerField = '';
var headerValue = '';
var part;
parser.onPartBegin = function () {
part = new Part();
};
parser.onHeaderField = function (b, start, end) {
headerField += b.toString(self.encoding, start, end);
};
parser.onHeaderValue = function (b, start, end) {
headerValue += b.toString(self.encoding, start, end);
};
parser.onHeaderEnd = function () {
headerField = headerField.toLowerCase();
part.headers[headerField] = headerValue;
var m;
if (headerField == 'content-disposition') {
if (m = headerValue.match(/name="([^"]+)"/i)) {
part.name = m[1];
}
part.filename = self._fileName(headerValue) || '';
}
else if (headerField == 'content-type') {
part.mime = headerValue;
}
headerField = '';
headerValue = '';
};
parser.onHeadersEnd = function () {
self.onpart(part);
};
parser.onPartData = function (b, start, end) {
part.onData.trigger(b.slice(start, end));
};
parser.onPartEnd = function () {
part.onEnd.trigger({});
};
parser.onEnd = function () {
self._ended = true;
self._maybeEnd();
};
this._parser = parser;
}
_initUrlencodedOrJsonOrXml(type) {
if (type && type.indexOf('json') >= 0) {
type = 'json';
}
else if (type && type.indexOf('xml') >= 0) {
type = 'xml';
}
else {
type = 'urlencoded';
}
this.type = type;
var parser = new QuerystringParser(type);
var self = this;
if (type == 'json' || type == 'xml') {
// parser.onField = function() {};
parser.onEnd = function (data) {
self._ended = true;
Object.assign(self.fields, data);
self._maybeEnd();
};
}
else {
parser.onField = function (name, value) {
self.fields[name] = value;
self.onField.trigger({ name: name, value: value });
};
parser.onEnd = function () {
self._ended = true;
self._maybeEnd();
};
}
this._parser = parser;
}
_uploadPath(filename) {
var name = '';
for (var i = 0; i < 32; i++) {
name += Math.floor(Math.random() * 16).toString(16);
}
if (this.keepExtensions) {
var ext = Path.extname(filename);
ext = ext.replace(/(\.[a-z0-9]+).*/, '$1');
name += ext;
}
return Path.join(this.uploadDir, 'temp_upload_' + name);
}
_maybeEnd() {
if (!this._ended || this._flushing)
return;
this.onEnd.trigger({});
}
}
exports.IncomingForm = IncomingForm;
function stateToString(stateNumber) {
for (var state in S) {
var number = S[state];
if (number === stateNumber)
return state;
}
return '';
}
//
exports.default = {
IncomingForm: IncomingForm,
temp_dir: temp_dir,
STATUS: STATUS,
stateToString: stateToString,
};