From 5e1b750b088aa127915cca0549d4551e7e6d6d6d Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 5 Aug 2023 11:32:52 +0000 Subject: [PATCH] http2: initial proposal for addtl http/2 settings Currently, node.js http/2 is limited in sending SETTINGs, that are currently implemented by nghttp2. However, nghttp2 has the ability to send arbitary SETTINGs, that are not known beforehand. This patch adds this feature including a fall back mechanism, if a SETTING is implemented in a later nghttp2 or node version. Fixes: https://github.com/nodejs/node/issues/1337 --- lib/internal/errors.js | 2 ++ lib/internal/http2/core.js | 7 ++++ lib/internal/http2/util.js | 65 ++++++++++++++++++++++++++++++++++++++ src/node_http2.cc | 15 ++++++++- src/node_http2.h | 2 +- src/node_http2_state.h | 15 ++++++--- 6 files changed, 99 insertions(+), 7 deletions(-) diff --git a/lib/internal/errors.js b/lib/internal/errors.js index bed02da76a3bab..2f2b7d15c9eb3b 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1153,6 +1153,8 @@ E('ERR_HTTP2_STREAM_CANCEL', function(error) { E('ERR_HTTP2_STREAM_ERROR', 'Stream closed with error code %s', Error); E('ERR_HTTP2_STREAM_SELF_DEPENDENCY', 'A stream cannot depend on itself', Error); +E('ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS', + 'Number of custom settings exceeds MAX_ADDITIONAL_SETTINGS', Error); E('ERR_HTTP2_TOO_MANY_INVALID_FRAMES', 'Too many invalid HTTP/2 frames', Error); E('ERR_HTTP2_TRAILERS_ALREADY_SENT', 'Trailing headers have already been sent', Error); diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index 1a689859aff752..9ed96c785edb39 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -946,6 +946,9 @@ function pingCallback(cb) { // All settings are optional and may be left undefined const validateSettings = hideStackFrames((settings) => { if (settings === undefined) return; + + assertIsObject(settings.customSettings, 'customSettings', 'Number'); + assertWithinRange('headerTableSize', settings.headerTableSize, 0, kMaxInt); @@ -3384,6 +3387,10 @@ function getUnpackedSettings(buf, options = kEmptyObject) { break; case NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL: settings.enableConnectProtocol = value !== 0; + break; + default: + if (!settings.customSettings) settings.customSettings = {}; + settings.customSettings[id] = value; } offset += 4; } diff --git a/lib/internal/http2/util.js b/lib/internal/http2/util.js index 6d4a7f94b3d11a..839dfcd0c90365 100644 --- a/lib/internal/http2/util.js +++ b/lib/internal/http2/util.js @@ -25,6 +25,7 @@ const { ERR_HTTP2_INVALID_CONNECTION_HEADERS, ERR_HTTP2_INVALID_PSEUDOHEADER, ERR_HTTP2_INVALID_SETTING_VALUE, + ERR_HTTP2_TOO_MANY_CUSTOMSETTINGS, ERR_INVALID_ARG_TYPE, ERR_INVALID_HTTP_TOKEN, }, @@ -192,6 +193,9 @@ const IDX_SETTINGS_MAX_HEADER_LIST_SIZE = 5; const IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL = 6; const IDX_SETTINGS_FLAGS = 7; +// Maximum number of allowed additional settings +const MAX_ADDITIONAL_SETTINGS = 10; + const IDX_SESSION_STATE_EFFECTIVE_LOCAL_WINDOW_SIZE = 0; const IDX_SESSION_STATE_EFFECTIVE_RECV_DATA_LENGTH = 1; const IDX_SESSION_STATE_NEXT_STREAM_ID = 2; @@ -350,6 +354,67 @@ function getSettings(session, remote) { function updateSettingsBuffer(settings) { let flags = 0; + let numCustomSettings = 0; + + if (typeof settings.customSettings === 'object') { + const customSettings = settings.customSettings; + for (const setting in customSettings) { + const val = customSettings[setting]; + if (typeof val === 'number') { + let set = false; + const nsetting = Number(setting); + if (nsetting < IDX_SETTINGS_FLAGS) { + set = true; + switch (nsetting) { + case IDX_SETTINGS_HEADER_TABLE_SIZE: + flags |= (1 << IDX_SETTINGS_HEADER_TABLE_SIZE); + settingsBuffer[IDX_SETTINGS_HEADER_TABLE_SIZE] = + val; + break; + case IDX_SETTINGS_ENABLE_PUSH: + flags |= (1 << IDX_SETTINGS_ENABLE_PUSH); + settingsBuffer[IDX_SETTINGS_ENABLE_PUSH] = val; + break; + case IDX_SETTINGS_INITIAL_WINDOW_SIZE: + flags |= (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE); + settingsBuffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE] = + val; + break; + case IDX_SETTINGS_MAX_FRAME_SIZE: + flags |= (1 << IDX_SETTINGS_MAX_FRAME_SIZE); + settingsBuffer[IDX_SETTINGS_MAX_FRAME_SIZE] = + val; + break; + case IDX_SETTINGS_MAX_CONCURRENT_STREAMS: + flags |= (1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS); + settingsBuffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS] = val; + break; + case IDX_SETTINGS_MAX_HEADER_LIST_SIZE: + flags |= (1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE); + settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE] = + val; + break; + case IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL: + flags |= (1 << IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL); + settingsBuffer[IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL] = val; + break; + default: + set = false; + break; + } + } + if (!set) { // not supported + if (numCustomSettings === MAX_ADDITIONAL_SETTINGS) + throw new ERR_HTTP2_TOO_MANY_CUSTOMSETTINGS(); + settingsBuffer[IDX_SETTINGS_FLAGS + 1 + 2 * numCustomSettings + 1] = nsetting; + settingsBuffer[IDX_SETTINGS_FLAGS + 1 + 2 * numCustomSettings + 2] = val; + numCustomSettings++; + } + } + } + } + settingsBuffer[IDX_SETTINGS_FLAGS + 1] = numCustomSettings; + if (typeof settings.headerTableSize === 'number') { flags |= (1 << IDX_SETTINGS_HEADER_TABLE_SIZE); settingsBuffer[IDX_SETTINGS_HEADER_TABLE_SIZE] = diff --git a/src/node_http2.cc b/src/node_http2.cc index 070b40ae0a6ad6..ebb1ab63c3ff80 100644 --- a/src/node_http2.cc +++ b/src/node_http2.cc @@ -228,6 +228,16 @@ size_t Http2Settings::Init( HTTP2_SETTINGS(V) #undef V + uint32_t numAddSettings = buffer[IDX_SETTINGS_COUNT + 1]; + if (numAddSettings > 0) { + uint32_t offset = IDX_SETTINGS_COUNT + 1 + 1; + for (uint32_t i = 0; i < numAddSettings; i++) { + uint32_t key = buffer[offset + i * 2 + 0]; + uint32_t val = buffer[offset + i * 2 + 1]; + entries[count++] = nghttp2_settings_entry{(int32_t)key, val}; + } + } + return count; } #undef GRABSETTING @@ -262,7 +272,7 @@ Local Http2Settings::Pack() { } Local Http2Settings::Pack(Http2State* state) { - nghttp2_settings_entry entries[IDX_SETTINGS_COUNT]; + nghttp2_settings_entry entries[IDX_SETTINGS_COUNT + MAX_ADDITIONAL_SETTINGS]; size_t count = Init(state, entries); return Pack(state->env(), count, entries); } @@ -298,6 +308,8 @@ void Http2Settings::Update(Http2Session* session, get_setting fn) { fn(session->session(), NGHTTP2_SETTINGS_ ## name); HTTP2_SETTINGS(V) #undef V + buffer[IDX_SETTINGS_COUNT + 1] = + 0; // no additional settings are coming, clear them } // Initializes the shared TypedArray with the default settings values. @@ -314,6 +326,7 @@ void Http2Settings::RefreshDefaults(Http2State* http2_state) { #undef V buffer[IDX_SETTINGS_COUNT] = flags; + buffer[IDX_SETTINGS_COUNT + 1] = 0; // no additional settings } diff --git a/src/node_http2.h b/src/node_http2.h index 87f6ab8305a7d8..6b7fd746021507 100644 --- a/src/node_http2.h +++ b/src/node_http2.h @@ -1035,7 +1035,7 @@ class Http2Settings : public AsyncWrap { v8::Global callback_; uint64_t startTime_; size_t count_ = 0; - nghttp2_settings_entry entries_[IDX_SETTINGS_COUNT]; + nghttp2_settings_entry entries_[IDX_SETTINGS_COUNT + MAX_ADDITIONAL_SETTINGS]; }; class Origins { diff --git a/src/node_http2_state.h b/src/node_http2_state.h index f9ac6b40c3410a..72a96da3816fe5 100644 --- a/src/node_http2_state.h +++ b/src/node_http2_state.h @@ -21,6 +21,9 @@ namespace http2 { IDX_SETTINGS_COUNT }; + // number of max additional settings, thus settings not implemente by nghttp2 + const size_t MAX_ADDITIONAL_SETTINGS = 10; + enum Http2SessionStateIndex { IDX_SESSION_STATE_EFFECTIVE_LOCAL_WINDOW_SIZE, IDX_SESSION_STATE_EFFECTIVE_RECV_DATA_LENGTH, @@ -108,10 +111,11 @@ class Http2State : public BaseObject { offsetof(http2_state_internal, options_buffer), IDX_OPTIONS_FLAGS + 1, root_buffer), - settings_buffer(realm->isolate(), - offsetof(http2_state_internal, settings_buffer), - IDX_SETTINGS_COUNT + 1, - root_buffer) {} + settings_buffer( + realm->isolate(), + offsetof(http2_state_internal, settings_buffer), + IDX_SETTINGS_COUNT + 1 + 1 + 2 * MAX_ADDITIONAL_SETTINGS, + root_buffer) {} AliasedUint8Array root_buffer; AliasedFloat64Array session_state_buffer; @@ -135,7 +139,8 @@ class Http2State : public BaseObject { double stream_stats_buffer[IDX_STREAM_STATS_COUNT]; double session_stats_buffer[IDX_SESSION_STATS_COUNT]; uint32_t options_buffer[IDX_OPTIONS_FLAGS + 1]; - uint32_t settings_buffer[IDX_SETTINGS_COUNT + 1]; + uint32_t settings_buffer[IDX_SETTINGS_COUNT + 1 + 1 + + 2 * MAX_ADDITIONAL_SETTINGS]; }; };