tls: session API returns
authorFedor Indutny <fedor.indutny@gmail.com>
Mon, 17 Jun 2013 10:11:13 +0000 (12:11 +0200)
committerFedor Indutny <fedor.indutny@gmail.com>
Mon, 17 Jun 2013 12:00:26 +0000 (14:00 +0200)
doc/api/tls.markdown
lib/_tls_wrap.js
src/node_crypto.h
src/node_crypto_bio.h
src/tls_wrap.cc
src/tls_wrap.h
test/simple/test-tls-session-cache.js [new file with mode: 0644]

index 7c37a6f..fc0d8af 100644 (file)
@@ -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
index 9294634..849c2c4 100644 (file)
@@ -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);
index 2d5500a..c3c4a89 100644 (file)
@@ -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);
index 4794453..dc45fae 100644 (file)
@@ -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:
index 89d2790..c42898d 100644 (file)
@@ -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
index 8650756..b36a5b8 100644 (file)
@@ -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 (file)
index 0000000..fdc4ae1
--- /dev/null
@@ -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);
+    }
+  });
+}