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(