Skip to content

Commit edd8bd0

Browse files
cjihrigmcollina
authored andcommitted
cli: add --max-http-header-size flag
Allow the maximum size of HTTP headers to be overridden from the command line. co-authored-by: Matteo Collina <hello@matteocollina.com> PR-URL: #24811Fixes: #24692 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Myles Borins <myles.borins@gmail.com> Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com> Reviewed-By: Сковорода Никита Андреевич <chalkerx@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Jeremiah Senkpiel <fishrock123@rocketmail.com>
1 parent c80ac7f commit edd8bd0

7 files changed

Lines changed: 184 additions & 28 deletions

File tree

‎doc/api/cli.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,13 @@ added: v9.0.0
197197

198198
Specify the `file` of the custom [experimental ECMAScript Module][] loader.
199199

200+
### `--max-http-header-size=size`
201+
<!-- YAML
202+
added: REPLACEME
203+
-->
204+
205+
Specify the maximum size, in bytes, of HTTP headers. Defaults to 8KB.
206+
200207
### `--napi-modules`
201208
<!-- YAML
202209
added: v7.10.0
@@ -604,6 +611,7 @@ Node.js options that are allowed are:
604611
-`--inspect-brk`
605612
-`--inspect-port`
606613
-`--loader`
614+
-`--max-http-header-size`
607615
-`--napi-modules`
608616
-`--no-deprecation`
609617
-`--no-force-async-hooks-checks`

‎doc/node.1‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,9 @@ Specify the
139139
as a custom loader, to load
140140
.Fl-experimental-modules .
141141
.
142+
.ItFl-max-http-header-sizeNs=NsArsize
143+
Specify the maximum size of HTTP headers in bytes. Defaults to 8KB.
144+
.
142145
.ItFl-napi-modules
143146
This option is a no-op.
144147
It is kept for compatibility.

‎src/node_http_parser_impl.h‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -830,7 +830,7 @@ class Parser : public AsyncWrap, public StreamListener {
830830
intTrackHeader(size_t len) {
831831
#ifdef NODE_EXPERIMENTAL_HTTP
832832
header_nread_ += len;
833-
if (header_nread_ >= kMaxHeaderSize) {
833+
if (header_nread_ >= per_process_opts->max_http_header_size) {
834834
llhttp_set_error_reason(&parser_, "HPE_HEADER_OVERFLOW:Header overflow");
835835
returnHPE_USER;
836836
}
@@ -892,9 +892,6 @@ class Parser : public AsyncWrap, public StreamListener {
892892
typedefint (Parser::*DataCall)(constchar* at, size_t length);
893893

894894
staticconstparser_settings_t settings;
895-
#ifdef NODE_EXPERIMENTAL_HTTP
896-
staticconstuint64_tkMaxHeaderSize = 8 * 1024;
897-
#endif/* NODE_EXPERIMENTAL_HTTP */
898895
};
899896

900897
constparser_settings_t Parser::settings = {
@@ -916,6 +913,14 @@ const parser_settings_t Parser::settings = {
916913
};
917914

918915

916+
#ifndef NODE_EXPERIMENTAL_HTTP
917+
voidInitMaxHttpHeaderSizeOnce() {
918+
constuint32_t max_http_header_size = per_process_opts->max_http_header_size;
919+
http_parser_set_max_header_size(max_http_header_size);
920+
}
921+
#endif/* NODE_EXPERIMENTAL_HTTP */
922+
923+
919924
voidInitializeHttpParser(Local<Object> target,
920925
Local<Value> unused,
921926
Local<Context> context,
@@ -965,6 +970,11 @@ void InitializeHttpParser(Local<Object> target,
965970
target->Set(env->context(),
966971
FIXED_ONE_BYTE_STRING(env->isolate(), "HTTPParser"),
967972
t->GetFunction(env->context()).ToLocalChecked()).FromJust();
973+
974+
#ifndef NODE_EXPERIMENTAL_HTTP
975+
staticuv_once_t init_once = UV_ONCE_INIT;
976+
uv_once(&init_once, InitMaxHttpHeaderSizeOnce);
977+
#endif/* NODE_EXPERIMENTAL_HTTP */
968978
}
969979

970980
} // anonymous namespace

‎src/node_options.cc‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,10 @@ PerProcessOptionsParser::PerProcessOptionsParser() {
252252
kAllowedInEnvironment);
253253
AddAlias("--trace-events-enabled", {
254254
"--trace-event-categories", "v8,node,node.async_hooks" });
255+
AddOption("--max-http-header-size",
256+
"set the maximum size of HTTP headers (default: 8KB)",
257+
&PerProcessOptions::max_http_header_size,
258+
kAllowedInEnvironment);
255259
AddOption("--v8-pool-size",
256260
"set V8's thread pool size",
257261
&PerProcessOptions::v8_thread_pool_size,

‎src/node_options.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ class PerProcessOptions : public Options {
151151
std::string title;
152152
std::string trace_event_categories;
153153
std::string trace_event_file_pattern = "node_trace.${rotation}.log";
154+
uint64_t max_http_header_size = 8 * 1024;
154155
int64_t v8_thread_pool_size = 4;
155156
bool zero_fill_all_buffers = false;
156157

‎test/sequential/test-http-max-http-headers.js‎

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@ const common = require('../common');
44
constassert=require('assert');
55
consthttp=require('http');
66
constnet=require('net');
7-
constMAX=8*1024;// 8KB
7+
constMAX=+(process.argv[2]||8*1024);// Command line option, or 8KB.
88

99
const{ getOptionValue }=require('internal/options');
1010

11+
console.log('pid is',process.pid);
12+
console.log('max header size is',getOptionValue('--max-http-header-size'));
13+
console.log('current http parser is',getOptionValue('--http-parser'));
14+
1115
// Verify that we cannot receive more than 8KB of headers.
1216

1317
functiononce(cb){
@@ -38,19 +42,15 @@ function fillHeaders(headers, currentSize, valid = false) {
3842

3943
// Generate valid headers
4044
if(valid){
41-
// TODO(mcollina): understand why -9 is needed instead of -1
42-
headers=headers.slice(0,-9);
45+
// TODO(mcollina): understand why -32 is needed instead of -1
46+
headers=headers.slice(0,-32);
4347
}
4448
returnheaders+'\r\n\r\n';
4549
}
4650

47-
consttimeout=common.platformTimeout(10);
48-
4951
functionwriteHeaders(socket,headers){
5052
constarray=[];
51-
52-
// this is off from 1024 so that \r\n does not get split
53-
constchunkSize=1000;
53+
constchunkSize=100;
5454
letlast=0;
5555

5656
for(leti=0;i<headers.length/chunkSize;i++){
@@ -65,19 +65,25 @@ function writeHeaders(socket, headers) {
6565
next();
6666

6767
functionnext(){
68-
if(socket.write(array.shift())){
69-
if(array.length===0){
70-
socket.end();
71-
}else{
72-
setTimeout(next,timeout);
73-
}
68+
if(socket.destroyed){
69+
console.log('socket was destroyed early, data left to write:',
70+
array.join('').length);
71+
return;
72+
}
73+
74+
constchunk=array.shift();
75+
76+
if(chunk){
77+
console.log('writing chunk of size',chunk.length);
78+
socket.write(chunk,next);
7479
}else{
75-
socket.once('drain',next);
80+
socket.end();
7681
}
7782
}
7883
}
7984

8085
functiontest1(){
86+
console.log('test1');
8187
letheaders=
8288
'HTTP/1.1 200 OK\r\n'+
8389
'Content-Length: 0\r\n'+
@@ -92,6 +98,9 @@ function test1() {
9298
writeHeaders(sock,headers);
9399
sock.resume();
94100
});
101+
102+
// The socket might error but that's ok
103+
sock.on('error',()=>{});
95104
});
96105

97106
server.listen(0,common.mustCall(()=>{
@@ -100,17 +109,17 @@ function test1() {
100109

101110
client.on('error',common.mustCall((err)=>{
102111
assert.strictEqual(err.code,'HPE_HEADER_OVERFLOW');
103-
server.close();
104-
setImmediate(test2);
112+
server.close(test2);
105113
}));
106114
}));
107115
}
108116

109117
consttest2=common.mustCall(()=>{
118+
console.log('test2');
110119
letheaders=
111120
'GET / HTTP/1.1\r\n'+
112121
'Host: localhost\r\n'+
113-
'Agent: node\r\n'+
122+
'Agent: nod2\r\n'+
114123
'X-CRASH: ';
115124

116125
// /, Host, localhost, Agent, node, X-CRASH, a...
@@ -119,7 +128,7 @@ const test2 = common.mustCall(() => {
119128

120129
constserver=http.createServer(common.mustNotCall());
121130

122-
server.on('clientError',common.mustCall((err)=>{
131+
server.once('clientError',common.mustCall((err)=>{
123132
assert.strictEqual(err.code,'HPE_HEADER_OVERFLOW');
124133
}));
125134

@@ -131,34 +140,46 @@ const test2 = common.mustCall(() => {
131140
});
132141

133142
finished(client,common.mustCall((err)=>{
134-
server.close();
135-
setImmediate(test3);
143+
server.close(test3);
136144
}));
137145
}));
138146
});
139147

140148
consttest3=common.mustCall(()=>{
149+
console.log('test3');
141150
letheaders=
142151
'GET / HTTP/1.1\r\n'+
143152
'Host: localhost\r\n'+
144-
'Agent: node\r\n'+
153+
'Agent: nod3\r\n'+
145154
'X-CRASH: ';
146155

147156
// /, Host, localhost, Agent, node, X-CRASH, a...
148157
constcurrentSize=1+4+9+5+4+7;
149158
headers=fillHeaders(headers,currentSize,true);
150159

160+
console.log('writing',headers.length);
161+
151162
constserver=http.createServer(common.mustCall((req,res)=>{
152-
res.end('hello world');
153-
setImmediate(server.close.bind(server));
163+
res.end('hello from test3 server');
164+
server.close();
154165
}));
155166

167+
server.on('clientError',(err)=>{
168+
console.log(err.code);
169+
if(err.code==='HPE_HEADER_OVERFLOW'){
170+
console.log(err.rawPacket.toString('hex'));
171+
}
172+
});
173+
server.on('clientError',common.mustNotCall());
174+
156175
server.listen(0,common.mustCall(()=>{
157176
constclient=net.connect(server.address().port);
158177
client.on('connect',()=>{
159178
writeHeaders(client,headers);
160179
client.resume();
161180
});
181+
182+
client.pipe(process.stdout);
162183
}));
163184
});
164185

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
'use strict';
2+
3+
constcommon=require('../common');
4+
constassert=require('assert');
5+
const{ spawn }=require('child_process');
6+
constpath=require('path');
7+
consttestName=path.join(__dirname,'test-http-max-http-headers.js');
8+
constparsers=['legacy','llhttp'];
9+
10+
consttimeout=common.platformTimeout(100);
11+
12+
consttests=[];
13+
14+
functiontest(fn){
15+
tests.push(fn);
16+
}
17+
18+
parsers.forEach((parser)=>{
19+
test(function(cb){
20+
console.log('running subtest expecting failure');
21+
22+
// Validate that the test fails if the max header size is too small.
23+
constargs=['--expose-internals',
24+
`--http-parser=${parser}`,
25+
'--max-http-header-size=1024',
26+
testName];
27+
constcp=spawn(process.execPath,args,{stdio: 'inherit'});
28+
29+
cp.on('close',common.mustCall((code,signal)=>{
30+
assert.strictEqual(code,1);
31+
assert.strictEqual(signal,null);
32+
cb();
33+
}));
34+
});
35+
36+
test(function(cb){
37+
console.log('running subtest expecting success');
38+
39+
constenv=Object.assign({},process.env,{
40+
NODE_DEBUG: 'http'
41+
});
42+
43+
// Validate that the test fails if the max header size is too small.
44+
// Validate that the test now passes if the same limit becomes large enough.
45+
constargs=['--expose-internals',
46+
`--http-parser=${parser}`,
47+
'--max-http-header-size=1024',
48+
testName,
49+
'1024'];
50+
constcp=spawn(process.execPath,args,{
51+
env,
52+
stdio: 'inherit'
53+
});
54+
55+
cp.on('close',common.mustCall((code,signal)=>{
56+
assert.strictEqual(code,0);
57+
assert.strictEqual(signal,null);
58+
cb();
59+
}));
60+
});
61+
62+
// Next, repeat the same checks using NODE_OPTIONS if it is supported.
63+
if(process.config.variables.node_without_node_options){
64+
constenv=Object.assign({},process.env,{
65+
NODE_OPTIONS: `--http-parser=${parser} --max-http-header-size=1024`
66+
});
67+
68+
test(function(cb){
69+
console.log('running subtest expecting failure');
70+
71+
// Validate that the test fails if the max header size is too small.
72+
constargs=['--expose-internals',testName];
73+
constcp=spawn(process.execPath,args,{ env,stdio: 'inherit'});
74+
75+
cp.on('close',common.mustCall((code,signal)=>{
76+
assert.strictEqual(code,1);
77+
assert.strictEqual(signal,null);
78+
cb();
79+
}));
80+
});
81+
82+
test(function(cb){
83+
// Validate that the test now passes if the same limit
84+
// becomes large enough.
85+
constargs=['--expose-internals',testName,'1024'];
86+
constcp=spawn(process.execPath,args,{ env,stdio: 'inherit'});
87+
88+
cp.on('close',common.mustCall((code,signal)=>{
89+
assert.strictEqual(code,0);
90+
assert.strictEqual(signal,null);
91+
cb();
92+
}));
93+
});
94+
}
95+
});
96+
97+
functionrunTest(){
98+
constfn=tests.shift();
99+
100+
if(!fn){
101+
return;
102+
}
103+
104+
fn(()=>{
105+
setTimeout(runTest,timeout);
106+
});
107+
}
108+
109+
runTest();

0 commit comments

Comments
 (0)