diff --git a/lib/https.js b/lib/https.js index 1036ea3d78..7482f3142a 100644 --- a/lib/https.js +++ b/lib/https.js @@ -23,12 +23,10 @@ var tls = require('tls'); var http = require('http'); var inherits = require('util').inherits; -var NPN_ENABLED = process.binding('constants').NPN_ENABLED; - function Server(opts, requestListener) { if (!(this instanceof Server)) return new Server(opts, requestListener); - if (NPN_ENABLED && !opts.NPNProtocols) { + if (process.features.tls_npn && !opts.NPNProtocols) { opts.NPNProtocols = ['http/1.1', 'http/1.0']; } @@ -64,7 +62,7 @@ Agent.prototype.defaultPort = 443; Agent.prototype._getConnection = function(options, cb) { - if (NPN_ENABLED && !this.options.NPNProtocols) { + if (process.features.tls_npn && !this.options.NPNProtocols) { this.options.NPNProtocols = ['http/1.1', 'http/1.0']; } diff --git a/lib/https2.js b/lib/https2.js index b095ae7cad..c1c9b7109b 100644 --- a/lib/https2.js +++ b/lib/https2.js @@ -23,12 +23,10 @@ var tls = require('tls'); var http = require('http'); var inherits = require('util').inherits; -var NPN_ENABLED = process.binding('constants').NPN_ENABLED; - function Server(opts, requestListener) { if (!(this instanceof Server)) return new Server(opts, requestListener); - if (NPN_ENABLED && !opts.NPNProtocols) { + if (process.features.tls_npn && !opts.NPNProtocols) { opts.NPNProtocols = ['http/1.1', 'http/1.0']; } diff --git a/lib/tls.js b/lib/tls.js index 938d9033e1..d60b32cdb0 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -27,8 +27,6 @@ var stream = require('stream'); var END_OF_FILE = 42; var assert = require('assert').ok; -var NPN_ENABLED = process.binding('constants').NPN_ENABLED; - var debug; if (process.env.NODE_DEBUG && /tls/.test(process.env.NODE_DEBUG)) { debug = function(a) { console.error('TLS:', a); }; @@ -40,7 +38,6 @@ if (process.env.NODE_DEBUG && /tls/.test(process.env.NODE_DEBUG)) { var Connection = null; try { Connection = process.binding('crypto').Connection; - exports.NPN_ENABLED = NPN_ENABLED; } catch (e) { throw new Error('node.js not compiled with openssl crypto support.'); } @@ -478,17 +475,19 @@ EncryptedStream.prototype._pusher = function(pool, offset, length) { */ function SecurePair(credentials, isServer, requestCert, rejectUnauthorized, - NPNProtocols) { + options) { if (!(this instanceof SecurePair)) { return new SecurePair(credentials, isServer, requestCert, rejectUnauthorized, - NPNProtocols); + options); } var self = this; + options || (options = {}); + events.EventEmitter.call(this); this._secureEstablished = false; @@ -514,11 +513,19 @@ function SecurePair(credentials, isServer, requestCert, rejectUnauthorized, this._requestCert = requestCert ? true : false; this.ssl = new Connection(this.credentials.context, - this._isServer ? true : false, this._requestCert, + this._isServer ? true : false, + this._isServer ? this._requestCert : options.servername, this._rejectUnauthorized); - if (NPN_ENABLED && NPNProtocols) { - this.ssl.setNPNProtocols(NPNProtocols); + if (process.features.tls_sni) { + if (this._isServer && options.SNICallback) { + this.ssl.setSNICallback(options.SNICallback); + } + this.servername = null; + } + + if (process.features.tls_npn && options.NPNProtocols) { + this.ssl.setNPNProtocols(options.NPNProtocols); this.npnProtocol = null; } @@ -629,9 +636,14 @@ SecurePair.prototype.cycle = function(depth) { SecurePair.prototype.maybeInitFinished = function() { if (this.ssl && !this._secureEstablished && this.ssl.isInitFinished()) { - if (NPN_ENABLED) { + if (process.features.tls_npn) { this.npnProtocol = this.ssl.getNegotiatedProtocol(); } + + if (process.features.tls_sni) { + this.servername = this.ssl.getServername(); + } + this._secureEstablished = true; debug('secure established'); this.emit('secure'); @@ -789,7 +801,10 @@ function Server(/* [options], listener */) { true, self.requestCert, self.rejectUnauthorized, - self.NPNProtocols); + { + NPNProtocols: self.NPNProtocols, + SNICallback: self.SNICallback + }); var cleartext = pipe(pair, socket); cleartext._controlReleased = false; @@ -797,6 +812,8 @@ function Server(/* [options], listener */) { pair.on('secure', function() { pair.cleartext.authorized = false; pair.cleartext.npnProtocol = pair.npnProtocol; + pair.cleartext.servername = pair.servername; + if (!self.requestCert) { cleartext._controlReleased = true; self.emit('secureConnection', pair.cleartext, pair.encrypted); @@ -858,6 +875,38 @@ Server.prototype.setOptions = function(options) { if (options.secureProtocol) this.secureProtocol = options.secureProtocol; if (options.secureOptions) this.secureOptions = options.secureOptions; if (options.NPNProtocols) convertNPNProtocols(options.NPNProtocols, this); + if (options.SNICallback) { + this.SNICallback = options.SNICallback; + } else { + this.SNICallback = this.SNICallback.bind(this); + } +}; + +// SNI Contexts High-Level API +Server.prototype._contexts = []; +Server.prototype.addContext = function(servername, credentials) { + if (!servername) { + throw 'Servername is required parameter for Server.addContext'; + } + + var re = new RegExp('^' + + servername.replace(/([\.^$+?\-\\[\]{}])/g, '\\$1') + .replace(/\*/g, '.*') + + '$'); + this._contexts.push([re, crypto.createCredentials(credentials).context]); +}; + +Server.prototype.SNICallback = function(servername) { + var ctx; + + this._contexts.some(function(elem) { + if (servername.match(elem[0]) !== null) { + ctx = elem[1]; + return true; + } + }); + + return ctx; }; @@ -902,7 +951,10 @@ exports.connect = function(port /* host, options, cb */) { convertNPNProtocols(options.NPNProtocols, this); var pair = new SecurePair(sslcontext, false, true, false, - this.NPNProtocols); + { + NPNProtocols: this.NPNProtocols, + servername: options.servername + }); var cleartext = pipe(pair, socket); diff --git a/src/node.cc b/src/node.cc index fed828598f..6c63c9f5d4 100644 --- a/src/node.cc +++ b/src/node.cc @@ -142,6 +142,18 @@ static bool use_uv = true; // disabled by default for now static bool use_http2 = false; +#ifdef OPENSSL_NPN_NEGOTIATED +static bool use_npn = true; +#else +static bool use_npn = false; +#endif + +#ifdef SSL_CTRL_SET_TLSEXT_SERVERNAME_CB +static bool use_sni = true; +#else +static bool use_sni = false; +#endif + // Buffer for getpwnam_r(), getgrpam_r() and other misc callers; keep this // scoped at file-level rather than method-level to avoid excess stack usage. static char getbuf[PATH_MAX + 1]; @@ -2031,6 +2043,8 @@ static Handle GetFeatures() { obj->Set(String::NewSymbol("uv"), Boolean::New(use_uv)); obj->Set(String::NewSymbol("http2"), Boolean::New(use_http2)); obj->Set(String::NewSymbol("ipv6"), True()); // TODO ping libuv + obj->Set(String::NewSymbol("tls_npn"), Boolean::New(use_npn)); + obj->Set(String::NewSymbol("tls_sni"), Boolean::New(use_sni)); obj->Set(String::NewSymbol("tls"), Boolean::New(get_builtin_module("crypto") != NULL)); diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 05a1112212..f07bf785b2 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -61,11 +61,14 @@ static Persistent name_symbol; static Persistent version_symbol; static Persistent ext_key_usage_symbol; +static Persistent secure_context_constructor; void SecureContext::Initialize(Handle target) { HandleScope scope; Local t = FunctionTemplate::New(SecureContext::New); + secure_context_constructor = Persistent::New(t); + t->InstanceTemplate()->SetInternalFieldCount(1); t->SetClassName(String::NewSymbol("SecureContext")); @@ -585,6 +588,12 @@ void Connection::Initialize(Handle target) { NODE_SET_PROTOTYPE_METHOD(t, "setNPNProtocols", Connection::SetNPNProtocols); #endif + +#ifdef SSL_CTRL_SET_TLSEXT_SERVERNAME_CB + NODE_SET_PROTOTYPE_METHOD(t, "getServername", Connection::GetServername); + NODE_SET_PROTOTYPE_METHOD(t, "setSNICallback", Connection::SetSNICallback); +#endif + target->Set(String::NewSymbol("Connection"), t->GetFunction()); } @@ -704,6 +713,56 @@ int Connection::SelectNextProtoCallback_(SSL *s, } #endif +#ifdef SSL_CTRL_SET_TLSEXT_SERVERNAME_CB +int Connection::SelectSNIContextCallback_(SSL *s, int *ad, void* arg) { + HandleScope scope; + + Connection *p = static_cast SSL_get_app_data(s); + + const char* servername = SSL_get_servername(s, TLSEXT_NAMETYPE_host_name); + + if (servername) { + if (!p->servername_.IsEmpty()) { + p->servername_.Dispose(); + } + p->servername_ = Persistent::New(String::New(servername)); + + // Call sniCallback_ and use it's return value as context + if (!p->sniCallback_.IsEmpty()) { + if (!p->sniContext_.IsEmpty()) { + p->sniContext_.Dispose(); + } + + // Get callback init args + Local argv[1] = {*p->servername_}; + Local callback = *p->sniCallback_; + + TryCatch try_catch; + + // Call it + Local ret = callback->Call(Context::GetCurrent()->Global(), + 1, + argv); + + if (try_catch.HasCaught()) { + FatalException(try_catch); + } + + // If ret is SecureContext + if (secure_context_constructor->HasInstance(ret)) { + p->sniContext_ = Persistent::New(ret); + SecureContext *sc = ObjectWrap::Unwrap( + Local::Cast(ret)); + SSL_set_SSL_CTX(s, sc->ctx_); + } else { + return SSL_TLSEXT_ERR_NOACK; + } + } + } + + return SSL_TLSEXT_ERR_OK; +} +#endif Handle Connection::New(const Arguments& args) { HandleScope scope; @@ -724,8 +783,9 @@ Handle Connection::New(const Arguments& args) { p->bio_read_ = BIO_new(BIO_s_mem()); p->bio_write_ = BIO_new(BIO_s_mem()); -#ifdef OPENSSL_NPN_NEGOTIATED SSL_set_app_data(p->ssl_, p); + +#ifdef OPENSSL_NPN_NEGOTIATED if (is_server) { // Server should advertise NPN protocols SSL_CTX_set_next_protos_advertised_cb(sc->ctx_, @@ -740,6 +800,15 @@ Handle Connection::New(const Arguments& args) { } #endif +#ifdef SSL_CTRL_SET_TLSEXT_SERVERNAME_CB + if (is_server) { + SSL_CTX_set_tlsext_servername_callback(sc->ctx_, SelectSNIContextCallback_); + } else { + String::Utf8Value servername(args[2]->ToString()); + SSL_set_tlsext_host_name(p->ssl_, *servername); + } +#endif + SSL_set_bio(p->ssl_, p->bio_read_, p->bio_write_); #ifdef SSL_MODE_RELEASE_BUFFERS @@ -1333,6 +1402,39 @@ Handle Connection::SetNPNProtocols(const Arguments& args) { }; #endif +#ifdef SSL_CTRL_SET_TLSEXT_SERVERNAME_CB +Handle Connection::GetServername(const Arguments& args) { + HandleScope scope; + + Connection *ss = Connection::Unwrap(args); + + if (ss->is_server_ && !ss->servername_.IsEmpty()) { + return ss->servername_; + } else { + return False(); + } +} + +Handle Connection::SetSNICallback(const Arguments& args) { + HandleScope scope; + + Connection *ss = Connection::Unwrap(args); + + if (args.Length() < 1 || !args[0]->IsFunction()) { + return ThrowException(Exception::Error(String::New( + "Must give a Function as first argument"))); + } + + // Release old handle + if (!ss->sniCallback_.IsEmpty()) { + ss->sniCallback_.Dispose(); + } + ss->sniCallback_ = Persistent::New( + Local::Cast(args[0])); + + return True(); +} +#endif static void HexEncode(unsigned char *md_value, int md_len, diff --git a/src/node_crypto.h b/src/node_crypto.h index fd286e640a..ec8153e433 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -104,6 +104,12 @@ class Connection : ObjectWrap { v8::Persistent selectedNPNProto_; #endif +#ifdef SSL_CTRL_SET_TLSEXT_SERVERNAME_CB + v8::Persistent sniCallback_; + v8::Persistent sniContext_; + v8::Persistent servername_; +#endif + protected: static v8::Handle New(const v8::Arguments& args); static v8::Handle EncIn(const v8::Arguments& args); @@ -135,6 +141,13 @@ class Connection : ObjectWrap { unsigned int inlen, void *arg); #endif +#ifdef SSL_CTRL_SET_TLSEXT_SERVERNAME_CB + // SNI + static v8::Handle GetServername(const v8::Arguments& args); + static v8::Handle SetSNICallback(const v8::Arguments& args); + static int SelectSNIContextCallback_(SSL *s, int *ad, void* arg); +#endif + int HandleBIOError(BIO *bio, const char* func, int rv); int HandleSSLError(const char* func, int rv); @@ -162,6 +175,12 @@ class Connection : ObjectWrap { if (!npnProtos_.IsEmpty()) npnProtos_.Dispose(); if (!selectedNPNProto_.IsEmpty()) selectedNPNProto_.Dispose(); #endif + +#ifdef SSL_CTRL_SET_TLSEXT_SERVERNAME_CB + if (!sniCallback_.IsEmpty()) sniCallback_.Dispose(); + if (!sniContext_.IsEmpty()) sniContext_.Dispose(); + if (!servername_.IsEmpty()) servername_.Dispose(); +#endif } private: diff --git a/test/simple/test-tls-npn-server-client.js b/test/simple/test-tls-npn-server-client.js index 0c3d257604..c81a9e0634 100644 --- a/test/simple/test-tls-npn-server-client.js +++ b/test/simple/test-tls-npn-server-client.js @@ -19,9 +19,7 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -var NPN_ENABLED = process.binding('constants').NPN_ENABLED; - -if (!process.versions.openssl || !NPN_ENABLED) { +if (!process.features.tls_npn) { console.error("Skipping because node compiled without OpenSSL or " + "with old OpenSSL version."); process.exit(0); diff --git a/test/simple/test-tls-sni-server-client.js b/test/simple/test-tls-sni-server-client.js new file mode 100644 index 0000000000..526d6502f4 --- /dev/null +++ b/test/simple/test-tls-sni-server-client.js @@ -0,0 +1,112 @@ +// 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.features.tls_sni) { + console.error("Skipping because node compiled without OpenSSL or " + + "with old OpenSSL version."); + process.exit(0); +} + +var common = require('../common'), + assert = require('assert'), + fs = require('fs'), + tls = require('tls'); + +function filenamePEM(n) { + return require('path').join(common.fixturesDir, 'keys', n + ".pem"); +} + +function loadPEM(n) { + return fs.readFileSync(filenamePEM(n)); +} + +var serverOptions = { + key: loadPEM('agent2-key'), + cert: loadPEM('agent2-cert') +}; + +var SNIContexts = { + 'a.example.com': { + key: loadPEM('agent1-key'), + cert: loadPEM('agent1-cert') + }, + 'asterisk.test.com': { + key: loadPEM('agent3-key'), + cert: loadPEM('agent3-cert') + } +}; + + +var clientsOptions = [{ + key: loadPEM('agent1-key'), + cert: loadPEM('agent1-cert'), + ca: [loadPEM('ca1-cert')], + servername: 'a.example.com' +},{ + key: loadPEM('agent2-key'), + cert: loadPEM('agent2-cert'), + ca: [loadPEM('ca2-cert')], + servername: 'b.test.com' +},{ + key: loadPEM('agent3-key'), + cert: loadPEM('agent3-cert'), + ca: [loadPEM('ca1-cert')], + servername: 'c.wrong.com' +}]; + +var serverPort = common.PORT; + +var serverResults = [], + clientResults = []; + +var server = tls.createServer(serverOptions, function(c) { + serverResults.push(c.servername); +}); + +server.addContext('a.example.com', SNIContexts['a.example.com']); +server.addContext('*.test.com', SNIContexts['asterisk.test.com']); + +server.listen(serverPort, startTest); + +function startTest() { + function connectClient(options, callback) { + var client = tls.connect(serverPort, 'localhost', options, function() { + clientResults.push(client.authorized); + client.destroy(); + + callback(); + }); + }; + + connectClient(clientsOptions[0], function() { + connectClient(clientsOptions[1], function() { + connectClient(clientsOptions[2], function() { + server.close(); + }); + }); + }); +}; + +process.on('exit', function() { + assert.deepEqual(serverResults, ['a.example.com', 'b.test.com', + 'c.wrong.com']); + assert.deepEqual(clientResults, [true, true, false]); +});