Skip to content

Commit 44ca874

Browse files
metcoder95targos
authored andcommitted
http2: add lenient flag for RFC-9113
PR-URL: #58116 Reviewed-By: Tim Perry <pimterry@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
1 parent f8a2a1e commit 44ca874

File tree

7 files changed

+196
-2
lines changed

7 files changed

+196
-2
lines changed

doc/api/http2.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2919,6 +2919,10 @@ changes:
29192919
a server should wait when an [`'unknownProtocol'`][] is emitted. If the
29202920
socket has not been destroyed by that time the server will destroy it.
29212921
**Default:** `10000`.
2922+
* `strictFieldWhitespaceValidation` {boolean} If `true`, it turns on strict leading
2923+
and trailing whitespace validation for HTTP/2 header field names and values
2924+
as per [RFC-9113](https://www.rfc-editor.org/rfc/rfc9113.html#section-8.2.1).
2925+
**Default:** `true`.
29222926
* ...: Any [`net.createServer()`][] option can be provided.
29232927
* `onRequestHandler` {Function} See [Compatibility API][]
29242928
* Returns: {Http2Server}
@@ -3090,6 +3094,10 @@ changes:
30903094
a server should wait when an [`'unknownProtocol'`][] event is emitted. If
30913095
the socket has not been destroyed by that time the server will destroy it.
30923096
**Default:** `10000`.
3097+
* `strictFieldWhitespaceValidation` {boolean} If `true`, it turns on strict leading
3098+
and trailing whitespace validation for HTTP/2 header field names and values
3099+
as per [RFC-9113](https://www.rfc-editor.org/rfc/rfc9113.html#section-8.2.1).
3100+
**Default:** `true`.
30933101
* `onRequestHandler` {Function} See [Compatibility API][]
30943102
* Returns: {Http2SecureServer}
30953103

@@ -3245,6 +3253,10 @@ changes:
32453253
a server should wait when an [`'unknownProtocol'`][] event is emitted. If
32463254
the socket has not been destroyed by that time the server will destroy it.
32473255
**Default:** `10000`.
3256+
* `strictFieldWhitespaceValidation` {boolean} If `true`, it turns on strict leading
3257+
and trailing whitespace validation for HTTP/2 header field names and values
3258+
as per [RFC-9113](https://www.rfc-editor.org/rfc/rfc9113.html#section-8.2.1).
3259+
**Default:** `true`.
32483260
* `listener` {Function} Will be registered as a one-time listener of the
32493261
[`'connect'`][] event.
32503262
* Returns: {ClientHttp2Session}

lib/internal/http2/util.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,8 @@ const IDX_OPTIONS_MAX_SESSION_MEMORY = 8;
229229
const IDX_OPTIONS_MAX_SETTINGS = 9;
230230
const IDX_OPTIONS_STREAM_RESET_RATE = 10;
231231
const IDX_OPTIONS_STREAM_RESET_BURST = 11;
232-
const IDX_OPTIONS_FLAGS = 12;
232+
const IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION = 12;
233+
const IDX_OPTIONS_FLAGS = 13;
233234

234235
function updateOptionsBuffer(options) {
235236
let flags = 0;
@@ -293,6 +294,13 @@ function updateOptionsBuffer(options) {
293294
optionsBuffer[IDX_OPTIONS_STREAM_RESET_BURST] =
294295
MathMax(1, options.streamResetBurst);
295296
}
297+
298+
if (typeof options.strictFieldWhitespaceValidation === 'boolean') {
299+
flags |= (1 << IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION);
300+
optionsBuffer[IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION] =
301+
options.strictFieldWhitespaceValidation === true ? 0 : 1;
302+
}
303+
296304
optionsBuffer[IDX_OPTIONS_FLAGS] = flags;
297305
}
298306

src/node_http2.cc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,12 @@ Http2Options::Http2Options(Http2State* http2_state, SessionType type) {
159159
buffer[IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS]);
160160
}
161161

162+
// Validate headers in accordance to RFC-9113
163+
if (flags & (1 << IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION)) {
164+
nghttp2_option_set_no_rfc9113_leading_and_trailing_ws_validation(
165+
option, buffer[IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION]);
166+
}
167+
162168
// The padding strategy sets the mechanism by which we determine how much
163169
// additional frame padding to apply to DATA and HEADERS frames. Currently
164170
// this is set on a per-session basis, but eventually we may switch to

src/node_http2_state.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ namespace http2 {
6060
IDX_OPTIONS_MAX_SETTINGS,
6161
IDX_OPTIONS_STREAM_RESET_RATE,
6262
IDX_OPTIONS_STREAM_RESET_BURST,
63+
IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION,
6364
IDX_OPTIONS_FLAGS
6465
};
6566

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
const assert = require('assert');
7+
const http2 = require('http2');
8+
const body =
9+
'<html><head></head><body><h1>this is some data</h2></body></html>';
10+
11+
const server = http2.createServer((req, res) => {
12+
res.setHeader('foobar', 'baz ');
13+
res.setHeader('X-POWERED-BY', 'node-test\t');
14+
res.setHeader('x-h2-header', '\tconnection-test');
15+
res.setHeader('x-h2-header-2', ' connection-test');
16+
res.setHeader('x-h2-header-3', 'connection-test ');
17+
res.end(body);
18+
});
19+
20+
const server2 = http2.createServer((req, res) => {
21+
res.setHeader('foobar', 'baz ');
22+
res.setHeader('X-POWERED-BY', 'node-test\t');
23+
res.setHeader('x-h2-header', '\tconnection-test');
24+
res.setHeader('x-h2-header-2', ' connection-test');
25+
res.setHeader('x-h2-header-3', 'connection-test ');
26+
res.end(body);
27+
});
28+
29+
server.listen(0, common.mustCall(() => {
30+
server2.listen(0, common.mustCall(() => {
31+
const client = http2.connect(`http://localhost:${server.address().port}`);
32+
const client2 = http2.connect(`http://localhost:${server2.address().port}`, { strictFieldWhitespaceValidation: false });
33+
const headers = { ':path': '/' };
34+
const req = client.request(headers);
35+
36+
req.setEncoding('utf8');
37+
req.on('response', common.mustCall(function(headers) {
38+
assert.strictEqual(headers.foobar, undefined);
39+
assert.strictEqual(headers['x-powered-by'], undefined);
40+
assert.strictEqual(headers['x-powered-by'], undefined);
41+
assert.strictEqual(headers['x-h2-header'], undefined);
42+
assert.strictEqual(headers['x-h2-header-2'], undefined);
43+
assert.strictEqual(headers['x-h2-header-3'], undefined);
44+
}));
45+
46+
let data = '';
47+
req.on('data', (d) => data += d);
48+
req.on('end', () => {
49+
assert.strictEqual(body, data);
50+
client.close();
51+
client.on('close', common.mustCall(() => {
52+
server.close();
53+
}));
54+
55+
const req2 = client2.request(headers);
56+
let data2 = '';
57+
req2.setEncoding('utf8');
58+
req2.on('response', common.mustCall(function(headers) {
59+
assert.strictEqual(headers.foobar, 'baz ');
60+
assert.strictEqual(headers['x-powered-by'], 'node-test\t');
61+
assert.strictEqual(headers['x-h2-header'], '\tconnection-test');
62+
assert.strictEqual(headers['x-h2-header-2'], ' connection-test');
63+
assert.strictEqual(headers['x-h2-header-3'], 'connection-test ');
64+
}));
65+
req2.on('data', (d) => data2 += d);
66+
req2.on('end', () => {
67+
assert.strictEqual(body, data2);
68+
client2.close();
69+
client2.on('close', common.mustCall(() => {
70+
server2.close();
71+
}));
72+
});
73+
req2.end();
74+
});
75+
76+
req.end();
77+
}));
78+
}));
79+
80+
server.on('error', common.mustNotCall());
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
const assert = require('assert');
7+
const http2 = require('http2');
8+
const body =
9+
'<html><head></head><body><h1>this is some data</h2></body></html>';
10+
11+
const server = http2.createServer((req, res) => {
12+
assert.strictEqual(req.headers['x-powered-by'], undefined);
13+
assert.strictEqual(req.headers.foobar, undefined);
14+
assert.strictEqual(req.headers['x-h2-header'], undefined);
15+
assert.strictEqual(req.headers['x-h2-header-2'], undefined);
16+
assert.strictEqual(req.headers['x-h2-header-3'], undefined);
17+
assert.strictEqual(req.headers['x-h2-header-4'], undefined);
18+
res.writeHead(200);
19+
res.end(body);
20+
});
21+
22+
const server2 = http2.createServer({ strictFieldWhitespaceValidation: false }, (req, res) => {
23+
assert.strictEqual(req.headers.foobar, 'baz ');
24+
assert.strictEqual(req.headers['x-powered-by'], 'node-test\t');
25+
assert.strictEqual(req.headers['x-h2-header'], '\tconnection-test');
26+
assert.strictEqual(req.headers['x-h2-header-2'], ' connection-test');
27+
assert.strictEqual(req.headers['x-h2-header-3'], 'connection-test ');
28+
assert.strictEqual(req.headers['x-h2-header-4'], 'connection-test\t');
29+
res.writeHead(200);
30+
res.end(body);
31+
});
32+
33+
server.listen(0, common.mustCall(() => {
34+
server2.listen(0, common.mustCall(() => {
35+
const client = http2.connect(`http://localhost:${server.address().port}`);
36+
const client2 = http2.connect(`http://localhost:${server2.address().port}`);
37+
const headers = {
38+
'foobar': 'baz ',
39+
':path': '/',
40+
'x-powered-by': 'node-test\t',
41+
'x-h2-header': '\tconnection-test',
42+
'x-h2-header-2': ' connection-test',
43+
'x-h2-header-3': 'connection-test ',
44+
'x-h2-header-4': 'connection-test\t'
45+
};
46+
const req = client.request(headers);
47+
48+
req.setEncoding('utf8');
49+
req.on('response', common.mustCall(function(headers) {
50+
assert.strictEqual(headers[':status'], 200);
51+
}));
52+
53+
let data = '';
54+
req.on('data', (d) => data += d);
55+
req.on('end', () => {
56+
assert.strictEqual(body, data);
57+
client.close();
58+
client.on('close', common.mustCall(() => {
59+
server.close();
60+
}));
61+
62+
const req2 = client2.request(headers);
63+
let data2 = '';
64+
req2.setEncoding('utf8');
65+
req2.on('response', common.mustCall(function(headers) {
66+
assert.strictEqual(headers[':status'], 200);
67+
}));
68+
req2.on('data', (d) => data2 += d);
69+
req2.on('end', () => {
70+
assert.strictEqual(body, data2);
71+
client2.close();
72+
client2.on('close', common.mustCall(() => {
73+
server2.close();
74+
}));
75+
});
76+
req2.end();
77+
});
78+
79+
req.end();
80+
}));
81+
}));
82+
83+
server.on('error', common.mustNotCall());

test/parallel/test-http2-util-update-options-buffer.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ const IDX_OPTIONS_MAX_SESSION_MEMORY = 8;
2525
const IDX_OPTIONS_MAX_SETTINGS = 9;
2626
const IDX_OPTIONS_STREAM_RESET_RATE = 10;
2727
const IDX_OPTIONS_STREAM_RESET_BURST = 11;
28-
const IDX_OPTIONS_FLAGS = 12;
28+
const IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION = 12;
29+
const IDX_OPTIONS_FLAGS = 13;
2930

3031
{
3132
updateOptionsBuffer({
@@ -41,6 +42,7 @@ const IDX_OPTIONS_FLAGS = 12;
4142
maxSettings: 10,
4243
streamResetRate: 11,
4344
streamResetBurst: 12,
45+
strictFieldWhitespaceValidation: false
4446
});
4547

4648
strictEqual(optionsBuffer[IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE], 1);
@@ -55,6 +57,7 @@ const IDX_OPTIONS_FLAGS = 12;
5557
strictEqual(optionsBuffer[IDX_OPTIONS_MAX_SETTINGS], 10);
5658
strictEqual(optionsBuffer[IDX_OPTIONS_STREAM_RESET_RATE], 11);
5759
strictEqual(optionsBuffer[IDX_OPTIONS_STREAM_RESET_BURST], 12);
60+
strictEqual(optionsBuffer[IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION], 1);
5861

5962
const flags = optionsBuffer[IDX_OPTIONS_FLAGS];
6063

@@ -69,6 +72,7 @@ const IDX_OPTIONS_FLAGS = 12;
6972
ok(flags & (1 << IDX_OPTIONS_MAX_SETTINGS));
7073
ok(flags & (1 << IDX_OPTIONS_STREAM_RESET_RATE));
7174
ok(flags & (1 << IDX_OPTIONS_STREAM_RESET_BURST));
75+
ok(flags & (1 << IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION));
7276
}
7377

7478
{

0 commit comments

Comments
 (0)