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: #24811 Fixes: #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

+184
-28
lines changed

doc/api/cli.md

+8
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

+3
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+
.It Fl -max-http-header-size Ns = Ns Ar size
143+
Specify the maximum size of HTTP headers in bytes. Defaults to 8KB.
144+
.
142145
.It Fl -napi-modules
143146
This option is a no-op.
144147
It is kept for compatibility.

src/node_http_parser_impl.h

+14-4
Original file line numberDiff line numberDiff line change
@@ -830,7 +830,7 @@ class Parser : public AsyncWrap, public StreamListener {
830830
int TrackHeader(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
return HPE_USER;
836836
}
@@ -892,9 +892,6 @@ class Parser : public AsyncWrap, public StreamListener {
892892
typedef int (Parser::*DataCall)(const char* at, size_t length);
893893

894894
static const parser_settings_t settings;
895-
#ifdef NODE_EXPERIMENTAL_HTTP
896-
static const uint64_t kMaxHeaderSize = 8 * 1024;
897-
#endif /* NODE_EXPERIMENTAL_HTTP */
898895
};
899896

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

918915

916+
#ifndef NODE_EXPERIMENTAL_HTTP
917+
void InitMaxHttpHeaderSizeOnce() {
918+
const uint32_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
void InitializeHttpParser(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+
static uv_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

+4
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

+1
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

+45-24
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@ const common = require('../common');
44
const assert = require('assert');
55
const http = require('http');
66
const net = require('net');
7-
const MAX = 8 * 1024; // 8KB
7+
const MAX = +(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
function once(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
return headers + '\r\n\r\n';
4549
}
4650

47-
const timeout = common.platformTimeout(10);
48-
4951
function writeHeaders(socket, headers) {
5052
const array = [];
51-
52-
// this is off from 1024 so that \r\n does not get split
53-
const chunkSize = 1000;
53+
const chunkSize = 100;
5454
let last = 0;
5555

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

6767
function next() {
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+
const chunk = 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
function test1() {
86+
console.log('test1');
8187
let headers =
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
const test2 = common.mustCall(() => {
118+
console.log('test2');
110119
let headers =
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
const server = 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
const test3 = common.mustCall(() => {
149+
console.log('test3');
141150
let headers =
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
const currentSize = 1 + 4 + 9 + 5 + 4 + 7;
149158
headers = fillHeaders(headers, currentSize, true);
150159

160+
console.log('writing', headers.length);
161+
151162
const server = 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
const client = 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

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const { spawn } = require('child_process');
6+
const path = require('path');
7+
const testName = path.join(__dirname, 'test-http-max-http-headers.js');
8+
const parsers = ['legacy', 'llhttp'];
9+
10+
const timeout = common.platformTimeout(100);
11+
12+
const tests = [];
13+
14+
function test(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+
const args = ['--expose-internals',
24+
`--http-parser=${parser}`,
25+
'--max-http-header-size=1024',
26+
testName];
27+
const cp = 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+
const env = 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+
const args = ['--expose-internals',
46+
`--http-parser=${parser}`,
47+
'--max-http-header-size=1024',
48+
testName,
49+
'1024'];
50+
const cp = 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+
const env = 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+
const args = ['--expose-internals', testName];
73+
const cp = 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+
const args = ['--expose-internals', testName, '1024'];
86+
const cp = 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+
function runTest() {
98+
const fn = 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)