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:
James M Snell 2018-10-05 15:09:07 -07:00
parent 8290015d0a
commit 0ad8c7319d
No known key found for this signature in database
GPG Key ID: 7341B15C070877AC
9 changed files with 135 additions and 6 deletions

View File

@ -2278,7 +2278,7 @@ not work.
For incoming headers: For incoming headers:
* The `:status` header is converted to `number`. * The `:status` header is converted to `number`.
* Duplicates of `:status`, `:method`, `:authority`, `:scheme`, `:path`, * 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`, `access-control-max-age`, `access-control-request-method`, `content-encoding`,
`content-language`, `content-length`, `content-location`, `content-md5`, `content-language`, `content-length`, `content-location`, `content-md5`,
`content-range`, `content-type`, `date`, `dnt`, `etag`, `expires`, `from`, `content-range`, `content-type`, `date`, `dnt`, `etag`, `expires`, `from`,
@ -2335,6 +2335,10 @@ properties.
* `maxHeaderListSize` {number} Specifies the maximum size (uncompressed octets) * `maxHeaderListSize` {number} Specifies the maximum size (uncompressed octets)
of header list that will be accepted. The minimum allowed value is 0. The 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`. 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. All additional properties on the settings object are ignored.
@ -2501,6 +2505,36 @@ req.on('end', () => {
req.end('Jane'); 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 ## Compatibility API
The Compatibility API has the goal of providing a similar developer experience 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 [Readable Stream]: stream.html#stream_class_stream_readable
[RFC 7838]: https://tools.ietf.org/html/rfc7838 [RFC 7838]: https://tools.ietf.org/html/rfc7838
[RFC 8336]: https://tools.ietf.org/html/rfc8336 [RFC 8336]: https://tools.ietf.org/html/rfc8336
[RFC 8441]: https://tools.ietf.org/html/rfc8441
[Using `options.selectPadding()`]: #http2_using_options_selectpadding [Using `options.selectPadding()`]: #http2_using_options_selectpadding
[`'checkContinue'`]: #http2_event_checkcontinue [`'checkContinue'`]: #http2_event_checkcontinue
[`'request'`]: #http2_event_request [`'request'`]: #http2_event_request

View File

@ -190,6 +190,7 @@ const {
HTTP2_HEADER_DATE, HTTP2_HEADER_DATE,
HTTP2_HEADER_METHOD, HTTP2_HEADER_METHOD,
HTTP2_HEADER_PATH, HTTP2_HEADER_PATH,
HTTP2_HEADER_PROTOCOL,
HTTP2_HEADER_SCHEME, HTTP2_HEADER_SCHEME,
HTTP2_HEADER_STATUS, HTTP2_HEADER_STATUS,
HTTP2_HEADER_CONTENT_LENGTH, HTTP2_HEADER_CONTENT_LENGTH,
@ -1450,7 +1451,7 @@ class ClientHttp2Session extends Http2Session {
const connect = headers[HTTP2_HEADER_METHOD] === HTTP2_METHOD_CONNECT; const connect = headers[HTTP2_HEADER_METHOD] === HTTP2_METHOD_CONNECT;
if (!connect) { if (!connect || headers[HTTP2_HEADER_PROTOCOL] !== undefined) {
if (headers[HTTP2_HEADER_AUTHORITY] === undefined) if (headers[HTTP2_HEADER_AUTHORITY] === undefined)
headers[HTTP2_HEADER_AUTHORITY] = this[kAuthority]; headers[HTTP2_HEADER_AUTHORITY] = this[kAuthority];
if (headers[HTTP2_HEADER_SCHEME] === undefined) if (headers[HTTP2_HEADER_SCHEME] === undefined)

View File

@ -20,6 +20,7 @@ const {
HTTP2_HEADER_AUTHORITY, HTTP2_HEADER_AUTHORITY,
HTTP2_HEADER_SCHEME, HTTP2_HEADER_SCHEME,
HTTP2_HEADER_PATH, HTTP2_HEADER_PATH,
HTTP2_HEADER_PROTOCOL,
HTTP2_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, HTTP2_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS,
HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE, HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE,
HTTP2_HEADER_ACCESS_CONTROL_REQUEST_METHOD, HTTP2_HEADER_ACCESS_CONTROL_REQUEST_METHOD,
@ -78,7 +79,8 @@ const kValidPseudoHeaders = new Set([
HTTP2_HEADER_METHOD, HTTP2_HEADER_METHOD,
HTTP2_HEADER_AUTHORITY, HTTP2_HEADER_AUTHORITY,
HTTP2_HEADER_SCHEME, HTTP2_HEADER_SCHEME,
HTTP2_HEADER_PATH HTTP2_HEADER_PATH,
HTTP2_HEADER_PROTOCOL
]); ]);
// This set contains headers that are permitted to have only a single // 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_AUTHORITY,
HTTP2_HEADER_SCHEME, HTTP2_HEADER_SCHEME,
HTTP2_HEADER_PATH, HTTP2_HEADER_PATH,
HTTP2_HEADER_PROTOCOL,
HTTP2_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, HTTP2_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS,
HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE, HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE,
HTTP2_HEADER_ACCESS_CONTROL_REQUEST_METHOD, 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_FRAME_SIZE = 3;
const IDX_SETTINGS_MAX_CONCURRENT_STREAMS = 4; const IDX_SETTINGS_MAX_CONCURRENT_STREAMS = 4;
const IDX_SETTINGS_MAX_HEADER_LIST_SIZE = 5; 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_LOCAL_WINDOW_SIZE = 0;
const IDX_SESSION_STATE_EFFECTIVE_RECV_DATA_LENGTH = 1; const IDX_SESSION_STATE_EFFECTIVE_RECV_DATA_LENGTH = 1;
@ -277,6 +281,12 @@ function getDefaultSettings() {
settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE]; 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; return holder;
} }
@ -294,7 +304,8 @@ function getSettings(session, remote) {
initialWindowSize: settingsBuffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE], initialWindowSize: settingsBuffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE],
maxFrameSize: settingsBuffer[IDX_SETTINGS_MAX_FRAME_SIZE], maxFrameSize: settingsBuffer[IDX_SETTINGS_MAX_FRAME_SIZE],
maxConcurrentStreams: settingsBuffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS], 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); flags |= (1 << IDX_SETTINGS_ENABLE_PUSH);
settingsBuffer[IDX_SETTINGS_ENABLE_PUSH] = Number(settings.enablePush); 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; settingsBuffer[IDX_SETTINGS_FLAGS] = flags;
} }

View File

@ -219,6 +219,7 @@ void Http2Session::Http2Settings::Init() {
GRABSETTING(INITIAL_WINDOW_SIZE, "initial window size"); GRABSETTING(INITIAL_WINDOW_SIZE, "initial window size");
GRABSETTING(MAX_HEADER_LIST_SIZE, "max header list size"); GRABSETTING(MAX_HEADER_LIST_SIZE, "max header list size");
GRABSETTING(ENABLE_PUSH, "enable push"); GRABSETTING(ENABLE_PUSH, "enable push");
GRABSETTING(ENABLE_CONNECT_PROTOCOL, "enable connect protocol");
#undef GRABSETTING #undef GRABSETTING
@ -287,6 +288,8 @@ void Http2Session::Http2Settings::Update(Environment* env,
fn(**session, NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE); fn(**session, NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE);
buffer[IDX_SETTINGS_ENABLE_PUSH] = buffer[IDX_SETTINGS_ENABLE_PUSH] =
fn(**session, NGHTTP2_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. // 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_INITIAL_WINDOW_SIZE);
NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_MAX_FRAME_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_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_NONE);
NODE_DEFINE_CONSTANT(constants, PADDING_STRATEGY_ALIGNED); NODE_DEFINE_CONSTANT(constants, PADDING_STRATEGY_ALIGNED);

View File

@ -160,6 +160,7 @@ struct nghttp2_header : public MemoryRetainer {
V(AUTHORITY, ":authority") \ V(AUTHORITY, ":authority") \
V(SCHEME, ":scheme") \ V(SCHEME, ":scheme") \
V(PATH, ":path") \ V(PATH, ":path") \
V(PROTOCOL, ":protocol") \
V(ACCEPT_CHARSET, "accept-charset") \ V(ACCEPT_CHARSET, "accept-charset") \
V(ACCEPT_ENCODING, "accept-encoding") \ V(ACCEPT_ENCODING, "accept-encoding") \
V(ACCEPT_LANGUAGE, "accept-language") \ V(ACCEPT_LANGUAGE, "accept-language") \

View File

@ -15,6 +15,7 @@ namespace http2 {
IDX_SETTINGS_MAX_FRAME_SIZE, IDX_SETTINGS_MAX_FRAME_SIZE,
IDX_SETTINGS_MAX_CONCURRENT_STREAMS, IDX_SETTINGS_MAX_CONCURRENT_STREAMS,
IDX_SETTINGS_MAX_HEADER_LIST_SIZE, IDX_SETTINGS_MAX_HEADER_LIST_SIZE,
IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL,
IDX_SETTINGS_COUNT IDX_SETTINGS_COUNT
}; };

View File

@ -99,6 +99,7 @@ const expectedHeaderNames = {
HTTP2_HEADER_AUTHORITY: ':authority', HTTP2_HEADER_AUTHORITY: ':authority',
HTTP2_HEADER_SCHEME: ':scheme', HTTP2_HEADER_SCHEME: ':scheme',
HTTP2_HEADER_PATH: ':path', HTTP2_HEADER_PATH: ':path',
HTTP2_HEADER_PROTOCOL: ':protocol',
HTTP2_HEADER_DATE: 'date', HTTP2_HEADER_DATE: 'date',
HTTP2_HEADER_ACCEPT_CHARSET: 'accept-charset', HTTP2_HEADER_ACCEPT_CHARSET: 'accept-charset',
HTTP2_HEADER_ACCEPT_ENCODING: 'accept-encoding', HTTP2_HEADER_ACCEPT_ENCODING: 'accept-encoding',
@ -219,7 +220,8 @@ const expectedNGConstants = {
NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS: 3, NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS: 3,
NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE: 4, NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE: 4,
NGHTTP2_SETTINGS_MAX_FRAME_SIZE: 5, 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 = { const defaultSettings = {

View File

@ -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();
}));
}));
}));

View 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();
}));
}));