diff --git a/node.gyp b/node.gyp index 5a1b924b04..931eca4274 100644 --- a/node.gyp +++ b/node.gyp @@ -159,7 +159,12 @@ 'conditions': [ [ 'node_use_openssl=="true"', { 'defines': [ 'HAVE_OPENSSL=1' ], - 'sources': [ 'src/node_crypto.cc', 'src/node_crypto_bio.cc' ], + 'sources': [ + 'src/node_crypto.cc', + 'src/node_crypto_bio.cc', + 'src/tls_wrap.cc', + 'src/tls_wrap.h' + ], 'conditions': [ [ 'node_shared_openssl=="false"', { 'dependencies': [ './deps/openssl/openssl.gyp:openssl' ], diff --git a/src/node_crypto_bio.cc b/src/node_crypto_bio.cc index 547c711488..adb1e336e9 100644 --- a/src/node_crypto_bio.cc +++ b/src/node_crypto_bio.cc @@ -82,6 +82,12 @@ int NodeBIO::Read(BIO* bio, char* out, int len) { } +char* NodeBIO::Peek(size_t* size) { + *size = read_head_->write_pos_ - read_head_->read_pos_; + return read_head_->data_ + read_head_->read_pos_; +} + + int NodeBIO::Write(BIO* bio, const char* data, int len) { BIO_clear_retry_flags(bio); @@ -318,6 +324,29 @@ void NodeBIO::Write(const char* data, size_t size) { } +char* NodeBIO::PeekWritable(size_t* size) { + size_t available = kBufferLength - write_head_->write_pos_; + if (*size != 0 && available > *size) + available = *size; + else + *size = available; + + return write_head_->data_ + write_head_->write_pos_; +} + + +void NodeBIO::Commit(size_t size) { + write_head_->write_pos_ += size; + length_ += size; + assert(write_head_->write_pos_ <= kBufferLength); + + // Allocate new buffer if write head is full, + // and there're no other place to go + TryAllocateForWrite(); + write_head_ = write_head_->next_; +} + + void NodeBIO::TryAllocateForWrite() { // If write head is full, next buffer is either read head or not empty. if (write_head_->write_pos_ == kBufferLength && diff --git a/src/node_crypto_bio.h b/src/node_crypto_bio.h index c2fd6a0ba7..4794453ef8 100644 --- a/src/node_crypto_bio.h +++ b/src/node_crypto_bio.h @@ -30,6 +30,13 @@ class NodeBIO { return &method_; } + NodeBIO() : length_(0), read_head_(&head_), write_head_(&head_) { + // Loop head + head_.next_ = &head_; + } + + ~NodeBIO(); + static int New(BIO* bio); static int Free(BIO* bio); static int Read(BIO* bio, char* out, int len); @@ -38,27 +45,6 @@ class NodeBIO { static int Gets(BIO* bio, char* out, int size); static long Ctrl(BIO* bio, int cmd, long num, void* ptr); - protected: - static const size_t kBufferLength = 16 * 1024; - - class Buffer { - public: - Buffer() : read_pos_(0), write_pos_(0), next_(NULL) { - } - - size_t read_pos_; - size_t write_pos_; - Buffer* next_; - char data_[kBufferLength]; - }; - - NodeBIO() : length_(0), read_head_(&head_), write_head_(&head_) { - // Loop head - head_.next_ = &head_; - } - - ~NodeBIO(); - // Allocate new buffer for write if needed void TryAllocateForWrite(); @@ -69,6 +55,10 @@ class NodeBIO { // Deallocate children of write head's child if they're empty void FreeEmpty(); + // Return pointer to internal data and amount of + // contiguous data available to read + char* Peek(size_t* size); + // Find first appearance of `delim` in buffer or `limit` if `delim` // wasn't found. size_t IndexOf(char delim, size_t limit); @@ -79,6 +69,13 @@ class NodeBIO { // Put `len` bytes from `data` into buffer void Write(const char* data, size_t size); + // Return pointer to internal data and amount of + // contiguous data available for future writes + char* PeekWritable(size_t* size); + + // Commit reserved data + void Commit(size_t size); + // Return size of buffer in bytes size_t inline Length() { return length_; @@ -89,6 +86,20 @@ class NodeBIO { return static_cast(bio->ptr); } + protected: + static const size_t kBufferLength = 16 * 1024; + + class Buffer { + public: + Buffer() : read_pos_(0), write_pos_(0), next_(NULL) { + } + + size_t read_pos_; + size_t write_pos_; + Buffer* next_; + char data_[kBufferLength]; + }; + size_t length_; Buffer head_; Buffer* read_head_; diff --git a/src/node_extensions.h b/src/node_extensions.h index 4238f8c521..8e1c7021c7 100644 --- a/src/node_extensions.h +++ b/src/node_extensions.h @@ -34,6 +34,7 @@ NODE_EXT_LIST_ITEM(node_zlib) // libuv rewrite NODE_EXT_LIST_ITEM(node_timer_wrap) NODE_EXT_LIST_ITEM(node_tcp_wrap) +NODE_EXT_LIST_ITEM(node_tls_wrap) NODE_EXT_LIST_ITEM(node_udp_wrap) NODE_EXT_LIST_ITEM(node_pipe_wrap) NODE_EXT_LIST_ITEM(node_cares_wrap) diff --git a/src/tls_wrap.cc b/src/tls_wrap.cc new file mode 100644 index 0000000000..89d2790d74 --- /dev/null +++ b/src/tls_wrap.cc @@ -0,0 +1,1153 @@ +// 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. + +#include "tls_wrap.h" +#include "node_buffer.h" // Buffer +#include "node_crypto.h" // SecureContext +#include "node_crypto_bio.h" // NodeBIO +#include "node_wrap.h" // WithGenericStream +#include "node_counters.h" + +namespace node { + +using namespace v8; +using crypto::SecureContext; + +static Persistent onread_sym; +static Persistent onerror_sym; +static Persistent onsniselect_sym; +static Persistent onhandshakestart_sym; +static Persistent onhandshakedone_sym; +static Persistent subject_sym; +static Persistent subjectaltname_sym; +static Persistent modulus_sym; +static Persistent exponent_sym; +static Persistent issuer_sym; +static Persistent valid_from_sym; +static Persistent valid_to_sym; +static Persistent fingerprint_sym; +static Persistent name_sym; +static Persistent version_sym; +static Persistent ext_key_usage_sym; + +static Persistent tlsWrap; + +static const int X509_NAME_FLAGS = ASN1_STRFLGS_ESC_CTRL + | ASN1_STRFLGS_ESC_MSB + | XN_FLAG_SEP_MULTILINE + | XN_FLAG_FN_SN; + + +TLSCallbacks::TLSCallbacks(Kind kind, + Handle sc, + StreamWrapCallbacks* old) + : StreamWrapCallbacks(old), + kind_(kind), + ssl_(NULL), + enc_in_(NULL), + enc_out_(NULL), + clear_in_(NULL), + write_size_(0), + pending_write_item_(NULL), + started_(false), + established_(false), + shutdown_(false) { + + // Persist SecureContext + sc_ = ObjectWrap::Unwrap(sc); + sc_handle_ = Persistent::New(node_isolate, sc); + + handle_ = Persistent::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_); + + InitSSL(); +} + + +TLSCallbacks::~TLSCallbacks() { + SSL_free(ssl_); + ssl_ = NULL; + enc_in_ = NULL; + enc_out_ = NULL; + delete clear_in_; + clear_in_ = NULL; + + sc_ = NULL; + sc_handle_.Dispose(node_isolate); + sc_handle_.Clear(); + + handle_.Dispose(node_isolate); + handle_.Clear(); + +#ifdef OPENSSL_NPN_NEGOTIATED + npn_protos_.Dispose(node_isolate); + npn_protos_.Clear(); + selected_npn_proto_.Dispose(node_isolate); + selected_npn_proto_.Clear(); +#endif // OPENSSL_NPN_NEGOTIATED + +#ifdef SSL_CTRL_SET_TLSEXT_SERVERNAME_CB + servername_.Dispose(node_isolate); + servername_.Clear(); + sni_context_.Dispose(node_isolate); + sni_context_.Clear(); +#endif // SSL_CTRL_SET_TLSEXT_SERVERNAME_CB +} + + +void TLSCallbacks::InvokeQueued(int status) { + // Empty queue - ignore call + if (pending_write_item_ == NULL) + return; + + QUEUE* q = &pending_write_item_->member_; + pending_write_item_ = NULL; + + // Process old queue + while (q != &write_item_queue_) { + QUEUE* next = static_cast(QUEUE_NEXT(q)); + WriteItem* wi = container_of(q, WriteItem, member_); + wi->cb_(&wi->w_->req_, status); + delete wi; + q = next; + } +} + + +static int VerifyCallback(int preverify_ok, X509_STORE_CTX *ctx) { + // Quoting SSL_set_verify(3ssl): + // + // The VerifyCallback function is used to control the behaviour when + // the SSL_VERIFY_PEER flag is set. It must be supplied by the + // application and receives two arguments: preverify_ok indicates, + // whether the verification of the certificate in question was passed + // (preverify_ok=1) or not (preverify_ok=0). x509_ctx is a pointer to + // the complete context used for the certificate chain verification. + // + // The certificate chain is checked starting with the deepest nesting + // level (the root CA certificate) and worked upward to the peer's + // certificate. At each level signatures and issuer attributes are + // checked. Whenever a verification error is found, the error number is + // stored in x509_ctx and VerifyCallback is called with preverify_ok=0. + // By applying X509_CTX_store_* functions VerifyCallback can locate the + // certificate in question and perform additional steps (see EXAMPLES). + // If no error is found for a certificate, VerifyCallback is called + // with preverify_ok=1 before advancing to the next level. + // + // The return value of VerifyCallback controls the strategy of the + // further verification process. If VerifyCallback returns 0, the + // verification process is immediately stopped with "verification + // failed" state. If SSL_VERIFY_PEER is set, a verification failure + // alert is sent to the peer and the TLS/SSL handshake is terminated. If + // VerifyCallback returns 1, the verification process is continued. If + // VerifyCallback always returns 1, the TLS/SSL handshake will not be + // terminated with respect to verification failures and the connection + // will be established. The calling process can however retrieve the + // error code of the last verification error using + // SSL_get_verify_result(3) or by maintaining its own error storage + // managed by VerifyCallback. + // + // If no VerifyCallback is specified, the default callback will be + // used. Its return value is identical to preverify_ok, so that any + // verification failure will lead to a termination of the TLS/SSL + // handshake with an alert message, if SSL_VERIFY_PEER is set. + // + // Since we cannot perform I/O quickly enough in this callback, we ignore + // all preverify_ok errors and let the handshake continue. It is + // imparative that the user use Connection::VerifyError after the + // 'secure' callback has been made. + return 1; +} + + +void TLSCallbacks::InitSSL() { + assert(ssl_ == NULL); + + // Initialize SSL + ssl_ = SSL_new(sc_->ctx_); + enc_in_ = BIO_new(NodeBIO::GetMethod()); + enc_out_ = BIO_new(NodeBIO::GetMethod()); + + SSL_set_bio(ssl_, enc_in_, enc_out_); + + // NOTE: This could be overriden in SetVerifyMode + SSL_set_verify(ssl_, SSL_VERIFY_NONE, VerifyCallback); + +#ifdef SSL_MODE_RELEASE_BUFFERS + long mode = SSL_get_mode(ssl_); + SSL_set_mode(ssl_, mode | SSL_MODE_RELEASE_BUFFERS); +#endif // SSL_MODE_RELEASE_BUFFERS + + SSL_set_app_data(ssl_, this); + SSL_set_info_callback(ssl_, SSLInfoCallback); + + if (kind_ == kTLSServer) { + SSL_set_accept_state(ssl_); + +#ifdef OPENSSL_NPN_NEGOTIATED + // Server should advertise NPN protocols + SSL_CTX_set_next_protos_advertised_cb(sc_->ctx_, + AdvertiseNextProtoCallback, + this); +#endif // OPENSSL_NPN_NEGOTIATED + +#ifdef SSL_CTRL_SET_TLSEXT_SERVERNAME_CB + SSL_CTX_set_tlsext_servername_callback(sc_->ctx_, SelectSNIContextCallback); + SSL_CTX_set_tlsext_servername_arg(sc_->ctx_, this); +#endif // SSL_CTRL_SET_TLSEXT_SERVERNAME_CB + } else if (kind_ == kTLSClient) { + SSL_set_connect_state(ssl_); + +#ifdef OPENSSL_NPN_NEGOTIATED + // Client should select protocol from list of advertised + // If server supports NPN + SSL_CTX_set_next_proto_select_cb(sc_->ctx_, + SelectNextProtoCallback, + this); +#endif // OPENSSL_NPN_NEGOTIATED + } else { + // Unexpected + abort(); + } + + // Initialize ring for queud clear data + clear_in_ = new NodeBIO(); +} + + +Handle TLSCallbacks::Wrap(const Arguments& args) { + HandleScope scope(node_isolate); + + if (args.Length() < 1 || !args[0]->IsObject()) + return ThrowTypeError("First argument should be a StreamWrap instance"); + if (args.Length() < 2 || !args[1]->IsObject()) + return ThrowTypeError("Second argument should be a SecureContext instance"); + if (args.Length() < 3 || !args[2]->IsBoolean()) + return ThrowTypeError("Third argument should be boolean"); + + Local stream = args[0].As(); + Local sc = args[1].As(); + Kind kind = args[2]->IsTrue() ? kTLSServer : kTLSClient; + + TLSCallbacks* callbacks = NULL; + WITH_GENERIC_STREAM(stream, { + callbacks = new TLSCallbacks(kind, sc, wrap->GetCallbacks()); + wrap->OverrideCallbacks(callbacks); + }); + + if (callbacks == NULL) + return Null(node_isolate); + + return scope.Close(callbacks->handle_); +} + + +Handle TLSCallbacks::Start(const Arguments& args) { + HandleScope scope(node_isolate); + + UNWRAP(TLSCallbacks); + + if (wrap->started_) + return ThrowError("Already started."); + wrap->started_ = true; + + // Send ClientHello handshake + assert(wrap->kind_ == kTLSClient); + wrap->ClearOut(); + wrap->EncOut(); + + return Null(node_isolate); +} + + +void TLSCallbacks::SSLInfoCallback(const SSL* ssl_, int where, int ret) { + // Be compatible with older versions of OpenSSL. SSL_get_app_data() wants + // a non-const SSL* in OpenSSL <= 0.9.7e. + SSL* ssl = const_cast(ssl_); + if (where & SSL_CB_HANDSHAKE_START) { + HandleScope scope(node_isolate); + TLSCallbacks* c = static_cast(SSL_get_app_data(ssl)); + if (c->handle_->Has(onhandshakestart_sym)) + MakeCallback(c->handle_, onhandshakestart_sym, 0, NULL); + } + if (where & SSL_CB_HANDSHAKE_DONE) { + HandleScope scope(node_isolate); + TLSCallbacks* c = static_cast(SSL_get_app_data(ssl)); + c->established_ = true; + if (c->handle_->Has(onhandshakedone_sym)) + MakeCallback(c->handle_, onhandshakedone_sym, 0, NULL); + } +} + + +void TLSCallbacks::EncOut() { + // Write in progress + if (write_size_ != 0) + return; + + // Split-off queue + if (established_ && !QUEUE_EMPTY(&write_item_queue_)) { + pending_write_item_ = container_of(QUEUE_NEXT(&write_item_queue_), + WriteItem, + member_); + QUEUE_INIT(&write_item_queue_); + } + + // No data to write + if (BIO_pending(enc_out_) == 0) { + InvokeQueued(0); + return; + } + + char* data = NodeBIO::FromBIO(enc_out_)->Peek(&write_size_); + assert(write_size_ != 0); + + write_req_.data = this; + uv_buf_t buf = uv_buf_init(data, write_size_); + int r = uv_write(&write_req_, wrap_->GetStream(), &buf, 1, EncOutCb); + + // Ignore errors, this should be already handled in js + if (!r) { + if (wrap_->GetStream()->type == UV_TCP) { + NODE_COUNT_NET_BYTES_SENT(write_size_); + } else if (wrap_->GetStream()->type == UV_NAMED_PIPE) { + NODE_COUNT_PIPE_BYTES_SENT(write_size_); + } + } +} + + +void TLSCallbacks::EncOutCb(uv_write_t* req, int status) { + HandleScope scope(node_isolate); + + TLSCallbacks* callbacks = static_cast(req->data); + + // Handle error + if (status) { + // Ignore errors after shutdown + if (callbacks->shutdown_) + return; + + // Notify about error + SetErrno(uv_last_error(uv_default_loop())); + Local arg = String::Concat( + String::New("write cb error, status: "), + Integer::New(status, node_isolate)->ToString()); + MakeCallback(callbacks->handle_, onerror_sym, 1, &arg); + callbacks->InvokeQueued(status); + return; + } + + // Commit + NodeBIO::FromBIO(callbacks->enc_out_)->Read(NULL, callbacks->write_size_); + + // Try writing more data + callbacks->write_size_ = 0; + callbacks->EncOut(); +} + + +Handle TLSCallbacks::GetSSLError(int status, int* err) { + HandleScope scope(node_isolate); + + *err = SSL_get_error(ssl_, status); + switch (*err) { + case SSL_ERROR_NONE: + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + break; + case SSL_ERROR_ZERO_RETURN: + return scope.Close(String::NewSymbol("ZERO_RETURN")); + break; + default: + { + BUF_MEM* mem; + BIO* bio; + + assert(*err == SSL_ERROR_SSL || *err == SSL_ERROR_SYSCALL); + + bio = BIO_new(BIO_s_mem()); + assert(bio != NULL); + ERR_print_errors(bio); + BIO_get_mem_ptr(bio, &mem); + Handle r = Exception::Error(String::New(mem->data, mem->length)); + BIO_free_all(bio); + + return scope.Close(r); + } + } + return Handle(); +} + + +void TLSCallbacks::ClearOut() { + HandleScope scope(node_isolate); + + assert(ssl_ != NULL); + + char out[kClearOutChunkSize]; + int read; + do { + read = SSL_read(ssl_, out, sizeof(out)); + if (read > 0) { + Local buff = Local::New(Buffer::New(out, read)->handle_); + Handle argv[3] = { + buff, + Integer::New(0, node_isolate), + Integer::New(read, node_isolate) + }; + MakeCallback(Self(), onread_sym, ARRAY_SIZE(argv), argv); + } + } while (read > 0); + + if (read == -1) { + int err; + Handle argv = GetSSLError(read, &err); + + if (!argv.IsEmpty()) + MakeCallback(handle_, onerror_sym, 1, &argv); + } +} + + +bool TLSCallbacks::ClearIn() { + HandleScope scope(node_isolate); + + int written = 0; + while (clear_in_->Length() > 0) { + size_t avail = 0; + char* data = clear_in_->Peek(&avail); + written = SSL_write(ssl_, data, avail); + assert(written == -1 || written == static_cast(avail)); + if (written == -1) + break; + clear_in_->Read(NULL, avail); + } + + // All written + if (clear_in_->Length() == 0) { + assert(written >= 0); + return true; + } + + // Error or partial write + int err; + Handle argv = GetSSLError(written, &err); + if (!argv.IsEmpty()) + MakeCallback(handle_, onerror_sym, 1, &argv); + + return false; +} + + +int TLSCallbacks::DoWrite(WriteWrap* w, + uv_buf_t* bufs, + size_t count, + uv_stream_t* send_handle, + uv_write_cb cb) { + HandleScope scope(node_isolate); + + assert(send_handle == NULL); + + // Queue callback to execute it on next tick + WriteItem* wi = new WriteItem(w, cb); + bool empty = true; + + // Empty writes should not go through encryption process + size_t i; + for (i = 0; i < count; i++) + if (bufs[i].len > 0) { + empty = false; + break; + } + if (empty) { + ClearOut(); + // However if there any data that should be written to socket, + // callback should not be invoked immediately + if (BIO_pending(enc_out_) == 0) + return uv_write(&w->req_, wrap_->GetStream(), bufs, count, cb); + } + + QUEUE_INSERT_TAIL(&write_item_queue_, &wi->member_); + + // Write queued data + if (empty) { + EncOut(); + return 0; + } + + // Process enqueued data first + if (!ClearIn()) { + // If there're still data to process - enqueue current one + for (i = 0; i < count; i++) + clear_in_->Write(bufs[i].base, bufs[i].len); + return 0; + } + + int written = 0; + for (i = 0; i < count; i++) { + written = SSL_write(ssl_, bufs[i].base, bufs[i].len); + assert(written == -1 || written == static_cast(bufs[i].len)); + if (written == -1) + break; + } + + if (i != count) { + int err; + Handle argv = GetSSLError(written, &err); + if (!argv.IsEmpty()) { + MakeCallback(handle_, onerror_sym, 1, &argv); + return -1; + } + + // No errors, queue rest + for (; i < count; i++) + clear_in_->Write(bufs[i].base, bufs[i].len); + } + + // Try writing data immediately + EncOut(); + + return 0; +} + + +void TLSCallbacks::AfterWrite(WriteWrap* w) { + // Intentionally empty +} + + +uv_buf_t TLSCallbacks::DoAlloc(uv_handle_t* handle, size_t suggested_size) { + size_t size = suggested_size; + char* data = NodeBIO::FromBIO(enc_in_)->PeekWritable(&size); + return uv_buf_init(data, size); +} + + +void TLSCallbacks::DoRead(uv_stream_t* handle, + ssize_t nread, + uv_buf_t buf, + uv_handle_type pending) { + if (nread < 0) { + uv_err_t err = uv_last_error(uv_default_loop()); + SetErrno(err); + + // Error should be emitted only after all data was read + ClearOut(); + MakeCallback(Self(), onread_sym, 0, NULL); + return; + } + + // Only client connections can receive data + assert(ssl_ != NULL); + + // Commit read data + NodeBIO::FromBIO(enc_in_)->Commit(nread); + + // Cycle OpenSSL state + ClearIn(); + ClearOut(); + EncOut(); +} + + +int TLSCallbacks::DoShutdown(ShutdownWrap* req_wrap, uv_shutdown_cb cb) { + if (SSL_shutdown(ssl_) == 0) + SSL_shutdown(ssl_); + shutdown_ = true; + EncOut(); + return StreamWrapCallbacks::DoShutdown(req_wrap, cb); +} + + +#define CASE_X509_ERR(CODE) case X509_V_ERR_##CODE: reason = #CODE; break; +Handle TLSCallbacks::VerifyError(const Arguments& args) { + HandleScope scope(node_isolate); + + UNWRAP(TLSCallbacks); + + // XXX Do this check in JS land? + X509* peer_cert = SSL_get_peer_certificate(wrap->ssl_); + if (peer_cert == NULL) { + // We requested a certificate and they did not send us one. + // Definitely an error. + // XXX is this the right error message? + return scope.Close(Exception::Error( + String::New("UNABLE_TO_GET_ISSUER_CERT"))); + } + X509_free(peer_cert); + + long x509_verify_error = SSL_get_verify_result(wrap->ssl_); + + const char* reason = NULL; + Local s; + switch (x509_verify_error) { + case X509_V_OK: + return Null(node_isolate); + CASE_X509_ERR(UNABLE_TO_GET_ISSUER_CERT) + CASE_X509_ERR(UNABLE_TO_GET_CRL) + CASE_X509_ERR(UNABLE_TO_DECRYPT_CERT_SIGNATURE) + CASE_X509_ERR(UNABLE_TO_DECRYPT_CRL_SIGNATURE) + CASE_X509_ERR(UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY) + CASE_X509_ERR(CERT_SIGNATURE_FAILURE) + CASE_X509_ERR(CRL_SIGNATURE_FAILURE) + CASE_X509_ERR(CERT_NOT_YET_VALID) + CASE_X509_ERR(CERT_HAS_EXPIRED) + CASE_X509_ERR(CRL_NOT_YET_VALID) + CASE_X509_ERR(CRL_HAS_EXPIRED) + CASE_X509_ERR(ERROR_IN_CERT_NOT_BEFORE_FIELD) + CASE_X509_ERR(ERROR_IN_CERT_NOT_AFTER_FIELD) + CASE_X509_ERR(ERROR_IN_CRL_LAST_UPDATE_FIELD) + CASE_X509_ERR(ERROR_IN_CRL_NEXT_UPDATE_FIELD) + CASE_X509_ERR(OUT_OF_MEM) + CASE_X509_ERR(DEPTH_ZERO_SELF_SIGNED_CERT) + CASE_X509_ERR(SELF_SIGNED_CERT_IN_CHAIN) + CASE_X509_ERR(UNABLE_TO_GET_ISSUER_CERT_LOCALLY) + CASE_X509_ERR(UNABLE_TO_VERIFY_LEAF_SIGNATURE) + CASE_X509_ERR(CERT_CHAIN_TOO_LONG) + CASE_X509_ERR(CERT_REVOKED) + CASE_X509_ERR(INVALID_CA) + CASE_X509_ERR(PATH_LENGTH_EXCEEDED) + CASE_X509_ERR(INVALID_PURPOSE) + CASE_X509_ERR(CERT_UNTRUSTED) + CASE_X509_ERR(CERT_REJECTED) + default: + s = String::New(X509_verify_cert_error_string(x509_verify_error)); + break; + } + + if (s.IsEmpty()) { + s = String::New(reason); + } + + return scope.Close(Exception::Error(s)); +} +#undef CASE_X509_ERR + + +Handle TLSCallbacks::SetVerifyMode(const Arguments& args) { + HandleScope scope(node_isolate); + + UNWRAP(TLSCallbacks); + + if (args.Length() < 2 || !args[0]->IsBoolean() || !args[1]->IsBoolean()) + return ThrowTypeError("Bad arguments, expected two booleans"); + + int verify_mode; + if (wrap->kind_ == kTLSServer) { + bool request_cert = args[0]->IsTrue(); + if (!request_cert) { + // Note reject_unauthorized ignored. + verify_mode = SSL_VERIFY_NONE; + } else { + bool reject_unauthorized = args[1]->IsTrue(); + verify_mode = SSL_VERIFY_PEER; + if (reject_unauthorized) verify_mode |= SSL_VERIFY_FAIL_IF_NO_PEER_CERT; + } + } else { + // Note request_cert and reject_unauthorized are ignored for clients. + verify_mode = SSL_VERIFY_NONE; + } + + // Always allow a connection. We'll reject in javascript. + SSL_set_verify(wrap->ssl_, verify_mode, VerifyCallback); + + return True(node_isolate); +} + + +Handle TLSCallbacks::IsSessionReused(const Arguments& args) { + HandleScope scope(node_isolate); + + UNWRAP(TLSCallbacks); + + return scope.Close(Boolean::New(SSL_session_reused(wrap->ssl_))); +} + + +Handle TLSCallbacks::GetPeerCertificate(const Arguments& args) { + HandleScope scope(node_isolate); + + UNWRAP(TLSCallbacks); + + Local info = Object::New(); + X509* peer_cert = SSL_get_peer_certificate(wrap->ssl_); + if (peer_cert != NULL) { + BIO* bio = BIO_new(BIO_s_mem()); + BUF_MEM* mem; + if (X509_NAME_print_ex(bio, + X509_get_subject_name(peer_cert), + 0, + X509_NAME_FLAGS) > 0) { + BIO_get_mem_ptr(bio, &mem); + info->Set(subject_sym, String::New(mem->data, mem->length)); + } + (void) BIO_reset(bio); + + if (X509_NAME_print_ex(bio, + X509_get_issuer_name(peer_cert), + 0, + X509_NAME_FLAGS) > 0) { + BIO_get_mem_ptr(bio, &mem); + info->Set(issuer_sym, String::New(mem->data, mem->length)); + } + (void) BIO_reset(bio); + + int index = X509_get_ext_by_NID(peer_cert, NID_subject_alt_name, -1); + if (index >= 0) { + X509_EXTENSION* ext; + int rv; + + ext = X509_get_ext(peer_cert, index); + assert(ext != NULL); + + rv = X509V3_EXT_print(bio, ext, 0, 0); + assert(rv == 1); + + BIO_get_mem_ptr(bio, &mem); + info->Set(subjectaltname_sym, String::New(mem->data, mem->length)); + + (void) BIO_reset(bio); + } + + EVP_PKEY* pkey = NULL; + RSA* rsa = NULL; + if (NULL != (pkey = X509_get_pubkey(peer_cert)) && + NULL != (rsa = EVP_PKEY_get1_RSA(pkey))) { + BN_print(bio, rsa->n); + BIO_get_mem_ptr(bio, &mem); + info->Set(modulus_sym, String::New(mem->data, mem->length) ); + (void) BIO_reset(bio); + + BN_print(bio, rsa->e); + BIO_get_mem_ptr(bio, &mem); + info->Set(exponent_sym, String::New(mem->data, mem->length) ); + (void) BIO_reset(bio); + } + + if (pkey != NULL) { + EVP_PKEY_free(pkey); + pkey = NULL; + } + if (rsa != NULL) { + RSA_free(rsa); + rsa = NULL; + } + + ASN1_TIME_print(bio, X509_get_notBefore(peer_cert)); + BIO_get_mem_ptr(bio, &mem); + info->Set(valid_from_sym, String::New(mem->data, mem->length)); + (void) BIO_reset(bio); + + ASN1_TIME_print(bio, X509_get_notAfter(peer_cert)); + BIO_get_mem_ptr(bio, &mem); + info->Set(valid_to_sym, String::New(mem->data, mem->length)); + BIO_free_all(bio); + + unsigned int md_size, i; + unsigned char md[EVP_MAX_MD_SIZE]; + if (X509_digest(peer_cert, EVP_sha1(), md, &md_size)) { + const char hex[] = "0123456789ABCDEF"; + char fingerprint[EVP_MAX_MD_SIZE * 3]; + + for (i = 0; i < md_size; i++) { + fingerprint[3*i] = hex[(md[i] & 0xf0) >> 4]; + fingerprint[(3*i)+1] = hex[(md[i] & 0x0f)]; + fingerprint[(3*i)+2] = ':'; + } + + if (md_size > 0) + fingerprint[(3*(md_size-1))+2] = '\0'; + else + fingerprint[0] = '\0'; + + info->Set(fingerprint_sym, String::New(fingerprint)); + } + + STACK_OF(ASN1_OBJECT)* eku = static_cast( + X509_get_ext_d2i(peer_cert, + NID_ext_key_usage, + NULL, + NULL)); + if (eku != NULL) { + Local ext_key_usage = Array::New(); + char buf[256]; + + for (int i = 0; i < sk_ASN1_OBJECT_num(eku); i++) { + memset(buf, 0, sizeof(buf)); + OBJ_obj2txt(buf, sizeof(buf) - 1, sk_ASN1_OBJECT_value(eku, i), 1); + ext_key_usage->Set(Integer::New(i, node_isolate), String::New(buf)); + } + + sk_ASN1_OBJECT_pop_free(eku, ASN1_OBJECT_free); + info->Set(ext_key_usage_sym, ext_key_usage); + } + + X509_free(peer_cert); + } + + return scope.Close(info); +} + + +Handle TLSCallbacks::GetSession(const Arguments& args) { + HandleScope scope(node_isolate); + + UNWRAP(TLSCallbacks); + + SSL_SESSION* sess = SSL_get_session(wrap->ssl_); + if (!sess) + return Undefined(node_isolate); + + int slen = i2d_SSL_SESSION(sess, NULL); + assert(slen > 0); + + if (slen > 0) { + unsigned char* sbuf = new unsigned char[slen]; + unsigned char* p = sbuf; + i2d_SSL_SESSION(sess, &p); + Local s = Encode(sbuf, slen, BINARY); + delete[] sbuf; + return scope.Close(s); + } + + return Null(node_isolate); +} + + +Handle TLSCallbacks::SetSession(const Arguments& args) { + HandleScope scope(node_isolate); + + UNWRAP(TLSCallbacks); + + if (wrap->started_) + return ThrowError("Already started."); + + if (args.Length() < 1 || + (!args[0]->IsString() && !Buffer::HasInstance(args[0]))) { + return ThrowTypeError("Bad argument"); + } + + size_t slen = Buffer::Length(args[0]); + char* sbuf = new char[slen]; + + ssize_t wlen = DecodeWrite(sbuf, slen, args[0], BINARY); + assert(wlen == static_cast(slen)); + + const unsigned char* p = reinterpret_cast(sbuf); + SSL_SESSION* sess = d2i_SSL_SESSION(NULL, &p, wlen); + + delete[] sbuf; + + if (!sess) + return Undefined(node_isolate); + + int r = SSL_set_session(wrap->ssl_, sess); + SSL_SESSION_free(sess); + + if (!r) { + Local eStr = String::New("SSL_set_session error"); + return ThrowException(Exception::Error(eStr)); + } + + return True(node_isolate); +} + + +Handle TLSCallbacks::GetCurrentCipher(const Arguments& args) { + HandleScope scope(node_isolate); + + UNWRAP(TLSCallbacks); + + const SSL_CIPHER* c; + + c = SSL_get_current_cipher(wrap->ssl_); + if (c == NULL) + return Undefined(node_isolate); + + const char* cipher_name = SSL_CIPHER_get_name(c); + const char* cipher_version = SSL_CIPHER_get_version(c); + + Local info = Object::New(); + info->Set(name_sym, String::New(cipher_name)); + info->Set(version_sym, String::New(cipher_version)); + return scope.Close(info); +} + + +#ifdef OPENSSL_NPN_NEGOTIATED +int TLSCallbacks::AdvertiseNextProtoCallback(SSL* s, + const unsigned char** data, + unsigned int* len, + void* arg) { + TLSCallbacks* p = static_cast(arg); + + if (p->npn_protos_.IsEmpty()) { + // No initialization - no NPN protocols + *data = reinterpret_cast(""); + *len = 0; + } else { + *data = reinterpret_cast( + Buffer::Data(p->npn_protos_)); + *len = Buffer::Length(p->npn_protos_); + } + + return SSL_TLSEXT_ERR_OK; +} + + +int TLSCallbacks::SelectNextProtoCallback(SSL* s, + unsigned char** out, + unsigned char* outlen, + const unsigned char* in, + unsigned int inlen, + void* arg) { + TLSCallbacks* p = static_cast(arg); + + // Release old protocol handler if present + if (!p->selected_npn_proto_.IsEmpty()) { + p->selected_npn_proto_.Dispose(node_isolate); + } + + if (p->npn_protos_.IsEmpty()) { + // We should at least select one protocol + // If server is using NPN + *out = reinterpret_cast(const_cast("http/1.1")); + *outlen = 8; + + // set status: unsupported + p->selected_npn_proto_ = Persistent::New(node_isolate, + False(node_isolate)); + + return SSL_TLSEXT_ERR_OK; + } + + const unsigned char* npn_protos = + reinterpret_cast(Buffer::Data(p->npn_protos_)); + size_t len = Buffer::Length(p->npn_protos_); + + int status = SSL_select_next_proto(out, outlen, in, inlen, npn_protos, len); + Handle result; + switch (status) { + case OPENSSL_NPN_UNSUPPORTED: + result = Null(node_isolate); + break; + case OPENSSL_NPN_NEGOTIATED: + result = String::New(reinterpret_cast(*out), *outlen); + break; + case OPENSSL_NPN_NO_OVERLAP: + result = False(node_isolate); + break; + default: + break; + } + + if (!result.IsEmpty()) + p->selected_npn_proto_ = Persistent::New(node_isolate, result); + + return SSL_TLSEXT_ERR_OK; +} + + +Handle TLSCallbacks::GetNegotiatedProto(const Arguments& args) { + HandleScope scope(node_isolate); + + UNWRAP(TLSCallbacks); + + if (wrap->kind_ == kTLSClient) { + if (wrap->selected_npn_proto_.IsEmpty()) + return Undefined(node_isolate); + else + return wrap->selected_npn_proto_; + } + + const unsigned char* npn_proto; + unsigned int npn_proto_len; + + SSL_get0_next_proto_negotiated(wrap->ssl_, &npn_proto, &npn_proto_len); + + if (!npn_proto) + return False(node_isolate); + + return scope.Close(String::New(reinterpret_cast(npn_proto), + npn_proto_len)); +} + + +Handle TLSCallbacks::SetNPNProtocols(const Arguments& args) { + HandleScope scope(node_isolate); + + UNWRAP(TLSCallbacks); + + if (args.Length() < 1 || !Buffer::HasInstance(args[0])) + return ThrowTypeError("Must give a Buffer as first argument"); + + // Release old handle + if (!wrap->npn_protos_.IsEmpty()) + wrap->npn_protos_.Dispose(node_isolate); + + wrap->npn_protos_ = + Persistent::New(node_isolate, args[0]->ToObject()); + + return True(node_isolate); +} +#endif // OPENSSL_NPN_NEGOTIATED + + +#ifdef SSL_CTRL_SET_TLSEXT_SERVERNAME_CB +Handle TLSCallbacks::GetServername(const Arguments& args) { + HandleScope scope(node_isolate); + + UNWRAP(TLSCallbacks); + + if (wrap->kind_ == kTLSServer && !wrap->servername_.IsEmpty()) { + return wrap->servername_; + } else { + return False(node_isolate); + } +} + + +Handle TLSCallbacks::SetServername(const Arguments& args) { + HandleScope scope(node_isolate); + + UNWRAP(TLSCallbacks); + + if (args.Length() < 1 || !args[0]->IsString()) + return ThrowTypeError("First argument should be a string"); + + if (wrap->started_) + return ThrowException(Exception::Error(String::New("Already started."))); + + if (wrap->kind_ != kTLSClient) + return False(node_isolate); + +#ifdef SSL_CTRL_SET_TLSEXT_SERVERNAME_CB + String::Utf8Value servername(args[0].As()); + SSL_set_tlsext_host_name(wrap->ssl_, *servername); +#endif // SSL_CTRL_SET_TLSEXT_SERVERNAME_CB + + return Null(node_isolate); +} + + +int TLSCallbacks::SelectSNIContextCallback(SSL* s, int* ad, void* arg) { + HandleScope scope(node_isolate); + + TLSCallbacks* p = static_cast(arg); + + const char* servername = SSL_get_servername(s, TLSEXT_NAMETYPE_host_name); + + if (servername) { + if (!p->servername_.IsEmpty()) + p->servername_.Dispose(node_isolate); + p->servername_ = Persistent::New(node_isolate, + String::New(servername)); + + // Call the SNI callback and use its return value as context + if (p->handle_->Has(onsniselect_sym)) { + if (!p->sni_context_.IsEmpty()) + p->sni_context_.Dispose(node_isolate); + + // Get callback init args + Local argv[1] = {*p->servername_}; + + // Call it + Local ret = Local::New(node_isolate, + MakeCallback(p->handle_, + onsniselect_sym, + ARRAY_SIZE(argv), + argv)); + + // If ret is SecureContext + if (ret->IsUndefined()) + return SSL_TLSEXT_ERR_NOACK; + + p->sni_context_ = Persistent::New(node_isolate, ret); + SecureContext* sc = ObjectWrap::Unwrap(ret.As()); + SSL_set_SSL_CTX(s, sc->ctx_); + } + } + + return SSL_TLSEXT_ERR_OK; +} +#endif // SSL_CTRL_SET_TLSEXT_SERVERNAME_CB + + +void TLSCallbacks::Initialize(Handle target) { + HandleScope scope(node_isolate); + + NODE_SET_METHOD(target, "wrap", TLSCallbacks::Wrap); + + Local t = FunctionTemplate::New(); + t->InstanceTemplate()->SetInternalFieldCount(1); + t->SetClassName(String::NewSymbol("TLSWrap")); + + NODE_SET_PROTOTYPE_METHOD(t, "start", Start); + 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, "getCurrentCipher", GetCurrentCipher); + NODE_SET_PROTOTYPE_METHOD(t, "verifyError", VerifyError); + NODE_SET_PROTOTYPE_METHOD(t, "setVerifyMode", SetVerifyMode); + NODE_SET_PROTOTYPE_METHOD(t, "isSessionReused", IsSessionReused); + +#ifdef OPENSSL_NPN_NEGOTIATED + NODE_SET_PROTOTYPE_METHOD(t, "getNegotiatedProtocol", GetNegotiatedProto); + NODE_SET_PROTOTYPE_METHOD(t, "setNPNProtocols", SetNPNProtocols); +#endif // OPENSSL_NPN_NEGOTIATED + +#ifdef SSL_CTRL_SET_TLSEXT_SERVERNAME_CB + NODE_SET_PROTOTYPE_METHOD(t, "getServername", GetServername); + NODE_SET_PROTOTYPE_METHOD(t, "setServername", SetServername); +#endif // SSL_CRT_SET_TLSEXT_SERVERNAME_CB + + tlsWrap = Persistent::New(node_isolate, t->GetFunction()); + + onread_sym = NODE_PSYMBOL("onread"); + onsniselect_sym = NODE_PSYMBOL("onsniselect"); + onerror_sym = NODE_PSYMBOL("onerror"); + onhandshakestart_sym = NODE_PSYMBOL("onhandshakestart"); + onhandshakedone_sym = NODE_PSYMBOL("onhandshakedone"); + + subject_sym = NODE_PSYMBOL("subject"); + issuer_sym = NODE_PSYMBOL("issuer"); + valid_from_sym = NODE_PSYMBOL("valid_from"); + valid_to_sym = NODE_PSYMBOL("valid_to"); + subjectaltname_sym = NODE_PSYMBOL("subjectaltname"); + modulus_sym = NODE_PSYMBOL("modulus"); + exponent_sym = NODE_PSYMBOL("exponent"); + fingerprint_sym = NODE_PSYMBOL("fingerprint"); + name_sym = NODE_PSYMBOL("name"); + version_sym = NODE_PSYMBOL("version"); + ext_key_usage_sym = NODE_PSYMBOL("ext_key_usage"); +} + +} // namespace node + +NODE_MODULE(node_tls_wrap, node::TLSCallbacks::Initialize) diff --git a/src/tls_wrap.h b/src/tls_wrap.h new file mode 100644 index 0000000000..86507567b8 --- /dev/null +++ b/src/tls_wrap.h @@ -0,0 +1,154 @@ +// 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. + +#ifndef SRC_TLS_WRAP_H_ +#define SRC_TLS_WRAP_H_ + +#include + +#include "v8.h" +#include "stream_wrap.h" +#include "queue.h" + +namespace node { + +// Forward-declarations +class NodeBIO; +class WriteWrap; +namespace crypto { + class SecureContext; +} + +class TLSCallbacks : public StreamWrapCallbacks { + public: + enum Kind { + kTLSClient, + kTLSServer + }; + + static void Initialize(v8::Handle target); + + int DoWrite(WriteWrap* w, + uv_buf_t* bufs, + size_t count, + uv_stream_t* send_handle, + uv_write_cb cb); + void AfterWrite(WriteWrap* w); + uv_buf_t DoAlloc(uv_handle_t* handle, size_t suggested_size); + void DoRead(uv_stream_t* handle, + ssize_t nread, + uv_buf_t buf, + uv_handle_type pending); + int DoShutdown(ShutdownWrap* req_wrap, uv_shutdown_cb cb); + + protected: + static const int kClearOutChunkSize = 1024; + + class WriteItem { + public: + WriteItem(WriteWrap* w, uv_write_cb cb) : w_(w), cb_(cb) { + } + ~WriteItem() { + w_ = NULL; + cb_ = NULL; + } + + WriteWrap* w_; + uv_write_cb cb_; + QUEUE member_; + }; + + TLSCallbacks(Kind kind, v8::Handle sc, StreamWrapCallbacks* old); + ~TLSCallbacks(); + + static void SSLInfoCallback(const SSL* ssl_, int where, int ret); + void InitSSL(); + void EncOut(); + static void EncOutCb(uv_write_t* req, int status); + bool ClearIn(); + void ClearOut(); + void InvokeQueued(int status); + + v8::Handle GetSSLError(int status, int* err); + + static v8::Handle Wrap(const v8::Arguments& args); + static v8::Handle Start(const v8::Arguments& args); + static v8::Handle GetPeerCertificate(const v8::Arguments& args); + static v8::Handle GetSession(const v8::Arguments& args); + static v8::Handle SetSession(const v8::Arguments& args); + static v8::Handle LoadSession(const v8::Arguments& args); + static v8::Handle GetCurrentCipher(const v8::Arguments& args); + static v8::Handle VerifyError(const v8::Arguments& args); + static v8::Handle SetVerifyMode(const v8::Arguments& args); + static v8::Handle IsSessionReused(const v8::Arguments& args); + +#ifdef OPENSSL_NPN_NEGOTIATED + static v8::Handle GetNegotiatedProto(const v8::Arguments& args); + static v8::Handle SetNPNProtocols(const v8::Arguments& args); + static int AdvertiseNextProtoCallback(SSL* s, + const unsigned char** data, + unsigned int* len, + void* arg); + static int SelectNextProtoCallback(SSL* s, + unsigned char** out, + unsigned char* outlen, + const unsigned char* in, + unsigned int inlen, + void* arg); +#endif // OPENSSL_NPN_NEGOTIATED + +#ifdef SSL_CTRL_SET_TLSEXT_SERVERNAME_CB + static v8::Handle GetServername(const v8::Arguments& args); + static v8::Handle SetServername(const v8::Arguments& args); + static int SelectSNIContextCallback(SSL* s, int* ad, void* arg); +#endif // SSL_CTRL_SET_TLSEXT_SERVERNAME_CB + + Kind kind_; + crypto::SecureContext* sc_; + v8::Persistent sc_handle_; + v8::Persistent handle_; + SSL* ssl_; + BIO* enc_in_; + BIO* enc_out_; + NodeBIO* clear_in_; + uv_write_t write_req_; + size_t write_size_; + size_t write_queue_size_; + QUEUE write_item_queue_; + WriteItem* pending_write_item_; + bool started_; + bool established_; + bool shutdown_; + +#ifdef OPENSSL_NPN_NEGOTIATED + v8::Persistent npn_protos_; + v8::Persistent selected_npn_proto_; +#endif // OPENSSL_NPN_NEGOTIATED + +#ifdef SSL_CTRL_SET_TLSEXT_SERVERNAME_CB + v8::Persistent servername_; + v8::Persistent sni_context_; +#endif // SSL_CTRL_SET_TLSEXT_SERVERNAME_CB +}; + +} // namespace node + +#endif // SRC_TLS_WRAP_H_