Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion src/quic/http3.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
#include "session.h"
#include "sessionticket.h"

#include <string_view>

namespace node {

using v8::Array;
Expand Down Expand Up @@ -544,9 +546,27 @@ class Http3ApplicationImpl final : public Session::Application {
HeadersKind kind,
const Local<Array>& 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<const char*>(header.name),
header.namelen);
if (name != ":path") continue;

const std::string_view path(reinterpret_cast<const char*>(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()) {
Expand Down
59 changes: 44 additions & 15 deletions test/parallel/test-quic-h3-header-validation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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');
Expand All @@ -69,7 +79,7 @@ const decoder = new TextDecoder();
});
this.writer.writeSync('ok');
this.writer.endSync();
}),
}, 2),
});

const clientSession = await connect(serverEndpoint.address, {
Expand All @@ -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');
Expand All @@ -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();
}
Expand Down
Loading