tls: introduce asynchronous `newSession`
authorFedor Indutny <fedor.indutny@gmail.com>
Fri, 14 Feb 2014 13:01:34 +0000 (17:01 +0400)
committerFedor Indutny <fedor.indutny@gmail.com>
Mon, 17 Feb 2014 21:07:09 +0000 (01:07 +0400)
fix #7105

doc/api/tls.markdown
lib/_tls_legacy.js
lib/_tls_wrap.js
src/env.h
src/node_crypto.cc
src/node_crypto.h
src/tls_wrap.cc
src/tls_wrap.h
test/simple/test-tls-session-cache.js

index 2a4c312..372bbd9 100644 (file)
@@ -484,10 +484,11 @@ established - it will be forwarded here.
 
 ### Event: 'newSession'
 
-`function (sessionId, sessionData) { }`
+`function (sessionId, sessionData, callback) { }`
 
 Emitted on creation of TLS session. May be used to store sessions in external
-storage.
+storage. `callback` must be invoked eventually, otherwise no data will be
+sent or received from secure connection.
 
 NOTE: adding this event listener will have an effect only on connections
 established after addition of event listener.
index be3c8af..6916c1e 100644 (file)
@@ -653,7 +653,27 @@ function onclienthello(hello) {
 
 function onnewsession(key, session) {
   if (!this.server) return;
-  this.server.emit('newSession', key, session);
+
+  var self = this;
+  var once = false;
+
+  self.server.emit('newSession', key, session, function() {
+    if (once)
+      return;
+    once = true;
+
+    if (self.ssl)
+      self.ssl.newSessionDone();
+  });
+}
+
+
+function onnewsessiondone() {
+  if (!this.server) return;
+
+  // Cycle through data
+  this.cleartext.read(0);
+  this.encrypted.read(0);
 }
 
 
index 3cda50d..c0af83a 100644 (file)
@@ -138,8 +138,25 @@ function onclienthello(hello) {
 
 
 function onnewsession(key, session) {
-  if (this.server)
-    this.server.emit('newSession', key, session);
+  if (!this.server)
+    return;
+
+  var self = this;
+  var once = false;
+
+  this._newSessionPending = true;
+  this.server.emit('newSession', key, session, function() {
+    if (once)
+      return;
+    once = true;
+
+    self.ssl.newSessionDone();
+
+    self._newSessionPending = false;
+    if (self._securePending)
+      self._finishInit();
+    self._securePending = false;
+  });
 }
 
 
@@ -164,6 +181,8 @@ function TLSSocket(socket, options) {
 
   this._tlsOptions = options;
   this._secureEstablished = false;
+  this._securePending = false;
+  this._newSessionPending = false;
   this._controlReleased = false;
   this._SNICallback = null;
   this.ssl = null;
@@ -347,6 +366,12 @@ TLSSocket.prototype._releaseControl = function() {
 };
 
 TLSSocket.prototype._finishInit = function() {
+  // `newSession` callback wasn't called yet
+  if (this._newSessionPending) {
+    this._securePending = true;
+    return;
+  }
+
   if (process.features.tls_npn) {
     this.npnProtocol = this.ssl.getNegotiatedProtocol();
   }
index f1cd4a1..99f0e20 100644 (file)
--- a/src/env.h
+++ b/src/env.h
@@ -121,6 +121,7 @@ namespace node {
   V(onhandshakestart_string, "onhandshakestart")                              \
   V(onmessage_string, "onmessage")                                            \
   V(onnewsession_string, "onnewsession")                                      \
+  V(onnewsessiondone_string, "onnewsessiondone")                              \
   V(onread_string, "onread")                                                  \
   V(onselect_string, "onselect")                                              \
   V(onsignal_string, "onsignal")                                              \
index 1d5fd25..d0a26a6 100644 (file)
@@ -857,6 +857,7 @@ void SSLWrap<Base>::AddMethods(Handle<FunctionTemplate> t) {
   NODE_SET_PROTOTYPE_METHOD(t, "renegotiate", Renegotiate);
   NODE_SET_PROTOTYPE_METHOD(t, "shutdown", Shutdown);
   NODE_SET_PROTOTYPE_METHOD(t, "getTLSTicket", GetTLSTicket);
+  NODE_SET_PROTOTYPE_METHOD(t, "newSessionDone", NewSessionDone);
 
 #ifdef SSL_set_max_send_fragment
   NODE_SET_PROTOTYPE_METHOD(t, "setMaxSendFragment", SetMaxSendFragment);
@@ -929,6 +930,7 @@ int SSLWrap<Base>::NewSessionCallback(SSL* s, SSL_SESSION* sess) {
                                       reinterpret_cast<char*>(sess->session_id),
                                       sess->session_id_length);
   Local<Value> argv[] = { session, buff };
+  w->new_session_wait_ = true;
   w->MakeCallback(env->onnewsession_string(), ARRAY_SIZE(argv), argv);
 
   return 0;
@@ -1267,6 +1269,16 @@ void SSLWrap<Base>::GetTLSTicket(const FunctionCallbackInfo<Value>& args) {
 }
 
 
+template <class Base>
+void SSLWrap<Base>::NewSessionDone(const FunctionCallbackInfo<Value>& args) {
+  HandleScope scope(args.GetIsolate());
+
+  Base* w = Unwrap<Base>(args.This());
+  w->new_session_wait_ = false;
+  w->NewSessionDoneCb();
+}
+
+
 #ifdef SSL_set_max_send_fragment
 template <class Base>
 void SSLWrap<Base>::SetMaxSendFragment(
@@ -1651,6 +1663,13 @@ void Connection::SetShutdownFlags() {
 }
 
 
+void Connection::NewSessionDoneCb() {
+  HandleScope scope(env()->isolate());
+
+  MakeCallback(env()->onnewsessiondone_string(), 0, NULL);
+}
+
+
 void Connection::Initialize(Environment* env, Handle<Object> target) {
   Local<FunctionTemplate> t = FunctionTemplate::New(Connection::New);
   t->InstanceTemplate()->SetInternalFieldCount(1);
index 889d270..729d4fc 100644 (file)
@@ -137,7 +137,8 @@ class SSLWrap {
       : env_(env),
         kind_(kind),
         next_sess_(NULL),
-        session_callbacks_(false) {
+        session_callbacks_(false),
+        new_session_wait_(false) {
     ssl_ = SSL_new(sc->ctx_);
     assert(ssl_ != NULL);
   }
@@ -162,6 +163,7 @@ class SSLWrap {
   inline void enable_session_callbacks() { session_callbacks_ = true; }
   inline bool is_server() const { return kind_ == kServer; }
   inline bool is_client() const { return kind_ == kClient; }
+  inline bool is_waiting_new_session() const { return new_session_wait_; }
 
  protected:
   static void InitNPN(SecureContext* sc, Base* base);
@@ -188,6 +190,7 @@ class SSLWrap {
   static void Renegotiate(const v8::FunctionCallbackInfo<v8::Value>& args);
   static void Shutdown(const v8::FunctionCallbackInfo<v8::Value>& args);
   static void GetTLSTicket(const v8::FunctionCallbackInfo<v8::Value>& args);
+  static void NewSessionDone(const v8::FunctionCallbackInfo<v8::Value>& args);
 
 #ifdef SSL_set_max_send_fragment
   static void SetMaxSendFragment(
@@ -219,6 +222,7 @@ class SSLWrap {
   SSL_SESSION* next_sess_;
   SSL* ssl_;
   bool session_callbacks_;
+  bool new_session_wait_;
   ClientHelloParser hello_parser_;
 
 #ifdef OPENSSL_NPN_NEGOTIATED
@@ -291,6 +295,7 @@ class Connection : public SSLWrap<Connection>, public AsyncWrap {
 
   void ClearError();
   void SetShutdownFlags();
+  void NewSessionDoneCb();
 
   Connection(Environment* env,
              v8::Local<v8::Object> wrap,
@@ -319,6 +324,7 @@ class Connection : public SSLWrap<Connection>, public AsyncWrap {
 
   friend class ClientHelloParser;
   friend class SecureContext;
+  friend class SSLWrap<Connection>;
 };
 
 class CipherBase : public BaseObject {
index 4e5b07f..0e63444 100644 (file)
@@ -81,6 +81,7 @@ TLSCallbacks::TLSCallbacks(Environment* env,
       established_(false),
       shutdown_(false),
       error_(NULL),
+      cycle_depth_(0),
       eof_(false) {
   node::Wrap<TLSCallbacks>(object(), this);
 
@@ -158,6 +159,11 @@ bool TLSCallbacks::InvokeQueued(int status) {
 }
 
 
+void TLSCallbacks::NewSessionDoneCb() {
+  Cycle();
+}
+
+
 void TLSCallbacks::InitSSL() {
   // Initialize SSL
   enc_in_ = NodeBIO::New();
@@ -309,6 +315,10 @@ void TLSCallbacks::EncOut() {
   if (write_size_ != 0)
     return;
 
+  // Wait for `newSession` callback to be invoked
+  if (is_waiting_new_session())
+    return;
+
   // Split-off queue
   if (established_ && !QUEUE_EMPTY(&write_item_queue_))
     MakePending();
index 946cc1c..646e701 100644 (file)
@@ -102,11 +102,18 @@ class TLSCallbacks : public crypto::SSLWrap<TLSCallbacks>,
   void ClearOut();
   void MakePending();
   bool InvokeQueued(int status);
+  void NewSessionDoneCb();
 
   inline void Cycle() {
-    ClearIn();
-    ClearOut();
-    EncOut();
+    // Prevent recursion
+    if (++cycle_depth_ > 1)
+      return;
+
+    for (; cycle_depth_ > 0; cycle_depth_--) {
+      ClearIn();
+      ClearOut();
+      EncOut();
+    }
   }
 
   v8::Local<v8::Value> GetSSLError(int status, int* err, const char** msg);
@@ -144,6 +151,7 @@ class TLSCallbacks : public crypto::SSLWrap<TLSCallbacks>,
   bool established_;
   bool shutdown_;
   const char* error_;
+  int cycle_depth_;
 
   // If true - delivered EOF to the js-land, either after `close_notify`, or
   // after the `UV_EOF` on socket.
@@ -155,6 +163,8 @@ class TLSCallbacks : public crypto::SSLWrap<TLSCallbacks>,
 
   static size_t error_off_;
   static char error_buf_[1024];
+
+  friend class SSLWrap<TLSCallbacks>;
 };
 
 }  // namespace node
index 4619365..a60ff94 100644 (file)
@@ -64,12 +64,16 @@ function doTest(testOptions, callback) {
     ++requestCount;
     cleartext.end();
   });
-  server.on('newSession', function(id, data) {
-    assert.ok(!session);
-    session = {
-      id: id,
-      data: data
-    };
+  server.on('newSession', function(id, data, cb) {
+    // Emulate asynchronous store
+    setTimeout(function() {
+      assert.ok(!session);
+      session = {
+        id: id,
+        data: data
+      };
+      cb();
+    }, 1000);
   });
   server.on('resumeSession', function(id, callback) {
     ++resumeCount;