Skip to content

Commit

Permalink
http: add headersTimeout timer and response logic
Browse files Browse the repository at this point in the history
  • Loading branch information
ShogunPanda committed Jan 28, 2022
1 parent 5a61ca6 commit 9b01e5f
Show file tree
Hide file tree
Showing 18 changed files with 327 additions and 287 deletions.
15 changes: 6 additions & 9 deletions doc/api/http.md
Expand Up @@ -1349,15 +1349,12 @@ added:
Limit the amount of time the parser will wait to receive the complete HTTP
headers.

In case of inactivity, the rules defined in [`server.timeout`][] apply. However,
that inactivity based timeout would still allow the connection to be kept open
if the headers are being sent very slowly (by default, up to a byte per 2
minutes). In order to prevent this, whenever header data arrives an additional
check is made that more than `server.headersTimeout` milliseconds has not
passed since the connection was established. If the check fails, a `'timeout'`
event is emitted on the server object, and (by default) the socket is destroyed.
See [`server.timeout`][] for more information on how timeout behavior can be
customized.
If the timeout expires, the server responds with status 408 without
forwarding the request to the request listener and then closes the connection.

It must be set to a non-zero value (e.g. 120 seconds) to protect against
potential Denial-of-Service attacks in case the server is deployed without a
reverse proxy in front.

### `server.listen()`

Expand Down
8 changes: 7 additions & 1 deletion lib/_http_common.js
Expand Up @@ -46,6 +46,7 @@ let debug = require('internal/util/debuglog').debuglog('http', (fn) => {

const kIncomingMessage = Symbol('IncomingMessage');
const kRequestTimeout = Symbol('RequestTimeout');
const kHeadersTimeout = Symbol('HeadersTimeout');
const kOnMessageBegin = HTTPParser.kOnMessageBegin | 0;
const kOnHeaders = HTTPParser.kOnHeaders | 0;
const kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0;
Expand Down Expand Up @@ -103,9 +104,13 @@ function parserOnHeadersComplete(versionMajor, versionMinor, headers, method,
incoming.upgrade = upgrade;

if (socket) {
debug('requestTimeout timer moved to req');
debug('headersTimeout and requestTimeout timer moved to req');

incoming[kRequestTimeout] = incoming.socket[kRequestTimeout];
incoming[kHeadersTimeout] = incoming.socket[kHeadersTimeout];

incoming.socket[kRequestTimeout] = undefined;
incoming.socket[kHeadersTimeout] = undefined;
}

let n = headers.length;
Expand Down Expand Up @@ -273,6 +278,7 @@ module.exports = {
methods,
parsers,
kIncomingMessage,
kHeadersTimeout,
kRequestTimeout,
HTTPParser,
isLenient,
Expand Down
38 changes: 32 additions & 6 deletions lib/_http_server.js
Expand Up @@ -41,6 +41,7 @@ const {
chunkExpression,
kIncomingMessage,
kRequestTimeout,
kHeadersTimeout,
HTTPParser,
isLenient,
_checkInvalidHeaderChar: checkInvalidHeaderChar,
Expand Down Expand Up @@ -534,13 +535,13 @@ function connectionListenerInternal(server, socket) {
// When receiving new requests on the same socket (pipelining or keep alive)
// make sure the requestTimeout is active.
parser[kOnMessageBegin] =
setRequestTimeout.bind(undefined,
server, socket);
setRequestTimeouts.bind(undefined,
server, socket);

// This protects from DOS attack where an attacker establish the connection
// without sending any data on applications where server.timeout is left to
// the default value of zero.
setRequestTimeout(server, socket);
setRequestTimeouts(server, socket);

socket._paused = false;
}
Expand Down Expand Up @@ -719,8 +720,10 @@ function onParserExecuteCommon(server, socket, parser, state, ret, d) {

socket.readableFlowing = null;

// Clear the requestTimeout after upgrading the connection.
// Clear the requestTimeout and headersTimeout
// timers after upgrading the connection.
clearRequestTimeout(req);
clearHeadersTimeout(req);

server.emit(eventName, req, socket, bodyHead);
} else {
Expand All @@ -734,7 +737,7 @@ function onParserExecuteCommon(server, socket, parser, state, ret, d) {
// When receiving new requests on the same socket (pipelining or keep alive)
// make sure the requestTimeout is active.
parser[kOnMessageBegin] =
setRequestTimeout.bind(undefined, server, socket);
setRequestTimeouts.bind(undefined, server, socket);
}

if (socket._paused && socket.parser) {
Expand All @@ -757,7 +760,7 @@ function clearIncoming(req) {
}
}

function setRequestTimeout(server, socket) {
function setRequestTimeouts(server, socket) {
// Set the request timeout handler.
if (
!socket[kRequestTimeout] &&
Expand All @@ -767,13 +770,25 @@ function setRequestTimeout(server, socket) {
socket[kRequestTimeout] =
setTimeout(onRequestTimeout, server.requestTimeout, socket).unref();
}

// Set the headers timeout handler.
if (
!socket[kHeadersTimeout] &&
server.headersTimeout && server.headersTimeout > 0
) {
debug('headersTimeout timer set');
socket[kHeadersTimeout] =
setTimeout(onRequestTimeout, server.headersTimeout, socket).unref();
}
}

function clearRequestTimeout(req) {
if (!req) {
req = this;
}

clearHeadersTimeout(req);

if (!req[kRequestTimeout]) {
return;
}
Expand All @@ -783,6 +798,16 @@ function clearRequestTimeout(req) {
req[kRequestTimeout] = undefined;
}

function clearHeadersTimeout(req) {
if (!req[kHeadersTimeout]) {
return;
}

debug('headersTimeout timer cleared');
clearTimeout(req[kHeadersTimeout]);
req[kHeadersTimeout] = undefined;
}

function resOnFinish(req, res, socket, state, server) {
if (onResponseFinishChannel.hasSubscribers) {
onResponseFinishChannel.publish({
Expand Down Expand Up @@ -850,6 +875,7 @@ function emitCloseNT(self) {
// new message. In this callback we setup the response object and pass it
// to the user.
function parserOnIncoming(server, socket, state, req, keepAlive) {
clearHeadersTimeout(req);
resetSocketTimeout(server, socket, state);

if (req.upgrade) {
Expand Down
34 changes: 2 additions & 32 deletions src/node_http_parser.cc
Expand Up @@ -211,7 +211,6 @@ class Parser : public AsyncWrap, public StreamListener {
num_fields_ = num_values_ = 0;
url_.Reset();
status_message_.Reset();
header_parsing_start_time_ = uv_hrtime();

Local<Value> cb = object()->Get(env()->context(), kOnMessageBegin)
.ToLocalChecked();
Expand Down Expand Up @@ -301,7 +300,6 @@ class Parser : public AsyncWrap, public StreamListener {

int on_headers_complete() {
header_nread_ = 0;
header_parsing_start_time_ = 0;

// Arguments for the on-headers-complete javascript callback. This
// list needs to be kept in sync with the actual argument list for
Expand Down Expand Up @@ -548,7 +546,6 @@ class Parser : public AsyncWrap, public StreamListener {
Environment* env = Environment::GetCurrent(args);

uint64_t max_http_header_size = 0;
uint64_t headers_timeout = 0;
uint32_t lenient_flags = kLenientNone;

CHECK(args[0]->IsInt32());
Expand All @@ -568,11 +565,6 @@ class Parser : public AsyncWrap, public StreamListener {
lenient_flags = args[3].As<Int32>()->Value();
}

if (args.Length() > 4) {
CHECK(args[4]->IsInt32());
headers_timeout = args[4].As<Int32>()->Value();
}

llhttp_type_t type =
static_cast<llhttp_type_t>(args[0].As<Int32>()->Value());

Expand All @@ -589,7 +581,7 @@ class Parser : public AsyncWrap, public StreamListener {

parser->set_provider_type(provider);
parser->AsyncReset(args[1].As<Object>());
parser->Init(type, max_http_header_size, lenient_flags, headers_timeout);
parser->Init(type, max_http_header_size, lenient_flags);
}

template <bool should_pause>
Expand Down Expand Up @@ -693,24 +685,6 @@ class Parser : public AsyncWrap, public StreamListener {
if (ret.IsEmpty())
return;

// check header parsing time
if (header_parsing_start_time_ != 0 && headers_timeout_ != 0) {
uint64_t now = uv_hrtime();
uint64_t parsing_time = (now - header_parsing_start_time_) / 1000000;

if (parsing_time > headers_timeout_) {
Local<Value> cb =
object()->Get(env()->context(), kOnTimeout).ToLocalChecked();

if (!cb->IsFunction())
return;

MakeCallback(cb.As<Function>(), 0, nullptr);

return;
}
}

Local<Value> cb =
object()->Get(env()->context(), kOnExecute).ToLocalChecked();

Expand Down Expand Up @@ -856,7 +830,7 @@ class Parser : public AsyncWrap, public StreamListener {


void Init(llhttp_type_t type, uint64_t max_http_header_size,
uint32_t lenient_flags, uint64_t headers_timeout) {
uint32_t lenient_flags) {
llhttp_init(&parser_, type, &settings);

if (lenient_flags & kLenientHeaders) {
Expand All @@ -877,8 +851,6 @@ class Parser : public AsyncWrap, public StreamListener {
have_flushed_ = false;
got_exception_ = false;
max_http_header_size_ = max_http_header_size;
header_parsing_start_time_ = 0;
headers_timeout_ = headers_timeout;
}


Expand Down Expand Up @@ -929,8 +901,6 @@ class Parser : public AsyncWrap, public StreamListener {
bool pending_pause_ = false;
uint64_t header_nread_ = 0;
uint64_t max_http_header_size_;
uint64_t headers_timeout_;
uint64_t header_parsing_start_time_ = 0;

BaseObjectPtr<BindingData> binding_data_;

Expand Down
2 changes: 1 addition & 1 deletion test/async-hooks/test-graph.http.js
Expand Up @@ -44,7 +44,7 @@ process.on('exit', () => {
triggerAsyncId: 'tcp:2' },
{ type: 'Timeout',
id: 'timeout:1',
triggerAsyncId: 'httpincomingmessage:1' },
triggerAsyncId: 'tcp:2' },
{ type: 'SHUTDOWNWRAP',
id: 'shutdown:1',
triggerAsyncId: 'tcp:2' } ]
Expand Down
62 changes: 62 additions & 0 deletions test/parallel/test-http-server-headers-timeout-delayed-headers.js
@@ -0,0 +1,62 @@
'use strict';

const common = require('../common');
const assert = require('assert');
const { createServer } = require('http');
const { connect } = require('net');

// This test validates that the server returns 408
// after server.headersTimeout if the client
// pauses before start sending the request.

let sendDelayedRequestHeaders;
const server = createServer(common.mustNotCall());
server.on('connection', common.mustCall(() => {
assert.strictEqual(typeof sendDelayedRequestHeaders, 'function');
sendDelayedRequestHeaders();
}));

// 60 seconds is the default
assert.strictEqual(server.headersTimeout, 60000);
const headersTimeout = common.platformTimeout(1000);
server.headersTimeout = headersTimeout;
assert.strictEqual(server.headersTimeout, headersTimeout);

// Make sure requestTimeout is big enough for the headersTimeout.
server.requestTimeout = 0;

// Check that timeout event is not triggered
server.once('timeout', common.mustNotCall((socket) => {
socket.destroy();
}));

server.listen(0, common.mustCall(() => {
const client = connect(server.address().port);
let response = '';

client.on('data', common.mustCall((chunk) => {
response += chunk.toString('utf-8');
}));

const errOrEnd = common.mustSucceed(function(err) {
assert.strictEqual(
response,
'HTTP/1.1 408 Request Timeout\r\nConnection: close\r\n\r\n'
);
server.close();
});

client.on('end', errOrEnd);
client.on('error', errOrEnd);

client.resume();

sendDelayedRequestHeaders = common.mustCall(() => {
setTimeout(() => {
client.write('POST / HTTP/1.1\r\n');
client.write('Content-Length: 20\r\n');
client.write('Connection: close\r\n\r\n');
client.write('12345678901234567890\r\n\r\n');
}, common.platformTimeout(headersTimeout * 2)).unref();
});
}));
@@ -0,0 +1,62 @@
'use strict';

const common = require('../common');
const assert = require('assert');
const { createServer } = require('http');
const { connect } = require('net');

// This test validates that the server returns 408
// after server.headersTimeout if the client
// pauses sending in the middle of a header.

let sendDelayedRequestHeaders;
const server = createServer(common.mustNotCall());
server.on('connection', common.mustCall(() => {
assert.strictEqual(typeof sendDelayedRequestHeaders, 'function');
sendDelayedRequestHeaders();
}));

// 60 seconds is the default
assert.strictEqual(server.headersTimeout, 60000);
const headersTimeout = common.platformTimeout(1000);
server.headersTimeout = headersTimeout;
assert.strictEqual(server.headersTimeout, headersTimeout);

// Make sure requestTimeout is big enough for the headersTimeout.
server.requestTimeout = 0;

// Check that timeout event is not triggered
server.once('timeout', common.mustNotCall((socket) => {
socket.destroy();
}));

server.listen(0, common.mustCall(() => {
const client = connect(server.address().port);
let response = '';

client.on('data', common.mustCall((chunk) => {
response += chunk.toString('utf-8');
}));

const errOrEnd = common.mustSucceed(function(err) {
assert.strictEqual(
response,
'HTTP/1.1 408 Request Timeout\r\nConnection: close\r\n\r\n'
);
server.close();
});

client.on('end', errOrEnd);
client.on('error', errOrEnd);

client.resume();
client.write('GET / HTTP/1.1\r\n');
client.write('Connection: close\r\n');
client.write('X-CRASH: ');

sendDelayedRequestHeaders = common.mustCall(() => {
setTimeout(() => {
client.write('1234567890\r\n\r\n');
}, common.platformTimeout(headersTimeout * 2)).unref();
});
}));

0 comments on commit 9b01e5f

Please sign in to comment.