apmanager: monitor events from hostapd control interface

Monitor hostapd control interface for unsolicited event notifications,
such as station connect and disconnect events.

BUG=chromium:431759
TEST=USE="asan clang" FEATURES=test emerge-$BOARD apmanager
Manual Test:
1. Setup an AP service using apmanager.
2. Connect a client to the AP, verify that "Station connected" message is
   in "/var/log/message".
3. Disconnect the client from the AP, verify that "Station disconnect"
   message is in "/var/log/message".

Change-Id: I7410411c43db9670d7ae5e4cf4f9d83a2a2c6337
Reviewed-on: https://chromium-review.googlesource.com/240548
Trybot-Ready: Zeping Qiu <zqiu@chromium.org>
Tested-by: Zeping Qiu <zqiu@chromium.org>
Reviewed-by: Paul Stewart <pstew@chromium.org>
Commit-Queue: Zeping Qiu <zqiu@chromium.org>
diff --git a/hostapd_monitor.cc b/hostapd_monitor.cc
new file mode 100644
index 0000000..1bc9285
--- /dev/null
+++ b/hostapd_monitor.cc
@@ -0,0 +1,205 @@
+// 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 "apmanager/hostapd_monitor.h"
+
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/un.h>
+
+#include <base/bind.h>
+#include <base/logging.h>
+#include <base/strings/stringprintf.h>
+#include <shill/net/io_handler_factory_container.h>
+#include <shill/net/sockets.h>
+
+using base::Bind;
+using base::Unretained;
+using shill::IOHandlerFactoryContainer;
+using std::string;
+
+namespace apmanager {
+
+// static.
+const char HostapdMonitor::kLocalPathFormat[] =
+    "/var/run/apmanager/hostapd/hostapd_ctrl_%s";
+const char HostapdMonitor::kHostapdCmdAttach[] = "ATTACH";
+const char HostapdMonitor::kHostapdRespOk[] = "OK\n";
+const char HostapdMonitor::kHostapdEventStationConnected[] = "AP-STA-CONNECTED";
+const char HostapdMonitor::kHostapdEventStationDisconnected[] =
+    "AP-STA-DISCONNECTED";
+const int HostapdMonitor::kHostapdCtrlIfaceCheckIntervalMs = 500;
+const int HostapdMonitor::kHostapdCtrlIfaceCheckMaxAttempts = 5;
+const int HostapdMonitor::kHostapdAttachTimeoutMs = 1000;
+const int HostapdMonitor::kInvalidSocket = -1;
+
+HostapdMonitor::HostapdMonitor(const EventCallback& callback,
+                               const string& control_interface_path,
+                               const string& network_interface_name)
+    : sockets_(new shill::Sockets()),
+      event_callback_(callback),
+      dest_path_(base::StringPrintf("%s/%s",
+                                    control_interface_path.c_str(),
+                                    network_interface_name.c_str())),
+      local_path_(base::StringPrintf(kLocalPathFormat,
+                                     network_interface_name.c_str())),
+      hostapd_socket_(kInvalidSocket),
+      io_handler_factory_(
+          IOHandlerFactoryContainer::GetInstance()->GetIOHandlerFactory()),
+      event_dispatcher_(EventDispatcher::GetInstance()),
+      started_(false) {}
+
+HostapdMonitor::~HostapdMonitor() {
+  if (hostapd_socket_ != kInvalidSocket) {
+    unlink(local_path_.c_str());
+    sockets_->Close(hostapd_socket_);
+  }
+}
+
+void HostapdMonitor::Start() {
+  if (started_) {
+    LOG(ERROR) << "HostapdMonitor already started";
+    return;
+  }
+
+  hostapd_ctrl_iface_check_count_ = 0;
+  // Start off by checking the control interface file for the hostapd process.
+  event_dispatcher_->PostTask(
+      Bind(&HostapdMonitor::HostapdCtrlIfaceCheckTask, Unretained(this)));
+  started_ = true;
+}
+
+void HostapdMonitor::HostapdCtrlIfaceCheckTask() {
+  struct stat buf;
+  if (stat(dest_path_.c_str(), &buf) != 0) {
+    if (hostapd_ctrl_iface_check_count_ >= kHostapdCtrlIfaceCheckMaxAttempts) {
+      // TODO(zqiu): this indicates the hostapd failed to start. Invoke
+      // callback indicating hostapd start failure.
+      LOG(ERROR) << "Timeout waiting for hostapd control interface";
+    } else {
+      hostapd_ctrl_iface_check_count_++;
+      event_dispatcher_->PostDelayedTask(
+          base::Bind(&HostapdMonitor::HostapdCtrlIfaceCheckTask,
+                     base::Unretained(this)),
+          kHostapdCtrlIfaceCheckIntervalMs);
+    }
+    return;
+  }
+
+  // Attach to the control interface to receive unsolicited event notifications.
+  AttachToHostapd();
+}
+
+void HostapdMonitor::AttachToHostapd() {
+  if (hostapd_socket_ != kInvalidSocket) {
+    LOG(ERROR) << "Socket already initialized";
+    return;
+  }
+
+  // Setup socket address for local file and remote file.
+  struct sockaddr_un local;
+  local.sun_family = AF_UNIX;
+  snprintf(local.sun_path, sizeof(local.sun_path), "%s", local_path_.c_str());
+  struct sockaddr_un dest;
+  dest.sun_family = AF_UNIX;
+  snprintf(dest.sun_path, sizeof(dest.sun_path), "%s", dest_path_.c_str());
+
+  // Setup socket for interprocess communication.
+  hostapd_socket_ = sockets_->Socket(PF_UNIX, SOCK_DGRAM, 0);
+  if (hostapd_socket_ < 0) {
+    LOG(ERROR) << "Failed to open hostapd socket";
+    return;
+  }
+  if (sockets_->Bind(hostapd_socket_,
+                     reinterpret_cast<struct sockaddr*>(&local),
+                     sizeof(local)) < 0) {
+    LOG(ERROR) << "Failed to bind to local socket: " << strerror(errno);
+    return;
+  }
+  if (sockets_->Connect(hostapd_socket_,
+                        reinterpret_cast<struct sockaddr*>(&dest),
+                        sizeof(dest)) < 0) {
+    LOG(ERROR) << "Failed to connect: " << strerror(errno);
+    return;
+  }
+
+  // Setup IO Input handler.
+  hostapd_input_handler_.reset(io_handler_factory_->CreateIOInputHandler(
+      hostapd_socket_,
+      Bind(&HostapdMonitor::ParseMessage, Unretained(this)),
+      Bind(&HostapdMonitor::OnReadError, Unretained(this))));
+
+  if (!SendMessage(kHostapdCmdAttach, strlen(kHostapdCmdAttach))) {
+    LOG(ERROR) << "Failed to attach to hostapd";
+    return;
+  }
+
+  // Start a timer for ATTACH response.
+  attach_timeout_callback_.Reset(
+      Bind(&HostapdMonitor::AttachTimeoutHandler, Unretained(this)));
+  event_dispatcher_->PostDelayedTask(attach_timeout_callback_.callback(),
+                                     kHostapdAttachTimeoutMs);
+  return;
+}
+
+void HostapdMonitor::AttachTimeoutHandler() {
+  LOG(ERROR) << "Timeout waiting for attach response";
+}
+
+// Method for sending message to hostapd control interface.
+bool HostapdMonitor::SendMessage(const char* message, size_t length) {
+  if (sockets_->Send(hostapd_socket_, message, length, 0) < 0) {
+    LOG(ERROR) << "Send to hostapd failed: " << strerror(errno);
+    return false;
+  }
+
+  return true;
+}
+
+void HostapdMonitor::ParseMessage(shill::InputData* data) {
+  string str(reinterpret_cast<const char*>(data->buf), data->len);
+  // "OK" response for the "ATTACH" command.
+  if (str == kHostapdRespOk) {
+    attach_timeout_callback_.Cancel();
+    return;
+  }
+
+  // Event messages are in format of <[Level]>[Event] [Detail message].
+  // For example: <2>AP-STA-CONNECTED 00:11:22:33:44:55
+  // Refer to wpa_ctrl.h for complete list of possible events.
+  if (str.find_first_of('<', 0) == 0 && str.find_first_of('>', 0) == 2) {
+    // Remove the log level.
+    string msg = str.substr(3);
+    string event;
+    string data;
+    size_t pos = msg.find_first_of(' ', 0);
+    if (pos == string::npos) {
+      event = msg;
+    } else {
+      event = msg.substr(0, pos);
+      data = msg.substr(pos + 1);
+    }
+
+    Event event_code;
+    if (event == kHostapdEventStationConnected) {
+      event_code = kStationConnected;
+    } else if (event == kHostapdEventStationDisconnected) {
+      event_code = kStationDisconnected;
+    } else {
+      LOG(INFO) << "Received unknown event: " << event;
+      return;
+    }
+    event_callback_.Run(event_code, data);
+    return;
+  }
+
+  LOG(INFO) << "Received unknown message: " << str;
+}
+
+void HostapdMonitor::OnReadError(const string& error_msg) {
+  LOG(FATAL) << "Hostapd Socket read returns error: "
+             << error_msg;
+}
+
+}  // namespace apmanager