Skip to content

Commit dd0897a

Browse files
committed
http2: 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: nodejs#1337
1 parent e9ff810 commit dd0897a

File tree

6 files changed

+117
-7
lines changed

6 files changed

+117
-7
lines changed

lib/internal/errors.js

+2
Original file line numberDiff line numberDiff line change
@@ -1154,6 +1154,8 @@ E('ERR_HTTP2_STREAM_CANCEL', function(error) {
11541154
E('ERR_HTTP2_STREAM_ERROR', 'Stream closed with error code %s', Error);
11551155
E('ERR_HTTP2_STREAM_SELF_DEPENDENCY',
11561156
'A stream cannot depend on itself', Error);
1157+
E('ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS',
1158+
'Number of custom settings exceeds MAX_ADDITIONAL_SETTINGS', Error);
11571159
E('ERR_HTTP2_TOO_MANY_INVALID_FRAMES', 'Too many invalid HTTP/2 frames', Error);
11581160
E('ERR_HTTP2_TRAILERS_ALREADY_SENT',
11591161
'Trailing headers have already been sent', Error);

lib/internal/http2/core.js

+7
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,9 @@ function pingCallback(cb) {
946946
// All settings are optional and may be left undefined
947947
const validateSettings = hideStackFrames((settings) => {
948948
if (settings === undefined) return;
949+
950+
assertIsObject(settings.customSettings, 'customSettings', 'Number');
951+
949952
assertWithinRange('headerTableSize',
950953
settings.headerTableSize,
951954
0, kMaxInt);
@@ -3384,6 +3387,10 @@ function getUnpackedSettings(buf, options = kEmptyObject) {
33843387
break;
33853388
case NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL:
33863389
settings.enableConnectProtocol = value !== 0;
3390+
break;
3391+
default:
3392+
if (!settings.customSettings) settings.customSettings = {};
3393+
settings.customSettings[id] = value;
33873394
}
33883395
offset += 4;
33893396
}

lib/internal/http2/util.js

+79
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const {
88
Error,
99
MathMax,
1010
Number,
11+
NumberIsNaN,
1112
ObjectDefineProperty,
1213
ObjectKeys,
1314
SafeSet,
@@ -25,6 +26,7 @@ const {
2526
ERR_HTTP2_INVALID_CONNECTION_HEADERS,
2627
ERR_HTTP2_INVALID_PSEUDOHEADER,
2728
ERR_HTTP2_INVALID_SETTING_VALUE,
29+
ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS,
2830
ERR_INVALID_ARG_TYPE,
2931
ERR_INVALID_HTTP_TOKEN,
3032
},
@@ -192,6 +194,9 @@ const IDX_SETTINGS_MAX_HEADER_LIST_SIZE = 5;
192194
const IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL = 6;
193195
const IDX_SETTINGS_FLAGS = 7;
194196

197+
// Maximum number of allowed additional settings
198+
const MAX_ADDITIONAL_SETTINGS = 10;
199+
195200
const IDX_SESSION_STATE_EFFECTIVE_LOCAL_WINDOW_SIZE = 0;
196201
const IDX_SESSION_STATE_EFFECTIVE_RECV_DATA_LENGTH = 1;
197202
const IDX_SESSION_STATE_NEXT_STREAM_ID = 2;
@@ -350,6 +355,80 @@ function getSettings(session, remote) {
350355

351356
function updateSettingsBuffer(settings) {
352357
let flags = 0;
358+
let numCustomSettings = 0;
359+
360+
if (typeof settings.customSettings === 'object') {
361+
const customSettings = settings.customSettings;
362+
for (const setting in customSettings) {
363+
const val = customSettings[setting];
364+
if (typeof val === 'number') {
365+
let set = false;
366+
const nsetting = Number(setting);
367+
if (NumberIsNaN(nsetting) ||
368+
typeof nsetting !== 'number' ||
369+
0 >= nsetting ||
370+
nsetting > 0xffff)
371+
throw new ERR_HTTP2_INVALID_SETTING_VALUE.RangeError(
372+
'Range Error', nsetting, 0, 0xffff);
373+
if (NumberIsNaN(val) ||
374+
typeof val !== 'number' ||
375+
0 >= val ||
376+
val > 0xffffffff)
377+
throw new ERR_HTTP2_INVALID_SETTING_VALUE.RangeError(
378+
'Range Error', val, 0, 0xffffffff);
379+
if (nsetting < IDX_SETTINGS_FLAGS) {
380+
set = true;
381+
switch (nsetting) {
382+
case IDX_SETTINGS_HEADER_TABLE_SIZE:
383+
flags |= (1 << IDX_SETTINGS_HEADER_TABLE_SIZE);
384+
settingsBuffer[IDX_SETTINGS_HEADER_TABLE_SIZE] =
385+
val;
386+
break;
387+
case IDX_SETTINGS_ENABLE_PUSH:
388+
flags |= (1 << IDX_SETTINGS_ENABLE_PUSH);
389+
settingsBuffer[IDX_SETTINGS_ENABLE_PUSH] = val;
390+
break;
391+
case IDX_SETTINGS_INITIAL_WINDOW_SIZE:
392+
flags |= (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE);
393+
settingsBuffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE] =
394+
val;
395+
break;
396+
case IDX_SETTINGS_MAX_FRAME_SIZE:
397+
flags |= (1 << IDX_SETTINGS_MAX_FRAME_SIZE);
398+
settingsBuffer[IDX_SETTINGS_MAX_FRAME_SIZE] =
399+
val;
400+
break;
401+
case IDX_SETTINGS_MAX_CONCURRENT_STREAMS:
402+
flags |= (1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS);
403+
settingsBuffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS] = val;
404+
break;
405+
case IDX_SETTINGS_MAX_HEADER_LIST_SIZE:
406+
flags |= (1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE);
407+
settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE] =
408+
val;
409+
break;
410+
case IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL:
411+
flags |= (1 << IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL);
412+
settingsBuffer[IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL] = val;
413+
break;
414+
default:
415+
set = false;
416+
break;
417+
}
418+
}
419+
if (!set) { // not supported
420+
if (numCustomSettings === MAX_ADDITIONAL_SETTINGS)
421+
throw new ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS();
422+
423+
settingsBuffer[IDX_SETTINGS_FLAGS + 1 + 2 * numCustomSettings + 1] = nsetting;
424+
settingsBuffer[IDX_SETTINGS_FLAGS + 1 + 2 * numCustomSettings + 2] = val;
425+
numCustomSettings++;
426+
}
427+
}
428+
}
429+
}
430+
settingsBuffer[IDX_SETTINGS_FLAGS + 1] = numCustomSettings;
431+
353432
if (typeof settings.headerTableSize === 'number') {
354433
flags |= (1 << IDX_SETTINGS_HEADER_TABLE_SIZE);
355434
settingsBuffer[IDX_SETTINGS_HEADER_TABLE_SIZE] =

src/node_http2.cc

+14-1
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,16 @@ size_t Http2Settings::Init(
228228
HTTP2_SETTINGS(V)
229229
#undef V
230230

231+
uint32_t numAddSettings = buffer[IDX_SETTINGS_COUNT + 1];
232+
if (numAddSettings > 0) {
233+
uint32_t offset = IDX_SETTINGS_COUNT + 1 + 1;
234+
for (uint32_t i = 0; i < numAddSettings; i++) {
235+
uint32_t key = buffer[offset + i * 2 + 0];
236+
uint32_t val = buffer[offset + i * 2 + 1];
237+
entries[count++] = nghttp2_settings_entry{(int32_t)key, val};
238+
}
239+
}
240+
231241
return count;
232242
}
233243
#undef GRABSETTING
@@ -262,7 +272,7 @@ Local<Value> Http2Settings::Pack() {
262272
}
263273

264274
Local<Value> Http2Settings::Pack(Http2State* state) {
265-
nghttp2_settings_entry entries[IDX_SETTINGS_COUNT];
275+
nghttp2_settings_entry entries[IDX_SETTINGS_COUNT + MAX_ADDITIONAL_SETTINGS];
266276
size_t count = Init(state, entries);
267277
return Pack(state->env(), count, entries);
268278
}
@@ -298,6 +308,8 @@ void Http2Settings::Update(Http2Session* session, get_setting fn) {
298308
fn(session->session(), NGHTTP2_SETTINGS_ ## name);
299309
HTTP2_SETTINGS(V)
300310
#undef V
311+
buffer[IDX_SETTINGS_COUNT + 1] =
312+
0; // no additional settings are coming, clear them
301313
}
302314

303315
// Initializes the shared TypedArray with the default settings values.
@@ -314,6 +326,7 @@ void Http2Settings::RefreshDefaults(Http2State* http2_state) {
314326
#undef V
315327

316328
buffer[IDX_SETTINGS_COUNT] = flags;
329+
buffer[IDX_SETTINGS_COUNT + 1] = 0; // no additional settings
317330
}
318331

319332

src/node_http2.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -1035,7 +1035,7 @@ class Http2Settings : public AsyncWrap {
10351035
v8::Global<v8::Function> callback_;
10361036
uint64_t startTime_;
10371037
size_t count_ = 0;
1038-
nghttp2_settings_entry entries_[IDX_SETTINGS_COUNT];
1038+
nghttp2_settings_entry entries_[IDX_SETTINGS_COUNT + MAX_ADDITIONAL_SETTINGS];
10391039
};
10401040

10411041
class Origins {

src/node_http2_state.h

+14-5
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ namespace http2 {
2121
IDX_SETTINGS_COUNT
2222
};
2323

24+
// number of max additional settings, thus settings not implemented by nghttp2
25+
const size_t MAX_ADDITIONAL_SETTINGS = 10;
26+
2427
enum Http2SessionStateIndex {
2528
IDX_SESSION_STATE_EFFECTIVE_LOCAL_WINDOW_SIZE,
2629
IDX_SESSION_STATE_EFFECTIVE_RECV_DATA_LENGTH,
@@ -108,10 +111,11 @@ class Http2State : public BaseObject {
108111
offsetof(http2_state_internal, options_buffer),
109112
IDX_OPTIONS_FLAGS + 1,
110113
root_buffer),
111-
settings_buffer(realm->isolate(),
112-
offsetof(http2_state_internal, settings_buffer),
113-
IDX_SETTINGS_COUNT + 1,
114-
root_buffer) {}
114+
settings_buffer(
115+
realm->isolate(),
116+
offsetof(http2_state_internal, settings_buffer),
117+
IDX_SETTINGS_COUNT + 1 + 1 + 2 * MAX_ADDITIONAL_SETTINGS,
118+
root_buffer) {}
115119

116120
AliasedUint8Array root_buffer;
117121
AliasedFloat64Array session_state_buffer;
@@ -135,7 +139,12 @@ class Http2State : public BaseObject {
135139
double stream_stats_buffer[IDX_STREAM_STATS_COUNT];
136140
double session_stats_buffer[IDX_SESSION_STATS_COUNT];
137141
uint32_t options_buffer[IDX_OPTIONS_FLAGS + 1];
138-
uint32_t settings_buffer[IDX_SETTINGS_COUNT + 1];
142+
// first + 1: number of actual nghttp2 supported settings
143+
// second + 1: number of additional settings not suppoted by nghttp2
144+
// 2 * MAX_ADDITIONAL_SETTINGS: settings id and value for each
145+
// additional setting
146+
uint32_t settings_buffer[IDX_SETTINGS_COUNT + 1 + 1 +
147+
2 * MAX_ADDITIONAL_SETTINGS];
139148
};
140149
};
141150

0 commit comments

Comments
 (0)