fix(journal): don't crash the process on a malformed request URL
A request for path `//` (also `///`, `/\`, ...) makes `new URL(req.url, base)` throw ERR_INVALID_URL. serveStatic runs synchronously inside the createServer callback, so the throw is an uncaught exception that kills the process. Docker (`unless-stopped`) restarts it, and a client looping on `//` crash-loops the journal — a trivial unauthenticated DoS. This fired the "Container restart loop" Grafana alert in production (journal restarted ~10x in 6 minutes). Guard the URL parse with try/catch and fall through to the React Router handler, which 404s malformed paths cleanly (the same way it already handles scanner probes like /root/.ssh/id_rsa). Extract serveStatic into its own module so it can be unit-tested without booting the HTTP server, and add a regression test covering the malformed-path cases. Widen the journal vitest include to discover co-located tests for root-level server infra. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3d7dfda45f
commit
26d45cf2cb
4 changed files with 98 additions and 38 deletions
55
apps/journal/serve-static.ts
Normal file
55
apps/journal/serve-static.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { createReadStream, statSync } from "node:fs";
|
||||
import { join, extname, resolve } from "node:path";
|
||||
|
||||
const CLIENT_DIR = resolve(import.meta.dirname, "build", "client");
|
||||
|
||||
const MIME: Record<string, string> = {
|
||||
".js": "application/javascript",
|
||||
".css": "text/css",
|
||||
".html": "text/html",
|
||||
".json": "application/json",
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".svg": "image/svg+xml",
|
||||
".ico": "image/x-icon",
|
||||
".woff": "font/woff",
|
||||
".woff2": "font/woff2",
|
||||
};
|
||||
|
||||
/**
|
||||
* Serve a static asset from the client build. Returns true if the request
|
||||
* was handled, false if it should fall through to the app request handler.
|
||||
*
|
||||
* Runs synchronously inside the HTTP server callback, so it must never
|
||||
* throw: a thrown error here is an uncaught exception that crashes the
|
||||
* whole process. Malformed request paths (e.g. `//`, `///`, `/\`) make
|
||||
* `new URL` throw `ERR_INVALID_URL`; we catch that and fall through so
|
||||
* React Router 404s the request instead of taking the server down.
|
||||
*/
|
||||
export function serveStatic(req: IncomingMessage, res: ServerResponse): boolean {
|
||||
if (req.method !== "GET" && req.method !== "HEAD") return false;
|
||||
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
const filePath = resolve(join(CLIENT_DIR, url.pathname));
|
||||
|
||||
if (!filePath.startsWith(CLIENT_DIR)) return false;
|
||||
|
||||
try {
|
||||
if (!statSync(filePath).isFile()) return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
res.setHeader("Content-Type", MIME[extname(filePath)] ?? "application/octet-stream");
|
||||
if (url.pathname.startsWith("/assets/")) {
|
||||
res.setHeader("Cache-Control", "public, immutable, max-age=31536000");
|
||||
}
|
||||
createReadStream(filePath).pipe(res);
|
||||
return true;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue