summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/api/tls.markdown25
-rw-r--r--lib/_tls_wrap.js35
-rw-r--r--src/node_crypto.h3
-rw-r--r--src/node_crypto_bio.h4
-rw-r--r--src/tls_wrap.cc265
-rw-r--r--src/tls_wrap.h41
-rw-r--r--test/simple/test-tls-session-cache.js118
7 files changed, 480 insertions, 11 deletions
diff --git a/doc/api/tls.markdown b/doc/api/tls.markdown
index 7c37a6f5d..fc0d8af50 100644
--- a/doc/api/tls.markdown
+++ b/doc/api/tls.markdown
@@ -403,6 +403,31 @@ established - it will be forwarded here.
`tlsSocket` is the [tls.TLSSocket][] that the error originated from.
+### Event: 'newSession'
+
+`function (sessionId, sessionData) { }`
+
+Emitted on creation of TLS session. May be used to store sessions in external
+storage.
+
+NOTE: adding this event listener will have an effect only on connections
+established after addition of event listener.
+
+
+### Event: 'resumeSession'
+
+`function (sessionId, callback) { }`
+
+Emitted when client wants to resume previous TLS session. Event listener may
+perform lookup in external storage using given `sessionId`, and invoke
+`callback(null, sessionData)` once finished. If session can't be resumed
+(i.e. doesn't exist in storage) one may call `callback(null, null)`. Calling
+`callback(err)` will terminate incoming connection and destroy socket.
+
+NOTE: adding this event listener will have an effect only on connections
+established after addition of event listener.
+
+
### server.listen(port, [host], [callback])
Begin accepting connections on the specified `port` and `host`. If the
diff --git a/lib/_tls_wrap.js b/lib/_tls_wrap.js
index 92946345b..849c2c4f5 100644
--- a/lib/_tls_wrap.js
+++ b/lib/_tls_wrap.js
@@ -47,6 +47,33 @@ function onhandshakedone() {
}
+function onclienthello(hello) {
+ var self = this,
+ once = false;
+
+ function callback(err, session) {
+ if (once)
+ return self.destroy(new Error('TLS session callback was called twice'));
+ once = true;
+
+ if (err)
+ return self.destroy(err);
+
+ self.ssl.loadSession(session);
+ }
+
+ if (hello.sessionId.length <= 0 ||
+ !this.server.emit('resumeSession', hello.sessionId, callback)) {
+ callback(null, null);
+ }
+}
+
+
+function onnewsession(key, session) {
+ this.server.emit('newSession', key, session);
+}
+
+
/**
* Provides a wrap of socket stream to do encrypted communication.
*/
@@ -92,6 +119,7 @@ TLSSocket.prototype._init = function() {
// Wrap socket's handle
var credentials = options.credentials || crypto.createCredentials();
this.ssl = tls_wrap.wrap(this._handle, credentials.context, options.isServer);
+ this.server = options.server || null;
// For clients, we will always have either a given ca list or be using
// default one
@@ -104,8 +132,15 @@ TLSSocket.prototype._init = function() {
if (options.isServer) {
this.ssl.onhandshakestart = onhandshakestart.bind(this);
this.ssl.onhandshakedone = onhandshakedone.bind(this);
+ this.ssl.onclienthello = onclienthello.bind(this);
+ this.ssl.onnewsession = onnewsession.bind(this);
this.ssl.lastHandshakeTime = 0;
this.ssl.handshakes = 0;
+
+ if (this.server.listeners('resumeSession').length > 0 ||
+ this.server.listeners('newSession').length > 0) {
+ this.ssl.enableSessionCallbacks();
+ }
} else {
this.ssl.onhandshakestart = function() {};
this.ssl.onhandshakedone = this._finishInit.bind(this);
diff --git a/src/node_crypto.h b/src/node_crypto.h
index 2d5500a4e..c3c4a89a8 100644
--- a/src/node_crypto.h
+++ b/src/node_crypto.h
@@ -60,9 +60,10 @@ class SecureContext : ObjectWrap {
// TODO: ca_store_ should probably be removed, it's not used anywhere.
X509_STORE *ca_store_;
- protected:
static const int kMaxSessionSize = 10 * 1024;
+ protected:
+
static v8::Handle<v8::Value> New(const v8::Arguments& args);
static v8::Handle<v8::Value> Init(const v8::Arguments& args);
static v8::Handle<v8::Value> SetKey(const v8::Arguments& args);
diff --git a/src/node_crypto_bio.h b/src/node_crypto_bio.h
index 4794453ef..dc45faee2 100644
--- a/src/node_crypto_bio.h
+++ b/src/node_crypto_bio.h
@@ -87,7 +87,9 @@ class NodeBIO {
}
protected:
- static const size_t kBufferLength = 16 * 1024;
+ // NOTE: Size is maximum TLS frame length, this is required if we want
+ // to fit whole ClientHello into one Buffer of NodeBIO.
+ static const size_t kBufferLength = 16 * 1024 + 5;
class Buffer {
public:
diff --git a/src/tls_wrap.cc b/src/tls_wrap.cc
index 89d2790d7..c42898de8 100644
--- a/src/tls_wrap.cc
+++ b/src/tls_wrap.cc
@@ -36,6 +36,8 @@ static Persistent<String> onerror_sym;
static Persistent<String> onsniselect_sym;
static Persistent<String> onhandshakestart_sym;
static Persistent<String> onhandshakedone_sym;
+static Persistent<String> onclienthello_sym;
+static Persistent<String> onnewsession_sym;
static Persistent<String> subject_sym;
static Persistent<String> subjectaltname_sym;
static Persistent<String> modulus_sym;
@@ -47,6 +49,7 @@ static Persistent<String> fingerprint_sym;
static Persistent<String> name_sym;
static Persistent<String> version_sym;
static Persistent<String> ext_key_usage_sym;
+static Persistent<String> sessionid_sym;
static Persistent<Function> tlsWrap;
@@ -69,7 +72,9 @@ TLSCallbacks::TLSCallbacks(Kind kind,
pending_write_item_(NULL),
started_(false),
established_(false),
- shutdown_(false) {
+ shutdown_(false),
+ session_callbacks_(false),
+ next_sess_(NULL) {
// Persist SecureContext
sc_ = ObjectWrap::Unwrap<SecureContext>(sc);
@@ -78,17 +83,68 @@ TLSCallbacks::TLSCallbacks(Kind kind,
handle_ = Persistent<Object>::New(node_isolate, tlsWrap->NewInstance());
handle_->SetAlignedPointerInInternalField(0, this);
- // No session cache support
- SSL_CTX_sess_set_get_cb(sc_->ctx_, NULL);
- SSL_CTX_sess_set_new_cb(sc_->ctx_, NULL);
-
// Initialize queue for clearIn writes
QUEUE_INIT(&write_item_queue_);
+ // Initialize hello parser
+ hello_.state = kParseEnded;
+ hello_.frame_len = 0;
+ hello_.body_offset = 0;
+
+ // We've our own session callbacks
+ SSL_CTX_sess_set_get_cb(sc_->ctx_, GetSessionCallback);
+ SSL_CTX_sess_set_new_cb(sc_->ctx_, NewSessionCallback);
+
InitSSL();
}
+SSL_SESSION* TLSCallbacks::GetSessionCallback(SSL* s,
+ unsigned char* key,
+ int len,
+ int* copy) {
+ HandleScope scope(node_isolate);
+
+ TLSCallbacks* c = static_cast<TLSCallbacks*>(SSL_get_app_data(s));
+
+ *copy = 0;
+ SSL_SESSION* sess = c->next_sess_;
+ c->next_sess_ = NULL;
+
+ return sess;
+}
+
+
+int TLSCallbacks::NewSessionCallback(SSL* s, SSL_SESSION* sess) {
+ HandleScope scope(node_isolate);
+
+ TLSCallbacks* c = static_cast<TLSCallbacks*>(SSL_get_app_data(s));
+ if (!c->session_callbacks_)
+ return 0;
+
+ // Check if session is small enough to be stored
+ int size = i2d_SSL_SESSION(sess, NULL);
+ if (size > SecureContext::kMaxSessionSize)
+ return 0;
+
+ // Serialize session
+ Local<Object> buff = Local<Object>::New(Buffer::New(size)->handle_);
+ unsigned char* serialized = reinterpret_cast<unsigned char*>(
+ Buffer::Data(buff));
+ memset(serialized, 0, size);
+ i2d_SSL_SESSION(sess, &serialized);
+
+ Local<Object> session = Local<Object>::New(
+ Buffer::New(reinterpret_cast<char*>(sess->session_id),
+ sess->session_id_length)->handle_);
+ Handle<Value> argv[2] = { session, buff };
+
+ MakeCallback(c->handle_, onnewsession_sym, ARRAY_SIZE(argv), argv);
+
+ return 0;
+}
+
+
TLSCallbacks::~TLSCallbacks() {
SSL_free(ssl_);
ssl_ = NULL;
@@ -306,6 +362,10 @@ void TLSCallbacks::SSLInfoCallback(const SSL* ssl_, int where, int ret) {
void TLSCallbacks::EncOut() {
+ // Ignore cycling data if ClientHello wasn't yet parsed
+ if (hello_.state != kParseEnded)
+ return;
+
// Write in progress
if (write_size_ != 0)
return;
@@ -406,6 +466,10 @@ Handle<Value> TLSCallbacks::GetSSLError(int status, int* err) {
void TLSCallbacks::ClearOut() {
+ // Ignore cycling data if ClientHello wasn't yet parsed
+ if (hello_.state != kParseEnded)
+ return;
+
HandleScope scope(node_isolate);
assert(ssl_ != NULL);
@@ -436,6 +500,10 @@ void TLSCallbacks::ClearOut() {
bool TLSCallbacks::ClearIn() {
+ // Ignore cycling data if ClientHello wasn't yet parsed
+ if (hello_.state != kParseEnded)
+ return false;
+
HandleScope scope(node_isolate);
int written = 0;
@@ -569,10 +637,12 @@ void TLSCallbacks::DoRead(uv_stream_t* handle,
// Commit read data
NodeBIO::FromBIO(enc_in_)->Commit(nread);
- // Cycle OpenSSL state
- ClearIn();
- ClearOut();
- EncOut();
+ // Parse ClientHello first
+ if (hello_.state != kParseEnded)
+ return ParseClientHello();
+
+ // Cycle OpenSSL's state
+ Cycle();
}
@@ -585,6 +655,138 @@ int TLSCallbacks::DoShutdown(ShutdownWrap* req_wrap, uv_shutdown_cb cb) {
}
+void TLSCallbacks::ParseClientHello() {
+ enum FrameType {
+ kChangeCipherSpec = 20,
+ kAlert = 21,
+ kHandshake = 22,
+ kApplicationData = 23,
+ kOther = 255
+ };
+
+ enum HandshakeType {
+ kClientHello = 1
+ };
+
+ assert(session_callbacks_);
+ HandleScope scope(node_isolate);
+
+ NodeBIO* enc_in = NodeBIO::FromBIO(enc_in_);
+
+ size_t avail = 0;
+ uint8_t* data = reinterpret_cast<uint8_t*>(enc_in->Peek(&avail));
+ assert(avail == 0 || data != NULL);
+
+ // Vars for parsing hello
+ bool is_clienthello = false;
+ uint8_t session_size = -1;
+ uint8_t* session_id = NULL;
+ Local<Object> hello_obj;
+ Handle<Value> argv[1];
+
+ switch (hello_.state) {
+ case kParseWaiting:
+ // >= 5 bytes for header parsing
+ if (avail < 5)
+ break;
+
+ if (data[0] == kChangeCipherSpec ||
+ data[0] == kAlert ||
+ data[0] == kHandshake ||
+ data[0] == kApplicationData) {
+ hello_.frame_len = (data[3] << 8) + data[4];
+ hello_.state = kParseTLSHeader;
+ hello_.body_offset = 5;
+ } else {
+ hello_.frame_len = (data[0] << 8) + data[1];
+ hello_.state = kParseSSLHeader;
+ if (*data & 0x40) {
+ // header with padding
+ hello_.body_offset = 3;
+ } else {
+ // without padding
+ hello_.body_offset = 2;
+ }
+ }
+
+ // Sanity check (too big frame, or too small)
+ // Let OpenSSL handle it
+ if (hello_.frame_len >= kMaxTLSFrameLen)
+ return ParseFinish();
+
+ // Fall through
+ case kParseTLSHeader:
+ case kParseSSLHeader:
+ // >= 5 + frame size bytes for frame parsing
+ if (avail < hello_.body_offset + hello_.frame_len)
+ break;
+
+ // Skip unsupported frames and gather some data from frame
+
+ // TODO(indutny): Check protocol version
+ if (data[hello_.body_offset] == kClientHello) {
+ is_clienthello = true;
+ uint8_t* body;
+ size_t session_offset;
+
+ if (hello_.state == kParseTLSHeader) {
+ // Skip frame header, hello header, protocol version and random data
+ session_offset = hello_.body_offset + 4 + 2 + 32;
+
+ if (session_offset + 1 < avail) {
+ body = data + session_offset;
+ session_size = *body;
+ session_id = body + 1;
+ }
+ } else if (hello_.state == kParseSSLHeader) {
+ // Skip header, version
+ session_offset = hello_.body_offset + 3;
+
+ if (session_offset + 4 < avail) {
+ body = data + session_offset;
+
+ int ciphers_size = (body[0] << 8) + body[1];
+
+ if (body + 4 + ciphers_size < data + avail) {
+ session_size = (body[2] << 8) + body[3];
+ session_id = body + 4 + ciphers_size;
+ }
+ }
+ } else {
+ // Whoa? How did we get here?
+ abort();
+ }
+
+ // Check if we overflowed (do not reply with any private data)
+ if (session_id == NULL ||
+ session_size > 32 ||
+ session_id + session_size > data + avail) {
+ return ParseFinish();
+ }
+
+ // TODO(indutny): Parse other things?
+ }
+
+ // Not client hello - let OpenSSL handle it
+ if (!is_clienthello)
+ return ParseFinish();
+
+ hello_.state = kParsePaused;
+ hello_obj = Object::New();
+ hello_obj->Set(sessionid_sym,
+ Buffer::New(reinterpret_cast<char*>(session_id),
+ session_size)->handle_);
+
+ argv[0] = hello_obj;
+ MakeCallback(handle_, onclienthello_sym, 1, argv);
+ break;
+ case kParseEnded:
+ default:
+ break;
+ }
+}
+
+
#define CASE_X509_ERR(CODE) case X509_V_ERR_##CODE: reason = #CODE; break;
Handle<Value> TLSCallbacks::VerifyError(const Arguments& args) {
HandleScope scope(node_isolate);
@@ -690,6 +892,20 @@ Handle<Value> TLSCallbacks::IsSessionReused(const Arguments& args) {
}
+Handle<Value> TLSCallbacks::EnableSessionCallbacks(const Arguments& args) {
+ HandleScope scope(node_isolate);
+
+ UNWRAP(TLSCallbacks);
+
+ wrap->session_callbacks_ = true;
+ wrap->hello_.state = kParseWaiting;
+ wrap->hello_.frame_len = 0;
+ wrap->hello_.body_offset = 0;
+
+ return scope.Close(Null(node_isolate));
+}
+
+
Handle<Value> TLSCallbacks::GetPeerCertificate(const Arguments& args) {
HandleScope scope(node_isolate);
@@ -879,6 +1095,30 @@ Handle<Value> TLSCallbacks::SetSession(const Arguments& args) {
}
+Handle<Value> TLSCallbacks::LoadSession(const Arguments& args) {
+ HandleScope scope(node_isolate);
+
+ UNWRAP(TLSCallbacks);
+
+ if (args.Length() >= 1 && Buffer::HasInstance(args[0])) {
+ ssize_t slen = Buffer::Length(args[0]);
+ char* sbuf = Buffer::Data(args[0]);
+
+ const unsigned char* p = reinterpret_cast<unsigned char*>(sbuf);
+ SSL_SESSION* sess = d2i_SSL_SESSION(NULL, &p, slen);
+
+ // Setup next session and move hello to the BIO buffer
+ if (wrap->next_sess_ != NULL)
+ SSL_SESSION_free(wrap->next_sess_);
+ wrap->next_sess_ = sess;
+ }
+
+ wrap->ParseFinish();
+
+ return True(node_isolate);
+}
+
+
Handle<Value> TLSCallbacks::GetCurrentCipher(const Arguments& args) {
HandleScope scope(node_isolate);
@@ -1112,10 +1352,14 @@ void TLSCallbacks::Initialize(Handle<Object> target) {
NODE_SET_PROTOTYPE_METHOD(t, "getPeerCertificate", GetPeerCertificate);
NODE_SET_PROTOTYPE_METHOD(t, "getSession", GetSession);
NODE_SET_PROTOTYPE_METHOD(t, "setSession", SetSession);
+ NODE_SET_PROTOTYPE_METHOD(t, "loadSession", LoadSession);
NODE_SET_PROTOTYPE_METHOD(t, "getCurrentCipher", GetCurrentCipher);
NODE_SET_PROTOTYPE_METHOD(t, "verifyError", VerifyError);
NODE_SET_PROTOTYPE_METHOD(t, "setVerifyMode", SetVerifyMode);
NODE_SET_PROTOTYPE_METHOD(t, "isSessionReused", IsSessionReused);
+ NODE_SET_PROTOTYPE_METHOD(t,
+ "enableSessionCallbacks",
+ EnableSessionCallbacks);
#ifdef OPENSSL_NPN_NEGOTIATED
NODE_SET_PROTOTYPE_METHOD(t, "getNegotiatedProtocol", GetNegotiatedProto);
@@ -1134,6 +1378,8 @@ void TLSCallbacks::Initialize(Handle<Object> target) {
onerror_sym = NODE_PSYMBOL("onerror");
onhandshakestart_sym = NODE_PSYMBOL("onhandshakestart");
onhandshakedone_sym = NODE_PSYMBOL("onhandshakedone");
+ onclienthello_sym = NODE_PSYMBOL("onclienthello");
+ onnewsession_sym = NODE_PSYMBOL("onnewsession");
subject_sym = NODE_PSYMBOL("subject");
issuer_sym = NODE_PSYMBOL("issuer");
@@ -1146,6 +1392,7 @@ void TLSCallbacks::Initialize(Handle<Object> target) {
name_sym = NODE_PSYMBOL("name");
version_sym = NODE_PSYMBOL("version");
ext_key_usage_sym = NODE_PSYMBOL("ext_key_usage");
+ sessionid_sym = NODE_PSYMBOL("sessionId");
}
} // namespace node
diff --git a/src/tls_wrap.h b/src/tls_wrap.h
index 86507567b..b36a5b8d4 100644
--- a/src/tls_wrap.h
+++ b/src/tls_wrap.h
@@ -61,7 +61,24 @@ class TLSCallbacks : public StreamWrapCallbacks {
protected:
static const int kClearOutChunkSize = 1024;
+ static const size_t kMaxTLSFrameLen = 16 * 1024 + 5;
+
+ // ClientHello parser types
+ enum ParseState {
+ kParseWaiting,
+ kParseTLSHeader,
+ kParseSSLHeader,
+ kParsePaused,
+ kParseEnded
+ };
+
+ struct HelloState {
+ ParseState state;
+ size_t frame_len;
+ size_t body_offset;
+ };
+ // Write callback queue's item
class WriteItem {
public:
WriteItem(WriteWrap* w, uv_write_cb cb) : w_(w), cb_(cb) {
@@ -86,6 +103,18 @@ class TLSCallbacks : public StreamWrapCallbacks {
bool ClearIn();
void ClearOut();
void InvokeQueued(int status);
+ void ParseClientHello();
+
+ inline void ParseFinish() {
+ hello_.state = kParseEnded;
+ Cycle();
+ }
+
+ inline void Cycle() {
+ ClearIn();
+ ClearOut();
+ EncOut();
+ }
v8::Handle<v8::Value> GetSSLError(int status, int* err);
@@ -99,6 +128,14 @@ class TLSCallbacks : public StreamWrapCallbacks {
static v8::Handle<v8::Value> VerifyError(const v8::Arguments& args);
static v8::Handle<v8::Value> SetVerifyMode(const v8::Arguments& args);
static v8::Handle<v8::Value> IsSessionReused(const v8::Arguments& args);
+ static v8::Handle<v8::Value> EnableSessionCallbacks(const v8::Arguments& args);
+
+ // TLS Session API
+ static SSL_SESSION* GetSessionCallback(SSL* s,
+ unsigned char* key,
+ int len,
+ int* copy);
+ static int NewSessionCallback(SSL* s, SSL_SESSION* sess);
#ifdef OPENSSL_NPN_NEGOTIATED
static v8::Handle<v8::Value> GetNegotiatedProto(const v8::Arguments& args);
@@ -134,9 +171,13 @@ class TLSCallbacks : public StreamWrapCallbacks {
size_t write_queue_size_;
QUEUE write_item_queue_;
WriteItem* pending_write_item_;
+ HelloState hello_;
+ int hello_body_;
bool started_;
bool established_;
bool shutdown_;
+ bool session_callbacks_;
+ SSL_SESSION* next_sess_;
#ifdef OPENSSL_NPN_NEGOTIATED
v8::Persistent<v8::Object> npn_protos_;
diff --git a/test/simple/test-tls-session-cache.js b/test/simple/test-tls-session-cache.js
new file mode 100644
index 000000000..fdc4ae17d
--- /dev/null
+++ b/test/simple/test-tls-session-cache.js
@@ -0,0 +1,118 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+if (!process.versions.openssl) {
+ console.error('Skipping because node compiled without OpenSSL.');
+ process.exit(0);
+}
+require('child_process').exec('openssl version', function(err) {
+ if (err !== null) {
+ console.error('Skipping because openssl command is not available.');
+ process.exit(0);
+ }
+ doTest();
+});
+
+function doTest() {
+ var common = require('../common');
+ var assert = require('assert');
+ var tls = require('tls');
+ var fs = require('fs');
+ var join = require('path').join;
+ var spawn = require('child_process').spawn;
+
+ var keyFile = join(common.fixturesDir, 'agent.key');
+ var certFile = join(common.fixturesDir, 'agent.crt');
+ var key = fs.readFileSync(keyFile);
+ var cert = fs.readFileSync(certFile);
+ var options = {
+ key: key,
+ cert: cert,
+ ca: [cert],
+ requestCert: true
+ };
+ var requestCount = 0;
+ var session;
+ var badOpenSSL = false;
+
+ var server = tls.createServer(options, function(cleartext) {
+ cleartext.on('error', function(er) {
+ // We're ok with getting ECONNRESET in this test, but it's
+ // timing-dependent, and thus unreliable. Any other errors
+ // are just failures, though.
+ if (er.code !== 'ECONNRESET')
+ throw er;
+ });
+ ++requestCount;
+ cleartext.end();
+ });
+ server.on('newSession', function(id, data) {
+ assert.ok(!session);
+ session = {
+ id: id,
+ data: data
+ };
+ });
+ server.on('resumeSession', function(id, callback) {
+ assert.ok(session);
+ assert.equal(session.id.toString('hex'), id.toString('hex'));
+
+ // Just to check that async really works there
+ setTimeout(function() {
+ callback(null, session.data);
+ }, 100);
+ });
+ server.listen(common.PORT, function() {
+ var client = spawn('openssl', [
+ 's_client',
+ '-connect', 'localhost:' + common.PORT,
+ '-key', join(common.fixturesDir, 'agent.key'),
+ '-cert', join(common.fixturesDir, 'agent.crt'),
+ '-reconnect',
+ '-no_ticket'
+ ], {
+ stdio: [ 0, 1, 'pipe' ]
+ });
+ var err = '';
+ client.stderr.setEncoding('utf8');
+ client.stderr.on('data', function(chunk) {
+ err += chunk;
+ });
+ client.on('exit', function(code) {
+ if (/^unknown option/.test(err)) {
+ // using an incompatible version of openssl
+ assert(code);
+ badOpenSSL = true;
+ } else
+ assert.equal(code, 0);
+ server.close();
+ });
+ });
+
+ process.on('exit', function() {
+ if (!badOpenSSL) {
+ assert.ok(session);
+
+ // initial request + reconnect requests (5 times)
+ assert.equal(requestCount, 6);
+ }
+ });
+}