From 2ca5a3db47b930912161074c7b514c769113433b Mon Sep 17 00:00:00 2001 From: Fedor Indutny Date: Wed, 22 Jul 2015 21:18:38 -0700 Subject: [PATCH] https: reuse TLS sessions in Agent Fix: #1499 PR-URL: https://github.com/nodejs/io.js/pull/2228 Reviewed-By: Shigeki Ohtsu Reviewed-By: Trevor Norris --- lib/_http_agent.js | 1 + lib/_tls_wrap.js | 14 ++- lib/https.js | 50 ++++++++- test/parallel/test-https-agent-session-reuse.js | 130 ++++++++++++++++++++++++ 4 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 test/parallel/test-https-agent-session-reuse.js diff --git a/lib/_http_agent.js b/lib/_http_agent.js index 02f5e04..9208044 100644 --- a/lib/_http_agent.js +++ b/lib/_http_agent.js @@ -171,6 +171,7 @@ Agent.prototype.createSocket = function(req, options) { } var name = self.getName(options); + options._agentKey = name; debug('createConnection', name, options); options.encoding = null; diff --git a/lib/_tls_wrap.js b/lib/_tls_wrap.js index f42da43..1823469 100644 --- a/lib/_tls_wrap.js +++ b/lib/_tls_wrap.js @@ -584,6 +584,17 @@ TLSSocket.prototype._start = function() { this._handle.start(); }; +TLSSocket.prototype._isSessionResumed = function _isSessionResumed(session) { + if (!session) + return false; + + var next = this.getSession(); + if (!next) + return false; + + return next.equals(session); +}; + TLSSocket.prototype.setServername = function(name) { this._handle.setServername(name); }; @@ -999,7 +1010,8 @@ exports.connect = function(/* [port, host], options, cb */) { var verifyError = socket._handle.verifyError(); // Verify that server's identity matches it's certificate's names - if (!verifyError) { + // Unless server has resumed our existing session + if (!verifyError && !socket._isSessionResumed(options.session)) { var cert = socket.getPeerCertificate(); verifyError = options.checkServerIdentity(hostname, cert); } diff --git a/lib/https.js b/lib/https.js index 21103b7..abe4a20 100644 --- a/lib/https.js +++ b/lib/https.js @@ -58,7 +58,25 @@ function createConnection(port, host, options) { } debug('createConnection', options); - return tls.connect(options); + + if (options._agentKey) { + const session = this._getSession(options._agentKey); + if (session) { + debug('reuse session for %j', options._agentKey); + options = util._extend({ + session: session + }, options); + } + } + + const self = this; + const socket = tls.connect(options, function() { + if (!options._agentKey) + return; + + self._cacheSession(options._agentKey, socket.getSession()); + }); + return socket; } @@ -66,6 +84,14 @@ function Agent(options) { http.Agent.call(this, options); this.defaultPort = 443; this.protocol = 'https:'; + this.maxCachedSessions = this.options.maxCachedSessions; + if (this.maxCachedSessions === undefined) + this.maxCachedSessions = 100; + + this._sessionCache = { + map: {}, + list: [] + }; } inherits(Agent, http.Agent); Agent.prototype.createConnection = createConnection; @@ -100,6 +126,28 @@ Agent.prototype.getName = function(options) { return name; }; +Agent.prototype._getSession = function _getSession(key) { + return this._sessionCache.map[key]; +}; + +Agent.prototype._cacheSession = function _cacheSession(key, session) { + // Fast case - update existing entry + if (this._sessionCache.map[key]) { + this._sessionCache.map[key] = session; + return; + } + + // Put new entry + if (this._sessionCache.list.length >= this.maxCachedSessions) { + const oldKey = this._sessionCache.list.shift(); + debug('evicting %j', oldKey); + delete this._sessionCache.map[oldKey]; + } + + this._sessionCache.list.push(key); + this._sessionCache.map[key] = session; +}; + const globalAgent = new Agent(); exports.globalAgent = globalAgent; diff --git a/test/parallel/test-https-agent-session-reuse.js b/test/parallel/test-https-agent-session-reuse.js new file mode 100644 index 0000000..2c20252 --- /dev/null +++ b/test/parallel/test-https-agent-session-reuse.js @@ -0,0 +1,130 @@ +'use strict'; +var common = require('../common'); +var assert = require('assert'); + +if (!common.hasCrypto) { + console.log('1..0 # Skipped: missing crypto'); + return; +} + +var https = require('https'); +var crypto = require('crypto'); + +var fs = require('fs'); + +var options = { + key: fs.readFileSync(common.fixturesDir + '/keys/agent1-key.pem'), + cert: fs.readFileSync(common.fixturesDir + '/keys/agent1-cert.pem') +}; + +var ca = fs.readFileSync(common.fixturesDir + '/keys/ca1-cert.pem'); + +var clientSessions = {}; +var serverRequests = 0; + +var agent = new https.Agent({ + maxCachedSessions: 1 +}); + +var server = https.createServer(options, function(req, res) { + if (req.url === '/drop-key') + server.setTicketKeys(crypto.randomBytes(48)); + + serverRequests++; + res.end('ok'); +}).listen(common.PORT, function() { + var queue = [ + { + name: 'first', + + method: 'GET', + path: '/', + servername: 'agent1', + ca: ca, + port: common.PORT + }, + { + name: 'first-reuse', + + method: 'GET', + path: '/', + servername: 'agent1', + ca: ca, + port: common.PORT + }, + { + name: 'cipher-change', + + method: 'GET', + path: '/', + servername: 'agent1', + + // Choose different cipher to use different cache entry + ciphers: 'AES256-SHA', + ca: ca, + port: common.PORT + }, + // Change the ticket key to ensure session is updated in cache + { + name: 'before-drop', + + method: 'GET', + path: '/drop-key', + servername: 'agent1', + ca: ca, + port: common.PORT + }, + + // Ticket will be updated starting from this + { + name: 'after-drop', + + method: 'GET', + path: '/', + servername: 'agent1', + ca: ca, + port: common.PORT + }, + { + name: 'after-drop-reuse', + + method: 'GET', + path: '/', + servername: 'agent1', + ca: ca, + port: common.PORT + } + ]; + + function request() { + var options = queue.shift(); + options.agent = agent; + https.request(options, function(res) { + clientSessions[options.name] = res.socket.getSession(); + + res.resume(); + res.on('end', function() { + if (queue.length !== 0) + return request(); + server.close(); + }); + }).end(); + } + request(); +}); + +process.on('exit', function() { + assert.equal(serverRequests, 6); + assert.equal(clientSessions['first'].toString('hex'), + clientSessions['first-reuse'].toString('hex')); + assert.notEqual(clientSessions['first'].toString('hex'), + clientSessions['cipher-change'].toString('hex')); + assert.notEqual(clientSessions['first'].toString('hex'), + clientSessions['before-drop'].toString('hex')); + assert.notEqual(clientSessions['cipher-change'].toString('hex'), + clientSessions['before-drop'].toString('hex')); + assert.notEqual(clientSessions['before-drop'].toString('hex'), + clientSessions['after-drop'].toString('hex')); + assert.equal(clientSessions['after-drop'].toString('hex'), + clientSessions['after-drop-reuse'].toString('hex')); +}); -- 2.7.4