-
Notifications
You must be signed in to change notification settings - Fork 29.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
http: fix incorrect headersTimeout measurement #30184
Changes from all commits
f7281ca
8cfd1b8
aa83993
845d4bb
01c2228
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -142,6 +142,7 @@ const STATUS_CODES = { | |
}; | ||
|
||
const kOnExecute = HTTPParser.kOnExecute | 0; | ||
const kOnMessageBegin = HTTPParser.kOnMessageBegin | 0; | ||
|
||
class HTTPServerAsyncResource { | ||
constructor(type, socket) { | ||
|
@@ -428,8 +429,6 @@ function connectionListenerInternal(server, socket) { | |
isLenient() : server.insecureHTTPParser, | ||
); | ||
parser.socket = socket; | ||
|
||
// We are starting to wait for our headers. | ||
parser.parsingHeadersStart = nowDate(); | ||
socket.parser = parser; | ||
|
||
|
@@ -481,6 +480,7 @@ function connectionListenerInternal(server, socket) { | |
} | ||
parser[kOnExecute] = | ||
onParserExecute.bind(undefined, server, socket, parser, state); | ||
parser[kOnMessageBegin] = onParserMessageBegin.bind(undefined, parser); | ||
|
||
socket._paused = false; | ||
} | ||
|
@@ -568,11 +568,17 @@ function socketOnData(server, socket, parser, state, d) { | |
onParserExecuteCommon(server, socket, parser, state, ret, d); | ||
} | ||
|
||
function onParserMessageBegin(parser) { | ||
// We are starting to wait for the headers. | ||
parser.parsingHeadersStart = nowDate(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So when the parsing starts we capture when it started. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess it's out of scope for this PR, but why are we depending on |
||
} | ||
|
||
function onParserExecute(server, socket, parser, state, ret) { | ||
socket._unrefTimer(); | ||
const start = parser.parsingHeadersStart; | ||
|
||
debug('SERVER socketOnParserExecute %d', ret); | ||
|
||
const start = parser.parsingHeadersStart; | ||
// If we have not parsed the headers, destroy the socket | ||
// after server.headersTimeout to protect from DoS attacks. | ||
// start === 0 means that we have parsed headers. | ||
|
@@ -720,10 +726,6 @@ function emitCloseNT(self) { | |
function parserOnIncoming(server, socket, state, req, keepAlive) { | ||
resetSocketTimeout(server, socket, state); | ||
|
||
if (server.keepAliveTimeout > 0) { | ||
req.on('end', resetHeadersTimeoutOnReqEnd); | ||
} | ||
|
||
// Set to zero to communicate that we have finished parsing. | ||
socket.parser.parsingHeadersStart = 0; | ||
|
||
|
@@ -851,17 +853,6 @@ function generateSocketListenerWrapper(originalFnName) { | |
}; | ||
} | ||
|
||
function resetHeadersTimeoutOnReqEnd() { | ||
debug('resetHeadersTimeoutOnReqEnd'); | ||
|
||
const parser = this.socket.parser; | ||
// Parser can be null if the socket was destroyed | ||
// in that case, there is nothing to do. | ||
if (parser) { | ||
parser.parsingHeadersStart = nowDate(); | ||
} | ||
} | ||
|
||
module.exports = { | ||
STATUS_CODES, | ||
Server, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -74,6 +74,7 @@ const uint32_t kOnHeadersComplete = 1; | |
const uint32_t kOnBody = 2; | ||
const uint32_t kOnMessageComplete = 3; | ||
const uint32_t kOnExecute = 4; | ||
const uint32_t kOnMessageBegin = 5; | ||
// Any more fields than this will be flushed into JS | ||
const size_t kMaxHeaderFieldsCount = 32; | ||
|
||
|
@@ -181,6 +182,22 @@ class Parser : public AsyncWrap, public StreamListener { | |
num_fields_ = num_values_ = 0; | ||
url_.Reset(); | ||
status_message_.Reset(); | ||
|
||
Local<Object> obj = object(); | ||
Local<Value> cb = obj->Get(env()->context(), | ||
kOnMessageBegin).ToLocalChecked(); | ||
|
||
if (!cb->IsFunction()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. will it affect performance? Not sure if there is a better way. An extra callback will be invoked for every request |
||
return 0; | ||
|
||
MaybeLocal<Value> r = MakeCallback(cb.As<Function>(), 0, nullptr); | ||
|
||
if (r.IsEmpty()) { | ||
got_exception_ = true; | ||
llhttp_set_error_reason(&parser_, "HPE_JS_EXCEPTION:JS Exception"); | ||
return HPE_USER; | ||
} | ||
|
||
return 0; | ||
} | ||
|
||
|
@@ -890,6 +907,8 @@ void InitializeHttpParser(Local<Object> target, | |
Integer::NewFromUnsigned(env->isolate(), kOnMessageComplete)); | ||
t->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "kOnExecute"), | ||
Integer::NewFromUnsigned(env->isolate(), kOnExecute)); | ||
t->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "kOnMessageBegin"), | ||
Integer::NewFromUnsigned(env->isolate(), kOnMessageBegin)); | ||
|
||
Local<Array> methods = Array::New(env->isolate()); | ||
#define V(num, name, string) \ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
'use strict'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this test fails on master with a timeout, and it's based on the test cases mentioned in #27363 This PR attempts to fix the problem. |
||
|
||
const common = require('../common'); | ||
const http = require('http'); | ||
const net = require('net'); | ||
const { finished } = require('stream'); | ||
|
||
const headers = | ||
'GET / HTTP/1.1\r\n' + | ||
'Host: localhost\r\n' + | ||
'Connection: keep-alive\r\n' + | ||
'Agent: node\r\n'; | ||
|
||
const baseTimeout = 1000; | ||
|
||
const server = http.createServer(common.mustCall((req, res) => { | ||
req.resume(); | ||
res.writeHead(200); | ||
res.end(); | ||
}, 2)); | ||
|
||
server.keepAliveTimeout = 10 * baseTimeout; | ||
server.headersTimeout = baseTimeout; | ||
|
||
server.once('timeout', common.mustNotCall((socket) => { | ||
socket.destroy(); | ||
})); | ||
|
||
server.listen(0, () => { | ||
const client = net.connect(server.address().port); | ||
|
||
// first request | ||
client.write(headers); | ||
client.write('\r\n'); | ||
|
||
setTimeout(() => { | ||
// second request | ||
client.write(headers); | ||
// `headersTimeout` doesn't seem to fire if request headers | ||
// are sent in one packet. | ||
setTimeout(() => { | ||
client.write('\r\n'); | ||
client.end(); | ||
}, 10); | ||
}, baseTimeout + 10); | ||
|
||
finished(client, common.mustCall((err) => { | ||
server.close(); | ||
})); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
my understanding was that onMessageBegin will be called when the parser starts parsing a new request's headers.