Skip to content

Commit b149eef

Browse files
OrKoNmcollina
authored andcommitted
http: fix incorrect headersTimeout measurement
For keep-alive connections, the headersTimeout may fire during subsequent request because the measurement was reset after a request and not before a request. PR-URL: #32329 Fixes: #27363 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 8b9e877 commit b149eef

6 files changed

+104
-38
lines changed

lib/_http_client.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -723,7 +723,8 @@ function tickOnSocket(req, socket) {
723723
new HTTPClientAsyncResource('HTTPINCOMINGMESSAGE', req),
724724
req.maxHeaderSize || 0,
725725
req.insecureHTTPParser === undefined ?
726-
isLenient() : req.insecureHTTPParser);
726+
isLenient() : req.insecureHTTPParser,
727+
0);
727728
parser.socket = socket;
728729
parser.outgoing = req;
729730
req.parser = parser;

lib/_http_common.js

+2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ const kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0;
4747
const kOnBody = HTTPParser.kOnBody | 0;
4848
const kOnMessageComplete = HTTPParser.kOnMessageComplete | 0;
4949
const kOnExecute = HTTPParser.kOnExecute | 0;
50+
const kOnTimeout = HTTPParser.kOnTimeout | 0;
5051

5152
const MAX_HEADER_PAIRS = 2000;
5253

@@ -165,6 +166,7 @@ const parsers = new FreeList('parsers', 1000, function parsersCb() {
165166
parser[kOnHeadersComplete] = parserOnHeadersComplete;
166167
parser[kOnBody] = parserOnBody;
167168
parser[kOnMessageComplete] = parserOnMessageComplete;
169+
parser[kOnTimeout] = null;
168170

169171
return parser;
170172
});

lib/_http_server.js

+11-34
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ const { OutgoingMessage } = require('_http_outgoing');
4949
const {
5050
kOutHeaders,
5151
kNeedDrain,
52-
nowDate,
5352
emitStatistics
5453
} = require('internal/http');
5554
const {
@@ -142,6 +141,7 @@ const STATUS_CODES = {
142141
};
143142

144143
const kOnExecute = HTTPParser.kOnExecute | 0;
144+
const kOnTimeout = HTTPParser.kOnTimeout | 0;
145145

146146
class HTTPServerAsyncResource {
147147
constructor(type, socket) {
@@ -426,11 +426,9 @@ function connectionListenerInternal(server, socket) {
426426
server.maxHeaderSize || 0,
427427
server.insecureHTTPParser === undefined ?
428428
isLenient() : server.insecureHTTPParser,
429+
server.headersTimeout || 0,
429430
);
430431
parser.socket = socket;
431-
432-
// We are starting to wait for our headers.
433-
parser.parsingHeadersStart = nowDate();
434432
socket.parser = parser;
435433

436434
// Propagate headers limit from server instance to parser
@@ -482,6 +480,9 @@ function connectionListenerInternal(server, socket) {
482480
parser[kOnExecute] =
483481
onParserExecute.bind(undefined, server, socket, parser, state);
484482

483+
parser[kOnTimeout] =
484+
onParserTimeout.bind(undefined, server, socket);
485+
485486
socket._paused = false;
486487
}
487488

@@ -570,21 +571,15 @@ function socketOnData(server, socket, parser, state, d) {
570571

571572
function onParserExecute(server, socket, parser, state, ret) {
572573
socket._unrefTimer();
573-
const start = parser.parsingHeadersStart;
574574
debug('SERVER socketOnParserExecute %d', ret);
575+
onParserExecuteCommon(server, socket, parser, state, ret, undefined);
576+
}
575577

576-
// If we have not parsed the headers, destroy the socket
577-
// after server.headersTimeout to protect from DoS attacks.
578-
// start === 0 means that we have parsed headers.
579-
if (start !== 0 && nowDate() - start > server.headersTimeout) {
580-
const serverTimeout = server.emit('timeout', socket);
581-
582-
if (!serverTimeout)
583-
socket.destroy();
584-
return;
585-
}
578+
function onParserTimeout(server, socket) {
579+
const serverTimeout = server.emit('timeout', socket);
586580

587-
onParserExecuteCommon(server, socket, parser, state, ret, undefined);
581+
if (!serverTimeout)
582+
socket.destroy();
588583
}
589584

590585
const noop = () => {};
@@ -722,13 +717,6 @@ function emitCloseNT(self) {
722717
function parserOnIncoming(server, socket, state, req, keepAlive) {
723718
resetSocketTimeout(server, socket, state);
724719

725-
if (server.keepAliveTimeout > 0) {
726-
req.on('end', resetHeadersTimeoutOnReqEnd);
727-
}
728-
729-
// Set to zero to communicate that we have finished parsing.
730-
socket.parser.parsingHeadersStart = 0;
731-
732720
if (req.upgrade) {
733721
req.upgrade = req.method === 'CONNECT' ||
734722
server.listenerCount('upgrade') > 0;
@@ -853,17 +841,6 @@ function generateSocketListenerWrapper(originalFnName) {
853841
};
854842
}
855843

856-
function resetHeadersTimeoutOnReqEnd() {
857-
debug('resetHeadersTimeoutOnReqEnd');
858-
859-
const parser = this.socket.parser;
860-
// Parser can be null if the socket was destroyed
861-
// in that case, there is nothing to do.
862-
if (parser) {
863-
parser.parsingHeadersStart = nowDate();
864-
}
865-
}
866-
867844
module.exports = {
868845
STATUS_CODES,
869846
Server,

src/node_http_parser.cc

+35-2
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ const uint32_t kOnHeadersComplete = 1;
7474
const uint32_t kOnBody = 2;
7575
const uint32_t kOnMessageComplete = 3;
7676
const uint32_t kOnExecute = 4;
77+
const uint32_t kOnTimeout = 5;
7778
// Any more fields than this will be flushed into JS
7879
const size_t kMaxHeaderFieldsCount = 32;
7980

@@ -181,6 +182,7 @@ class Parser : public AsyncWrap, public StreamListener {
181182
num_fields_ = num_values_ = 0;
182183
url_.Reset();
183184
status_message_.Reset();
185+
header_parsing_start_time_ = uv_hrtime();
184186
return 0;
185187
}
186188

@@ -504,6 +506,7 @@ class Parser : public AsyncWrap, public StreamListener {
504506
bool lenient = args[3]->IsTrue();
505507

506508
uint64_t max_http_header_size = 0;
509+
uint64_t headers_timeout = 0;
507510

508511
CHECK(args[0]->IsInt32());
509512
CHECK(args[1]->IsObject());
@@ -516,6 +519,11 @@ class Parser : public AsyncWrap, public StreamListener {
516519
max_http_header_size = env->options()->max_http_header_size;
517520
}
518521

522+
if (args.Length() > 4) {
523+
CHECK(args[4]->IsInt32());
524+
headers_timeout = args[4].As<Number>()->Value();
525+
}
526+
519527
llhttp_type_t type =
520528
static_cast<llhttp_type_t>(args[0].As<Int32>()->Value());
521529

@@ -532,7 +540,7 @@ class Parser : public AsyncWrap, public StreamListener {
532540

533541
parser->set_provider_type(provider);
534542
parser->AsyncReset(args[1].As<Object>());
535-
parser->Init(type, max_http_header_size, lenient);
543+
parser->Init(type, max_http_header_size, lenient, headers_timeout);
536544
}
537545

538546
template <bool should_pause>
@@ -636,6 +644,24 @@ class Parser : public AsyncWrap, public StreamListener {
636644
if (ret.IsEmpty())
637645
return;
638646

647+
// check header parsing time
648+
if (header_parsing_start_time_ != 0 && headers_timeout_ != 0) {
649+
uint64_t now = uv_hrtime();
650+
uint64_t parsing_time = (now - header_parsing_start_time_) / 1e6;
651+
652+
if (parsing_time > headers_timeout_) {
653+
Local<Value> cb =
654+
object()->Get(env()->context(), kOnTimeout).ToLocalChecked();
655+
656+
if (!cb->IsFunction())
657+
return;
658+
659+
MakeCallback(cb.As<Function>(), 0, nullptr);
660+
661+
return;
662+
}
663+
}
664+
639665
Local<Value> cb =
640666
object()->Get(env()->context(), kOnExecute).ToLocalChecked();
641667

@@ -779,7 +805,8 @@ class Parser : public AsyncWrap, public StreamListener {
779805
}
780806

781807

782-
void Init(llhttp_type_t type, uint64_t max_http_header_size, bool lenient) {
808+
void Init(llhttp_type_t type, uint64_t max_http_header_size,
809+
bool lenient, uint64_t headers_timeout) {
783810
llhttp_init(&parser_, type, &settings);
784811
llhttp_set_lenient(&parser_, lenient);
785812
header_nread_ = 0;
@@ -790,6 +817,8 @@ class Parser : public AsyncWrap, public StreamListener {
790817
have_flushed_ = false;
791818
got_exception_ = false;
792819
max_http_header_size_ = max_http_header_size;
820+
header_parsing_start_time_ = 0;
821+
headers_timeout_ = headers_timeout;
793822
}
794823

795824

@@ -831,6 +860,8 @@ class Parser : public AsyncWrap, public StreamListener {
831860
bool pending_pause_ = false;
832861
uint64_t header_nread_ = 0;
833862
uint64_t max_http_header_size_;
863+
uint64_t headers_timeout_;
864+
uint64_t header_parsing_start_time_ = 0;
834865

835866
// These are helper functions for filling `http_parser_settings`, which turn
836867
// a member function of Parser into a C-style HTTP parser callback.
@@ -890,6 +921,8 @@ void InitializeHttpParser(Local<Object> target,
890921
Integer::NewFromUnsigned(env->isolate(), kOnMessageComplete));
891922
t->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "kOnExecute"),
892923
Integer::NewFromUnsigned(env->isolate(), kOnExecute));
924+
t->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "kOnTimeout"),
925+
Integer::NewFromUnsigned(env->isolate(), kOnTimeout));
893926

894927
Local<Array> methods = Array::New(env->isolate());
895928
#define V(num, name, string) \

test/async-hooks/test-graph.http.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,12 @@ process.on('exit', () => {
3939
id: 'httpclientrequest:1',
4040
triggerAsyncId: 'tcpserver:1' },
4141
{ type: 'TCPWRAP', id: 'tcp:2', triggerAsyncId: 'tcpserver:1' },
42-
{ type: 'Timeout', id: 'timeout:1', triggerAsyncId: 'tcp:2' },
4342
{ type: 'HTTPINCOMINGMESSAGE',
4443
id: 'httpincomingmessage:1',
4544
triggerAsyncId: 'tcp:2' },
45+
{ type: 'Timeout',
46+
id: 'timeout:1',
47+
triggerAsyncId: 'httpincomingmessage:1' },
4648
{ type: 'SHUTDOWNWRAP',
4749
id: 'shutdown:1',
4850
triggerAsyncId: 'tcp:2' } ]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const http = require('http');
5+
const net = require('net');
6+
const { finished } = require('stream');
7+
8+
const headers =
9+
'GET / HTTP/1.1\r\n' +
10+
'Host: localhost\r\n' +
11+
'Connection: keep-alive\r\n' +
12+
'Agent: node\r\n';
13+
14+
const baseTimeout = 1000;
15+
16+
const server = http.createServer(common.mustCall((req, res) => {
17+
req.resume();
18+
res.writeHead(200);
19+
res.end();
20+
}, 2));
21+
22+
server.keepAliveTimeout = 10 * baseTimeout;
23+
server.headersTimeout = baseTimeout;
24+
25+
server.once('timeout', common.mustNotCall((socket) => {
26+
socket.destroy();
27+
}));
28+
29+
server.listen(0, () => {
30+
const client = net.connect(server.address().port);
31+
32+
// first request
33+
client.write(headers);
34+
client.write('\r\n');
35+
36+
setTimeout(() => {
37+
// second request
38+
client.write(headers);
39+
// `headersTimeout` doesn't seem to fire if request
40+
// is sent altogether.
41+
setTimeout(() => {
42+
client.write('\r\n');
43+
client.end();
44+
}, 10);
45+
}, baseTimeout + 10);
46+
47+
client.resume();
48+
finished(client, common.mustCall((err) => {
49+
server.close();
50+
}));
51+
});

0 commit comments

Comments
 (0)