Skip to content

Commit

Permalink
cli: add --max-http-header-size flag
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
2 people authored and MylesBorins committed Dec 25, 2018
1 parent a57aed1 commit 9b2ffc8
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 25 deletions.
8 changes: 8 additions & 0 deletions doc/api/cli.md
Expand Up @@ -181,6 +181,13 @@ added: v9.0.0


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


### `--max-http-header-size=size`
<!-- YAML
added: REPLACEME
-->

Specify the maximum size, in bytes, of HTTP headers. Defaults to 8KB.

### `--napi-modules` ### `--napi-modules`
<!-- YAML <!-- YAML
added: v7.10.0 added: v7.10.0
Expand Down Expand Up @@ -584,6 +591,7 @@ Node.js options that are allowed are:
- `--inspect-brk` - `--inspect-brk`
- `--inspect-port` - `--inspect-port`
- `--loader` - `--loader`
- `--max-http-header-size`
- `--napi-modules` - `--napi-modules`
- `--no-deprecation` - `--no-deprecation`
- `--no-force-async-hooks-checks` - `--no-force-async-hooks-checks`
Expand Down
3 changes: 3 additions & 0 deletions doc/node.1
Expand Up @@ -133,6 +133,9 @@ Specify the
as a custom loader, to load as a custom loader, to load
.Fl -experimental-modules . .Fl -experimental-modules .
. .
.It Fl -max-http-header-size Ns = Ns Ar size
Specify the maximum size of HTTP headers in bytes. Defaults to 8KB.
.
.It Fl -napi-modules .It Fl -napi-modules
This option is a no-op. This option is a no-op.
It is kept for compatibility. It is kept for compatibility.
Expand Down
7 changes: 7 additions & 0 deletions src/node_http_parser.cc
Expand Up @@ -738,6 +738,10 @@ const struct http_parser_settings Parser::settings = {
nullptr // on_chunk_complete nullptr // on_chunk_complete
}; };


void InitMaxHttpHeaderSizeOnce() {
const uint32_t max_http_header_size = per_process_opts->max_http_header_size;
http_parser_set_max_header_size(max_http_header_size);
}


void Initialize(Local<Object> target, void Initialize(Local<Object> target,
Local<Value> unused, Local<Value> unused,
Expand Down Expand Up @@ -784,6 +788,9 @@ void Initialize(Local<Object> target,


target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "HTTPParser"), target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "HTTPParser"),
t->GetFunction(env->context()).ToLocalChecked()); t->GetFunction(env->context()).ToLocalChecked());

static uv_once_t init_once = UV_ONCE_INIT;
uv_once(&init_once, InitMaxHttpHeaderSizeOnce);
} }


} // anonymous namespace } // anonymous namespace
Expand Down
4 changes: 4 additions & 0 deletions src/node_options.cc
Expand Up @@ -236,6 +236,10 @@ PerProcessOptionsParser::PerProcessOptionsParser() {
kAllowedInEnvironment); kAllowedInEnvironment);
AddAlias("--trace-events-enabled", { AddAlias("--trace-events-enabled", {
"--trace-event-categories", "v8,node,node.async_hooks" }); "--trace-event-categories", "v8,node,node.async_hooks" });
AddOption("--max-http-header-size",
"set the maximum size of HTTP headers (default: 8KB)",
&PerProcessOptions::max_http_header_size,
kAllowedInEnvironment);
AddOption("--v8-pool-size", AddOption("--v8-pool-size",
"set V8's thread pool size", "set V8's thread pool size",
&PerProcessOptions::v8_thread_pool_size, &PerProcessOptions::v8_thread_pool_size,
Expand Down
1 change: 1 addition & 0 deletions src/node_options.h
Expand Up @@ -116,6 +116,7 @@ class PerProcessOptions : public Options {
std::string title; std::string title;
std::string trace_event_categories; std::string trace_event_categories;
std::string trace_event_file_pattern = "node_trace.${rotation}.log"; std::string trace_event_file_pattern = "node_trace.${rotation}.log";
uint64_t max_http_header_size = 8 * 1024;
int64_t v8_thread_pool_size = 4; int64_t v8_thread_pool_size = 4;
bool zero_fill_all_buffers = false; bool zero_fill_all_buffers = false;


Expand Down
72 changes: 47 additions & 25 deletions test/sequential/test-http-max-http-headers.js
@@ -1,10 +1,15 @@
// Flags: --expose-internals
'use strict'; 'use strict';

const assert = require('assert'); const assert = require('assert');
const common = require('../common'); const common = require('../common');
const http = require('http'); const http = require('http');
const net = require('net'); const net = require('net');
const MAX = 8 * 1024; // 8KB const MAX = +(process.argv[2] || 8 * 1024); // Command line option, or 8KB.

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

console.log('pid is', process.pid);
console.log('max header size is', getOptionValue('--max-http-header-size'));


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


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


const timeout = common.platformTimeout(10);

function writeHeaders(socket, headers) { function writeHeaders(socket, headers) {
const array = []; const array = [];

const chunkSize = 100;
// this is off from 1024 so that \r\n does not get split
const chunkSize = 1000;
let last = 0; let last = 0;


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


function next() { function next() {
if (socket.write(array.shift())) { if (socket.destroyed) {
if (array.length === 0) { console.log('socket was destroyed early, data left to write:',
socket.end(); array.join('').length);
} else { return;
setTimeout(next, timeout); }
}
const chunk = array.shift();

if (chunk) {
console.log('writing chunk of size', chunk.length);
socket.write(chunk, next);
} else { } else {
socket.once('drain', next); socket.end();
} }
} }
} }


function test1() { function test1() {
console.log('test1');
let headers = let headers =
'HTTP/1.1 200 OK\r\n' + 'HTTP/1.1 200 OK\r\n' +
'Content-Length: 0\r\n' + 'Content-Length: 0\r\n' +
Expand All @@ -82,6 +89,9 @@ function test1() {
writeHeaders(sock, headers); writeHeaders(sock, headers);
sock.resume(); sock.resume();
}); });

// The socket might error but that's ok
sock.on('error', () => {});
}); });


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


client.on('error', common.mustCall((err) => { client.on('error', common.mustCall((err) => {
assert.strictEqual(err.code, 'HPE_HEADER_OVERFLOW'); assert.strictEqual(err.code, 'HPE_HEADER_OVERFLOW');
server.close(); server.close(test2);
setImmediate(test2);
})); }));
})); }));
} }


const test2 = common.mustCall(() => { const test2 = common.mustCall(() => {
console.log('test2');
let headers = let headers =
'GET / HTTP/1.1\r\n' + 'GET / HTTP/1.1\r\n' +
'Host: localhost\r\n' + 'Host: localhost\r\n' +
'Agent: node\r\n' + 'Agent: nod2\r\n' +
'X-CRASH: '; 'X-CRASH: ';


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


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


server.on('clientError', common.mustCall((err) => { server.once('clientError', common.mustCall((err) => {
assert.strictEqual(err.code, 'HPE_HEADER_OVERFLOW'); assert.strictEqual(err.code, 'HPE_HEADER_OVERFLOW');
})); }));


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


finished(client, common.mustCall((err) => { finished(client, common.mustCall((err) => {
server.close(); server.close(test3);
setImmediate(test3);
})); }));
})); }));
}); });


const test3 = common.mustCall(() => { const test3 = common.mustCall(() => {
console.log('test3');
let headers = let headers =
'GET / HTTP/1.1\r\n' + 'GET / HTTP/1.1\r\n' +
'Host: localhost\r\n' + 'Host: localhost\r\n' +
'Agent: node\r\n' + 'Agent: nod3\r\n' +
'X-CRASH: '; 'X-CRASH: ';


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


console.log('writing', headers.length);

const server = http.createServer(common.mustCall((req, res) => { const server = http.createServer(common.mustCall((req, res) => {
res.end('hello world'); res.end('hello from test3 server');
setImmediate(server.close.bind(server)); server.close();
})); }));


server.on('clientError', (err) => {
console.log(err.code);
if (err.code === 'HPE_HEADER_OVERFLOW') {
console.log(err.rawPacket.toString('hex'));
}
});
server.on('clientError', common.mustNotCall());

server.listen(0, common.mustCall(() => { server.listen(0, common.mustCall(() => {
const client = net.connect(server.address().port); const client = net.connect(server.address().port);
client.on('connect', () => { client.on('connect', () => {
writeHeaders(client, headers); writeHeaders(client, headers);
client.resume(); client.resume();
}); });

client.pipe(process.stdout);
})); }));
}); });


Expand Down
104 changes: 104 additions & 0 deletions test/sequential/test-set-http-max-http-headers.js
@@ -0,0 +1,104 @@
'use strict';

const common = require('../common');
const assert = require('assert');
const { spawn } = require('child_process');
const path = require('path');
const testName = path.join(__dirname, 'test-http-max-http-headers.js');

const timeout = common.platformTimeout(100);

const tests = [];

function test(fn) {
tests.push(fn);
}

test(function(cb) {
console.log('running subtest expecting failure');

// Validate that the test fails if the max header size is too small.
const args = ['--expose-internals',
'--max-http-header-size=1024',
testName];
const cp = spawn(process.execPath, args, { stdio: 'inherit' });

cp.on('close', common.mustCall((code, signal) => {
assert.strictEqual(code, 1);
assert.strictEqual(signal, null);
cb();
}));
});

test(function(cb) {
console.log('running subtest expecting success');

const env = Object.assign({}, process.env, {
NODE_DEBUG: 'http'
});

// Validate that the test fails if the max header size is too small.
// Validate that the test now passes if the same limit becomes large enough.
const args = ['--expose-internals',
'--max-http-header-size=1024',
testName,
'1024'];
const cp = spawn(process.execPath, args, {
env,
stdio: 'inherit'
});

cp.on('close', common.mustCall((code, signal) => {
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
cb();
}));
});

// Next, repeat the same checks using NODE_OPTIONS if it is supported.
if (process.config.variables.node_without_node_options) {
const env = Object.assign({}, process.env, {
NODE_OPTIONS: '--max-http-header-size=1024'
});

test(function(cb) {
console.log('running subtest expecting failure');

// Validate that the test fails if the max header size is too small.
const args = ['--expose-internals', testName];
const cp = spawn(process.execPath, args, { env, stdio: 'inherit' });

cp.on('close', common.mustCall((code, signal) => {
assert.strictEqual(code, 1);
assert.strictEqual(signal, null);
cb();
}));
});

test(function(cb) {
// Validate that the test now passes if the same limit
// becomes large enough.
const args = ['--expose-internals', testName, '1024'];
const cp = spawn(process.execPath, args, { env, stdio: 'inherit' });

cp.on('close', common.mustCall((code, signal) => {
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
cb();
}));
});
}

function runTest() {
const fn = tests.shift();

if (!fn) {
return;
}

fn(() => {
setTimeout(runTest, timeout);
});
}

runTest();

0 comments on commit 9b2ffc8

Please sign in to comment.