Add RequestContext primitive (todo §0); harden static serving (HEAD, control-char, stream-error logging)
This commit is contained in:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user