Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

tls: async session storage

  • Loading branch information...
commit 8e0c830cd0038577e36456d3e027a4150d68c933 1 parent 790d651
@indutny indutny authored
View
19 doc/api/tls.markdown
@@ -373,6 +373,25 @@ When a client connection emits an 'error' event before secure connection is
established - it will be forwarded here.
+### Event: 'newSession'
+
+`function (sessionId, sessionData) { }`
+
+Emitted on creation of TLS session. May be used to store sessions in external
+storage.
+
+
+### 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.
+
+
### server.listen(port, [host], [callback])
Begin accepting connections on the specified `port` and `host`. If the
View
42 lib/tls.js
@@ -725,6 +725,37 @@ function onhandshakedone() {
debug('onhandshakedone');
}
+function onclienthello(hello) {
+ var self = this,
+ once = false;
+
+ this.encrypted.pause();
+ this.cleartext.pause();
+ function callback(err, session) {
+ if (once) return;
+ once = true;
+
+ if (err) return self.socket.destroy(err);
+
+ self.ssl.loadSession(session);
+
+ self.encrypted.resume();
+ self.cleartext.resume();
+ }
+
+ if (hello.sessionId.length <= 0 ||
+ !this.server ||
+ !this.server.emit('resumeSession', hello.sessionId, callback)) {
+ callback(null, null);
+ }
+}
+
+
+function onnewsession(key, session) {
+ if (!this.server) return;
+ this.server.emit('newSession', key, session);
+}
+
/**
* Provides a pair of streams to do encrypted communication.
@@ -746,6 +777,7 @@ function SecurePair(credentials, isServer, requestCert, rejectUnauthorized,
events.EventEmitter.call(this);
+ this.server = options.server;
this._secureEstablished = false;
this._isServer = isServer ? true : false;
this._encWriteState = true;
@@ -768,13 +800,16 @@ function SecurePair(credentials, isServer, requestCert, rejectUnauthorized,
this._requestCert = requestCert ? true : false;
this.ssl = new Connection(this.credentials.context,
- this._isServer ? true : false,
- this._isServer ? this._requestCert : options.servername,
- this._rejectUnauthorized);
+ this._isServer ? true : false,
+ this._isServer ? this._requestCert :
+ options.servername,
+ this._rejectUnauthorized);
if (this._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.handshakes = 0;
this.ssl.timer = null;
}
@@ -1084,6 +1119,7 @@ function Server(/* [options], listener */) {
self.requestCert,
self.rejectUnauthorized,
{
+ server: self,
NPNProtocols: self.NPNProtocols,
SNICallback: self.SNICallback
});
View
250 src/node_crypto.cc
@@ -84,6 +84,9 @@ static Persistent<String> version_symbol;
static Persistent<String> ext_key_usage_symbol;
static Persistent<String> onhandshakestart_sym;
static Persistent<String> onhandshakedone_sym;
+static Persistent<String> onclienthello_sym;
+static Persistent<String> onnewsession_sym;
+static Persistent<String> sessionid_sym;
static Persistent<FunctionTemplate> secure_context_constructor;
@@ -221,15 +224,71 @@ Handle<Value> SecureContext::Init(const Arguments& args) {
}
sc->ctx_ = SSL_CTX_new(method);
- // Enable session caching?
- SSL_CTX_set_session_cache_mode(sc->ctx_, SSL_SESS_CACHE_SERVER);
- // SSL_CTX_set_session_cache_mode(sc->ctx_,SSL_SESS_CACHE_OFF);
+
+ // SSL session cache configuration
+ SSL_CTX_set_session_cache_mode(sc->ctx_,
+ SSL_SESS_CACHE_SERVER |
+ SSL_SESS_CACHE_NO_INTERNAL |
+ SSL_SESS_CACHE_NO_AUTO_CLEAR);
+ SSL_CTX_sess_set_get_cb(sc->ctx_, GetSessionCallback);
+ SSL_CTX_sess_set_new_cb(sc->ctx_, NewSessionCallback);
sc->ca_store_ = NULL;
return True();
}
+SSL_SESSION* SecureContext::GetSessionCallback(SSL* s,
+ unsigned char* key,
+ int len,
+ int* copy) {
+ HandleScope scope;
+
+ Connection* p = static_cast<Connection*>(SSL_get_app_data(s));
+
+ *copy = 0;
+ SSL_SESSION* sess = p->next_sess_;
+ p->next_sess_ = NULL;
+
+ return sess;
+}
+
+
+void SessionDataFree(char* data, void* hint) {
+ delete[] data;
+}
+
+
+int SecureContext::NewSessionCallback(SSL* s, SSL_SESSION* sess) {
+ HandleScope scope;
+
+ Connection* p = static_cast<Connection*>(SSL_get_app_data(s));
+
+ // Check if session is small enough to be stored
+ int size = i2d_SSL_SESSION(sess, NULL);
+ if (size > kMaxSessionSize) return 0;
+
+ // Serialize session
+ char* serialized = new char[size];
+ unsigned char* pserialized = reinterpret_cast<unsigned char*>(serialized);
+ memset(serialized, 0, size);
+ i2d_SSL_SESSION(sess, &pserialized);
+
+ Handle<Value> argv[2] = {
+ Buffer::New(reinterpret_cast<char*>(sess->session_id),
+ sess->session_id_length)->handle_,
+ Buffer::New(serialized, size, SessionDataFree, NULL)->handle_
+ };
+
+ if (onnewsession_sym.IsEmpty()) {
+ onnewsession_sym = NODE_PSYMBOL("onnewsession");
+ }
+ MakeCallback(p->handle_, onnewsession_sym, ARRAY_SIZE(argv), argv);
+
+ return 0;
+}
+
+
// Takes a string or buffer and loads it into a BIO.
// Caller responsible for BIO_free-ing the returned object.
static BIO* LoadBIO (Handle<Value> v) {
@@ -667,6 +726,150 @@ Handle<Value> SecureContext::LoadPKCS12(const Arguments& args) {
}
+size_t ClientHelloParser::Write(const uint8_t* data, size_t len) {
+ HandleScope scope;
+
+ // Just accumulate data, everything will be pushed to BIO later
+ if (state_ == kPaused) return 0;
+
+ // Copy incoming data to the internal buffer
+ // (which has a size of the biggest possible TLS frame)
+ size_t available = sizeof(data_) - offset_;
+ size_t copied = len < available ? len : available;
+ memcpy(data_ + offset_, data, copied);
+ offset_ += copied;
+
+ // Vars for parsing hello
+ bool is_clienthello = false;
+ uint8_t session_size = -1;
+ uint8_t* session_id = NULL;
+ Local<Object> hello;
+ Handle<Value> argv[1];
+
+ switch (state_) {
+ case kWaiting:
+ // >= 5 bytes for header parsing
+ if (offset_ < 5) break;
+
+ if (data_[0] == kChangeCipherSpec || data_[0] == kAlert ||
+ data_[0] == kHandshake || data_[0] == kApplicationData) {
+ frame_len_ = (data_[3] << 8) + data_[4];
+ state_ = kTLSHeader;
+ body_offset_ = 5;
+ } else {
+ frame_len_ = (data_[0] << 8) + data_[1];
+ state_ = kSSLHeader;
+ if (*data_ & 0x40) {
+ // header with padding
+ body_offset_ = 3;
+ } else {
+ // without padding
+ body_offset_ = 2;
+ }
+ }
+
+ // Sanity check (too big frame, or too small)
+ if (frame_len_ >= sizeof(data_)) {
+ // Let OpenSSL handle it
+ Finish();
+ return copied;
+ }
+ case kTLSHeader:
+ case kSSLHeader:
+ // >= 5 + frame size bytes for frame parsing
+ if (offset_ < body_offset_ + frame_len_) break;
+
+ // Skip unsupported frames and gather some data from frame
+
+ // TODO: Check protocol version
+ if (data_[body_offset_] == kClientHello) {
+ is_clienthello = true;
+ uint8_t* body;
+ size_t session_offset;
+
+ if (state_ == kTLSHeader) {
+ // Skip frame header, hello header, protocol version and random data
+ session_offset = body_offset_ + 4 + 2 + 32;
+
+ if (session_offset + 1 < offset_) {
+ body = data_ + session_offset;
+ session_size = *body;
+ session_id = body + 1;
+ }
+ } else if (state_ == kSSLHeader) {
+ // Skip header, version
+ session_offset = body_offset_ + 3;
+
+ if (session_offset + 4 < offset_) {
+ body = data_ + session_offset;
+
+ int ciphers_size = (body[0] << 8) + body[1];
+
+ if (body + 4 + ciphers_size < data_ + offset_) {
+ 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_ + offset_) {
+ Finish();
+ return copied;
+ }
+
+ // TODO: Parse other things?
+ }
+
+ // Not client hello - let OpenSSL handle it
+ if (!is_clienthello) {
+ Finish();
+ return copied;
+ }
+
+ // Parse frame, call javascript handler and
+ // move parser into the paused state
+ if (onclienthello_sym.IsEmpty()) {
+ onclienthello_sym = NODE_PSYMBOL("onclienthello");
+ }
+ if (sessionid_sym.IsEmpty()) {
+ sessionid_sym = NODE_PSYMBOL("sessionId");
+ }
+
+ state_ = kPaused;
+ hello = Object::New();
+ hello->Set(sessionid_sym,
+ Buffer::New(reinterpret_cast<char*>(session_id),
+ session_size)->handle_);
+
+ argv[0] = hello;
+ MakeCallback(conn_->handle_, onclienthello_sym, 1, argv);
+ break;
+ case kEnded:
+ default:
+ break;
+ }
+
+ return copied;
+}
+
+
+void ClientHelloParser::Finish() {
+ assert(state_ != kEnded);
+ state_ = kEnded;
+
+ // Write all accumulated data
+ int r = BIO_write(conn_->bio_read_, reinterpret_cast<char*>(data_), offset_);
+ conn_->HandleBIOError(conn_->bio_read_, "BIO_write", r);
+ conn_->SetShutdownFlags();
+}
+
+
#ifdef SSL_PRINT_DEBUG
# define DEBUG_PRINT(...) fprintf (stderr, __VA_ARGS__)
#else
@@ -789,6 +992,7 @@ void Connection::Initialize(Handle<Object> target) {
NODE_SET_PROTOTYPE_METHOD(t, "getPeerCertificate", Connection::GetPeerCertificate);
NODE_SET_PROTOTYPE_METHOD(t, "getSession", Connection::GetSession);
NODE_SET_PROTOTYPE_METHOD(t, "setSession", Connection::SetSession);
+ NODE_SET_PROTOTYPE_METHOD(t, "loadSession", Connection::LoadSession);
NODE_SET_PROTOTYPE_METHOD(t, "isSessionReused", Connection::IsSessionReused);
NODE_SET_PROTOTYPE_METHOD(t, "isInitFinished", Connection::IsInitFinished);
NODE_SET_PROTOTYPE_METHOD(t, "verifyError", Connection::VerifyError);
@@ -1112,9 +1316,17 @@ Handle<Value> Connection::EncIn(const Arguments& args) {
String::New("off + len > buffer.length")));
}
- int bytes_written = BIO_write(ss->bio_read_, buffer_data + off, len);
- ss->HandleBIOError(ss->bio_read_, "BIO_write", bytes_written);
- ss->SetShutdownFlags();
+ int bytes_written;
+ char* data = buffer_data + off;
+
+ if (ss->is_server_ && !ss->hello_parser_.ended()) {
+ bytes_written = ss->hello_parser_.Write(reinterpret_cast<uint8_t*>(data),
+ len);
+ } else {
+ bytes_written = BIO_write(ss->bio_read_, data, len);
+ ss->HandleBIOError(ss->bio_read_, "BIO_write", bytes_written);
+ ss->SetShutdownFlags();
+ }
return scope.Close(Integer::New(bytes_written));
}
@@ -1444,7 +1656,7 @@ Handle<Value> Connection::SetSession(const Arguments& args) {
ssize_t wlen = DecodeWrite(sbuf, slen, args[0], BINARY);
assert(wlen == slen);
- const unsigned char* p = (unsigned char*) sbuf;
+ const unsigned char* p = reinterpret_cast<const unsigned char*>(sbuf);
SSL_SESSION* sess = d2i_SSL_SESSION(NULL, &p, wlen);
delete [] sbuf;
@@ -1463,6 +1675,30 @@ Handle<Value> Connection::SetSession(const Arguments& args) {
return True();
}
+Handle<Value> Connection::LoadSession(const Arguments& args) {
+ HandleScope scope;
+
+ Connection *ss = Connection::Unwrap(args);
+
+ if (args.Length() >= 1 && Buffer::HasInstance(args[0])) {
+ ssize_t slen = Buffer::Length(args[0].As<Object>());
+ char* sbuf = Buffer::Data(args[0].As<Object>());
+
+ 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 (ss->next_sess_ != NULL) {
+ SSL_SESSION_free(ss->next_sess_);
+ }
+ ss->next_sess_ = sess;
+ }
+
+ ss->hello_parser_.Finish();
+
+ return True();
+}
+
Handle<Value> Connection::IsSessionReused(const Arguments& args) {
HandleScope scope;
View
71 src/node_crypto.h
@@ -49,6 +49,9 @@ namespace crypto {
static X509_STORE* root_cert_store;
+// Forward declaration
+class Connection;
+
class SecureContext : ObjectWrap {
public:
static void Initialize(v8::Handle<v8::Object> target);
@@ -58,6 +61,8 @@ class SecureContext : ObjectWrap {
X509_STORE *ca_store_;
protected:
+ static const int kMaxSessionSize = 10 * 1024;
+
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);
@@ -71,6 +76,12 @@ class SecureContext : ObjectWrap {
static v8::Handle<v8::Value> Close(const v8::Arguments& args);
static v8::Handle<v8::Value> LoadPKCS12(const v8::Arguments& args);
+ static SSL_SESSION* GetSessionCallback(SSL* s,
+ unsigned char* key,
+ int len,
+ int* copy);
+ static int NewSessionCallback(SSL* s, SSL_SESSION* sess);
+
SecureContext() : ObjectWrap() {
ctx_ = NULL;
ca_store_ = NULL;
@@ -100,6 +111,51 @@ class SecureContext : ObjectWrap {
private:
};
+class ClientHelloParser {
+ public:
+ enum FrameType {
+ kChangeCipherSpec = 20,
+ kAlert = 21,
+ kHandshake = 22,
+ kApplicationData = 23,
+ kOther = 255
+ };
+
+ enum HandshakeType {
+ kClientHello = 1
+ };
+
+ enum ParseState {
+ kWaiting,
+ kTLSHeader,
+ kSSLHeader,
+ kPaused,
+ kEnded
+ };
+
+ ClientHelloParser(Connection* c) : conn_(c),
+ state_(kWaiting),
+ offset_(0),
+ body_offset_(0),
+ written_(0) {
+ }
+
+ size_t Write(const uint8_t* data, size_t len);
+ void Finish();
+
+ inline bool ended() { return state_ == kEnded; }
+
+ private:
+ Connection* conn_;
+ ParseState state_;
+ size_t frame_len_;
+
+ uint8_t data_[18432];
+ size_t offset_;
+ size_t body_offset_;
+ size_t written_;
+};
+
class Connection : ObjectWrap {
public:
static void Initialize(v8::Handle<v8::Object> target);
@@ -126,6 +182,7 @@ class Connection : ObjectWrap {
static v8::Handle<v8::Value> GetPeerCertificate(const v8::Arguments& args);
static v8::Handle<v8::Value> GetSession(const v8::Arguments& args);
static v8::Handle<v8::Value> SetSession(const v8::Arguments& args);
+ static v8::Handle<v8::Value> LoadSession(const v8::Arguments& args);
static v8::Handle<v8::Value> IsSessionReused(const v8::Arguments& args);
static v8::Handle<v8::Value> IsInitFinished(const v8::Arguments& args);
static v8::Handle<v8::Value> VerifyError(const v8::Arguments& args);
@@ -168,9 +225,10 @@ class Connection : ObjectWrap {
return ss;
}
- Connection() : ObjectWrap() {
+ Connection() : ObjectWrap(), hello_parser_(this) {
bio_read_ = bio_write_ = NULL;
ssl_ = NULL;
+ next_sess_ = NULL;
}
~Connection() {
@@ -179,6 +237,11 @@ class Connection : ObjectWrap {
ssl_ = NULL;
}
+ if (next_sess_ != NULL) {
+ SSL_SESSION_free(next_sess_);
+ next_sess_ = NULL;
+ }
+
#ifdef OPENSSL_NPN_NEGOTIATED
if (!npnProtos_.IsEmpty()) npnProtos_.Dispose();
if (!selectedNPNProto_.IsEmpty()) selectedNPNProto_.Dispose();
@@ -198,7 +261,13 @@ class Connection : ObjectWrap {
BIO *bio_write_;
SSL *ssl_;
+ ClientHelloParser hello_parser_;
+
bool is_server_; /* coverity[member_decl] */
+ SSL_SESSION* next_sess_;
+
+ friend class ClientHelloParser;
+ friend class SecureContext;
};
void InitCrypto(v8::Handle<v8::Object> target);
View
22 test/simple/test-tls-session-cache.js
@@ -50,18 +50,36 @@ function doTest() {
requestCert: true
};
var requestCount = 0;
+ var session;
var server = tls.createServer(options, function(cleartext) {
++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'
+ '-reconnect',
+ '-no_ticket'
], {
customFds: [0, 1, 2]
});
@@ -72,6 +90,8 @@ function doTest() {
});
process.on('exit', function() {
+ assert.ok(session);
+
// initial request + reconnect requests (5 times)
assert.equal(requestCount, 6);
});
Please sign in to comment.
Something went wrong with that request. Please try again.