Add RequestContext primitive (todo §0); harden static serving (HEAD, control-char, stream-error logging)

This commit is contained in:
2026-06-14 19:33:17 +02:00
parent b4c149db27
commit c544387d3a
12 changed files with 158 additions and 34 deletions

View File

@@ -22,8 +22,11 @@ export function contentTypeFor(filePath: string): string {
return contentTypes[extname(filePath).toLowerCase()] ?? "application/octet-stream";
}
// Resolves a request path inside `dir`, or null if it would escape (path traversal).
// Resolves a request path inside `dir`, or null if it would escape (traversal) or
// carries a control char (NUL etc.) — rejecting those here makes the guard explicit
// rather than relying on a downstream `stat` to throw.
export function resolveStaticPath(dir: string, requestedPath: string): string | null {
if (/[\x00-\x1f]/.test(requestedPath)) return null;
const filePath = join(dir, requestedPath);
const rel = relative(dir, filePath);
return rel.startsWith("..") || isAbsolute(rel) ? null : filePath;
@@ -33,7 +36,7 @@ function plain(res: ServerResponse, status: number, body: string): void {
res.writeHead(status, { "content-type": "text/plain; charset=utf-8" }).end(body);
}
export async function serveStatic(dir: string, requestedPath: string, res: ServerResponse): Promise<void> {
export async function serveStatic(dir: string, requestedPath: string, res: ServerResponse, head = false): Promise<void> {
let decoded: string;
try {
decoded = decodeURIComponent(requestedPath);
@@ -48,10 +51,14 @@ export async function serveStatic(dir: string, requestedPath: string, res: Serve
const info = await stat(filePath);
if (!info.isFile()) return plain(res, 404, "Not Found");
res.writeHead(200, { "content-length": info.size, "content-type": contentTypeFor(filePath) });
if (head) return void res.end(); // headers only — skip opening the file
// Headers are already sent, so a mid-stream read error can't become an HTTP error —
// destroy the response to signal a truncated body instead of leaving the socket open.
// log it and destroy the response to signal a truncated body, not a hung socket.
createReadStream(filePath)
.on("error", () => res.destroy())
.on("error", (err) => {
console.error(err);
res.destroy();
})
.pipe(res);
} catch {
plain(res, 404, "Not Found");