mirror of
https://github.com/nodejs/node.git
synced 2024-11-21 10:59:27 +00:00
http2: add RFC 8441 extended connect protocol support
PR-URL: https://github.com/nodejs/node/pull/23284 Reviewed-By: Refael Ackermann <refack@gmail.com> Reviewed-By: Gus Caplan <me@gus.host> Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
This commit is contained in:
parent
8290015d0a
commit
0ad8c7319d
@ -2278,7 +2278,7 @@ not work.
|
||||
For incoming headers:
|
||||
* The `:status` header is converted to `number`.
|
||||
* Duplicates of `:status`, `:method`, `:authority`, `:scheme`, `:path`,
|
||||
`age`, `authorization`, `access-control-allow-credentials`,
|
||||
`:protocol`, `age`, `authorization`, `access-control-allow-credentials`,
|
||||
`access-control-max-age`, `access-control-request-method`, `content-encoding`,
|
||||
`content-language`, `content-length`, `content-location`, `content-md5`,
|
||||
`content-range`, `content-type`, `date`, `dnt`, `etag`, `expires`, `from`,
|
||||
@ -2335,6 +2335,10 @@ properties.
|
||||
* `maxHeaderListSize` {number} Specifies the maximum size (uncompressed octets)
|
||||
of header list that will be accepted. The minimum allowed value is 0. The
|
||||
maximum allowed value is 2<sup>32</sup>-1. **Default:** `65535`.
|
||||
* `enableConnectProtocol`{boolean} Specifies `true` if the "Extended Connect
|
||||
Protocol" defined by [RFC 8441][] is to be enabled. This setting is only
|
||||
meaningful if sent by the server. Once the `enableConnectProtocol` setting
|
||||
has been enabled for a given `Http2Session`, it cannot be disabled.
|
||||
|
||||
All additional properties on the settings object are ignored.
|
||||
|
||||
@ -2501,6 +2505,36 @@ req.on('end', () => {
|
||||
req.end('Jane');
|
||||
```
|
||||
|
||||
### The Extended CONNECT Protocol
|
||||
|
||||
[RFC 8441][] defines an "Extended CONNECT Protocol" extension to HTTP/2 that
|
||||
may be used to bootstrap the use of an `Http2Stream` using the `CONNECT`
|
||||
method as a tunnel for other communication protocols (such as WebSockets).
|
||||
|
||||
The use of the Extended CONNECT Protocol is enabled by HTTP/2 servers by using
|
||||
the `enableConnectProtocol` setting:
|
||||
|
||||
```js
|
||||
const http2 = require('http2');
|
||||
const settings = { enableConnectProtocol: true };
|
||||
const server = http2.createServer({ settings });
|
||||
```
|
||||
|
||||
Once the client receives the `SETTINGS` frame from the server indicating that
|
||||
the extended CONNECT may be used, it may send `CONNECT` requests that use the
|
||||
`':protocol'` HTTP/2 pseudo-header:
|
||||
|
||||
```js
|
||||
const http2 = require('http2');
|
||||
const client = http2.connect('http://localhost:8080');
|
||||
client.on('remoteSettings', (settings) => {
|
||||
if (settings.enableConnectProtocol) {
|
||||
const req = client.request({ ':method': 'CONNECT', ':protocol': 'foo' });
|
||||
// ...
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Compatibility API
|
||||
|
||||
The Compatibility API has the goal of providing a similar developer experience
|
||||
@ -3361,6 +3395,7 @@ following additional properties:
|
||||
[Readable Stream]: stream.html#stream_class_stream_readable
|
||||
[RFC 7838]: https://tools.ietf.org/html/rfc7838
|
||||
[RFC 8336]: https://tools.ietf.org/html/rfc8336
|
||||
[RFC 8441]: https://tools.ietf.org/html/rfc8441
|
||||
[Using `options.selectPadding()`]: #http2_using_options_selectpadding
|
||||
[`'checkContinue'`]: #http2_event_checkcontinue
|
||||
[`'request'`]: #http2_event_request
|
||||
|
@ -190,6 +190,7 @@ const {
|
||||
HTTP2_HEADER_DATE,
|
||||
HTTP2_HEADER_METHOD,
|
||||
HTTP2_HEADER_PATH,
|
||||
HTTP2_HEADER_PROTOCOL,
|
||||
HTTP2_HEADER_SCHEME,
|
||||
HTTP2_HEADER_STATUS,
|
||||
HTTP2_HEADER_CONTENT_LENGTH,
|
||||
@ -1450,7 +1451,7 @@ class ClientHttp2Session extends Http2Session {
|
||||
|
||||
const connect = headers[HTTP2_HEADER_METHOD] === HTTP2_METHOD_CONNECT;
|
||||
|
||||
if (!connect) {
|
||||
if (!connect || headers[HTTP2_HEADER_PROTOCOL] !== undefined) {
|
||||
if (headers[HTTP2_HEADER_AUTHORITY] === undefined)
|
||||
headers[HTTP2_HEADER_AUTHORITY] = this[kAuthority];
|
||||
if (headers[HTTP2_HEADER_SCHEME] === undefined)
|
||||
|
@ -20,6 +20,7 @@ const {
|
||||
HTTP2_HEADER_AUTHORITY,
|
||||
HTTP2_HEADER_SCHEME,
|
||||
HTTP2_HEADER_PATH,
|
||||
HTTP2_HEADER_PROTOCOL,
|
||||
HTTP2_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS,
|
||||
HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE,
|
||||
HTTP2_HEADER_ACCESS_CONTROL_REQUEST_METHOD,
|
||||
@ -78,7 +79,8 @@ const kValidPseudoHeaders = new Set([
|
||||
HTTP2_HEADER_METHOD,
|
||||
HTTP2_HEADER_AUTHORITY,
|
||||
HTTP2_HEADER_SCHEME,
|
||||
HTTP2_HEADER_PATH
|
||||
HTTP2_HEADER_PATH,
|
||||
HTTP2_HEADER_PROTOCOL
|
||||
]);
|
||||
|
||||
// This set contains headers that are permitted to have only a single
|
||||
@ -89,6 +91,7 @@ const kSingleValueHeaders = new Set([
|
||||
HTTP2_HEADER_AUTHORITY,
|
||||
HTTP2_HEADER_SCHEME,
|
||||
HTTP2_HEADER_PATH,
|
||||
HTTP2_HEADER_PROTOCOL,
|
||||
HTTP2_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS,
|
||||
HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE,
|
||||
HTTP2_HEADER_ACCESS_CONTROL_REQUEST_METHOD,
|
||||
@ -155,7 +158,8 @@ const IDX_SETTINGS_INITIAL_WINDOW_SIZE = 2;
|
||||
const IDX_SETTINGS_MAX_FRAME_SIZE = 3;
|
||||
const IDX_SETTINGS_MAX_CONCURRENT_STREAMS = 4;
|
||||
const IDX_SETTINGS_MAX_HEADER_LIST_SIZE = 5;
|
||||
const IDX_SETTINGS_FLAGS = 6;
|
||||
const IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL = 6;
|
||||
const IDX_SETTINGS_FLAGS = 7;
|
||||
|
||||
const IDX_SESSION_STATE_EFFECTIVE_LOCAL_WINDOW_SIZE = 0;
|
||||
const IDX_SESSION_STATE_EFFECTIVE_RECV_DATA_LENGTH = 1;
|
||||
@ -277,6 +281,12 @@ function getDefaultSettings() {
|
||||
settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE];
|
||||
}
|
||||
|
||||
if ((flags & (1 << IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL)) ===
|
||||
(1 << IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL)) {
|
||||
holder.enableConnectProtocol =
|
||||
settingsBuffer[IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL];
|
||||
}
|
||||
|
||||
return holder;
|
||||
}
|
||||
|
||||
@ -294,7 +304,8 @@ function getSettings(session, remote) {
|
||||
initialWindowSize: settingsBuffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE],
|
||||
maxFrameSize: settingsBuffer[IDX_SETTINGS_MAX_FRAME_SIZE],
|
||||
maxConcurrentStreams: settingsBuffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS],
|
||||
maxHeaderListSize: settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE]
|
||||
maxHeaderListSize: settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE],
|
||||
enableConnectProtocol: settingsBuffer[IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL]
|
||||
};
|
||||
}
|
||||
|
||||
@ -329,6 +340,11 @@ function updateSettingsBuffer(settings) {
|
||||
flags |= (1 << IDX_SETTINGS_ENABLE_PUSH);
|
||||
settingsBuffer[IDX_SETTINGS_ENABLE_PUSH] = Number(settings.enablePush);
|
||||
}
|
||||
if (typeof settings.enableConnectProtocol === 'boolean') {
|
||||
flags |= (1 << IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL);
|
||||
settingsBuffer[IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL] =
|
||||
Number(settings.enableConnectProtocol);
|
||||
}
|
||||
|
||||
settingsBuffer[IDX_SETTINGS_FLAGS] = flags;
|
||||
}
|
||||
|
@ -219,6 +219,7 @@ void Http2Session::Http2Settings::Init() {
|
||||
GRABSETTING(INITIAL_WINDOW_SIZE, "initial window size");
|
||||
GRABSETTING(MAX_HEADER_LIST_SIZE, "max header list size");
|
||||
GRABSETTING(ENABLE_PUSH, "enable push");
|
||||
GRABSETTING(ENABLE_CONNECT_PROTOCOL, "enable connect protocol");
|
||||
|
||||
#undef GRABSETTING
|
||||
|
||||
@ -287,6 +288,8 @@ void Http2Session::Http2Settings::Update(Environment* env,
|
||||
fn(**session, NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE);
|
||||
buffer[IDX_SETTINGS_ENABLE_PUSH] =
|
||||
fn(**session, NGHTTP2_SETTINGS_ENABLE_PUSH);
|
||||
buffer[IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL] =
|
||||
fn(**session, NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL);
|
||||
}
|
||||
|
||||
// Initializes the shared TypedArray with the default settings values.
|
||||
@ -3091,6 +3094,7 @@ void Initialize(Local<Object> target,
|
||||
NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE);
|
||||
NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_MAX_FRAME_SIZE);
|
||||
NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE);
|
||||
NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL);
|
||||
|
||||
NODE_DEFINE_CONSTANT(constants, PADDING_STRATEGY_NONE);
|
||||
NODE_DEFINE_CONSTANT(constants, PADDING_STRATEGY_ALIGNED);
|
||||
|
@ -160,6 +160,7 @@ struct nghttp2_header : public MemoryRetainer {
|
||||
V(AUTHORITY, ":authority") \
|
||||
V(SCHEME, ":scheme") \
|
||||
V(PATH, ":path") \
|
||||
V(PROTOCOL, ":protocol") \
|
||||
V(ACCEPT_CHARSET, "accept-charset") \
|
||||
V(ACCEPT_ENCODING, "accept-encoding") \
|
||||
V(ACCEPT_LANGUAGE, "accept-language") \
|
||||
|
@ -15,6 +15,7 @@ namespace http2 {
|
||||
IDX_SETTINGS_MAX_FRAME_SIZE,
|
||||
IDX_SETTINGS_MAX_CONCURRENT_STREAMS,
|
||||
IDX_SETTINGS_MAX_HEADER_LIST_SIZE,
|
||||
IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL,
|
||||
IDX_SETTINGS_COUNT
|
||||
};
|
||||
|
||||
|
@ -99,6 +99,7 @@ const expectedHeaderNames = {
|
||||
HTTP2_HEADER_AUTHORITY: ':authority',
|
||||
HTTP2_HEADER_SCHEME: ':scheme',
|
||||
HTTP2_HEADER_PATH: ':path',
|
||||
HTTP2_HEADER_PROTOCOL: ':protocol',
|
||||
HTTP2_HEADER_DATE: 'date',
|
||||
HTTP2_HEADER_ACCEPT_CHARSET: 'accept-charset',
|
||||
HTTP2_HEADER_ACCEPT_ENCODING: 'accept-encoding',
|
||||
@ -219,7 +220,8 @@ const expectedNGConstants = {
|
||||
NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS: 3,
|
||||
NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE: 4,
|
||||
NGHTTP2_SETTINGS_MAX_FRAME_SIZE: 5,
|
||||
NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE: 6
|
||||
NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE: 6,
|
||||
NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL: 8
|
||||
};
|
||||
|
||||
const defaultSettings = {
|
||||
|
@ -0,0 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
if (!common.hasCrypto)
|
||||
common.skip('missing crypto');
|
||||
const assert = require('assert');
|
||||
const http2 = require('http2');
|
||||
|
||||
const settings = { enableConnectProtocol: true };
|
||||
const server = http2.createServer({ settings });
|
||||
server.on('stream', common.mustNotCall());
|
||||
server.on('session', common.mustCall((session) => {
|
||||
// This will force the connection to close because once extended connect
|
||||
// is on, it cannot be turned off. The server is behaving badly.
|
||||
session.settings({ enableConnectProtocol: false });
|
||||
}));
|
||||
|
||||
server.listen(0, common.mustCall(() => {
|
||||
const client = http2.connect(`http://localhost:${server.address().port}`);
|
||||
client.on('remoteSettings', common.mustCall((settings) => {
|
||||
assert(settings.enableConnectProtocol);
|
||||
const req = client.request({
|
||||
':method': 'CONNECT',
|
||||
':protocol': 'foo'
|
||||
});
|
||||
req.on('error', common.mustCall(() => {
|
||||
server.close();
|
||||
}));
|
||||
}));
|
||||
}));
|
39
test/parallel/test-http2-connect-method-extended.js
Normal file
39
test/parallel/test-http2-connect-method-extended.js
Normal file
@ -0,0 +1,39 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
if (!common.hasCrypto)
|
||||
common.skip('missing crypto');
|
||||
const assert = require('assert');
|
||||
const http2 = require('http2');
|
||||
|
||||
const settings = { enableConnectProtocol: true };
|
||||
const server = http2.createServer({ settings });
|
||||
server.on('stream', common.mustCall((stream, headers) => {
|
||||
assert.strictEqual(headers[':method'], 'CONNECT');
|
||||
assert.strictEqual(headers[':scheme'], 'http');
|
||||
assert.strictEqual(headers[':protocol'], 'foo');
|
||||
assert.strictEqual(headers[':authority'],
|
||||
`localhost:${server.address().port}`);
|
||||
assert.strictEqual(headers[':path'], '/');
|
||||
stream.respond();
|
||||
stream.end('ok');
|
||||
}));
|
||||
|
||||
server.listen(0, common.mustCall(() => {
|
||||
const client = http2.connect(`http://localhost:${server.address().port}`);
|
||||
client.on('remoteSettings', common.mustCall((settings) => {
|
||||
assert(settings.enableConnectProtocol);
|
||||
const req = client.request({
|
||||
':method': 'CONNECT',
|
||||
':protocol': 'foo'
|
||||
});
|
||||
req.resume();
|
||||
req.on('end', common.mustCall());
|
||||
req.on('close', common.mustCall(() => {
|
||||
assert.strictEqual(req.rstCode, 0);
|
||||
server.close();
|
||||
client.close();
|
||||
}));
|
||||
req.end();
|
||||
}));
|
||||
}));
|
Loading…
Reference in New Issue
Block a user