diff --git a/src/quic/http3.cc b/src/quic/http3.cc index b6d876af60f6..93c236357f3d 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -19,6 +19,8 @@ #include "session.h" #include "sessionticket.h" +#include + namespace node { using v8::Array; @@ -544,9 +546,27 @@ class Http3ApplicationImpl final : public Session::Application { HeadersKind kind, const Local& headers, HeadersFlags flags = HeadersFlags::NONE) override { - Session::SendPendingDataScope send_scope(&session()); Http3Headers nva(env(), headers); + if (kind == HeadersKind::INITIAL) { + for (size_t n = 0; n < nva.length(); n++) { + const auto& header = nva.data()[n]; + const std::string_view name(reinterpret_cast(header.name), + header.namelen); + if (name != ":path") continue; + + const std::string_view path(reinterpret_cast(header.value), + header.valuelen); + if (!path.starts_with('/') && path != "*") { + THROW_ERR_INVALID_ARG_VALUE( + env(), "The :path header must start with \"/\" or be \"*\""); + return false; + } + } + } + + Session::SendPendingDataScope send_scope(&session()); + switch (kind) { case HeadersKind::HINTS: { if (!session().is_server()) { diff --git a/test/parallel/test-quic-h3-header-validation.mjs b/test/parallel/test-quic-h3-header-validation.mjs index 873991a89864..e6df8f91ff5b 100644 --- a/test/parallel/test-quic-h3-header-validation.mjs +++ b/test/parallel/test-quic-h3-header-validation.mjs @@ -34,13 +34,17 @@ const decoder = new TextDecoder(); // lowercased (buildNgHeaderString lowercases before passing to nghttp3). { const serverDone = Promise.withResolvers(); + let closedStreams = 0; const serverEndpoint = await listen(mustCall(async (ss) => { ss.onstream = mustCall(async (stream) => { await stream.closed; - ss.close(); - serverDone.resolve(); - }); + closedStreams++; + if (closedStreams === 2) { + ss.close(); + serverDone.resolve(); + } + }, 2); }), { sni: { '*': { keys: [key], certs: [cert] } }, onheaders: mustCall(function(headers) { @@ -51,6 +55,12 @@ const decoder = new TextDecoder(); `Header name "${name}" should be lowercase`); } + if (headers[':path'] === '*') { + assert.strictEqual(headers[':method'], 'OPTIONS'); + this.sendHeaders({ ':status': '204' }, { terminal: true }); + return; + } + // Verify specific headers arrived lowercased. assert.strictEqual(headers[':method'], 'GET'); assert.strictEqual(headers[':path'], '/test'); @@ -69,7 +79,7 @@ const decoder = new TextDecoder(); }); this.writer.writeSync('ok'); this.writer.endSync(); - }), + }, 2), }); const clientSession = await connect(serverEndpoint.address, { @@ -78,17 +88,18 @@ const decoder = new TextDecoder(); }); await clientSession.opened; + const requestHeaders = { + // Mixed-case names — should be lowercased by buildNgHeaderString. + ':method': 'GET', + ':path': '/test', + ':scheme': 'https', + ':authority': 'localhost', + 'X-Custom-Header': 'Value1', + 'Content-Type': 'text/plain', + 'X-Mixed-Case': 'MixedValue', + }; + const stream = await clientSession.createBidirectionalStream({ - headers: { - // Mixed-case names — should be lowercased by buildNgHeaderString. - ':method': 'GET', - ':path': '/test', - ':scheme': 'https', - ':authority': 'localhost', - 'X-Custom-Header': 'Value1', - 'Content-Type': 'text/plain', - 'X-Mixed-Case': 'MixedValue', - }, onheaders: mustCall(function(headers) { // Client should also receive lowercased response header names. assert.strictEqual(headers[':status'], '200'); @@ -103,9 +114,27 @@ const decoder = new TextDecoder(); }), }); + assert.throws(() => stream.sendHeaders({ + ...requestHeaders, + ':path': 'testwtpath', + }), { code: 'ERR_INVALID_ARG_VALUE' }); + assert.strictEqual( + stream.sendHeaders(requestHeaders, { terminal: true }), true); + + const asteriskStream = await clientSession.createBidirectionalStream(); + assert.strictEqual(asteriskStream.sendHeaders({ + ...requestHeaders, + ':method': 'OPTIONS', + ':path': '*', + }, { terminal: true }), true); + const body = await bytes(stream); assert.strictEqual(decoder.decode(body), 'ok'); - await Promise.all([stream.closed, serverDone.promise]); + await Promise.all([ + stream.closed, + asteriskStream.closed, + serverDone.promise, + ]); await clientSession.close(); await serverEndpoint.close(); }