UNPKG

@stream-toolbox/multipart

Version:
331 lines (330 loc) 15.6 kB
"use strict"; 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;