http2: add lenient flag for RFC-9113
PR-URL: https://github.com/nodejs/node/pull/58116 Reviewed-By: Tim Perry <pimterry@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
This commit is contained in:
parent
31e592631f
commit
e8a0f5b063
@ -2916,6 +2916,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}
|
||||
@ -3087,6 +3091,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}
|
||||
|
||||
@ -3242,6 +3250,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}
|
||||
|
@ -218,7 +218,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;
|
||||
@ -282,6 +283,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;
|
||||
}
|
||||
|
||||
|
@ -158,6 +158,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
|
||||
|
@ -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
|
||||
};
|
||||
|
||||
|
80
test/parallel/test-http2-server-rfc-9113-client.js
Normal file
80
test/parallel/test-http2-server-rfc-9113-client.js
Normal file
@ -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
test/parallel/test-http2-server-rfc-9113-server.js
Normal file
83
test/parallel/test-http2-server-rfc-9113-server.js
Normal file
@ -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());
|
@ -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({
|
||||
@ -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);
|
||||
@ -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];
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user