/*
 * platform-cros.c - CrOS platform DBus integration
 * Copyright (c) 2013 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 "config.h"

#include <ctype.h>
#include <dbus/dbus.h>
#include <errno.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#include <time.h>

#include <event2/event.h>

#include "src/dbus.h"
#include "src/platform.h"
#include "src/tlsdate.h"
#include "src/util.h"

static const char kMatchFormatData[] = "interface='%s',member='%s',arg0='%s'";
static const char *kMatchFormat = kMatchFormatData;
static const char kMatchNoArgFormatData[] = "interface='%s',member='%s'";
static const char *kMatchNoArgFormat = kMatchNoArgFormatData;

static const char kLibCrosDestData[] = "org.chromium.LibCrosService";
static const char *kLibCrosDest = kLibCrosDestData;
static const char kLibCrosInterfaceData[] = "org.chromium.LibCrosServiceInterface";
static const char *kLibCrosInterface = kLibCrosInterfaceData;
static const char kLibCrosPathData[] = "/org/chromium/LibCrosService";
static const char *kLibCrosPath = kLibCrosPathData;
static const char kResolveNetworkProxyData[] = "ResolveNetworkProxy";
static const char *kResolveNetworkProxy = kResolveNetworkProxyData;

static const char kDBusInterfaceData[] = "org.freedesktop.DBus";
static const char *kDBusInterface = kDBusInterfaceData;
static const char kNameOwnerChangedData[] = "NameOwnerChanged";
static const char *kNameOwnerChanged = kNameOwnerChangedData;
static const char kNameAcquiredData[] = "NameAcquired";
static const char *kNameAcquired = kNameAcquiredData;

static const char kManagerInterfaceData[] = "org.chromium.flimflam.Manager";
static const char *kManagerInterface = kManagerInterfaceData;

static const char kServiceInterfaceData[] = "org.chromium.flimflam.Service";
static const char *kServiceInterface = kServiceInterfaceData;
static const char kMemberData[] = "PropertyChanged";
static const char *kMember = kMemberData;

static const char kProxyConfigData[] = "ProxyConfig";
static const char *kProxyConfig = kProxyConfigData;
static const char kDefaultServiceData[] = "DefaultService";
static const char *kDefaultService = kDefaultServiceData;

static const char kResolveInterfaceData[] = "org.torproject.tlsdate.Resolver";
static const char *kResolveInterface = kResolveInterfaceData;
static const char kResolveMemberData[] = "ProxyChange";
static const char *kResolveMember = kResolveMemberData;

/* TODO(wad) Integrate with cros_system_api/dbus/service_constants.h */
static const char kPowerManagerInterfaceData[] = "org.chromium.PowerManager";
static const char *kPowerManagerInterface = kPowerManagerInterfaceData;
static const char kSuspendDoneData[] = "SuspendDone";
static const char *kSuspendDone = kSuspendDoneData;

static const char kErrorServiceUnknownData[] = "org.freedesktop.DBus.Error.ServiceUnknown";
static const char *kErrorServiceUnknown = kErrorServiceUnknownData;

struct platform_state
{
  struct event_base *base;
  struct state *state;
  DBusMessage **resolve_msg;
  int resolve_msg_count;
  uint32_t resolve_network_proxy_serial;
};

static
bool
get_valid_hostport (const char *hostport, char *out, size_t len)
{
  bool host = true;
  const char *end = hostport + strlen (hostport);
  const char *c;
  *out = '\0';
  /* Hosts begin with alphanumeric only. */
  if (!isalnum (*hostport))
    {
      info ("Host does not start with alnum");
      return false;
    }
  *out++ = *hostport;
  for (c = hostport + 1;  c < end && len > 0; ++c, ++out, --len)
    {
      *out = *c;
      if (host)
        {
          if (isalnum (*c) || *c == '-' || *c == '.')
            {
              continue;
            }
          if (*c == ':')
            {
              host = false;
              continue;
            }
        }
      else
        {
          if (isdigit (*c))
            continue;
        }
      *out = '\0';
      return false;
    }
  *out = '\0';
  return true;
}

/* Convert PAC return format to tlsdated url format */
/* TODO(wad) support multiple proxies when Chromium does:
 * PROXY x.x.x.x:yyyy; PROXY z.z.z.z:aaaaa
 */
static
void
canonicalize_pac (const char *pac_fmt, char *proxy_url, size_t len)
{
  size_t type_len;
  int copied = 0;
  const char *space;
  /* host[255]:port[6]\0 */
  char hostport[6 + 255 + 2];
  proxy_url[0] = '\0';
  if (len < 1)
    return;
  if (!strcmp (pac_fmt, "DIRECT"))
    {
      return;
    }
  /* Find type */
  space = strchr (pac_fmt, ' ');
  if (!space)
    return;
  type_len = space - pac_fmt;
  if (!get_valid_hostport (space + 1, hostport, sizeof (hostport)))
    {
      error ("invalid host:port: %s", space + 1);
      return;
    }
  proxy_url[0] = '\0';
  if (!strncmp (pac_fmt, "PROXY", type_len))
    {
      copied = snprintf (proxy_url, len, "http://%s", hostport);
    }
  else if (!strncmp (pac_fmt, "SOCKS", type_len))
    {
      copied = snprintf (proxy_url, len, "socks4://%s", hostport);
    }
  else if (!strncmp (pac_fmt, "SOCKS5", type_len))
    {
      copied = snprintf (proxy_url, len, "socks5://%s", hostport);
    }
  else if (!strncmp (pac_fmt, "HTTPS", type_len))
    {
      copied = snprintf (proxy_url, len, "https://%s", hostport);
    }
  else
    {
      error ("pac_fmt unmatched: '%s' %zu", pac_fmt, type_len);
    }
  if (copied < 0 || ((size_t) copied) >= len)
    {
      error ("canonicalize_pac: truncation '%s'", proxy_url);
      proxy_url[0] = '\0';
      return;
    }
}

static
DBusHandlerResult
handle_service_change (DBusConnection *connection,
                       DBusMessage *message,
                       struct platform_state *ctx)
{
  DBusMessageIter iter, subiter;
  DBusError error;
  const char *pname;
  const char *pval;
  const char *service;
  dbus_error_init (&error);
  verb_debug ("[event:cros:%s]: fired", __func__);
  /* TODO(wad) Track the current DefaultService only fire when it changes */
  service = dbus_message_get_path (message);
  if (!service)
    return DBUS_HANDLER_RESULT_HANDLED;
  /* Shill emits string:ProxyConfig variant string:"..." */
  if (!dbus_message_iter_init (message, &iter))
    return DBUS_HANDLER_RESULT_HANDLED;
  if (dbus_message_iter_get_arg_type (&iter) != DBUS_TYPE_STRING)
    return DBUS_HANDLER_RESULT_HANDLED;
  dbus_message_iter_get_basic (&iter, &pname);
  /* Make sure we are only firing on a ProxyConfig property change. */
  if (strcmp (pname, kProxyConfig))
    return DBUS_HANDLER_RESULT_HANDLED;
  if (!dbus_message_iter_next (&iter))
    return DBUS_HANDLER_RESULT_HANDLED;
  if (dbus_message_iter_get_arg_type (&iter) != DBUS_TYPE_VARIANT)
    return DBUS_HANDLER_RESULT_HANDLED;
  dbus_message_iter_recurse (&iter, &subiter);
  if (dbus_message_iter_get_arg_type (&subiter) != DBUS_TYPE_STRING)
    return DBUS_HANDLER_RESULT_HANDLED;
  dbus_message_iter_get_basic (&subiter, &pval);
  /* Right now, nothing is done with the Shill proxy value because
   * Chromium handles .pac resolution.  This may be more useful for
   * ignoring incomplete proxy values sent while a user is typing.
   */
  action_kickoff_time_sync (-1, EV_TIMEOUT, ctx->state);
  return DBUS_HANDLER_RESULT_HANDLED;
}

static
DBusHandlerResult
handle_manager_change (DBusConnection *connection,
                       DBusMessage *message,
                       struct platform_state *ctx)
{
  DBusMessageIter iter, subiter;
  DBusError error;
  const char *pname;
  const char *pval;
  verb_debug ("[event:cros:%s]: fired", __func__);
  dbus_error_init (&error);
  if (!dbus_message_iter_init (message, &iter))
    return DBUS_HANDLER_RESULT_HANDLED;
  if (dbus_message_iter_get_arg_type (&iter) != DBUS_TYPE_STRING)
    return DBUS_HANDLER_RESULT_HANDLED;
  dbus_message_iter_get_basic (&iter, &pname);
  /* Make sure we caught the right property. */
  if (strcmp (pname, kDefaultService))
    return DBUS_HANDLER_RESULT_HANDLED;
  if (!dbus_message_iter_next (&iter))
    return DBUS_HANDLER_RESULT_HANDLED;
  if (dbus_message_iter_get_arg_type (&iter) != DBUS_TYPE_VARIANT)
    return DBUS_HANDLER_RESULT_HANDLED;
  dbus_message_iter_recurse (&iter, &subiter);
  if (dbus_message_iter_get_arg_type (&subiter) != DBUS_TYPE_OBJECT_PATH)
    return DBUS_HANDLER_RESULT_HANDLED;
  dbus_message_iter_get_basic (&subiter, &pval);
  /* TODO(wad) Filter on the currently active service in pval. */
  verb_debug ("[event:cros:%s] service change on path %s",
         __func__, pval);
  action_kickoff_time_sync (-1, EV_TIMEOUT, ctx->state);
  return DBUS_HANDLER_RESULT_HANDLED;
}

static
DBusHandlerResult
handle_suspend_done (DBusConnection *connection,
                     DBusMessage *message,
                     struct platform_state *ctx)
{
  verb_debug ("[event:cros:%s]: fired", __func__);
  /* Coming back from resume, trigger a continuity and time
   * check just in case none of the other events happen.
   */
  action_kickoff_time_sync (-1, EV_TIMEOUT, ctx->state);
  return DBUS_HANDLER_RESULT_HANDLED;
}

static
DBusHandlerResult
handle_proxy_change (DBusConnection *connection,
                     DBusMessage *message,
                     struct platform_state *ctx)
{
  DBusMessageIter iter;
  DBusError error;
  const char *pname;
  const char *pval;
  char time_host[MAX_PROXY_URL];
  int url_len = 0;
  struct source *src = ctx->state->opts.sources;
  verb_debug ("[event:cros:%s]: fired", __func__);
  if (ctx->state->opts.cur_source && ctx->state->opts.cur_source->next)
    src = ctx->state->opts.cur_source->next;
  if (!ctx->state->resolving)
    {
      info ("[event:cros:%s] Unexpected ResolveNetworkProxy signal seen",
            __func__);
      return DBUS_HANDLER_RESULT_HANDLED;
    }
  dbus_error_init (&error);
  /* Shill emits string:ProxyConfig variant string:"..." */
  if (!dbus_message_iter_init (message, &iter))
    return DBUS_HANDLER_RESULT_HANDLED;
  if (dbus_message_iter_get_arg_type (&iter) != DBUS_TYPE_STRING)
    return DBUS_HANDLER_RESULT_HANDLED;
  dbus_message_iter_get_basic (&iter, &pname);
  /* Make sure this was the resolution we asked for */
  url_len = snprintf (time_host, sizeof (time_host), "https://%s:%s",
                      src->host, src->port);
  if (url_len < 0 || ((size_t) url_len) >= sizeof (time_host))
    {
      error ("[event:cros:%s]: current source url is too long",
             __func__);
    }
  if (strcmp (pname, time_host))
    {
      error ("[event:cros:%s]: resolved host mismatch: %s v %s",
             __func__, pname, time_host);
      return DBUS_HANDLER_RESULT_HANDLED;
    }
  if (!dbus_message_iter_next (&iter))
    return DBUS_HANDLER_RESULT_HANDLED;
  if (dbus_message_iter_get_arg_type (&iter) != DBUS_TYPE_STRING)
    return DBUS_HANDLER_RESULT_HANDLED;
  dbus_message_iter_get_basic (&iter, &pval);
  ctx->state->resolving = 0;
  canonicalize_pac (pval, ctx->state->dynamic_proxy, sizeof (ctx->state->dynamic_proxy));
  trigger_event (ctx->state, E_TLSDATE, 1);
  return DBUS_HANDLER_RESULT_HANDLED;
}

static
DBusHandlerResult
handle_dbus_change (DBusConnection *connection,
                    DBusMessage *message,
                    struct platform_state *ctx)
{
  DBusMessageIter iter;
  DBusError error;
  const char *pname;
  verb_debug ("[event:cros:%s]: fired", __func__);
  dbus_error_init (&error);
  if (!dbus_message_iter_init (message, &iter))
    return DBUS_HANDLER_RESULT_HANDLED;
  if (dbus_message_iter_get_arg_type (&iter) != DBUS_TYPE_STRING)
    return DBUS_HANDLER_RESULT_HANDLED;
  dbus_message_iter_get_basic (&iter, &pname);
  /* Make sure we caught the right property. */
  if (strcmp (pname, kLibCrosDest))
    return DBUS_HANDLER_RESULT_HANDLED;
  action_kickoff_time_sync (-1, EV_TIMEOUT, ctx->state);
  return DBUS_HANDLER_RESULT_HANDLED;
}

static
void
action_resolve_proxy (evutil_socket_t fd, short what, void *arg)
{
  struct platform_state *ctx = arg;
  struct dbus_state *dbus_state = ctx->state->dbus;
  DBusConnection *conn = dbus_state->conn;
  struct source *src = ctx->state->opts.sources;
  verb_debug ("[event:%s] fired", __func__);
  /* Emulate tlsdate-monitor.c:build_argv and choose the next source */
  if (ctx->state->opts.cur_source && ctx->state->opts.cur_source->next)
    src = ctx->state->opts.cur_source->next;
  if (ctx->state->resolving || ctx->resolve_network_proxy_serial)
    {
      /* Note, this is not the same as the response signal. It just avoids
       * multiple requests in a single dispatch window.
       */
      info ("[event:%s] no resolve_proxy sent; pending method_reply",
            __func__);
      return;
    }
  ctx->state->dynamic_proxy[0] = '\0';
  if (ctx->resolve_msg[src->id] == NULL)
    {
      info ("[event:%s] no dynamic proxy for %s:%s", __func__,
            src->host, src->port);
      trigger_event (ctx->state, E_TLSDATE, 1);
      return;
    }
  info ("[event:%s] resolving proxy for %s:%s", __func__,
        src->host, src->port);
  ctx->state->resolving = 1;
  if (!dbus_connection_send (conn,
                             ctx->resolve_msg[src->id],
                             &ctx->resolve_network_proxy_serial))
    {
      error ("[event:%s] cannot send ResolveNetworkProxy query!", __func__);
      return;
    }
}

static
DBusHandlerResult
dbus_filter (DBusConnection *connection, DBusMessage *message, void *data)
{
  struct platform_state *state = data;
  /* Terminate gracefully if DBus goes away. */
  if (dbus_message_is_signal (message, DBUS_INTERFACE_LOCAL, "Disconnected"))
    {
      error ("[cros] DBus system bus has become inaccessible. Terminating.");
      /* Trigger a graceful teardown. */
      kill (getpid(), SIGINT);
      return DBUS_HANDLER_RESULT_HANDLED;
    }
  /* Hand it over to the service dispatcher. */
  if (dbus_message_get_type (message) == DBUS_MESSAGE_TYPE_METHOD_CALL)
    return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;

  /* Handle explicitly defined signals only. */
  if (dbus_message_is_signal (message, kDBusInterface, kNameAcquired))
    {
      info ("[cros] DBus name acquired successfully");
      return DBUS_HANDLER_RESULT_HANDLED;
    }
  if (dbus_message_is_signal (message, kServiceInterface, kMember))
    return handle_service_change (connection, message, state);
  if (dbus_message_is_signal (message, kManagerInterface, kMember))
    return handle_manager_change (connection, message, state);
  if (dbus_message_is_signal (message, kResolveInterface, kResolveMember))
    return handle_proxy_change (connection, message, state);
  if (dbus_message_is_signal (message, kDBusInterface, kNameOwnerChanged))
    return handle_dbus_change (connection, message, state);
  if (dbus_message_is_signal (message, kPowerManagerInterface, kSuspendDone))
    return handle_suspend_done (connection, message, state);
  if (dbus_message_is_error (message, kErrorServiceUnknown))
    {
      info ("[cros] org.chromium.LibCrosService.ResolveNetworkProxy is missing");
      info ("[cros] skipping proxy resolution for now");
      /* Fire off tlsdate rather than letting it fail silently. */
      state->resolve_network_proxy_serial = 0;
      state->state->resolving = 0;
      trigger_event (state->state, E_TLSDATE, 1);
      return DBUS_HANDLER_RESULT_HANDLED;
    }
  /* Indicates a successful resolve request was issued. */
  if (dbus_message_get_type (message) == DBUS_MESSAGE_TYPE_METHOD_RETURN)
    {
      uint32_t serial = dbus_message_get_reply_serial (message);
      if (serial == state->resolve_network_proxy_serial)
        {
          state->resolve_network_proxy_serial = 0;
          return DBUS_HANDLER_RESULT_HANDLED;
        }
      info ("[cros] unknown DBus METHOD_RETURN seen: %u", serial);
      return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
    }
  verb_debug ("[cros] unknown message received: "
         "type=%s dest=%s interface=%s member=%s path=%s sig=%s error_name=%s",
         dbus_message_type_to_string (dbus_message_get_type (message)),
         dbus_message_get_destination (message),
         dbus_message_get_interface (message),
         dbus_message_get_member (message),
         dbus_message_get_path (message),
         dbus_message_get_signature (message),
         dbus_message_get_error_name (message));
  return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}

int
add_match (DBusConnection *conn, const char *interface, const char *member,
           const char *arg0)
{
  char match[1024];
  DBusError error;
  int len;
  dbus_error_init (&error);
  if (arg0)
    {
      len = snprintf (match, sizeof (match), kMatchFormat,
                      interface, member, arg0);
    }
  else
    {
      len = snprintf (match, sizeof (match), kMatchNoArgFormat,
                      interface, member);
    }
  if (len < 0 || ((size_t) len) >= sizeof (match))
    {
      error ("[dbus] match truncated for '%s,%s'", interface, member);
      return 1;
    }
  dbus_bus_add_match (conn, match, &error);
  if (dbus_error_is_set (&error))
    {
      error ("[dbus] failed to add_match for '%s,%s'; error: %s, %s",
             interface, member, error.name, error.message);
      dbus_error_free (&error);
      return 1;
    }
  return 0;
}

DBusMessage *
new_resolver_message(const struct source *src)
{
  char time_host[MAX_PROXY_URL];
  void *time_host_ptr = &time_host;
  int url_len;
  DBusMessage *res;
  DBusMessageIter args;
  if (!src->proxy || strcmp (src->proxy, "dynamic"))
    {
      return NULL;
    }
  res = dbus_message_new_method_call (kLibCrosDest, kLibCrosPath,
                                      kLibCrosInterface, kResolveNetworkProxy);
  if (!res)
    {
      error ("[cros] could not setup dynamic proxy for source %d", src->id);
      return NULL;
    }
  /* Build the time_host */
  url_len = snprintf (time_host, sizeof (time_host), "https://%s:%s",
                      src->host, src->port);
  if (url_len < 0 || ((size_t) url_len) >= sizeof (time_host))
    {
      fatal ("[cros] source %d url is too long! (%d)", src->id, url_len);
    }
  /* Finish the message */
  dbus_message_iter_init_append (res, &args);
  if (!dbus_message_iter_append_basic (&args, DBUS_TYPE_STRING, &time_host_ptr) ||
      !dbus_message_iter_append_basic (&args, DBUS_TYPE_STRING, &kResolveInterface) ||
      !dbus_message_iter_append_basic (&args, DBUS_TYPE_STRING, &kResolveMember))
    {
      fatal ("[cros could not append arguments for resolver message");
    }
  return res;
}

int
platform_init_cros (struct state *state)
{
  /* Watch for per-service ProxyConfig property changes */
  struct event_base *base = state->base;
  struct dbus_state *dbus_state = state->dbus;
  if (!dbus_state)
    {
      info ("[cros] DBus not connected, skipping platform initialization.");
      return 0;
    }
  struct source *src = NULL;
  int sources = 0;
  DBusConnection *conn = dbus_state->conn;
  DBusError error;
  struct platform_state *platform_state =
      calloc (1, sizeof (struct platform_state));
  if (!platform_state)
    {
      error ("[cros] could not allocate platform_state");
      return -1;
    }
  /* TODO(wad) Follow up with dbus_error_free() where needed. */
  dbus_error_init (&error);
  /* Add watches for: proxy changes, default service changes, proxy resolution,
   * LibCrosService ownership, and power state changes.
   */
  if (add_match (conn, kServiceInterface, kMember, kProxyConfig) ||
      add_match (conn, kManagerInterface, kMember, kDefaultService) ||
      add_match (conn, kResolveInterface, kResolveMember, NULL) ||
      add_match (conn, kDBusInterface, kNameOwnerChanged, kLibCrosDest) ||
      add_match (conn, kPowerManagerInterface, kSuspendDone, NULL))
    return 1;

  /* Allocate one per source */
  for (src = state->opts.sources; src; src = src->next, ++sources);
  platform_state->resolve_msg_count = sources;
  platform_state->resolve_msg = calloc (sources, sizeof (DBusMessage *));
  if (!platform_state->resolve_msg)
    {
      error ("[cros] cannot allocate resolver messages");
      free (platform_state);
      return -1;
    }
  for (src = state->opts.sources; src; src = src->next)
    {
      if (src->id >= sources)
        fatal ("Source ID is greater than available sources!");
      platform_state->resolve_msg[src->id] = new_resolver_message (src);
      if (platform_state->resolve_msg[src->id])
        src->proxy = state->dynamic_proxy;
    }
  state->dynamic_proxy[0] = '\0';
  if (state->opts.proxy && !strcmp (state->opts.proxy, "dynamic"))
    {
      info ("[cros] default dynamic proxy support");
      state->opts.proxy = state->dynamic_proxy;
    }
  platform_state->base = base;
  platform_state->state = state;
  /* Add the dynamic resolver if tlsdate doesn't already have one. */
  if (!state->events[E_RESOLVER])
    {
      state->events[E_RESOLVER] = event_new (base, -1, EV_TIMEOUT,
                                             action_resolve_proxy,
                                             platform_state);
      if (!state->events[E_RESOLVER])
        /* Let's not clean up DBus. */
        fatal ("Could not allocated resolver event");
      /* Wake up as a NET event since it'll self-block until DBus has a chance
       * to send it.
       */
      event_priority_set (state->events[E_RESOLVER], PRI_NET);
    }
  /* Each platform can attach their own filter, but the filter func needs to be
   * willing to DBUS_HANDLER_RESULT_NOT_YET_HANDLED on unexpected events.
   */
  /* TODO(wad) add the clean up function as the callback. */
  if (!dbus_connection_add_filter (conn,
                                   dbus_filter, platform_state, NULL))
    {
      error ("Failed to register signal handler callback");
      return 1;
    }
  return 0;
}