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