Skip to content

Commit 7b327ea

Browse files
jasnelltargos
authored andcommitted
http2: add RFC 8441 extended connect protocol support
PR-URL: #23284 Reviewed-By: Refael Ackermann <[email protected]> Reviewed-By: Gus Caplan <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Trivikram Kamat <[email protected]>
1 parent 95bdf37 commit 7b327ea

9 files changed

+135
-6
lines changed

doc/api/http2.md

+36-1
Original file line numberDiff line numberDiff line change
@@ -2278,7 +2278,7 @@ not work.
22782278
For incoming headers:
22792279
* The `:status` header is converted to `number`.
22802280
* Duplicates of `:status`, `:method`, `:authority`, `:scheme`, `:path`,
2281-
`age`, `authorization`, `access-control-allow-credentials`,
2281+
`:protocol`, `age`, `authorization`, `access-control-allow-credentials`,
22822282
`access-control-max-age`, `access-control-request-method`, `content-encoding`,
22832283
`content-language`, `content-length`, `content-location`, `content-md5`,
22842284
`content-range`, `content-type`, `date`, `dnt`, `etag`, `expires`, `from`,
@@ -2335,6 +2335,10 @@ properties.
23352335
* `maxHeaderListSize` {number} Specifies the maximum size (uncompressed octets)
23362336
of header list that will be accepted. The minimum allowed value is 0. The
23372337
maximum allowed value is 2<sup>32</sup>-1. **Default:** `65535`.
2338+
* `enableConnectProtocol`{boolean} Specifies `true` if the "Extended Connect
2339+
Protocol" defined by [RFC 8441][] is to be enabled. This setting is only
2340+
meaningful if sent by the server. Once the `enableConnectProtocol` setting
2341+
has been enabled for a given `Http2Session`, it cannot be disabled.
23382342

23392343
All additional properties on the settings object are ignored.
23402344

@@ -2501,6 +2505,36 @@ req.on('end', () => {
25012505
req.end('Jane');
25022506
```
25032507

2508+
### The Extended CONNECT Protocol
2509+
2510+
[RFC 8441][] defines an "Extended CONNECT Protocol" extension to HTTP/2 that
2511+
may be used to bootstrap the use of an `Http2Stream` using the `CONNECT`
2512+
method as a tunnel for other communication protocols (such as WebSockets).
2513+
2514+
The use of the Extended CONNECT Protocol is enabled by HTTP/2 servers by using
2515+
the `enableConnectProtocol` setting:
2516+
2517+
```js
2518+
const http2 = require('http2');
2519+
const settings = { enableConnectProtocol: true };
2520+
const server = http2.createServer({ settings });
2521+
```
2522+
2523+
Once the client receives the `SETTINGS` frame from the server indicating that
2524+
the extended CONNECT may be used, it may send `CONNECT` requests that use the
2525+
`':protocol'` HTTP/2 pseudo-header:
2526+
2527+
```js
2528+
const http2 = require('http2');
2529+
const client = http2.connect('http://localhost:8080');
2530+
client.on('remoteSettings', (settings) => {
2531+
if (settings.enableConnectProtocol) {
2532+
const req = client.request({ ':method': 'CONNECT', ':protocol': 'foo' });
2533+
// ...
2534+
}
2535+
});
2536+
```
2537+
25042538
## Compatibility API
25052539

25062540
The Compatibility API has the goal of providing a similar developer experience
@@ -3361,6 +3395,7 @@ following additional properties:
33613395
[Readable Stream]: stream.html#stream_class_stream_readable
33623396
[RFC 7838]: https://tools.ietf.org/html/rfc7838
33633397
[RFC 8336]: https://tools.ietf.org/html/rfc8336
3398+
[RFC 8441]: https://tools.ietf.org/html/rfc8441
33643399
[Using `options.selectPadding()`]: #http2_using_options_selectpadding
33653400
[`'checkContinue'`]: #http2_event_checkcontinue
33663401
[`'request'`]: #http2_event_request

lib/internal/http2/core.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ const {
191191
HTTP2_HEADER_DATE,
192192
HTTP2_HEADER_METHOD,
193193
HTTP2_HEADER_PATH,
194+
HTTP2_HEADER_PROTOCOL,
194195
HTTP2_HEADER_SCHEME,
195196
HTTP2_HEADER_STATUS,
196197
HTTP2_HEADER_CONTENT_LENGTH,
@@ -1451,7 +1452,7 @@ class ClientHttp2Session extends Http2Session {
14511452

14521453
const connect = headers[HTTP2_HEADER_METHOD] === HTTP2_METHOD_CONNECT;
14531454

1454-
if (!connect) {
1455+
if (!connect || headers[HTTP2_HEADER_PROTOCOL] !== undefined) {
14551456
if (headers[HTTP2_HEADER_AUTHORITY] === undefined)
14561457
headers[HTTP2_HEADER_AUTHORITY] = this[kAuthority];
14571458
if (headers[HTTP2_HEADER_SCHEME] === undefined)

lib/internal/http2/util.js

+19-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const {
2020
HTTP2_HEADER_AUTHORITY,
2121
HTTP2_HEADER_SCHEME,
2222
HTTP2_HEADER_PATH,
23+
HTTP2_HEADER_PROTOCOL,
2324
HTTP2_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS,
2425
HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE,
2526
HTTP2_HEADER_ACCESS_CONTROL_REQUEST_METHOD,
@@ -78,7 +79,8 @@ const kValidPseudoHeaders = new Set([
7879
HTTP2_HEADER_METHOD,
7980
HTTP2_HEADER_AUTHORITY,
8081
HTTP2_HEADER_SCHEME,
81-
HTTP2_HEADER_PATH
82+
HTTP2_HEADER_PATH,
83+
HTTP2_HEADER_PROTOCOL
8284
]);
8385

8486
// This set contains headers that are permitted to have only a single
@@ -89,6 +91,7 @@ const kSingleValueHeaders = new Set([
8991
HTTP2_HEADER_AUTHORITY,
9092
HTTP2_HEADER_SCHEME,
9193
HTTP2_HEADER_PATH,
94+
HTTP2_HEADER_PROTOCOL,
9295
HTTP2_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS,
9396
HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE,
9497
HTTP2_HEADER_ACCESS_CONTROL_REQUEST_METHOD,
@@ -155,7 +158,8 @@ const IDX_SETTINGS_INITIAL_WINDOW_SIZE = 2;
155158
const IDX_SETTINGS_MAX_FRAME_SIZE = 3;
156159
const IDX_SETTINGS_MAX_CONCURRENT_STREAMS = 4;
157160
const IDX_SETTINGS_MAX_HEADER_LIST_SIZE = 5;
158-
const IDX_SETTINGS_FLAGS = 6;
161+
const IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL = 6;
162+
const IDX_SETTINGS_FLAGS = 7;
159163

160164
const IDX_SESSION_STATE_EFFECTIVE_LOCAL_WINDOW_SIZE = 0;
161165
const IDX_SESSION_STATE_EFFECTIVE_RECV_DATA_LENGTH = 1;
@@ -277,6 +281,12 @@ function getDefaultSettings() {
277281
settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE];
278282
}
279283

284+
if ((flags & (1 << IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL)) ===
285+
(1 << IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL)) {
286+
holder.enableConnectProtocol =
287+
settingsBuffer[IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL];
288+
}
289+
280290
return holder;
281291
}
282292

@@ -294,7 +304,8 @@ function getSettings(session, remote) {
294304
initialWindowSize: settingsBuffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE],
295305
maxFrameSize: settingsBuffer[IDX_SETTINGS_MAX_FRAME_SIZE],
296306
maxConcurrentStreams: settingsBuffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS],
297-
maxHeaderListSize: settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE]
307+
maxHeaderListSize: settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE],
308+
enableConnectProtocol: settingsBuffer[IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL]
298309
};
299310
}
300311

@@ -329,6 +340,11 @@ function updateSettingsBuffer(settings) {
329340
flags |= (1 << IDX_SETTINGS_ENABLE_PUSH);
330341
settingsBuffer[IDX_SETTINGS_ENABLE_PUSH] = Number(settings.enablePush);
331342
}
343+
if (typeof settings.enableConnectProtocol === 'boolean') {
344+
flags |= (1 << IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL);
345+
settingsBuffer[IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL] =
346+
Number(settings.enableConnectProtocol);
347+
}
332348

333349
settingsBuffer[IDX_SETTINGS_FLAGS] = flags;
334350
}

src/node_http2.cc

+4
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ void Http2Session::Http2Settings::Init() {
219219
GRABSETTING(INITIAL_WINDOW_SIZE, "initial window size");
220220
GRABSETTING(MAX_HEADER_LIST_SIZE, "max header list size");
221221
GRABSETTING(ENABLE_PUSH, "enable push");
222+
GRABSETTING(ENABLE_CONNECT_PROTOCOL, "enable connect protocol");
222223

223224
#undef GRABSETTING
224225

@@ -287,6 +288,8 @@ void Http2Session::Http2Settings::Update(Environment* env,
287288
fn(**session, NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE);
288289
buffer[IDX_SETTINGS_ENABLE_PUSH] =
289290
fn(**session, NGHTTP2_SETTINGS_ENABLE_PUSH);
291+
buffer[IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL] =
292+
fn(**session, NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL);
290293
}
291294

292295
// Initializes the shared TypedArray with the default settings values.
@@ -3091,6 +3094,7 @@ void Initialize(Local<Object> target,
30913094
NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE);
30923095
NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_MAX_FRAME_SIZE);
30933096
NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE);
3097+
NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL);
30943098

30953099
NODE_DEFINE_CONSTANT(constants, PADDING_STRATEGY_NONE);
30963100
NODE_DEFINE_CONSTANT(constants, PADDING_STRATEGY_ALIGNED);

src/node_http2.h

+1
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ struct nghttp2_header : public MemoryRetainer {
160160
V(AUTHORITY, ":authority") \
161161
V(SCHEME, ":scheme") \
162162
V(PATH, ":path") \
163+
V(PROTOCOL, ":protocol") \
163164
V(ACCEPT_CHARSET, "accept-charset") \
164165
V(ACCEPT_ENCODING, "accept-encoding") \
165166
V(ACCEPT_LANGUAGE, "accept-language") \

src/node_http2_state.h

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ namespace http2 {
1515
IDX_SETTINGS_MAX_FRAME_SIZE,
1616
IDX_SETTINGS_MAX_CONCURRENT_STREAMS,
1717
IDX_SETTINGS_MAX_HEADER_LIST_SIZE,
18+
IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL,
1819
IDX_SETTINGS_COUNT
1920
};
2021

test/parallel/test-http2-binding.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ const expectedHeaderNames = {
9797
HTTP2_HEADER_AUTHORITY: ':authority',
9898
HTTP2_HEADER_SCHEME: ':scheme',
9999
HTTP2_HEADER_PATH: ':path',
100+
HTTP2_HEADER_PROTOCOL: ':protocol',
100101
HTTP2_HEADER_DATE: 'date',
101102
HTTP2_HEADER_ACCEPT_CHARSET: 'accept-charset',
102103
HTTP2_HEADER_ACCEPT_ENCODING: 'accept-encoding',
@@ -217,7 +218,8 @@ const expectedNGConstants = {
217218
NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS: 3,
218219
NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE: 4,
219220
NGHTTP2_SETTINGS_MAX_FRAME_SIZE: 5,
220-
NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE: 6
221+
NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE: 6,
222+
NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL: 8
221223
};
222224

223225
const defaultSettings = {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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+
9+
const settings = { enableConnectProtocol: true };
10+
const server = http2.createServer({ settings });
11+
server.on('stream', common.mustNotCall());
12+
server.on('session', common.mustCall((session) => {
13+
// This will force the connection to close because once extended connect
14+
// is on, it cannot be turned off. The server is behaving badly.
15+
session.settings({ enableConnectProtocol: false });
16+
}));
17+
18+
server.listen(0, common.mustCall(() => {
19+
const client = http2.connect(`http://localhost:${server.address().port}`);
20+
client.on('remoteSettings', common.mustCall((settings) => {
21+
assert(settings.enableConnectProtocol);
22+
const req = client.request({
23+
':method': 'CONNECT',
24+
':protocol': 'foo'
25+
});
26+
req.on('error', common.mustCall(() => {
27+
server.close();
28+
}));
29+
}));
30+
}));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
9+
const settings = { enableConnectProtocol: true };
10+
const server = http2.createServer({ settings });
11+
server.on('stream', common.mustCall((stream, headers) => {
12+
assert.strictEqual(headers[':method'], 'CONNECT');
13+
assert.strictEqual(headers[':scheme'], 'http');
14+
assert.strictEqual(headers[':protocol'], 'foo');
15+
assert.strictEqual(headers[':authority'],
16+
`localhost:${server.address().port}`);
17+
assert.strictEqual(headers[':path'], '/');
18+
stream.respond();
19+
stream.end('ok');
20+
}));
21+
22+
server.listen(0, common.mustCall(() => {
23+
const client = http2.connect(`http://localhost:${server.address().port}`);
24+
client.on('remoteSettings', common.mustCall((settings) => {
25+
assert(settings.enableConnectProtocol);
26+
const req = client.request({
27+
':method': 'CONNECT',
28+
':protocol': 'foo'
29+
});
30+
req.resume();
31+
req.on('end', common.mustCall());
32+
req.on('close', common.mustCall(() => {
33+
assert.strictEqual(req.rstCode, 0);
34+
server.close();
35+
client.close();
36+
}));
37+
req.end();
38+
}));
39+
}));

0 commit comments

Comments
 (0)