From 8e28193cc239a8389a1edda6810402005c1336cc Mon Sep 17 00:00:00 2001 From: Fedor Indutny Date: Fri, 2 Aug 2013 16:16:13 +0400 Subject: [PATCH] tls_wrap: DRY ClientHelloParser Share ClientHelloParser code between `tls_wrap.cc` and `node_crypto.cc`. fix #5959 --- node.gyp | 2 + src/node_crypto.cc | 179 +++++----------------- src/node_crypto.h | 55 ++----- src/node_crypto_clienthello-inl.h | 74 +++++++++ src/node_crypto_clienthello.cc | 240 +++++++++++++++++++++++++++++ src/node_crypto_clienthello.h | 124 +++++++++++++++ src/tls_wrap.cc | 247 +++++------------------------- src/tls_wrap.h | 29 +--- 8 files changed, 536 insertions(+), 414 deletions(-) create mode 100644 src/node_crypto_clienthello-inl.h create mode 100644 src/node_crypto_clienthello.cc create mode 100644 src/node_crypto_clienthello.h diff --git a/node.gyp b/node.gyp index b06f692a49..d6aefae26d 100644 --- a/node.gyp +++ b/node.gyp @@ -161,8 +161,10 @@ 'sources': [ 'src/node_crypto.cc', 'src/node_crypto_bio.cc', + 'src/node_crypto_clienthello.cc', 'src/node_crypto.h', 'src/node_crypto_bio.h', + 'src/node_crypto_clienthello.h', 'src/tls_wrap.cc', 'src/tls_wrap.h' ], diff --git a/src/node_crypto.cc b/src/node_crypto.cc index e68d91dccb..a4687a9d18 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -794,149 +794,38 @@ void SecureContext::SetTicketKeys(const FunctionCallbackInfo& args) { } -size_t ClientHelloParser::Write(const uint8_t* data, size_t len) { - HandleScope scope(node_isolate); - - // Just accumulate data, everything will be pushed to BIO later - if (state_ == kPaused) return 0; - - // Copy incoming data to the internal buffer - // (which has a size of the biggest possible TLS frame) - size_t available = sizeof(data_) - offset_; - size_t copied = len < available ? len : available; - memcpy(data_ + offset_, data, copied); - offset_ += copied; - - // Vars for parsing hello - bool is_clienthello = false; - uint8_t session_size = -1; - uint8_t* session_id = NULL; - Local hello; - Handle argv[1]; - - switch (state_) { - case kWaiting: - // >= 5 bytes for header parsing - if (offset_ < 5) - break; - - if (data_[0] == kChangeCipherSpec || - data_[0] == kAlert || - data_[0] == kHandshake || - data_[0] == kApplicationData) { - frame_len_ = (data_[3] << 8) + data_[4]; - state_ = kTLSHeader; - body_offset_ = 5; - } else { - frame_len_ = (data_[0] << 8) + data_[1]; - state_ = kSSLHeader; - if (*data_ & 0x40) { - // header with padding - body_offset_ = 3; - } else { - // without padding - body_offset_ = 2; - } - } - - // Sanity check (too big frame, or too small) - if (frame_len_ >= sizeof(data_)) { - // Let OpenSSL handle it - Finish(); - return copied; - } - case kTLSHeader: - case kSSLHeader: - // >= 5 + frame size bytes for frame parsing - if (offset_ < body_offset_ + frame_len_) - break; - - // Skip unsupported frames and gather some data from frame - - if (data_[body_offset_] == kClientHello) { - is_clienthello = true; - uint8_t* body; - size_t session_offset; - - if (state_ == kTLSHeader) { - // Skip frame header, hello header, protocol version and random data - session_offset = body_offset_ + 4 + 2 + 32; - - if (session_offset + 1 < offset_) { - body = data_ + session_offset; - session_size = *body; - session_id = body + 1; - } - } else if (state_ == kSSLHeader) { - // Skip header, version - session_offset = body_offset_ + 3; - - if (session_offset + 4 < offset_) { - body = data_ + session_offset; - - int ciphers_size = (body[0] << 8) + body[1]; - - if (body + 4 + ciphers_size < data_ + offset_) { - 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_ + offset_) { - Finish(); - return copied; - } - } +void Connection::OnClientHello(void* arg, + const ClientHelloParser::ClientHello& hello) { + HandleScope scope(node_isolate); + Connection* c = static_cast(arg); - // Not client hello - let OpenSSL handle it - if (!is_clienthello) { - Finish(); - return copied; - } + if (onclienthello_sym.IsEmpty()) + onclienthello_sym = String::New("onclienthello"); + if (sessionid_sym.IsEmpty()) + sessionid_sym = String::New("sessionId"); - // Parse frame, call javascript handler and - // move parser into the paused state - if (onclienthello_sym.IsEmpty()) - onclienthello_sym = String::New("onclienthello"); - if (sessionid_sym.IsEmpty()) - sessionid_sym = String::New("sessionId"); - - state_ = kPaused; - hello = Object::New(); - hello->Set(sessionid_sym, - Buffer::New(reinterpret_cast(session_id), - session_size)); - - argv[0] = hello; - MakeCallback(conn_->handle(node_isolate), - onclienthello_sym, - ARRAY_SIZE(argv), - argv); - break; - case kEnded: - default: - break; - } + Local obj = Object::New(); + obj->Set(sessionid_sym, + Buffer::New(reinterpret_cast(hello.session_id()), + hello.session_size())); - return copied; + Handle argv[1] = { obj }; + MakeCallback(c->handle(node_isolate), + onclienthello_sym, + ARRAY_SIZE(argv), + argv); } -void ClientHelloParser::Finish() { - assert(state_ != kEnded); - state_ = kEnded; +void Connection::OnClientHelloParseEnd(void* arg) { + Connection* c = static_cast(arg); // Write all accumulated data - int r = BIO_write(conn_->bio_read_, reinterpret_cast(data_), offset_); - conn_->HandleBIOError(conn_->bio_read_, "BIO_write", r); - conn_->SetShutdownFlags(); + int r = BIO_write(c->bio_read_, + reinterpret_cast(c->hello_data_), + c->hello_offset_); + c->HandleBIOError(c->bio_read_, "BIO_write", r); + c->SetShutdownFlags(); } @@ -1398,9 +1287,21 @@ void Connection::EncIn(const FunctionCallbackInfo& args) { int bytes_written; char* data = buffer_data + off; - if (ss->is_server_ && !ss->hello_parser_.ended()) { - bytes_written = ss->hello_parser_.Write(reinterpret_cast(data), - len); + if (ss->is_server_ && !ss->hello_parser_.IsEnded()) { + // Just accumulate data, everything will be pushed to BIO later + if (ss->hello_parser_.IsPaused()) { + bytes_written = 0; + } else { + // Copy incoming data to the internal buffer + // (which has a size of the biggest possible TLS frame) + size_t available = sizeof(ss->hello_data_) - ss->hello_offset_; + size_t copied = len < available ? len : available; + memcpy(ss->hello_data_ + ss->hello_offset_, data, copied); + ss->hello_offset_ += copied; + + ss->hello_parser_.Parse(ss->hello_data_, ss->hello_offset_); + bytes_written = copied; + } } else { bytes_written = BIO_write(ss->bio_read_, data, len); ss->HandleBIOError(ss->bio_read_, "BIO_write", bytes_written); @@ -1766,7 +1667,7 @@ void Connection::LoadSession(const FunctionCallbackInfo& args) { ss->next_sess_ = sess; } - ss->hello_parser_.Finish(); + ss->hello_parser_.End(); } diff --git a/src/node_crypto.h b/src/node_crypto.h index aa275cc32d..d695994ce3 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -23,6 +23,8 @@ #define SRC_NODE_CRYPTO_H_ #include "node.h" +#include "node_crypto_clienthello.h" // ClientHelloParser +#include "node_crypto_clienthello-inl.h" #include "node_object_wrap.h" #ifdef OPENSSL_NPN_NEGOTIATED @@ -117,49 +119,6 @@ class SecureContext : ObjectWrap { private: }; -class ClientHelloParser { - public: - enum FrameType { - kChangeCipherSpec = 20, - kAlert = 21, - kHandshake = 22, - kApplicationData = 23, - kOther = 255 - }; - - enum HandshakeType { - kClientHello = 1 - }; - - enum ParseState { - kWaiting, - kTLSHeader, - kSSLHeader, - kPaused, - kEnded - }; - - explicit ClientHelloParser(Connection* c) : conn_(c), - state_(kWaiting), - offset_(0), - body_offset_(0) { - } - - size_t Write(const uint8_t* data, size_t len); - void Finish(); - - inline bool ended() { return state_ == kEnded; } - - private: - Connection* conn_; - ParseState state_; - size_t frame_len_; - - uint8_t data_[18432]; - size_t offset_; - size_t body_offset_; -}; - class Connection : ObjectWrap { public: static void Initialize(v8::Handle target); @@ -221,6 +180,10 @@ class Connection : ObjectWrap { static int SelectSNIContextCallback_(SSL* s, int* ad, void* arg); #endif + static void OnClientHello(void* arg, + const ClientHelloParser::ClientHello& hello); + static void OnClientHelloParseEnd(void* arg); + int HandleBIOError(BIO* bio, const char* func, int rv); enum ZeroStatus { @@ -244,10 +207,11 @@ class Connection : ObjectWrap { return ss; } - Connection() : ObjectWrap(), hello_parser_(this) { + Connection() : ObjectWrap(), hello_offset_(0) { bio_read_ = bio_write_ = NULL; ssl_ = NULL; next_sess_ = NULL; + hello_parser_.Start(OnClientHello, OnClientHelloParseEnd, this); } ~Connection() { @@ -285,6 +249,9 @@ class Connection : ObjectWrap { bool is_server_; /* coverity[member_decl] */ SSL_SESSION* next_sess_; + uint8_t hello_data_[18432]; + size_t hello_offset_; + friend class ClientHelloParser; friend class SecureContext; }; diff --git a/src/node_crypto_clienthello-inl.h b/src/node_crypto_clienthello-inl.h new file mode 100644 index 0000000000..82c0d2782c --- /dev/null +++ b/src/node_crypto_clienthello-inl.h @@ -0,0 +1,74 @@ +// 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_NODE_CRYPTO_CLIENTHELLO_INL_H_ +#define SRC_NODE_CRYPTO_CLIENTHELLO_INL_H_ + +#include + +namespace node { + +inline void ClientHelloParser::Reset() { + frame_len_ = 0; + body_offset_ = 0; + extension_offset_ = 0; + session_size_ = 0; + session_id_ = NULL; + tls_ticket_size_ = -1; + tls_ticket_ = NULL; +} + +inline void ClientHelloParser::Start(ClientHelloParser::OnHelloCb onhello_cb, + ClientHelloParser::OnEndCb onend_cb, + void* onend_arg) { + if (!IsEnded()) + return; + Reset(); + + assert(onhello_cb != NULL); + + state_ = kWaiting; + onhello_cb_ = onhello_cb; + onend_cb_ = onend_cb; + cb_arg_ = onend_arg; +} + +inline void ClientHelloParser::End() { + if (state_ == kEnded) + return; + state_ = kEnded; + if (onend_cb_ != NULL) { + onend_cb_(cb_arg_); + onend_cb_ = NULL; + } +} + +inline bool ClientHelloParser::IsEnded() const { + return state_ == kEnded; +} + +inline bool ClientHelloParser::IsPaused() const { + return state_ == kPaused; +} + +} // namespace node + +#endif // SRC_NODE_CRYPTO_CLIENTHELLO_INL_H_ diff --git a/src/node_crypto_clienthello.cc b/src/node_crypto_clienthello.cc new file mode 100644 index 0000000000..5c1ecfaa97 --- /dev/null +++ b/src/node_crypto_clienthello.cc @@ -0,0 +1,240 @@ +// 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 "node_crypto_clienthello.h" +#include "node_crypto_clienthello-inl.h" +#include "node_buffer.h" // Buffer + +namespace node { + +void ClientHelloParser::Parse(const uint8_t* data, size_t avail) { + switch (state_) { + case kWaiting: + if (!ParseRecordHeader(data, avail)) + break; + // Fall through + case kTLSHeader: + case kSSL2Header: + ParseHeader(data, avail); + break; + case kPaused: + // Just nop + case kEnded: + // Already ended, just ignore it + break; + default: + break; + } +} + + +bool ClientHelloParser::ParseRecordHeader(const uint8_t* data, size_t avail) { + // >= 5 bytes for header parsing + if (avail < 5) + return false; + + if (data[0] == kChangeCipherSpec || + data[0] == kAlert || + data[0] == kHandshake || + data[0] == kApplicationData) { + frame_len_ = (data[3] << 8) + data[4]; + state_ = kTLSHeader; + body_offset_ = 5; + } else { +#ifdef OPENSSL_NO_SSL2 + frame_len_ = ((data[0] << 8) & kSSL2HeaderMask) + data[1]; + state_ = kSSL2Header; + if (data[0] & kSSL2TwoByteHeaderBit) { + // header without padding + body_offset_ = 2; + } else { + // header with padding + body_offset_ = 3; + } +#else + End(); + return false; +#endif // OPENSSL_NO_SSL2 + } + + // Sanity check (too big frame, or too small) + // Let OpenSSL handle it + if (frame_len_ >= kMaxTLSFrameLen) { + End(); + return false; + } + + return true; +} + + +void ClientHelloParser::ParseHeader(const uint8_t* data, size_t avail) { + // >= 5 + frame size bytes for frame parsing + if (body_offset_ + frame_len_ > avail) + return; + + // Skip unsupported frames and gather some data from frame + + // TODO(indutny): Check hello protocol version + if (data[body_offset_] == kClientHello) { + if (state_ == kTLSHeader) { + if (!ParseTLSClientHello(data, avail)) + return End(); + } else if (state_ == kSSL2Header) { +#ifdef OPENSSL_NO_SSL2 + if (!ParseSSL2ClientHello(data, avail)) + return End(); +#else + abort(); // Unreachable +#endif // OPENSSL_NO_SSL2 + } else { + // We couldn't get here, but whatever + return End(); + } + + // 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 End(); + } + } + + state_ = kPaused; + ClientHello hello; + hello.session_id_ = session_id_; + hello.session_size_ = session_size_; + hello.has_ticket_ = tls_ticket_ != NULL && tls_ticket_size_ != 0; + onhello_cb_(cb_arg_, hello); +} + + +void ClientHelloParser::ParseExtension(ClientHelloParser::ExtensionType type, + const uint8_t* data, + size_t len) { + // NOTE: In case of anything we're just returning back, ignoring the problem. + // That's because we're heavily relying on OpenSSL to solve any problem with + // incoming data. + switch (type) { + case kTLSSessionTicket: + tls_ticket_size_ = len; + tls_ticket_ = data + len; + break; + default: + // Ignore + break; + } +} + + +bool ClientHelloParser::ParseTLSClientHello(const uint8_t* data, size_t avail) { + const uint8_t* body; + + // Skip frame header, hello header, protocol version and random data + size_t session_offset = body_offset_ + 4 + 2 + 32; + + if (session_offset + 1 >= avail) + return false; + + body = data + session_offset; + session_size_ = *body; + session_id_ = body + 1; + + size_t cipher_offset = session_offset + 1 + session_size_; + + // Session OOB failure + if (cipher_offset + 1 >= avail) + return false; + + uint16_t cipher_len = + (data[cipher_offset] << 8) + data[cipher_offset + 1]; + size_t comp_offset = cipher_offset + 2 + cipher_len; + + // Cipher OOB failure + if (comp_offset >= avail) + return false; + + uint8_t comp_len = data[comp_offset]; + size_t extension_offset = comp_offset + 1 + comp_len; + + // Compression OOB failure + if (extension_offset > avail) + return false; + + // No extensions present + if (extension_offset == avail) + return true; + + size_t ext_off = extension_offset + 2; + + // Parse known extensions + while (ext_off < avail) { + // Extension OOB + if (ext_off + 4 > avail) + return false; + + uint16_t ext_type = (data[ext_off] << 8) + data[ext_off + 1]; + uint16_t ext_len = (data[ext_off + 2] << 8) + data[ext_off + 3]; + ext_off += 4; + + // Extension OOB + if (ext_off + ext_len > avail) + return false; + + ParseExtension(static_cast(ext_type), + data + ext_off, + ext_len); + + ext_off += ext_len; + } + + // Extensions OOB failure + if (ext_off > avail) + return false; + + return true; +} + + +#ifdef OPENSSL_NO_SSL2 +bool ClientHelloParser::ParseSSL2ClientHello(const uint8_t* data, + size_t avail) { + const uint8_t* body; + + // Skip header, version + size_t session_offset = body_offset_ + 3; + + if (session_offset + 4 < avail) { + body = data + session_offset; + + uint16_t 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; + } + } + + return true; +} +#endif // OPENSSL_NO_SSL2 + +} // namespace node diff --git a/src/node_crypto_clienthello.h b/src/node_crypto_clienthello.h new file mode 100644 index 0000000000..6c98f5c22f --- /dev/null +++ b/src/node_crypto_clienthello.h @@ -0,0 +1,124 @@ +// 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_NODE_CRYPTO_CLIENTHELLO_H_ +#define SRC_NODE_CRYPTO_CLIENTHELLO_H_ + +#include "node.h" + +#include // size_t +#include // NULL + +namespace node { + +class ClientHelloParser { + public: + ClientHelloParser() : state_(kEnded), + onhello_cb_(NULL), + onend_cb_(NULL), + cb_arg_(NULL) { + Reset(); + } + + class ClientHello { + public: + ClientHello() { + } + + 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_; } + + private: + uint8_t session_size_; + const uint8_t* session_id_; + bool has_ticket_; + + friend class ClientHelloParser; + }; + + typedef void (*OnHelloCb)(void* arg, const ClientHello& hello); + typedef void (*OnEndCb)(void* arg); + + void Parse(const uint8_t* data, size_t avail); + + inline void Reset(); + inline void Start(OnHelloCb onhello_cb, OnEndCb onend_cb, void* onend_arg); + inline void End(); + inline bool IsPaused() const; + inline bool IsEnded() const; + + private: + static const uint8_t kSSL2TwoByteHeaderBit = 0x80; + static const uint8_t kSSL2HeaderMask = 0x3f; + static const size_t kMaxTLSFrameLen = 16 * 1024 + 5; + static const size_t kMaxSSLExFrameLen = 32 * 1024; + + enum ParseState { + kWaiting, + kTLSHeader, + kSSL2Header, + kPaused, + kEnded + }; + + enum FrameType { + kChangeCipherSpec = 20, + kAlert = 21, + kHandshake = 22, + kApplicationData = 23, + kOther = 255 + }; + + enum HandshakeType { + kClientHello = 1 + }; + + enum ExtensionType { + kTLSSessionTicket = 35 + }; + + bool ParseRecordHeader(const uint8_t* data, size_t avail); + void ParseHeader(const uint8_t* data, size_t avail); + void ParseExtension(ExtensionType type, + const uint8_t* data, + size_t len); + bool ParseTLSClientHello(const uint8_t* data, size_t avail); +#ifdef OPENSSL_NO_SSL2 + bool ParseSSL2ClientHello(const uint8_t* data, size_t avail); +#endif // OPENSSL_NO_SSL2 + + ParseState state_; + OnHelloCb onhello_cb_; + OnEndCb onend_cb_; + void* cb_arg_; + size_t frame_len_; + size_t body_offset_; + size_t extension_offset_; + uint8_t session_size_; + const uint8_t* session_id_; + uint16_t tls_ticket_size_; + const uint8_t* tls_ticket_; +}; + +} // namespace node + +#endif // SRC_NODE_CRYPTO_CLIENTHELLO_H_ diff --git a/src/tls_wrap.cc b/src/tls_wrap.cc index f7fe4e1e1c..0fbb7e5741 100644 --- a/src/tls_wrap.cc +++ b/src/tls_wrap.cc @@ -23,6 +23,8 @@ #include "node_buffer.h" // Buffer #include "node_crypto.h" // SecureContext #include "node_crypto_bio.h" // NodeBIO +#include "node_crypto_clienthello.h" // ClientHelloParser +#include "node_crypto_clienthello-inl.h" #include "node_wrap.h" // WithGenericStream #include "node_counters.h" @@ -102,11 +104,6 @@ TLSCallbacks::TLSCallbacks(Kind kind, // 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); @@ -371,7 +368,7 @@ void TLSCallbacks::SSLInfoCallback(const SSL* ssl_, int where, int ret) { void TLSCallbacks::EncOut() { // Ignore cycling data if ClientHello wasn't yet parsed - if (hello_.state != kParseEnded) + if (!hello_.IsEnded()) return; // Write in progress @@ -474,7 +471,7 @@ Handle TLSCallbacks::GetSSLError(int status, int* err) { void TLSCallbacks::ClearOut() { // Ignore cycling data if ClientHello wasn't yet parsed - if (hello_.state != kParseEnded) + if (!hello_.IsEnded()) return; HandleScope scope(node_isolate); @@ -506,7 +503,7 @@ void TLSCallbacks::ClearOut() { bool TLSCallbacks::ClearIn() { // Ignore cycling data if ClientHello wasn't yet parsed - if (hello_.state != kParseEnded) + if (!hello_.IsEnded()) return false; HandleScope scope(node_isolate); @@ -638,11 +635,17 @@ void TLSCallbacks::DoRead(uv_stream_t* handle, assert(ssl_ != NULL); // Commit read data - NodeBIO::FromBIO(enc_in_)->Commit(nread); + NodeBIO* enc_in = NodeBIO::FromBIO(enc_in_); + enc_in->Commit(nread); // Parse ClientHello first - if (hello_.state != kParseEnded) - return ParseClientHello(); + if (!hello_.IsEnded()) { + assert(session_callbacks_); + size_t avail = 0; + uint8_t* data = reinterpret_cast(enc_in->Peek(&avail)); + assert(avail == 0 || data != NULL); + return hello_.Parse(data, avail); + } // Cycle OpenSSL's state Cycle(); @@ -658,198 +661,6 @@ int TLSCallbacks::DoShutdown(ShutdownWrap* req_wrap, uv_shutdown_cb cb) { } -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(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; - uint16_t tls_ticket_size = -1; - uint8_t* tls_ticket = NULL; - Local hello_obj; - Handle 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; - } - - size_t cipher_offset = session_offset + 1 + session_size; - - // Session OOB failure - if (cipher_offset + 1 >= avail) - return ParseFinish(); - - uint16_t cipher_len = - (data[cipher_offset] << 8) + data[cipher_offset + 1]; - size_t comp_offset = cipher_offset + 2 + cipher_len; - - // Cipher OOB failure - if (comp_offset >= avail) - return ParseFinish(); - - uint8_t comp_len = data[comp_offset]; - size_t extension_offset = comp_offset + 1 + comp_len; - - // Compression OOB failure - if (extension_offset > avail) - return ParseFinish(); - - // Extensions present - if (extension_offset != avail) { - size_t ext_off = extension_offset + 2; - - // Parse known extensions - while (ext_off < avail) { - // Extension OOB - if (avail - ext_off < 4) - return ParseFinish(); - - uint16_t ext_type = (data[ext_off] << 8) + data[ext_off + 1]; - uint16_t ext_len = (data[ext_off + 2] << 8) + data[ext_off + 3]; - - // Extension OOB - if (ext_off + ext_len + 4 > avail) - return ParseFinish(); - - ext_off += 4; - - // TLS Session Ticket - if (ext_type == 35) { - tls_ticket_size = ext_len; - tls_ticket = data + ext_off; - } - - ext_off += ext_len; - } - - // Extensions OOB failure - if (ext_off > avail) - return ParseFinish(); - } - } 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(session_id), - session_size)); - bool have_tls_ticket = (tls_ticket != NULL && tls_ticket_size != 0); - hello_obj->Set(tls_ticket_sym, Boolean::New(have_tls_ticket)); - - argv[0] = hello_obj; - MakeCallback(object(), onclienthello_sym, 1, argv); - } - break; - case kParseEnded: - default: - break; - } -} - - #define CASE_X509_ERR(CODE) case X509_V_ERR_##CODE: reason = #CODE; break; void TLSCallbacks::VerifyError(const FunctionCallbackInfo& args) { HandleScope scope(node_isolate); @@ -959,9 +770,31 @@ void TLSCallbacks::EnableSessionCallbacks( UNWRAP(TLSCallbacks); wrap->session_callbacks_ = true; - wrap->hello_.state = kParseWaiting; - wrap->hello_.frame_len = 0; - wrap->hello_.body_offset = 0; + wrap->hello_.Start(OnClientHello, OnClientHelloParseEnd, wrap); +} + + +void TLSCallbacks::OnClientHello(void* arg, + const ClientHelloParser::ClientHello& hello) { + HandleScope scope(node_isolate); + + TLSCallbacks* c = static_cast(arg); + + Local hello_obj = Object::New(); + Local buff = Buffer::New( + reinterpret_cast(hello.session_id()), + hello.session_size()); + hello_obj->Set(sessionid_sym, buff); + hello_obj->Set(tls_ticket_sym, Boolean::New(hello.has_ticket())); + + Handle argv[1] = { hello_obj }; + MakeCallback(c->object(), onclienthello_sym, 1, argv); +} + + +void TLSCallbacks::OnClientHelloParseEnd(void* arg) { + TLSCallbacks* c = static_cast(arg); + c->Cycle(); } @@ -1168,7 +1001,7 @@ void TLSCallbacks::LoadSession(const FunctionCallbackInfo& args) { wrap->next_sess_ = sess; } - wrap->ParseFinish(); + wrap->hello_.End(); } diff --git a/src/tls_wrap.h b/src/tls_wrap.h index 6db4b6e41b..eb8d7ce15d 100644 --- a/src/tls_wrap.h +++ b/src/tls_wrap.h @@ -23,6 +23,7 @@ #define SRC_TLS_WRAP_H_ #include "node.h" +#include "node_crypto_clienthello.h" #include "queue.h" #include "stream_wrap.h" #include "v8.h" @@ -62,22 +63,6 @@ class TLSCallbacks : public StreamWrapCallbacks { protected: static const int kClearOutChunkSize = 1024; - static const size_t kMaxTLSFrameLen = 16 * 1024 + 5; - - // ClientHello parser types - enum ParseState { - kParseWaiting, - kParseTLSHeader, - kParseSSLHeader, - kParsePaused, - kParseEnded - }; - - struct HelloState { - ParseState state; - size_t frame_len; - size_t body_offset; - }; // Write callback queue's item class WriteItem { @@ -104,12 +89,6 @@ class TLSCallbacks : public StreamWrapCallbacks { bool ClearIn(); void ClearOut(); void InvokeQueued(int status); - void ParseClientHello(); - - inline void ParseFinish() { - hello_.state = kParseEnded; - Cycle(); - } inline void Cycle() { ClearIn(); @@ -118,6 +97,9 @@ class TLSCallbacks : public StreamWrapCallbacks { } v8::Handle GetSSLError(int status, int* err); + static void OnClientHello(void* arg, + const ClientHelloParser::ClientHello& hello); + static void OnClientHelloParseEnd(void* arg); static void Wrap(const v8::FunctionCallbackInfo& args); static void Start(const v8::FunctionCallbackInfo& args); @@ -183,13 +165,12 @@ class TLSCallbacks : public StreamWrapCallbacks { size_t write_queue_size_; QUEUE write_item_queue_; WriteItem* pending_write_item_; - HelloState hello_; - int hello_body_; bool started_; bool established_; bool shutdown_; bool session_callbacks_; SSL_SESSION* next_sess_; + ClientHelloParser hello_; #ifdef OPENSSL_NPN_NEGOTIATED v8::Persistent npn_protos_;