Skip to content
Open
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
9 changes: 9 additions & 0 deletions lib/internal/quic/quic.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const {
PromiseResolve,
PromiseWithResolvers,
SafeSet,
String,
Symbol,
SymbolAsyncDispose,
SymbolAsyncIterator,
Expand Down Expand Up @@ -2065,6 +2066,14 @@ class QuicStream {
'The negotiated QUIC application protocol does not support headers');
}
validateObject(headers, 'headers');
const path = headers[':path'];
if (path !== undefined) {
const value = String(path);
if (value[0] !== '/' && value !== '*') {
Comment thread
Archkon marked this conversation as resolved.
throw new ERR_INVALID_ARG_VALUE(
'headers.:path', path, 'must start with "/"');
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not convinced this is the correct place for this validation. It is http specific while sendHeaders here could apply to other quic applications in the future.

}
const { terminal = false } = options;
const headerString = buildNgHeaderString(
headers, assertValidPseudoHeader, true /* strictSingleValueFields */);
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