From b8616cdfb0e8939b03b4c61c5632e0230085e61d Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Fri, 29 Dec 2023 18:43:27 +0100 Subject: [PATCH] http2: receive customsettings This commit gives node.js the ability to also receive custom settings, in addition to sending, them which was implemented before. The custom settings received are limited to setting ids, that were specified before, when creating the session eithers through the server or the client. --- doc/api/http2.md | 21 ++- lib/internal/http2/core.js | 32 +++++ lib/internal/http2/util.js | 66 +++++++++- src/node_http2.cc | 124 ++++++++++++++++-- src/node_http2.h | 22 +++- src/node_http2_state.h | 2 +- ...st-http2-client-settings-before-connect.js | 52 +++++++- test/parallel/test-http2-max-settings.js | 22 +++- test/parallel/test-http2-session-settings.js | 73 ++++++++++- 9 files changed, 383 insertions(+), 31 deletions(-) diff --git a/doc/api/http2.md b/doc/api/http2.md index 2ec398393f6865..8b3ce0aad24d3f 100644 --- a/doc/api/http2.md +++ b/doc/api/http2.md @@ -2498,6 +2498,11 @@ changes: **Default:** `100`. * `settings` {HTTP/2 Settings Object} The initial settings to send to the remote peer upon connection. + * `remoteCustomSettings` {Array} The array of integer values determines the + settings types, which are included in the `CustomSettings`-property of + the received remoteSettings. Please see the `CustomSettings`-property of + the `Http2Settings` object for more information, + on the allowed setting types. * `Http1IncomingMessage` {http.IncomingMessage} Specifies the `IncomingMessage` class to used for HTTP/1 fallback. Useful for extending the original `http.IncomingMessage`. **Default:** `http.IncomingMessage`. @@ -2652,6 +2657,10 @@ changes: **Default:** `100`. * `settings` {HTTP/2 Settings Object} The initial settings to send to the remote peer upon connection. + * `remoteCustomSettings` {Array} The array of integer values determines the + settings types, which are included in the `customSettings`-property of the + received remoteSettings. Please see the `customSettings`-property of the + `Http2Settings` object for more information, on the allowed setting types. * ...: Any [`tls.createServer()`][] options can be provided. For servers, the identity options (`pfx` or `key`/`cert`) are usually required. * `origins` {string\[]} An array of origin strings to send within an `ORIGIN` @@ -2780,6 +2789,10 @@ changes: `'https:'` * `settings` {HTTP/2 Settings Object} The initial settings to send to the remote peer upon connection. + * `remoteCustomSettings` {Array} The array of integer values determines the + settings types, which are included in the `CustomSettings`-property of the + received remoteSettings. Please see the `CustomSettings`-property of the + `Http2Settings` object for more information, on the allowed setting types. * `createConnection` {Function} An optional callback that receives the `URL` instance passed to `connect` and the `options` object, and returns any [`Duplex`][] stream that is to be used as the connection for this session. @@ -3022,9 +3035,11 @@ properties. it should be greater than 6, although it is not an error. The values need to be unsigned integers in the range from 0 to 2^32-1. Currently, a maximum of up 10 custom settings is supported. - It is only supported for sending SETTINGS. - Custom settings are not supported for the functions retrieving remote and - local settings as nghttp2 does not pass unknown HTTP/2 settings to Node.js. + It is only supported for sending SETTINGS, or for receiving settings values + specified in the `remoteCustomSettings` options of the server or client + object. Do not mix the `customSettings`-mechanism for a settings id with + interfaces for the natively handled settings, in case a setting becomes + natively supported in a future node version. All additional properties on the settings object are ignored. diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index e6056c395cda68..69956d2885e1f6 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -9,9 +9,11 @@ const { FunctionPrototypeBind, FunctionPrototypeCall, MathMin, + Number, ObjectAssign, ObjectKeys, ObjectDefineProperty, + ObjectEntries, ObjectPrototypeHasOwnProperty, Promise, PromisePrototypeThen, @@ -105,6 +107,7 @@ const { ERR_HTTP2_STREAM_CANCEL, ERR_HTTP2_STREAM_ERROR, ERR_HTTP2_STREAM_SELF_DEPENDENCY, + ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS, ERR_HTTP2_TRAILERS_ALREADY_SENT, ERR_HTTP2_TRAILERS_NOT_READY, ERR_HTTP2_UNSUPPORTED_PROTOCOL, @@ -140,6 +143,7 @@ const { const { assertIsObject, + assertIsArray, assertValidPseudoHeader, assertValidPseudoHeaderResponse, assertValidPseudoHeaderTrailer, @@ -155,7 +159,9 @@ const { kRequest, kProxySocket, mapToHeaders, + MAX_ADDITIONAL_SETTINGS, NghttpError, + remoteCustomSettingsToBuffer, sessionName, toHeaderObject, updateOptionsBuffer, @@ -947,6 +953,15 @@ function pingCallback(cb) { const validateSettings = hideStackFrames((settings) => { if (settings === undefined) return; assertIsObject.withoutStackTrace(settings.customSettings, 'customSettings', 'Number'); + if (settings.customSettings) { + const entries = ObjectEntries(settings.customSettings); + if (entries.length > MAX_ADDITIONAL_SETTINGS) + throw new ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS(); + for (const { 0: key, 1: value } of entries) { + assertWithinRange.withoutStackTrace('customSettings:id', Number(key), 0, 0xffff); + assertWithinRange.withoutStackTrace('customSettings:value', Number(value), 0, kMaxInt); + } + } assertWithinRange.withoutStackTrace('headerTableSize', settings.headerTableSize, @@ -1031,6 +1046,9 @@ function setupHandle(socket, type, options) { this[kState].flags |= SESSION_FLAGS_READY; updateOptionsBuffer(options); + if (options.remoteCustomSettings) { + remoteCustomSettingsToBuffer(options.remoteCustomSettings); + } const handle = new binding.Http2Session(type); handle[kOwner] = this; @@ -3103,6 +3121,13 @@ function initializeOptions(options) { assertIsObject(options.settings, 'options.settings'); options.settings = { ...options.settings }; + assertIsArray(options.remoteCustomSettings, 'options.remoteCustomSettings'); + if (options.remoteCustomSettings) { + options.remoteCustomSettings = [ ...options.remoteCustomSettings ]; + if (options.remoteCustomSettings.length > MAX_ADDITIONAL_SETTINGS) + throw new ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS(); + } + if (options.maxSessionInvalidFrames !== undefined) validateUint32(options.maxSessionInvalidFrames, 'maxSessionInvalidFrames'); @@ -3277,6 +3302,13 @@ function connect(authority, options, listener) { assertIsObject(options, 'options'); options = { ...options }; + assertIsArray(options.remoteCustomSettings, 'options.remoteCustomSettings'); + if (options.remoteCustomSettings) { + options.remoteCustomSettings = [ ...options.remoteCustomSettings ]; + if (options.remoteCustomSettings.length > MAX_ADDITIONAL_SETTINGS) + throw new ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS(); + } + if (typeof authority === 'string') authority = new URL(authority); diff --git a/lib/internal/http2/util.js b/lib/internal/http2/util.js index 8578cc9cc8e5fb..4b0cc941a5e078 100644 --- a/lib/internal/http2/util.js +++ b/lib/internal/http2/util.js @@ -278,8 +278,19 @@ function updateOptionsBuffer(options) { optionsBuffer[IDX_OPTIONS_FLAGS] = flags; } +function addCustomSettingsToObj() { + const toRet = {}; + const num = settingsBuffer[IDX_SETTINGS_FLAGS + 1]; + for (let i = 0; i < num; i++) { + toRet[settingsBuffer[IDX_SETTINGS_FLAGS + 1 + 2 * i + 1].toString()] = + Number(settingsBuffer[IDX_SETTINGS_FLAGS + 1 + 2 * i + 2]); + } + return toRet; +} + function getDefaultSettings() { settingsBuffer[IDX_SETTINGS_FLAGS] = 0; + settingsBuffer[IDX_SETTINGS_FLAGS + 1] = 0; // Length of custom settings binding.refreshDefaultSettings(); const holder = { __proto__: null }; @@ -327,6 +338,8 @@ function getDefaultSettings() { settingsBuffer[IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL] === 1; } + if (settingsBuffer[IDX_SETTINGS_FLAGS + 1]) holder.customSettings = addCustomSettingsToObj(); + return holder; } @@ -338,7 +351,7 @@ function getSettings(session, remote) { else session.localSettings(); - return { + const toRet = { headerTableSize: settingsBuffer[IDX_SETTINGS_HEADER_TABLE_SIZE], enablePush: !!settingsBuffer[IDX_SETTINGS_ENABLE_PUSH], initialWindowSize: settingsBuffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE], @@ -349,6 +362,8 @@ function getSettings(session, remote) { enableConnectProtocol: !!settingsBuffer[IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL], }; + if (settingsBuffer[IDX_SETTINGS_FLAGS + 1]) toRet.customSettings = addCustomSettingsToObj(); + return toRet; } function updateSettingsBuffer(settings) { @@ -415,12 +430,22 @@ function updateSettingsBuffer(settings) { } } if (!set) { // not supported - if (numCustomSettings === MAX_ADDITIONAL_SETTINGS) - throw new ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS(); + let i = 0; + while (i < numCustomSettings) { + if (settingsBuffer[IDX_SETTINGS_FLAGS + 1 + 2 * i + 1] === nsetting) { + settingsBuffer[IDX_SETTINGS_FLAGS + 1 + 2 * i + 2] = val; + break; + } + i++; + } + if (i === numCustomSettings) { + 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 + 2 * numCustomSettings + 1] = nsetting; + settingsBuffer[IDX_SETTINGS_FLAGS + 1 + 2 * numCustomSettings + 2] = val; + numCustomSettings++; + } } } } @@ -475,6 +500,24 @@ function updateSettingsBuffer(settings) { settingsBuffer[IDX_SETTINGS_FLAGS] = flags; } +function remoteCustomSettingsToBuffer(remoteCustomSettings) { + if (remoteCustomSettings.length > MAX_ADDITIONAL_SETTINGS) + throw new ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS(); + let numCustomSettings = 0; + for (let i = 0; i < remoteCustomSettings.length; i++) { + const nsetting = remoteCustomSettings[i]; + if (typeof nsetting === 'number' && nsetting <= 0xffff && + nsetting >= 0) { + settingsBuffer[IDX_SETTINGS_FLAGS + 1 + 2 * numCustomSettings + 1] = nsetting; + numCustomSettings++; + } else + throw new ERR_HTTP2_INVALID_SETTING_VALUE.RangeError( + 'Range Error', nsetting, 0, 0xffff); + + } + settingsBuffer[IDX_SETTINGS_FLAGS + 1] = numCustomSettings; +} + function getSessionState(session) { session.refreshState(); return { @@ -649,6 +692,14 @@ const assertIsObject = hideStackFrames((value, name, types) => { } }); +const assertIsArray = hideStackFrames((value, name, types) => { + if (value !== undefined && + (value === null || + !ArrayIsArray(value))) { + throw new ERR_INVALID_ARG_TYPE.HideStackFramesError(name, types || 'Array', value); + } +}); + const assertWithinRange = hideStackFrames( (name, value, min = 0, max = Infinity) => { if (value !== undefined && @@ -732,6 +783,7 @@ function getAuthority(headers) { module.exports = { assertIsObject, + assertIsArray, assertValidPseudoHeader, assertValidPseudoHeaderResponse, assertValidPseudoHeaderTrailer, @@ -747,7 +799,9 @@ module.exports = { kProxySocket, kRequest, mapToHeaders, + MAX_ADDITIONAL_SETTINGS, NghttpError, + remoteCustomSettingsToBuffer, sessionName, toHeaderObject, updateOptionsBuffer, diff --git a/src/node_http2.cc b/src/node_http2.cc index ebb1ab63c3ff80..0d0faaaa752c4a 100644 --- a/src/node_http2.cc +++ b/src/node_http2.cc @@ -227,7 +227,6 @@ size_t Http2Settings::Init( #define V(name) GRABSETTING(entries, count, name); HTTP2_SETTINGS(V) #undef V - uint32_t numAddSettings = buffer[IDX_SETTINGS_COUNT + 1]; if (numAddSettings > 0) { uint32_t offset = IDX_SETTINGS_COUNT + 1 + 1; @@ -300,7 +299,7 @@ Local Http2Settings::Pack( // Updates the shared TypedArray with the current remote or local settings for // the session. -void Http2Settings::Update(Http2Session* session, get_setting fn) { +void Http2Settings::Update(Http2Session* session, get_setting fn, bool local) { AliasedUint32Array& buffer = session->http2_state()->settings_buffer; #define V(name) \ @@ -308,8 +307,37 @@ 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 + struct Http2Session::custom_settings_state& custom_settings = + session->custom_settings(local); + uint32_t count = 0; + size_t imax = std::min(custom_settings.number, MAX_ADDITIONAL_SETTINGS); + for (size_t i = 0; i < imax; i++) { + // We flag unset the settings with a bit above the allowed range + if (!(custom_settings.entries[i].settings_id & (~0xffff))) { + uint32_t settings_id = + (uint32_t)(custom_settings.entries[i].settings_id & 0xffff); + size_t j = 0; + while (j < count) { + if ((buffer[IDX_SETTINGS_COUNT + 1 + j * 2 + 1] & 0xffff) == + settings_id) { + buffer[IDX_SETTINGS_COUNT + 1 + j * 2 + 1] = settings_id; + buffer[IDX_SETTINGS_COUNT + 1 + j * 2 + 2] = + custom_settings.entries[i].value; + break; + } + j++; + } + if (j == count && count < MAX_ADDITIONAL_SETTINGS) { + buffer[IDX_SETTINGS_COUNT + 1 + count * 2 + 1] = settings_id; + buffer[IDX_SETTINGS_COUNT + 1 + count * 2 + 2] = + custom_settings.entries[i].value; + count++; + } + } + // Comment for code review, + // one might also set the javascript object with an undefined value + } + buffer[IDX_SETTINGS_COUNT + 1] = count; } // Initializes the shared TypedArray with the default settings values. @@ -332,6 +360,9 @@ void Http2Settings::RefreshDefaults(Http2State* http2_state) { void Http2Settings::Send() { Http2Scope h2scope(session_.get()); + + // We have to update the local custom settings + session_->UpdateLocalCustomSettings(count_, &entries_[0]); CHECK_EQ(nghttp2_submit_settings( session_->session(), NGHTTP2_FLAG_NONE, @@ -339,6 +370,34 @@ void Http2Settings::Send() { count_), 0); } +void Http2Session::UpdateLocalCustomSettings(size_t count, + nghttp2_settings_entry* entries) { + size_t number = local_custom_settings_.number; + for (size_t i = 0; i < count; ++i) { + nghttp2_settings_entry& s_entry = entries[i]; + if (s_entry.settings_id >= IDX_SETTINGS_COUNT) { + // look if already included + size_t j = 0; + while (j < number) { + nghttp2_settings_entry& d_entry = local_custom_settings_.entries[j]; + if (d_entry.settings_id == s_entry.settings_id) { + d_entry.value = s_entry.value; + break; + } + j++; + } + if (j == number && number < MAX_ADDITIONAL_SETTINGS) { + nghttp2_settings_entry& d_entry = + local_custom_settings_.entries[number]; + d_entry.settings_id = s_entry.settings_id; + d_entry.value = s_entry.value; + number++; + } + } + } + local_custom_settings_.number = number; +} + void Http2Settings::Done(bool ack) { uint64_t end = uv_hrtime(); double duration = (end - startTime_) / 1e6; @@ -505,6 +564,11 @@ Http2Session::Http2Session(Http2State* http2_state, max_outstanding_pings_ = opts.max_outstanding_pings(); max_outstanding_settings_ = opts.max_outstanding_settings(); + local_custom_settings_.number = 0; + remote_custom_settings_.number = 0; + // now, import possible custom_settings + FetchAllowedRemoteCustomSettings(); + padding_strategy_ = opts.padding_strategy(); bool hasGetPaddingCallback = @@ -547,6 +611,24 @@ Http2Session::~Http2Session() { CHECK_EQ(current_nghttp2_memory_, 0); } +void Http2Session::FetchAllowedRemoteCustomSettings() { + AliasedUint32Array& buffer = http2_state_->settings_buffer; + uint32_t numAddSettings = buffer[IDX_SETTINGS_COUNT + 1]; + if (numAddSettings > 0) { + nghttp2_settings_entry* entries = remote_custom_settings_.entries; + uint32_t offset = IDX_SETTINGS_COUNT + 1 + 1; + size_t count = 0; + for (uint32_t i = 0; i < numAddSettings; i++) { + uint32_t key = + (buffer[offset + i * 2 + 0] & 0xffff) | + (1 + << 16); // setting the bit 16 indicates, that no values has been set + entries[count++] = nghttp2_settings_entry{(int32_t)key, 0}; + } + remote_custom_settings_.number = count; + } +} + void Http2Session::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("streams", streams_); tracker->TrackField("outstanding_pings", outstanding_pings_); @@ -561,12 +643,11 @@ void Http2Session::MemoryInfo(MemoryTracker* tracker) const { std::string Http2Session::diagnostic_name() const { return std::string("Http2Session ") + TypeName() + " (" + - std::to_string(static_cast(get_async_id())) + ")"; + std::to_string(static_cast(get_async_id())) + ")"; } MaybeLocal Http2StreamPerformanceEntryTraits::GetDetails( - Environment* env, - const Http2StreamPerformanceEntry& entry) { + Environment* env, const Http2StreamPerformanceEntry& entry) { Local obj = Object::New(env->isolate()); #define SET(name, val) \ @@ -1554,6 +1635,26 @@ void Http2Session::HandleSettingsFrame(const nghttp2_frame* frame) { bool ack = frame->hd.flags & NGHTTP2_FLAG_ACK; if (!ack) { js_fields_->bitfield &= ~(1 << kSessionRemoteSettingsIsUpToDate); + // update additional settings + if (remote_custom_settings_.number > 0) { + nghttp2_settings_entry* iv = frame->settings.iv; + size_t niv = frame->settings.niv; + size_t numsettings = remote_custom_settings_.number; + for (size_t i = 0; i < niv; ++i) { + int32_t settings_id = iv[i].settings_id; + if (settings_id >= + IDX_SETTINGS_COUNT) { // unsupported, additional settings + for (size_t j = 0; j < numsettings; ++j) { + if ((remote_custom_settings_.entries[j].settings_id & 0xFFFF) == + settings_id) { + remote_custom_settings_.entries[j].settings_id = settings_id; + remote_custom_settings_.entries[j].value = iv[i].value; + break; + } + } + } + } + } if (!(js_fields_->bitfield & (1 << kSessionHasRemoteSettingsListeners))) return; // This is not a SETTINGS acknowledgement, notify and return @@ -2620,11 +2721,11 @@ void Http2Session::SetLocalWindowSize( // A TypedArray instance is shared between C++ and JS land to contain the // SETTINGS (either remote or local). RefreshSettings updates the current // values established for each of the settings so those can be read in JS land. -template +template void Http2Session::RefreshSettings(const FunctionCallbackInfo& args) { Http2Session* session; ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - Http2Settings::Update(session, fn); + Http2Settings::Update(session, fn, local); Debug(session, "settings refreshed for session"); } @@ -3290,12 +3391,13 @@ void Initialize(Local target, isolate, session, "localSettings", - Http2Session::RefreshSettings); + Http2Session::RefreshSettings); SetProtoMethod( isolate, session, "remoteSettings", - Http2Session::RefreshSettings); + Http2Session::RefreshSettings); SetConstructorFunction(context, target, "Http2Session", session); Local constants = Object::New(isolate); diff --git a/src/node_http2.h b/src/node_http2.h index 6b7fd746021507..3ba05cbe7f9ce6 100644 --- a/src/node_http2.h +++ b/src/node_http2.h @@ -627,6 +627,15 @@ class Http2Session : public AsyncWrap, flags_ |= kSessionStateClosed; } + struct custom_settings_state { + size_t number; + nghttp2_settings_entry entries[MAX_ADDITIONAL_SETTINGS]; + }; + + custom_settings_state& custom_settings(bool local) { + return local ? local_custom_settings_ : remote_custom_settings_; + } + #define IS_FLAG(name, flag) \ bool is_##name() const { return flags_ & flag; } \ void set_##name(bool on = true) { \ @@ -715,7 +724,7 @@ class Http2Session : public AsyncWrap, static void AltSvc(const v8::FunctionCallbackInfo& args); static void Origin(const v8::FunctionCallbackInfo& args); - template + template static void RefreshSettings(const v8::FunctionCallbackInfo& args); uv_loop_t* event_loop() const { @@ -739,6 +748,9 @@ class Http2Session : public AsyncWrap, current_session_memory_ -= amount; } + void UpdateLocalCustomSettings(size_t count_, + nghttp2_settings_entry* entries_); + // Tell our custom memory allocator that this rcbuf is independent of // this session now, and may outlive it. void StopTrackingRcbuf(nghttp2_rcbuf* buf); @@ -776,6 +788,8 @@ class Http2Session : public AsyncWrap, private: void EmitStatistics(); + void FetchAllowedRemoteCustomSettings(); + // Frame Padding Strategies ssize_t OnDWordAlignedPadding(size_t frameLength, size_t maxPayloadLen); @@ -915,6 +929,9 @@ class Http2Session : public AsyncWrap, size_t max_outstanding_settings_ = kDefaultMaxSettings; std::queue> outstanding_settings_; + struct custom_settings_state local_custom_settings_; + struct custom_settings_state remote_custom_settings_; + std::vector outgoing_buffers_; std::vector outgoing_storage_; size_t outgoing_length_ = 0; @@ -1018,8 +1035,7 @@ class Http2Settings : public AsyncWrap { static void RefreshDefaults(Http2State* http2_state); // Update the local or remote settings for the given session - static void Update(Http2Session* session, - get_setting fn); + static void Update(Http2Session* session, get_setting fn, bool local); private: static size_t Init( diff --git a/src/node_http2_state.h b/src/node_http2_state.h index 487ddad51d8c22..4683b71c227ae2 100644 --- a/src/node_http2_state.h +++ b/src/node_http2_state.h @@ -140,7 +140,7 @@ class Http2State : public BaseObject { double session_stats_buffer[IDX_SESSION_STATS_COUNT]; uint32_t options_buffer[IDX_OPTIONS_FLAGS + 1]; // first + 1: number of actual nghttp2 supported settings - // second + 1: number of additional settings not suppoted by nghttp2 + // second + 1: number of additional settings not supported by nghttp2 // 2 * MAX_ADDITIONAL_SETTINGS: settings id and value for each // additional setting uint32_t settings_buffer[IDX_SETTINGS_COUNT + 1 + 1 + diff --git a/test/parallel/test-http2-client-settings-before-connect.js b/test/parallel/test-http2-client-settings-before-connect.js index d370b49ce050dd..c621a55fbf0063 100644 --- a/test/parallel/test-http2-client-settings-before-connect.js +++ b/test/parallel/test-http2-client-settings-before-connect.js @@ -48,6 +48,56 @@ server.listen(0, common.mustCall(() => { ) ); + assert.throws( + () => client.settings({ customSettings: { + 0x11: 5, + 0x12: 5, + 0x13: 5, + 0x14: 5, + 0x15: 5, + 0x16: 5, + 0x17: 5, + 0x18: 5, + 0x19: 5, + 0x1A: 5, // more than 10 + 0x1B: 5 + } }), + { + code: 'ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS', + name: 'Error' + } + ); + + assert.throws( + () => client.settings({ customSettings: { + 0x10000: 5, + } }), + { + code: 'ERR_HTTP2_INVALID_SETTING_VALUE', + name: 'RangeError' + } + ); + + assert.throws( + () => client.settings({ customSettings: { + 0x55: 0x100000000, + } }), + { + code: 'ERR_HTTP2_INVALID_SETTING_VALUE', + name: 'RangeError' + } + ); + + assert.throws( + () => client.settings({ customSettings: { + 0x55: -1, + } }), + { + code: 'ERR_HTTP2_INVALID_SETTING_VALUE', + name: 'RangeError' + } + ); + [1, true, {}, []].forEach((invalidCallback) => assert.throws( () => client.settings({}, invalidCallback), @@ -58,7 +108,7 @@ server.listen(0, common.mustCall(() => { ) ); - client.settings({ maxFrameSize: 1234567 }); + client.settings({ maxFrameSize: 1234567, customSettings: { 0xbf: 12 } }); const req = client.request(); req.on('response', common.mustCall()); diff --git a/test/parallel/test-http2-max-settings.js b/test/parallel/test-http2-max-settings.js index 0ae792855ae3d5..e5f05e5b5898d1 100644 --- a/test/parallel/test-http2-max-settings.js +++ b/test/parallel/test-http2-max-settings.js @@ -15,7 +15,7 @@ const server = http2.createServer({ maxSettings: 1 }); server.on('session', common.mustCall((session) => { session.on('stream', common.mustNotCall()); session.on('remoteSettings', common.mustNotCall()); -})); +}, 2)); server.on('stream', common.mustNotCall()); server.listen(0, common.mustCall(() => { @@ -30,7 +30,23 @@ server.listen(0, common.mustCall(() => { }, }); - client.on('error', common.mustCall(() => { - server.close(); + client.on('error', common.mustCall((err) => { + // The same but with custom settings + const client2 = http2.connect( + `http://localhost:${server.address().port}`, { + settings: { + // The actual settings values do not matter. + headerTableSize: 1000, + customSettings: { + 0x14: 45 + } + }, + }); + + client2.on('error', common.mustCall(() => { + server.close(); + })); })); + + })); diff --git a/test/parallel/test-http2-session-settings.js b/test/parallel/test-http2-session-settings.js index bcf78c6aa512a0..3f94cc3fb2dcd8 100644 --- a/test/parallel/test-http2-session-settings.js +++ b/test/parallel/test-http2-session-settings.js @@ -6,7 +6,17 @@ if (!common.hasCrypto) const assert = require('assert'); const h2 = require('http2'); -const server = h2.createServer(); +const server = h2.createServer({ + remoteCustomSettings: [ + 55, + ], + settings: { + customSettings: { + 1244: 456 + } + } +} +); server.on( 'stream', @@ -20,6 +30,24 @@ server.on( assert.strictEqual(typeof settings.maxConcurrentStreams, 'number'); assert.strictEqual(typeof settings.maxHeaderListSize, 'number'); assert.strictEqual(typeof settings.maxHeaderSize, 'number'); + assert.strictEqual(typeof settings.customSettings, 'object'); + let countCustom = 0; + if (settings.customSettings[55]) { + assert.strictEqual(typeof settings.customSettings[55], 'number'); + assert.strictEqual(settings.customSettings[55], 12); + countCustom++; + } + if (settings.customSettings[155]) { + // Should not happen actually + assert.strictEqual(typeof settings.customSettings[155], 'number'); + countCustom++; + } + if (settings.customSettings[1244]) { + assert.strictEqual(typeof settings.customSettings[1244], 'number'); + assert.strictEqual(settings.customSettings[1244], 456); + countCustom++; + } + assert.strictEqual(countCustom, 1); }; const localSettings = stream.session.localSettings; @@ -51,8 +79,15 @@ server.listen( const client = h2.connect(`http://localhost:${server.address().port}`, { settings: { enablePush: false, - initialWindowSize: 123456 - } + initialWindowSize: 123456, + customSettings: { + 55: 12, + 155: 144 // should not arrive + }, + }, + remoteCustomSettings: [ + 1244, + ] }); client.on( @@ -62,6 +97,7 @@ server.listen( assert.strictEqual(settings.enablePush, false); assert.strictEqual(settings.initialWindowSize, 123456); assert.strictEqual(settings.maxFrameSize, 16384); + assert.strictEqual(settings.customSettings[55], 12); }, 2) ); @@ -117,6 +153,37 @@ server.listen( ); }); + // Same tests as for the client on customSettings + assert.throws( + () => client.settings({ customSettings: { + 0x10000: 5, + } }), + { + code: 'ERR_HTTP2_INVALID_SETTING_VALUE', + name: 'RangeError' + } + ); + + assert.throws( + () => client.settings({ customSettings: { + 55: 0x100000000, + } }), + { + code: 'ERR_HTTP2_INVALID_SETTING_VALUE', + name: 'RangeError' + } + ); + + assert.throws( + () => client.settings({ customSettings: { + 55: -1, + } }), + { + code: 'ERR_HTTP2_INVALID_SETTING_VALUE', + name: 'RangeError' + } + ); + // Error checks for enablePush [1, {}, 'test', [], null, Infinity, NaN].forEach((i) => { assert.throws(