Skip to content

Commit 9b2ffc8

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 <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 a57aed1 commit 9b2ffc8

7 files changed

+174
-25
lines changed

doc/api/cli.md

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

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

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

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

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

+47-25
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
// Flags: --expose-internals
12
'use strict';
2-
33
const assert = require('assert');
44
const common = require('../common');
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.
8+
9+
const { getOptionValue } = require('internal/options');
10+
11+
console.log('pid is', process.pid);
12+
console.log('max header size is', getOptionValue('--max-http-header-size'));
813

914
// Verify that we cannot receive more than 8KB of headers.
1015

@@ -28,19 +33,15 @@ function fillHeaders(headers, currentSize, valid = false) {
2833
headers += 'a'.repeat(MAX - headers.length - 3);
2934
// Generate valid headers
3035
if (valid) {
31-
// TODO(mcollina): understand why -9 is needed instead of -1
32-
headers = headers.slice(0, -9);
36+
// TODO(mcollina): understand why -32 is needed instead of -1
37+
headers = headers.slice(0, -32);
3338
}
3439
return headers + '\r\n\r\n';
3540
}
3641

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

4647
for (let i = 0; i < headers.length / chunkSize; i++) {
@@ -55,19 +56,25 @@ function writeHeaders(socket, headers) {
5556
next();
5657

5758
function next() {
58-
if (socket.write(array.shift())) {
59-
if (array.length === 0) {
60-
socket.end();
61-
} else {
62-
setTimeout(next, timeout);
63-
}
59+
if (socket.destroyed) {
60+
console.log('socket was destroyed early, data left to write:',
61+
array.join('').length);
62+
return;
63+
}
64+
65+
const chunk = array.shift();
66+
67+
if (chunk) {
68+
console.log('writing chunk of size', chunk.length);
69+
socket.write(chunk, next);
6470
} else {
65-
socket.once('drain', next);
71+
socket.end();
6672
}
6773
}
6874
}
6975

7076
function test1() {
77+
console.log('test1');
7178
let headers =
7279
'HTTP/1.1 200 OK\r\n' +
7380
'Content-Length: 0\r\n' +
@@ -82,6 +89,9 @@ function test1() {
8289
writeHeaders(sock, headers);
8390
sock.resume();
8491
});
92+
93+
// The socket might error but that's ok
94+
sock.on('error', () => {});
8595
});
8696

8797
server.listen(0, common.mustCall(() => {
@@ -90,17 +100,17 @@ function test1() {
90100

91101
client.on('error', common.mustCall((err) => {
92102
assert.strictEqual(err.code, 'HPE_HEADER_OVERFLOW');
93-
server.close();
94-
setImmediate(test2);
103+
server.close(test2);
95104
}));
96105
}));
97106
}
98107

99108
const test2 = common.mustCall(() => {
109+
console.log('test2');
100110
let headers =
101111
'GET / HTTP/1.1\r\n' +
102112
'Host: localhost\r\n' +
103-
'Agent: node\r\n' +
113+
'Agent: nod2\r\n' +
104114
'X-CRASH: ';
105115

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

110120
const server = http.createServer(common.mustNotCall());
111121

112-
server.on('clientError', common.mustCall((err) => {
122+
server.once('clientError', common.mustCall((err) => {
113123
assert.strictEqual(err.code, 'HPE_HEADER_OVERFLOW');
114124
}));
115125

@@ -121,34 +131,46 @@ const test2 = common.mustCall(() => {
121131
});
122132

123133
finished(client, common.mustCall((err) => {
124-
server.close();
125-
setImmediate(test3);
134+
server.close(test3);
126135
}));
127136
}));
128137
});
129138

130139
const test3 = common.mustCall(() => {
140+
console.log('test3');
131141
let headers =
132142
'GET / HTTP/1.1\r\n' +
133143
'Host: localhost\r\n' +
134-
'Agent: node\r\n' +
144+
'Agent: nod3\r\n' +
135145
'X-CRASH: ';
136146

137147
// /, Host, localhost, Agent, node, X-CRASH, a...
138148
const currentSize = 1 + 4 + 9 + 5 + 4 + 7;
139149
headers = fillHeaders(headers, currentSize, true);
140150

151+
console.log('writing', headers.length);
152+
141153
const server = http.createServer(common.mustCall((req, res) => {
142-
res.end('hello world');
143-
setImmediate(server.close.bind(server));
154+
res.end('hello from test3 server');
155+
server.close();
144156
}));
145157

158+
server.on('clientError', (err) => {
159+
console.log(err.code);
160+
if (err.code === 'HPE_HEADER_OVERFLOW') {
161+
console.log(err.rawPacket.toString('hex'));
162+
}
163+
});
164+
server.on('clientError', common.mustNotCall());
165+
146166
server.listen(0, common.mustCall(() => {
147167
const client = net.connect(server.address().port);
148168
client.on('connect', () => {
149169
writeHeaders(client, headers);
150170
client.resume();
151171
});
172+
173+
client.pipe(process.stdout);
152174
}));
153175
});
154176

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)