import type { Context, Middleware, RequestLimitsConfig } from "@wrnexus/core"; export type RequestHardeningOptions = RequestLimitsConfig; const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]); function headerBytes(headers: Headers): { count: number; bytes: number } { let count = 0; let bytes = 0; headers.forEach((value, name) => { count++; bytes += new TextEncoder().encode(`${name}:${value}\r\n`).byteLength; }); return { count, bytes }; } function hostAllowed(host: string, rules: string[]): boolean { const normalized = host.toLowerCase().split(":")[0]!; return rules.some((rule) => { const candidate = rule.toLowerCase(); return candidate.startsWith("*.") ? normalized.endsWith(candidate.slice(1)) : normalized === candidate; }); } function reject(status: number, message: string): Response { return new Response(message, { status, headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" }, }); } export function requestHardening(options: RequestHardeningOptions = {}): Middleware { const maxUrlLength = options.maxUrlLength ?? 8_192; const maxHeaderCount = options.maxHeaderCount ?? 100; const maxHeaderBytes = options.maxHeaderBytes ?? 32 * 1024; const maxQueryParameters = options.maxQueryParameters ?? 200; const maxBodyBytes = options.maxBodyBytes ?? 10 * 1024 * 1024; const timeoutMs = options.timeoutMs ?? 30_000; const maxConcurrent = options.maxConcurrent ?? 1_000; let concurrent = 0; return async (ctx: Context, next) => { if (ctx.req.url.length > maxUrlLength) return reject(414, "URI Too Long"); const measured = headerBytes(ctx.req.headers); if (measured.count > maxHeaderCount || measured.bytes > maxHeaderBytes) { return reject(431, "Request Header Fields Too Large"); } if ([...ctx.url.searchParams].length > maxQueryParameters) { return reject(400, "Too many query parameters"); } const contentLength = Number(ctx.req.headers.get("content-length") ?? "0"); if (Number.isFinite(contentLength) && contentLength > maxBodyBytes) { return reject(413, "Payload Too Large"); } if (options.trustedHosts?.length) { const host = ctx.req.headers.get("host") ?? ctx.url.host; if (!hostAllowed(host, options.trustedHosts)) return reject(421, "Misdirected Request"); } if (options.fetchMetadata !== false && !SAFE_METHODS.has(ctx.req.method.toUpperCase())) { const site = ctx.req.headers.get("sec-fetch-site"); if (site === "cross-site") return reject(403, "Cross-site request denied"); } if (concurrent >= maxConcurrent) return reject(503, "Server Busy"); concurrent++; let timer: ReturnType | undefined; try { const timeout = new Promise((resolve) => { timer = setTimeout(() => resolve(reject(504, "Request Timeout")), timeoutMs); }); return await Promise.race([Promise.resolve(next()), timeout]); } finally { if (timer) clearTimeout(timer); concurrent--; } }; }