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_
+