@trifrost/core
Version:
Blazingly fast, runtime-agnostic server framework for modern edge and node environments
269 lines (268 loc) • 11.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseMultipart = parseMultipart;
exports.parseBody = parseBody;
const number_1 = require("@valkyriestudios/utils/number");
const formdata_1 = require("@valkyriestudios/utils/formdata");
const constants_1 = require("../../types/constants");
const encoder = new TextEncoder();
const enc_newline = encoder.encode('\r\n');
const enc_double_newline = encoder.encode('\r\n\r\n');
const decoders = {};
const RGX_BOUNDARY = /boundary=([^;]+)/;
const RGX_NAME = /name="([^"]+)"/;
const RGX_FILENAME = /filename="([^"]*)"/;
const RGX_CHARSET = /charset=([^;\s]+)/i;
/**
* Simple Uint8Array indexOf implementation
*/
function indexOf(haystack, needle, start = 0) {
outer: for (let i = start; i <= haystack.length - needle.length; i++) {
for (let j = 0; j < needle.length; j++) {
if (haystack[i + j] !== needle[j])
continue outer;
}
return i;
}
return -1;
}
/**
* Decodes a uint8array buffer with the provided decoder and
* potentially strips byte-order-marks prepended to the text
*/
function decode(dec, buf) {
const text = dec.decode(buf);
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
}
/**
* Returns true/false if the buffer size is bigger than the type limit, falling back to
* checking against the fallback limit
*
* @param {TriFrostContext} ctx - TriFrost Context
* @param {Uint8Array} buf - Buffer to check against
* @param {number|undefined} typeLim - Type limit
* @param {number|undefined} globalLim - Global limit
*/
function isAboveLimit(ctx, buf, typeLim, globalLim) {
const bufLength = buf.byteLength;
if ((0, number_1.isInt)(typeLim)) {
if (bufLength > typeLim) {
ctx.logger.debug('parseBody: too large', { size: bufLength, limit: typeLim });
return true;
}
return false;
}
if ((0, number_1.isInt)(globalLim) && bufLength > globalLim) {
ctx.logger.debug('parseBody: too large', { size: bufLength, limit: globalLim });
return true;
}
return false;
}
/**
* Parses multipart/form-data from a Uint8Array and returns a FormData (binary-safe)
*/
async function parseMultipart(ctx, bytes, boundary, decoder, config) {
const form = new FormData();
const files = [];
const files_allowed = Array.isArray(config.files?.types) ? new Set(config.files.types) : null;
const delimiter = encoder.encode('--' + boundary);
/* Get start index using delimiter */
let start = indexOf(bytes, delimiter);
if (start === -1)
return form;
start += delimiter.length + enc_newline.length;
while (start < bytes.length) {
const end_idx = indexOf(bytes, delimiter, start);
if (end_idx === -1)
break;
/* trim trailing \r\n */
const part = bytes.subarray(start, end_idx - 2);
const header_end_idx = indexOf(part, enc_double_newline);
if (header_end_idx === -1) {
start = end_idx + delimiter.length + enc_newline.length;
continue;
}
const content = part.subarray(header_end_idx + 4);
let disposition = null;
let type = null;
let part_decoder = decoder;
const header_lines = decoder.decode(part.subarray(0, header_end_idx)).split('\r\n');
for (let i = 0; i < header_lines.length; i++) {
const [key, ...val_parts] = header_lines[i].split(':');
switch (key.trim().toLowerCase()) {
case 'content-disposition':
disposition = val_parts.join(':').trim();
break;
case 'content-type': {
type = val_parts.join(':').trim();
/* Determine part decoder */
const charset_match = type.match(RGX_CHARSET);
if (charset_match) {
const charset = charset_match[1].trim().toLowerCase();
if (decoders[charset]) {
part_decoder = decoders[charset];
}
else {
part_decoder = new TextDecoder(charset, { ignoreBOM: true, fatal: true });
decoders[charset] = part_decoder;
}
}
break;
}
default:
break;
}
}
/* Get name of the value */
const name_match = (disposition || '').match(RGX_NAME);
if (name_match) {
const name = name_match[1];
/**
* Get potential file name, if file name is found it means we've hit a file.
* Otherwise we're dealing with a raw value
*/
const filename_match = disposition.match(RGX_FILENAME);
if (filename_match) {
const filename = filename_match[1];
if (config.files === null) {
/* Option: skip files entirely */
ctx.logger.debug('parseBody@multipart: skipping file due to allowFiles=false', { name, filename });
}
else if ((0, number_1.isInt)(config.files?.maxCount) && files.length >= config.files.maxCount) {
/* Option: Max file count */
ctx.logger.debug('parseBody@multipart: file skipped due to maxFileCount', { filename });
}
else if ((0, number_1.isInt)(config.files?.maxSize) && content.length > config.files.maxSize) {
/* Option: Max file size */
ctx.logger.debug('parseBody@multipart: file too large', { filename, size: content.length });
}
else if (content.length > 0) {
try {
const n_type = (type || constants_1.MimeTypes.BINARY);
/* Option: Allowed file types */
if (files_allowed && !files_allowed.has(n_type)) {
ctx.logger.debug('parseBody@multipart: disallowed type', { filename, type: n_type, size: content.length });
}
else {
form.append(name, new File([content], filename, { type: n_type }));
files.push(filename);
}
}
catch (err) {
ctx.logger.debug('parseBody@multipart: Failed to create File', { msg: err.message, filename, type });
}
}
else {
ctx.logger.debug('parseBody@multipart: Empty file skipped', { name, filename });
}
}
else {
form.append(name, decode(part_decoder, content));
}
}
/* Continue to next chunk in array */
start = end_idx + delimiter.length + enc_newline.length;
}
return form;
}
/**
* Parses a raw body into an object for use on a trifrost context
*/
async function parseBody(ctx, buf, config) {
if (!(buf instanceof Uint8Array))
return {};
const raw_type = typeof ctx.headers?.['content-type'] === 'string' ? ctx.headers['content-type'] : '';
const [mime, ...params] = raw_type.split(';');
const type = mime.trim().toLowerCase();
/* Determine charset */
let charset = 'utf-8';
for (let i = 0; i < params.length; i++) {
const [k, v] = params[i].trim().split('=');
if (k.toLowerCase() === 'charset' && v) {
charset = v.trim().toLowerCase();
break;
}
}
try {
/* Get decoder */
let strict_decoder;
if (charset in decoders) {
strict_decoder = decoders[charset];
}
else {
strict_decoder = new TextDecoder(charset, { ignoreBOM: true, fatal: true });
decoders[charset] = strict_decoder;
}
switch (type) {
case constants_1.MimeTypes.JSON:
case constants_1.MimeTypes.JSON_TEXT:
case constants_1.MimeTypes.JSON_LD:
if (isAboveLimit(ctx, buf, config.json?.limit, config.limit))
return null;
return JSON.parse(decode(strict_decoder, buf));
case constants_1.MimeTypes.JSON_ND: {
if (isAboveLimit(ctx, buf, config.json?.limit, config.limit))
return null;
const text = decode(strict_decoder, buf);
const lines = text.trim().split('\n');
const acc = [];
for (let i = 0; i < lines.length; i++)
acc.push(JSON.parse(lines[i]));
return { raw: acc };
}
case constants_1.MimeTypes.HTML:
case constants_1.MimeTypes.TEXT:
case constants_1.MimeTypes.CSV:
case constants_1.MimeTypes.XML:
case constants_1.MimeTypes.XML_TEXT:
if (isAboveLimit(ctx, buf, config.text?.limit, config.limit))
return null;
return { raw: decode(strict_decoder, buf) };
case constants_1.MimeTypes.FORM_URLENCODED: {
if (isAboveLimit(ctx, buf, config.form?.limit, config.limit))
return null;
const form = new FormData();
const parts = decode(strict_decoder, buf).split('&');
for (let i = 0; i < parts.length; i++) {
const [key, value] = parts[i].split('=');
if (key && value !== undefined) {
try {
form.append(decodeURIComponent(key), decodeURIComponent(value));
}
catch {
ctx.logger.debug('parseBody@form: Failed to decode', { key, value });
}
}
}
return (0, formdata_1.toObject)(form, {
raw: config.form?.normalizeRaw ?? [],
normalize_bool: config.form?.normalizeBool ?? true,
normalize_date: config.form?.normalizeDate ?? true,
normalize_null: config.form?.normalizeNull ?? true,
normalize_number: config.form?.normalizeNumber ?? false,
});
}
case constants_1.MimeTypes.FORM_MULTIPART: {
if (isAboveLimit(ctx, buf, config.form?.limit, config.limit))
return null;
const boundary = raw_type.match(RGX_BOUNDARY)?.[1];
if (!boundary)
throw new Error('multipart: Missing boundary');
const form = await parseMultipart(ctx, buf, boundary, strict_decoder, config.form || {});
return (0, formdata_1.toObject)(form, {
raw: config.form?.normalizeRaw ?? [],
normalize_bool: config.form?.normalizeBool ?? true,
normalize_date: config.form?.normalizeDate ?? true,
normalize_null: config.form?.normalizeNull ?? true,
normalize_number: config.form?.normalizeNumber ?? false,
});
}
default:
return isAboveLimit(ctx, buf, config.limit) ? null : { raw: buf };
}
}
catch (err) {
ctx.logger.debug('parseBody: Failed to parse', { type, msg: err.message });
return {};
}
}