Skip to content

Commit

Permalink
Add support for TLS SNI
Browse files Browse the repository at this point in the history
Fixes #1411
  • Loading branch information
indutny authored and ry committed Jul 29, 2011
1 parent 9dd9792 commit 9010f5f
Show file tree
Hide file tree
Showing 8 changed files with 315 additions and 22 deletions.
6 changes: 2 additions & 4 deletions lib/https.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
}

Expand Down Expand Up @@ -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'];
}

Expand Down
4 changes: 1 addition & 3 deletions lib/https2.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
}

Expand Down
74 changes: 63 additions & 11 deletions lib/tls.js
Original file line number Diff line number Diff line change
Expand Up @@ -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); };
Expand All @@ -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.');
}
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}

Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -789,14 +801,19 @@ 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;

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);
Expand Down Expand Up @@ -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;
};


Expand Down Expand Up @@ -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);

Expand Down
14 changes: 14 additions & 0 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -2031,6 +2043,8 @@ static Handle<Object> 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));

Expand Down
104 changes: 103 additions & 1 deletion src/node_crypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,14 @@ static Persistent<String> name_symbol;
static Persistent<String> version_symbol;
static Persistent<String> ext_key_usage_symbol;

static Persistent<FunctionTemplate> secure_context_constructor;

void SecureContext::Initialize(Handle<Object> target) {
HandleScope scope;

Local<FunctionTemplate> t = FunctionTemplate::New(SecureContext::New);
secure_context_constructor = Persistent<FunctionTemplate>::New(t);

t->InstanceTemplate()->SetInternalFieldCount(1);
t->SetClassName(String::NewSymbol("SecureContext"));

Expand Down Expand Up @@ -585,6 +588,12 @@ void Connection::Initialize(Handle<Object> 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());
}

Expand Down Expand Up @@ -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<Connection*> 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<String>::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<Value> argv[1] = {*p->servername_};
Local<Function> callback = *p->sniCallback_;

TryCatch try_catch;

// Call it
Local<Value> 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<Value>::New(ret);
SecureContext *sc = ObjectWrap::Unwrap<SecureContext>(
Local<Object>::Cast(ret));
SSL_set_SSL_CTX(s, sc->ctx_);
} else {
return SSL_TLSEXT_ERR_NOACK;
}
}
}

return SSL_TLSEXT_ERR_OK;
}
#endif

Handle<Value> Connection::New(const Arguments& args) {
HandleScope scope;
Expand All @@ -724,8 +783,9 @@ Handle<Value> 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_,
Expand All @@ -740,6 +800,15 @@ Handle<Value> 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
Expand Down Expand Up @@ -1333,6 +1402,39 @@ Handle<Value> Connection::SetNPNProtocols(const Arguments& args) {
};
#endif

#ifdef SSL_CTRL_SET_TLSEXT_SERVERNAME_CB
Handle<Value> 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<Value> 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<Function>::New(
Local<Function>::Cast(args[0]));

return True();
}
#endif

static void HexEncode(unsigned char *md_value,
int md_len,
Expand Down
Loading

0 comments on commit 9010f5f

Please sign in to comment.