buffet: Add correct handling of XMPP IQ stanzas

Implemented a more intelligent handling of IQ requests and responses.

Each time an IQ request is sent, a new unique request ID is generated
and then a response with the same ID is expected. If no reponse is
received within a timeout interval (of 30 seconds) a timeout callback
is called allowing the caller to handle this event correctly.

Changed the XMPP connection handshake implementation which used some
of IQ stanza exchange with the server to use the new IqStanzaHandler
class.

BUG=brillo:1138
TEST=`FEATURES=test emerge-link buffet`

Change-Id: I9534169466159d7531e5f01a25a0583ca6b341c3
Reviewed-on: https://chromium-review.googlesource.com/274446
Trybot-Ready: Alex Vakulenko <avakulenko@chromium.org>
Tested-by: Alex Vakulenko <avakulenko@chromium.org>
Reviewed-by: Vitaly Buka <vitalybuka@chromium.org>
Commit-Queue: Alex Vakulenko <avakulenko@chromium.org>
diff --git a/buffet/notification/xmpp_iq_stanza_handler.cc b/buffet/notification/xmpp_iq_stanza_handler.cc
new file mode 100644
index 0000000..e3a14c0
--- /dev/null
+++ b/buffet/notification/xmpp_iq_stanza_handler.cc
@@ -0,0 +1,128 @@
+// 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/notification/xmpp_iq_stanza_handler.h"
+
+#include <base/bind.h>
+#include <base/strings/string_number_conversions.h>
+#include <base/strings/stringprintf.h>
+
+#include "buffet/notification/xml_node.h"
+#include "buffet/notification/xmpp_channel.h"
+
+namespace buffet {
+
+namespace {
+
+// Default timeout for <iq> requests to the server. If the response hasn't been
+// received within this time interval, the request is considered as failed.
+const int kTimeoutIntervalSeconds = 30;
+
+// Builds an XML stanza that looks like this:
+//  <iq id='${id}' type='${type}' from='${from}' to='${to}'>$body</iq>
+// where 'to' and 'from' are optional attributes.
+std::string BuildIqStanza(const std::string& id,
+                          const std::string& type,
+                          const std::string& to,
+                          const std::string& from,
+                          const std::string& body) {
+  std::string to_attr;
+  if (!to.empty()) {
+    CHECK_EQ(std::string::npos, to.find_first_of("<'>"))
+        << "Destination address contains invalid XML characters";
+    base::StringAppendF(&to_attr, " to='%s'", to.c_str());
+  }
+  std::string from_attr;
+  if (!from.empty()) {
+    CHECK_EQ(std::string::npos, from.find_first_of("<'>"))
+        << "Source address contains invalid XML characters";
+    base::StringAppendF(&from_attr, " from='%s'", from.c_str());
+  }
+  return base::StringPrintf("<iq id='%s' type='%s'%s%s>%s</iq>",
+                            id.c_str(), type.c_str(), from_attr.c_str(),
+                            to_attr.c_str(), body.c_str());
+}
+
+}  // anonymous namespace
+
+IqStanzaHandler::IqStanzaHandler(
+    XmppChannelInterface* xmpp_channel,
+    const scoped_refptr<base::SingleThreadTaskRunner>& task_runner)
+    : xmpp_channel_{xmpp_channel}, task_runner_{task_runner} {}
+
+void IqStanzaHandler::SendRequest(
+    const std::string& type,
+    const std::string& from,
+    const std::string& to,
+    const std::string& body,
+    const ResponseCallback& response_callback,
+    const TimeoutCallback& timeout_callback) {
+  // Remember the response callback to call later.
+  requests_.emplace(++last_request_id_, response_callback);
+  // Schedule a time-out callback for this request.
+  task_runner_->PostDelayedTask(
+      FROM_HERE,
+      base::Bind(&IqStanzaHandler::OnTimeOut,
+                 weak_ptr_factory_.GetWeakPtr(),
+                 last_request_id_,
+                 timeout_callback),
+      base::TimeDelta::FromSeconds(kTimeoutIntervalSeconds));
+
+  std::string message = BuildIqStanza(std::to_string(last_request_id_),
+                                      type, to, from, body);
+  xmpp_channel_->SendMessage(message);
+}
+
+bool IqStanzaHandler::HandleIqStanza(std::unique_ptr<XmlNode> stanza) {
+  std::string type;
+  if (!stanza->GetAttribute("type", &type)) {
+    LOG(ERROR) << "IQ stanza missing 'type' attribute";
+    return false;
+  }
+
+  std::string id_str;
+  if (!stanza->GetAttribute("id", &id_str)) {
+    LOG(ERROR) << "IQ stanza missing 'id' attribute";
+    return false;
+  }
+
+  if (type == "result" || type == "error") {
+    // These are response stanzas from the server.
+    // Find the corresponding request.
+    RequestId id;
+    if (!base::StringToInt(id_str, &id)) {
+      LOG(ERROR) << "IQ stanza's 'id' attribute is invalid";
+      return false;
+    }
+    auto p = requests_.find(id);
+    if (p != requests_.end()) {
+      task_runner_->PostTask(
+          FROM_HERE,
+          base::Bind(p->second, base::Passed(std::move(stanza))));
+      requests_.erase(p);
+    }
+  } else {
+    // We do not support server-initiated IQ requests ("set" / "get" / "query").
+    // So just reply with "not implemented" error (and swap "to"/"from" attrs).
+    std::string error_body =
+        "<error type='modify'>"
+        "<feature-not-implemented xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>"
+        "</error>";
+    std::string message = BuildIqStanza(id_str, "error",
+                                        stanza->GetAttributeOrEmpty("from"),
+                                        stanza->GetAttributeOrEmpty("to"),
+                                        error_body);
+    xmpp_channel_->SendMessage(message);
+  }
+  return true;
+}
+
+void IqStanzaHandler::OnTimeOut(RequestId id,
+                                const TimeoutCallback& timeout_callback) {
+  // Request has not been processed yes, so a real timeout occurred.
+  if (requests_.erase(id) > 0)
+    timeout_callback.Run();
+}
+
+}  // namespace buffet