diff --git a/doc/api/http.md b/doc/api/http.md index ca845b394b859a..c0ccc2b5a68445 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -1455,6 +1455,23 @@ added: v0.1.90 Stops the server from accepting new connections. See [`net.Server.close()`][]. +### `server.closeAllConnections()` + + + +Closes all connections connected to this server. + +### `server.closeIdleConnections()` + + + +Closes all connections connected to this server which are not sending a request +or waiting for a response. + ### `server.headersTimeout` + +See [`http.Server.closeAllConnections()`][]. + +### `server.closeIdleConnections()` + + + +See [`http.Server.closeIdleConnections()`][]. ### `server.headersTimeout` @@ -529,8 +545,10 @@ headers: max-age=0; pin-sha256="WoiWRyIOVNa9ihaBciRSC7XHjliYS9VwUGOIud4PB18="; p [`http.Server#requestTimeout`]: http.md#serverrequesttimeout [`http.Server#setTimeout()`]: http.md#serversettimeoutmsecs-callback [`http.Server#timeout`]: http.md#servertimeout +[`http.Server.close()`]: http.md#serverclosecallback +[`http.Server.closeAllConnections()`]: http.md#servercloseallconnections +[`http.Server.closeIdleConnections()`]: http.md#servercloseidleconnections [`http.Server`]: http.md#class-httpserver -[`http.close()`]: http.md#serverclosecallback [`http.createServer()`]: http.md#httpcreateserveroptions-requestlistener [`http.get()`]: http.md#httpgetoptions-callback [`http.request()`]: http.md#httprequestoptions-callback diff --git a/lib/_http_server.js b/lib/_http_server.js index 84be32c78c4075..c8dc22929bfabd 100644 --- a/lib/_http_server.js +++ b/lib/_http_server.js @@ -409,10 +409,11 @@ function storeHTTPOptions(options) { function setupConnectionsTracking(server) { // Start connection handling server[kConnections] = new ConnectionsList(); - if (server.headersTimeout > 0 || server.requestTimeout > 0) { - server[kConnectionsCheckingInterval] = - setInterval(checkConnections.bind(server), server.connectionsCheckingInterval).unref(); - } + + // This checker is started without checking whether any headersTimeout or requestTimeout is non zero + // otherwise it would not be started if such timeouts are modified after createServer. + server[kConnectionsCheckingInterval] = + setInterval(checkConnections.bind(server), server.connectionsCheckingInterval).unref(); } function Server(options, requestListener) { @@ -458,6 +459,22 @@ Server.prototype.close = function() { ReflectApply(net.Server.prototype.close, this, arguments); }; +Server.prototype.closeAllConnections = function() { + const connections = this[kConnections].all(); + + for (let i = 0, l = connections.length; i < l; i++) { + connections[i].socket.destroy(); + } +}; + +Server.prototype.closeIdleConnections = function() { + const connections = this[kConnections].idle(); + + for (let i = 0, l = connections.length; i < l; i++) { + connections[i].socket.destroy(); + } +}; + Server.prototype.setTimeout = function setTimeout(msecs, callback) { this.timeout = msecs; if (callback) @@ -489,6 +506,10 @@ Server.prototype[EE.captureRejectionSymbol] = function(err, event, ...args) { }; function checkConnections() { + if (this.headersTimeout === 0 && this.requestTimeout === 0) { + return; + } + const expired = this[kConnections].expired(this.headersTimeout, this.requestTimeout); for (let i = 0; i < expired.length; i++) { diff --git a/lib/https.js b/lib/https.js index 74ce93baaafe35..6ca3f335d0f885 100644 --- a/lib/https.js +++ b/lib/https.js @@ -87,6 +87,10 @@ function Server(opts, requestListener) { ObjectSetPrototypeOf(Server.prototype, tls.Server.prototype); ObjectSetPrototypeOf(Server, tls.Server); +Server.prototype.closeAllConnections = HttpServer.prototype.closeAllConnections; + +Server.prototype.closeIdleConnections = HttpServer.prototype.closeIdleConnections; + Server.prototype.setTimeout = HttpServer.prototype.setTimeout; /** diff --git a/src/node_http_parser.cc b/src/node_http_parser.cc index c1255b8cbd3b13..7ff5b0fe2ff76f 100644 --- a/src/node_http_parser.cc +++ b/src/node_http_parser.cc @@ -257,9 +257,10 @@ class Parser : public AsyncWrap, public StreamListener { SET_SELF_SIZE(Parser) int on_message_begin() { - // Important: Pop from the list BEFORE resetting the last_message_start_ + // Important: Pop from the lists BEFORE resetting the last_message_start_ // otherwise std::set.erase will fail. if (connectionsList_ != nullptr) { + connectionsList_->Pop(this); connectionsList_->PopActive(this); } @@ -270,6 +271,7 @@ class Parser : public AsyncWrap, public StreamListener { status_message_.Reset(); if (connectionsList_ != nullptr) { + connectionsList_->Push(this); connectionsList_->PushActive(this); } @@ -492,14 +494,19 @@ class Parser : public AsyncWrap, public StreamListener { int on_message_complete() { HandleScope scope(env()->isolate()); - // Important: Pop from the list BEFORE resetting the last_message_start_ + // Important: Pop from the lists BEFORE resetting the last_message_start_ // otherwise std::set.erase will fail. if (connectionsList_ != nullptr) { + connectionsList_->Pop(this); connectionsList_->PopActive(this); } last_message_start_ = 0; + if (connectionsList_ != nullptr) { + connectionsList_->Push(this); + } + if (num_fields_) Flush(); // Flush trailing HTTP headers. @@ -666,12 +673,14 @@ class Parser : public AsyncWrap, public StreamListener { if (connectionsList != nullptr) { parser->connectionsList_ = connectionsList; - parser->connectionsList_->Push(parser); - // This protects from a DoS attack where an attacker establishes // the connection without sending any data on applications where // server.timeout is left to the default value of zero. parser->last_message_start_ = uv_hrtime(); + + // Important: Push into the lists AFTER setting the last_message_start_ + // otherwise std::set.erase will fail later. + parser->connectionsList_->Push(parser); parser->connectionsList_->PushActive(parser); } else { parser->connectionsList_ = nullptr; @@ -1044,10 +1053,14 @@ class Parser : public AsyncWrap, public StreamListener { }; bool ParserComparator::operator()(const Parser* lhs, const Parser* rhs) const { - if (lhs->last_message_start_ == 0) { - return false; - } else if (rhs->last_message_start_ == 0) { + if (lhs->last_message_start_ == 0 && rhs->last_message_start_ == 0) { + // When both parsers are idle, guarantee strict order by + // comparing pointers as ints. + return lhs < rhs; + } else if (lhs->last_message_start_ == 0) { return true; + } else if (rhs->last_message_start_ == 0) { + return false; } return lhs->last_message_start_ < rhs->last_message_start_; diff --git a/test/parallel/test-http-server-close-all.js b/test/parallel/test-http-server-close-all.js new file mode 100644 index 00000000000000..79fc5c75b70daf --- /dev/null +++ b/test/parallel/test-http-server-close-all.js @@ -0,0 +1,57 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const { createServer } = require('http'); +const { connect } = require('net'); + +let connections = 0; + +const server = createServer(common.mustCall(function(req, res) { + res.writeHead(200, { Connection: 'keep-alive' }); + res.end(); +}), { + headersTimeout: 0, + keepAliveTimeout: 0, + requestTimeout: common.platformTimeout(60000), +}); + +server.on('connection', function() { + connections++; +}); + +server.listen(0, function() { + const port = server.address().port; + + // Create a first request but never finish it + const client1 = connect(port); + + client1.on('close', common.mustCall()); + + client1.on('error', () => {}); + + client1.write('GET / HTTP/1.1'); + + // Create a second request, let it finish but leave the connection opened using HTTP keep-alive + const client2 = connect(port); + let response = ''; + + client2.on('data', common.mustCall((chunk) => { + response += chunk.toString('utf-8'); + + if (response.endsWith('0\r\n\r\n')) { + assert(response.startsWith('HTTP/1.1 200 OK\r\nConnection: keep-alive')); + assert.strictEqual(connections, 2); + + server.closeAllConnections(); + server.close(common.mustCall()); + + // This timer should never go off as the server.close should shut everything down + setTimeout(common.mustNotCall(), common.platformTimeout(1500)).unref(); + } + })); + + client2.on('close', common.mustCall()); + + client2.write('GET / HTTP/1.1\r\n\r\n'); +}); diff --git a/test/parallel/test-http-server-close-idle.js b/test/parallel/test-http-server-close-idle.js new file mode 100644 index 00000000000000..b9389f1e599c72 --- /dev/null +++ b/test/parallel/test-http-server-close-idle.js @@ -0,0 +1,69 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const { createServer } = require('http'); +const { connect } = require('net'); + +let connections = 0; + +const server = createServer(common.mustCall(function(req, res) { + res.writeHead(200, { Connection: 'keep-alive' }); + res.end(); +}), { + headersTimeout: 0, + keepAliveTimeout: 0, + requestTimeout: common.platformTimeout(60000), +}); + +server.on('connection', function() { + connections++; +}); + +server.listen(0, function() { + const port = server.address().port; + let client1Closed = false; + let client2Closed = false; + + // Create a first request but never finish it + const client1 = connect(port); + + client1.on('close', common.mustCall(() => { + client1Closed = true; + })); + + client1.on('error', () => {}); + + client1.write('GET / HTTP/1.1'); + + // Create a second request, let it finish but leave the connection opened using HTTP keep-alive + const client2 = connect(port); + let response = ''; + + client2.on('data', common.mustCall((chunk) => { + response += chunk.toString('utf-8'); + + if (response.endsWith('0\r\n\r\n')) { + assert(response.startsWith('HTTP/1.1 200 OK\r\nConnection: keep-alive')); + assert.strictEqual(connections, 2); + + server.closeIdleConnections(); + server.close(common.mustCall()); + + // Check that only the idle connection got closed + setTimeout(common.mustCall(() => { + assert(!client1Closed); + assert(client2Closed); + + server.closeAllConnections(); + server.close(common.mustCall()); + }), common.platformTimeout(500)).unref(); + } + })); + + client2.on('close', common.mustCall(() => { + client2Closed = true; + })); + + client2.write('GET / HTTP/1.1\r\n\r\n'); +}); diff --git a/test/parallel/test-https-server-close-all.js b/test/parallel/test-https-server-close-all.js new file mode 100644 index 00000000000000..408af625d1f773 --- /dev/null +++ b/test/parallel/test-https-server-close-all.js @@ -0,0 +1,68 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +if (!common.hasCrypto) { + common.skip('missing crypto'); +} + +const { createServer } = require('https'); +const { connect } = require('tls'); + +const fixtures = require('../common/fixtures'); + +const options = { + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem') +}; + +let connections = 0; + +const server = createServer(options, common.mustCall(function(req, res) { + res.writeHead(200, { Connection: 'keep-alive' }); + res.end(); +}), { + headersTimeout: 0, + keepAliveTimeout: 0, + requestTimeout: common.platformTimeout(60000), +}); + +server.on('connection', function() { + connections++; +}); + +server.listen(0, function() { + const port = server.address().port; + + // Create a first request but never finish it + const client1 = connect({ port, rejectUnauthorized: false }); + + client1.on('close', common.mustCall()); + + client1.on('error', () => {}); + + client1.write('GET / HTTP/1.1'); + + // Create a second request, let it finish but leave the connection opened using HTTP keep-alive + const client2 = connect({ port, rejectUnauthorized: false }); + let response = ''; + + client2.on('data', common.mustCall((chunk) => { + response += chunk.toString('utf-8'); + + if (response.endsWith('0\r\n\r\n')) { + assert(response.startsWith('HTTP/1.1 200 OK\r\nConnection: keep-alive')); + assert.strictEqual(connections, 2); + + server.closeAllConnections(); + server.close(common.mustCall()); + + // This timer should never go off as the server.close should shut everything down + setTimeout(common.mustNotCall(), common.platformTimeout(1500)).unref(); + } + })); + + client2.on('close', common.mustCall()); + + client2.write('GET / HTTP/1.1\r\n\r\n'); +}); diff --git a/test/parallel/test-https-server-close-idle.js b/test/parallel/test-https-server-close-idle.js new file mode 100644 index 00000000000000..ea43c4367cb738 --- /dev/null +++ b/test/parallel/test-https-server-close-idle.js @@ -0,0 +1,80 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +if (!common.hasCrypto) { + common.skip('missing crypto'); +} + +const { createServer } = require('https'); +const { connect } = require('tls'); + +const fixtures = require('../common/fixtures'); + +const options = { + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem') +}; + +let connections = 0; + +const server = createServer(options, common.mustCall(function(req, res) { + res.writeHead(200, { Connection: 'keep-alive' }); + res.end(); +}), { + headersTimeout: 0, + keepAliveTimeout: 0, + requestTimeout: common.platformTimeout(60000), +}); + +server.on('connection', function() { + connections++; +}); + +server.listen(0, function() { + const port = server.address().port; + let client1Closed = false; + let client2Closed = false; + + // Create a first request but never finish it + const client1 = connect({ port, rejectUnauthorized: false }); + + client1.on('close', common.mustCall(() => { + client1Closed = true; + })); + + client1.on('error', () => {}); + + client1.write('GET / HTTP/1.1'); + + // Create a second request, let it finish but leave the connection opened using HTTP keep-alive + const client2 = connect({ port, rejectUnauthorized: false }); + let response = ''; + + client2.on('data', common.mustCall((chunk) => { + response += chunk.toString('utf-8'); + + if (response.endsWith('0\r\n\r\n')) { + assert(response.startsWith('HTTP/1.1 200 OK\r\nConnection: keep-alive')); + assert.strictEqual(connections, 2); + + server.closeIdleConnections(); + server.close(common.mustCall()); + + // Check that only the idle connection got closed + setTimeout(common.mustCall(() => { + assert(!client1Closed); + assert(client2Closed); + + server.closeAllConnections(); + server.close(common.mustCall()); + }), common.platformTimeout(500)).unref(); + } + })); + + client2.on('close', common.mustCall(() => { + client2Closed = true; + })); + + client2.write('GET / HTTP/1.1\r\n\r\n'); +});