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;
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;
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);
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;
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;
void TLSCallbacks::ClearOut() {
+ // Ignore cycling data if ClientHello wasn't yet parsed
+ if (hello_.state != kParseEnded)
+ return;
+
HandleScope scope(node_isolate);
assert(ssl_ != NULL);
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;
// 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();
}
}
+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);
}
+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);
}
+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);
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);
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");
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
--- /dev/null
+// 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);
+ }
+ });
+}