diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 39e512762a470c..27f63469154270 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1279,6 +1279,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 58d0783701286d..e6056c395cda68 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -946,6 +946,8 @@ function pingCallback(cb) { // All settings are optional and may be left undefined const validateSettings = hideStackFrames((settings) => { if (settings === undefined) return; + assertIsObject.withoutStackTrace(settings.customSettings, 'customSettings', 'Number'); + assertWithinRange.withoutStackTrace('headerTableSize', settings.headerTableSize, 0, kMaxInt); @@ -3387,6 +3389,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 a9d636c2f20581..8578cc9cc8e5fb 100644 --- a/lib/internal/http2/util.js +++ b/lib/internal/http2/util.js @@ -8,6 +8,7 @@ const { Error, MathMax, Number, + NumberIsNaN, ObjectKeys, SafeSet, String, @@ -24,6 +25,7 @@ const { ERR_HTTP2_INVALID_CONNECTION_HEADERS, ERR_HTTP2_INVALID_PSEUDOHEADER: { HideStackFramesError: ERR_HTTP2_INVALID_PSEUDOHEADER }, ERR_HTTP2_INVALID_SETTING_VALUE, + ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS, ERR_INVALID_ARG_TYPE, ERR_INVALID_HTTP_TOKEN, }, @@ -190,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; @@ -348,6 +353,80 @@ 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 (NumberIsNaN(nsetting) || + typeof nsetting !== 'number' || + 0 >= nsetting || + nsetting > 0xffff) + throw new ERR_HTTP2_INVALID_SETTING_VALUE.RangeError( + 'Range Error', nsetting, 0, 0xffff); + if (NumberIsNaN(val) || + typeof val !== 'number' || + 0 >= val || + val > 0xffffffff) + throw new ERR_HTTP2_INVALID_SETTING_VALUE.RangeError( + 'Range Error', val, 0, 0xffffffff); + 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_CUSTOM_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..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..487ddad51d8c22 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 implemented 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,12 @@ 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]; + // first + 1: number of actual nghttp2 supported settings + // second + 1: number of additional settings not suppoted by nghttp2 + // 2 * MAX_ADDITIONAL_SETTINGS: settings id and value for each + // additional setting + uint32_t settings_buffer[IDX_SETTINGS_COUNT + 1 + 1 + + 2 * MAX_ADDITIONAL_SETTINGS]; }; };