Skip to content

http2: add lenient flag for RFC-9113 #58116

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
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
12 changes: 12 additions & 0 deletions doc/api/http2.md
Original file line number Diff line number Diff line change
Expand Up @@ -2899,6 +2899,10 @@ changes:
a server should wait when an [`'unknownProtocol'`][] is emitted. If the
socket has not been destroyed by that time the server will destroy it.
**Default:** `10000`.
* `strictFieldWhitespaceValidation` {boolean} If `true`, it turns on strict leading
and trailing whitespace validation for HTTP/2 header field names and values
as per [RFC-9113](https://www.rfc-editor.org/rfc/rfc9113.html#section-8.2.1).
**Default:** `true`.
* ...: Any [`net.createServer()`][] option can be provided.
* `onRequestHandler` {Function} See [Compatibility API][]
* Returns: {Http2Server}
Expand Down Expand Up @@ -3070,6 +3074,10 @@ changes:
a server should wait when an [`'unknownProtocol'`][] event is emitted. If
the socket has not been destroyed by that time the server will destroy it.
**Default:** `10000`.
* `strictFieldWhitespaceValidation` {boolean} If `true`, it turns on strict leading
and trailing whitespace validation for HTTP/2 header field names and values
as per [RFC-9113](https://www.rfc-editor.org/rfc/rfc9113.html#section-8.2.1).
**Default:** `true`.
* `onRequestHandler` {Function} See [Compatibility API][]
* Returns: {Http2SecureServer}

Expand Down Expand Up @@ -3225,6 +3233,10 @@ changes:
a server should wait when an [`'unknownProtocol'`][] event is emitted. If
the socket has not been destroyed by that time the server will destroy it.
**Default:** `10000`.
* `strictFieldWhitespaceValidation` {boolean} If `true`, it turns on strict leading
and trailing whitespace validation for HTTP/2 header field names and values
as per [RFC-9113](https://www.rfc-editor.org/rfc/rfc9113.html#section-8.2.1).
**Default:** `true`.
* `listener` {Function} Will be registered as a one-time listener of the
[`'connect'`][] event.
* Returns: {ClientHttp2Session}
Expand Down
10 changes: 9 additions & 1 deletion lib/internal/http2/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,8 @@ const IDX_OPTIONS_MAX_SESSION_MEMORY = 8;
const IDX_OPTIONS_MAX_SETTINGS = 9;
const IDX_OPTIONS_STREAM_RESET_RATE = 10;
const IDX_OPTIONS_STREAM_RESET_BURST = 11;
const IDX_OPTIONS_FLAGS = 12;
const IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION = 12;
const IDX_OPTIONS_FLAGS = 13;

function updateOptionsBuffer(options) {
let flags = 0;
Expand Down Expand Up @@ -293,6 +294,13 @@ function updateOptionsBuffer(options) {
optionsBuffer[IDX_OPTIONS_STREAM_RESET_BURST] =
MathMax(1, options.streamResetBurst);
}

if (typeof options.strictFieldWhitespaceValidation === 'boolean') {
flags |= (1 << IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION);
optionsBuffer[IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION] =
options.strictFieldWhitespaceValidation === true ? 0 : 1;
}

optionsBuffer[IDX_OPTIONS_FLAGS] = flags;
}

Expand Down
6 changes: 6 additions & 0 deletions src/node_http2.cc
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@ Http2Options::Http2Options(Http2State* http2_state, SessionType type) {
buffer[IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS]);
}

// Validate headers in accordance to RFC-9113
if (flags & (1 << IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION)) {
nghttp2_option_set_no_rfc9113_leading_and_trailing_ws_validation(
option, buffer[IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION]);
}

// The padding strategy sets the mechanism by which we determine how much
// additional frame padding to apply to DATA and HEADERS frames. Currently
// this is set on a per-session basis, but eventually we may switch to
Expand Down
1 change: 1 addition & 0 deletions src/node_http2_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ namespace http2 {
IDX_OPTIONS_MAX_SETTINGS,
IDX_OPTIONS_STREAM_RESET_RATE,
IDX_OPTIONS_STREAM_RESET_BURST,
IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION,
IDX_OPTIONS_FLAGS
};

Expand Down
80 changes: 80 additions & 0 deletions test/parallel/test-http2-server-rfc-9113-client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
'use strict';

const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const http2 = require('http2');
const body =
'<html><head></head><body><h1>this is some data</h2></body></html>';

const server = http2.createServer((req, res) => {
res.setHeader('foobar', 'baz ');
res.setHeader('X-POWERED-BY', 'node-test\t');
res.setHeader('x-h2-header', '\tconnection-test');
res.setHeader('x-h2-header-2', ' connection-test');
res.setHeader('x-h2-header-3', 'connection-test ');
res.end(body);
});

const server2 = http2.createServer((req, res) => {
res.setHeader('foobar', 'baz ');
res.setHeader('X-POWERED-BY', 'node-test\t');
res.setHeader('x-h2-header', '\tconnection-test');
res.setHeader('x-h2-header-2', ' connection-test');
res.setHeader('x-h2-header-3', 'connection-test ');
res.end(body);
});

server.listen(0, common.mustCall(() => {
server2.listen(0, common.mustCall(() => {
const client = http2.connect(`http://localhost:${server.address().port}`);
const client2 = http2.connect(`http://localhost:${server2.address().port}`, { strictFieldWhitespaceValidation: false });
const headers = { ':path': '/' };
const req = client.request(headers);

req.setEncoding('utf8');
req.on('response', common.mustCall(function(headers) {
assert.strictEqual(headers.foobar, undefined);
assert.strictEqual(headers['x-powered-by'], undefined);
assert.strictEqual(headers['x-powered-by'], undefined);
assert.strictEqual(headers['x-h2-header'], undefined);
assert.strictEqual(headers['x-h2-header-2'], undefined);
assert.strictEqual(headers['x-h2-header-3'], undefined);
}));

let data = '';
req.on('data', (d) => data += d);
req.on('end', () => {
assert.strictEqual(body, data);
client.close();
client.on('close', common.mustCall(() => {
server.close();
}));

const req2 = client2.request(headers);
let data2 = '';
req2.setEncoding('utf8');
req2.on('response', common.mustCall(function(headers) {
assert.strictEqual(headers.foobar, 'baz ');
assert.strictEqual(headers['x-powered-by'], 'node-test\t');
assert.strictEqual(headers['x-h2-header'], '\tconnection-test');
assert.strictEqual(headers['x-h2-header-2'], ' connection-test');
assert.strictEqual(headers['x-h2-header-3'], 'connection-test ');
}));
req2.on('data', (d) => data2 += d);
req2.on('end', () => {
assert.strictEqual(body, data2);
client2.close();
client2.on('close', common.mustCall(() => {
server2.close();
}));
});
req2.end();
});

req.end();
}));
}));

server.on('error', common.mustNotCall());
83 changes: 83 additions & 0 deletions test/parallel/test-http2-server-rfc-9113-server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
'use strict';

const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const http2 = require('http2');
const body =
'<html><head></head><body><h1>this is some data</h2></body></html>';

const server = http2.createServer((req, res) => {
assert.strictEqual(req.headers['x-powered-by'], undefined);
assert.strictEqual(req.headers.foobar, undefined);
assert.strictEqual(req.headers['x-h2-header'], undefined);
assert.strictEqual(req.headers['x-h2-header-2'], undefined);
assert.strictEqual(req.headers['x-h2-header-3'], undefined);
assert.strictEqual(req.headers['x-h2-header-4'], undefined);
res.writeHead(200);
res.end(body);
});

const server2 = http2.createServer({ strictFieldWhitespaceValidation: false }, (req, res) => {
assert.strictEqual(req.headers.foobar, 'baz ');
assert.strictEqual(req.headers['x-powered-by'], 'node-test\t');
assert.strictEqual(req.headers['x-h2-header'], '\tconnection-test');
assert.strictEqual(req.headers['x-h2-header-2'], ' connection-test');
assert.strictEqual(req.headers['x-h2-header-3'], 'connection-test ');
assert.strictEqual(req.headers['x-h2-header-4'], 'connection-test\t');
res.writeHead(200);
res.end(body);
});

server.listen(0, common.mustCall(() => {
server2.listen(0, common.mustCall(() => {
const client = http2.connect(`http://localhost:${server.address().port}`);
const client2 = http2.connect(`http://localhost:${server2.address().port}`);
const headers = {
'foobar': 'baz ',
':path': '/',
'x-powered-by': 'node-test\t',
'x-h2-header': '\tconnection-test',
'x-h2-header-2': ' connection-test',
'x-h2-header-3': 'connection-test ',
'x-h2-header-4': 'connection-test\t'
};
const req = client.request(headers);

req.setEncoding('utf8');
req.on('response', common.mustCall(function(headers) {
assert.strictEqual(headers[':status'], 200);
}));

let data = '';
req.on('data', (d) => data += d);
req.on('end', () => {
assert.strictEqual(body, data);
client.close();
client.on('close', common.mustCall(() => {
server.close();
}));

const req2 = client2.request(headers);
let data2 = '';
req2.setEncoding('utf8');
req2.on('response', common.mustCall(function(headers) {
assert.strictEqual(headers[':status'], 200);
}));
req2.on('data', (d) => data2 += d);
req2.on('end', () => {
assert.strictEqual(body, data2);
client2.close();
client2.on('close', common.mustCall(() => {
server2.close();
}));
});
req2.end();
});

req.end();
}));
}));

server.on('error', common.mustNotCall());
6 changes: 5 additions & 1 deletion test/parallel/test-http2-util-update-options-buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ const IDX_OPTIONS_MAX_SESSION_MEMORY = 8;
const IDX_OPTIONS_MAX_SETTINGS = 9;
const IDX_OPTIONS_STREAM_RESET_RATE = 10;
const IDX_OPTIONS_STREAM_RESET_BURST = 11;
const IDX_OPTIONS_FLAGS = 12;
const IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION = 12;
const IDX_OPTIONS_FLAGS = 13;

{
updateOptionsBuffer({
Expand All @@ -41,6 +42,7 @@ const IDX_OPTIONS_FLAGS = 12;
maxSettings: 10,
streamResetRate: 11,
streamResetBurst: 12,
strictFieldWhitespaceValidation: false
});

strictEqual(optionsBuffer[IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE], 1);
Expand All @@ -55,6 +57,7 @@ const IDX_OPTIONS_FLAGS = 12;
strictEqual(optionsBuffer[IDX_OPTIONS_MAX_SETTINGS], 10);
strictEqual(optionsBuffer[IDX_OPTIONS_STREAM_RESET_RATE], 11);
strictEqual(optionsBuffer[IDX_OPTIONS_STREAM_RESET_BURST], 12);
strictEqual(optionsBuffer[IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION], 1);

const flags = optionsBuffer[IDX_OPTIONS_FLAGS];

Expand All @@ -69,6 +72,7 @@ const IDX_OPTIONS_FLAGS = 12;
ok(flags & (1 << IDX_OPTIONS_MAX_SETTINGS));
ok(flags & (1 << IDX_OPTIONS_STREAM_RESET_RATE));
ok(flags & (1 << IDX_OPTIONS_STREAM_RESET_BURST));
ok(flags & (1 << IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION));
}

{
Expand Down
Loading