From 01d94670036ce6a0aa40e329417d0c2e87d710fe 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/http2/core.js | 7 +++++ lib/internal/http2/util.js | 63 ++++++++++++++++++++++++++++++++++++++ src/node_http2.cc | 15 ++++++++- src/node_http2.h | 2 +- src/node_http2_state.h | 9 ++++-- 5 files changed, 92 insertions(+), 4 deletions(-) diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index 1a689859aff752..ae3716dd3e42e7 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..5a66fce63cc23e 100644 --- a/lib/internal/http2/util.js +++ b/lib/internal/http2/util.js @@ -192,6 +192,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 +353,66 @@ function getSettings(session, remote) { function updateSettingsBuffer(settings) { let flags = 0; + let numCustomSettings = 0; + + if (typeof settings.customSettings === 'object') { + let 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 Error('Number of custom settings exceeds MAX_ADDITIONAL_SETTINGS'); + 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..6c5f0015300678 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..d185260cbc8a50 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, @@ -110,7 +113,8 @@ class Http2State : public BaseObject { root_buffer), settings_buffer(realm->isolate(), offsetof(http2_state_internal, settings_buffer), - IDX_SETTINGS_COUNT + 1, + IDX_SETTINGS_COUNT + 1 + 1 + + 2 * MAX_ADDITIONAL_SETTINGS, root_buffer) {} AliasedUint8Array root_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]; }; };