Skip to content

Commit 4e8f11d

Browse files
fatal10110mcollina
authored andcommittedSep 19, 2021
http: limit requests per connection
Fixes: nodejs#40071 PR-URL: nodejs#40082 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Robert Nagy <[email protected]>
1 parent b0d5eec commit 4e8f11d

5 files changed

+258
-16
lines changed
 

‎doc/api/http.md

+16
Original file line numberDiff line numberDiff line change
@@ -1352,6 +1352,22 @@ By default, the Server does not timeout sockets. However, if a callback
13521352
is assigned to the Server's `'timeout'` event, timeouts must be handled
13531353
explicitly.
13541354

1355+
### `server.maxRequestsPerSocket`
1356+
<!-- YAML
1357+
added: REPLACEME
1358+
-->
1359+
1360+
* {number} Requests per socket. **Default:** null (no limit)
1361+
1362+
The maximum number of requests socket can handle
1363+
before closing keep alive connection.
1364+
1365+
A value of `null` will disable the limit.
1366+
1367+
When limit is reach it will set `Connection` header value to `closed`,
1368+
but will not actually close the connection, subsequent requests sent
1369+
after the limit is reached will get `503 Service Unavailable` as a response.
1370+
13551371
### `server.timeout`
13561372
<!-- YAML
13571373
added: v0.9.12

‎lib/_http_outgoing.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ function OutgoingMessage() {
113113
this._last = false;
114114
this.chunkedEncoding = false;
115115
this.shouldKeepAlive = true;
116+
this.maxRequestsOnConnectionReached = false;
116117
this._defaultKeepAlive = true;
117118
this.useChunkedEncodingByDefault = true;
118119
this.sendDate = false;
@@ -446,7 +447,9 @@ function _storeHeader(firstLine, headers) {
446447
} else if (!state.connection) {
447448
const shouldSendKeepAlive = this.shouldKeepAlive &&
448449
(state.contLen || this.useChunkedEncodingByDefault || this.agent);
449-
if (shouldSendKeepAlive) {
450+
if (shouldSendKeepAlive && this.maxRequestsOnConnectionReached) {
451+
header += 'Connection: close\r\n';
452+
} else if (shouldSendKeepAlive) {
450453
header += 'Connection: keep-alive\r\n';
451454
if (this._keepAliveTimeout && this._defaultKeepAlive) {
452455
const timeoutSeconds = MathFloor(this._keepAliveTimeout / 1000);

‎lib/_http_server.js

+36-15
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,7 @@ function Server(options, requestListener) {
394394
this.timeout = 0;
395395
this.keepAliveTimeout = 5000;
396396
this.maxHeadersCount = null;
397+
this.maxRequestsPerSocket = null;
397398
this.headersTimeout = 60 * 1000; // 60 seconds
398399
this.requestTimeout = 0;
399400
}
@@ -485,6 +486,7 @@ function connectionListenerInternal(server, socket) {
485486
// need to pause TCP socket/HTTP parser, and wait until the data will be
486487
// sent to the client.
487488
outgoingData: 0,
489+
requestsCount: 0,
488490
keepAliveTimeoutSet: false
489491
};
490492
state.onData = socketOnData.bind(undefined,
@@ -903,28 +905,47 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {
903905
resOnFinish.bind(undefined,
904906
req, res, socket, state, server));
905907

906-
if (req.headers.expect !== undefined &&
907-
(req.httpVersionMajor === 1 && req.httpVersionMinor === 1)) {
908-
if (RegExpPrototypeTest(continueExpression, req.headers.expect)) {
909-
res._expect_continue = true;
908+
let handled = false;
910909

911-
if (server.listenerCount('checkContinue') > 0) {
912-
server.emit('checkContinue', req, res);
910+
if (req.httpVersionMajor === 1 && req.httpVersionMinor === 1) {
911+
if (typeof server.maxRequestsPerSocket === 'number') {
912+
state.requestsCount++;
913+
res.maxRequestsOnConnectionReached = (
914+
server.maxRequestsPerSocket <= state.requestsCount);
915+
}
916+
917+
if (typeof server.maxRequestsPerSocket === 'number' &&
918+
(server.maxRequestsPerSocket < state.requestsCount)) {
919+
handled = true;
920+
921+
res.writeHead(503);
922+
res.end();
923+
} else if (req.headers.expect !== undefined) {
924+
handled = true;
925+
926+
if (RegExpPrototypeTest(continueExpression, req.headers.expect)) {
927+
res._expect_continue = true;
928+
929+
if (server.listenerCount('checkContinue') > 0) {
930+
server.emit('checkContinue', req, res);
931+
} else {
932+
res.writeContinue();
933+
server.emit('request', req, res);
934+
}
935+
} else if (server.listenerCount('checkExpectation') > 0) {
936+
server.emit('checkExpectation', req, res);
913937
} else {
914-
res.writeContinue();
915-
server.emit('request', req, res);
938+
res.writeHead(417);
939+
res.end();
916940
}
917-
} else if (server.listenerCount('checkExpectation') > 0) {
918-
server.emit('checkExpectation', req, res);
919-
} else {
920-
res.writeHead(417);
921-
res.end();
922941
}
923-
} else {
924-
req.on('end', clearRequestTimeout);
942+
}
925943

944+
if (!handled) {
945+
req.on('end', clearRequestTimeout);
926946
server.emit('request', req, res);
927947
}
948+
928949
return 0; // No special treatment.
929950
}
930951

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const net = require('net');
5+
const http = require('http');
6+
const assert = require('assert');
7+
8+
const bodySent = 'This is my request';
9+
10+
function assertResponse(headers, body, expectClosed) {
11+
if (expectClosed) {
12+
assert.match(headers, /Connection: close\r\n/m);
13+
assert.strictEqual(headers.search(/Keep-Alive: timeout=5\r\n/m), -1);
14+
assert.match(body, /Hello World!/m);
15+
} else {
16+
assert.match(headers, /Connection: keep-alive\r\n/m);
17+
assert.match(headers, /Keep-Alive: timeout=5\r\n/m);
18+
assert.match(body, /Hello World!/m);
19+
}
20+
}
21+
22+
function writeRequest(socket, withBody) {
23+
if (withBody) {
24+
socket.write('POST / HTTP/1.1\r\n');
25+
socket.write('Connection: keep-alive\r\n');
26+
socket.write('Content-Type: text/plain\r\n');
27+
socket.write(`Content-Length: ${bodySent.length}\r\n\r\n`);
28+
socket.write(`${bodySent}\r\n`);
29+
socket.write('\r\n\r\n');
30+
} else {
31+
socket.write('GET / HTTP/1.1\r\n');
32+
socket.write('Connection: keep-alive\r\n');
33+
socket.write('\r\n\r\n');
34+
}
35+
}
36+
37+
const server = http.createServer((req, res) => {
38+
let body = '';
39+
req.on('data', (data) => {
40+
body += data;
41+
});
42+
43+
req.on('end', () => {
44+
if (req.method === 'POST') {
45+
assert.strictEqual(bodySent, body);
46+
}
47+
res.writeHead(200, { 'Content-Type': 'text/plain' });
48+
res.write('Hello World!');
49+
res.end();
50+
});
51+
});
52+
53+
function initialRequests(socket, numberOfRequests, cb) {
54+
let buffer = '';
55+
56+
writeRequest(socket);
57+
58+
socket.on('data', (data) => {
59+
buffer += data;
60+
61+
if (buffer.endsWith('\r\n\r\n')) {
62+
if (--numberOfRequests === 0) {
63+
socket.removeAllListeners('data');
64+
cb();
65+
} else {
66+
const [headers, body] = buffer.trim().split('\r\n\r\n');
67+
assertResponse(headers, body);
68+
buffer = '';
69+
writeRequest(socket, true);
70+
}
71+
}
72+
});
73+
}
74+
75+
76+
server.maxRequestsPerSocket = 3;
77+
server.listen(0, common.mustCall((res) => {
78+
const socket = new net.Socket();
79+
const anotherSocket = new net.Socket();
80+
81+
socket.on('end', common.mustCall(() => {
82+
server.close();
83+
}));
84+
85+
socket.on('ready', common.mustCall(() => {
86+
// Do 2 of 3 allowed requests and ensure they still alive
87+
initialRequests(socket, 2, common.mustCall(() => {
88+
anotherSocket.connect({ port: server.address().port });
89+
}));
90+
}));
91+
92+
anotherSocket.on('ready', common.mustCall(() => {
93+
// Do another 2 requests with another socket
94+
// enusre that this will not affect the first socket
95+
initialRequests(anotherSocket, 2, common.mustCall(() => {
96+
let buffer = '';
97+
98+
// Send the rest of the calls to the first socket
99+
// and see connection is closed
100+
socket.on('data', common.mustCall((data) => {
101+
buffer += data;
102+
103+
if (buffer.endsWith('\r\n\r\n')) {
104+
const [headers, body] = buffer.trim().split('\r\n\r\n');
105+
assertResponse(headers, body, true);
106+
anotherSocket.end();
107+
socket.end();
108+
}
109+
}));
110+
111+
writeRequest(socket, true);
112+
}));
113+
}));
114+
115+
socket.connect({ port: server.address().port });
116+
}));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const net = require('net');
5+
const http = require('http');
6+
const assert = require('assert');
7+
8+
const bodySent = 'This is my request';
9+
10+
function assertResponse(headers, body, expectClosed) {
11+
if (expectClosed) {
12+
assert.match(headers, /Connection: close\r\n/m);
13+
assert.strictEqual(headers.search(/Keep-Alive: timeout=5\r\n/m), -1);
14+
assert.match(body, /Hello World!/m);
15+
} else {
16+
assert.match(headers, /Connection: keep-alive\r\n/m);
17+
assert.match(headers, /Keep-Alive: timeout=5\r\n/m);
18+
assert.match(body, /Hello World!/m);
19+
}
20+
}
21+
22+
function writeRequest(socket) {
23+
socket.write('POST / HTTP/1.1\r\n');
24+
socket.write('Connection: keep-alive\r\n');
25+
socket.write('Content-Type: text/plain\r\n');
26+
socket.write(`Content-Length: ${bodySent.length}\r\n\r\n`);
27+
socket.write(`${bodySent}\r\n`);
28+
socket.write('\r\n\r\n');
29+
}
30+
31+
const server = http.createServer((req, res) => {
32+
let body = '';
33+
req.on('data', (data) => {
34+
body += data;
35+
});
36+
37+
req.on('end', () => {
38+
if (req.method === 'POST') {
39+
assert.strictEqual(bodySent, body);
40+
}
41+
42+
res.writeHead(200, { 'Content-Type': 'text/plain' });
43+
res.write('Hello World!');
44+
res.end();
45+
});
46+
});
47+
48+
server.maxRequestsPerSocket = 3;
49+
50+
server.listen(0, common.mustCall((res) => {
51+
const socket = new net.Socket();
52+
53+
socket.on('end', common.mustCall(() => {
54+
server.close();
55+
}));
56+
57+
socket.on('ready', common.mustCall(() => {
58+
writeRequest(socket);
59+
writeRequest(socket);
60+
writeRequest(socket);
61+
writeRequest(socket);
62+
}));
63+
64+
let buffer = '';
65+
66+
socket.on('data', (data) => {
67+
buffer += data;
68+
69+
const responseParts = buffer.trim().split('\r\n\r\n');
70+
71+
if (responseParts.length === 8) {
72+
assertResponse(responseParts[0], responseParts[1]);
73+
assertResponse(responseParts[2], responseParts[3]);
74+
assertResponse(responseParts[4], responseParts[5], true);
75+
76+
assert.match(responseParts[6], /HTTP\/1\.1 503 Service Unavailable/m);
77+
assert.match(responseParts[6], /Connection: close\r\n/m);
78+
assert.strictEqual(responseParts[6].search(/Keep-Alive: timeout=5\r\n/m), -1);
79+
assert.strictEqual(responseParts[7].search(/Hello World!/m), -1);
80+
81+
socket.end();
82+
}
83+
});
84+
85+
socket.connect({ port: server.address().port });
86+
}));

0 commit comments

Comments
 (0)
Please sign in to comment.