File http-keep-alive.patch of Package nodejs4
commit b13b4a9ffb6038be1c21c20ece50d5fe367807b3
Author: Matteo Collina <hello@matteocollina.com>
Date: Sat Dec 1 16:29:13 2018 +0100
http: prevent slowloris with keepalive connections
Fixes: https://github.com/nodejs-private/security/issues/214
PR-URL: https://github.com/nodejs-private/node-private/pull/162
Reviewed-By: Rod Vagg <rod@vagg.org>
Reviewed-By: Sam Roberts <vieuxtech@gmail.com>
Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
commit e9ae4aaaad5947075bdb3d1f558281aa1a729b36
Author: Alexey Orlenko <eaglexrlnk@gmail.com>
Date: Thu Jun 8 17:20:24 2017 +0300
http: fix timeout reset after keep-alive timeout
Fix the logic of resetting the socket timeout of keep-alive HTTP
connections and add two tests:
* `test-http-server-keep-alive-timeout-slow-server` is a regression test
for GH-13391. It ensures that the server-side keep-alive timeout will
not fire during processing of a request.
* `test-http-server-keep-alive-timeout-slow-client-headers` ensures that
the regular socket timeout is restored as soon as a client starts
sending a new request, not as soon as the whole message is received,
so that the keep-alive timeout will not fire while, e.g., the client
is sending large cookies.
commit 06a208d3166493cc5bbc63b5cdbe473dbf4ad0ec
Author: realwakka <realwakka@gmail.com>
Date: Sun Jun 4 14:03:11 2017 +0900
test: refactor test-http-server-keep-alive-timeout
Make the same reliability changes that were applied to the https test in
ce5745bf92f586c58366e9f738441d69118f2c18.
Refs: https://github.com/nodejs/node/pull/13312
PR-URL: https://github.com/nodejs/node/pull/13448
Reviewed-By: Rich Trott <rtrott@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Refael Ackermann <refack@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Alexey Orlenko <eaglexrlnk@gmail.com>
commit 1c7fbdc53bbf8bf43ca32b2c6e9f513508ac90fa
Author: Rich Trott <rtrott@gmail.com>
Date: Tue May 30 13:06:52 2017 -0700
test: improve test-https-server-keep-alive-timeout
The test is flaky under load. These changes greatly improve reliability.
* Use a recurring interval to determine when the test should end rather
than a timer.
* Increase server timeout to 500ms to allow for events being delayed by
system load
Changing to an interval has the added benefit of reducing the test run
time from over 2 seconds to under 1 second.
Fixes: https://github.com/nodejs/node/issues/13307
PR-URL: https://github.com/nodejs/node/pull/13312
Reviewed-By: Refael Ackermann <refack@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Alexey Orlenko <eaglexrlnk@gmail.com>
commit f23b3b6bad391cc3efe4cc815954f009c05fe63a
Author: Timur Shemsedinov <timur.shemsedinov@gmail.com>
Date: Thu Oct 29 21:53:43 2015 +0200
http: destroy sockets after keepAliveTimeout
Implement server.keepAliveTimeout in addition to server.timeout to
prevent temporary socket/memory leaking in keep-alive mode.
PR-URL: https://github.com/nodejs/node/pull/2534
Author: Timur Shemsedinov <timur.shemsedinov@gmail.com>
Author: Alexey Orlenko <eaglexrlnk@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Franziska Hinkelmann <franziska.hinkelmann@gmail.com>
Reviewed-By: Refael Ackermann <refack@gmail.com>
Index: node-v4.9.1/lib/_http_server.js
===================================================================
--- node-v4.9.1.orig/lib/_http_server.js
+++ node-v4.9.1/lib/_http_server.js
@@ -250,6 +250,7 @@ function Server(requestListener) {
});
this.timeout = 2 * 60 * 1000;
+ this.keepAliveTimeout = 5000;
this._pendingResponseData = 0;
this.headersTimeout = 40 * 1000; // 40 seconds
@@ -323,6 +324,8 @@ function connectionListener(socket) {
socket.destroy();
});
+ socket._keepAliveTimeoutSet = false;
+
var parser = parsers.alloc();
parser.reinitialize(HTTPParser.REQUEST);
parser.socket = socket;
@@ -373,6 +376,15 @@ function connectionListener(socket) {
function socketOnData(d) {
assert(!socket._paused);
debug('SERVER socketOnData %d', d.length);
+
+ if (socket._keepAliveTimeoutSet) {
+ socket.setTimeout(0);
+ if (self.timeout) {
+ socket.setTimeout(self.timeout);
+ }
+ socket._keepAliveTimeoutSet = false;
+ }
+
var ret = parser.execute(d);
onParserExecuteCommon(ret, d);
@@ -399,6 +411,8 @@ function connectionListener(socket) {
}
function onParserExecuteCommon(ret, d) {
+ resetSocketTimeout(self, socket);
+
if (ret instanceof Error) {
debug('parse error');
socket.destroy(ret);
@@ -481,8 +495,14 @@ function connectionListener(socket) {
}
function parserOnIncoming(req, shouldKeepAlive) {
+ resetSocketTimeout(self, socket);
+
incoming.push(req);
+ if (self.keepAliveTimeout > 0) {
+ req.on('end', resetHeadersTimeoutOnReqEnd);
+ }
+
// If the writable end isn't consuming, then stop reading
// so that we don't become overwhelmed by a flood of
// pipelined requests that may never be resolved.
@@ -534,6 +554,12 @@ function connectionListener(socket) {
if (res._last) {
socket.destroySoon();
+ } else if (outgoing.length === 0) {
+ if (self.keepAliveTimeout) {
+ socket.setTimeout(0);
+ socket.setTimeout(self.keepAliveTimeout);
+ socket._keepAliveTimeoutSet = true;
+ }
} else {
// start sending the next message
var m = outgoing.shift();
@@ -561,6 +587,14 @@ function connectionListener(socket) {
}
exports._connectionListener = connectionListener;
+function resetSocketTimeout(server, socket) {
+ if (!socket._keepAliveTimeoutSet)
+ return;
+
+ socket.setTimeout(server.timeout || 0);
+ socket._keepAliveTimeoutSet = false;
+}
+
function onSocketResume() {
// It may seem that the socket is resumed, but this is an enemy's trick to
// deceive us! `resume` is emitted asynchronously, and may be called from
@@ -608,3 +642,15 @@ function socketOnWrap(ev, fn) {
return res;
}
+
+function resetHeadersTimeoutOnReqEnd() {
+ debug('resetHeadersTimeoutOnReqEnd');
+
+ var parser = this.socket.parser;
+ // Parser can be null if the socket was destroyed
+ // in that case, there is nothing to do.
+ if (parser !== null) {
+ parser.parsingHeadersStart = nowDate();
+ }
+}
+
Index: node-v4.9.1/test/parallel/test-http-server-keep-alive-timeout-slow-client-headers.js
===================================================================
--- /dev/null
+++ node-v4.9.1/test/parallel/test-http-server-keep-alive-timeout-slow-client-headers.js
@@ -0,0 +1,57 @@
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const http = require('http');
+const net = require('net');
+
+const server = http.createServer(common.mustCall((req, res) => {
+ res.end();
+}, 2));
+
+server.keepAliveTimeout = common.platformTimeout(100);
+
+server.listen(0, common.mustCall(() => {
+ const port = server.address().port;
+ const socket = net.connect({ port }, common.mustCall(() => {
+ request(common.mustCall(() => {
+ // Make a second request on the same socket, after the keep-alive timeout
+ // has been set on the server side.
+ request(common.mustCall(() => {}));
+ }));
+ }));
+
+ server.on('timeout', common.mustCall(() => {
+ socket.end();
+ server.close();
+ }));
+
+ function request(callback) {
+ socket.setEncoding('utf8');
+ socket.on('data', onData);
+ let response = '';
+
+ // Simulate a client that sends headers slowly (with a period of inactivity
+ // that is longer than the keep-alive timeout).
+ socket.write('GET / HTTP/1.1\r\n' +
+ `Host: localhost:${port}\r\n`);
+ setTimeout(() => {
+ socket.write('Connection: keep-alive\r\n' +
+ '\r\n');
+ }, common.platformTimeout(300));
+
+ function onData(chunk) {
+ response += chunk;
+ if (chunk.includes('\r\n')) {
+ socket.removeListener('data', onData);
+ onHeaders();
+ }
+ }
+
+ function onHeaders() {
+ assert.ok(response.includes('HTTP/1.1 200 OK\r\n'));
+ assert.ok(response.includes('Connection: keep-alive\r\n'));
+ callback();
+ }
+ }
+}));
Index: node-v4.9.1/test/parallel/test-http-slow-headers-keepalive.js
===================================================================
--- /dev/null
+++ node-v4.9.1/test/parallel/test-http-slow-headers-keepalive.js
@@ -0,0 +1,59 @@
+'use strict';
+
+const common = require('../common');
+const http = require('http');
+const net = require('net');
+
+const headers =
+ 'GET / HTTP/1.1\r\n' +
+ 'Host: localhost\r\n' +
+ 'Connection: keep-alive' +
+ 'Agent: node\r\n';
+
+let sendCharEvery = 1000;
+
+const server = http.createServer(common.mustCall((req, res) => {
+ res.writeHead(200);
+ res.end();
+}));
+
+// Pass a REAL env variable to shortening up the default
+// value which is 40s otherwise this is useful for manual
+// testing
+if (!process.env.REAL) {
+ sendCharEvery = common.platformTimeout(10);
+ server.headersTimeout = 2 * sendCharEvery;
+}
+
+server.once('timeout', common.mustCall((socket) => {
+ socket.destroy();
+}));
+
+server.listen(0, () => {
+ const client = net.connect(server.address().port);
+ client.write(headers);
+ // finish the first request
+ client.write('\r\n');
+ // second request
+ client.write(headers);
+ client.write('X-CRASH: ');
+
+ const interval = setInterval(() => {
+ client.write('a');
+ }, sendCharEvery);
+
+ client.resume();
+
+ const onClose = common.mustCall(() => {
+ client.removeListener('close', onClose);
+ client.removeListener('error', onClose);
+ client.removeListener('end', onClose);
+ clearInterval(interval);
+ server.close();
+ });
+
+ client.on('error', onClose);
+ client.on('close', onClose);
+ client.on('end', onClose);
+});
+
Index: node-v4.9.1/test/parallel/test-http-server-keep-alive-timeout-slow-server.js
===================================================================
--- /dev/null
+++ node-v4.9.1/test/parallel/test-http-server-keep-alive-timeout-slow-server.js
@@ -0,0 +1,50 @@
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const http = require('http');
+
+const server = http.createServer(common.mustCall((req, res) => {
+ if (req.url === '/first') {
+ res.end('ok');
+ return;
+ }
+ setTimeout(() => {
+ res.end('ok');
+ }, common.platformTimeout(500));
+}, 2));
+
+server.keepAliveTimeout = common.platformTimeout(200);
+
+const agent = new http.Agent({
+ keepAlive: true,
+ maxSockets: 1
+});
+
+function request(path, callback) {
+ const port = server.address().port;
+ const req = http.request({ agent, path, port }, common.mustCall((res) => {
+ assert.strictEqual(res.statusCode, 200);
+
+ res.setEncoding('utf8');
+
+ let result = '';
+ res.on('data', (chunk) => {
+ result += chunk;
+ });
+
+ res.on('end', common.mustCall(() => {
+ assert.strictEqual(result, 'ok');
+ callback();
+ }));
+ }));
+ req.end();
+}
+
+server.listen(0, common.mustCall(() => {
+ request('/first', () => {
+ request('/second', () => {
+ server.close();
+ });
+ });
+}));
Index: node-v4.9.1/doc/api/http.md
===================================================================
--- node-v4.9.1.orig/doc/api/http.md
+++ node-v4.9.1/doc/api/http.md
@@ -729,17 +729,33 @@ customised.
added: v0.9.12
-->
-* {Number} Default = 120000 (2 minutes)
+* {number} Timeout in milliseconds. Defaults to 120000 (2 minutes).
The number of milliseconds of inactivity before a socket is presumed
to have timed out.
-Note that the socket timeout logic is set up on connection, so
-changing this value only affects *new* connections to the server, not
-any existing connections.
+A value of 0 will disable the timeout behavior on incoming connections.
-Set to 0 to disable any kind of automatic timeout behavior on incoming
-connections.
+*Note*: The socket timeout logic is set up on connection, so changing this
+value only affects new connections to the server, not any existing connections.
+
+### server.keepAliveTimeout
+<!-- YAML
+added: REPLACEME
+-->
+
+* {number} Timeout in milliseconds. Defaults to 5000 (5 seconds).
+
+The number of milliseconds of inactivity a server needs to wait for additional
+incoming data, after it has finished writing the last response, before a socket
+will be destroyed. If the server receives new data before the keep-alive
+timeout has fired, it will reset the regular inactivity timeout, i.e.,
+[`server.timeout`][].
+
+A value of 0 will disable the keep-alive timeout behavior on incoming connections.
+
+*Note*: The socket timeout logic is set up on connection, so changing this
+value only affects new connections to the server, not any existing connections.
## Class: http.ServerResponse
<!-- YAML
@@ -1498,6 +1514,7 @@ There are a few special headers that sho
[`response.write(data, encoding)`]: #http_response_write_chunk_encoding_callback
[`response.writeContinue()`]: #http_response_writecontinue
[`response.writeHead()`]: #http_response_writehead_statuscode_statusmessage_headers
+[`server.timeout`]: #http_server_timeout
[`socket.setKeepAlive()`]: net.html#net_socket_setkeepalive_enable_initialdelay
[`socket.setNoDelay()`]: net.html#net_socket_setnodelay_nodelay
[`socket.setTimeout()`]: net.html#net_socket_settimeout_timeout_callback
Index: node-v4.9.1/doc/api/https.md
===================================================================
--- node-v4.9.1.orig/doc/api/https.md
+++ node-v4.9.1/doc/api/https.md
@@ -41,6 +41,14 @@ added: v0.11.2
See [`http.Server#timeout`][].
+### server.keepAliveTimeout
+<!-- YAML
+added: REPLACEME
+-->
+- {number} Defaults to 5000 (5 seconds).
+
+See [`http.Server#keepAliveTimeout`][].
+
## https.createServer(options[, requestListener])
<!-- YAML
added: v0.3.4
@@ -264,6 +272,7 @@ var req = https.request(options, (res) =
[`globalAgent`]: #https_https_globalagent
[`http.Agent`]: http.html#http_class_http_agent
[`http.Server#headersTimeout`]: http.html#http_server_headerstimeout
+[`http.Server#keepAliveTimeout`]: http.html#http_server_keepalivetimeout
[`http.close()`]: http.html#http_server_close_callback
[`http.get()`]: http.html#http_http_get_options_callback
[`http.listen()`]: http.html#http_server_listen_port_hostname_backlog_callback
Index: node-v4.9.1/lib/https.js
===================================================================
--- node-v4.9.1.orig/lib/https.js
+++ node-v4.9.1/lib/https.js
@@ -34,7 +34,7 @@ function Server(opts, requestListener) {
});
this.timeout = 2 * 60 * 1000;
-
+ this.keepAliveTimeout = 5000;
this.headersTimeout = 40 * 1000; // 40 seconds
}
inherits(Server, tls.Server);
Index: node-v4.9.1/test/parallel/test-http-server-keep-alive-timeout.js
===================================================================
--- /dev/null
+++ node-v4.9.1/test/parallel/test-http-server-keep-alive-timeout.js
@@ -0,0 +1,95 @@
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const http = require('http');
+const net = require('net');
+
+const tests = [];
+
+function test(fn) {
+ if (!tests.length) {
+ process.nextTick(run);
+ }
+ tests.push(fn);
+}
+
+function run() {
+ const fn = tests.shift();
+ if (fn) fn(run);
+}
+
+test(function serverEndKeepAliveTimeoutWithPipeline(cb) {
+ let socket;
+ let destroyedSockets = 0;
+ let timeoutCount = 0;
+ let requestCount = 0;
+ process.on('exit', () => {
+ assert.strictEqual(timeoutCount, 1);
+ assert.strictEqual(requestCount, 3);
+ assert.strictEqual(destroyedSockets, 1);
+ });
+ const server = http.createServer((req, res) => {
+ socket = req.socket;
+ requestCount++;
+ res.end();
+ });
+ server.setTimeout(200, (socket) => {
+ timeoutCount++;
+ socket.destroy();
+ });
+ server.keepAliveTimeout = 50;
+ server.listen(0, common.mustCall(() => {
+ const options = {
+ port: server.address().port,
+ allowHalfOpen: true
+ };
+ const c = net.connect(options, () => {
+ c.write('GET /1 HTTP/1.1\r\nHost: localhost\r\n\r\n');
+ c.write('GET /2 HTTP/1.1\r\nHost: localhost\r\n\r\n');
+ c.write('GET /3 HTTP/1.1\r\nHost: localhost\r\n\r\n');
+ });
+ setTimeout(() => {
+ server.close();
+ if (socket.destroyed) destroyedSockets++;
+ cb();
+ }, 1000);
+ }));
+});
+
+test(function serverNoEndKeepAliveTimeoutWithPipeline(cb) {
+ let socket;
+ let destroyedSockets = 0;
+ let timeoutCount = 0;
+ let requestCount = 0;
+ process.on('exit', () => {
+ assert.strictEqual(timeoutCount, 1);
+ assert.strictEqual(requestCount, 3);
+ assert.strictEqual(destroyedSockets, 1);
+ });
+ const server = http.createServer((req, res) => {
+ socket = req.socket;
+ requestCount++;
+ });
+ server.setTimeout(200, (socket) => {
+ timeoutCount++;
+ socket.destroy();
+ });
+ server.keepAliveTimeout = 50;
+ server.listen(0, common.mustCall(() => {
+ const options = {
+ port: server.address().port,
+ allowHalfOpen: true
+ };
+ const c = net.connect(options, () => {
+ c.write('GET /1 HTTP/1.1\r\nHost: localhost\r\n\r\n');
+ c.write('GET /2 HTTP/1.1\r\nHost: localhost\r\n\r\n');
+ c.write('GET /3 HTTP/1.1\r\nHost: localhost\r\n\r\n');
+ });
+ setTimeout(() => {
+ server.close();
+ if (socket.destroyed) destroyedSockets++;
+ cb();
+ }, 1000);
+ }));
+});
Index: node-v4.9.1/test/parallel/test-https-server-keep-alive-timeout.js
===================================================================
--- /dev/null
+++ node-v4.9.1/test/parallel/test-https-server-keep-alive-timeout.js
@@ -0,0 +1,85 @@
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const https = require('https');
+const tls = require('tls');
+const fs = require('fs');
+
+const tests = [];
+
+const serverOptions = {
+ key: fs.readFileSync(common.fixturesDir + '/keys/agent1-key.pem'),
+ cert: fs.readFileSync(common.fixturesDir + '/keys/agent1-cert.pem')
+};
+
+function test(fn) {
+ if (!tests.length) {
+ process.nextTick(run);
+ }
+ tests.push(fn);
+}
+
+function run() {
+ const fn = tests.shift();
+ if (fn) fn(run);
+}
+
+test(function serverKeepAliveTimeoutWithPipeline(cb) {
+ let requestCount = 0;
+ process.on('exit', function() {
+ assert.strictEqual(requestCount, 3);
+ });
+ const server = https.createServer(serverOptions, (req, res) => {
+ requestCount++;
+ res.end();
+ });
+ server.setTimeout(500, common.mustCall((socket) => {
+ // End this test and call `run()` for the next test (if any).
+ socket.destroy();
+ server.close();
+ cb();
+ }));
+ server.keepAliveTimeout = 50;
+ server.listen(0, common.mustCall(() => {
+ const options = {
+ port: server.address().port,
+ allowHalfOpen: true,
+ rejectUnauthorized: false
+ };
+ const c = tls.connect(options, () => {
+ c.write('GET /1 HTTP/1.1\r\nHost: localhost\r\n\r\n');
+ c.write('GET /2 HTTP/1.1\r\nHost: localhost\r\n\r\n');
+ c.write('GET /3 HTTP/1.1\r\nHost: localhost\r\n\r\n');
+ });
+ }));
+});
+
+test(function serverNoEndKeepAliveTimeoutWithPipeline(cb) {
+ let requestCount = 0;
+ process.on('exit', () => {
+ assert.strictEqual(requestCount, 3);
+ });
+ const server = https.createServer(serverOptions, (req, res) => {
+ requestCount++;
+ });
+ server.setTimeout(500, common.mustCall((socket) => {
+ // End this test and call `run()` for the next test (if any).
+ socket.destroy();
+ server.close();
+ cb();
+ }));
+ server.keepAliveTimeout = 50;
+ server.listen(0, common.mustCall(() => {
+ const options = {
+ port: server.address().port,
+ allowHalfOpen: true,
+ rejectUnauthorized: false
+ };
+ const c = tls.connect(options, () => {
+ c.write('GET /1 HTTP/1.1\r\nHost: localhost\r\n\r\n');
+ c.write('GET /2 HTTP/1.1\r\nHost: localhost\r\n\r\n');
+ c.write('GET /3 HTTP/1.1\r\nHost: localhost\r\n\r\n');
+ });
+ }));
+});