Skip to content

Commit 23414a6

Browse files
martenrichterrichardlau
authored andcommitted
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: #1337 PR-URL: #49025 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Marco Ippolito <[email protected]>
1 parent da17a25 commit 23414a6

File tree

10 files changed

+222
-11
lines changed

10 files changed

+222
-11
lines changed

‎doc/api/errors.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1710,6 +1710,12 @@ When setting the priority for an HTTP/2 stream, the stream may be marked as
17101710
a dependency for a parent stream. This error code is used when an attempt is
17111711
made to mark a stream and dependent of itself.
17121712

1713+
<aid="ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS"></a>
1714+
1715+
### `ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS`
1716+
1717+
The number of supported custom settings (10) has been exceeded.
1718+
17131719
<aid="ERR_HTTP2_TOO_MANY_INVALID_FRAMES"></a>
17141720

17151721
### `ERR_HTTP2_TOO_MANY_INVALID_FRAMES`

‎doc/api/http2.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3010,6 +3010,19 @@ properties.
30103010
meaningful if sent by the server. Once the `enableConnectProtocol` setting
30113011
has been enabled for a given `Http2Session`, it cannot be disabled.
30123012
**Default:**`false`.
3013+
*`customSettings`{Object} Specifies additional settings, yet not implemented
3014+
in node and the underlying libraries. The key of the object defines the
3015+
numeric value of the settings type (as defined in the "HTTP/2 SETTINGS"
3016+
registry established by \[RFC 7540]) and the values the actual numeric value
3017+
of the settings.
3018+
The settings type has to be an integer in the range from 1 to 2^16-1.
3019+
It should not be a settings type already handled by node, i.e. currently
3020+
it should be greater than 6, although it is not an error.
3021+
The values need to be unsigned integers in the range from 0 to 2^32-1.
3022+
Currently, a maximum of up 10 custom settings is supported.
3023+
It is only supported for sending SETTINGS.
3024+
Custom settings are not supported for the functions retrieving remote and
3025+
local settings as nghttp2 does not pass unknown HTTP/2 settings to Node.js.
30133026

30143027
All additional properties on the settings object are ignored.
30153028

‎lib/internal/errors.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1279,6 +1279,8 @@ E('ERR_HTTP2_STREAM_CANCEL', function(error){
12791279
E('ERR_HTTP2_STREAM_ERROR','Stream closed with error code %s',Error);
12801280
E('ERR_HTTP2_STREAM_SELF_DEPENDENCY',
12811281
'A stream cannot depend on itself',Error);
1282+
E('ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS',
1283+
'Number of custom settings exceeds MAX_ADDITIONAL_SETTINGS',Error);
12821284
E('ERR_HTTP2_TOO_MANY_INVALID_FRAMES','Too many invalid HTTP/2 frames',Error);
12831285
E('ERR_HTTP2_TRAILERS_ALREADY_SENT',
12841286
'Trailing headers have already been sent',Error);

‎lib/internal/http2/core.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,8 @@ function pingCallback(cb){
946946
// All settings are optional and may be left undefined
947947
constvalidateSettings=hideStackFrames((settings)=>{
948948
if(settings===undefined)return;
949+
assertIsObject.withoutStackTrace(settings.customSettings,'customSettings','Number');
950+
949951
assertWithinRange.withoutStackTrace('headerTableSize',
950952
settings.headerTableSize,
951953
0,kMaxInt);
@@ -3387,6 +3389,10 @@ function getUnpackedSettings(buf, options = kEmptyObject){
33873389
break;
33883390
caseNGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL:
33893391
settings.enableConnectProtocol=value!==0;
3392+
break;
3393+
default:
3394+
if(!settings.customSettings)settings.customSettings={};
3395+
settings.customSettings[id]=value;
33903396
}
33913397
offset+=4;
33923398
}

‎lib/internal/http2/util.js‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const{
88
Error,
99
MathMax,
1010
Number,
11+
NumberIsNaN,
1112
ObjectKeys,
1213
SafeSet,
1314
String,
@@ -24,6 +25,7 @@ const{
2425
ERR_HTTP2_INVALID_CONNECTION_HEADERS,
2526
ERR_HTTP2_INVALID_PSEUDOHEADER: {HideStackFramesError: ERR_HTTP2_INVALID_PSEUDOHEADER},
2627
ERR_HTTP2_INVALID_SETTING_VALUE,
28+
ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS,
2729
ERR_INVALID_ARG_TYPE,
2830
ERR_INVALID_HTTP_TOKEN,
2931
},
@@ -190,6 +192,9 @@ const IDX_SETTINGS_MAX_HEADER_LIST_SIZE = 5;
190192
constIDX_SETTINGS_ENABLE_CONNECT_PROTOCOL=6;
191193
constIDX_SETTINGS_FLAGS=7;
192194

195+
// Maximum number of allowed additional settings
196+
constMAX_ADDITIONAL_SETTINGS=10;
197+
193198
constIDX_SESSION_STATE_EFFECTIVE_LOCAL_WINDOW_SIZE=0;
194199
constIDX_SESSION_STATE_EFFECTIVE_RECV_DATA_LENGTH=1;
195200
constIDX_SESSION_STATE_NEXT_STREAM_ID=2;
@@ -348,6 +353,80 @@ function getSettings(session, remote){
348353

349354
functionupdateSettingsBuffer(settings){
350355
letflags=0;
356+
letnumCustomSettings=0;
357+
358+
if(typeofsettings.customSettings==='object'){
359+
constcustomSettings=settings.customSettings;
360+
for(constsettingincustomSettings){
361+
constval=customSettings[setting];
362+
if(typeofval==='number'){
363+
letset=false;
364+
constnsetting=Number(setting);
365+
if(NumberIsNaN(nsetting)||
366+
typeofnsetting!=='number'||
367+
0>=nsetting||
368+
nsetting>0xffff)
369+
thrownewERR_HTTP2_INVALID_SETTING_VALUE.RangeError(
370+
'Range Error',nsetting,0,0xffff);
371+
if(NumberIsNaN(val)||
372+
typeofval!=='number'||
373+
0>=val||
374+
val>0xffffffff)
375+
thrownewERR_HTTP2_INVALID_SETTING_VALUE.RangeError(
376+
'Range Error',val,0,0xffffffff);
377+
if(nsetting<IDX_SETTINGS_FLAGS){
378+
set=true;
379+
switch(nsetting){
380+
caseIDX_SETTINGS_HEADER_TABLE_SIZE:
381+
flags|=(1<<IDX_SETTINGS_HEADER_TABLE_SIZE);
382+
settingsBuffer[IDX_SETTINGS_HEADER_TABLE_SIZE]=
383+
val;
384+
break;
385+
caseIDX_SETTINGS_ENABLE_PUSH:
386+
flags|=(1<<IDX_SETTINGS_ENABLE_PUSH);
387+
settingsBuffer[IDX_SETTINGS_ENABLE_PUSH]=val;
388+
break;
389+
caseIDX_SETTINGS_INITIAL_WINDOW_SIZE:
390+
flags|=(1<<IDX_SETTINGS_INITIAL_WINDOW_SIZE);
391+
settingsBuffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE]=
392+
val;
393+
break;
394+
caseIDX_SETTINGS_MAX_FRAME_SIZE:
395+
flags|=(1<<IDX_SETTINGS_MAX_FRAME_SIZE);
396+
settingsBuffer[IDX_SETTINGS_MAX_FRAME_SIZE]=
397+
val;
398+
break;
399+
caseIDX_SETTINGS_MAX_CONCURRENT_STREAMS:
400+
flags|=(1<<IDX_SETTINGS_MAX_CONCURRENT_STREAMS);
401+
settingsBuffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS]=val;
402+
break;
403+
caseIDX_SETTINGS_MAX_HEADER_LIST_SIZE:
404+
flags|=(1<<IDX_SETTINGS_MAX_HEADER_LIST_SIZE);
405+
settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE]=
406+
val;
407+
break;
408+
caseIDX_SETTINGS_ENABLE_CONNECT_PROTOCOL:
409+
flags|=(1<<IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL);
410+
settingsBuffer[IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL]=val;
411+
break;
412+
default:
413+
set=false;
414+
break;
415+
}
416+
}
417+
if(!set){// not supported
418+
if(numCustomSettings===MAX_ADDITIONAL_SETTINGS)
419+
thrownewERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS();
420+
421+
settingsBuffer[IDX_SETTINGS_FLAGS+1+2*numCustomSettings+1]=nsetting;
422+
settingsBuffer[IDX_SETTINGS_FLAGS+1+2*numCustomSettings+2]=val;
423+
numCustomSettings++;
424+
}
425+
}
426+
}
427+
}
428+
settingsBuffer[IDX_SETTINGS_FLAGS+1]=numCustomSettings;
429+
351430
if(typeofsettings.headerTableSize==='number'){
352431
flags|=(1<<IDX_SETTINGS_HEADER_TABLE_SIZE);
353432
settingsBuffer[IDX_SETTINGS_HEADER_TABLE_SIZE]=

‎src/node_http2.cc‎

Lines changed: 14 additions & 1 deletion
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
returnPack(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‎

Lines changed: 1 addition & 1 deletion
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
classOrigins{

‎src/node_http2_state.h‎

Lines changed: 14 additions & 5 deletions
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+
constsize_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

‎test/parallel/test-http2-getpackedsettings.js‎

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ assert.deepStrictEqual(val, check);
2929
['maxHeaderListSize',2**32-1],
3030
['maxHeaderSize',0],
3131
['maxHeaderSize',2**32-1],
32+
['customSettings',{'9999': 301}],
3233
].forEach((i)=>{
3334
// Valid options should not throw.
3435
http2.getPackedSettings({[i[0]]: i[1]});
@@ -93,6 +94,7 @@ http2.getPackedSettings({enablePush: false });
9394
0x00,0x05,0x00,0x00,0x4e,0x20,
9495
0x00,0x06,0x00,0x00,0x00,0x64,
9596
0x00,0x08,0x00,0x00,0x00,0x00,
97+
0x27,0x0F,0x00,0x00,0x01,0x2d,
9698
]);
9799

98100
constpacked=http2.getPackedSettings({
@@ -104,12 +106,90 @@ http2.getPackedSettings({enablePush: false });
104106
maxHeaderSize: 100,
105107
enablePush: true,
106108
enableConnectProtocol: false,
107-
foo: 'ignored'
109+
foo: 'ignored',
110+
customSettings: {'9999': 301}
108111
});
109-
assert.strictEqual(packed.length,42);
112+
assert.strictEqual(packed.length,48);
110113
assert.deepStrictEqual(packed,check);
111114
}
112115

116+
// Check if multiple custom settings can be set
117+
{
118+
constcheck=Buffer.from([
119+
0x00,0x01,0x00,0x00,0x00,0x64,
120+
0x00,0x02,0x00,0x00,0x00,0x01,
121+
0x00,0x03,0x00,0x00,0x00,0xc8,
122+
0x00,0x04,0x00,0x00,0x00,0x64,
123+
0x00,0x05,0x00,0x00,0x4e,0x20,
124+
0x00,0x06,0x00,0x00,0x00,0x64,
125+
0x00,0x08,0x00,0x00,0x00,0x00,
126+
0x03,0xf3,0x00,0x00,0x07,0x9F,
127+
0x0a,0x2e,0x00,0x00,0x00,0x58,
128+
]);
129+
130+
constpacked=http2.getPackedSettings({
131+
headerTableSize: 100,
132+
initialWindowSize: 100,
133+
maxFrameSize: 20000,
134+
maxConcurrentStreams: 200,
135+
maxHeaderListSize: 100,
136+
maxHeaderSize: 100,
137+
enablePush: true,
138+
enableConnectProtocol: false,
139+
customSettings: {'2606': 88,'1011': 1951}
140+
});
141+
assert.strictEqual(packed.length,54);
142+
assert.deepStrictEqual(packed,check);
143+
}
144+
145+
{
146+
// Check if wrong custom settings cause an error
147+
148+
assert.throws(()=>{
149+
http2.getPackedSettings({
150+
customSettings: {'-1': 659685}
151+
});
152+
},{
153+
code: 'ERR_HTTP2_INVALID_SETTING_VALUE',
154+
name: 'RangeError'
155+
});
156+
157+
assert.throws(()=>{
158+
http2.getPackedSettings({
159+
customSettings: {'10': 34577577777}
160+
});
161+
},{
162+
code: 'ERR_HTTP2_INVALID_SETTING_VALUE',
163+
name: 'RangeError'
164+
});
165+
166+
assert.throws(()=>{
167+
http2.getPackedSettings({
168+
customSettings: {'notvalid': -777}
169+
});
170+
},{
171+
code: 'ERR_HTTP2_INVALID_SETTING_VALUE',
172+
name: 'RangeError'
173+
});
174+
175+
assert.throws(()=>{
176+
http2.getPackedSettings({
177+
customSettings: {'11': 11,'12': 12,'13': 13,'14': 14,'15': 15,'16': 16,
178+
'17': 17,'18': 18,'19': 19,'20': 20,'21': 21}
179+
});
180+
},{
181+
code: 'ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS'
182+
});
183+
assert.throws(()=>{
184+
http2.getPackedSettings({
185+
customSettings: {'11': 11,'12': 12,'13': 13,'14': 14,'15': 15,'16': 16,
186+
'17': 17,'18': 18,'19': 19,'20': 20,'21': 21,'22': 22}
187+
});
188+
},{
189+
code: 'ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS'
190+
});
191+
}
192+
113193
// Check for not passing settings.
114194
{
115195
constpacked=http2.getPackedSettings();
@@ -124,7 +204,8 @@ http2.getPackedSettings({enablePush: false });
124204
0x00,0x04,0x00,0x00,0x00,0x64,
125205
0x00,0x06,0x00,0x00,0x00,0x64,
126206
0x00,0x02,0x00,0x00,0x00,0x01,
127-
0x00,0x08,0x00,0x00,0x00,0x00]);
207+
0x00,0x08,0x00,0x00,0x00,0x00,
208+
0x27,0x0F,0x00,0x00,0x01,0x2d]);
128209

129210
[1,true,'',[],{},NaN].forEach((input)=>{
130211
assert.throws(()=>{
@@ -157,6 +238,7 @@ http2.getPackedSettings({enablePush: false });
157238
assert.strictEqual(settings.maxHeaderSize,100);
158239
assert.strictEqual(settings.enablePush,true);
159240
assert.strictEqual(settings.enableConnectProtocol,false);
241+
assert.deepStrictEqual(settings.customSettings,{'9999': 301});
160242
}
161243

162244
{

‎test/parallel/test-http2-update-settings.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ testUpdateSettingsWith({
1919
'maxHeaderListSize': 1,
2020
'maxFrameSize': 16385,
2121
'enablePush': false,
22-
'enableConnectProtocol': true
22+
'enableConnectProtocol': true,
23+
'customSettings': {'9999': 301}
2324
}
2425
});
2526
testUpdateSettingsWith({

0 commit comments

Comments
(0)