Skip to content
Open
49 changes: 44 additions & 5 deletions src/js/internal/http1_server_fallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,14 @@ function createHttp1FallbackResponseHandle(socket, shouldKeepAlive, keepAliveTim
// 'request' with http.IncomingMessage/ServerResponse, like Node's httpConnectionListener routing.
function connectionListenerHTTP1(server, socket, options) {
const http = require("node:http");
const { HTTPParser, prepareError, calculateLenientFlags, continueExpression } = require("node:_http_common");
const {
HTTPParser,
prepareError,
calculateLenientFlags,
continueExpression,
parserOnHeaders,
MAX_HEADER_PAIRS,
} = require("node:_http_common");
const { kHandle: kHttp1ResponseHandle } = require("internal/http");
const { allMethods } = process.binding("http_parser");

Expand All @@ -251,6 +258,7 @@ function connectionListenerHTTP1(server, socket, options) {
connections.add(socket);
socket[kHttp1ActiveRequests] = 0;

const kOnHeaders = HTTPParser.kOnHeaders | 0;
const kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0;
const kOnBody = HTTPParser.kOnBody | 0;
const kOnMessageComplete = HTTPParser.kOnMessageComplete | 0;
Expand All @@ -262,10 +270,12 @@ function connectionListenerHTTP1(server, socket, options) {
parser.initialize(HTTPParser.REQUEST, {}, server.maxHeaderSize || 0, lenientFlags);
parser.socket = socket;
socket.parser = parser;
// Blocks past the parser's 32-field buffer, and trailers, arrive via kOnHeaders (as in _http_common).
parser._headers = [];
parser._url = "";
const { maxHeadersCount } = server;
if (typeof maxHeadersCount === "number") {
parser.maxHeaderPairs = maxHeadersCount << 1;
}
parser.maxHeaderPairs = typeof maxHeadersCount === "number" ? maxHeadersCount << 1 : MAX_HEADER_PAIRS;
parser[kOnHeaders] = parserOnHeaders;

let req = null;
let pendingUpgrade = null;
Expand All @@ -281,6 +291,19 @@ function connectionListenerHTTP1(server, socket, options) {
upgrade,
shouldKeepAlive,
) {
// Both are undefined once anything went through kOnHeaders (_http_common's parserOnHeadersComplete).
if (rawHeaders === undefined) {
rawHeaders = parser._headers;
parser._headers = [];
}
if (url === undefined) {
url = parser._url;
parser._url = "";
}
let headersLength = rawHeaders.length;
const { maxHeaderPairs } = parser;
if (maxHeaderPairs > 0) headersLength = Math.min(headersLength, maxHeaderPairs);

socket[kHttp1ActiveRequests]++;

req = new IncomingMessageClass(socket);
Expand All @@ -291,7 +314,7 @@ function connectionListenerHTTP1(server, socket, options) {
req.url = url;
req.method = typeof methodNum === "number" ? allMethods[methodNum] : methodNum;
req.upgrade = upgrade;
req._addHeaderLines(rawHeaders, rawHeaders.length);
req._addHeaderLines(rawHeaders, headersLength);

// Node's parserOnIncoming: upgrade only sticks for CONNECT or when an 'upgrade' listener
// exists; otherwise fall through to normal dispatch. Returning 2 makes llhttp stop after
Expand All @@ -318,6 +341,10 @@ function connectionListenerHTTP1(server, socket, options) {
// path must carry them too or keep-alive responses lose their timeout line.
res._keepAliveTimeout = keepAliveTimeout;
res._maxRequestsPerSocket = server.maxRequestsPerSocket;
// Node's parserOnIncoming (RFC 9112 §3.2): answered with a 400 below and the connection closed.
const missingHostHeader =
versionMajor === 1 && versionMinor === 1 && server.requireHostHeader && req.headers.host === undefined;
if (missingHostHeader) shouldKeepAlive = false;
const handle = createHttp1FallbackResponseHandle(socket, shouldKeepAlive, keepAliveTimeout);
handle.onfinished = function () {
socket[kHttp1ActiveRequests] = Math.max(0, (socket[kHttp1ActiveRequests] || 1) - 1);
Expand All @@ -334,6 +361,12 @@ function connectionListenerHTTP1(server, socket, options) {
this.detachSocket(socket);
});

if (missingHostHeader) {
res.writeHead(400, { Connection: "close" });
res.end();
return 0;
}

// Node's parserOnIncoming Expect routing (the native dispatcher applies the
// same at _http_server.ts's DISPATCH_HAS_EXPECT branch).
const expect = req.headers.expect;
Expand All @@ -360,8 +393,14 @@ function connectionListenerHTTP1(server, socket, options) {
if (req && !req._dumped) req.push(chunk);
};
parser[kOnMessageComplete] = function onHttp1MessageComplete() {
// Collected after the header block, so these are trailers (_http_common's parserOnMessageComplete).
const trailers = parser._headers;
const trailersLength = trailers.length;
if (trailersLength !== 0) parser._headers = [];
parser._url = "";
if (req) {
req.complete = true;
if (trailersLength !== 0) req._addHeaderLines(trailers, trailersLength);
req.push(null);
}
};
Expand Down
2 changes: 2 additions & 0 deletions src/js/node/_http_common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,8 @@ export default {
freeParser,
methods,
parsers,
parserOnHeaders,
MAX_HEADER_PAIRS,
kIncomingMessage,
kSkipPendingData,
HTTPParser,
Expand Down
3 changes: 3 additions & 0 deletions src/js/node/http2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6804,6 +6804,9 @@ class Http2SecureServer extends tls.Server {
this.requestTimeout = http1Options.requestTimeout ?? 300000;
this.maxHeadersCount = http1Options.maxHeadersCount ?? null;
this.maxRequestsPerSocket = http1Options.maxRequestsPerSocket ?? 0;
const requireHostHeader = http1Options.requireHostHeader;
if (requireHostHeader !== undefined) validateBoolean(requireHostHeader, "options.requireHostHeader");
this.requireHostHeader = requireHostHeader ?? true;
// connectionListenerHTTP1 reads these off the server when initializing
// the per-connection parser, matching Node's storeHTTP1Options.
this.maxHeaderSize = http1Options.maxHeaderSize;
Expand Down
Loading