Files
Chris Tate c8b42772a8 fix(http): enforce HTTP/1.1 Host requirement (#388)
* fix(http): enforce HTTP/1.1 Host requirement

- Enforce Node's default Host behavior while preserving opt-out and HTTP/1.0 handling.

- Match the 400 wire response and add C/LLVM differential coverage.

* feat(http): add static HTTP trailer and streaming support

- Parse incoming chunk trailers and expose distinct/raw trailer reads.
- Frame outgoing trailers and add streaming controls with differential coverage.

* fix(http): harden request trailer parsing

- Safely discard trailers after early responses while preserving parser validation.
- Bound aggregate head and trailer parsing by bytes and field count.
- Add regressions for early response trailers and overflow rejection.
2026-09-23 10:21:53 -05:00

104 lines
3.5 KiB
TypeScript

import { createServer } from "node:http";
const server = createServer((req, res) => {
if (req.url === "/quit") {
res.end("bye");
server.close(() => console.log("server closed"));
return;
}
console.log(`start ${req.url} raw=${req.rawTrailers.length} note=${req.trailers["x-note"] === undefined ? "absent" : "present"}`);
if (req.url === "/early") {
res.end("early");
return;
}
let body = "";
req.on("data", (chunk: Buffer) => { body += chunk.toString("utf8"); });
req.on("end", () => {
if (req.url === "/incoming") {
const head = req.headersDistinct["x-head"];
const notes = req.trailersDistinct["x-note"];
const auth = req.trailersDistinct.authorization;
const note = req.trailers["x-note"];
const cookie = req.trailers.cookie;
const authorization = req.trailers.authorization;
console.log(`incoming body=${body} head=${head === undefined ? "-" : head.join("|")} note=${note === undefined ? "-" : note} cookie=${cookie === undefined ? "-" : cookie} auth=${authorization === undefined ? "-" : authorization}`);
console.log(`distinct notes=${notes === undefined ? "-" : notes.join("|")} auth=${auth === undefined ? "-" : auth.join("|")} raw=${req.rawTrailers.join("|")}`);
res.setHeader("Trailer", "X-Reply");
res.write("a");
res.addTrailers({ "X-Reply": "done" });
res.end("b");
return;
}
if (req.url === "/fixed") {
res.setHeader("Content-Length", "2");
res.addTrailers({ "X-Ignored": "hidden" });
res.end("ok");
return;
}
if (req.url === "/replace") {
res.setHeader("Trailer", "X-New");
res.write("a");
res.addTrailers({ "X-Old": "stale" });
res.addTrailers({ "X-New": "fresh" });
res.end();
return;
}
if (req.url === "/explicit") {
res.setHeader("Transfer-Encoding", "chunked");
res.setHeader("Trailer", "X-Explicit");
res.addTrailers({ "X-Explicit": "yes" });
res.end("ok");
return;
}
if (req.url === "/declared") {
res.setHeader("Trailer", "X-Later");
res.addTrailers({ "X-Later": "yes" });
res.end("ok");
return;
}
if (req.url === "/invalid-framing") {
res.setHeader("Trailer", "X-Later");
res.setHeader("Content-Length", "2");
try {
res.end("no");
} catch (err) {
console.log(`invalid framing: ${err instanceof Error ? (err as Error).message : "?"}`);
res.removeHeader("Content-Length");
res.addTrailers({ "X-Later": "yes" });
res.end("ok");
}
return;
}
try {
res.addTrailers({ "Bad Name": "bad" });
} catch (err) {
console.log(`invalid name: ${err instanceof TypeError ? (err as Error).message : "?"}`);
}
try {
res.addTrailers({ "X-Bad": "bad\nvalue" });
} catch (err) {
console.log(`invalid value: ${err instanceof TypeError ? (err as Error).message : "?"}`);
}
try {
res.addTrailers({ "X\u0000Bad": "bad" });
} catch (err) {
console.log(`invalid NUL name: ${err instanceof TypeError}`);
}
try {
res.addTrailers({ "X-Nul": "bad\u0000value" });
} catch (err) {
console.log(`invalid NUL value: ${err instanceof TypeError}`);
}
res.setHeader("Trailer", "X-Pair, X-Other");
res.flushHeaders();
res.addTrailers([["X-Pair", "one"], ["X-Other", "two"]]);
res.end("ok");
});
req.resume();
});
server.listen(0, () => {
console.log("listening");
process.stderr.write(`PORT ${server.address().port}\n`);
});