Skip to content

Commit f7281ca

Browse files
committed
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. Fixes: nodejs#27363
1 parent 40b559a commit f7281ca

File tree

4 files changed

+81
-19
lines changed

4 files changed

+81
-19
lines changed

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 kOnMessageBegin = HTTPParser.kOnMessageBegin | 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[kOnMessageBegin] = null;
168170

169171
return parser;
170172
});

lib/_http_server.js

+9-19
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ const STATUS_CODES = {
142142
};
143143

144144
const kOnExecute = HTTPParser.kOnExecute | 0;
145+
const kOnMessageBegin = HTTPParser.kOnMessageBegin | 0;
145146

146147
class HTTPServerAsyncResource {
147148
constructor(type, socket) {
@@ -428,9 +429,6 @@ function connectionListenerInternal(server, socket) {
428429
isLenient() : server.insecureHTTPParser,
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
@@ -481,6 +479,7 @@ function connectionListenerInternal(server, socket) {
481479
}
482480
parser[kOnExecute] =
483481
onParserExecute.bind(undefined, server, socket, parser, state);
482+
parser[kOnMessageBegin] = onParserMessageBegin.bind(undefined, parser);
484483

485484
socket._paused = false;
486485
}
@@ -568,11 +567,17 @@ function socketOnData(server, socket, parser, state, d) {
568567
onParserExecuteCommon(server, socket, parser, state, ret, d);
569568
}
570569

570+
function onParserMessageBegin(parser) {
571+
// We are starting to wait for the headers.
572+
parser.parsingHeadersStart = nowDate();
573+
}
574+
571575
function onParserExecute(server, socket, parser, state, ret) {
572576
socket._unrefTimer();
573-
const start = parser.parsingHeadersStart;
577+
574578
debug('SERVER socketOnParserExecute %d', ret);
575579

580+
const start = parser.parsingHeadersStart;
576581
// If we have not parsed the headers, destroy the socket
577582
// after server.headersTimeout to protect from DoS attacks.
578583
// start === 0 means that we have parsed headers.
@@ -720,10 +725,6 @@ function emitCloseNT(self) {
720725
function parserOnIncoming(server, socket, state, req, keepAlive) {
721726
resetSocketTimeout(server, socket, state);
722727

723-
if (server.keepAliveTimeout > 0) {
724-
req.on('end', resetHeadersTimeoutOnReqEnd);
725-
}
726-
727728
// Set to zero to communicate that we have finished parsing.
728729
socket.parser.parsingHeadersStart = 0;
729730

@@ -851,17 +852,6 @@ function generateSocketListenerWrapper(originalFnName) {
851852
};
852853
}
853854

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

src/node_http_parser.cc

+20
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 kOnMessageBegin = 5;
7778
// Any more fields than this will be flushed into JS
7879
const size_t kMaxHeaderFieldsCount = 32;
7980

@@ -181,6 +182,23 @@ class Parser : public AsyncWrap, public StreamListener {
181182
num_fields_ = num_values_ = 0;
182183
url_.Reset();
183184
status_message_.Reset();
185+
186+
Local<Object> obj = object();
187+
Local<Value> cb = obj->Get(env()->context(),
188+
kOnMessageBegin).ToLocalChecked();
189+
190+
if (!cb->IsFunction())
191+
return 0;
192+
193+
Local<Value> argv[0];
194+
MaybeLocal<Value> r = MakeCallback(cb.As<Function>(), 0, argv);
195+
196+
if (r.IsEmpty()) {
197+
got_exception_ = true;
198+
llhttp_set_error_reason(&parser_, "HPE_JS_EXCEPTION:JS Exception");
199+
return HPE_USER;
200+
}
201+
184202
return 0;
185203
}
186204

@@ -890,6 +908,8 @@ void InitializeHttpParser(Local<Object> target,
890908
Integer::NewFromUnsigned(env->isolate(), kOnMessageComplete));
891909
t->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "kOnExecute"),
892910
Integer::NewFromUnsigned(env->isolate(), kOnExecute));
911+
t->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "kOnMessageBegin"),
912+
Integer::NewFromUnsigned(env->isolate(), kOnMessageBegin));
893913

894914
Local<Array> methods = Array::New(env->isolate());
895915
#define V(num, name, string) \
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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 headers
40+
// are sent in one packet.
41+
setTimeout(() => {
42+
client.write('\r\n');
43+
client.end();
44+
}, 10);
45+
}, baseTimeout + 10);
46+
47+
finished(client, common.mustCall((err) => {
48+
server.close();
49+
}));
50+
});

0 commit comments

Comments
 (0)