/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
 *
 * Copyright © 2018 Endless Mobile, Inc.
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * Authors:
 *  - Philip Withnall <withnall@endlessm.com>
 */

#include "config.h"

#include <glib.h>
#include <glib-object.h>
#include <glib/gi18n-lib.h>
#include <glib/gstdio.h>
#include <gio/gio.h>
#include <libgsystemservice/peer-manager.h>
#include <libgsystemservice/peer-manager-dbus.h>
#include <stdlib.h>


/**
 * SECTION:peer-manager-dbus
 * @short_description: D-Bus peer management and notification (implementation)
 * @stability: Stable
 * @include: libgsystemservice/peer-manager-dbus.h
 *
 * An implementation of the #GssPeerManager interface which draws its data
 * from the D-Bus daemon. This is the only expected runtime implementation of
 * the interface, and has only been split out from the interface to allow for
 * easier unit testing of anything which uses it.
 *
 * The credentials of a peer are retrieved from the D-Bus daemon using
 * [`GetConnectionCredentials`](https://dbus.freedesktop.org/doc/dbus-specification.html#bus-messages-get-connection-credentials),
 * and reading `/proc/$pid/cmdline` to get the absolute path to the executable
 * for each peer, which we use as an identifier for it. This is not atomic or
 * particularly trusted, as PIDs can be reused in the time it takes us to query
 * the information, and processes can modify their own cmdline file, but
 * without an LSM enabled in the kernel and dbus-daemon, it’s the best we can do
 * for identifying processes.
 *
 * Since: 0.1.0
 */

typedef struct
{
  char *argv0;  /* (owned) (nullable) */
  int pidfd;  /* (owned) */
  uid_t uid;
} PeerCredentials;

static void
peer_credentials_free (PeerCredentials *credentials)
{
  g_clear_pointer (&credentials->argv0, g_free);
  if (credentials->pidfd >= 0)
    g_close (credentials->pidfd, NULL);
  g_free (credentials);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC (PeerCredentials, peer_credentials_free)

static void gss_peer_manager_dbus_peer_manager_init (GssPeerManagerInterface *iface);
static void gss_peer_manager_dbus_dispose           (GObject                 *object);

static void gss_peer_manager_dbus_get_property (GObject      *object,
                                                guint         property_id,
                                                GValue        *value,
                                                GParamSpec   *pspec);
static void gss_peer_manager_dbus_set_property (GObject      *object,
                                                guint         property_id,
                                                const GValue *value,
                                                GParamSpec   *pspec);

static void         gss_peer_manager_dbus_ensure_peer_credentials_async  (GssPeerManager       *manager,
                                                                          const gchar          *sender,
                                                                          GCancellable         *cancellable,
                                                                          GAsyncReadyCallback   callback,
                                                                          gpointer              user_data);
static gboolean     gss_peer_manager_dbus_ensure_peer_credentials_finish (GssPeerManager       *manager,
                                                                          GAsyncResult         *result,
                                                                          GError              **error);
static const gchar *gss_peer_manager_dbus_get_peer_argv0                 (GssPeerManager       *manager,
                                                                          const gchar          *sender);
static int          gss_peer_manager_dbus_get_peer_pidfd                 (GssPeerManager       *manager,
                                                                          const gchar          *sender);
static uid_t        gss_peer_manager_dbus_get_peer_uid                   (GssPeerManager       *manager,
                                                                          const gchar          *sender);

/**
 * GssPeerManagerDBus:
 *
 * An implementation of #GssPeerManager.
 *
 * Since: 0.1.0
 */
struct _GssPeerManagerDBus
{
  GObject parent;

  GDBusConnection *connection;  /* (owned) */

  /* Hold the watch IDs of all peers who have added entries at some point. */
  GPtrArray *peer_watch_ids;  /* (owned) */

  /* Cache of peer credentials. */
  GHashTable *peer_credentials;  /* (owned) (element-type utf8 PeerCredentials) */
};

typedef enum
{
  PROP_CONNECTION = 1,
} GssPeerManagerDBusProperty;

G_DEFINE_TYPE_WITH_CODE (GssPeerManagerDBus, gss_peer_manager_dbus, G_TYPE_OBJECT,
                         G_IMPLEMENT_INTERFACE (GSS_TYPE_PEER_MANAGER,
                                                gss_peer_manager_dbus_peer_manager_init))
static void
gss_peer_manager_dbus_class_init (GssPeerManagerDBusClass *klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  GParamSpec *props[PROP_CONNECTION + 1] = { NULL, };

  object_class->dispose = gss_peer_manager_dbus_dispose;
  object_class->get_property = gss_peer_manager_dbus_get_property;
  object_class->set_property = gss_peer_manager_dbus_set_property;

  /**
   * GssPeerManagerDBus:connection:
   *
   * D-Bus connection to use for retrieving peer credentials.
   *
   * Since: 0.1.0
   */
  props[PROP_CONNECTION] =
      g_param_spec_object ("connection", "Connection",
                           "D-Bus connection to use for retrieving peer credentials.",
                           G_TYPE_DBUS_CONNECTION,
                           G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

  g_object_class_install_properties (object_class, G_N_ELEMENTS (props), props);
}

static void
gss_peer_manager_dbus_peer_manager_init (GssPeerManagerInterface *iface)
{
  iface->ensure_peer_credentials_async = gss_peer_manager_dbus_ensure_peer_credentials_async;
  iface->ensure_peer_credentials_finish = gss_peer_manager_dbus_ensure_peer_credentials_finish;
  iface->get_peer_argv0 = gss_peer_manager_dbus_get_peer_argv0;
  iface->get_peer_pidfd = gss_peer_manager_dbus_get_peer_pidfd;
  iface->get_peer_uid = gss_peer_manager_dbus_get_peer_uid;
}

static void
watcher_id_free (gpointer data)
{
  g_bus_unwatch_name (GPOINTER_TO_UINT (data));
}

static void
gss_peer_manager_dbus_init (GssPeerManagerDBus *self)
{
  self->peer_watch_ids = g_ptr_array_new_with_free_func (watcher_id_free);
  self->peer_credentials = g_hash_table_new_full (g_str_hash, g_str_equal,
                                                  g_free, (GDestroyNotify) peer_credentials_free);
}

static void
gss_peer_manager_dbus_dispose (GObject *object)
{
  GssPeerManagerDBus *self = GSS_PEER_MANAGER_DBUS (object);

  g_clear_object (&self->connection);

  g_clear_pointer (&self->peer_credentials, g_hash_table_unref);
  g_clear_pointer (&self->peer_watch_ids, g_ptr_array_unref);

  /* Chain up to the parent class */
  G_OBJECT_CLASS (gss_peer_manager_dbus_parent_class)->dispose (object);
}

static void
gss_peer_manager_dbus_get_property (GObject    *object,
                                    guint       property_id,
                                    GValue     *value,
                                    GParamSpec *pspec)
{
  GssPeerManagerDBus *self = GSS_PEER_MANAGER_DBUS (object);

  switch ((GssPeerManagerDBusProperty) property_id)
    {
    case PROP_CONNECTION:
      g_value_set_object (value, self->connection);
      break;
    default:
      g_assert_not_reached ();
    }
}

static void
gss_peer_manager_dbus_set_property (GObject      *object,
                                    guint         property_id,
                                    const GValue *value,
                                    GParamSpec   *pspec)
{
  GssPeerManagerDBus *self = GSS_PEER_MANAGER_DBUS (object);

  switch ((GssPeerManagerDBusProperty) property_id)
    {
    case PROP_CONNECTION:
      /* Construct only. */
      g_assert (self->connection == NULL);
      self->connection = g_value_dup_object (value);
      break;
    default:
      g_assert_not_reached ();
    }
}

static void
peer_vanished_cb (GDBusConnection *connection,
                  const gchar     *name,
                  gpointer         user_data)
{
  GssPeerManagerDBus *self = GSS_PEER_MANAGER_DBUS (user_data);

  g_debug ("%s: Removing peer credentials for ‘%s’ from cache", G_STRFUNC, name);
  if (g_hash_table_remove (self->peer_credentials, name))
    {
      /* Notify users of this API. */
      g_signal_emit_by_name (self, "peer-vanished", name);
    }
}

/* An async function for getting credentials for D-Bus peers, either by querying
 * the bus, or by getting them from a cache. */
static void ensure_peer_credentials_cb (GObject      *obj,
                                        GAsyncResult *result,
                                        gpointer      user_data);

static void
gss_peer_manager_dbus_ensure_peer_credentials_async (GssPeerManager      *manager,
                                                     const gchar         *sender,
                                                     GCancellable        *cancellable,
                                                     GAsyncReadyCallback  callback,
                                                     gpointer             user_data)
{
  GssPeerManagerDBus *self = GSS_PEER_MANAGER_DBUS (manager);

  g_autoptr(GTask) task = g_task_new (self, cancellable, callback, user_data);
  g_task_set_source_tag (task, (gpointer) gss_peer_manager_dbus_ensure_peer_credentials_async);
  g_task_set_task_data (task, g_strdup (sender), g_free);

  /* See if the sender is in the cache already. */
  if (g_hash_table_contains (self->peer_credentials, sender))
    {
      g_debug ("%s: Found credentials in cache", G_STRFUNC);
      g_task_return_boolean (task, TRUE);
    }
  else
    {
      /* Watch the peer so we can know if/when it disappears. */
      guint watch_id = g_bus_watch_name_on_connection (self->connection, sender,
                                                       G_BUS_NAME_WATCHER_FLAGS_NONE,
                                                       NULL, peer_vanished_cb,
                                                       self, NULL);
      g_ptr_array_add (self->peer_watch_ids, GUINT_TO_POINTER (watch_id));

      /* And query for its credentials. */
      g_dbus_connection_call_with_unix_fd_list (self->connection,
                                                "org.freedesktop.DBus",
                                                "/",
                                                "org.freedesktop.DBus",
                                                "GetConnectionCredentials",
                                                g_variant_new ("(s)", sender),
                                                G_VARIANT_TYPE ("(a{sv})"),
                                                G_DBUS_CALL_FLAGS_NONE,
                                                -1  /* default timeout */,
                                                NULL  /* FD list */,
                                                cancellable,
                                                ensure_peer_credentials_cb,
                                                g_steal_pointer (&task));
    }
}

static void
ensure_peer_credentials_cb (GObject      *obj,
                            GAsyncResult *result,
                            gpointer      user_data)
{
  g_autoptr(GTask) task = G_TASK (user_data);
  GssPeerManagerDBus *self = GSS_PEER_MANAGER_DBUS (g_task_get_source_object (task));
  GDBusConnection *connection = G_DBUS_CONNECTION (obj);
  const gchar *sender = g_task_get_task_data (task);
  g_autoptr(GUnixFDList) fd_list = NULL;
  g_autoptr(GError) local_error = NULL;

  /* Finish looking up the sender. */
  g_autoptr(GVariant) retval = NULL;
  retval = g_dbus_connection_call_with_unix_fd_list_finish (connection, &fd_list,
                                                            result, &local_error);

  if (retval == NULL)
    {
      g_task_return_error (task, g_steal_pointer (&local_error));
      return;
    }

  /* From the credentials information from D-Bus, we can get the process ID,
   * and then look up the process name. Note that this is racy (the process
   * ID may get recycled between GetConnectionCredentials() returning and us
   * querying the kernel for the process name), but there’s nothing we can
   * do about that. The correct approach is to use an LSM label as returned
   * by GetConnectionCredentials(), but EOS doesn’t support any LSM.
   * We would look at /proc/$pid/exe, but that requires elevated privileges
   * (CAP_SYS_PTRACE, but I can’t get that working; so it would require root
   * privileges). Instead, we look at /proc/$pid/cmdline, which is accessible
   * by all. Unfortunately, it is also forgeable.
   *
   * Using the ProcessFD to get a pidfd for the process is race-free, so do
   * that where possible. */
  guint process_id;
  gboolean have_process_id, have_pidfd, have_uid;
  gint32 process_fd_index;
  guint32 uid;
  g_autoptr(PeerCredentials) peer_credentials = g_new0 (PeerCredentials, 1);
  g_autofree char *process_id_debug = NULL, *pidfd_debug = NULL, *uid_debug = NULL;
  g_autofree char *cmdline = NULL;
  g_autofree char *pid_str = NULL;

  g_autoptr(GVariant) credentials = g_variant_get_child_value (retval, 0);

  have_process_id = g_variant_lookup (credentials, "ProcessID", "u", &process_id);
  have_pidfd = g_variant_lookup (credentials, "ProcessFD", "h", &process_fd_index);
  have_uid = g_variant_lookup (credentials, "UnixUserID", "u", &uid);

  if (have_process_id)
    {
      pid_str = g_strdup_printf ("%u", process_id);
      g_autofree gchar *proc_pid_cmdline = g_build_filename ("/proc", pid_str, "cmdline", NULL);

      g_debug ("%s: Getting contents of ‘%s’", G_STRFUNC, proc_pid_cmdline);

      /* Assume the path is always the first nul-terminated segment.
       * If this fails to find the file in /proc, that’s probably because the
       * process is sandboxed (e.g. systemd’s PrivatePIDs=, ProtectProc= or
       * ProcSubset= options). Ignore the failure if so. */
      if (!g_file_get_contents (proc_pid_cmdline, &cmdline, NULL, &local_error) &&
          !g_error_matches (local_error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
        {
          g_task_return_new_error (task, GSS_PEER_MANAGER_ERROR,
                                   GSS_PEER_MANAGER_ERROR_IDENTIFYING_PEER,
                                   _("Executable path for peer ‘%s’ (process ID: %s) "
                                     "could not be determined: %s"),
                                   sender, pid_str, local_error->message);
          return;
        }
      else if (local_error != NULL)
        {
          g_debug ("%s: Could not open ‘%s’ — is this process sandboxed? Ignoring.",
                   G_STRFUNC, proc_pid_cmdline);
        }

      g_clear_error (&local_error);
    }

  if (have_process_id && cmdline != NULL)
    {
      /* Resolve to an absolute path, since what we get back might not be absolute. */
      g_autofree gchar *sender_path = g_find_program_in_path (cmdline);

      if (sender_path == NULL)
        {
          g_autofree gchar *message =
              g_strdup_printf (_("Path ‘%s’ could not be resolved"), cmdline);
          g_task_return_new_error (task, GSS_PEER_MANAGER_ERROR,
                                   GSS_PEER_MANAGER_ERROR_IDENTIFYING_PEER,
                                   _("Executable path for peer ‘%s’ (process ID: %s) "
                                     "could not be determined: %s"),
                                   sender, pid_str, message);
          return;
        }

      process_id_debug = g_strdup_printf ("; path is ‘%s’ (resolved from ‘%s’)",
                                          sender_path, cmdline);
      peer_credentials->argv0 = g_steal_pointer (&sender_path);
    }

  if (have_pidfd)
    {
      int pidfd = g_unix_fd_list_get (fd_list, process_fd_index, &local_error);

      if (pidfd < 0)
        {
          g_task_return_new_error (task, GSS_PEER_MANAGER_ERROR,
                                   GSS_PEER_MANAGER_ERROR_IDENTIFYING_PEER,
                                   _("Process ID for peer ‘%s’ could not be determined: %s"),
                                   sender, local_error->message);
          return;
        }

      pidfd_debug = g_strdup_printf ("; pidfd is %d", pidfd);
      peer_credentials->pidfd = g_steal_fd (&pidfd);
    }

  if (have_uid)
    {
      uid_debug = g_strdup_printf ("; UID is %u", uid);
      peer_credentials->uid = uid;
    }

  if (!have_process_id && !have_pidfd && !have_uid)
    {
      g_task_return_new_error (task, GSS_PEER_MANAGER_ERROR,
                               GSS_PEER_MANAGER_ERROR_IDENTIFYING_PEER,
                               _("Process ID for peer ‘%s’ could not be determined: %s"),
                               sender, _("D-Bus daemon did not provide enough information"));
      return;
    }

  g_debug ("%s: Got credentials from D-Bus daemon%s%s%s",
           G_STRFUNC,
           (process_id_debug != NULL) ? process_id_debug : "",
           (pidfd_debug != NULL) ? pidfd_debug : "",
           (uid_debug != NULL) ? uid_debug : "");

  g_hash_table_replace (self->peer_credentials,
                        g_strdup (sender), g_steal_pointer (&peer_credentials));

  g_task_return_boolean (task, TRUE);
}

static gboolean
gss_peer_manager_dbus_ensure_peer_credentials_finish (GssPeerManager  *manager,
                                                      GAsyncResult    *result,
                                                      GError         **error)
{
  g_return_val_if_fail (g_task_is_valid (result, manager), FALSE);
  g_return_val_if_fail (g_async_result_is_tagged (result, (gpointer) gss_peer_manager_dbus_ensure_peer_credentials_async), FALSE);

  return g_task_propagate_boolean (G_TASK (result), error);
}

static gboolean
gss_peer_manager_dbus_get_peer_credentials (GssPeerManagerDBus     *self,
                                            const char             *sender,
                                            const char             *debug_credential_name,
                                            const PeerCredentials **out_credentials)
{
  const PeerCredentials *credentials;

  g_debug ("%s: Querying %s for peer ‘%s’", G_STRFUNC, debug_credential_name, sender);
  credentials = g_hash_table_lookup (self->peer_credentials, sender);

  if (out_credentials != NULL)
    *out_credentials = credentials;
  return (credentials != NULL);
}

static const gchar *
gss_peer_manager_dbus_get_peer_argv0 (GssPeerManager *manager,
                                      const gchar    *sender)
{
  GssPeerManagerDBus *self = GSS_PEER_MANAGER_DBUS (manager);
  const PeerCredentials *credentials = NULL;

  return gss_peer_manager_dbus_get_peer_credentials (self, sender, "argv0", &credentials) ? credentials->argv0 : NULL;
}

static int
gss_peer_manager_dbus_get_peer_pidfd (GssPeerManager *manager,
                                      const gchar    *sender)
{
  GssPeerManagerDBus *self = GSS_PEER_MANAGER_DBUS (manager);
  const PeerCredentials *credentials = NULL;

  return gss_peer_manager_dbus_get_peer_credentials (self, sender, "pidfd", &credentials) ? credentials->pidfd : -1;
}

static uid_t
gss_peer_manager_dbus_get_peer_uid (GssPeerManager *manager,
                                    const gchar    *sender)
{
  GssPeerManagerDBus *self = GSS_PEER_MANAGER_DBUS (manager);
  const PeerCredentials *credentials = NULL;

  return gss_peer_manager_dbus_get_peer_credentials (self, sender, "UID", &credentials) ? credentials->uid : (uid_t) -1;
}

/**
 * gss_peer_manager_dbus_new:
 * @connection: a #GDBusConnection
 *
 * Create a #GssPeerManagerDBus object to wrap the given existing @connection.
 *
 * Returns: (transfer full): a new #GssPeerManagerDBus wrapping @connection
 * Since: 0.1.0
 */
GssPeerManagerDBus *
gss_peer_manager_dbus_new (GDBusConnection *connection)
{
  g_return_val_if_fail (G_IS_DBUS_CONNECTION (connection), NULL);

  return g_object_new (GSS_TYPE_PEER_MANAGER_DBUS,
                       "connection", connection,
                       NULL);
}
