Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
http2: add RFC 8441 extended connect protocol support
  • Loading branch information
jasnell committed Oct 5, 2018
commit 5c008e8a631c5c234953484cbab4c5a2f188a948
37 changes: 36 additions & 1 deletion doc/api/http2.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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' });
// ...
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just when it got interesting ;) How does one use this feature? Is the idea that req is a raw Duplex stream now and another protocol can use it as its underlying transport stream?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Effectively yeah... RFC 8441 says that the HTTP/2 Stream is to be used in place of the Socket... so here, the Http2Stream (on both client and server) is the underlying transport... layering FTW!

There are some challenges, however, in that the ws WebSocket implementation has a number of timing and API expectations built in that don't quite match up but, for the most part, it just kinda works.

}
});
```

## Compatibility API

The Compatibility API has the goal of providing a similar developer experience
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion lib/internal/http2/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
22 changes: 19 additions & 3 deletions lib/internal/http2/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand All @@ -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]
};
}

Expand Down Expand Up @@ -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;
}
Expand Down
4 changes: 4 additions & 0 deletions src/node_http2.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/node_http2.h
Original file line number Diff line number Diff line change
Expand Up @@ -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") \
Expand Down
1 change: 1 addition & 0 deletions src/node_http2_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
};

Expand Down
4 changes: 3 additions & 1 deletion test/parallel/test-http2-binding.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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 = {
Expand Down
30 changes: 30 additions & 0 deletions test/parallel/test-http2-connect-method-extended-cant-turn-off.js
Original file line number Diff line number Diff line change
@@ -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 changes: 39 additions & 0 deletions test/parallel/test-http2-connect-method-extended.js
Original file line number Diff line number Diff line change
@@ -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();
}));
}));