Skip to content

Commit 8928c07

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. Backport-PR-URL: #25168 co-authored-by: Matteo Collina <[email protected]> PR-URL: #24811 Fixes: #24692 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Myles Borins <[email protected]> Reviewed-By: Michael Dawson <[email protected]> Reviewed-By: Сковорода Никита Андреевич <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Jeremiah Senkpiel <[email protected]>
1 parent 82ad2c2 commit 8928c07

File tree

7 files changed

+174
-24
lines changed

7 files changed

+174
-24
lines changed

doc/api/cli.md

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

182182
Specify the `file` of the custom [experimental ECMAScript Module][] loader.
183183

184+
### `--max-http-header-size=size`
185+
<!-- YAML
186+
added: REPLACEME
187+
-->
188+
189+
Specify the maximum size, in bytes, of HTTP headers. Defaults to 8KB.
190+
184191
### `--napi-modules`
185192
<!-- YAML
186193
added: v7.10.0
@@ -584,6 +591,7 @@ Node.js options that are allowed are:
584591
- `--inspect-brk`
585592
- `--inspect-port`
586593
- `--loader`
594+
- `--max-http-header-size`
587595
- `--napi-modules`
588596
- `--no-deprecation`
589597
- `--no-force-async-hooks-checks`

doc/node.1

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ Specify the
133133
as a custom loader, to load
134134
.Fl -experimental-modules .
135135
.
136+
.It Fl -max-http-header-size Ns = Ns Ar size
137+
Specify the maximum size of HTTP headers in bytes. Defaults to 8KB.
138+
.
136139
.It Fl -napi-modules
137140
This option is a no-op.
138141
It is kept for compatibility.

src/node_http_parser.cc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,10 @@ const struct http_parser_settings Parser::settings = {
738738
nullptr // on_chunk_complete
739739
};
740740

741+
void InitMaxHttpHeaderSizeOnce() {
742+
const uint32_t max_http_header_size = per_process_opts->max_http_header_size;
743+
http_parser_set_max_header_size(max_http_header_size);
744+
}
741745

742746
void Initialize(Local<Object> target,
743747
Local<Value> unused,
@@ -784,6 +788,9 @@ void Initialize(Local<Object> target,
784788

785789
target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "HTTPParser"),
786790
t->GetFunction(env->context()).ToLocalChecked());
791+
792+
static uv_once_t init_once = UV_ONCE_INIT;
793+
uv_once(&init_once, InitMaxHttpHeaderSizeOnce);
787794
}
788795

789796
} // anonymous namespace

src/node_options.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,10 @@ PerProcessOptionsParser::PerProcessOptionsParser() {
236236
kAllowedInEnvironment);
237237
AddAlias("--trace-events-enabled", {
238238
"--trace-event-categories", "v8,node,node.async_hooks" });
239+
AddOption("--max-http-header-size",
240+
"set the maximum size of HTTP headers (default: 8KB)",
241+
&PerProcessOptions::max_http_header_size,
242+
kAllowedInEnvironment);
239243
AddOption("--v8-pool-size",
240244
"set V8's thread pool size",
241245
&PerProcessOptions::v8_thread_pool_size,

src/node_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ class PerProcessOptions : public Options {
116116
std::string title;
117117
std::string trace_event_categories;
118118
std::string trace_event_file_pattern = "node_trace.${rotation}.log";
119+
uint64_t max_http_header_size = 8 * 1024;
119120
int64_t v8_thread_pool_size = 4;
120121
bool zero_fill_all_buffers = false;
121122

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

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
'use strict';
2+
// Flags: --expose_internals
23

34
const assert = require('assert');
45
const common = require('../common');
56
const http = require('http');
67
const net = require('net');
7-
const MAX = 8 * 1024; // 8KB
8+
const MAX = +(process.argv[2] || 8 * 1024); // Command line option, or 8KB.
9+
10+
const { getOptionValue } = require('internal/options');
11+
12+
console.log('pid is', process.pid);
13+
console.log('max header size is', getOptionValue('--max-http-header-size'));
814

915
// Verify that we cannot receive more than 8KB of headers.
1016

@@ -28,19 +34,15 @@ function fillHeaders(headers, currentSize, valid = false) {
2834
headers += 'a'.repeat(MAX - headers.length - 3);
2935
// Generate valid headers
3036
if (valid) {
31-
// TODO(mcollina): understand why -9 is needed instead of -1
32-
headers = headers.slice(0, -9);
37+
// TODO(mcollina): understand why -32 is needed instead of -1
38+
headers = headers.slice(0, -32);
3339
}
3440
return headers + '\r\n\r\n';
3541
}
3642

37-
const timeout = common.platformTimeout(10);
38-
3943
function writeHeaders(socket, headers) {
4044
const array = [];
41-
42-
// this is off from 1024 so that \r\n does not get split
43-
const chunkSize = 1000;
45+
const chunkSize = 100;
4446
let last = 0;
4547

4648
for (let i = 0; i < headers.length / chunkSize; i++) {
@@ -55,19 +57,25 @@ function writeHeaders(socket, headers) {
5557
next();
5658

5759
function next() {
58-
if (socket.write(array.shift())) {
59-
if (array.length === 0) {
60-
socket.end();
61-
} else {
62-
setTimeout(next, timeout);
63-
}
60+
if (socket.destroyed) {
61+
console.log('socket was destroyed early, data left to write:',
62+
array.join('').length);
63+
return;
64+
}
65+
66+
const chunk = array.shift();
67+
68+
if (chunk) {
69+
console.log('writing chunk of size', chunk.length);
70+
socket.write(chunk, next);
6471
} else {
65-
socket.once('drain', next);
72+
socket.end();
6673
}
6774
}
6875
}
6976

7077
function test1() {
78+
console.log('test1');
7179
let headers =
7280
'HTTP/1.1 200 OK\r\n' +
7381
'Content-Length: 0\r\n' +
@@ -82,6 +90,9 @@ function test1() {
8290
writeHeaders(sock, headers);
8391
sock.resume();
8492
});
93+
94+
// The socket might error but that's ok
95+
sock.on('error', () => {});
8596
});
8697

8798
server.listen(0, common.mustCall(() => {
@@ -90,17 +101,17 @@ function test1() {
90101

91102
client.on('error', common.mustCall((err) => {
92103
assert.strictEqual(err.code, 'HPE_HEADER_OVERFLOW');
93-
server.close();
94-
setImmediate(test2);
104+
server.close(test2);
95105
}));
96106
}));
97107
}
98108

99109
const test2 = common.mustCall(() => {
110+
console.log('test2');
100111
let headers =
101112
'GET / HTTP/1.1\r\n' +
102113
'Host: localhost\r\n' +
103-
'Agent: node\r\n' +
114+
'Agent: nod2\r\n' +
104115
'X-CRASH: ';
105116

106117
// /, Host, localhost, Agent, node, X-CRASH, a...
@@ -109,7 +120,7 @@ const test2 = common.mustCall(() => {
109120

110121
const server = http.createServer(common.mustNotCall());
111122

112-
server.on('clientError', common.mustCall((err) => {
123+
server.once('clientError', common.mustCall((err) => {
113124
assert.strictEqual(err.code, 'HPE_HEADER_OVERFLOW');
114125
}));
115126

@@ -121,34 +132,46 @@ const test2 = common.mustCall(() => {
121132
});
122133

123134
finished(client, common.mustCall((err) => {
124-
server.close();
125-
setImmediate(test3);
135+
server.close(test3);
126136
}));
127137
}));
128138
});
129139

130140
const test3 = common.mustCall(() => {
141+
console.log('test3');
131142
let headers =
132143
'GET / HTTP/1.1\r\n' +
133144
'Host: localhost\r\n' +
134-
'Agent: node\r\n' +
145+
'Agent: nod3\r\n' +
135146
'X-CRASH: ';
136147

137148
// /, Host, localhost, Agent, node, X-CRASH, a...
138149
const currentSize = 1 + 4 + 9 + 5 + 4 + 7;
139150
headers = fillHeaders(headers, currentSize, true);
140151

152+
console.log('writing', headers.length);
153+
141154
const server = http.createServer(common.mustCall((req, res) => {
142-
res.end('hello world');
143-
setImmediate(server.close.bind(server));
155+
res.end('hello from test3 server');
156+
server.close();
144157
}));
145158

159+
server.on('clientError', (err) => {
160+
console.log(err.code);
161+
if (err.code === 'HPE_HEADER_OVERFLOW') {
162+
console.log(err.rawPacket.toString('hex'));
163+
}
164+
});
165+
server.on('clientError', common.mustNotCall());
166+
146167
server.listen(0, common.mustCall(() => {
147168
const client = net.connect(server.address().port);
148169
client.on('connect', () => {
149170
writeHeaders(client, headers);
150171
client.resume();
151172
});
173+
174+
client.pipe(process.stdout);
152175
}));
153176
});
154177

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

0 commit comments

Comments
 (0)