@stream-toolbox/multipart
Version:
A streaming parser for multipart/form-data type request body.
331 lines (330 loc) • 15.6 kB
JavaScript
;
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var stream_1 = require("stream");
var StreamSearch = require("@stream-toolbox/search");
var PartFile = (function (_super) {
__extends(PartFile, _super);
function PartFile() {
var _this = _super !== null && _super.apply(this, arguments) || this;
_this.size = 0;
return _this;
}
return PartFile;
}(stream_1.Readable));
var STATES;
(function (STATES) {
STATES[STATES["UNSTART"] = 0] = "UNSTART";
STATES[STATES["PART_HEADER"] = 1] = "PART_HEADER";
STATES[STATES["PART_BODY"] = 2] = "PART_BODY";
STATES[STATES["POST_BOUNDARY"] = 3] = "POST_BOUNDARY";
STATES[STATES["END"] = 4] = "END";
})(STATES || (STATES = {}));
var fromV16 = parseInt(process.version.replace(/^v/, "")) >= 16;
function multipart(rs, opts) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
if (!(rs instanceof stream_1.Readable)) {
throw new Error("Expected readable stream, got ".concat(typeof rs));
}
var boundaryText = (_a = opts === null || opts === void 0 ? void 0 : opts.boundary) !== null && _a !== void 0 ? _a : (_c = (_b = rs.headers) === null || _b === void 0 ? void 0 : _b["content-type"]) === null || _c === void 0 ? void 0 : _c.replace(/^multipart\/form-data;\s?boundary=/, "");
if (!(boundaryText === null || boundaryText === void 0 ? void 0 : boundaryText.length)) {
throw new Error("Empty boundary");
}
var boundary = Buffer.from("\r\n--".concat(boundaryText));
var CRLF = Buffer.from("\r\n");
var onField = opts === null || opts === void 0 ? void 0 : opts.onField;
var onFile = opts === null || opts === void 0 ? void 0 : opts.onFile;
var MAX_FIELDS = (_e = (_d = opts === null || opts === void 0 ? void 0 : opts.limits) === null || _d === void 0 ? void 0 : _d.fields) !== null && _e !== void 0 ? _e : 256;
var MAX_FIELD_SIZE = (_g = (_f = opts === null || opts === void 0 ? void 0 : opts.limits) === null || _f === void 0 ? void 0 : _f.fieldSize) !== null && _g !== void 0 ? _g : 65536;
var MAX_FILES = (_j = (_h = opts === null || opts === void 0 ? void 0 : opts.limits) === null || _h === void 0 ? void 0 : _h.files) !== null && _j !== void 0 ? _j : 256;
var MAX_FILE_SIZE = (_l = (_k = opts === null || opts === void 0 ? void 0 : opts.limits) === null || _k === void 0 ? void 0 : _k.fileSize) !== null && _l !== void 0 ? _l : Infinity;
var MAX_PART_HEADERS = (_o = (_m = opts === null || opts === void 0 ? void 0 : opts.limits) === null || _m === void 0 ? void 0 : _m.partHeaders) !== null && _o !== void 0 ? _o : 3;
var MAX_PART_HEADER_SIZE = (_q = (_p = opts === null || opts === void 0 ? void 0 : opts.limits) === null || _p === void 0 ? void 0 : _p.partHeaderSize) !== null && _q !== void 0 ? _q : 1024;
var MAX_TOTAL_SIZE = (_s = (_r = opts === null || opts === void 0 ? void 0 : opts.limits) === null || _r === void 0 ? void 0 : _r.totalSize) !== null && _s !== void 0 ? _s : Infinity;
return new Promise(function (_resolve, _reject) {
var settled = false;
var state = STATES.UNSTART;
var tmpChunks = [];
var misMatchSize = 0;
var fields = 0;
var files = 0;
var partHeaders = 0;
var totalSize = 0;
var meta;
var file;
var parts = [];
var searcher = new StreamSearch(boundary, function (isMatch, chunk) {
if (state === STATES.END) {
return reject(new Error("multipart/form-data has ended"));
}
if (isMatch) {
switch (state) {
case STATES.UNSTART:
state = STATES.POST_BOUNDARY;
searcher.resetNeedle(CRLF);
break;
case STATES.PART_HEADER:
if (misMatchSize) {
if (++partHeaders > MAX_PART_HEADERS) {
return reject(new Error("Header count exceeded the limit"));
}
var header = Buffer.concat(tmpChunks).toString("utf-8");
tmpChunks = [];
misMatchSize = 0;
var colonIdx = header.indexOf(":");
if (colonIdx < 1) {
return;
}
var hName = header.substring(0, colonIdx).toLowerCase();
var hValue = header.substring(colonIdx + 1).trim();
switch (hName) {
case "content-disposition":
var pairs = hValue.split(";");
for (var i = 1; i < pairs.length; i++) {
var _a = pairs[i].trim().split("="), key = _a[0], value = _a[1];
if ((key === "name" || key === "filename") && value !== undefined) {
meta[key] = value.replace(/^"|"$/g, "");
}
}
break;
case "content-type":
meta.mimeType = hValue;
break;
case "content-transfer-encoding":
meta.encoding = hValue;
break;
}
}
else {
if (!meta.name) {
return reject(new Error("Part missing name"));
}
if (meta.hasOwnProperty("filename")) {
if (++files > MAX_FILES) {
return reject(new Error("File part count exceeded the limit"));
}
var name_1 = meta.name;
if (onFile && meta.filename) {
file = new PartFile({
read: function () {
if (rs.isPaused()) {
rs.resume();
}
},
});
parts.push(new Promise(function (success) {
var _a;
(_a = onFile(file, meta, function (err, data) {
err ? reject(err) : success([name_1, data]);
})) === null || _a === void 0 ? void 0 : _a.then(function (data) {
success([name_1, data]);
}, reject);
}));
}
else {
parts.push(Promise.resolve([name_1, undefined]));
}
}
else {
if (++fields > MAX_FIELDS) {
return reject(new Error("Field part count exceeded the limit"));
}
}
partHeaders = 0;
searcher.resetNeedle(boundary);
state = STATES.PART_BODY;
}
break;
case STATES.PART_BODY:
if (meta.hasOwnProperty("filename")) {
if (file) {
file.size = misMatchSize;
file.push(null);
file = null;
}
}
else {
var name_2 = meta.name;
if (onField) {
parts.push(new Promise(function (success) {
var _a;
(_a = onField(Buffer.concat(tmpChunks), meta, function (err, data) {
err ? reject(err) : success([name_2, data]);
})) === null || _a === void 0 ? void 0 : _a.then(function (data) {
success([name_2, data]);
}, reject);
}));
}
else {
parts.push(Promise.resolve([name_2, Buffer.concat(tmpChunks).toString("utf-8")]));
}
}
tmpChunks = [];
misMatchSize = 0;
searcher.resetNeedle(CRLF);
state = STATES.POST_BOUNDARY;
break;
case STATES.POST_BOUNDARY:
if (misMatchSize === 0) {
meta = { name: "" };
state = STATES.PART_HEADER;
}
else if (Buffer.concat(tmpChunks).equals(Buffer.from("--"))) {
state = STATES.END;
Promise.all(parts).then(resolve);
}
else {
reject(new Error("Invalid data after boundary"));
}
break;
}
}
else {
misMatchSize += chunk.length;
switch (state) {
case STATES.UNSTART:
if (misMatchSize > 0) {
return reject(new Error("Readable stream should be start with boundary"));
}
break;
case STATES.PART_HEADER:
if (misMatchSize > MAX_PART_HEADER_SIZE) {
return reject(new Error("Header size exceeded the limit"));
}
tmpChunks.push(chunk);
break;
case STATES.PART_BODY:
if (meta.hasOwnProperty("filename")) {
if (misMatchSize > MAX_FILE_SIZE) {
return reject(new Error("File size exceeded the limit"));
}
if (file) {
if (!file.push(chunk) && !rs.isPaused()) {
rs.pause();
}
}
}
else {
if (misMatchSize > MAX_FIELD_SIZE) {
return reject(new Error("Field size exceeded the limit"));
}
tmpChunks.push(chunk);
}
break;
case STATES.POST_BOUNDARY:
if (misMatchSize > 2) {
return reject(new Error("Invalid data after boundary"));
}
tmpChunks.push(chunk);
break;
}
}
});
searcher.push(Buffer.from("\r\n"));
function onData(chunk) {
if (settled) {
return;
}
totalSize += chunk.length;
if (totalSize > MAX_TOTAL_SIZE) {
return reject(new Error("Total size exceeded the limit"));
}
searcher.push(chunk);
}
var onEnd = (function () {
var called = false;
return function () {
if (called) {
return;
}
called = true;
searcher.flush();
if (file) {
file.emit("error", new Error("Request socket closed"));
}
if (state === STATES.POST_BOUNDARY && Buffer.concat(tmpChunks).equals(Buffer.from("--"))) {
state = STATES.END;
Promise.all(parts).then(resolve);
}
if (state !== STATES.END) {
reject(new Error("Incomplete multipart/form-data"));
}
};
})();
function cleanUp() {
settled = true;
rs.removeListener("data", onData);
rs.removeListener("error", reject);
rs.removeListener("end", onEnd);
if (fromV16) {
rs.removeListener("close", onEnd);
}
else {
rs.removeListener("aborted", onEnd);
}
}
function resolve(data) {
if (settled) {
return;
}
cleanUp();
_resolve(data);
}
function reject(err) {
if (settled) {
return;
}
cleanUp();
if (file && !file.destroyed) {
file.destroy(err);
}
if ((opts === null || opts === void 0 ? void 0 : opts.autoDestroy) && !rs.destroyed) {
rs.destroy(err);
}
_reject(err);
}
rs.on("data", onData);
rs.once("error", reject);
rs.once("end", onEnd);
if (fromV16) {
rs.once("close", onEnd);
}
else {
rs.once("aborted", onEnd);
}
}).then(function (parts) {
var arrayResult = {};
for (var i = 0; i < parts.length; i++) {
var _a = parts[i], name_3 = _a[0], value = _a[1];
if (Object.hasOwnProperty.call(arrayResult, name_3)) {
arrayResult[name_3].push(value);
}
else {
arrayResult[name_3] = [value];
}
}
if ((opts === null || opts === void 0 ? void 0 : opts.resultFormat) === "array") {
return arrayResult;
}
var commonResult = {};
for (var name_4 in arrayResult) {
var value = arrayResult[name_4];
commonResult[name_4] = value.length === 1 ? value[0] : value;
}
return commonResult;
});
}
module.exports = multipart;