diff options
author | sergeyu <sergeyu@chromium.org> | 2015-03-09 12:41:59 -0700 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2015-03-09 19:42:42 +0000 |
commit | 76d9000d15c864bb96db8adea06441689517aa00 (patch) | |
tree | e7796ccae2b22ead6124190d64eb91367b74fecb /remoting | |
parent | b48f18835a8253a6ef8402e4a646fe1663726496 (diff) | |
download | chromium_src-76d9000d15c864bb96db8adea06441689517aa00.zip chromium_src-76d9000d15c864bb96db8adea06441689517aa00.tar.gz chromium_src-76d9000d15c864bb96db8adea06441689517aa00.tar.bz2 |
Remove dependency on XMPP implementation in WebRTC
Now XMPP is implemented directly in remoting/signaling instead of
relying on WebRTC. The new code is mostly just a translation of
the XMPP implementation we use in the webapp from JavaScript to
C++ and uses the same layout and class names, except that the main
class is called XmppSignalStrategy instead of XmppConnection.
BUG=459756, 443806, 417336
Review URL: https://codereview.chromium.org/958703003
Cr-Commit-Position: refs/heads/master@{#319712}
Diffstat (limited to 'remoting')
-rw-r--r-- | remoting/remoting.gyp | 1 | ||||
-rw-r--r-- | remoting/remoting_nacl.gyp | 1 | ||||
-rw-r--r-- | remoting/remoting_srcs.gypi | 4 | ||||
-rw-r--r-- | remoting/remoting_test.gypi | 3 | ||||
-rw-r--r-- | remoting/signaling/signal_strategy.h | 1 | ||||
-rw-r--r-- | remoting/signaling/xmpp_login_handler.cc | 241 | ||||
-rw-r--r-- | remoting/signaling/xmpp_login_handler.h | 142 | ||||
-rw-r--r-- | remoting/signaling/xmpp_login_handler_unittest.cc | 223 | ||||
-rw-r--r-- | remoting/signaling/xmpp_signal_strategy.cc | 587 | ||||
-rw-r--r-- | remoting/signaling/xmpp_signal_strategy.h | 55 | ||||
-rw-r--r-- | remoting/signaling/xmpp_signal_strategy_unittest.cc | 268 | ||||
-rw-r--r-- | remoting/signaling/xmpp_stream_parser.cc | 171 | ||||
-rw-r--r-- | remoting/signaling/xmpp_stream_parser.h | 44 | ||||
-rw-r--r-- | remoting/signaling/xmpp_stream_parser_unittest.cc | 94 |
14 files changed, 1615 insertions, 220 deletions
diff --git a/remoting/remoting.gyp b/remoting/remoting.gyp index 61e260b..6d783c2 100644 --- a/remoting/remoting.gyp +++ b/remoting/remoting.gyp @@ -235,6 +235,7 @@ '../jingle/jingle.gyp:jingle_glue', '../jingle/jingle.gyp:notifier', '../net/net.gyp:net', + '../third_party/expat/expat.gyp:expat', '../third_party/libjingle/libjingle.gyp:libjingle', 'remoting_base', ], diff --git a/remoting/remoting_nacl.gyp b/remoting/remoting_nacl.gyp index 14af09a..5412772 100644 --- a/remoting/remoting_nacl.gyp +++ b/remoting/remoting_nacl.gyp @@ -105,6 +105,7 @@ '../native_client_sdk/native_client_sdk_untrusted.gyp:nacl_io_untrusted', '../net/net_nacl.gyp:net_nacl', '../third_party/boringssl/boringssl_nacl.gyp:boringssl_nacl', + '../third_party/expat/expat_nacl.gyp:expat_nacl', '../third_party/khronos/khronos.gyp:khronos_headers', '../third_party/libjingle/libjingle_nacl.gyp:libjingle_nacl', '../third_party/libvpx/libvpx_nacl.gyp:libvpx_nacl', diff --git a/remoting/remoting_srcs.gypi b/remoting/remoting_srcs.gypi index 9be7597..1248808a 100644 --- a/remoting/remoting_srcs.gypi +++ b/remoting/remoting_srcs.gypi @@ -203,6 +203,10 @@ 'signaling/signal_strategy.h', 'signaling/xmpp_signal_strategy.cc', 'signaling/xmpp_signal_strategy.h', + 'signaling/xmpp_stream_parser.cc', + 'signaling/xmpp_stream_parser.h', + 'signaling/xmpp_login_handler.cc', + 'signaling/xmpp_login_handler.h', ], 'remoting_client_sources': [ diff --git a/remoting/remoting_test.gypi b/remoting/remoting_test.gypi index 749ed5c..f0ac015 100644 --- a/remoting/remoting_test.gypi +++ b/remoting/remoting_test.gypi @@ -237,6 +237,9 @@ 'signaling/log_to_server_unittest.cc', 'signaling/server_log_entry_unittest.cc', 'signaling/server_log_entry_unittest.h', + 'signaling/xmpp_login_handler_unittest.cc', + 'signaling/xmpp_stream_parser_unittest.cc', + 'signaling/xmpp_signal_strategy_unittest.cc', 'test/access_token_fetcher_unittest.cc', 'test/app_remoting_test_driver_environment_unittest.cc', 'test/remote_host_info_fetcher_unittest.cc', diff --git a/remoting/signaling/signal_strategy.h b/remoting/signaling/signal_strategy.h index 4d54379..8d6649a 100644 --- a/remoting/signaling/signal_strategy.h +++ b/remoting/signaling/signal_strategy.h @@ -34,6 +34,7 @@ class SignalStrategy { OK, AUTHENTICATION_FAILED, NETWORK_ERROR, + PROTOCOL_ERROR, }; // Callback interface for signaling event. Event handlers are not diff --git a/remoting/signaling/xmpp_login_handler.cc b/remoting/signaling/xmpp_login_handler.cc new file mode 100644 index 0000000..cc3ae94 --- /dev/null +++ b/remoting/signaling/xmpp_login_handler.cc @@ -0,0 +1,241 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "remoting/signaling/xmpp_login_handler.h" + +#include "base/base64.h" +#include "base/bind.h" +#include "base/logging.h" +#include "remoting/signaling/xmpp_stream_parser.h" +#include "third_party/webrtc/libjingle/xmllite/xmlelement.h" + +// Undefine SendMessage and ERROR defined in Windows headers. +#ifdef SendMessage +#undef SendMessage +#endif + +#ifdef ERROR +#undef ERROR +#endif + +namespace remoting { + +const char kOAuthMechanism[] = "X-OAUTH2"; + +buzz::StaticQName kXmppIqName = {"jabber:client", "iq"}; + +char kXmppBindNs[] = "urn:ietf:params:xml:ns:xmpp-bind"; +buzz::StaticQName kXmppBindName = {kXmppBindNs, "bind"}; +buzz::StaticQName kXmppJidName = {kXmppBindNs, "jid"}; + +buzz::StaticQName kJabberFeaturesName = {"http://etherx.jabber.org/streams", + "features"}; + +char kXmppTlsNs[] = "urn:ietf:params:xml:ns:xmpp-tls"; +buzz::StaticQName kStartTlsName = {kXmppTlsNs, "starttls"}; +buzz::StaticQName kTlsProceedName = {kXmppTlsNs, "proceed"}; + +char kXmppSaslNs[] = "urn:ietf:params:xml:ns:xmpp-sasl"; +buzz::StaticQName kSaslMechanismsName = {kXmppSaslNs, "mechanisms"}; +buzz::StaticQName kSaslMechanismName = {kXmppSaslNs, "mechanism"}; +buzz::StaticQName kSaslSuccessName = {kXmppSaslNs, "success"}; + +XmppLoginHandler::XmppLoginHandler(const std::string& server, + const std::string& username, + const std::string& auth_token, + TlsMode tls_mode, + Delegate* delegate) + : server_(server), + username_(username), + auth_token_(auth_token), + tls_mode_(tls_mode), + delegate_(delegate), + state_(State::INIT) { +} + +XmppLoginHandler::~XmppLoginHandler() { +} + +void XmppLoginHandler::Start() { + switch (tls_mode_) { + case TlsMode::NO_TLS: + state_ = State::WAIT_STREAM_HEADER_AFTER_TLS; + StartAuthHandshake(); + break; + case TlsMode::WITH_HANDSHAKE: + state_ = State::WAIT_STREAM_HEADER; + StartStream("<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\"/>"); + break; + case TlsMode::WITHOUT_HANDSHAKE: + // If <starttls> handshake is not required then start TLS right away. + state_ = State::STARTING_TLS; + delegate_->StartTls(); + break; + } +} + +void XmppLoginHandler::OnDataReceived(const std::string& data) { + DCHECK(state_ != State::INIT && state_ != State::DONE && + state_ != State::ERROR); + stream_parser_->AppendData(data); +} + +void XmppLoginHandler::OnStanza(scoped_ptr<buzz::XmlElement> stanza) { + switch (state_) { + case State::WAIT_STREAM_HEADER: { + if (stanza->Name() == kJabberFeaturesName && + stanza->FirstNamed(kStartTlsName) != nullptr) { + state_ = State::WAIT_STARTTLS_RESPONSE; + } else { + LOG(ERROR) << "Server doesn't support TLS."; + OnError(SignalStrategy::PROTOCOL_ERROR); + } + break; + } + + case State::WAIT_STARTTLS_RESPONSE: { + if (stanza->Name() == kTlsProceedName) { + state_ = State::STARTING_TLS; + delegate_->StartTls(); + } else { + LOG(ERROR) << "Failed to start TLS: " << stanza->Str(); + OnError(SignalStrategy::PROTOCOL_ERROR); + } + break; + } + + case State::WAIT_STREAM_HEADER_AFTER_TLS: { + buzz::XmlElement* mechanisms_element = + stanza->FirstNamed(kSaslMechanismsName); + bool oauth_supported = false; + if (mechanisms_element) { + for (buzz::XmlElement* element = + mechanisms_element->FirstNamed(kSaslMechanismName); + element; element = element->NextNamed(kSaslMechanismName)) { + if (element->BodyText() == kOAuthMechanism) { + oauth_supported = true; + break; + } + } + } + + if (!oauth_supported) { + LOG(ERROR) << kOAuthMechanism + << " auth mechanism is not supported by the server."; + OnError(SignalStrategy::PROTOCOL_ERROR); + return; + } + + state_ = State::WAIT_AUTH_RESULT; + break; + } + + case State::WAIT_AUTH_RESULT: { + if (stanza->Name() == kSaslSuccessName) { + state_ = State::WAIT_STREAM_HEADER_AFTER_AUTH; + StartStream( + "<iq type=\"set\" id=\"0\">" + "<bind xmlns=\"urn:ietf:params:xml:ns:xmpp-bind\">" + "<resource>chromoting</resource>" + "</bind>" + "</iq>" + "<iq type=\"set\" id=\"1\">" + "<session xmlns=\"urn:ietf:params:xml:ns:xmpp-session\"/>" + "</iq>"); + } else { + OnError(SignalStrategy::AUTHENTICATION_FAILED); + } + break; + } + + case State::WAIT_STREAM_HEADER_AFTER_AUTH: + if (stanza->Name() == kJabberFeaturesName && + stanza->FirstNamed(kXmppBindName) != nullptr) { + state_ = State::WAIT_BIND_RESULT; + } else { + LOG(ERROR) << "Server doesn't support bind after authentication."; + OnError(SignalStrategy::PROTOCOL_ERROR); + } + break; + + case State::WAIT_BIND_RESULT: { + buzz::XmlElement* bind = stanza->FirstNamed(kXmppBindName); + buzz::XmlElement* jid = bind ? bind->FirstNamed(kXmppJidName) : nullptr; + if (stanza->Attr(buzz::QName("", "id")) != "0" || + stanza->Attr(buzz::QName("", "type")) != "result" || !jid) { + LOG(ERROR) << "Received unexpected response to bind: " << stanza->Str(); + OnError(SignalStrategy::PROTOCOL_ERROR); + return; + } + jid_ = jid->BodyText(); + state_ = State::WAIT_SESSION_IQ_RESULT; + break; + } + + case State::WAIT_SESSION_IQ_RESULT: + if (stanza->Name() != kXmppIqName || + stanza->Attr(buzz::QName("", "id")) != "1" || + stanza->Attr(buzz::QName("", "type")) != "result") { + LOG(ERROR) << "Failed to start session: " << stanza->Str(); + OnError(SignalStrategy::PROTOCOL_ERROR); + return; + } + state_ = State::DONE; + delegate_->OnHandshakeDone(jid_, stream_parser_.Pass()); + break; + + default: + NOTREACHED(); + break; + } +} + +void XmppLoginHandler::OnTlsStarted() { + DCHECK(state_ == State::STARTING_TLS); + state_ = State::WAIT_STREAM_HEADER_AFTER_TLS; + StartAuthHandshake(); +} + +void XmppLoginHandler::StartAuthHandshake() { + DCHECK(state_ == State::WAIT_STREAM_HEADER_AFTER_TLS); + + std::string cookie; + base::Base64Encode( + std::string("\0", 1) + username_ + std::string("\0", 1) + auth_token_, + &cookie); + StartStream( + "<auth xmlns=\"" + std::string(kXmppSaslNs) + "\" " + "mechanism=\"" + "X-OAUTH2" + "\" " + "auth:service=\"oauth2\" " + "auth:allow-generated-jid=\"true\" " + "auth:client-uses-full-bind-result=\"true\" " + "auth:allow-non-google-login=\"true\" " + "xmlns:auth=\"http://www.google.com/talk/protocol/auth\">" + + cookie + + "</auth>"); +}; + +void XmppLoginHandler::OnParserError() { + OnError(SignalStrategy::PROTOCOL_ERROR); +} + +void XmppLoginHandler::StartStream(const std::string& first_message) { + delegate_->SendMessage("<stream:stream to=\"" + server_ + + "\" version=\"1.0\" xmlns=\"jabber:client\" " + "xmlns:stream=\"http://etherx.jabber.org/streams\">" + + first_message); + stream_parser_.reset(new XmppStreamParser()); + stream_parser_->SetCallbacks( + base::Bind(&XmppLoginHandler::OnStanza, base::Unretained(this)), + base::Bind(&XmppLoginHandler::OnParserError, base::Unretained(this))); +} + +void XmppLoginHandler::OnError(SignalStrategy::Error error) { + if (state_ != State::ERROR) { + state_ = State::ERROR; + delegate_->OnLoginHandlerError(error); + } +} + +} // namespace remoting diff --git a/remoting/signaling/xmpp_login_handler.h b/remoting/signaling/xmpp_login_handler.h new file mode 100644 index 0000000..10af045 --- /dev/null +++ b/remoting/signaling/xmpp_login_handler.h @@ -0,0 +1,142 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef REMOTING_SIGNALING_XMPP_LOGIN_HANDLER_H_ +#define REMOTING_SIGNALING_XMPP_LOGIN_HANDLER_H_ + +#include <string> + +#include "base/memory/scoped_ptr.h" +#include "remoting/signaling/signal_strategy.h" + +// Undefine SendMessage and ERROR defined in Windows headers. +#ifdef SendMessage +#undef SendMessage +#endif + +#ifdef ERROR +#undef ERROR +#endif + +namespace remoting { + +class XmppStreamParser; + +// XmppLoginHandler handles authentication handshake for XmppSignalStrategy. It +// receives incoming data using onDataReceived(), calls Delegate::SendMessage() +// to send outgoing messages and calls Delegate::OnHandshakeDone() after +// authentication is finished successfully or Delegate::OnError() on error. +// +// See RFC3920 for description of XMPP and authentication handshake. +class XmppLoginHandler { + public: + class Delegate { + public: + Delegate() {} + + virtual void SendMessage(const std::string& message) = 0; + virtual void StartTls() = 0; + virtual void OnHandshakeDone(const std::string& jid, + scoped_ptr<XmppStreamParser> parser) = 0; + virtual void OnLoginHandlerError(SignalStrategy::Error error) = 0; + + protected: + virtual ~Delegate() {} + }; + + enum class TlsMode { + NO_TLS, + WITH_HANDSHAKE, + WITHOUT_HANDSHAKE, + }; + + XmppLoginHandler(const std::string& server, + const std::string& username, + const std::string& auth_token, + TlsMode tls_mode, + Delegate* delegate); + ~XmppLoginHandler(); + + void Start(); + void OnDataReceived(const std::string& data); + void OnTlsStarted(); + + private: + // States the handshake goes through. States are iterated from INIT to DONE + // sequentially, except for ERROR state which may be accepted at any point. + // + // Following messages are sent/received in each state: + // INIT + // client -> server: Stream header + // client -> server: <starttls> + // WAIT_STREAM_HEADER + // client <- server: Stream header with list of supported features which + // should include starttls. + // WAIT_STARTTLS_RESPONSE + // client <- server: <proceed> + // STARTING_TLS + // TLS handshake + // client -> server: Stream header + // client -> server: <auth> message with the OAuth2 token. + // WAIT_STREAM_HEADER_AFTER_TLS + // client <- server: Stream header with list of supported authentication + // methods which is expected to include X-OAUTH2 + // WAIT_AUTH_RESULT + // client <- server: <success> or <failure> + // client -> server: Stream header + // client -> server: <bind> + // client -> server: <iq><session/></iq> to start the session + // WAIT_STREAM_HEADER_AFTER_AUTH + // client <- server: Stream header with list of features that should + // include <bind>. + // WAIT_BIND_RESULT + // client <- server: <bind> result with JID. + // WAIT_SESSION_IQ_RESULT + // client <- server: result for <iq><session/></iq> + // DONE + enum class State { + INIT, + WAIT_STREAM_HEADER, + WAIT_STARTTLS_RESPONSE, + STARTING_TLS, + WAIT_STREAM_HEADER_AFTER_TLS, + WAIT_AUTH_RESULT, + WAIT_STREAM_HEADER_AFTER_AUTH, + WAIT_BIND_RESULT, + WAIT_SESSION_IQ_RESULT, + DONE, + ERROR, + }; + + // Callbacks for XmppStreamParser. + void OnStanza(scoped_ptr<buzz::XmlElement> stanza); + void OnParserError(); + + // Starts authentication handshake in WAIT_STREAM_HEADER_AFTER_TLS state. + void StartAuthHandshake(); + + // Helper used to send stream header. + void StartStream(const std::string& first_message); + + // Report the |error| to the delegate and changes |state_| to ERROR, + void OnError(SignalStrategy::Error error); + + std::string server_; + std::string username_; + std::string auth_token_; + TlsMode tls_mode_; + Delegate* delegate_; + + State state_; + + std::string jid_; + + scoped_ptr<XmppStreamParser> stream_parser_; + + DISALLOW_COPY_AND_ASSIGN(XmppLoginHandler); +}; + +} // namespace remoting + +#endif // REMOTING_SIGNALING_XMPP_LOGIN_HANDLER_H_ diff --git a/remoting/signaling/xmpp_login_handler_unittest.cc b/remoting/signaling/xmpp_login_handler_unittest.cc new file mode 100644 index 0000000..59afdb8 --- /dev/null +++ b/remoting/signaling/xmpp_login_handler_unittest.cc @@ -0,0 +1,223 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "remoting/signaling/xmpp_login_handler.h" + +#include "base/base64.h" +#include "base/message_loop/message_loop.h" +#include "base/run_loop.h" +#include "remoting/signaling/xmpp_stream_parser.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/webrtc/libjingle/xmllite/xmlelement.h" + +#ifdef SendMessage +#undef SendMessage +#endif + +#ifdef ERROR +#undef ERROR +#endif + +namespace remoting { + +char kTestUsername[] = "testUsername@gmail.com"; +char kTestToken[] = "testToken"; + +class XmppLoginHandlerTest : public testing::Test, + public XmppLoginHandler::Delegate { + public: + XmppLoginHandlerTest() + : start_tls_called_(false), error_(SignalStrategy::OK) {} + + void TearDown() override { + login_handler_.reset(); + parser_.reset(); + base::RunLoop().RunUntilIdle(); + } + + void SendMessage(const std::string& message) override { + sent_data_ += message; + } + + void StartTls() override { + start_tls_called_ = true; + } + + void OnHandshakeDone(const std::string& jid, + scoped_ptr<XmppStreamParser> parser) override { + jid_ = jid; + parser_ = parser.Pass(); + } + + void OnLoginHandlerError(SignalStrategy::Error error) override { + EXPECT_NE(error, SignalStrategy::OK); + error_ = error; + } + + protected: + void HandshakeBase(); + + base::MessageLoop message_loop_; + + scoped_ptr<XmppLoginHandler> login_handler_; + std::string sent_data_; + bool start_tls_called_; + std::string jid_; + scoped_ptr<XmppStreamParser> parser_; + SignalStrategy::Error error_; +}; + +void XmppLoginHandlerTest::HandshakeBase() { + login_handler_.reset( + new XmppLoginHandler("google.com", kTestUsername, kTestToken, + XmppLoginHandler::TlsMode::WITHOUT_HANDSHAKE, this)); + login_handler_->Start(); + EXPECT_TRUE(start_tls_called_); + + login_handler_->OnTlsStarted(); + std::string cookie; + base::Base64Encode( + std::string("\0", 1) + kTestUsername + std::string("\0", 1) + kTestToken, + &cookie); + EXPECT_EQ( + sent_data_, + "<stream:stream to=\"google.com\" version=\"1.0\" " + "xmlns=\"jabber:client\" " + "xmlns:stream=\"http://etherx.jabber.org/streams\">" + "<auth xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\" mechanism=\"X-OAUTH2\" " + "auth:service=\"oauth2\" auth:allow-generated-jid=\"true\" " + "auth:client-uses-full-bind-result=\"true\" " + "auth:allow-non-google-login=\"true\" " + "xmlns:auth=\"http://www.google.com/talk/protocol/auth\">" + cookie + + "</auth>"); + sent_data_.clear(); + + login_handler_->OnDataReceived( + "<stream:stream from=\"google.com\" id=\"DCDDE5171CB2154A\" " + "version=\"1.0\" " + "xmlns:stream=\"http://etherx.jabber.org/streams\" " + "xmlns=\"jabber:client\">" + "<stream:features>" + "<mechanisms xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">" + "<mechanism>X-OAUTH2</mechanism>" + "<mechanism>X-GOOGLE-TOKEN</mechanism>" + "<mechanism>PLAIN</mechanism>" + "</mechanisms>" + "</stream:features>"); +} + +TEST_F(XmppLoginHandlerTest, SuccessfulAuth) { + HandshakeBase(); + + login_handler_->OnDataReceived( + "<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"); + EXPECT_EQ( + sent_data_, + "<stream:stream to=\"google.com\" version=\"1.0\" " + "xmlns=\"jabber:client\" " + "xmlns:stream=\"http://etherx.jabber.org/streams\">" + "<iq type=\"set\" id=\"0\">" + "<bind xmlns=\"urn:ietf:params:xml:ns:xmpp-bind\">" + "<resource>chromoting</resource>" + "</bind>" + "</iq>" + "<iq type=\"set\" id=\"1\">" + "<session xmlns=\"urn:ietf:params:xml:ns:xmpp-session\"/>" + "</iq>"); + sent_data_.clear(); + + login_handler_->OnDataReceived( + "<stream:stream from=\"google.com\" id=\"104FA10576E2AA80\" " + "version=\"1.0\" " + "xmlns:stream=\"http://etherx.jabber.org/streams\" " + "xmlns=\"jabber:client\">" + "<stream:features>" + "<bind xmlns=\"urn:ietf:params:xml:ns:xmpp-bind\"/>" + "<session xmlns=\"urn:ietf:params:xml:ns:xmpp-session\"/>" + "</stream:features>" + "<iq id=\"0\" type=\"result\">" + "<bind xmlns=\"urn:ietf:params:xml:ns:xmpp-bind\">" + "<jid>" + std::string(kTestUsername) + "/chromoting52B4920E</jid>" + "</bind>" + "</iq>" + "<iq type=\"result\" id=\"1\"/>"); + + EXPECT_EQ(jid_, std::string(kTestUsername) + "/chromoting52B4920E"); + EXPECT_TRUE(parser_); +} + +TEST_F(XmppLoginHandlerTest, StartTlsHandshake) { + login_handler_.reset( + new XmppLoginHandler("google.com", kTestUsername, kTestToken, + XmppLoginHandler::TlsMode::WITH_HANDSHAKE, this)); + login_handler_->Start(); + EXPECT_FALSE(start_tls_called_); + + EXPECT_EQ(sent_data_, + "<stream:stream to=\"google.com\" version=\"1.0\" " + "xmlns=\"jabber:client\" " + "xmlns:stream=\"http://etherx.jabber.org/streams\">" + "<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\"/>"); + sent_data_.clear(); + + login_handler_->OnDataReceived( + "<stream:stream from=\"google.com\" id=\"78A87C70559EF28A\" " + "version=\"1.0\" " + "xmlns:stream=\"http://etherx.jabber.org/streams\" " + "xmlns=\"jabber:client\">" + "<stream:features>" + "<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\">" + "<required/>" + "</starttls>" + "<mechanisms xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">" + "<mechanism>X-OAUTH2</mechanism>" + "<mechanism>X-GOOGLE-TOKEN</mechanism>" + "</mechanisms>" + "</stream:features>"); + + login_handler_->OnDataReceived( + "<proceed xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\"/>"); + EXPECT_TRUE(start_tls_called_); +} + +TEST_F(XmppLoginHandlerTest, AuthError) { + HandshakeBase(); + + login_handler_->OnDataReceived( + "<failure xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">" + "<not-authorized/></failure>"); + EXPECT_EQ(error_, SignalStrategy::AUTHENTICATION_FAILED); +} + +TEST_F(XmppLoginHandlerTest, NoTls) { + login_handler_.reset( + new XmppLoginHandler("google.com", kTestUsername, kTestToken, + XmppLoginHandler::TlsMode::NO_TLS, this)); + login_handler_->Start(); + + EXPECT_FALSE(start_tls_called_); + std::string cookie; + base::Base64Encode( + std::string("\0", 1) + kTestUsername + std::string("\0", 1) + kTestToken, + &cookie); + EXPECT_EQ( + sent_data_, + "<stream:stream to=\"google.com\" version=\"1.0\" " + "xmlns=\"jabber:client\" " + "xmlns:stream=\"http://etherx.jabber.org/streams\">" + "<auth xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\" mechanism=\"X-OAUTH2\" " + "auth:service=\"oauth2\" auth:allow-generated-jid=\"true\" " + "auth:client-uses-full-bind-result=\"true\" " + "auth:allow-non-google-login=\"true\" " + "xmlns:auth=\"http://www.google.com/talk/protocol/auth\">" + cookie + + "</auth>"); +} + +TEST_F(XmppLoginHandlerTest, StreamParseError) { + HandshakeBase(); + login_handler_->OnDataReceived("BAD DATA"); + EXPECT_EQ(error_, SignalStrategy::PROTOCOL_ERROR); +} + +} // namespace remoting diff --git a/remoting/signaling/xmpp_signal_strategy.cc b/remoting/signaling/xmpp_signal_strategy.cc index f2d7b81..92c0db5 100644 --- a/remoting/signaling/xmpp_signal_strategy.cc +++ b/remoting/signaling/xmpp_signal_strategy.cc @@ -4,90 +4,147 @@ #include "remoting/signaling/xmpp_signal_strategy.h" +#include <vector> + #include "base/bind.h" #include "base/location.h" #include "base/logging.h" +#include "base/observer_list.h" +#include "base/rand_util.h" #include "base/single_thread_task_runner.h" -#include "base/strings/string_util.h" +#include "base/strings/string_number_conversions.h" #include "base/thread_task_runner_handle.h" -#include "jingle/glue/chrome_async_socket.h" -#include "jingle/glue/task_pump.h" -#include "jingle/glue/xmpp_client_socket_factory.h" -#include "jingle/notifier/base/gaia_constants.h" -#include "jingle/notifier/base/gaia_token_pre_xmpp_auth.h" +#include "base/threading/thread_checker.h" +#include "base/time/time.h" +#include "base/timer/timer.h" +#include "jingle/glue/proxy_resolving_client_socket.h" +#include "net/cert/cert_verifier.h" +#include "net/http/transport_security_state.h" #include "net/socket/client_socket_factory.h" +#include "net/socket/client_socket_handle.h" +#include "net/socket/ssl_client_socket.h" #include "net/url_request/url_request_context_getter.h" -#include "third_party/webrtc/base/thread.h" -#include "third_party/webrtc/libjingle/xmpp/prexmppauth.h" -#include "third_party/webrtc/libjingle/xmpp/saslcookiemechanism.h" - -const char kDefaultResourceName[] = "chromoting"; +#include "remoting/base/buffered_socket_writer.h" +#include "remoting/signaling/xmpp_login_handler.h" +#include "remoting/signaling/xmpp_stream_parser.h" +#include "third_party/webrtc/libjingle/xmllite/xmlelement.h" -// Use 58 seconds keep-alive interval, in case routers terminate +// Use 50 seconds keep-alive interval, in case routers terminate // connections that are idle for more than a minute. const int kKeepAliveIntervalSeconds = 50; -// Read buffer size used by ChromeAsyncSocket for read and write buffers. -// -// TODO(sergeyu): Currently jingle::ChromeAsyncSocket fails Write() when the -// write buffer is full and talk::XmppClient just ignores the error. As result -// chunks of data sent to the server are dropped (and they may not be full XMPP -// stanzas). The problem needs to be fixed either in XmppClient on -// ChromeAsyncSocket (e.g. ChromeAsyncSocket could close the connection when -// buffer is full). -const size_t kReadBufferSize = 64 * 1024; -const size_t kWriteBufferSize = 64 * 1024; +const int kReadBufferSize = 4096; const int kDefaultXmppPort = 5222; const int kDefaultHttpsPort = 443; namespace remoting { -XmppSignalStrategy::XmppServerConfig::XmppServerConfig() {} -XmppSignalStrategy::XmppServerConfig::~XmppServerConfig() {} +XmppSignalStrategy::XmppServerConfig::XmppServerConfig() + : port(kDefaultXmppPort), use_tls(true) { +} -XmppSignalStrategy::XmppSignalStrategy( +XmppSignalStrategy::XmppServerConfig::~XmppServerConfig() { +} + +class XmppSignalStrategy::Core : public XmppLoginHandler::Delegate { + public: + Core( + net::ClientSocketFactory* socket_factory, + const scoped_refptr<net::URLRequestContextGetter>& request_context_getter, + const XmppServerConfig& xmpp_server_config); + ~Core() override; + + void Connect(); + void Disconnect(); + State GetState() const; + Error GetError() const; + std::string GetLocalJid() const; + void AddListener(Listener* listener); + void RemoveListener(Listener* listener); + bool SendStanza(scoped_ptr<buzz::XmlElement> stanza); + + void SetAuthInfo(const std::string& username, + const std::string& auth_token); + + void VerifyNoListeners(); + + private: + void OnSocketConnected(int result); + void OnTlsConnected(int result); + + void ReadSocket(); + void OnReadResult(int result); + void HandleReadResult(int result); + + // XmppLoginHandler::Delegate interface. + void SendMessage(const std::string& message) override; + void StartTls() override; + void OnHandshakeDone(const std::string& jid, + scoped_ptr<XmppStreamParser> parser) override; + void OnLoginHandlerError(SignalStrategy::Error error) override; + + // Event handlers for XmppStreamParser. + void OnStanza(const scoped_ptr<buzz::XmlElement> stanza); + void OnParserError(); + + void OnNetworkError(int error); + + void SendKeepAlive(); + + net::ClientSocketFactory* socket_factory_; + scoped_refptr<net::URLRequestContextGetter> request_context_getter_; + XmppServerConfig xmpp_server_config_; + + // Used by the |socket_|. + scoped_ptr<net::CertVerifier> cert_verifier_; + scoped_ptr<net::TransportSecurityState> transport_security_state_; + + scoped_ptr<net::StreamSocket> socket_; + scoped_ptr<BufferedSocketWriter> writer_; + scoped_refptr<net::IOBuffer> read_buffer_; + bool read_pending_; + bool tls_pending_; + + scoped_ptr<XmppLoginHandler> login_handler_; + scoped_ptr<XmppStreamParser> stream_parser_; + std::string jid_; + + Error error_; + + ObserverList<Listener, true> listeners_; + + base::Timer keep_alive_timer_; + + base::ThreadChecker thread_checker_; + + DISALLOW_COPY_AND_ASSIGN(Core); +}; + +XmppSignalStrategy::Core::Core( net::ClientSocketFactory* socket_factory, const scoped_refptr<net::URLRequestContextGetter>& request_context_getter, const XmppSignalStrategy::XmppServerConfig& xmpp_server_config) : socket_factory_(socket_factory), request_context_getter_(request_context_getter), - resource_name_(kDefaultResourceName), - xmpp_client_(nullptr), xmpp_server_config_(xmpp_server_config), - state_(DISCONNECTED), - error_(OK) { + read_pending_(false), + tls_pending_(false), + error_(OK), + keep_alive_timer_( + FROM_HERE, + base::TimeDelta::FromSeconds(kKeepAliveIntervalSeconds), + base::Bind(&Core::SendKeepAlive, base::Unretained(this)), + true) { #if defined(NDEBUG) + // Non-secure connections are allowed only for debugging. CHECK(xmpp_server_config_.use_tls); #endif -} - -XmppSignalStrategy::~XmppSignalStrategy() { - Disconnect(); - - // Destroying task runner will destroy XmppClient, but XmppClient may be on - // the stack and it doesn't handle this case properly, so we need to delay - // destruction. - base::ThreadTaskRunnerHandle::Get()->DeleteSoon( - FROM_HERE, task_runner_.release()); -} - -void XmppSignalStrategy::Connect() { - DCHECK(CalledOnValidThread()); - - // Disconnect first if we are currently connected. - Disconnect(); - - buzz::XmppClientSettings settings; - buzz::Jid login_jid(xmpp_server_config_.username); - settings.set_user(login_jid.node()); - settings.set_host(login_jid.domain()); - settings.set_resource(resource_name_); - settings.set_token_service("oauth2"); - settings.set_auth_token(buzz::AUTH_MECHANISM_GOOGLE_TOKEN, - xmpp_server_config_.auth_token); - int port = xmpp_server_config_.port; + // TODO(sergeyu): Support for direct connections without TLS is not + // implemented yet. + if (!xmpp_server_config_.use_tls) + NOTIMPLEMENTED(); // Port 5222 may be blocked by firewall. talk.google.com allows connections on // port 443 which can be used instead of 5222. The webapp still requests to @@ -99,176 +156,364 @@ void XmppSignalStrategy::Connect() { // TODO(sergeyu): Once all hosts support connections on port 443 // the webapp needs to be updated to request port 443 and these 2 lines can be // removed. crbug.com/443384 - if (xmpp_server_config_.host == "talk.google.com" && port == kDefaultXmppPort) - port = kDefaultHttpsPort; + if (xmpp_server_config_.host == "talk.google.com" && + xmpp_server_config_.port == kDefaultXmppPort) { + xmpp_server_config_.port = kDefaultHttpsPort; + } +} - settings.set_server( - rtc::SocketAddress(xmpp_server_config_.host, port)); - settings.set_use_tls( - xmpp_server_config_.use_tls ? buzz::TLS_ENABLED : buzz::TLS_DISABLED); +XmppSignalStrategy::Core::~Core() { + Disconnect(); +} - // Enable fake SSL handshake when connecting over HTTPS port. - bool use_fake_ssl_client_socket = (port == kDefaultHttpsPort); +void XmppSignalStrategy::Core::Connect() { + DCHECK(thread_checker_.CalledOnValidThread()); - scoped_ptr<jingle_glue::XmppClientSocketFactory> xmpp_socket_factory( - new jingle_glue::XmppClientSocketFactory(socket_factory_, - net::SSLConfig(), - request_context_getter_, - use_fake_ssl_client_socket)); - buzz::AsyncSocket* socket = new jingle_glue::ChromeAsyncSocket( - xmpp_socket_factory.release(), kReadBufferSize, kWriteBufferSize); + // Disconnect first if we are currently connected. + Disconnect(); - task_runner_.reset(new jingle_glue::TaskPump()); - xmpp_client_ = new buzz::XmppClient(task_runner_.get()); - xmpp_client_->Connect( - settings, std::string(), socket, CreatePreXmppAuth(settings)); - xmpp_client_->SignalStateChange - .connect(this, &XmppSignalStrategy::OnConnectionStateChanged); - xmpp_client_->engine()->AddStanzaHandler(this, buzz::XmppEngine::HL_TYPE); - xmpp_client_->Start(); + error_ = OK; - SetState(CONNECTING); -} + FOR_EACH_OBSERVER(Listener, listeners_, + OnSignalStrategyStateChange(CONNECTING)); -void XmppSignalStrategy::Disconnect() { - DCHECK(CalledOnValidThread()); + socket_.reset(new jingle_glue::ProxyResolvingClientSocket( + socket_factory_, request_context_getter_, net::SSLConfig(), + net::HostPortPair(xmpp_server_config_.host, xmpp_server_config_.port))); + + int result = socket_->Connect(base::Bind( + &Core::OnSocketConnected, base::Unretained(this))); + if (result != net::ERR_IO_PENDING) + OnSocketConnected(result); +} - if (xmpp_client_) { - xmpp_client_->engine()->RemoveStanzaHandler(this); +void XmppSignalStrategy::Core::Disconnect() { + DCHECK(thread_checker_.CalledOnValidThread()); - xmpp_client_->Disconnect(); + if (socket_) { + login_handler_.reset(); + stream_parser_.reset(); + writer_.reset(); + socket_.reset(); - // |xmpp_client_| should be set to nullptr in OnConnectionStateChanged() - // in response to Disconnect() call above. - DCHECK(xmpp_client_ == nullptr); + FOR_EACH_OBSERVER(Listener, listeners_, + OnSignalStrategyStateChange(DISCONNECTED)); } } -SignalStrategy::State XmppSignalStrategy::GetState() const { - DCHECK(CalledOnValidThread()); - return state_; +SignalStrategy::State XmppSignalStrategy::Core::GetState() const { + DCHECK(thread_checker_.CalledOnValidThread()); + + if (stream_parser_) { + DCHECK(socket_); + return CONNECTED; + } else if (socket_) { + return CONNECTING; + } else { + return DISCONNECTED; + } } -SignalStrategy::Error XmppSignalStrategy::GetError() const { - DCHECK(CalledOnValidThread()); +SignalStrategy::Error XmppSignalStrategy::Core::GetError() const { + DCHECK(thread_checker_.CalledOnValidThread()); return error_; } -std::string XmppSignalStrategy::GetLocalJid() const { - DCHECK(CalledOnValidThread()); - return xmpp_client_->jid().Str(); +std::string XmppSignalStrategy::Core::GetLocalJid() const { + DCHECK(thread_checker_.CalledOnValidThread()); + return jid_; } -void XmppSignalStrategy::AddListener(Listener* listener) { - DCHECK(CalledOnValidThread()); +void XmppSignalStrategy::Core::AddListener(Listener* listener) { + DCHECK(thread_checker_.CalledOnValidThread()); listeners_.AddObserver(listener); } -void XmppSignalStrategy::RemoveListener(Listener* listener) { - DCHECK(CalledOnValidThread()); +void XmppSignalStrategy::Core::RemoveListener(Listener* listener) { + DCHECK(thread_checker_.CalledOnValidThread()); listeners_.RemoveObserver(listener); } -bool XmppSignalStrategy::SendStanza(scoped_ptr<buzz::XmlElement> stanza) { - DCHECK(CalledOnValidThread()); - if (!xmpp_client_) { +bool XmppSignalStrategy::Core::SendStanza(scoped_ptr<buzz::XmlElement> stanza) { + DCHECK(thread_checker_.CalledOnValidThread()); + + if (!stream_parser_) { VLOG(0) << "Dropping signalling message because XMPP " - "connection has been terminated."; + "connection has been terminated."; return false; } - buzz::XmppReturnStatus status = xmpp_client_->SendStanza(stanza.release()); - return status == buzz::XMPP_RETURN_OK || status == buzz::XMPP_RETURN_PENDING; + SendMessage(stanza->Str()); + return true; } -std::string XmppSignalStrategy::GetNextId() { - DCHECK(CalledOnValidThread()); - if (!xmpp_client_) { - // If the connection has been terminated then it doesn't matter - // what Id we return. - return std::string(); - } - return xmpp_client_->NextId(); +void XmppSignalStrategy::Core::SetAuthInfo(const std::string& username, + const std::string& auth_token) { + DCHECK(thread_checker_.CalledOnValidThread()); + xmpp_server_config_.username = username; + xmpp_server_config_.auth_token = auth_token; +} + +void XmppSignalStrategy::Core::SendMessage(const std::string& message) { + DCHECK(thread_checker_.CalledOnValidThread()); + scoped_refptr<net::IOBufferWithSize> buffer = + new net::IOBufferWithSize(message.size()); + memcpy(buffer->data(), message.data(), message.size()); + writer_->Write(buffer, base::Closure()); +} + +void XmppSignalStrategy::Core::StartTls() { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(login_handler_); + + // Reset the writer so we don't try to write to the raw socket anymore. + DCHECK_EQ(writer_->GetBufferSize(), 0); + writer_.reset(); + + DCHECK(!read_pending_); + + scoped_ptr<net::ClientSocketHandle> socket_handle( + new net::ClientSocketHandle()); + socket_handle->SetSocket(socket_.Pass()); + + cert_verifier_.reset(net::CertVerifier::CreateDefault()); + transport_security_state_.reset(new net::TransportSecurityState()); + net::SSLClientSocketContext context; + context.cert_verifier = cert_verifier_.get(); + context.transport_security_state = transport_security_state_.get(); + + socket_ = socket_factory_->CreateSSLClientSocket( + socket_handle.Pass(), + net::HostPortPair(xmpp_server_config_.host, kDefaultHttpsPort), + net::SSLConfig(), context); + + tls_pending_ = true; + int result = socket_->Connect( + base::Bind(&Core::OnTlsConnected, base::Unretained(this))); + if (result != net::ERR_IO_PENDING) + OnTlsConnected(result); } -bool XmppSignalStrategy::HandleStanza(const buzz::XmlElement* stanza) { - DCHECK(CalledOnValidThread()); +void XmppSignalStrategy::Core::OnHandshakeDone( + const std::string& jid, + scoped_ptr<XmppStreamParser> parser) { + DCHECK(thread_checker_.CalledOnValidThread()); + + jid_ = jid; + stream_parser_ = parser.Pass(); + stream_parser_->SetCallbacks( + base::Bind(&Core::OnStanza, base::Unretained(this)), + base::Bind(&Core::OnParserError, base::Unretained(this))); + + // Don't need |login_handler_| anymore. + login_handler_.reset(); + + FOR_EACH_OBSERVER(Listener, listeners_, + OnSignalStrategyStateChange(CONNECTED)); +} + +void XmppSignalStrategy::Core::OnLoginHandlerError( + SignalStrategy::Error error) { + DCHECK(thread_checker_.CalledOnValidThread()); + + error_ = error; + Disconnect(); +} + +void XmppSignalStrategy::Core::OnStanza( + const scoped_ptr<buzz::XmlElement> stanza) { + DCHECK(thread_checker_.CalledOnValidThread()); + ObserverListBase<Listener>::Iterator it(&listeners_); - Listener* listener; - while ((listener = it.GetNext()) != nullptr) { - if (listener->OnSignalStrategyIncomingStanza(stanza)) - return true; + for (Listener* listener = it.GetNext(); listener; listener = it.GetNext()) { + if (listener->OnSignalStrategyIncomingStanza(stanza.get())) + return; } - return false; } -void XmppSignalStrategy::SetAuthInfo(const std::string& username, - const std::string& auth_token) { - DCHECK(CalledOnValidThread()); - xmpp_server_config_.username = username; - xmpp_server_config_.auth_token = auth_token; +void XmppSignalStrategy::Core::OnParserError() { + DCHECK(thread_checker_.CalledOnValidThread()); + + error_ = NETWORK_ERROR; + Disconnect(); } -void XmppSignalStrategy::SetResourceName(const std::string &resource_name) { - DCHECK(CalledOnValidThread()); - resource_name_ = resource_name; +void XmppSignalStrategy::Core::OnSocketConnected(int result) { + DCHECK(thread_checker_.CalledOnValidThread()); + + if (result != net::OK) { + OnNetworkError(result); + return; + } + + writer_.reset(new BufferedSocketWriter()); + writer_->Init(socket_.get(), base::Bind(&Core::OnNetworkError, + base::Unretained(this))); + + XmppLoginHandler::TlsMode tls_mode; + if (xmpp_server_config_.use_tls) { + tls_mode = (xmpp_server_config_.port == kDefaultXmppPort) + ? XmppLoginHandler::TlsMode::WITH_HANDSHAKE + : XmppLoginHandler::TlsMode::WITHOUT_HANDSHAKE; + } else { + tls_mode = XmppLoginHandler::TlsMode::NO_TLS; + } + + // The server name is passed as to attribute in the <stream>. When connecting + // to talk.google.com it affects the certificate the server will use for TLS: + // talk.google.com uses gmail certificate when specified server is gmail.com + // or googlemail.com and google.com cert otherwise. In the same time it + // doesn't accept talk.google.com as target server. Here we use google.com + // server name when authenticating to talk.google.com. This ensures that the + // server will use google.com cert which will be accepted by the TLS + // implementation in Chrome (TLS API doesn't allow specifying domain other + // than the one that was passed to connect()). + std::string server = xmpp_server_config_.host; + if (server == "talk.google.com") + server = "google.com"; + + login_handler_.reset( + new XmppLoginHandler(server, xmpp_server_config_.username, + xmpp_server_config_.auth_token, tls_mode, this)); + login_handler_->Start(); + + ReadSocket(); } -void XmppSignalStrategy::OnConnectionStateChanged( - buzz::XmppEngine::State state) { - DCHECK(CalledOnValidThread()); +void XmppSignalStrategy::Core::OnTlsConnected(int result) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(tls_pending_); + tls_pending_ = false; - if (state == buzz::XmppEngine::STATE_OPEN) { - keep_alive_timer_.Start( - FROM_HERE, base::TimeDelta::FromSeconds(kKeepAliveIntervalSeconds), - this, &XmppSignalStrategy::SendKeepAlive); - SetState(CONNECTED); - } else if (state == buzz::XmppEngine::STATE_CLOSED) { - // Make sure we dump errors to the log. - int subcode; - buzz::XmppEngine::Error error = xmpp_client_->GetError(&subcode); - VLOG(0) << "XMPP connection was closed: error=" << error - << ", subcode=" << subcode; + if (result != net::OK) { + OnNetworkError(result); + return; + } - keep_alive_timer_.Stop(); + writer_.reset(new BufferedSocketWriter()); + writer_->Init(socket_.get(), base::Bind(&Core::OnNetworkError, + base::Unretained(this))); - // Client is destroyed by the TaskRunner after the client is - // closed. Reset the pointer so we don't try to use it later. - xmpp_client_ = nullptr; + login_handler_->OnTlsStarted(); - switch (error) { - case buzz::XmppEngine::ERROR_UNAUTHORIZED: - case buzz::XmppEngine::ERROR_AUTH: - case buzz::XmppEngine::ERROR_MISSING_USERNAME: - error_ = AUTHENTICATION_FAILED; - break; + ReadSocket(); +} - default: - error_ = NETWORK_ERROR; - } +void XmppSignalStrategy::Core::ReadSocket() { + DCHECK(thread_checker_.CalledOnValidThread()); - SetState(DISCONNECTED); + while (socket_ && !read_pending_ && !tls_pending_) { + read_buffer_ = new net::IOBuffer(kReadBufferSize); + int result = socket_->Read( + read_buffer_.get(), kReadBufferSize, + base::Bind(&Core::OnReadResult, base::Unretained(this))); + HandleReadResult(result); } } -void XmppSignalStrategy::SetState(State new_state) { - if (state_ != new_state) { - state_ = new_state; - FOR_EACH_OBSERVER(Listener, listeners_, - OnSignalStrategyStateChange(new_state)); +void XmppSignalStrategy::Core::OnReadResult(int result) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(read_pending_); + read_pending_ = false; + HandleReadResult(result); + ReadSocket(); +} + +void XmppSignalStrategy::Core::HandleReadResult(int result) { + DCHECK(thread_checker_.CalledOnValidThread()); + + if (result == net::ERR_IO_PENDING) { + read_pending_ = true; + return; + } + + if (result < 0) { + OnNetworkError(result); + return; } + + if (result == 0) { + // Connection was closed by the server. + error_ = OK; + Disconnect(); + return; + } + + if (stream_parser_) { + stream_parser_->AppendData(std::string(read_buffer_->data(), result)); + } else { + login_handler_->OnDataReceived(std::string(read_buffer_->data(), result)); + } +} + +void XmppSignalStrategy::Core::OnNetworkError(int error) { + DCHECK(thread_checker_.CalledOnValidThread()); + + LOG(ERROR) << "XMPP socket error " << error; + error_ = NETWORK_ERROR; + Disconnect(); +} + +void XmppSignalStrategy::Core::SendKeepAlive() { + DCHECK(thread_checker_.CalledOnValidThread()); + + if (GetState() == CONNECTED) + SendMessage(" "); +} + +XmppSignalStrategy::XmppSignalStrategy( + net::ClientSocketFactory* socket_factory, + const scoped_refptr<net::URLRequestContextGetter>& request_context_getter, + const XmppServerConfig& xmpp_server_config) + : core_(new Core(socket_factory, + request_context_getter, + xmpp_server_config)) { +} + +XmppSignalStrategy::~XmppSignalStrategy() { + // All listeners should be removed at this point, so it's safe to detach + // |core_|. + base::ThreadTaskRunnerHandle::Get()->DeleteSoon(FROM_HERE, core_.release()); +} + +void XmppSignalStrategy::Connect() { + core_->Connect(); +} + +void XmppSignalStrategy::Disconnect() { + core_->Disconnect(); +} + +SignalStrategy::State XmppSignalStrategy::GetState() const { + return core_->GetState(); +} + +SignalStrategy::Error XmppSignalStrategy::GetError() const { + return core_->GetError(); +} + +std::string XmppSignalStrategy::GetLocalJid() const { + return core_->GetLocalJid(); +} + +void XmppSignalStrategy::AddListener(Listener* listener) { + core_->AddListener(listener); } -void XmppSignalStrategy::SendKeepAlive() { - xmpp_client_->SendRaw(" "); +void XmppSignalStrategy::RemoveListener(Listener* listener) { + core_->RemoveListener(listener); +} +bool XmppSignalStrategy::SendStanza(scoped_ptr<buzz::XmlElement> stanza) { + return core_->SendStanza(stanza.Pass()); } -// static -buzz::PreXmppAuth* XmppSignalStrategy::CreatePreXmppAuth( - const buzz::XmppClientSettings& settings) { - buzz::Jid jid(settings.user(), settings.host(), buzz::STR_EMPTY); - return new notifier::GaiaTokenPreXmppAuth( - jid.Str(), settings.auth_token(), settings.token_service(), "X-OAUTH2"); +std::string XmppSignalStrategy::GetNextId() { + return base::Uint64ToString(base::RandUint64()); +} + +void XmppSignalStrategy::SetAuthInfo(const std::string& username, + const std::string& auth_token) { + core_->SetAuthInfo(username, auth_token); } } // namespace remoting diff --git a/remoting/signaling/xmpp_signal_strategy.h b/remoting/signaling/xmpp_signal_strategy.h index 2d8268b..62a8c18 100644 --- a/remoting/signaling/xmpp_signal_strategy.h +++ b/remoting/signaling/xmpp_signal_strategy.h @@ -2,42 +2,24 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// The XmppSignalStrategy encapsulates all the logic to perform the signaling -// STUN/ICE for jingle via a direct XMPP connection. -// -// This class is not threadsafe. - #ifndef REMOTING_SIGNALING_XMPP_SIGNAL_STRATEGY_H_ #define REMOTING_SIGNALING_XMPP_SIGNAL_STRATEGY_H_ #include "remoting/signaling/signal_strategy.h" -#include <vector> - #include "base/compiler_specific.h" -#include "base/observer_list.h" -#include "base/threading/non_thread_safe.h" -#include "base/timer/timer.h" -#include "third_party/webrtc/base/sigslot.h" -#include "third_party/webrtc/libjingle/xmpp/xmppclient.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" namespace net { class ClientSocketFactory; class URLRequestContextGetter; } // namespace net -namespace rtc { -class TaskRunner; -} // namespace rtc - namespace remoting { -class JingleThread; - -class XmppSignalStrategy : public base::NonThreadSafe, - public SignalStrategy, - public buzz::XmppStanzaHandler, - public sigslot::has_slots<> { +// XmppSignalStrategy implements SignalStrategy using direct XMPP connection. +class XmppSignalStrategy : public SignalStrategy { public: // XMPP Server configuration for XmppSignalStrategy. struct XmppServerConfig { @@ -69,41 +51,16 @@ class XmppSignalStrategy : public base::NonThreadSafe, bool SendStanza(scoped_ptr<buzz::XmlElement> stanza) override; std::string GetNextId() override; - // buzz::XmppStanzaHandler interface. - bool HandleStanza(const buzz::XmlElement* stanza) override; - // This method is used to update the auth info (for example when the OAuth // access token is renewed). It is OK to call this even when we are in the // CONNECTED state. It will be used on the next Connect() call. void SetAuthInfo(const std::string& username, const std::string& auth_token); - // Use this method to override the default resource name used (optional). - // This will be used on the next Connect() call. - void SetResourceName(const std::string& resource_name); - private: - static buzz::PreXmppAuth* CreatePreXmppAuth( - const buzz::XmppClientSettings& settings); - - void OnConnectionStateChanged(buzz::XmppEngine::State state); - void SetState(State new_state); - - void SendKeepAlive(); - - net::ClientSocketFactory* socket_factory_; - scoped_refptr<net::URLRequestContextGetter> request_context_getter_; - std::string resource_name_; - scoped_ptr<rtc::TaskRunner> task_runner_; - buzz::XmppClient* xmpp_client_; - XmppServerConfig xmpp_server_config_; - - State state_; - Error error_; - - ObserverList<Listener, true> listeners_; + class Core; - base::RepeatingTimer<XmppSignalStrategy> keep_alive_timer_; + scoped_ptr<Core> core_; DISALLOW_COPY_AND_ASSIGN(XmppSignalStrategy); }; diff --git a/remoting/signaling/xmpp_signal_strategy_unittest.cc b/remoting/signaling/xmpp_signal_strategy_unittest.cc new file mode 100644 index 0000000..ca850f1 --- /dev/null +++ b/remoting/signaling/xmpp_signal_strategy_unittest.cc @@ -0,0 +1,268 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "remoting/signaling/xmpp_signal_strategy.h" + +#include "base/base64.h" +#include "base/message_loop/message_loop.h" +#include "base/run_loop.h" +#include "net/socket/socket_test_util.h" +#include "net/url_request/url_request_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/webrtc/libjingle/xmllite/xmlelement.h" + +namespace remoting { + +namespace { + +class XmppSocketDataProvider: public net::SocketDataProvider { + public: + net::MockRead GetNextRead() override { + return net::MockRead(net::ASYNC, net::ERR_IO_PENDING); + } + + net::MockWriteResult OnWrite(const std::string& data) override { + written_data_.append(data); + return net::MockWriteResult(net::SYNCHRONOUS, data.size()); + } + + void Reset() override {} + + void ReceiveData(const std::string& text) { + socket()->OnReadComplete( + net::MockRead(net::ASYNC, text.data(), text.size())); + } + + void Close() { + ReceiveData(std::string()); + } + + void SimulateNetworkError() { + socket()->OnReadComplete( + net::MockRead(net::ASYNC, net::ERR_CONNECTION_RESET)); + } + + std::string GetAndClearWrittenData() { + std::string data; + data.swap(written_data_); + return data; + } + + private: + std::string written_data_; +}; + +} // namespace + +const char kTestUsername[] = "test_username@example.com"; +const char kTestAuthToken[] = "test_auth_token"; + +class XmppSignalStrategyTest : public testing::Test, + public SignalStrategy::Listener { + public: + XmppSignalStrategyTest() : message_loop_(base::MessageLoop::TYPE_IO) {} + + void SetUp() override { + scoped_ptr<net::TestURLRequestContext> context( + new net::TestURLRequestContext()); + request_context_getter_ = new net::TestURLRequestContextGetter( + message_loop_.task_runner(), context.Pass()); + + XmppSignalStrategy::XmppServerConfig config; + config.host = "talk.google.com"; + config.port = 443; + config.username = kTestUsername; + config.auth_token = kTestAuthToken; + signal_strategy_.reset(new XmppSignalStrategy( + &client_socket_factory_, request_context_getter_, config)); + signal_strategy_->AddListener(this); + } + + void TearDown() override { + signal_strategy_->RemoveListener(this); + signal_strategy_.reset(); + base::RunLoop().RunUntilIdle(); + } + + void OnSignalStrategyStateChange(SignalStrategy::State state) override { + state_history_.push_back(state); + } + + bool OnSignalStrategyIncomingStanza(const buzz::XmlElement* stanza) override { + received_messages_.push_back( + make_scoped_ptr(new buzz::XmlElement(*stanza))); + return true; + } + + void Connect(bool success); + + protected: + base::MessageLoop message_loop_; + scoped_refptr<net::TestURLRequestContextGetter> request_context_getter_; + net::MockClientSocketFactory client_socket_factory_; + scoped_ptr<XmppSocketDataProvider> socket_data_provider_; + scoped_ptr<net::SSLSocketDataProvider> ssl_socket_data_provider_; + scoped_ptr<XmppSignalStrategy> signal_strategy_; + + std::vector<SignalStrategy::State> state_history_; + ScopedVector<buzz::XmlElement> received_messages_; +}; + +void XmppSignalStrategyTest::Connect(bool success) { + EXPECT_EQ(SignalStrategy::DISCONNECTED, signal_strategy_->GetState()); + state_history_.clear(); + + socket_data_provider_.reset(new XmppSocketDataProvider()); + socket_data_provider_->set_connect_data( + net::MockConnect(net::ASYNC, net::OK)); + client_socket_factory_.AddSocketDataProvider(socket_data_provider_.get()); + + ssl_socket_data_provider_.reset( + new net::SSLSocketDataProvider(net::ASYNC, net::OK)); + client_socket_factory_.AddSSLSocketDataProvider( + ssl_socket_data_provider_.get()); + + signal_strategy_->Connect(); + + EXPECT_EQ(SignalStrategy::CONNECTING, signal_strategy_->GetState()); + EXPECT_EQ(1U, state_history_.size()); + EXPECT_EQ(SignalStrategy::CONNECTING, state_history_[0]); + + // No data written before TLS. + EXPECT_EQ("", socket_data_provider_->GetAndClearWrittenData()); + + base::RunLoop().RunUntilIdle(); + + socket_data_provider_->ReceiveData( + "<stream:stream from=\"google.com\" id=\"DCDDE5171CB2154A\" " + "version=\"1.0\" " + "xmlns:stream=\"http://etherx.jabber.org/streams\" " + "xmlns=\"jabber:client\">" + "<stream:features>" + "<mechanisms xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">" + "<mechanism>X-OAUTH2</mechanism>" + "<mechanism>X-GOOGLE-TOKEN</mechanism>" + "<mechanism>PLAIN</mechanism>" + "</mechanisms>" + "</stream:features>"); + + base::RunLoop().RunUntilIdle(); + + std::string cookie; + base::Base64Encode(std::string("\0", 1) + kTestUsername + + std::string("\0", 1) + kTestAuthToken, + &cookie); + // Expect auth message. + EXPECT_EQ( + "<stream:stream to=\"google.com\" version=\"1.0\" " + "xmlns=\"jabber:client\" " + "xmlns:stream=\"http://etherx.jabber.org/streams\">" + "<auth xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\" mechanism=\"X-OAUTH2\" " + "auth:service=\"oauth2\" auth:allow-generated-jid=\"true\" " + "auth:client-uses-full-bind-result=\"true\" " + "auth:allow-non-google-login=\"true\" " + "xmlns:auth=\"http://www.google.com/talk/protocol/auth\">" + cookie + + "</auth>", socket_data_provider_->GetAndClearWrittenData()); + + if (!success) { + socket_data_provider_->ReceiveData( + "<failure xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">" + "<not-authorized/></failure>"); + EXPECT_EQ(2U, state_history_.size()); + EXPECT_EQ(SignalStrategy::DISCONNECTED, state_history_[1]); + EXPECT_EQ(SignalStrategy::AUTHENTICATION_FAILED, + signal_strategy_->GetError()); + return; + } + + socket_data_provider_->ReceiveData( + "<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"); + + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ( + "<stream:stream to=\"google.com\" version=\"1.0\" " + "xmlns=\"jabber:client\" " + "xmlns:stream=\"http://etherx.jabber.org/streams\">" + "<iq type=\"set\" id=\"0\">" + "<bind xmlns=\"urn:ietf:params:xml:ns:xmpp-bind\">" + "<resource>chromoting</resource>" + "</bind>" + "</iq>" + "<iq type=\"set\" id=\"1\">" + "<session xmlns=\"urn:ietf:params:xml:ns:xmpp-session\"/>" + "</iq>", + socket_data_provider_->GetAndClearWrittenData()); + socket_data_provider_->ReceiveData( + "<stream:stream from=\"google.com\" id=\"104FA10576E2AA80\" " + "version=\"1.0\" " + "xmlns:stream=\"http://etherx.jabber.org/streams\" " + "xmlns=\"jabber:client\">" + "<stream:features>" + "<bind xmlns=\"urn:ietf:params:xml:ns:xmpp-bind\"/>" + "<session xmlns=\"urn:ietf:params:xml:ns:xmpp-session\"/>" + "</stream:features>" + "<iq id=\"0\" type=\"result\">" + "<bind xmlns=\"urn:ietf:params:xml:ns:xmpp-bind\">" + "<jid>" + std::string(kTestUsername) + "/chromoting52B4920E</jid>" + "</bind>" + "</iq>" + "<iq type=\"result\" id=\"1\"/>"); + + EXPECT_EQ(2U, state_history_.size()); + EXPECT_EQ(SignalStrategy::CONNECTED, state_history_[1]); +} + +TEST_F(XmppSignalStrategyTest, SendAndReceive) { + Connect(true); + + EXPECT_TRUE(signal_strategy_->SendStanza(make_scoped_ptr( + new buzz::XmlElement(buzz::QName(std::string(), "hello"))))); + EXPECT_EQ("<hello/>", socket_data_provider_->GetAndClearWrittenData()); + + socket_data_provider_->ReceiveData("<hi xmlns=\"hello\"/>"); + EXPECT_EQ(1U, received_messages_.size()); + EXPECT_EQ("<hi xmlns=\"hello\"/>", received_messages_[0]->Str()); +} + +TEST_F(XmppSignalStrategyTest, AuthError) { + Connect(false); +} + +TEST_F(XmppSignalStrategyTest, ConnectionClosed) { + Connect(true); + + socket_data_provider_->Close(); + + EXPECT_EQ(3U, state_history_.size()); + EXPECT_EQ(SignalStrategy::DISCONNECTED, state_history_[2]); + EXPECT_EQ(SignalStrategy::DISCONNECTED, signal_strategy_->GetState()); + EXPECT_EQ(SignalStrategy::OK, signal_strategy_->GetError()); + + // Can't send messages anymore. + EXPECT_FALSE(signal_strategy_->SendStanza(make_scoped_ptr( + new buzz::XmlElement(buzz::QName(std::string(), "hello"))))); + + // Try connecting again. + Connect(true); +} + +TEST_F(XmppSignalStrategyTest, NetworkError) { + Connect(true); + + socket_data_provider_->SimulateNetworkError(); + + EXPECT_EQ(3U, state_history_.size()); + EXPECT_EQ(SignalStrategy::DISCONNECTED, state_history_[2]); + EXPECT_EQ(SignalStrategy::NETWORK_ERROR, signal_strategy_->GetError()); + + // Can't send messages anymore. + EXPECT_FALSE(signal_strategy_->SendStanza(make_scoped_ptr( + new buzz::XmlElement(buzz::QName(std::string(), "hello"))))); + + // Try connecting again. + Connect(true); +} + +} // namespace remoting diff --git a/remoting/signaling/xmpp_stream_parser.cc b/remoting/signaling/xmpp_stream_parser.cc new file mode 100644 index 0000000..7c207cb --- /dev/null +++ b/remoting/signaling/xmpp_stream_parser.cc @@ -0,0 +1,171 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "remoting/signaling/xmpp_stream_parser.h" + +#include "base/location.h" +#include "base/logging.h" +#include "base/single_thread_task_runner.h" +#include "base/thread_task_runner_handle.h" +#include "third_party/webrtc/libjingle/xmllite/xmlbuilder.h" +#include "third_party/webrtc/libjingle/xmllite/xmlelement.h" +#include "third_party/webrtc/libjingle/xmllite/xmlparser.h" + +namespace remoting { + +class XmppStreamParser::Core : public buzz::XmlParseHandler { + public: + typedef base::Callback<void(scoped_ptr<buzz::XmlElement> stanza)> + OnStanzaCallback; + + Core(); + ~Core() override; + + void SetCallbacks(const OnStanzaCallback& on_stanza_callback, + const base::Closure& on_error_callback); + + void AppendData(const std::string& data); + + private: + // buzz::XmlParseHandler interface. + void StartElement(buzz::XmlParseContext* context, + const char* name, + const char** atts) override; + void EndElement(buzz::XmlParseContext* context, const char* name) override; + void CharacterData(buzz::XmlParseContext* context, + const char* text, + int len) override; + void Error(buzz::XmlParseContext* context, XML_Error error_code) override; + + void ProcessError(); + + OnStanzaCallback on_stanza_callback_; + base::Closure on_error_callback_; + + buzz::XmlParser parser_; + int depth_; + buzz::XmlBuilder builder_; + + bool error_; + + DISALLOW_COPY_AND_ASSIGN(Core); +}; + +XmppStreamParser::Core::Core() + : parser_(this), + depth_(0), + error_(false) { +} + +XmppStreamParser::Core::~Core() { +} + +void XmppStreamParser::Core::SetCallbacks( + const OnStanzaCallback& on_stanza_callback, + const base::Closure& on_error_callback) { + on_stanza_callback_ = on_stanza_callback; + on_error_callback_ = on_error_callback; +} + +void XmppStreamParser::Core::AppendData(const std::string& data) { + if (error_) + return; + parser_.Parse(data.data(), data.size(), false); +} + +void XmppStreamParser::Core::StartElement(buzz::XmlParseContext* context, + const char* name, + const char** atts) { + DCHECK(!error_); + + ++depth_; + if (depth_ == 1) { + scoped_ptr<buzz::XmlElement> header( + buzz::XmlBuilder::BuildElement(context, name, atts)); + if (!header) { + LOG(ERROR) << "Failed to parse XMPP stream header."; + ProcessError(); + } + return; + } + + builder_.StartElement(context, name, atts); +} + +void XmppStreamParser::Core::EndElement(buzz::XmlParseContext* context, + const char* name) { + DCHECK(!error_); + + --depth_; + if (depth_ == 0) { + LOG(ERROR) << "XMPP stream ended unexpectedly."; + ProcessError(); + return; + } + + builder_.EndElement(context, name); + + if (depth_ == 1) { + if (!on_stanza_callback_.is_null()) + on_stanza_callback_.Run(make_scoped_ptr(builder_.CreateElement())); + } +} + +void XmppStreamParser::Core::CharacterData(buzz::XmlParseContext* context, + const char* text, + int len) { + DCHECK(!error_); + + // Ignore data between stanzas. + if (depth_ <= 1) { + // Only whitespace is allowed outside of the stanzas. + bool all_spaces = true; + for (char c: std::string(text, len)) { + if (c != ' ') { + all_spaces = false; + break; + } + } + if (!all_spaces) { + LOG(ERROR) << "Received unexpected string: " << std::string(text, + text + len); + ProcessError(); + } + } else if (depth_ > 1) { + builder_.CharacterData(context, text, len); + } +} + +void XmppStreamParser::Core::Error(buzz::XmlParseContext* context, + XML_Error error_code) { + LOG(ERROR) << "XMPP parser error: " << error_code; + ProcessError(); +} + +void XmppStreamParser::Core::ProcessError() { + error_ = true; + if (!on_error_callback_.is_null()) + on_error_callback_.Run(); +} + +XmppStreamParser::XmppStreamParser() : core_(new Core()) { +} + +XmppStreamParser::~XmppStreamParser() { + // Set null callbacks and delete |core_| asynchronously to make sure it's not + // deleted from a callback. + core_->SetCallbacks(OnStanzaCallback(), base::Closure()); + base::ThreadTaskRunnerHandle::Get()->DeleteSoon(FROM_HERE, core_.release()); +} + +void XmppStreamParser::SetCallbacks(const OnStanzaCallback& on_stanza_callback, + const base::Closure& on_error_callback) { + core_->SetCallbacks(on_stanza_callback, on_error_callback); +} + +void XmppStreamParser::AppendData(const std::string& data) { + core_->AppendData(data); +} + +} // namespace remoting diff --git a/remoting/signaling/xmpp_stream_parser.h b/remoting/signaling/xmpp_stream_parser.h new file mode 100644 index 0000000..1ac5027 --- /dev/null +++ b/remoting/signaling/xmpp_stream_parser.h @@ -0,0 +1,44 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef REMOTING_SIGNALING_XMPP_STREAM_PARSER_H_ +#define REMOTING_SIGNALING_XMPP_STREAM_PARSER_H_ + +#include <string> + +#include "base/callback.h" + +namespace buzz { +class XmlElement; +} // namespace buzz + +namespace remoting { + +// XmppStreamParser is used to parse XMPP stream. Data is fed to the parser +// using appendData() method and it calls |on_stanza_callback\ and +// |on_error_callback| specified using SetCallbacks(). +class XmppStreamParser { + public: + typedef base::Callback<void(scoped_ptr<buzz::XmlElement> stanza)> + OnStanzaCallback; + + XmppStreamParser(); + ~XmppStreamParser(); + + void SetCallbacks(const OnStanzaCallback& on_stanza_callback, + const base::Closure& on_error_callback); + + void AppendData(const std::string& data); + + private: + class Core; + + scoped_ptr<Core> core_; + + DISALLOW_COPY_AND_ASSIGN(XmppStreamParser); +}; + +} // namespace remoting + +#endif // REMOTING_SIGNALING_XMPP_STREAM_PARSER_H_ diff --git a/remoting/signaling/xmpp_stream_parser_unittest.cc b/remoting/signaling/xmpp_stream_parser_unittest.cc new file mode 100644 index 0000000..a76e02c --- /dev/null +++ b/remoting/signaling/xmpp_stream_parser_unittest.cc @@ -0,0 +1,94 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "remoting/signaling/xmpp_stream_parser.h" + +#include "base/bind.h" +#include "base/memory/scoped_vector.h" +#include "base/message_loop/message_loop.h" +#include "base/run_loop.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/webrtc/libjingle/xmllite/xmlelement.h" + +namespace remoting { + +class XmppStreamParserTest : public testing::Test { + public: + XmppStreamParserTest() : error_(false) {} + + void SetUp() override { + parser_.reset(new remoting::XmppStreamParser()); + parser_->SetCallbacks( + base::Bind(&XmppStreamParserTest::OnStanza, base::Unretained(this)), + base::Bind(&XmppStreamParserTest::OnError, base::Unretained(this))); + } + + void TearDown() override { + parser_.reset(); + base::RunLoop().RunUntilIdle(); + } + + void OnStanza(scoped_ptr<buzz::XmlElement> stanza) { + received_stanzas_.push_back(stanza.Pass()); + } + + void OnError() { + error_ = true; + } + + protected: + base::MessageLoop message_loop_; + + scoped_ptr<XmppStreamParser> parser_; + ScopedVector<buzz::XmlElement> received_stanzas_; + bool error_; +}; + +TEST_F(XmppStreamParserTest, ParseXmppStream) { + parser_->AppendData("<stream><iq>text</iq>"); + EXPECT_EQ(received_stanzas_[0]->Str(), "<iq>text</iq>"); +}; + +TEST_F(XmppStreamParserTest, HandleMultipleIncomingStanzas) { + parser_->AppendData("<stream><iq>text</iq><iq>more text</iq>"); + EXPECT_EQ(received_stanzas_[0]->Str(), "<iq>text</iq>"); + EXPECT_EQ(received_stanzas_[1]->Str(), "<iq>more text</iq>"); +}; + +TEST_F(XmppStreamParserTest, IgnoreWhitespaceBetweenStanzas) { + parser_->AppendData("<stream> <iq>text</iq>"); + EXPECT_EQ(received_stanzas_[0]->Str(), "<iq>text</iq>"); +}; + +TEST_F(XmppStreamParserTest, AssembleMessagesFromChunks) { + parser_->AppendData("<stream><i"); + parser_->AppendData("q>"); + + // Split one UTF-8 sequence into two chunks + std::string data = "😃"; + parser_->AppendData(data.substr(0, 2)); + parser_->AppendData(data.substr(2)); + + parser_->AppendData("</iq>"); + + EXPECT_EQ(received_stanzas_[0]->Str(), "<iq>😃</iq>"); +}; + +TEST_F(XmppStreamParserTest, StopParsingOnErrors) { + parser_->AppendData("<stream><invalidtag p!='a'></invalidtag><iq>text</iq>"); + EXPECT_TRUE(error_); + EXPECT_TRUE(received_stanzas_.empty()); +}; + +TEST_F(XmppStreamParserTest, FailOnInvalidStreamHeader) { + parser_->AppendData("<stream p!='a'>"); + EXPECT_TRUE(error_); +}; + +TEST_F(XmppStreamParserTest, FailOnLooseText) { + parser_->AppendData("stream<"); + EXPECT_TRUE(error_); +}; + +} // namespace remoting |