Skip to content

Commit 2f192c4

Browse files
ShogunPandaRafaelGSS
authored andcommitted
http: added connection closing methods
Fixes: #41578 PR-URL: #42812 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
1 parent 3ab3086 commit 2f192c4

9 files changed

+360
-13
lines changed

doc/api/http.md

+17
Original file line numberDiff line numberDiff line change
@@ -1453,6 +1453,23 @@ added: v0.1.90
14531453

14541454
Stops the server from accepting new connections. See [`net.Server.close()`][].
14551455

1456+
### `server.closeAllConnections()`
1457+
1458+
<!-- YAML
1459+
added: REPLACEME
1460+
-->
1461+
1462+
Closes all connections connected to this server.
1463+
1464+
### `server.closeIdleConnections()`
1465+
1466+
<!-- YAML
1467+
added: REPLACEME
1468+
-->
1469+
1470+
Closes all connections connected to this server which are not sending a request
1471+
or waiting for a response.
1472+
14561473
### `server.headersTimeout`
14571474

14581475
<!-- YAML

doc/api/https.md

+20-2
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,23 @@ added: v0.1.90
133133
* `callback` {Function}
134134
* Returns: {https.Server}
135135

136-
See [`server.close()`][`http.close()`] from the HTTP module for details.
136+
See [`http.Server.close()`][].
137+
138+
### `server.closeAllConnections()`
139+
140+
<!-- YAML
141+
added: REPLACEME
142+
-->
143+
144+
See [`http.Server.closeAllConnections()`][].
145+
146+
### `server.closeIdleConnections()`
147+
148+
<!-- YAML
149+
added: REPLACEME
150+
-->
151+
152+
See [`http.Server.closeIdleConnections()`][].
137153

138154
### `server.headersTimeout`
139155

@@ -529,8 +545,10 @@ headers: max-age=0; pin-sha256="WoiWRyIOVNa9ihaBciRSC7XHjliYS9VwUGOIud4PB18="; p
529545
[`http.Server#requestTimeout`]: http.md#serverrequesttimeout
530546
[`http.Server#setTimeout()`]: http.md#serversettimeoutmsecs-callback
531547
[`http.Server#timeout`]: http.md#servertimeout
548+
[`http.Server.close()`]: http.md#serverclosecallback
549+
[`http.Server.closeAllConnections()`]: http.md#servercloseallconnections
550+
[`http.Server.closeIdleConnections()`]: http.md#servercloseidleconnections
532551
[`http.Server`]: http.md#class-httpserver
533-
[`http.close()`]: http.md#serverclosecallback
534552
[`http.createServer()`]: http.md#httpcreateserveroptions-requestlistener
535553
[`http.get()`]: http.md#httpgetoptions-callback
536554
[`http.request()`]: http.md#httprequestoptions-callback

lib/_http_server.js

+25-4
Original file line numberDiff line numberDiff line change
@@ -409,10 +409,11 @@ function storeHTTPOptions(options) {
409409
function setupConnectionsTracking(server) {
410410
// Start connection handling
411411
server[kConnections] = new ConnectionsList();
412-
if (server.headersTimeout > 0 || server.requestTimeout > 0) {
413-
server[kConnectionsCheckingInterval] =
414-
setInterval(checkConnections.bind(server), server.connectionsCheckingInterval).unref();
415-
}
412+
413+
// This checker is started without checking whether any headersTimeout or requestTimeout is non zero
414+
// otherwise it would not be started if such timeouts are modified after createServer.
415+
server[kConnectionsCheckingInterval] =
416+
setInterval(checkConnections.bind(server), server.connectionsCheckingInterval).unref();
416417
}
417418

418419
function Server(options, requestListener) {
@@ -458,6 +459,22 @@ Server.prototype.close = function() {
458459
ReflectApply(net.Server.prototype.close, this, arguments);
459460
};
460461

462+
Server.prototype.closeAllConnections = function() {
463+
const connections = this[kConnections].all();
464+
465+
for (let i = 0, l = connections.length; i < l; i++) {
466+
connections[i].socket.destroy();
467+
}
468+
};
469+
470+
Server.prototype.closeIdleConnections = function() {
471+
const connections = this[kConnections].idle();
472+
473+
for (let i = 0, l = connections.length; i < l; i++) {
474+
connections[i].socket.destroy();
475+
}
476+
};
477+
461478
Server.prototype.setTimeout = function setTimeout(msecs, callback) {
462479
this.timeout = msecs;
463480
if (callback)
@@ -489,6 +506,10 @@ Server.prototype[EE.captureRejectionSymbol] = function(err, event, ...args) {
489506
};
490507

491508
function checkConnections() {
509+
if (this.headersTimeout === 0 && this.requestTimeout === 0) {
510+
return;
511+
}
512+
492513
const expired = this[kConnections].expired(this.headersTimeout, this.requestTimeout);
493514

494515
for (let i = 0; i < expired.length; i++) {

lib/https.js

+4
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ function Server(opts, requestListener) {
8787
ObjectSetPrototypeOf(Server.prototype, tls.Server.prototype);
8888
ObjectSetPrototypeOf(Server, tls.Server);
8989

90+
Server.prototype.closeAllConnections = HttpServer.prototype.closeAllConnections;
91+
92+
Server.prototype.closeIdleConnections = HttpServer.prototype.closeIdleConnections;
93+
9094
Server.prototype.setTimeout = HttpServer.prototype.setTimeout;
9195

9296
/**

src/node_http_parser.cc

+20-7
Original file line numberDiff line numberDiff line change
@@ -257,9 +257,10 @@ class Parser : public AsyncWrap, public StreamListener {
257257
SET_SELF_SIZE(Parser)
258258

259259
int on_message_begin() {
260-
// Important: Pop from the list BEFORE resetting the last_message_start_
260+
// Important: Pop from the lists BEFORE resetting the last_message_start_
261261
// otherwise std::set.erase will fail.
262262
if (connectionsList_ != nullptr) {
263+
connectionsList_->Pop(this);
263264
connectionsList_->PopActive(this);
264265
}
265266

@@ -270,6 +271,7 @@ class Parser : public AsyncWrap, public StreamListener {
270271
status_message_.Reset();
271272

272273
if (connectionsList_ != nullptr) {
274+
connectionsList_->Push(this);
273275
connectionsList_->PushActive(this);
274276
}
275277

@@ -492,14 +494,19 @@ class Parser : public AsyncWrap, public StreamListener {
492494
int on_message_complete() {
493495
HandleScope scope(env()->isolate());
494496

495-
// Important: Pop from the list BEFORE resetting the last_message_start_
497+
// Important: Pop from the lists BEFORE resetting the last_message_start_
496498
// otherwise std::set.erase will fail.
497499
if (connectionsList_ != nullptr) {
500+
connectionsList_->Pop(this);
498501
connectionsList_->PopActive(this);
499502
}
500503

501504
last_message_start_ = 0;
502505

506+
if (connectionsList_ != nullptr) {
507+
connectionsList_->Push(this);
508+
}
509+
503510
if (num_fields_)
504511
Flush(); // Flush trailing HTTP headers.
505512

@@ -666,12 +673,14 @@ class Parser : public AsyncWrap, public StreamListener {
666673
if (connectionsList != nullptr) {
667674
parser->connectionsList_ = connectionsList;
668675

669-
parser->connectionsList_->Push(parser);
670-
671676
// This protects from a DoS attack where an attacker establishes
672677
// the connection without sending any data on applications where
673678
// server.timeout is left to the default value of zero.
674679
parser->last_message_start_ = uv_hrtime();
680+
681+
// Important: Push into the lists AFTER setting the last_message_start_
682+
// otherwise std::set.erase will fail later.
683+
parser->connectionsList_->Push(parser);
675684
parser->connectionsList_->PushActive(parser);
676685
} else {
677686
parser->connectionsList_ = nullptr;
@@ -1044,10 +1053,14 @@ class Parser : public AsyncWrap, public StreamListener {
10441053
};
10451054

10461055
bool ParserComparator::operator()(const Parser* lhs, const Parser* rhs) const {
1047-
if (lhs->last_message_start_ == 0) {
1048-
return false;
1049-
} else if (rhs->last_message_start_ == 0) {
1056+
if (lhs->last_message_start_ == 0 && rhs->last_message_start_ == 0) {
1057+
// When both parsers are idle, guarantee strict order by
1058+
// comparing pointers as ints.
1059+
return lhs < rhs;
1060+
} else if (lhs->last_message_start_ == 0) {
10501061
return true;
1062+
} else if (rhs->last_message_start_ == 0) {
1063+
return false;
10511064
}
10521065

10531066
return lhs->last_message_start_ < rhs->last_message_start_;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
5+
const { createServer } = require('http');
6+
const { connect } = require('net');
7+
8+
let connections = 0;
9+
10+
const server = createServer(common.mustCall(function(req, res) {
11+
res.writeHead(200, { Connection: 'keep-alive' });
12+
res.end();
13+
}), {
14+
headersTimeout: 0,
15+
keepAliveTimeout: 0,
16+
requestTimeout: common.platformTimeout(60000),
17+
});
18+
19+
server.on('connection', function() {
20+
connections++;
21+
});
22+
23+
server.listen(0, function() {
24+
const port = server.address().port;
25+
26+
// Create a first request but never finish it
27+
const client1 = connect(port);
28+
29+
client1.on('close', common.mustCall());
30+
31+
client1.on('error', () => {});
32+
33+
client1.write('GET / HTTP/1.1');
34+
35+
// Create a second request, let it finish but leave the connection opened using HTTP keep-alive
36+
const client2 = connect(port);
37+
let response = '';
38+
39+
client2.on('data', common.mustCall((chunk) => {
40+
response += chunk.toString('utf-8');
41+
42+
if (response.endsWith('0\r\n\r\n')) {
43+
assert(response.startsWith('HTTP/1.1 200 OK\r\nConnection: keep-alive'));
44+
assert.strictEqual(connections, 2);
45+
46+
server.closeAllConnections();
47+
server.close(common.mustCall());
48+
49+
// This timer should never go off as the server.close should shut everything down
50+
setTimeout(common.mustNotCall(), common.platformTimeout(1500)).unref();
51+
}
52+
}));
53+
54+
client2.on('close', common.mustCall());
55+
56+
client2.write('GET / HTTP/1.1\r\n\r\n');
57+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
5+
const { createServer } = require('http');
6+
const { connect } = require('net');
7+
8+
let connections = 0;
9+
10+
const server = createServer(common.mustCall(function(req, res) {
11+
res.writeHead(200, { Connection: 'keep-alive' });
12+
res.end();
13+
}), {
14+
headersTimeout: 0,
15+
keepAliveTimeout: 0,
16+
requestTimeout: common.platformTimeout(60000),
17+
});
18+
19+
server.on('connection', function() {
20+
connections++;
21+
});
22+
23+
server.listen(0, function() {
24+
const port = server.address().port;
25+
let client1Closed = false;
26+
let client2Closed = false;
27+
28+
// Create a first request but never finish it
29+
const client1 = connect(port);
30+
31+
client1.on('close', common.mustCall(() => {
32+
client1Closed = true;
33+
}));
34+
35+
client1.on('error', () => {});
36+
37+
client1.write('GET / HTTP/1.1');
38+
39+
// Create a second request, let it finish but leave the connection opened using HTTP keep-alive
40+
const client2 = connect(port);
41+
let response = '';
42+
43+
client2.on('data', common.mustCall((chunk) => {
44+
response += chunk.toString('utf-8');
45+
46+
if (response.endsWith('0\r\n\r\n')) {
47+
assert(response.startsWith('HTTP/1.1 200 OK\r\nConnection: keep-alive'));
48+
assert.strictEqual(connections, 2);
49+
50+
server.closeIdleConnections();
51+
server.close(common.mustCall());
52+
53+
// Check that only the idle connection got closed
54+
setTimeout(common.mustCall(() => {
55+
assert(!client1Closed);
56+
assert(client2Closed);
57+
58+
server.closeAllConnections();
59+
server.close(common.mustCall());
60+
}), common.platformTimeout(500)).unref();
61+
}
62+
}));
63+
64+
client2.on('close', common.mustCall(() => {
65+
client2Closed = true;
66+
}));
67+
68+
client2.write('GET / HTTP/1.1\r\n\r\n');
69+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
5+
if (!common.hasCrypto) {
6+
common.skip('missing crypto');
7+
}
8+
9+
const { createServer } = require('https');
10+
const { connect } = require('tls');
11+
12+
const fixtures = require('../common/fixtures');
13+
14+
const options = {
15+
key: fixtures.readKey('agent1-key.pem'),
16+
cert: fixtures.readKey('agent1-cert.pem')
17+
};
18+
19+
let connections = 0;
20+
21+
const server = createServer(options, common.mustCall(function(req, res) {
22+
res.writeHead(200, { Connection: 'keep-alive' });
23+
res.end();
24+
}), {
25+
headersTimeout: 0,
26+
keepAliveTimeout: 0,
27+
requestTimeout: common.platformTimeout(60000),
28+
});
29+
30+
server.on('connection', function() {
31+
connections++;
32+
});
33+
34+
server.listen(0, function() {
35+
const port = server.address().port;
36+
37+
// Create a first request but never finish it
38+
const client1 = connect({ port, rejectUnauthorized: false });
39+
40+
client1.on('close', common.mustCall());
41+
42+
client1.on('error', () => {});
43+
44+
client1.write('GET / HTTP/1.1');
45+
46+
// Create a second request, let it finish but leave the connection opened using HTTP keep-alive
47+
const client2 = connect({ port, rejectUnauthorized: false });
48+
let response = '';
49+
50+
client2.on('data', common.mustCall((chunk) => {
51+
response += chunk.toString('utf-8');
52+
53+
if (response.endsWith('0\r\n\r\n')) {
54+
assert(response.startsWith('HTTP/1.1 200 OK\r\nConnection: keep-alive'));
55+
assert.strictEqual(connections, 2);
56+
57+
server.closeAllConnections();
58+
server.close(common.mustCall());
59+
60+
// This timer should never go off as the server.close should shut everything down
61+
setTimeout(common.mustNotCall(), common.platformTimeout(1500)).unref();
62+
}
63+
}));
64+
65+
client2.on('close', common.mustCall());
66+
67+
client2.write('GET / HTTP/1.1\r\n\r\n');
68+
});

0 commit comments

Comments
 (0)