- `NPNProtocols`: An array or `Buffer` of possible NPN protocols. (Protocols
should be ordered by their priority).
- - `SNICallback`: A function that will be called if client supports SNI TLS
- extension. Only one argument will be passed to it: `servername`. And
- `SNICallback` should return SecureContext instance.
+ - `SNICallback(servername, cb)`: A function that will be called if client
+ supports SNI TLS extension. Two argument will be passed to it: `servername`,
+ and `cb`. `SNICallback` should invoke `cb(null, ctx)`, where `ctx` is a
+ SecureContext instance.
(You can use `crypto.createCredentials(...).context` to get proper
SecureContext). If `SNICallback` wasn't provided - default callback with
high-level API will be used (see below).
function onclienthello(hello) {
var self = this,
- once = false;
+ onceSession = false,
+ onceSNI = false;
function callback(err, session) {
- if (once)
- return self.destroy(new Error('TLS session callback was called twice'));
- once = true;
+ if (onceSession)
+ return self.destroy(new Error('TLS session callback was called 2 times'));
+ onceSession = true;
if (err)
return self.destroy(err);
- self.ssl.loadSession(session);
+ // NOTE: That we have disabled OpenSSL's internal session storage in
+ // `node_crypto.cc` and hence its safe to rely on getting servername only
+ // from clienthello or this place.
+ var ret = self.ssl.loadSession(session);
+
+ // Servername came from SSL session
+ // NOTE: TLS Session ticket doesn't include servername information
+ //
+ // Another note, From RFC3546:
+ //
+ // If, on the other hand, the older
+ // session is resumed, then the server MUST ignore extensions appearing
+ // in the client hello, and send a server hello containing no
+ // extensions; in this case the extension functionality negotiated
+ // during the original session initiation is applied to the resumed
+ // session.
+ //
+ // Therefore we should account session loading when dealing with servername
+ if (ret && ret.servername) {
+ self._SNICallback(ret.servername, onSNIResult);
+ } else if (hello.servername && self._SNICallback) {
+ self._SNICallback(hello.servername, onSNIResult);
+ } else {
+ self.ssl.endParser();
+ }
+ }
+
+ function onSNIResult(err, context) {
+ if (onceSNI)
+ return self.destroy(new Error('TLS SNI callback was called 2 times'));
+ onceSNI = true;
+
+ if (err)
+ return self.destroy(err);
+
+ if (context)
+ self.ssl.sni_context = context;
+
+ self.ssl.endParser();
}
if (hello.sessionId.length <= 0 ||
hello.tlsTicket ||
this.server &&
!this.server.emit('resumeSession', hello.sessionId, callback)) {
- callback(null, null);
+ // Invoke SNI callback, since we've no session to resume
+ if (hello.servername && this._SNICallback)
+ this._SNICallback(hello.servername, onSNIResult);
+ else
+ this.ssl.endParser();
}
}
this._tlsOptions = options;
this._secureEstablished = false;
this._controlReleased = false;
+ this._SNICallback = null;
this.ssl = null;
this.servername = null;
this.npnProtocol = null;
(options.SNICallback !== SNICallback ||
options.server._contexts.length)) {
assert(typeof options.SNICallback === 'function');
- this.ssl.onsniselect = options.SNICallback;
+ this._SNICallback = options.SNICallback;
+ this.ssl.enableHelloParser();
}
if (process.features.tls_npn && options.NPNProtocols)
this._contexts.push([re, crypto.createCredentials(credentials).context]);
};
-function SNICallback(servername) {
+function SNICallback(servername, callback) {
var ctx;
this._contexts.some(function(elem) {
}
});
- return ctx;
+ callback(null, ctx);
}
Server.prototype.SNICallback = SNICallback;
session_id_ = NULL;
tls_ticket_size_ = -1;
tls_ticket_ = NULL;
+ servername_size_ = 0;
+ servername_ = NULL;
}
inline void ClientHelloParser::Start(ClientHelloParser::OnHelloCb onhello_cb,
hello.session_id_ = session_id_;
hello.session_size_ = session_size_;
hello.has_ticket_ = tls_ticket_ != NULL && tls_ticket_size_ != 0;
+ hello.servername_ = servername_;
+ hello.servername_size_ = servername_size_;
onhello_cb_(cb_arg_, hello);
}
// That's because we're heavily relying on OpenSSL to solve any problem with
// incoming data.
switch (type) {
+ case kServerName:
+ {
+ if (len < 2)
+ return;
+ uint16_t server_names_len = (data[0] << 8) + data[1];
+ if (server_names_len + 2 > len)
+ return;
+ for (size_t offset = 2; offset < 2 + server_names_len; ) {
+ if (offset + 3 > len)
+ return;
+ uint8_t name_type = data[offset];
+ if (name_type != kServernameHostname)
+ return;
+ uint16_t name_len = (data[offset + 1] << 8) + data[offset + 2];
+ offset += 3;
+ if (offset + name_len > len)
+ return;
+ servername_ = data + offset;
+ servername_size_ = name_len;
+ offset += name_len;
+ }
+ }
+ break;
case kTLSSessionTicket:
tls_ticket_size_ = len;
tls_ticket_ = data + len;
inline uint8_t session_size() const { return session_size_; }
inline const uint8_t* session_id() const { return session_id_; }
inline bool has_ticket() const { return has_ticket_; }
+ inline uint8_t servername_size() const { return servername_size_; }
+ inline const uint8_t* servername() const { return servername_; }
private:
uint8_t session_size_;
const uint8_t* session_id_;
bool has_ticket_;
+ uint8_t servername_size_;
+ const uint8_t* servername_;
friend class ClientHelloParser;
};
static const uint8_t kSSL2HeaderMask = 0x3f;
static const size_t kMaxTLSFrameLen = 16 * 1024 + 5;
static const size_t kMaxSSLExFrameLen = 32 * 1024;
+ static const uint8_t kServernameHostname = 0;
enum ParseState {
kWaiting,
};
enum ExtensionType {
+ kServerName = 0,
kTLSSessionTicket = 35
};
size_t extension_offset_;
uint8_t session_size_;
const uint8_t* session_id_;
+ uint16_t servername_size_;
+ const uint8_t* servername_;
uint16_t tls_ticket_size_;
const uint8_t* tls_ticket_;
};
static Cached<String> onread_sym;
static Cached<String> onerror_sym;
-static Cached<String> onsniselect_sym;
static Cached<String> onhandshakestart_sym;
static Cached<String> onhandshakedone_sym;
static Cached<String> onclienthello_sym;
static Cached<String> ext_key_usage_sym;
static Cached<String> sessionid_sym;
static Cached<String> tls_ticket_sym;
+static Cached<String> servername_sym;
+static Cached<String> sni_context_sym;
static Persistent<Function> tlsWrap;
#endif // OPENSSL_NPN_NEGOTIATED
#ifdef SSL_CTRL_SET_TLSEXT_SERVERNAME_CB
- servername_.Dispose();
sni_context_.Dispose();
#endif // SSL_CTRL_SET_TLSEXT_SERVERNAME_CB
}
// Parse ClientHello first
if (!hello_.IsEnded()) {
- assert(session_callbacks_);
size_t avail = 0;
uint8_t* data = reinterpret_cast<uint8_t*>(enc_in->Peek(&avail));
assert(avail == 0 || data != NULL);
UNWRAP(TLSCallbacks);
wrap->session_callbacks_ = true;
+ EnableHelloParser(args);
+}
+
+
+void TLSCallbacks::EnableHelloParser(
+ const FunctionCallbackInfo<Value>& args) {
+ HandleScope scope(node_isolate);
+
+ UNWRAP(TLSCallbacks);
+
wrap->hello_.Start(OnClientHello, OnClientHelloParseEnd, wrap);
}
reinterpret_cast<const char*>(hello.session_id()),
hello.session_size());
hello_obj->Set(sessionid_sym, buff);
+ if (hello.servername() == NULL) {
+ hello_obj->Set(servername_sym, String::Empty(node_isolate));
+ } else {
+ Local<String> servername = String::New(
+ reinterpret_cast<const char*>(hello.servername()),
+ hello.servername_size());
+ hello_obj->Set(servername_sym, servername);
+ }
hello_obj->Set(tls_ticket_sym, Boolean::New(hello.has_ticket()));
Handle<Value> argv[1] = { hello_obj };
if (wrap->next_sess_ != NULL)
SSL_SESSION_free(wrap->next_sess_);
wrap->next_sess_ = sess;
+
+ Local<Object> info = Object::New();
+#ifndef OPENSSL_NO_TLSEXT
+ if (sess->tlsext_hostname == NULL) {
+ info->Set(servername_sym, False(node_isolate));
+ } else {
+ info->Set(servername_sym, String::New(sess->tlsext_hostname));
+ }
+#endif
+ args.GetReturnValue().Set(info);
}
+}
+
+void TLSCallbacks::EndParser(const FunctionCallbackInfo<Value>& args) {
+ HandleScope scope(node_isolate);
+
+ UNWRAP(TLSCallbacks);
wrap->hello_.End();
}
UNWRAP(TLSCallbacks);
- if (wrap->kind_ == kTLSServer && !wrap->servername_.IsEmpty()) {
- args.GetReturnValue().Set(wrap->servername_);
+ const char* servername = SSL_get_servername(wrap->ssl_,
+ TLSEXT_NAMETYPE_host_name);
+ if (servername != NULL) {
+ args.GetReturnValue().Set(String::New(servername));
} else {
args.GetReturnValue().Set(false);
}
const char* servername = SSL_get_servername(s, TLSEXT_NAMETYPE_host_name);
- if (servername) {
- p->servername_.Reset(node_isolate, String::New(servername));
-
+ if (servername != NULL) {
// Call the SNI callback and use its return value as context
Local<Object> object = p->object();
- if (object->Has(onsniselect_sym)) {
- p->sni_context_.Dispose();
+ Local<Value> ctx;
+ if (object->Has(sni_context_sym)) {
+ ctx = object->Get(sni_context_sym);
+ }
- Local<Value> arg = PersistentToLocal(node_isolate, p->servername_);
- Handle<Value> ret = MakeCallback(object, onsniselect_sym, 1, &arg);
+ if (ctx.IsEmpty() || ctx->IsUndefined())
+ return SSL_TLSEXT_ERR_NOACK;
- // If ret is SecureContext
- if (ret->IsUndefined())
- return SSL_TLSEXT_ERR_NOACK;
+ p->sni_context_.Dispose();
+ p->sni_context_.Reset(node_isolate, ctx);
- p->sni_context_.Reset(node_isolate, ret);
- SecureContext* sc = ObjectWrap::Unwrap<SecureContext>(ret.As<Object>());
- SSL_set_SSL_CTX(s, sc->ctx_);
- }
+ SecureContext* sc = ObjectWrap::Unwrap<SecureContext>(ctx.As<Object>());
+ SSL_set_SSL_CTX(s, sc->ctx_);
}
return SSL_TLSEXT_ERR_OK;
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, "endParser", EndParser);
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,
"enableSessionCallbacks",
EnableSessionCallbacks);
+ NODE_SET_PROTOTYPE_METHOD(t,
+ "enableHelloParser",
+ EnableHelloParser);
#ifdef OPENSSL_NPN_NEGOTIATED
NODE_SET_PROTOTYPE_METHOD(t, "getNegotiatedProtocol", GetNegotiatedProto);
tlsWrap.Reset(node_isolate, t->GetFunction());
onread_sym = String::New("onread");
- onsniselect_sym = String::New("onsniselect");
onerror_sym = String::New("onerror");
onhandshakestart_sym = String::New("onhandshakestart");
onhandshakedone_sym = String::New("onhandshakedone");
ext_key_usage_sym = String::New("ext_key_usage");
sessionid_sym = String::New("sessionId");
tls_ticket_sym = String::New("tlsTicket");
+ servername_sym = String::New("servername");
+ sni_context_sym = String::New("sni_context");
}
} // namespace node
static void GetSession(const v8::FunctionCallbackInfo<v8::Value>& args);
static void SetSession(const v8::FunctionCallbackInfo<v8::Value>& args);
static void LoadSession(const v8::FunctionCallbackInfo<v8::Value>& args);
+ static void EndParser(const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetCurrentCipher(const v8::FunctionCallbackInfo<v8::Value>& args);
static void VerifyError(const v8::FunctionCallbackInfo<v8::Value>& args);
static void SetVerifyMode(const v8::FunctionCallbackInfo<v8::Value>& args);
static void IsSessionReused(const v8::FunctionCallbackInfo<v8::Value>& args);
static void EnableSessionCallbacks(
const v8::FunctionCallbackInfo<v8::Value>& args);
+ static void EnableHelloParser(
+ const v8::FunctionCallbackInfo<v8::Value>& args);
// TLS Session API
static SSL_SESSION* GetSessionCallback(SSL* s,
#endif // OPENSSL_NPN_NEGOTIATED
#ifdef SSL_CTRL_SET_TLSEXT_SERVERNAME_CB
- v8::Persistent<v8::String> servername_;
v8::Persistent<v8::Value> sni_context_;
#endif // SSL_CTRL_SET_TLSEXT_SERVERNAME_CB
};
var serverOptions = {
key: loadPEM('agent2-key'),
cert: loadPEM('agent2-cert'),
- SNICallback: function(servername) {
+ SNICallback: function(servername, callback) {
var credentials = SNIContexts[servername];
- if (credentials)
- return crypto.createCredentials(credentials).context;
+
+ // Just to test asynchronous callback
+ setTimeout(function() {
+ if (credentials)
+ callback(null, crypto.createCredentials(credentials).context);
+ else
+ callback(null, null);
+ }, 100);
}
};