UNPKG

@wooksjs/http-body

Version:
126 lines (123 loc) 4.17 kB
import { EHttpStatusCode, HttpError, WooksURLSearchParams, useHeaders, useHttpContext, useRequest } from "@wooksjs/event-http"; //#region packages/http-body/src/utils/safe-json.ts const ILLEGAL_KEYS = [ "__proto__", "constructor", "prototype" ]; const illigalKeySet = new Set(ILLEGAL_KEYS); function safeJsonParse(src) { return JSON.parse(src, (key, value) => { assertKey(key); return value; }); } function assertKey(k) { if (illigalKeySet.has(k)) throw new HttpError(400, `Illegal key name "${k}"`); } //#endregion //#region packages/http-body/src/body.ts function useBody() { const { store } = useHttpContext(); const { init } = store("request"); const { rawBody } = useRequest(); const { "content-type": contentType } = useHeaders(); function contentIs(type) { return (contentType || "").includes(type); } const isJson = () => init("isJson", () => contentIs("application/json")); const isHtml = () => init("isHtml", () => contentIs("text/html")); const isXml = () => init("isXml", () => contentIs("text/xml")); const isText = () => init("isText", () => contentIs("text/plain")); const isBinary = () => init("isBinary", () => contentIs("application/octet-stream")); const isFormData = () => init("isFormData", () => contentIs("multipart/form-data")); const isUrlencoded = () => init("isUrlencoded", () => contentIs("application/x-www-form-urlencoded")); const parseBody = () => init("parsed", async () => { const body = await rawBody(); const sBody = body.toString(); if (isJson()) return jsonParser(sBody); else if (isFormData()) return formDataParser(sBody); else if (isUrlencoded()) return urlEncodedParser(sBody); else if (isBinary()) return textParser(sBody); else return textParser(sBody); }); function jsonParser(v) { try { return safeJsonParse(v); } catch (error) { throw new HttpError(400, error.message); } } function textParser(v) { return v; } function formDataParser(v) { const MAX_PARTS = 255; const MAX_KEY_LENGTH = 100; const MAX_VALUE_LENGTH = 100 * 1024; const boundary = `--${(/boundary=([^;]+)(?:;|$)/u.exec(contentType || "") || [, ""])[1]}`; if (!boundary) throw new HttpError(EHttpStatusCode.BadRequest, "form-data boundary not recognized"); const parts = v.trim().split(boundary); const result = Object.create(null); let key = ""; let partContentType = "text/plain"; let partCount = 0; for (const part of parts) { parsePart(); key = ""; partContentType = "text/plain"; if (!part.trim() || part.trim() === "--") continue; partCount++; if (partCount > MAX_PARTS) throw new HttpError(413, "Too many form fields"); let valueMode = false; const lines = part.trim().split(/\n/u).map((l) => l.trim()); for (const line of lines) { if (valueMode) { if (line.length + String(result[key] ?? "").length > MAX_VALUE_LENGTH) throw new HttpError(413, `Field "${key}" is too large`); result[key] = (result[key] ? `${result[key]}\n` : "") + line; continue; } if (!line) { valueMode = !!key; continue; } if (line.toLowerCase().startsWith("content-disposition: form-data;")) { key = (/name=([^;]+)/.exec(line) || [])[1].replace(/^["']|["']$/g, "") ?? ""; if (!key) throw new HttpError(400, `Could not read multipart name: ${line}`); if (key.length > MAX_KEY_LENGTH) throw new HttpError(413, "Field name too long"); if ([ "__proto__", "constructor", "prototype" ].includes(key)) throw new HttpError(400, `Illegal key name "${key}"`); continue; } if (line.toLowerCase().startsWith("content-type:")) { partContentType = (/content-type:\s?([^;]+)/i.exec(line) || [])[1] ?? ""; continue; } } } parsePart(); return result; function parsePart() { if (key && partContentType.includes("application/json") && typeof result[key] === "string") result[key] = safeJsonParse(result[key]); } } function urlEncodedParser(v) { return new WooksURLSearchParams(v.trim()).toJson(); } return { isJson, isHtml, isXml, isText, isBinary, isFormData, isUrlencoded, parseBody, rawBody }; } //#endregion export { useBody };