Skip to content

Commit 2ca5a3d

Browse files
committed
https: reuse TLS sessions in Agent
Fix: #1499 PR-URL: #2228 Reviewed-By: Shigeki Ohtsu <[email protected]> Reviewed-By: Trevor Norris <[email protected]>
1 parent 4e78cd7 commit 2ca5a3d

File tree

4 files changed

+193
-2
lines changed

4 files changed

+193
-2
lines changed

lib/_http_agent.js

+1
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ Agent.prototype.createSocket = function(req, options) {
171171
}
172172

173173
var name = self.getName(options);
174+
options._agentKey = name;
174175

175176
debug('createConnection', name, options);
176177
options.encoding = null;

lib/_tls_wrap.js

+13-1
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,17 @@ TLSSocket.prototype._start = function() {
584584
this._handle.start();
585585
};
586586

587+
TLSSocket.prototype._isSessionResumed = function _isSessionResumed(session) {
588+
if (!session)
589+
return false;
590+
591+
var next = this.getSession();
592+
if (!next)
593+
return false;
594+
595+
return next.equals(session);
596+
};
597+
587598
TLSSocket.prototype.setServername = function(name) {
588599
this._handle.setServername(name);
589600
};
@@ -999,7 +1010,8 @@ exports.connect = function(/* [port, host], options, cb */) {
9991010
var verifyError = socket._handle.verifyError();
10001011

10011012
// Verify that server's identity matches it's certificate's names
1002-
if (!verifyError) {
1013+
// Unless server has resumed our existing session
1014+
if (!verifyError && !socket._isSessionResumed(options.session)) {
10031015
var cert = socket.getPeerCertificate();
10041016
verifyError = options.checkServerIdentity(hostname, cert);
10051017
}

lib/https.js

+49-1
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,40 @@ function createConnection(port, host, options) {
5858
}
5959

6060
debug('createConnection', options);
61-
return tls.connect(options);
61+
62+
if (options._agentKey) {
63+
const session = this._getSession(options._agentKey);
64+
if (session) {
65+
debug('reuse session for %j', options._agentKey);
66+
options = util._extend({
67+
session: session
68+
}, options);
69+
}
70+
}
71+
72+
const self = this;
73+
const socket = tls.connect(options, function() {
74+
if (!options._agentKey)
75+
return;
76+
77+
self._cacheSession(options._agentKey, socket.getSession());
78+
});
79+
return socket;
6280
}
6381

6482

6583
function Agent(options) {
6684
http.Agent.call(this, options);
6785
this.defaultPort = 443;
6886
this.protocol = 'https:';
87+
this.maxCachedSessions = this.options.maxCachedSessions;
88+
if (this.maxCachedSessions === undefined)
89+
this.maxCachedSessions = 100;
90+
91+
this._sessionCache = {
92+
map: {},
93+
list: []
94+
};
6995
}
7096
inherits(Agent, http.Agent);
7197
Agent.prototype.createConnection = createConnection;
@@ -100,6 +126,28 @@ Agent.prototype.getName = function(options) {
100126
return name;
101127
};
102128

129+
Agent.prototype._getSession = function _getSession(key) {
130+
return this._sessionCache.map[key];
131+
};
132+
133+
Agent.prototype._cacheSession = function _cacheSession(key, session) {
134+
// Fast case - update existing entry
135+
if (this._sessionCache.map[key]) {
136+
this._sessionCache.map[key] = session;
137+
return;
138+
}
139+
140+
// Put new entry
141+
if (this._sessionCache.list.length >= this.maxCachedSessions) {
142+
const oldKey = this._sessionCache.list.shift();
143+
debug('evicting %j', oldKey);
144+
delete this._sessionCache.map[oldKey];
145+
}
146+
147+
this._sessionCache.list.push(key);
148+
this._sessionCache.map[key] = session;
149+
};
150+
103151
const globalAgent = new Agent();
104152

105153
exports.globalAgent = globalAgent;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
'use strict';
2+
var common = require('../common');
3+
var assert = require('assert');
4+
5+
if (!common.hasCrypto) {
6+
console.log('1..0 # Skipped: missing crypto');
7+
return;
8+
}
9+
10+
var https = require('https');
11+
var crypto = require('crypto');
12+
13+
var fs = require('fs');
14+
15+
var options = {
16+
key: fs.readFileSync(common.fixturesDir + '/keys/agent1-key.pem'),
17+
cert: fs.readFileSync(common.fixturesDir + '/keys/agent1-cert.pem')
18+
};
19+
20+
var ca = fs.readFileSync(common.fixturesDir + '/keys/ca1-cert.pem');
21+
22+
var clientSessions = {};
23+
var serverRequests = 0;
24+
25+
var agent = new https.Agent({
26+
maxCachedSessions: 1
27+
});
28+
29+
var server = https.createServer(options, function(req, res) {
30+
if (req.url === '/drop-key')
31+
server.setTicketKeys(crypto.randomBytes(48));
32+
33+
serverRequests++;
34+
res.end('ok');
35+
}).listen(common.PORT, function() {
36+
var queue = [
37+
{
38+
name: 'first',
39+
40+
method: 'GET',
41+
path: '/',
42+
servername: 'agent1',
43+
ca: ca,
44+
port: common.PORT
45+
},
46+
{
47+
name: 'first-reuse',
48+
49+
method: 'GET',
50+
path: '/',
51+
servername: 'agent1',
52+
ca: ca,
53+
port: common.PORT
54+
},
55+
{
56+
name: 'cipher-change',
57+
58+
method: 'GET',
59+
path: '/',
60+
servername: 'agent1',
61+
62+
// Choose different cipher to use different cache entry
63+
ciphers: 'AES256-SHA',
64+
ca: ca,
65+
port: common.PORT
66+
},
67+
// Change the ticket key to ensure session is updated in cache
68+
{
69+
name: 'before-drop',
70+
71+
method: 'GET',
72+
path: '/drop-key',
73+
servername: 'agent1',
74+
ca: ca,
75+
port: common.PORT
76+
},
77+
78+
// Ticket will be updated starting from this
79+
{
80+
name: 'after-drop',
81+
82+
method: 'GET',
83+
path: '/',
84+
servername: 'agent1',
85+
ca: ca,
86+
port: common.PORT
87+
},
88+
{
89+
name: 'after-drop-reuse',
90+
91+
method: 'GET',
92+
path: '/',
93+
servername: 'agent1',
94+
ca: ca,
95+
port: common.PORT
96+
}
97+
];
98+
99+
function request() {
100+
var options = queue.shift();
101+
options.agent = agent;
102+
https.request(options, function(res) {
103+
clientSessions[options.name] = res.socket.getSession();
104+
105+
res.resume();
106+
res.on('end', function() {
107+
if (queue.length !== 0)
108+
return request();
109+
server.close();
110+
});
111+
}).end();
112+
}
113+
request();
114+
});
115+
116+
process.on('exit', function() {
117+
assert.equal(serverRequests, 6);
118+
assert.equal(clientSessions['first'].toString('hex'),
119+
clientSessions['first-reuse'].toString('hex'));
120+
assert.notEqual(clientSessions['first'].toString('hex'),
121+
clientSessions['cipher-change'].toString('hex'));
122+
assert.notEqual(clientSessions['first'].toString('hex'),
123+
clientSessions['before-drop'].toString('hex'));
124+
assert.notEqual(clientSessions['cipher-change'].toString('hex'),
125+
clientSessions['before-drop'].toString('hex'));
126+
assert.notEqual(clientSessions['before-drop'].toString('hex'),
127+
clientSessions['after-drop'].toString('hex'));
128+
assert.equal(clientSessions['after-drop'].toString('hex'),
129+
clientSessions['after-drop-reuse'].toString('hex'));
130+
});

0 commit comments

Comments
 (0)