buffet: Add an XMPP client class This is a relatively simple XMPP client class, which at this point, is only used to keep the XMPP connection open to GCD. TEST=FEATURES=test emerge buffet BUG=brillo:95 Change-Id: I2e7c8d7352892bd7c94e630cc7872f32f2298ae4 Reviewed-on: https://chromium-review.googlesource.com/248351 Reviewed-by: Alex Vakulenko <avakulenko@chromium.org> Commit-Queue: Nathan Bullock <nathanbullock@google.com> Tested-by: Nathan Bullock <nathanbullock@google.com>
diff --git a/buffet/buffet.gyp b/buffet/buffet.gyp index d4c4541..6628f4d 100644 --- a/buffet/buffet.gyp +++ b/buffet/buffet.gyp
@@ -43,6 +43,8 @@ 'states/state_manager.cc', 'states/state_package.cc', 'utils.cc', + 'xmpp/xmpp_client.cc', + 'xmpp/xmpp_connection.cc', ], 'includes': ['../common-mk/generate-dbus-adaptors.gypi'], 'actions': [ @@ -117,6 +119,7 @@ 'states/state_change_queue_unittest.cc', 'states/state_manager_unittest.cc', 'states/state_package_unittest.cc', + 'xmpp/xmpp_client_unittest.cc', ], }, ],
diff --git a/buffet/xmpp/xmpp_client.cc b/buffet/xmpp/xmpp_client.cc new file mode 100644 index 0000000..d0f4a56 --- /dev/null +++ b/buffet/xmpp/xmpp_client.cc
@@ -0,0 +1,124 @@ +// Copyright 2015 The Chromium OS 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 "xmpp/xmpp_client.h" + +#include <arpa/inet.h> +#include <netdb.h> +#include <netinet/in.h> +#include <unistd.h> + +#include <string> + +#include <base/files/file_util.h> +#include <chromeos/data_encoding.h> +#include <chromeos/syslog_logging.h> + +namespace buffet { + +namespace { + +std::string BuildXmppStartStreamCommand() { + return "<stream:stream to='clouddevices.gserviceaccount.com' " + "xmlns:stream='http://etherx.jabber.org/streams' " + "xml:lang='*' version='1.0' xmlns='jabber:client'>"; +} + +std::string BuildXmppAuthenticateCommand( + const std::string& account, const std::string& token) { + chromeos::Blob credentials; + credentials.push_back(0); + credentials.insert(credentials.end(), account.begin(), account.end()); + credentials.push_back(0); + credentials.insert(credentials.end(), token.begin(), token.end()); + std::string msg = "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' " + "mechanism='X-OAUTH2' auth:service='oauth2' " + "auth:allow-non-google-login='true' " + "auth:client-uses-full-bind-result='true' " + "xmlns:auth='http://www.google.com/talk/protocol/auth'>" + + chromeos::data_encoding::Base64Encode(credentials) + + "</auth>"; + return msg; +} + +std::string BuildXmppBindCommand() { + return "<iq type='set' id='0'>" + "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/></iq>"; +} + +std::string BuildXmppStartSessionCommand() { + return "<iq type='set' id='1'>" + "<session xmlns='urn:ietf:params:xml:ns:xmpp-session'/></iq>"; +} + +std::string BuildXmppSubscribeCommand(const std::string& account) { + return "<iq type='set' to='" + account + "' " + "id='pushsubscribe1'><subscribe xmlns='google:push'>" + "<item channel='cloud_devices' from=''/>" + "</subscribe></iq>"; +} + +} // namespace + +void XmppClient::Read() { + std::string msg; + if (!connection_->Read(&msg) || msg.size() <= 0) { + LOG(ERROR) << "Failed to read from stream"; + return; + } + + // TODO(nathanbullock): Need to add support for TLS (brillo:191). + switch (state_) { + case XmppState::kStarted: + if (std::string::npos != msg.find(":features") && + std::string::npos != msg.find("X-GOOGLE-TOKEN")) { + state_ = XmppState::kAuthenticationStarted; + connection_->Write(BuildXmppAuthenticateCommand( + account_, access_token_)); + } + break; + case XmppState::kAuthenticationStarted: + if (std::string::npos != msg.find("success")) { + state_ = XmppState::kStreamRestartedPostAuthentication; + connection_->Write(BuildXmppStartStreamCommand()); + } + break; + case XmppState::kStreamRestartedPostAuthentication: + if (std::string::npos != msg.find(":features") && + std::string::npos != msg.find(":xmpp-session")) { + state_ = XmppState::kBindSent; + connection_->Write(BuildXmppBindCommand()); + } + break; + case XmppState::kBindSent: + if (std::string::npos != msg.find("iq") && + std::string::npos != msg.find("result")) { + state_ = XmppState::kSessionStarted; + connection_->Write(BuildXmppStartSessionCommand()); + } + break; + case XmppState::kSessionStarted: + if (std::string::npos != msg.find("iq") && + std::string::npos != msg.find("result")) { + state_ = XmppState::kSubscribeStarted; + connection_->Write(BuildXmppSubscribeCommand(account_)); + } + break; + case XmppState::kSubscribeStarted: + if (std::string::npos != msg.find("iq") && + std::string::npos != msg.find("result")) { + state_ = XmppState::kSubscribed; + } + break; + default: + break; + } +} + +void XmppClient::StartStream() { + state_ = XmppState::kStarted; + connection_->Write(BuildXmppStartStreamCommand()); +} + +} // namespace buffet
diff --git a/buffet/xmpp/xmpp_client.h b/buffet/xmpp/xmpp_client.h new file mode 100644 index 0000000..9eb5826 --- /dev/null +++ b/buffet/xmpp/xmpp_client.h
@@ -0,0 +1,70 @@ +// Copyright 2015 The Chromium OS 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 BUFFET_XMPP_XMPP_CLIENT_H_ +#define BUFFET_XMPP_XMPP_CLIENT_H_ + +#include <memory> +#include <string> + +#include <base/macros.h> + +#include "buffet/xmpp/xmpp_connection.h" + +namespace buffet { + +class XmppClient final { + public: + // |account| is the robot account for buffet and |access_token| + // it the OAuth token. Note that the OAuth token expires fairly frequently + // so you will need to reset the XmppClient every time this happens. + XmppClient(const std::string& account, + const std::string& access_token, + std::unique_ptr<XmppConnection> connection) + : account_{account}, + access_token_{access_token}, + connection_{std::move(connection)} {} + + int GetFileDescriptor() const { + return connection_->GetFileDescriptor(); + } + + // Needs to be called when new data is available from the connection. + void Read(); + + // Start talking to the XMPP server (authenticate, etc.) + void StartStream(); + + // Internal states for the XMPP stream. + enum class XmppState { + kNotStarted, + kStarted, + kAuthenticationStarted, + kStreamRestartedPostAuthentication, + kBindSent, + kSessionStarted, + kSubscribeStarted, + kSubscribed, + }; + + private: + // Robot account name for the device. + std::string account_; + + // OAuth access token for the account. Expires fairly frequently. + std::string access_token_; + + // The connection to the XMPP server. + std::unique_ptr<XmppConnection> connection_; + + XmppState state_{XmppState::kNotStarted}; + + friend class TestHelper; + DISALLOW_COPY_AND_ASSIGN(XmppClient); +}; + +} // namespace buffet + +#endif // BUFFET_XMPP_XMPP_CLIENT_H_ +
diff --git a/buffet/xmpp/xmpp_client_unittest.cc b/buffet/xmpp/xmpp_client_unittest.cc new file mode 100644 index 0000000..9736349 --- /dev/null +++ b/buffet/xmpp/xmpp_client_unittest.cc
@@ -0,0 +1,208 @@ +// Copyright 2015 The Chromium OS 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 "buffet/device_registration_info.h" + +#include <base/values.h> +#include <chromeos/key_value_store.h> +#include <chromeos/http/curl_api.h> +#include <gmock/gmock.h> +#include <gtest/gtest.h> + +#include "buffet/xmpp/xmpp_client.h" +#include "buffet/xmpp/xmpp_connection.h" + +using ::testing::DoAll; +using ::testing::Return; +using ::testing::SetArgPointee; +using ::testing::_; + +namespace buffet { + +namespace { + +constexpr char kAccountName[] = "Account@Name"; +constexpr char kAccessToken[] = "AccessToken"; + +constexpr char kStartStreamResponse[] = + "<stream:stream from=\"clouddevices.gserviceaccount.com\" " + "id=\"0CCF520913ABA04B\" 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>"; +constexpr char kAuthenticationResponse[] = + "<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"; +constexpr char kRestartStreamResponse[] = + "<stream:stream from=\"clouddevices.gserviceaccount.com\" " + "id=\"BE7D34E0B7589E2A\" 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>"; +constexpr char kBindResponse[] = + "<iq id=\"0\" type=\"result\">" + "<bind xmlns=\"urn:ietf:params:xml:ns:xmpp-bind\">" + "<jid>110cc78f78d7032cc7bf2c6e14c1fa7d@clouddevices.gserviceaccount.com" + "/19853128</jid></bind></iq>"; +constexpr char kSessionResponse[] = + "<iq type=\"result\" id=\"1\"/>"; +constexpr char kSubscribedResponse[] = + "<iq to=\"" + "110cc78f78d7032cc7bf2c6e14c1fa7d@clouddevices.gserviceaccount.com/" + "19853128\" from=\"" + "110cc78f78d7032cc7bf2c6e14c1fa7d@clouddevices.gserviceaccount.com\" " + "id=\"pushsubscribe1\" type=\"result\"/>"; + +constexpr char kStartStreamMessage[] = + "<stream:stream to='clouddevices.gserviceaccount.com' " + "xmlns:stream='http://etherx.jabber.org/streams' xml:lang='*' " + "version='1.0' xmlns='jabber:client'>"; +constexpr char kAuthenticationMessage[] = + "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='X-OAUTH2' " + "auth:service='oauth2' auth:allow-non-google-login='true' " + "auth:client-uses-full-bind-result='true' " + "xmlns:auth='http://www.google.com/talk/protocol/auth'>" + "AEFjY291bnRATmFtZQBBY2Nlc3NUb2tlbg==</auth>"; +constexpr char kBindMessage[] = + "<iq type='set' id='0'><bind " + "xmlns='urn:ietf:params:xml:ns:xmpp-bind'/></iq>"; +constexpr char kSessionMessage[] = + "<iq type='set' id='1'><session " + "xmlns='urn:ietf:params:xml:ns:xmpp-session'/></iq>"; +constexpr char kSubscribeMessage[] = + "<iq type='set' to='Account@Name' id='pushsubscribe1'>" + "<subscribe xmlns='google:push'><item channel='cloud_devices' from=''/>" + "</subscribe></iq>"; + +class MockXmppConnection : public XmppConnection { + public: + MockXmppConnection() = default; + + MOCK_CONST_METHOD1(Read, bool(std::string* msg)); + MOCK_CONST_METHOD1(Write, bool(const std::string& msg)); + + private: + DISALLOW_COPY_AND_ASSIGN(MockXmppConnection); +}; + +} // namespace + +class TestHelper { + public: + static void SetState(XmppClient* client, XmppClient::XmppState state) { + client->state_ = state; + } + + static XmppClient::XmppState GetState(const XmppClient& client) { + return client.state_; + } + + static XmppConnection* GetConnection(const XmppClient& client) { + return client.connection_.get(); + } +}; + +class XmppClientTest : public ::testing::Test { + protected: + void SetUp() override { + std::unique_ptr<XmppConnection> connection(new MockXmppConnection()); + xmpp_client_.reset(new XmppClient(kAccountName, kAccessToken, + std::move(connection))); + connection_ = static_cast<MockXmppConnection*>( + TestHelper::GetConnection(*xmpp_client_)); + } + + std::unique_ptr<XmppClient> xmpp_client_; + MockXmppConnection* connection_; // xmpp_client_ owns this. +}; + +TEST_F(XmppClientTest, StartStream) { + EXPECT_EQ(TestHelper::GetState(*xmpp_client_), + XmppClient::XmppState::kNotStarted); + EXPECT_CALL(*connection_, Write(kStartStreamMessage)) + .WillOnce(Return(true)); + xmpp_client_->StartStream(); + EXPECT_EQ(TestHelper::GetState(*xmpp_client_), + XmppClient::XmppState::kStarted); +} + +TEST_F(XmppClientTest, HandleStartedResponse) { + TestHelper::SetState(xmpp_client_.get(), + XmppClient::XmppState::kStarted); + EXPECT_CALL(*connection_, Read(_)) + .WillOnce(DoAll(SetArgPointee<0>(kStartStreamResponse), Return(true))); + EXPECT_CALL(*connection_, Write(kAuthenticationMessage)) + .WillOnce(Return(true)); + xmpp_client_->Read(); + EXPECT_EQ(TestHelper::GetState(*xmpp_client_), + XmppClient::XmppState::kAuthenticationStarted); +} + +TEST_F(XmppClientTest, HandleAuthenticationResponse) { + TestHelper::SetState( + xmpp_client_.get(), + XmppClient::XmppState::kAuthenticationStarted); + EXPECT_CALL(*connection_, Read(_)) + .WillOnce(DoAll(SetArgPointee<0>(kAuthenticationResponse), Return(true))); + EXPECT_CALL(*connection_, Write(kStartStreamMessage)) + .WillOnce(Return(true)); + xmpp_client_->Read(); + EXPECT_EQ(TestHelper::GetState(*xmpp_client_), + XmppClient::XmppState::kStreamRestartedPostAuthentication); +} + +TEST_F(XmppClientTest, HandleStreamRestartedResponse) { + TestHelper::SetState( + xmpp_client_.get(), + XmppClient::XmppState::kStreamRestartedPostAuthentication); + EXPECT_CALL(*connection_, Read(_)) + .WillOnce(DoAll(SetArgPointee<0>(kRestartStreamResponse), Return(true))); + EXPECT_CALL(*connection_, Write(kBindMessage)) + .WillOnce(Return(true)); + xmpp_client_->Read(); + EXPECT_EQ(TestHelper::GetState(*xmpp_client_), + XmppClient::XmppState::kBindSent); +} + +TEST_F(XmppClientTest, HandleBindResponse) { + TestHelper::SetState(xmpp_client_.get(), + XmppClient::XmppState::kBindSent); + EXPECT_CALL(*connection_, Read(_)) + .WillOnce(DoAll(SetArgPointee<0>(kBindResponse), Return(true))); + EXPECT_CALL(*connection_, Write(kSessionMessage)) + .WillOnce(Return(true)); + xmpp_client_->Read(); + EXPECT_EQ(TestHelper::GetState(*xmpp_client_), + XmppClient::XmppState::kSessionStarted); +} + +TEST_F(XmppClientTest, HandleSessionResponse) { + TestHelper::SetState(xmpp_client_.get(), + XmppClient::XmppState::kSessionStarted); + EXPECT_CALL(*connection_, Read(_)) + .WillOnce(DoAll(SetArgPointee<0>(kSessionResponse), Return(true))); + EXPECT_CALL(*connection_, Write(kSubscribeMessage)) + .WillOnce(Return(true)); + xmpp_client_->Read(); + EXPECT_EQ(TestHelper::GetState(*xmpp_client_), + XmppClient::XmppState::kSubscribeStarted); +} + +TEST_F(XmppClientTest, HandleSubscribeResponse) { + TestHelper::SetState(xmpp_client_.get(), + XmppClient::XmppState::kSubscribeStarted); + EXPECT_CALL(*connection_, Read(_)) + .WillOnce(DoAll(SetArgPointee<0>(kSubscribedResponse), Return(true))); + EXPECT_CALL(*connection_, Write(_)) + .Times(0); + xmpp_client_->Read(); + EXPECT_EQ(TestHelper::GetState(*xmpp_client_), + XmppClient::XmppState::kSubscribed); +} + +} // namespace buffet
diff --git a/buffet/xmpp/xmpp_connection.cc b/buffet/xmpp/xmpp_connection.cc new file mode 100644 index 0000000..8799493 --- /dev/null +++ b/buffet/xmpp/xmpp_connection.cc
@@ -0,0 +1,72 @@ +// Copyright 2015 The Chromium OS 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 "buffet/xmpp/xmpp_connection.h" + +#include <arpa/inet.h> +#include <netdb.h> +#include <netinet/in.h> +#include <unistd.h> + +#include <string> + +#include <base/files/file_util.h> +#include <chromeos/syslog_logging.h> + +namespace buffet { + +XmppConnection::~XmppConnection() { + if (fd_ > 0) { + close(fd_); + } +} + +bool XmppConnection::Initialize() { + LOG(INFO) << "Opening XMPP connection"; + + fd_ = socket(AF_INET, SOCK_STREAM, 0); + + // TODO(nathanbullock): Use gethostbyname_r. + struct hostent* server = gethostbyname("talk.google.com"); + if (server == NULL) { + LOG(WARNING) << "Failed to find host talk.google.com"; + return false; + } + + sockaddr_in serv_addr; + memset(&serv_addr, 0, sizeof(serv_addr)); + serv_addr.sin_family = AF_INET; + bcopy(server->h_addr, &(serv_addr.sin_addr), server->h_length); + serv_addr.sin_port = htons(5222); + + if (connect(fd_, (struct sockaddr*)&serv_addr, sizeof(sockaddr)) < 0) { + LOG(WARNING) << "Failed to connect to talk.google.com:5222"; + return false; + } + + return true; +} + +bool XmppConnection::Read(std::string* msg) const { + char buffer[4096]; // This should be large enough for our purposes. + ssize_t bytes = HANDLE_EINTR(read(fd_, buffer, sizeof(buffer))); + if (bytes < 0) { + LOG(WARNING) << "Failure reading"; + return false; + } + *msg = std::string{buffer, static_cast<size_t>(bytes)}; + VLOG(1) << "READ: (" << msg->size() << ")" << *msg; + return true; +} + +bool XmppConnection::Write(const std::string& msg) const { + VLOG(1) << "WRITE: (" << msg.size() << ")" << msg; + if (!base::WriteFileDescriptor(fd_, msg.c_str(), msg.size())) { + LOG(WARNING) << "Failure writing"; + return false; + } + return true; +} + +} // namespace buffet
diff --git a/buffet/xmpp/xmpp_connection.h b/buffet/xmpp/xmpp_connection.h new file mode 100644 index 0000000..741f4c8 --- /dev/null +++ b/buffet/xmpp/xmpp_connection.h
@@ -0,0 +1,40 @@ +// Copyright 2015 The Chromium OS 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 BUFFET_XMPP_XMPP_CONNECTION_H_ +#define BUFFET_XMPP_XMPP_CONNECTION_H_ + +#include <string> + +#include <base/macros.h> + +namespace buffet { + +class XmppConnection { + public: + XmppConnection() {} + virtual ~XmppConnection(); + + // Initialize the XMPP client. (Connects to talk.google.com:5222). + virtual bool Initialize(); + + int GetFileDescriptor() const { return fd_; } + + // Needs to be called when new data is available from the connection. + virtual bool Read(std::string* msg) const; + + // Start talking to the XMPP server (authenticate, etc.) + virtual bool Write(const std::string& msg) const; + + private: + // The file descriptor for the connection. + int fd_{-1}; + + DISALLOW_COPY_AND_ASSIGN(XmppConnection); +}; + +} // namespace buffet + +#endif // BUFFET_XMPP_XMPP_CONNECTION_H_ +