From f6cfc139f8b63121d5bc3e703cb2c720dc883ce0 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Fri, 5 Oct 2018 13:09:10 -0700 Subject: [PATCH 1/2] deps: update nghttp2 to 1.34.0 Key new feature: RFC 8441 `:protocol` support --- deps/nghttp2/lib/includes/nghttp2/nghttp2.h | 7 +++- .../nghttp2/lib/includes/nghttp2/nghttp2ver.h | 4 +- deps/nghttp2/lib/nghttp2_frame.c | 5 +++ deps/nghttp2/lib/nghttp2_hd.c | 20 +++++++--- deps/nghttp2/lib/nghttp2_hd.h | 1 + deps/nghttp2/lib/nghttp2_helper.c | 4 +- deps/nghttp2/lib/nghttp2_http.c | 39 ++++++++++++------- deps/nghttp2/lib/nghttp2_session.c | 35 +++++++++++++++++ deps/nghttp2/lib/nghttp2_session.h | 4 ++ deps/nghttp2/lib/nghttp2_stream.h | 3 +- 10 files changed, 96 insertions(+), 26 deletions(-) diff --git a/deps/nghttp2/lib/includes/nghttp2/nghttp2.h b/deps/nghttp2/lib/includes/nghttp2/nghttp2.h index 8c54b9c8cc464d..e7198b3d27314d 100644 --- a/deps/nghttp2/lib/includes/nghttp2/nghttp2.h +++ b/deps/nghttp2/lib/includes/nghttp2/nghttp2.h @@ -680,7 +680,12 @@ typedef enum { /** * SETTINGS_MAX_HEADER_LIST_SIZE */ - NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE = 0x06 + NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE = 0x06, + /** + * SETTINGS_ENABLE_CONNECT_PROTOCOL + * (`RFC 8441 `_) + */ + NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL = 0x08 } nghttp2_settings_id; /* Note: If we add SETTINGS, update the capacity of NGHTTP2_INBOUND_NUM_IV as well */ diff --git a/deps/nghttp2/lib/includes/nghttp2/nghttp2ver.h b/deps/nghttp2/lib/includes/nghttp2/nghttp2ver.h index 1f1d4808ca27c0..420adbd53dddce 100644 --- a/deps/nghttp2/lib/includes/nghttp2/nghttp2ver.h +++ b/deps/nghttp2/lib/includes/nghttp2/nghttp2ver.h @@ -29,7 +29,7 @@ * @macro * Version number of the nghttp2 library release */ -#define NGHTTP2_VERSION "1.33.0" +#define NGHTTP2_VERSION "1.34.0" /** * @macro @@ -37,6 +37,6 @@ * release. This is a 24 bit number with 8 bits for major number, 8 bits * for minor and 8 bits for patch. Version 1.2.3 becomes 0x010203. */ -#define NGHTTP2_VERSION_NUM 0x012100 +#define NGHTTP2_VERSION_NUM 0x012200 #endif /* NGHTTP2VER_H */ diff --git a/deps/nghttp2/lib/nghttp2_frame.c b/deps/nghttp2/lib/nghttp2_frame.c index 6e33f3c247f5cb..4821de40885736 100644 --- a/deps/nghttp2/lib/nghttp2_frame.c +++ b/deps/nghttp2/lib/nghttp2_frame.c @@ -1050,6 +1050,11 @@ int nghttp2_iv_check(const nghttp2_settings_entry *iv, size_t niv) { break; case NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE: break; + case NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL: + if (iv[i].value != 0 && iv[i].value != 1) { + return 0; + } + break; } } return 1; diff --git a/deps/nghttp2/lib/nghttp2_hd.c b/deps/nghttp2/lib/nghttp2_hd.c index 1eb3be33802c44..a61f0d47a6f79d 100644 --- a/deps/nghttp2/lib/nghttp2_hd.c +++ b/deps/nghttp2/lib/nghttp2_hd.c @@ -45,7 +45,7 @@ /* 3rd parameter is nghttp2_token value for header field name. We use first enum value if same header names are repeated (e.g., :status). */ -static nghttp2_hd_static_entry static_table[] = { +static const nghttp2_hd_static_entry static_table[] = { MAKE_STATIC_ENT(":authority", "", 0, 3153725150u), MAKE_STATIC_ENT(":method", "GET", 1, 695666056u), MAKE_STATIC_ENT(":method", "POST", 1, 695666056u), @@ -271,6 +271,15 @@ static int32_t lookup_token(const uint8_t *name, size_t namelen) { break; } break; + case 9: + switch (name[8]) { + case 'l': + if (memeq(":protoco", name, 8)) { + return NGHTTP2_TOKEN__PROTOCOL; + } + break; + } + break; case 10: switch (name[9]) { case 'e': @@ -1159,7 +1168,7 @@ static search_result search_static_table(const nghttp2_nv *nv, int32_t token, int name_only) { search_result res = {token, 0}; int i; - nghttp2_hd_static_entry *ent; + const nghttp2_hd_static_entry *ent; if (name_only) { return res; @@ -1184,7 +1193,7 @@ static search_result search_hd_table(nghttp2_hd_context *context, int indexing_mode, nghttp2_hd_map *map, uint32_t hash) { search_result res = {-1, 0}; - nghttp2_hd_entry *ent; + const nghttp2_hd_entry *ent; int exact_match; int name_only = indexing_mode == NGHTTP2_HD_NEVER_INDEXING; @@ -1289,8 +1298,9 @@ nghttp2_hd_nv nghttp2_hd_table_get(nghttp2_hd_context *context, size_t idx) { return hd_ringbuf_get(&context->hd_table, idx - NGHTTP2_STATIC_TABLE_LENGTH) ->nv; } else { - nghttp2_hd_static_entry *ent = &static_table[idx]; - nghttp2_hd_nv nv = {&ent->name, &ent->value, ent->token, + const nghttp2_hd_static_entry *ent = &static_table[idx]; + nghttp2_hd_nv nv = {(nghttp2_rcbuf *)&ent->name, + (nghttp2_rcbuf *)&ent->value, ent->token, NGHTTP2_NV_FLAG_NONE}; return nv; } diff --git a/deps/nghttp2/lib/nghttp2_hd.h b/deps/nghttp2/lib/nghttp2_hd.h index c64a1f2b9b406c..14ae98078957af 100644 --- a/deps/nghttp2/lib/nghttp2_hd.h +++ b/deps/nghttp2/lib/nghttp2_hd.h @@ -111,6 +111,7 @@ typedef enum { NGHTTP2_TOKEN_KEEP_ALIVE, NGHTTP2_TOKEN_PROXY_CONNECTION, NGHTTP2_TOKEN_UPGRADE, + NGHTTP2_TOKEN__PROTOCOL, } nghttp2_token; struct nghttp2_hd_entry; diff --git a/deps/nghttp2/lib/nghttp2_helper.c b/deps/nghttp2/lib/nghttp2_helper.c index 3b282c7301f95b..81a8a0cf99971a 100644 --- a/deps/nghttp2/lib/nghttp2_helper.c +++ b/deps/nghttp2/lib/nghttp2_helper.c @@ -340,7 +340,7 @@ const char *nghttp2_strerror(int error_code) { } /* Generated by gennmchartbl.py */ -static int VALID_HD_NAME_CHARS[] = { +static const int VALID_HD_NAME_CHARS[] = { 0 /* NUL */, 0 /* SOH */, 0 /* STX */, 0 /* ETX */, 0 /* EOT */, 0 /* ENQ */, 0 /* ACK */, 0 /* BEL */, 0 /* BS */, 0 /* HT */, 0 /* LF */, 0 /* VT */, @@ -428,7 +428,7 @@ int nghttp2_check_header_name(const uint8_t *name, size_t len) { } /* Generated by genvchartbl.py */ -static int VALID_HD_VALUE_CHARS[] = { +static const int VALID_HD_VALUE_CHARS[] = { 0 /* NUL */, 0 /* SOH */, 0 /* STX */, 0 /* ETX */, 0 /* EOT */, 0 /* ENQ */, 0 /* ACK */, 0 /* BEL */, 0 /* BS */, 1 /* HT */, 0 /* LF */, 0 /* VT */, diff --git a/deps/nghttp2/lib/nghttp2_http.c b/deps/nghttp2/lib/nghttp2_http.c index b08f8863f7ce16..6e8acfdcc141af 100644 --- a/deps/nghttp2/lib/nghttp2_http.c +++ b/deps/nghttp2/lib/nghttp2_http.c @@ -113,7 +113,7 @@ static int check_path(nghttp2_stream *stream) { } static int http_request_on_header(nghttp2_stream *stream, nghttp2_hd_nv *nv, - int trailer) { + int trailer, int connect_protocol) { if (nv->name->base[0] == ':') { if (trailer || (stream->http_flags & NGHTTP2_HTTP_FLAG_PSEUDO_HEADER_DISALLOWED)) { @@ -146,10 +146,6 @@ static int http_request_on_header(nghttp2_stream *stream, nghttp2_hd_nv *nv, return NGHTTP2_ERR_HTTP_HEADER; } stream->http_flags |= NGHTTP2_HTTP_FLAG_METH_CONNECT; - if (stream->http_flags & - (NGHTTP2_HTTP_FLAG__PATH | NGHTTP2_HTTP_FLAG__SCHEME)) { - return NGHTTP2_ERR_HTTP_HEADER; - } } break; case 'S': @@ -162,9 +158,6 @@ static int http_request_on_header(nghttp2_stream *stream, nghttp2_hd_nv *nv, } break; case NGHTTP2_TOKEN__PATH: - if (stream->http_flags & NGHTTP2_HTTP_FLAG_METH_CONNECT) { - return NGHTTP2_ERR_HTTP_HEADER; - } if (!check_pseudo_header(stream, nv, NGHTTP2_HTTP_FLAG__PATH)) { return NGHTTP2_ERR_HTTP_HEADER; } @@ -175,9 +168,6 @@ static int http_request_on_header(nghttp2_stream *stream, nghttp2_hd_nv *nv, } break; case NGHTTP2_TOKEN__SCHEME: - if (stream->http_flags & NGHTTP2_HTTP_FLAG_METH_CONNECT) { - return NGHTTP2_ERR_HTTP_HEADER; - } if (!check_pseudo_header(stream, nv, NGHTTP2_HTTP_FLAG__SCHEME)) { return NGHTTP2_ERR_HTTP_HEADER; } @@ -186,6 +176,15 @@ static int http_request_on_header(nghttp2_stream *stream, nghttp2_hd_nv *nv, stream->http_flags |= NGHTTP2_HTTP_FLAG_SCHEME_HTTP; } break; + case NGHTTP2_TOKEN__PROTOCOL: + if (!connect_protocol) { + return NGHTTP2_ERR_HTTP_HEADER; + } + + if (!check_pseudo_header(stream, nv, NGHTTP2_HTTP_FLAG__PROTOCOL)) { + return NGHTTP2_ERR_HTTP_HEADER; + } + break; case NGHTTP2_TOKEN_HOST: if (!check_pseudo_header(stream, nv, NGHTTP2_HTTP_FLAG_HOST)) { return NGHTTP2_ERR_HTTP_HEADER; @@ -265,7 +264,7 @@ static int http_response_on_header(nghttp2_stream *stream, nghttp2_hd_nv *nv, return NGHTTP2_ERR_REMOVE_HTTP_HEADER; } if (stream->status_code / 100 == 1 || - (stream->status_code == 200 && + (stream->status_code / 100 == 2 && (stream->http_flags & NGHTTP2_HTTP_FLAG_METH_CONNECT))) { return NGHTTP2_ERR_HTTP_HEADER; } @@ -458,7 +457,9 @@ int nghttp2_http_on_header(nghttp2_session *session, nghttp2_stream *stream, } if (session->server || frame->hd.type == NGHTTP2_PUSH_PROMISE) { - return http_request_on_header(stream, nv, trailer); + return http_request_on_header(stream, nv, trailer, + session->server && + session->pending_enable_connect_protocol); } return http_response_on_header(stream, nv, trailer); @@ -466,8 +467,11 @@ int nghttp2_http_on_header(nghttp2_session *session, nghttp2_stream *stream, int nghttp2_http_on_request_headers(nghttp2_stream *stream, nghttp2_frame *frame) { - if (stream->http_flags & NGHTTP2_HTTP_FLAG_METH_CONNECT) { - if ((stream->http_flags & NGHTTP2_HTTP_FLAG__AUTHORITY) == 0) { + if (!(stream->http_flags & NGHTTP2_HTTP_FLAG__PROTOCOL) && + (stream->http_flags & NGHTTP2_HTTP_FLAG_METH_CONNECT)) { + if ((stream->http_flags & + (NGHTTP2_HTTP_FLAG__SCHEME | NGHTTP2_HTTP_FLAG__PATH)) || + (stream->http_flags & NGHTTP2_HTTP_FLAG__AUTHORITY) == 0) { return -1; } stream->content_length = -1; @@ -478,6 +482,11 @@ int nghttp2_http_on_request_headers(nghttp2_stream *stream, (NGHTTP2_HTTP_FLAG__AUTHORITY | NGHTTP2_HTTP_FLAG_HOST)) == 0) { return -1; } + if ((stream->http_flags & NGHTTP2_HTTP_FLAG__PROTOCOL) && + ((stream->http_flags & NGHTTP2_HTTP_FLAG_METH_CONNECT) == 0 || + (stream->http_flags & NGHTTP2_HTTP_FLAG__AUTHORITY) == 0)) { + return -1; + } if (!check_path(stream)) { return -1; } diff --git a/deps/nghttp2/lib/nghttp2_session.c b/deps/nghttp2/lib/nghttp2_session.c index 418ad6663585f5..ef4932af4e4f58 100644 --- a/deps/nghttp2/lib/nghttp2_session.c +++ b/deps/nghttp2/lib/nghttp2_session.c @@ -4361,6 +4361,9 @@ int nghttp2_session_update_local_settings(nghttp2_session *session, case NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE: session->local_settings.max_header_list_size = iv[i].value; break; + case NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL: + session->local_settings.enable_connect_protocol = iv[i].value; + break; } } @@ -4499,6 +4502,26 @@ int nghttp2_session_on_settings_received(nghttp2_session *session, session->remote_settings.max_header_list_size = entry->value; + break; + case NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL: + + if (entry->value != 0 && entry->value != 1) { + return session_handle_invalid_connection( + session, frame, NGHTTP2_ERR_PROTO, + "SETTINGS: invalid SETTINGS_ENABLE_CONNECT_PROTOCOL"); + } + + if (!session->server && + session->remote_settings.enable_connect_protocol && + entry->value == 0) { + return session_handle_invalid_connection( + session, frame, NGHTTP2_ERR_PROTO, + "SETTINGS: server attempted to disable " + "SETTINGS_ENABLE_CONNECT_PROTOCOL"); + } + + session->remote_settings.enable_connect_protocol = entry->value; + break; } } @@ -5250,6 +5273,7 @@ static void inbound_frame_set_settings_entry(nghttp2_inbound_frame *iframe) { case NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE: case NGHTTP2_SETTINGS_MAX_FRAME_SIZE: case NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE: + case NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL: break; default: DEBUGF("recv: unknown settings id=0x%02x\n", iv.settings_id); @@ -7052,6 +7076,13 @@ int nghttp2_session_add_settings(nghttp2_session *session, uint8_t flags, } } + for (i = niv; i > 0; --i) { + if (iv[i - 1].settings_id == NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL) { + session->pending_enable_connect_protocol = (uint8_t)iv[i - 1].value; + break; + } + } + return 0; } @@ -7360,6 +7391,8 @@ uint32_t nghttp2_session_get_remote_settings(nghttp2_session *session, return session->remote_settings.max_frame_size; case NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE: return session->remote_settings.max_header_list_size; + case NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL: + return session->remote_settings.enable_connect_protocol; } assert(0); @@ -7381,6 +7414,8 @@ uint32_t nghttp2_session_get_local_settings(nghttp2_session *session, return session->local_settings.max_frame_size; case NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE: return session->local_settings.max_header_list_size; + case NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL: + return session->local_settings.enable_connect_protocol; } assert(0); diff --git a/deps/nghttp2/lib/nghttp2_session.h b/deps/nghttp2/lib/nghttp2_session.h index 5add50bc8bce16..40a8865a04b12f 100644 --- a/deps/nghttp2/lib/nghttp2_session.h +++ b/deps/nghttp2/lib/nghttp2_session.h @@ -164,6 +164,7 @@ typedef struct { uint32_t initial_window_size; uint32_t max_frame_size; uint32_t max_header_list_size; + uint32_t enable_connect_protocol; } nghttp2_settings_storage; typedef enum { @@ -321,6 +322,9 @@ struct nghttp2_session { /* Unacked local ENABLE_PUSH value. We use this to refuse PUSH_PROMISE before SETTINGS ACK is received. */ uint8_t pending_enable_push; + /* Unacked local ENABLE_CONNECT_PROTOCOL value. We use this to + accept :protocol header field before SETTINGS_ACK is received. */ + uint8_t pending_enable_connect_protocol; /* Nonzero if the session is server side. */ uint8_t server; /* Flags indicating GOAWAY is sent and/or received. The flags are diff --git a/deps/nghttp2/lib/nghttp2_stream.h b/deps/nghttp2/lib/nghttp2_stream.h index d1d5856d800e76..fb8dc14d67be6d 100644 --- a/deps/nghttp2/lib/nghttp2_stream.h +++ b/deps/nghttp2/lib/nghttp2_stream.h @@ -130,7 +130,8 @@ typedef enum { /* "http" or "https" scheme */ NGHTTP2_HTTP_FLAG_SCHEME_HTTP = 1 << 13, /* set if final response is expected */ - NGHTTP2_HTTP_FLAG_EXPECT_FINAL_RESPONSE = 1 << 14 + NGHTTP2_HTTP_FLAG_EXPECT_FINAL_RESPONSE = 1 << 14, + NGHTTP2_HTTP_FLAG__PROTOCOL = 1 << 15, } nghttp2_http_flag; struct nghttp2_stream { From 5c008e8a631c5c234953484cbab4c5a2f188a948 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Fri, 5 Oct 2018 15:09:07 -0700 Subject: [PATCH 2/2] http2: add RFC 8441 extended connect protocol support --- doc/api/http2.md | 37 +++++++++++++++++- lib/internal/http2/core.js | 3 +- lib/internal/http2/util.js | 22 +++++++++-- src/node_http2.cc | 4 ++ src/node_http2.h | 1 + src/node_http2_state.h | 1 + test/parallel/test-http2-binding.js | 4 +- ...2-connect-method-extended-cant-turn-off.js | 30 ++++++++++++++ .../test-http2-connect-method-extended.js | 39 +++++++++++++++++++ 9 files changed, 135 insertions(+), 6 deletions(-) create mode 100644 test/parallel/test-http2-connect-method-extended-cant-turn-off.js create mode 100644 test/parallel/test-http2-connect-method-extended.js diff --git a/doc/api/http2.md b/doc/api/http2.md index 698f1bf2d87e58..e1e82c9dce3456 100644 --- a/doc/api/http2.md +++ b/doc/api/http2.md @@ -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 232-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 diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index b1ed1eee8ff448..b08008857ec062 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -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) diff --git a/lib/internal/http2/util.js b/lib/internal/http2/util.js index 99466b36d3b321..94dc1198ea1060 100644 --- a/lib/internal/http2/util.js +++ b/lib/internal/http2/util.js @@ -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; } diff --git a/src/node_http2.cc b/src/node_http2.cc index 44353e2d57414c..2c339d7249562e 100644 --- a/src/node_http2.cc +++ b/src/node_http2.cc @@ -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 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); diff --git a/src/node_http2.h b/src/node_http2.h index 2ab452bf02aaa8..8ecca63aeb0c0e 100644 --- a/src/node_http2.h +++ b/src/node_http2.h @@ -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") \ diff --git a/src/node_http2_state.h b/src/node_http2_state.h index 64a0942f7ffa67..d21d0f90096074 100644 --- a/src/node_http2_state.h +++ b/src/node_http2_state.h @@ -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 }; diff --git a/test/parallel/test-http2-binding.js b/test/parallel/test-http2-binding.js index ae19149d1bc74d..6991f98afd72d1 100644 --- a/test/parallel/test-http2-binding.js +++ b/test/parallel/test-http2-binding.js @@ -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 = { diff --git a/test/parallel/test-http2-connect-method-extended-cant-turn-off.js b/test/parallel/test-http2-connect-method-extended-cant-turn-off.js new file mode 100644 index 00000000000000..f4d033efe65707 --- /dev/null +++ b/test/parallel/test-http2-connect-method-extended-cant-turn-off.js @@ -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(); + })); + })); +})); diff --git a/test/parallel/test-http2-connect-method-extended.js b/test/parallel/test-http2-connect-method-extended.js new file mode 100644 index 00000000000000..bb424c73f0a2ad --- /dev/null +++ b/test/parallel/test-http2-connect-method-extended.js @@ -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(); + })); +}));