Skip to content
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

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/_http_common.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0;
const kOnBody = HTTPParser.kOnBody | 0;
const kOnMessageComplete = HTTPParser.kOnMessageComplete | 0;
const kOnExecute = HTTPParser.kOnExecute | 0;
const kOnMessageBegin = HTTPParser.kOnMessageBegin | 0;

const MAX_HEADER_PAIRS = 2000;

Expand Down Expand Up @@ -165,6 +166,7 @@ const parsers = new FreeList('parsers', 1000, function parsersCb() {
parser[kOnHeadersComplete] = parserOnHeadersComplete;
parser[kOnBody] = parserOnBody;
parser[kOnMessageComplete] = parserOnMessageComplete;
parser[kOnMessageBegin] = null;
Copy link
Contributor Author

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.


return parser;
});
Expand Down
27 changes: 9 additions & 18 deletions lib/_http_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ const STATUS_CODES = {
};

const kOnExecute = HTTPParser.kOnExecute | 0;
const kOnMessageBegin = HTTPParser.kOnMessageBegin | 0;

class HTTPServerAsyncResource {
constructor(type, socket) {
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So when the parsing starts we capture when it started.

Copy link
Contributor

Choose a reason for hiding this comment

The 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 Date.now() for these things instead of the HR timer? What if a big NTP-sync shift happens during intervals like this? It could have a profound impact on how things run.

}

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.
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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,
Expand Down
19 changes: 19 additions & 0 deletions src/node_http_parser.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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())
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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;
}

Expand Down Expand Up @@ -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) \
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use strict';
Copy link
Contributor Author

@OrKoN OrKoN Oct 30, 2019

Choose a reason for hiding this comment

The 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();
}));
});