Skip to content

Commit 2ca5a3d

Browse files
committed
https: reuse TLS sessions in Agent
Fix: #1499 PR-URL: #2228 Reviewed-By: Shigeki Ohtsu <ohtsu@iij.ad.jp> Reviewed-By: Trevor Norris <trev.norris@gmail.com>
1 parent 4e78cd7 commit 2ca5a3d

4 files changed

Lines changed: 193 additions & 2 deletions

File tree

‎lib/_http_agent.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ Agent.prototype.createSocket = function(req, options) {
171171
}
172172

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

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

‎lib/_tls_wrap.js‎

Lines changed: 13 additions & 1 deletion
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+
returnfalse;
590+
591+
varnext=this.getSession();
592+
if(!next)
593+
returnfalse;
594+
595+
returnnext.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
varverifyError=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
varcert=socket.getPeerCertificate();
10041016
verifyError=options.checkServerIdentity(hostname,cert);
10051017
}

‎lib/https.js‎

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

6060
debug('createConnection',options);
61-
returntls.connect(options);
61+
62+
if(options._agentKey){
63+
constsession=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+
constself=this;
73+
constsocket=tls.connect(options,function(){
74+
if(!options._agentKey)
75+
return;
76+
77+
self._cacheSession(options._agentKey,socket.getSession());
78+
});
79+
returnsocket;
6280
}
6381

6482

6583
functionAgent(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
returnname;
101127
};
102128

129+
Agent.prototype._getSession=function_getSession(key){
130+
returnthis._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+
constoldKey=this._sessionCache.list.shift();
143+
debug('evicting %j',oldKey);
144+
deletethis._sessionCache.map[oldKey];
145+
}
146+
147+
this._sessionCache.list.push(key);
148+
this._sessionCache.map[key]=session;
149+
};
150+
103151
constglobalAgent=newAgent();
104152

105153
exports.globalAgent=globalAgent;
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
'use strict';
2+
varcommon=require('../common');
3+
varassert=require('assert');
4+
5+
if(!common.hasCrypto){
6+
console.log('1..0 # Skipped: missing crypto');
7+
return;
8+
}
9+
10+
varhttps=require('https');
11+
varcrypto=require('crypto');
12+
13+
varfs=require('fs');
14+
15+
varoptions={
16+
key: fs.readFileSync(common.fixturesDir+'/keys/agent1-key.pem'),
17+
cert: fs.readFileSync(common.fixturesDir+'/keys/agent1-cert.pem')
18+
};
19+
20+
varca=fs.readFileSync(common.fixturesDir+'/keys/ca1-cert.pem');
21+
22+
varclientSessions={};
23+
varserverRequests=0;
24+
25+
varagent=newhttps.Agent({
26+
maxCachedSessions: 1
27+
});
28+
29+
varserver=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+
varqueue=[
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+
functionrequest(){
100+
varoptions=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+
returnrequest();
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)