/*
 * This file is part of Cockpit.
 *
 * Copyright (C) 2013 Red Hat, Inc.
 *
 * Cockpit 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.
 *
 * Cockpit 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 Cockpit; If not, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include "cockpitdbusjson.h"

#include "cockpitpipechannel.h"
#include "cockpitdbuscache.h"
#include "cockpitdbusinternal.h"
#include "cockpitdbusmeta.h"
#include "cockpitdbusrules.h"

#include "common/cockpitchannel.h"
#include "common/cockpitjson.h"

#include <json-glib/json-glib.h>

#include <gio/gunixfdlist.h>

#include <string.h>

/**
 * CockpitDBusJson:
 *
 * A #CockpitChannel that sends DBus messages with the dbus-json payload
 * type.
 */

gboolean cockpit_dbus_json_allow_external = TRUE;

/* The maximum number of DBus passed fds open without a channel */
#define MAX_RECEIVED_DBUS_FDS 16

#define COCKPIT_DBUS_JSON(o)    (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_DBUS_JSON, CockpitDBusJson))

typedef struct {
  CockpitChannel parent;
  GDBusConnection *connection;
  GBusType bus_type;

  /* Talking to */
  const gchar *logname;
  const gchar *default_name;
  guint default_watch;
  gboolean default_watched;
  gboolean default_appeared;

  /* Call related */
  GCancellable *cancellable;
  GList *active_calls;
  GHashTable *interface_info;

  /* Per name information */
  GHashTable *peers;

  /* Invocations and publishing */
  GHashTable *invocations;
  GHashTable *registered;
  guint last_invocation;

  /* File descriptors */
  GQueue *fd_channel_ids;
} CockpitDBusJson;

typedef struct {
  gchar *name;
  CockpitDBusJson *dbus_json;

  guint subscribe_id;
  gboolean subscribed;

  /* Signal related */
  CockpitDBusRules *rules;

  /* Watch and introspection */
  CockpitDBusCache *cache;
  gulong meta_sig;
  gulong update_sig;
} CockpitDBusPeer;

typedef struct {
  CockpitChannelClass parent_class;
} CockpitDBusJsonClass;

G_DEFINE_TYPE (CockpitDBusJson, cockpit_dbus_json, COCKPIT_TYPE_CHANNEL);

#if !GLIB_CHECK_VERSION(2,46,0)
#define G_DBUS_MESSAGE_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION (1<<2)
#endif

/*
 * Shared D-Bus connections.
 *
 * @group_connections maps "group:address" strings to GDBusConnection
 * objects.
 *
 * @pending_group_connections maps "group:address" strings to GPtrArrays
 * with CockpitDBusJson objects that are waiting for this connection to
 * be established.
 */
static GHashTable *group_connections;
static GHashTable *pending_group_connections;

static const gchar *
value_type_name (JsonNode *node)
{
  GType type = json_node_get_value_type (node);
  if (type == G_TYPE_STRING)
    return "string";
  else if (type == G_TYPE_INT64)
    return "int";
  else if (type == G_TYPE_DOUBLE)
    return "double";
  else
    return g_type_name (type);
}

static gboolean
check_type (JsonNode *node,
            JsonNodeType type,
            GType sub_type,
            GError **error)
{
  if (JSON_NODE_TYPE (node) != type ||
      (type == JSON_NODE_VALUE && (json_node_get_value_type (node) != sub_type)))
    {
      g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                   "Unexpected type '%s' in argument", value_type_name (node));
      return FALSE;
    }
  return TRUE;
}

static gboolean
check_int_type (JsonNode *node,
                const GVariantType *type,
                gint64 min,
                gint64 max,
                GError **error)
{
  gint64 value;

  if (!check_type(node, JSON_NODE_VALUE, G_TYPE_INT64, error))
    return FALSE;

  value = json_node_get_int (node);
  if (value < min || value > max)
    {
      g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                  "Number '%" G_GINT64_FORMAT "' is not in range for the expected type '%.*s'", value,
                  (int) g_variant_type_get_string_length (type),
                  g_variant_type_peek_string (type));
      return FALSE;
    }

  return TRUE;
}

static GVariant *
parse_json (JsonNode *node,
            const GVariantType *type,
            GError **error);

static GVariant *
parse_json_tuple (JsonNode *node,
                  const GVariantType *child_type,
                  GError **error)
{
  GVariant *result = NULL;
  GPtrArray *children;
  GVariant *value;
  JsonArray *array;
  guint length;
  guint i;

  children = g_ptr_array_new ();

  if (!check_type (node, JSON_NODE_ARRAY, 0, error))
    goto out;

  array = json_node_get_array (node);
  length = json_array_get_length (array);

  for (i = 0; i < length; i++)
    {
      value = NULL;
      if (child_type == NULL)
        {
          g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                       "Too many values in tuple/struct");
        }
      else
        {
          value = parse_json (json_array_get_element (array, i),
                              child_type, error);
        }

      if (!value)
        goto out;

      g_ptr_array_add (children, value);
      child_type = g_variant_type_next (child_type);
    }

  if (child_type)
    {
      g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                   "Too few values in tuple/struct");
      goto out;
    }

  result = g_variant_new_tuple ((GVariant *const *)children->pdata,
                                children->len);
  children->len = 0;

out:
  g_ptr_array_foreach (children, (GFunc)g_variant_unref, NULL);
  g_ptr_array_free (children, TRUE);
  return result;
}

static GVariant *
parse_json_byte_array (JsonNode *node,
                       GError **error)
{
  static const char valid[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  GVariant *result = NULL;
  const gchar *value;
  gpointer data = NULL;
  gsize length;
  gsize pos;

  if (!check_type (node, JSON_NODE_VALUE, G_TYPE_STRING, error))
    goto out;

  value = json_node_get_string (node);

  pos = strspn (value, valid);
  while (value[pos] == '=')
    pos++;

  if (pos == 0)
    {
      data = NULL;
      length = 0;
    }
  else
    {
      /* base64 strings are always multiple of 3 */
      if (pos % 3 == 0 || value[pos] == '\0')
        data = g_base64_decode (value, &length);

      if (!data)
        {
          g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                       "Invalid base64 in argument");
          goto out;
        }
    }

  result = g_variant_new_from_data (G_VARIANT_TYPE_BYTESTRING,
                                    data, length, TRUE,
                                    g_free, data);

out:
  return result;
}

static GVariant *
parse_json_array (JsonNode *node,
                  const GVariantType *child_type,
                  GError **error)
{
  GVariant *result = NULL;
  GPtrArray *children;
  GVariant *child;
  JsonArray *array;
  guint length;
  guint i;

  children = g_ptr_array_new ();

  if (!check_type (node, JSON_NODE_ARRAY, 0, error))
    goto out;

  array = json_node_get_array (node);
  length = json_array_get_length (array);

  for (i = 0; i < length; i++)
    {
      child = parse_json (json_array_get_element (array, i),
                          child_type, error);
      if (!child)
        goto out;

      g_ptr_array_add (children, child);
    }

  result = g_variant_new_array (child_type,
                                (GVariant *const *)children->pdata,
                                children->len);
  children->len = 0;

out:
  g_ptr_array_foreach (children, (GFunc)g_variant_unref, NULL);
  g_ptr_array_free (children, TRUE);
  return result;
}

static GVariant *
parse_json_variant (JsonNode *node,
                    GError **error)
{
  GVariantType *inner_type;
  JsonObject *object;
  GVariant *inner;
  JsonNode *val;
  const gchar *sig;

  if (!check_type (node, JSON_NODE_OBJECT, 0, error))
    return NULL;

  object = json_node_get_object (node);
  val = json_object_get_member (object, "v");
  if (val == NULL)
    {
      g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                   "Variant object did not contain a 'v' field");
      return NULL;
    }
  if (!cockpit_json_get_string (object, "t", NULL, &sig) || !sig)
    {
      g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                   "Variant object did not contain valid 't' field");
      return NULL;
    }
  if (!g_variant_type_string_is_valid (sig))
    {
      g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                   "Variant 't' field '%s' is invalid", sig);
      return NULL;
    }

  inner_type = g_variant_type_new (sig);
  inner = parse_json (val, inner_type, error);
  g_variant_type_free (inner_type);

  if (!inner)
    return NULL;

  return g_variant_new_variant (inner);
}

static GVariant *
parse_json_dictionary (JsonNode *node,
                       const GVariantType *entry_type,
                       GError **error)
{
  const GVariantType *key_type;
  const GVariantType *value_type;
  GVariant *result = NULL;
  GPtrArray *children;
  JsonObject *object;
  JsonNode *key_node;
  GList *members = NULL;
  gboolean is_string;
  GVariant *value;
  GVariant *key;
  GVariant *child;
  GList *l;

  children = g_ptr_array_new ();

  if (!check_type (node, JSON_NODE_OBJECT, 0, error))
    goto out;

  object = json_node_get_object (node);
  key_type = g_variant_type_key (entry_type);
  value_type = g_variant_type_value (entry_type);

  is_string = (g_variant_type_equal (key_type, G_VARIANT_TYPE_STRING) ||
               g_variant_type_equal (key_type, G_VARIANT_TYPE_OBJECT_PATH) ||
               g_variant_type_equal (key_type, G_VARIANT_TYPE_SIGNATURE));

  members = json_object_get_members (object);
  for (l = members; l != NULL; l = g_list_next (l))
    {
      if (is_string)
        {
          key_node = json_node_new (JSON_NODE_VALUE);
          json_node_set_string (key_node, l->data);
        }
      else
        {
          key_node = cockpit_json_parse (l->data, -1, NULL);
          if (key_node == NULL)
            {
              g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                           "Unexpected key '%s' in dict entry", (gchar *)l->data);
              goto out;
            }
        }

      key = parse_json (key_node, key_type, error);
      json_node_free (key_node);

      if (!key)
        goto out;

      value = parse_json (json_object_get_member (object, l->data),
                          value_type, error);
      if (!value)
        {
          g_variant_unref (key);
          goto out;
        }

      child = g_variant_new_dict_entry (key, value);
      g_ptr_array_add (children, child);
    }

  result = g_variant_new_array (entry_type,
                                (GVariant *const *)children->pdata,
                                children->len);
  children->len = 0;

out:
  g_list_free (members);
  g_ptr_array_foreach (children, (GFunc)g_variant_unref, NULL);
  g_ptr_array_free (children, TRUE);
  return result;
}

static GVariant *
parse_json_object_path (JsonNode *node,
                        GError **error)
{
  GVariant *result = NULL;
  const gchar *str;

  if (!check_type (node, JSON_NODE_VALUE, G_TYPE_STRING, error))
    return NULL;

  str = json_node_get_string (node);
  if (g_variant_is_object_path (str))
    {
      result = g_variant_new_object_path (str);
    }
  else
    {
      g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                   "Invalid object path '%s'", str);
    }

  return result;
}

static GVariant *
parse_json_signature (JsonNode *node,
                      GError **error)
{
  const gchar *str;

  if (!check_type (node, JSON_NODE_VALUE, G_TYPE_STRING, error))
    return NULL;

  str = json_node_get_string (node);
  if (g_variant_is_signature (str))
    return g_variant_new_signature (str);
  else
    {
      g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                   "Invalid signature '%s'", str);
      return NULL;
    }
}

static void
parse_not_supported (const GVariantType *type,
                     GError **error)
{
  g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
               "Type '%.*s' is unknown or not supported",
               (int)g_variant_type_get_string_length (type),
               g_variant_type_peek_string (type));
}

static GVariant *
parse_json (JsonNode *node,
            const GVariantType *type,
            GError **error)
{
  const GVariantType *element_type;

  if (!g_variant_type_is_definite (type))
    {
      g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                   "Indefinite type '%.*s' is not supported",
                   (int)g_variant_type_get_string_length (type),
                   g_variant_type_peek_string (type));
      return NULL;
    }

  if (g_variant_type_is_basic (type))
    {
      if (g_variant_type_equal (type, G_VARIANT_TYPE_BOOLEAN))
        {
          if (check_type (node, JSON_NODE_VALUE, G_TYPE_BOOLEAN, error))
            return g_variant_new_boolean (json_node_get_boolean (node));
        }
      else if (g_variant_type_equal (type, G_VARIANT_TYPE_BYTE))
        {
          if (check_int_type (node, type, 0, G_MAXUINT8, error))
            return g_variant_new_byte (json_node_get_int (node));
        }
      else if (g_variant_type_equal (type, G_VARIANT_TYPE_INT16))
        {
          if (check_int_type (node, type, G_MININT16, G_MAXINT16, error))
            return g_variant_new_int16 (json_node_get_int (node));
        }
      else if (g_variant_type_equal (type, G_VARIANT_TYPE_UINT16))
        {
          if (check_int_type (node, type, 0, G_MAXUINT16, error))
            return g_variant_new_uint16 (json_node_get_int (node));
        }
      else if (g_variant_type_equal (type, G_VARIANT_TYPE_INT32))
        {
          if (check_int_type (node, type, G_MININT32, G_MAXINT32, error))
            return g_variant_new_int32 (json_node_get_int (node));
        }
      else if (g_variant_type_equal (type, G_VARIANT_TYPE_UINT32))
        {
          if (check_int_type (node, type, 0, G_MAXUINT32, error))
            return g_variant_new_uint32 (json_node_get_int (node));
        }
      else if (g_variant_type_equal (type, G_VARIANT_TYPE_INT64))
        {
          if (check_int_type (node, type, G_MININT64, G_MAXINT64, error))
            return g_variant_new_int64 (json_node_get_int (node));
        }
      else if (g_variant_type_equal (type, G_VARIANT_TYPE_UINT64))
        {
          /* Can never be larger than signed int64, because json-glib
           * only returns doubles or signed 64-bit integers when
           * encountering a JSON number. */
          if (check_int_type (node, type, 0, G_MAXINT64, error))
            return g_variant_new_uint64 (json_node_get_int (node));
        }
      else if (g_variant_type_equal (type, G_VARIANT_TYPE_DOUBLE))
        {
          if (check_type (node, JSON_NODE_VALUE, G_TYPE_INT64, NULL))
            return g_variant_new_double (json_node_get_int (node));
          else if (check_type (node, JSON_NODE_VALUE, G_TYPE_DOUBLE, error))
            return g_variant_new_double (json_node_get_double (node));
        }
      else if (g_variant_type_equal (type, G_VARIANT_TYPE_STRING))
        {
          if (check_type (node, JSON_NODE_VALUE, G_TYPE_STRING, error))
            return g_variant_new_string (json_node_get_string (node));
        }
      else if (g_variant_type_equal (type, G_VARIANT_TYPE_OBJECT_PATH))
        {
          return parse_json_object_path (node, error);
        }
      else if (g_variant_type_equal (type, G_VARIANT_TYPE_SIGNATURE))
        {
          return parse_json_signature (node, error);
        }
      else
        {
          parse_not_supported (type, error);
        }
    }
  else if (g_variant_type_is_variant (type))
    {
      return parse_json_variant (node, error);
    }
  else if (g_variant_type_is_array (type))
    {
      element_type = g_variant_type_element (type);
      if (g_variant_type_equal (element_type, G_VARIANT_TYPE_BYTE))
        return parse_json_byte_array (node, error);
      else if (g_variant_type_is_dict_entry (element_type))
        return parse_json_dictionary (node, element_type, error);
      else
        return parse_json_array (node, element_type, error);
    }
  else if (g_variant_type_is_tuple (type))
    {
      return parse_json_tuple (node, g_variant_type_first (type), error);
    }
  else
    {
      parse_not_supported (type, error);
    }

  return NULL;
}

typedef struct {
  GUnixFDList *fdlist;
  GQueue *fdids;
} VariantContext;

static JsonNode *
build_json (GVariant *value,
            VariantContext *context);

static JsonObject *
build_json_variant (GVariant *value,
                    VariantContext *context)
{
  GVariant *child;
  JsonObject *object;

  child = g_variant_get_variant (value);
  object = json_object_new ();
  json_object_set_string_member (object, "t", g_variant_get_type_string (child));
  json_object_set_member (object, "v", build_json (child, context));

  g_variant_unref (child);

  return object;
}

static JsonNode *
build_json_byte_array (GVariant *value)
{
  JsonNode *node;
  gconstpointer data;
  gsize length = 0;
  gchar *string;

  data = g_variant_get_fixed_array (value, &length, 1);
  if (length > 0)
    string = g_base64_encode (data, length);
  else
    string = NULL;
  node = json_node_new (JSON_NODE_VALUE);
  json_node_set_string (node, string ? string : "");
  g_free (string); /* unfortunately :S */

  return node;
}

static JsonArray *
build_json_array_or_tuple (GVariant *value,
                           VariantContext *context)
{
  GVariantIter iter;
  GVariant *child;
  JsonArray *array;

  array = json_array_new ();

  g_variant_iter_init (&iter, value);
  while ((child = g_variant_iter_next_value (&iter)) != NULL)
    {
      json_array_add_element (array, build_json (child, context));
      g_variant_unref (child);
    }

  return array;
}

static JsonObject *
build_json_dictionary (const GVariantType *entry_type,
                       GVariant *dict,
                       VariantContext *context)
{
  const GVariantType *key_type;
  GVariantIter iter;
  GVariant *child;
  GVariant *key;
  GVariant *value;
  gboolean is_string;
  gchar *key_string;
  JsonObject *object;

  object = json_object_new ();
  key_type = g_variant_type_key (entry_type);

  is_string = (g_variant_type_equal (key_type, G_VARIANT_TYPE_STRING) ||
               g_variant_type_equal (key_type, G_VARIANT_TYPE_OBJECT_PATH) ||
               g_variant_type_equal (key_type, G_VARIANT_TYPE_SIGNATURE));

  g_variant_iter_init (&iter, dict);
  while ((child = g_variant_iter_next_value (&iter)) != NULL)
    {
      key = g_variant_get_child_value (child, 0);
      value = g_variant_get_child_value (child, 1);

      if (is_string)
        {
          json_object_set_member (object, g_variant_get_string (key, NULL), build_json (value, context));
        }
      else
        {
          key_string = g_variant_print (key, FALSE);
          json_object_set_member (object, key_string, build_json (value, context));
          g_free (key_string);
        }

      g_variant_unref (key);
      g_variant_unref (value);
    }

  return object;
}

static JsonNode *
build_json_fd_channel (GVariant *value,
                       VariantContext *context)
{
  JsonObject *object;
  GError *error = NULL;
  JsonNode *node;
  gint fd = -1;
  gchar *old;
  const gchar *id;

  if (context->fdlist)
    {
      fd = g_unix_fd_list_get (context->fdlist, g_variant_get_handle (value), &error);
      if (fd == -1)
        {
          g_warning ("couldn't dup file descriptor from DBus message: %s", error->message);
          g_clear_error (&error);
        }
    }

  if (fd < 0)
    {
      node = json_node_new (JSON_NODE_NULL);
    }
  else
    {
      g_assert (context->fdlist != NULL);
      g_assert (context->fdids != NULL);

      node = json_node_new (JSON_NODE_OBJECT);

      /* Add a new internal channel name for this file descriptor */
      id = cockpit_pipe_channel_add_internal_fd (fd);
      g_queue_push_tail (context->fdids, (gpointer) g_strdup (id));

      /* And only keep the last N ready for opening channels */
      while (g_queue_get_length (context->fdids) > MAX_RECEIVED_DBUS_FDS)
        {
          old = (gchar *)g_queue_pop_head (context->fdids);
          cockpit_pipe_channel_remove_internal_fd (old);

          g_free (old);
        }

      /* This is sent back as the list of channel options to use */
      object = json_object_new ();
      json_object_set_string_member (object, "payload", "stream");
      json_object_set_string_member (object, "internal", id);
      json_node_set_object (node, object);
      json_object_unref (object);
    }

  return node;
}

static JsonNode *
build_json (GVariant *value,
            VariantContext *context)
{
  const GVariantType *type;
  const GVariantType *element_type;
  JsonObject *object;
  JsonArray *array;
  JsonNode *node;

  switch (g_variant_classify (value))
    {
    case G_VARIANT_CLASS_BOOLEAN:
      node = json_node_new (JSON_NODE_VALUE);
      json_node_set_boolean (node, g_variant_get_boolean (value));
      return node;

    case G_VARIANT_CLASS_BYTE:
      node = json_node_new (JSON_NODE_VALUE);
      json_node_set_int (node, g_variant_get_byte (value));
      return node;

    case G_VARIANT_CLASS_INT16:
      node = json_node_new (JSON_NODE_VALUE);
      json_node_set_int (node, g_variant_get_int16 (value));
      return node;

    case G_VARIANT_CLASS_UINT16:
      node = json_node_new (JSON_NODE_VALUE);
      json_node_set_int (node, g_variant_get_uint16 (value));
      return node;

    case G_VARIANT_CLASS_INT32:
      node = json_node_new (JSON_NODE_VALUE);
      json_node_set_int (node, g_variant_get_int32 (value));
      return node;

    case G_VARIANT_CLASS_UINT32:
      node = json_node_new (JSON_NODE_VALUE);
      json_node_set_int (node, g_variant_get_uint32 (value));
      return node;

    case G_VARIANT_CLASS_INT64:
      node = json_node_new (JSON_NODE_VALUE);
      json_node_set_int (node, g_variant_get_int64 (value));
      return node;

    case G_VARIANT_CLASS_UINT64:
      node = json_node_new (JSON_NODE_VALUE);
      json_node_set_int (node, g_variant_get_uint64 (value));
      return node;

    case G_VARIANT_CLASS_HANDLE:
      node = build_json_fd_channel (value, context);
      return node;

    case G_VARIANT_CLASS_DOUBLE:
      node = json_node_new (JSON_NODE_VALUE);
      json_node_set_double (node, g_variant_get_double (value));
      return node;

    case G_VARIANT_CLASS_STRING:      /* explicit fall-through */
    case G_VARIANT_CLASS_OBJECT_PATH: /* explicit fall-through */
    case G_VARIANT_CLASS_SIGNATURE:
      node = json_node_new (JSON_NODE_VALUE);
      json_node_set_string (node, g_variant_get_string (value, NULL));
      return node;

    case G_VARIANT_CLASS_VARIANT:
      object = build_json_variant (value, context);
      node = json_node_new (JSON_NODE_OBJECT);
      json_node_take_object (node, object);
      return node;

    case G_VARIANT_CLASS_ARRAY:
      type = g_variant_get_type (value);
      element_type = g_variant_type_element (type);
      if (g_variant_type_is_dict_entry (element_type))
        {
          object = build_json_dictionary (element_type, value, context);
          node = json_node_new (JSON_NODE_OBJECT);
          json_node_take_object (node, object);
        }
      else if (g_variant_type_equal (element_type, G_VARIANT_TYPE_BYTE))
        {
          node = build_json_byte_array (value);
        }
      else
        {
          array = build_json_array_or_tuple (value, context);
          node = json_node_new (JSON_NODE_ARRAY);
          json_node_set_array (node, array);
          json_array_unref (array);
        }
      return node;

    case G_VARIANT_CLASS_TUPLE:
      array = build_json_array_or_tuple (value, context);
      node = json_node_new (JSON_NODE_ARRAY);
      json_node_set_array (node, array);
      json_array_unref (array);
      return node;

    case G_VARIANT_CLASS_DICT_ENTRY:
    case G_VARIANT_CLASS_MAYBE:
    default:
      g_return_val_if_reached (NULL);
      break;
    }
}

static void
send_json_object (CockpitDBusJson *self,
                  JsonObject *object)
{
  GBytes *bytes;

  bytes = cockpit_json_write_bytes (object);
  cockpit_channel_send (COCKPIT_CHANNEL (self), bytes, TRUE);
  g_bytes_unref (bytes);
}

static JsonObject *
build_json_error (GError *error)
{
  JsonObject *object;
  JsonArray *reply;
  JsonArray *args;
  gchar *error_name;

  object = json_object_new ();
  reply = json_array_new ();
  args = json_array_new ();

  error_name = g_dbus_error_get_remote_error (error);
  g_dbus_error_strip_remote_error (error);

  json_array_add_string_element (reply, error_name != NULL ? error_name : "");
  g_free (error_name);

  if (error->message)
    json_array_add_string_element (args, error->message);
  json_array_add_array_element (reply, args);

  json_object_set_array_member (object, "error", reply);

  return object;
}

static gchar *
build_signature (GVariant *variant)
{
  const GVariantType *type;
  GString *sig;

  sig = g_string_new ("");
  type = g_variant_get_type (variant);
  for (type = g_variant_type_first (type);
       type != NULL;
       type = g_variant_type_next (type))
    {
      g_string_append_len (sig, g_variant_type_peek_string (type),
                           g_variant_type_get_string_length (type));
    }
  return g_string_free (sig, FALSE);
}

static JsonNode *
build_json_body (GVariant *body,
                 VariantContext *context,
                 gchar **type)
{
  if (body)
    {
      if (type)
        *type = build_signature (body);
      return build_json (body, context);
    }
  else
    {
      if (type)
        *type = NULL;
      return json_node_new (JSON_NODE_NULL);
    }
}

static JsonObject *
build_json_signal (const gchar *path,
                   const gchar *interface,
                   const gchar *member,
                   GVariant *body)
{
  JsonObject *object;
  JsonArray *signal;
  VariantContext context = { NULL };

  object = json_object_new ();
  signal = json_array_new ();
  json_array_add_string_element (signal, path);
  json_array_add_string_element (signal, interface);
  json_array_add_string_element (signal, member);
  json_array_add_element (signal, build_json_body (body, &context, NULL));
  json_object_set_array_member (object, "signal", signal);

  return object;
}

/* ---------------------------------------------------------------------------------------------------- */

typedef struct {
  /* Cleared by dispose */
  GList *link;
  CockpitDBusJson *dbus_json;

  /* Request data */
  JsonObject *request;

  /* Owned here */
  GVariantType *param_type;

  /* Owned by request */
  const gchar *cookie;
  const gchar *name;
  const gchar *interface;
  const gchar *method;
  const gchar *path;
  const gchar *type;
  const gchar *flags;
  gint timeout;
  JsonNode *args;
} CallData;

static CockpitDBusPeer *
ensure_peer (CockpitDBusJson *self,
             const gchar *name);

static void
send_dbus_error (CockpitDBusJson *self,
                 CallData *call,
                 GError *error)
{
  JsonObject *object;

  if (!call->cookie)
    {
      g_debug ("%s: dropping error without cookie: %s", self->logname, error->message);
      return;
    }

  g_debug ("%s: failed %s", self->logname, call->method);

  object = build_json_error (error);
  json_object_set_string_member (object, "id", call->cookie);
  send_json_object (self, object);
  json_object_unref (object);
}

typedef struct {
  CockpitDBusJson *dbus_json;
  JsonObject *message;
} WaitData;

static void
on_wait_complete (CockpitDBusCache *cache,
                  gpointer user_data)
{
  WaitData *wd = user_data;
  CockpitDBusJson *self = wd->dbus_json;

  if (!g_cancellable_is_cancelled (self->cancellable))
    send_json_object (self, wd->message);

  g_object_unref (wd->dbus_json);
  json_object_unref (wd->message);
  g_slice_free (WaitData, wd);
}

static void
send_with_barrier (CockpitDBusJson *self,
                   CockpitDBusPeer *peer,
                   JsonObject *message)
{
  WaitData *wd = g_slice_new (WaitData);
  wd->dbus_json = g_object_ref (self);
  wd->message = json_object_ref (message);
  cockpit_dbus_cache_barrier (peer->cache, on_wait_complete, wd);
}

static void
send_dbus_reply (CockpitDBusJson *self,
                 CallData *call,
                 GDBusMessage *message)
{
  CockpitDBusPeer *peer;
  VariantContext context = { NULL };
  GVariant *scrape = NULL;
  JsonObject *object;
  GString *flags;

  g_return_if_fail (call->cookie != NULL);

  JsonArray *reply;
  gchar *type = NULL;

  object = json_object_new ();
  reply = json_array_new ();
  if (g_dbus_message_get_message_type (message) == G_DBUS_MESSAGE_TYPE_ERROR)
    {
      g_debug ("%s: errorc for %s", self->logname, call->method);
      json_array_add_string_element (reply, g_dbus_message_get_error_name (message));
      json_object_set_array_member (object, "error", reply);
    }
  else
    {
      g_debug ("%s: reply for %s", self->logname, call->method);
      json_object_set_array_member (object, "reply", reply);
      scrape = g_dbus_message_get_body (message);
    }

  context.fdlist = g_dbus_message_get_unix_fd_list (message);
  if (context.fdlist)
    {
      if (!self->fd_channel_ids)
        self->fd_channel_ids = g_queue_new ();
      context.fdids = self->fd_channel_ids;
    }

  json_array_add_element (reply, build_json_body (g_dbus_message_get_body (message), &context,
                                                  call->type != NULL ? &type : NULL));

  if (type)
    {
      json_object_set_string_member (object, "type", type);
      g_free (type);
    }

  json_object_set_string_member (object, "id", call->cookie);

  if (call->flags)
    {
      flags = g_string_new ("");
      if (g_dbus_message_get_byte_order (message) == G_DBUS_MESSAGE_BYTE_ORDER_BIG_ENDIAN)
        g_string_append_c (flags, '>');
      else
        g_string_append_c (flags, '<');
      json_object_set_string_member (object, "flags", flags->str);
      g_string_free (flags, TRUE);
    }

  peer = ensure_peer (self, call->name);
  cockpit_dbus_cache_poke (peer->cache, call->path, call->interface);
  if (scrape)
    cockpit_dbus_cache_scrape (peer->cache, scrape);
  send_with_barrier (self, peer, object);

  json_object_unref (object);
}

static GVariantType *
calculate_method_param_type (GDBusInterfaceInfo *info,
                             const gchar *iface,
                             const gchar *method,
                             GError **error)
{
  const GVariantType *arg_types[256];
  GDBusMethodInfo *method_info = NULL;
  guint n;

  if (info)
    method_info = g_dbus_interface_info_lookup_method (info, method);
  if (method_info == NULL)
    {
      g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD,
                   "Introspection data for method %s %s not available", iface, method);
      return NULL;
    }
  else if (method_info->in_args)
    {
      for (n = 0; method_info->in_args[n] != NULL; n++)
        {
          /* DBus places a hard limit of 255 on signature length.
           * therefore number of args must be less than 256.
           */
          if (n >= G_N_ELEMENTS (arg_types))
            return NULL;

          arg_types[n] = G_VARIANT_TYPE (method_info->in_args[n]->signature);

          if G_UNLIKELY (arg_types[n] == NULL)
            return NULL;
        }
    }
  else
    {
      n = 0;
    }
  return g_variant_type_new_tuple (arg_types, n);
}

static void
call_data_free (CallData *call)
{
  if (call->dbus_json)
    call->dbus_json->active_calls = g_list_delete_link (call->dbus_json->active_calls, call->link);
  if (call->request)
    json_object_unref (call->request);
  if (call->param_type)
    g_variant_type_free (call->param_type);
  g_slice_free (CallData, call);
}

static void
on_send_message_reply (GObject *source,
                       GAsyncResult *result,
                       gpointer user_data)
{
  CallData *call = user_data;
  GError *error = NULL;
  GDBusMessage *message;

  message = g_dbus_connection_send_message_with_reply_finish (G_DBUS_CONNECTION (source),
                                                              result, &error);
  if (call->dbus_json)
    {
      if (error)
        {
          if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT))
            {
              g_clear_error (&error);
              g_set_error (&error, G_DBUS_ERROR, G_DBUS_ERROR_TIMEOUT,
                           "method call %s timed out", call->method);
            }

            send_dbus_error (call->dbus_json, call, error);
        }
      else
        {
          send_dbus_reply (call->dbus_json, call, message);
        }
    }

  g_clear_error (&error);
  g_clear_object (&message);
  call_data_free (call);
}


static void
handle_dbus_call_on_interface (CockpitDBusJson *self,
                               CallData *call)
{
  GVariant *parameters = NULL;
  GError *error = NULL;
  GDBusMessage *message = NULL;

  g_return_if_fail (call->param_type != NULL);
  parameters = parse_json (call->args, call->param_type, &error);

  if (!parameters)
    goto out;

  g_debug ("%s: invoking %s %s at %s", self->logname, call->interface, call->method, call->path);

  message = g_dbus_message_new_method_call (call->name,
                                            call->path,
                                            call->interface,
                                            call->method);

  /* When no flags or interactive flags not set */
  if (!call->flags || strchr (call->flags, 'i'))
    g_dbus_message_set_flags (message, G_DBUS_MESSAGE_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION);

  g_dbus_message_set_body (message, parameters);
  parameters = NULL;

  g_dbus_connection_send_message_with_reply (self->connection,
                                             message,
                                             G_DBUS_SEND_MESSAGE_FLAGS_NONE,
                                             call->timeout,
                                             NULL, /* serial */
                                             self->cancellable,
                                             call->cookie ? on_send_message_reply : NULL,
                                             call->cookie ? call : NULL);
  if (call->cookie)
    call = NULL; /* ownership assumed above */

out:
  if (error)
    {
      if (call)
        send_dbus_error (self, call, error);
      g_error_free (error);
    }
  if (call)
    call_data_free (call);
  if (message)
    g_object_unref (message);
}

static void
on_introspect_ready (CockpitDBusCache *cache,
                     GDBusInterfaceInfo *iface,
                     gpointer user_data)
{
  CallData *call = user_data;
  CockpitDBusJson *self = call->dbus_json;
  GError *error = NULL;

  /* Cancelled? */
  if (!call->dbus_json)
    {
      call_data_free (call);
      return;
    }

  call->param_type = calculate_method_param_type (iface, call->interface,
                                                  call->method, &error);

  if (error)
    {
      send_dbus_error (self, call, error);
      g_error_free (error);
      call_data_free (call);
    }
  else
    {
      handle_dbus_call_on_interface (self, call);
    }
}

static const gchar *
array_string_element (JsonArray *array,
                      guint i)
{
  JsonNode *node;

  node = json_array_get_element (array, i);
  if (node && JSON_NODE_HOLDS_VALUE (node) && json_node_get_value_type (node) == G_TYPE_STRING)
    return json_node_get_string (node);
  return NULL;
}

static gboolean
parse_json_method (CockpitDBusJson *self,
                   JsonNode *node,
                   const gchar *description,
                   const gchar **path,
                   const gchar **interface,
                   const gchar **method,
                   JsonNode **args)
{
  JsonArray *array;

  g_assert (description != NULL);
  g_assert (path != NULL);
  g_assert (interface != NULL);
  g_assert (method != NULL);
  g_assert (args != NULL);

  if (!JSON_NODE_HOLDS_ARRAY (node))
    {
      cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
                            "incorrect '%s' field in dbus command", description);
      return FALSE;
    }

  array = json_node_get_array (node);
  *path = array_string_element (array, 0);
  *interface = array_string_element (array, 1);
  *method = array_string_element (array, 2);

  *args = json_array_get_element (array, 3);
  if (!*args || !JSON_NODE_HOLDS_ARRAY (*args))
    {
      cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
                            "arguments field is invalid in dbus \"%s\"", description);
    }
  else if (!*path || !g_variant_is_object_path (*path))
    {
      cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
                            "object path is invalid in dbus \"%s\": %s", description, *path);
    }
  else if (!*interface || !g_dbus_is_interface_name (*interface))
    {
      cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
                            "interface name is invalid in dbus \"%s\": %s", description, *interface);
    }
  else if (!*method || !g_dbus_is_member_name (*method))
    {
      cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
                            "member name is invalid in dbus \"%s\": %s", description, *method);
    }
  else
    {
      return TRUE;
    }

  return FALSE;
}

static void
handle_dbus_call (CockpitDBusJson *self,
                  JsonObject *object)
{
  CockpitDBusPeer *peer = NULL;
  CallData *call;
  JsonNode *node;
  gchar *string;
  gint64 timeout;

  node = json_object_get_member (object, "call");
  g_return_if_fail (node != NULL);
  call = g_slice_new0 (CallData);

  if (!parse_json_method (self, node, "call", &call->path, &call->interface, &call->method, &call->args))
    {
      /* fall through to call invalid */
    }
  else if (!cockpit_json_get_string (object, "name", self->default_name, &call->name))
    {
      cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
                            "the \"name\" field is invalid in dbus call");
    }
  else if (self->bus_type != G_BUS_TYPE_NONE && call->name == NULL)
    {
      cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
                            "the \"name\" field is missing in dbus call");
    }
  else if (call->name != NULL && !g_dbus_is_name (call->name))
    {
      cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
                            "the \"name\" field in dbus call is not a valid bus name: %s", call->name);
    }
  else if (!cockpit_json_get_string (object, "id", NULL, &call->cookie))
    {
      cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
                            "the \"id\" field is invalid in call");
    }
  else if (!cockpit_json_get_string (object, "type", NULL, &call->type))
    {
      cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
                            "the \"type\" field is invalid in call");
    }
  else if (call->type && !g_variant_is_signature (call->type))
    {
      cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
                            "the \"type\" signature is not valid in dbus call: %s", call->type);
    }
  else if (!cockpit_json_get_string (object, "flags", NULL, &call->flags))
    {
      cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
                            "the \"flags\" field is invalid in dbus call");
    }
  else if (!cockpit_json_get_int(object, "timeout", G_MAXINT, &timeout) ||
           timeout <= 0 || timeout > G_MAXINT)
    {
      cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
                            "the \"timeout\" field is invalid in dbus call");
    }
  else
    {
      if (call->type)
        {
          string = g_strdup_printf ("(%s)", call->type);
          call->param_type = g_variant_type_new (string);
          g_free (string);
        }

      /* No arguments or zero arguments, can make call without introspecting */
      if (!call->param_type)
        {
          if (json_array_get_length (json_node_get_array (call->args)) == 0)
            call->param_type = g_variant_type_new ("()");
        }

      call->dbus_json = self;
      call->request = json_object_ref (object);
      self->active_calls = g_list_prepend (self->active_calls, call);
      call->link = g_list_find (self->active_calls, call);
      call->timeout = timeout;

      if (call->param_type)
        {
          /* Frees call data when done */
          handle_dbus_call_on_interface (self, call);
        }
      else
        {
          peer = ensure_peer (self, call->name);
          cockpit_dbus_cache_introspect (peer->cache, call->path,
                                         call->interface, on_introspect_ready, call);
        }

      /* Start processing call */
      return;
    }

  /* call was invalid */
  call_data_free (call);
}

static GVariantType *
calculate_signal_param_type (CockpitDBusJson *self,
                             const gchar *iface,
                             const gchar *signal)
{
  GDBusInterfaceInfo *info;
  const GVariantType *arg_types[256];
  GDBusSignalInfo *signal_info = NULL;
  guint n;

  info = g_hash_table_lookup (self->interface_info, iface);
  if (info)
    signal_info = g_dbus_interface_info_lookup_signal (info, signal);
  if (signal_info == NULL)
    {
      cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
                            "signal argument types for signal %s %s unknown", iface, signal);
      return NULL;
    }
  else if (signal_info->args)
    {
      for (n = 0; signal_info->args[n] != NULL; n++)
        {
          /* DBus places a hard limit of 255 on signature length.
           * therefore number of args must be less than 256.
           */
          if (n >= G_N_ELEMENTS (arg_types))
            return NULL;

          arg_types[n] = G_VARIANT_TYPE (signal_info->args[n]->signature);

          if G_UNLIKELY (arg_types[n] == NULL)
            return NULL;
        }
    }
  else
    {
      n = 0;
    }
  return g_variant_type_new_tuple (arg_types, n);
}

static void
handle_dbus_signal_on_interface (CockpitDBusJson *self,
                                 GVariantType *param_type,
                                 const gchar *destination,
                                 const gchar *path,
                                 const gchar *interface,
                                 const gchar *signal,
                                 JsonNode *args)
{
  CockpitChannel *channel;
  GVariant *parameters = NULL;
  GDBusMessage *message = NULL;
  GError *error = NULL;

  parameters = parse_json (args, param_type, &error);
  if (parameters)
    {
      g_debug ("%s: signal %s %s at %s", self->logname, interface, signal, path);

      message = g_dbus_message_new_signal (path, interface, signal);
      g_dbus_message_set_body (message, parameters);
      parameters = NULL;

      if (destination)
        g_dbus_message_set_destination (message, destination);

      g_dbus_connection_send_message (self->connection, message,
                                      G_DBUS_SEND_MESSAGE_FLAGS_NONE,
                                      NULL, &error);
    }

  if (error)
    {
      channel = COCKPIT_CHANNEL (self);
      if (error->code == G_IO_ERROR_INVALID_ARGUMENT ||
          error->code == G_DBUS_ERROR_INVALID_ARGS)
        {
          cockpit_channel_fail (channel, "protocol-error", "%s", error->message);
        }
      else
        {
          cockpit_channel_fail (channel, "internal-error", "%s", error->message);
        }
      g_error_free (error);
    }

  if (message)
    g_object_unref (message);
}

static void
handle_dbus_signal (CockpitDBusJson *self,
                    JsonObject *object)
{
  GVariantType *param_type = NULL;
  const gchar *interface;
  const gchar *destination;
  const gchar *path;
  const gchar *type;
  const gchar *flags;
  const gchar *signal;
  JsonNode *node;
  JsonNode *args;
  gchar *string;

  node = json_object_get_member (object, "signal");
  g_return_if_fail (node != NULL);

  if (!parse_json_method (self, node, "signal", &path, &interface, &signal, &args))
    {
      /* fall through to call invalid */
    }
  else if (!cockpit_json_get_string (object, "name", NULL, &destination))
    {
      cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
                            "the 'name' field is invalid in signal");
    }
  else if (destination != NULL && !g_dbus_is_name (destination))
    {
      cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
                            "the 'name' field is not a valid bus name: %s", destination);
    }
  else if (!cockpit_json_get_string (object, "type", NULL, &type))
    {
      cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
                            "the 'type' field is invalid in dbus signal");
    }
  else if (type && !g_variant_is_signature (type))
    {
      cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
                            "type signature is not valid in dbus signal: %s", type);
    }
  else if (!cockpit_json_get_string (object, "flags", NULL, &flags))
    {
      cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
                            "the 'flags' field is invalid in call");
    }
  else
    {
      if (type)
        {
          string = g_strdup_printf ("(%s)", type);
          param_type = g_variant_type_new (string);
          g_free (string);
        }
      else
        {
          param_type = calculate_signal_param_type (self, interface, signal);
        }

      if (param_type)
        {
          handle_dbus_signal_on_interface (self, param_type, destination,
                                           path, interface, signal, args);
        }
    }

  g_variant_type_free (param_type);
}

static void
on_add_match_ready (GObject *source,
                    GAsyncResult *result,
                    gpointer user_data)
{
  CockpitDBusJson *self = COCKPIT_DBUS_JSON (user_data);
  GError *error = NULL;
  GVariant *retval;

  retval = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), result, &error);
  if (error)
    {
      if (!g_cancellable_is_cancelled (self->cancellable) &&
          !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CLOSED))
        {
          cockpit_channel_fail (COCKPIT_CHANNEL (self), "internal-error",
                                "couldn't add match to bus: %s", error->message);
        }
      g_error_free (error);
    }
  if (retval)
    g_variant_unref (retval);
  g_object_unref (self);
}

static gboolean
parse_json_rule (CockpitDBusJson *self,
                 JsonNode *node,
                 const gchar **name,
                 const gchar **path,
                 const gchar **path_namespace,
                 const gchar **interface,
                 const gchar **signal,
                 const gchar **arg0)
{
  CockpitChannel *channel = COCKPIT_CHANNEL (self);
  JsonObject *object;
  gboolean valid;
  GList *names, *l;

  if (!JSON_NODE_HOLDS_OBJECT (node))
    {
      cockpit_channel_fail (channel, "protocol-error", "incorrect match field in dbus command");
      return FALSE;
    }

  object = json_node_get_object (node);

  if (name)
    *name = NULL;
  if (path)
    *path = NULL;
  if (path_namespace)
    *path_namespace = NULL;
  if (signal)
    *signal = NULL;
  if (interface)
    *interface = NULL;
  if (arg0)
    *arg0 = NULL;

  names = json_object_get_members (object);
  for (l = names; l != NULL; l = g_list_next (l))
    {
      valid = FALSE;
      if (name && g_str_equal (l->data, "name"))
        valid = cockpit_json_get_string (object, "name", NULL, name);
      if (interface && g_str_equal (l->data, "interface"))
        valid = cockpit_json_get_string (object, "interface", NULL, interface);
      else if (signal && g_str_equal (l->data, "member"))
        valid = cockpit_json_get_string (object, "member", NULL, signal);
      else if (path && g_str_equal (l->data, "path"))
        valid = cockpit_json_get_string (object, "path", NULL, path);
      else if (path_namespace && g_str_equal (l->data, "path_namespace"))
        valid = cockpit_json_get_string (object, "path_namespace", NULL, path_namespace);
      else if (arg0 && g_str_equal (l->data, "arg0"))
        valid = cockpit_json_get_string (object, "arg0", NULL, arg0);

      if (!valid)
        {
          cockpit_channel_fail (channel, "protocol-error",
                                "invalid or unsupported match field: %s", (gchar *)l->data);
          g_list_free (names);
          return FALSE;
        }
    }
  g_list_free (names);

  valid = FALSE;
  if (name && *name && !g_dbus_is_name (*name))
    cockpit_channel_fail (channel, "protocol-error", "match \"name\" is not valid: %s", *name);
  else if (path && *path && !g_variant_is_object_path (*path))
    cockpit_channel_fail (channel, "protocol-error", "match path is not valid: %s", *path);
  else if (path_namespace && *path_namespace && !g_variant_is_object_path (*path_namespace))
    cockpit_channel_fail (channel, "protocol-error", "match path_namespace is not valid: %s", *path_namespace);
  else if (interface && *interface && !g_dbus_is_interface_name (*interface))
    cockpit_channel_fail (channel, "protocol-error", "match interface is not valid: %s", *interface);
  else if (signal && *signal && !g_dbus_is_member_name (*signal))
    cockpit_channel_fail (channel, "protocol-error", "match \"member\" is not valid: %s", *signal);
  else if (arg0 && *arg0 && strchr (*arg0, '\'') != NULL)
    cockpit_channel_fail (channel, "protocol-error", "match arg0 is not valid: %s", *arg0);
  else if (path && path_namespace && *path && *path_namespace)
    cockpit_channel_fail (channel, "protocol-error", "match cannot specify both path and path_namespace");
  else
    valid = TRUE;

  if (name)
    {
      if (!*name)
        *name = self->default_name;
      if (!*name && self->bus_type != G_BUS_TYPE_NONE)
        {
          cockpit_channel_fail (channel, "protocol-error",
                                "%s: no \"name\" specified in match", self->logname);
          valid = FALSE;
        }
    }

  return valid;
}

static gchar *
build_dbus_match (CockpitDBusJson *self,
                  const gchar *name,
                  const gchar *path,
                  const gchar *path_namespace,
                  const gchar *interface,
                  const gchar *signal,
                  const gchar *arg0)
{
  GString *string = g_string_new ("type='signal'");
  if (!name)
    name = self->default_name;
  if (name)
    g_string_append_printf (string, ",sender='%s'", name);
  if (path)
    g_string_append_printf (string, ",path='%s'", path);
  if (path_namespace && !g_str_equal (path_namespace, "/"))
    g_string_append_printf (string, ",path_namespace='%s'", path_namespace);
  if (interface)
    g_string_append_printf (string, ",interface='%s'", interface);
  if (signal)
    g_string_append_printf (string, ",member='%s'", signal);
  if (arg0)
    g_string_append_printf (string, ",arg0='%s'", arg0);
  return g_string_free (string, FALSE);
}

static void
handle_dbus_add_match (CockpitDBusJson *self,
                       JsonObject *object)
{
  CockpitDBusPeer *peer = NULL;
  JsonNode *node;
  const gchar *name;
  const gchar *path;
  const gchar *path_namespace;
  const gchar *interface;
  const gchar *signal;
  const gchar *arg0;
  gchar *match;

  node = json_object_get_member (object, "add-match");
  g_return_if_fail (node != NULL);

  if (!parse_json_rule (self, node, &name, &path, &path_namespace, &interface, &signal, &arg0))
    return;

  peer = ensure_peer (self, name);
  if (cockpit_dbus_rules_add (peer->rules,
                              path ? path : path_namespace,
                              path_namespace ? TRUE : FALSE,
                              interface, signal, arg0))
    {
      if (peer->name)
        {
          match = build_dbus_match (self, name, path, path_namespace, interface, signal, arg0);
          g_dbus_connection_call (self->connection,
                                  "org.freedesktop.DBus",
                                  "/org/freedesktop/DBus",
                                  "org.freedesktop.DBus",
                                  "AddMatch",
                                  g_variant_new ("(s)", match),
                                  NULL, G_DBUS_CALL_FLAGS_NO_AUTO_START, -1,
                                  self->cancellable,
                                  on_add_match_ready,
                                  g_object_ref (self));
          g_free (match);
        }
    }
}

static void
on_remove_match_ready (GObject *source,
                       GAsyncResult *result,
                       gpointer user_data)
{
  CockpitDBusJson *self = COCKPIT_DBUS_JSON (user_data);
  GError *error = NULL;
  GVariant *retval;

  retval = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), result, &error);
  if (error)
    {
      if (!g_cancellable_is_cancelled (self->cancellable) &&
          !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CLOSED))
        {
          cockpit_channel_fail (COCKPIT_CHANNEL (self), "internal-error",
                                "couldn't remove match from bus: %s", error->message);
        }
      g_error_free (error);
    }
  if (retval)
    g_variant_unref (retval);
  g_object_unref (self);
}

static void
handle_dbus_remove_match (CockpitDBusJson *self,
                          JsonObject *object)
{
  CockpitDBusPeer *peer;
  JsonNode *node;
  const gchar *name;
  const gchar *path;
  const gchar *path_namespace;
  const gchar *interface;
  const gchar *signal;
  const gchar *arg0;
  gchar *match;

  node = json_object_get_member (object, "remove-match");
  g_return_if_fail (node != NULL);

  if (!parse_json_rule (self, node, &name, &path, &path_namespace, &interface, &signal, &arg0))
    return;

  peer = ensure_peer (self, name);
  if (cockpit_dbus_rules_remove (peer->rules,
                                 path ? path : path_namespace,
                                 path_namespace ? TRUE : FALSE,
                                 interface, signal, arg0))
    {
      if (peer->name)
        {
          match = build_dbus_match (self, name, path, path_namespace, interface, signal, arg0);
          g_dbus_connection_call (self->connection,
                                  "org.freedesktop.DBus",
                                  "/org/freedesktop/DBus",
                                  "org.freedesktop.DBus",
                                  "RemoveMatch",
                                  g_variant_new ("(s)", match),
                                  NULL, G_DBUS_CALL_FLAGS_NO_AUTO_START, -1,
                                  NULL, /* don't cancel removes */
                                  on_remove_match_ready,
                                  g_object_ref (self));
          g_free (match);
        }
    }
}

static void
maybe_include_name (CockpitDBusJson *self,
                    JsonObject *object,
                    const gchar *name)
{
  if (name && g_strcmp0 (name, self->default_name) != 0)
    json_object_set_string_member (object, "name", name);
}

static void
on_cache_meta (CockpitDBusCache *cache,
               GDBusInterfaceInfo *iface,
               gpointer user_data)
{
  CockpitDBusPeer *peer = user_data;
  JsonObject *interface;
  JsonObject *meta;
  JsonObject *message;

  interface = cockpit_dbus_meta_build (iface);

  meta = json_object_new ();
  json_object_set_object_member (meta, iface->name, interface);

  message = json_object_new ();
  json_object_set_object_member (message, "meta", meta);

  maybe_include_name (peer->dbus_json, message, peer->name);
  send_json_object (peer->dbus_json, message);
  json_object_unref (message);
}

static void
handle_dbus_meta (CockpitDBusJson *self,
                  JsonObject *object)
{
  CockpitChannel *channel = COCKPIT_CHANNEL (self);
  GDBusInterfaceInfo *iface;
  JsonObject *interface;
  GError *error = NULL;
  JsonObject *meta;
  GList *names, *l;
  JsonNode *node;

  node = json_object_get_member (object, "meta");
  g_return_if_fail (node != NULL);

  if (!JSON_NODE_HOLDS_OBJECT (node))
    {
      cockpit_channel_fail (channel, "protocol-error", "incorrect \"meta\" field in dbus command");
      return;
    }

  meta = json_node_get_object (node);
  names = json_object_get_members (meta);
  for (l = names; l != NULL; l = g_list_next (l))
    {
      if (!cockpit_json_get_object (meta, l->data, NULL, &interface))
        {
          cockpit_channel_fail (channel, "protocol-error", "invalid interface in dbus \"meta\" command");
          break;
        }

      iface = cockpit_dbus_meta_parse (l->data, interface, &error);
      if (iface)
        {
          cockpit_dbus_interface_info_push (self->interface_info, iface);
          g_dbus_interface_info_unref (iface);
        }
      else
        {
          cockpit_channel_fail (channel, "protocol-error", "%s", error->message);
          g_error_free (error);
          break;
        }
    }

  g_list_free (names);
}

static JsonObject *
build_json_update (GHashTable *paths)
{
  GHashTableIter i, j, k;
  GHashTable *interfaces;
  GHashTable *properties;
  const gchar *interface;
  const gchar *property;
  const gchar *path;
  JsonObject *notify;
  JsonObject *object;
  JsonObject *iface;
  GVariant *value;

  notify = json_object_new ();

  g_hash_table_iter_init (&i, paths);
  while (g_hash_table_iter_next (&i, (gpointer *)&path, (gpointer *)&interfaces))
    {
      object = json_object_new ();

      g_hash_table_iter_init (&j, interfaces);
      while (g_hash_table_iter_next (&j, (gpointer *)&interface, (gpointer *)&properties))
        {
          if (properties == NULL)
            {
              json_object_set_null_member (object, interface);
            }
          else
            {
              iface = json_object_new ();

              g_hash_table_iter_init (&k, properties);
              while (g_hash_table_iter_next (&k, (gpointer *)&property, (gpointer *)&value))
                json_object_set_member (iface, property, build_json (value, NULL));

              json_object_set_object_member (object, interface, iface);
            }
        }

      json_object_set_object_member (notify, path, object);
    }

  return notify;
}

static void
on_cache_update (CockpitDBusCache *cache,
                 GHashTable *update,
                 gpointer user_data)
{
  CockpitDBusPeer *peer = user_data;
  JsonObject *object = json_object_new ();
  maybe_include_name (peer->dbus_json, object, peer->name);
  json_object_set_object_member (object, "notify", build_json_update (update));
  send_json_object (peer->dbus_json, object);
  json_object_unref (object);
}

static void
handle_dbus_watch (CockpitDBusJson *self,
                   JsonObject *object)
{
  CockpitDBusPeer *peer = NULL;
  const gchar *name;
  const gchar *path;
  const gchar *path_namespace;
  const gchar *interface;
  gboolean is_namespace = FALSE;
  const gchar *cookie;
  JsonNode *node;

  node = json_object_get_member (object, "watch");
  g_return_if_fail (node != NULL);

  if (!parse_json_rule (self, node, &name, &path, &path_namespace, &interface, NULL, NULL))
    return;

  if (path_namespace)
    {
      path = path_namespace;
      is_namespace = TRUE;
    }

  peer = ensure_peer (self, name);
  cockpit_dbus_cache_watch (peer->cache, path, is_namespace, interface);

  if (!path)
    path = "/";

  /* Send back a reply when this has completed */
  if (cockpit_json_get_string (object, "id", NULL, &cookie))
    {
      object = json_object_new ();
      json_object_set_array_member (object, "reply", json_array_new ());
      json_object_set_string_member (object, "id", cookie);
      cockpit_dbus_cache_poke (peer->cache, path, NULL);
      send_with_barrier (self, peer, object);
      json_object_unref (object);
    }
}

static void
handle_dbus_unwatch (CockpitDBusJson *self,
                     JsonObject *object)
{
  CockpitDBusPeer *peer;
  const gchar *name;
  const gchar *path;
  const gchar *path_namespace;
  const gchar *interface;
  gboolean is_namespace = FALSE;
  JsonNode *node;

  node = json_object_get_member (object, "unwatch");
  g_return_if_fail (node != NULL);

  if (!parse_json_rule (self, node, &name, &path, &path_namespace, &interface, NULL, NULL))
    return;

  if (path_namespace)
    {
      path = path_namespace;
      is_namespace = TRUE;
    }

  peer = ensure_peer (self, name);
  cockpit_dbus_cache_unwatch (peer->cache, path, is_namespace, interface);
}

static GVariantType *
calculate_reply_param_type (CockpitChannel *channel,
                            GDBusMethodInvocation *invocation)
{
  const GVariantType *arg_types[256];
  const GDBusMethodInfo *method_info;
  guint n;

  method_info = g_dbus_method_invocation_get_method_info (invocation);
  if (method_info == NULL)
    {
      cockpit_channel_fail (channel, "protocol-error",
                            "reply argument types for signal %s %s unknown",
                            g_dbus_method_invocation_get_interface_name (invocation),
                            g_dbus_method_invocation_get_method_name (invocation));
      return NULL;
    }
  else if (method_info->out_args)
    {
      for (n = 0; method_info->out_args[n] != NULL; n++)
        {
          /* DBus places a hard limit of 255 on signature length.
           * therefore number of args must be less than 256.
           */
          if (n >= G_N_ELEMENTS (arg_types))
            g_return_val_if_reached (NULL);

          arg_types[n] = G_VARIANT_TYPE (method_info->out_args[n]->signature);

          if G_UNLIKELY (arg_types[n] == NULL)
            g_return_val_if_reached (NULL);
        }
    }
  else
    {
      n = 0;
    }
  return g_variant_type_new_tuple (arg_types, n);
}

static void
handle_dbus_reply (CockpitDBusJson *self,
                   JsonObject *object)
{
  CockpitChannel *channel = COCKPIT_CHANNEL (self);
  gpointer invocation = NULL;
  GVariantType *param_type;
  const gchar *cookie;
  GVariant *parameters;
  GError *error = NULL;
  JsonNode *args;
  JsonNode *node;

  node = json_object_get_member (object, "reply");
  g_return_if_fail (node != NULL);

  if (!JSON_NODE_HOLDS_ARRAY (node))
    {
      cockpit_channel_fail (channel, "protocol-error",
                            "incorrect \"reply\" field in dbus command");
    }
  else if (json_array_get_length (json_node_get_array (node)) < 1)
    {
      cockpit_channel_fail (channel, "protocol-error",
                            "missing values in dbus method \"reply\"");
    }
  else if (!cockpit_json_get_string (object, "id", NULL, &cookie))
    {
      cockpit_channel_fail (channel, "protocol-error",
                            "invalid \"id\" in dbus method reply");
    }
  else if (cookie == NULL)
    {
      cockpit_channel_fail (channel, "protocol-error",
                            "missing \"id\" in dbus method reply");
    }
  else if (!self->invocations ||
           !g_hash_table_lookup_extended (self->invocations, cookie, NULL, &invocation))
    {
      cockpit_channel_fail (channel, "protocol-error",
                            "unknown \"id\" in dbus method reply: %s", cookie);
    }
  else
    {
      param_type = calculate_reply_param_type (channel, invocation);
      args = json_array_get_element (json_node_get_array (node), 0);
      parameters = parse_json (args, param_type, &error);
      g_variant_type_free (param_type);

      if (error)
        {
          /* The invocation will be cleaned up by our dispose function */
          cockpit_channel_fail (channel, "protocol-error",
                                "invalid argument in dbus method reply: %s", error->message);
          g_error_free (error);
        }
      else
        {
          /* Reference to parameters is consumed */
          g_dbus_method_invocation_return_value (invocation, parameters);
          g_hash_table_remove (self->invocations, cookie);
        }
    }
}

static gboolean
parse_json_error (CockpitChannel *channel,
                  JsonNode *node,
                  const gchar **error_name,
                  const gchar **error_message)
{
  JsonArray *array;
  JsonNode *nested;
  JsonArray *params;
  guint length;

  g_assert (error_name != NULL);
  g_assert (error_message != NULL);

  *error_name = NULL;
  *error_message = NULL;

  if (!JSON_NODE_HOLDS_ARRAY (node))
    {
      cockpit_channel_fail (channel, "protocol-error",
                            "invalid \"error\" field in dbus command");
      return FALSE;
    }

  array = json_node_get_array (node);
  length = json_array_get_length (array);
  if (length < 1)
    {
      cockpit_channel_fail (channel, "protocol-error",
                            "missing \"error\" error name in dbus command");
      return FALSE;
    }

  *error_name = array_string_element (array, 0);
  if (!*error_name)
    {
      cockpit_channel_fail (channel, "protocol-error",
                            "invalid \"error\" error name in dbus command");
      return FALSE;
    }

  if (length >= 2)
    {
      nested = json_array_get_element (array, 1);
      if (!JSON_NODE_HOLDS_ARRAY (nested))
        {
          cockpit_channel_fail (channel, "protocol-error",
                                "invalid \"error\" parameters in dbus command");
          return FALSE;
        }

      params = json_node_get_array (nested);
      if (json_array_get_length (params) > 0)
        *error_message = array_string_element (params, 0);
    }

  if (!*error_message)
    *error_message = *error_name;
  return TRUE;
}

static void
handle_dbus_error (CockpitDBusJson *self,
                   JsonObject *object)
{
  CockpitChannel *channel = COCKPIT_CHANNEL (self);
  const gchar *error_message = NULL;
  const gchar *error_name = NULL;
  const gchar *cookie = NULL;
  gpointer invocation = NULL;
  JsonNode *node;

  node = json_object_get_member (object, "error");
  g_return_if_fail (node != NULL);

  if (!JSON_NODE_HOLDS_ARRAY (node))
    {
      cockpit_channel_fail (channel, "protocol-error",
                            "incorrect \"error\" field in dbus command");
    }
  else if (!parse_json_error (channel, node, &error_name, &error_message))
    {
      /* Fail */
    }
  else if (!cockpit_json_get_string (object, "id", NULL, &cookie))
    {
      cockpit_channel_fail (channel, "protocol-error",
                            "invalid \"id\" in dbus method error");
    }
  else if (cookie == NULL)
    {
      cockpit_channel_fail (channel, "protocol-error",
                            "missing \"id\" in dbus method error");
    }
  else if (!self->invocations ||
           !g_hash_table_lookup_extended (self->invocations, cookie, NULL, &invocation))
    {
      cockpit_channel_fail (channel, "protocol-error",
                            "unknown \"id\" in dbus method error: %s", cookie);
    }
  else
    {
      g_dbus_method_invocation_return_dbus_error (invocation, error_name, error_message);
      g_hash_table_remove (self->invocations, cookie);
    }
}

static void
on_method_invocation (GDBusConnection *connection,
                      const gchar *sender,
                      const gchar *object_path,
                      const gchar *interface_name,
                      const gchar *method_name,
                      GVariant *parameters,
                      GDBusMethodInvocation *invocation,
                      gpointer user_data)
{
  CockpitDBusJson *self = user_data;
  GDBusMessageFlags flags;
  GDBusMessage *message;
  gchar *cookie = NULL;
  JsonObject *object;
  JsonArray *call;
  VariantContext context = { NULL };

  message = g_dbus_method_invocation_get_message (invocation);
  flags = g_dbus_message_get_flags (message);

  object = json_object_new ();
  call = json_array_new ();
  json_array_add_string_element (call, object_path);
  json_array_add_string_element (call, interface_name);
  json_array_add_string_element (call, method_name);
  json_array_add_element (call, build_json (parameters, &context));
  json_object_set_array_member (object, "call", call);

  if (!(flags & G_DBUS_MESSAGE_FLAGS_NO_REPLY_EXPECTED))
    {
      g_assert (self->invocations != NULL);
      cookie = g_strdup_printf ("%d", self->last_invocation++);
      g_hash_table_insert (self->invocations, cookie, g_object_ref (invocation));
      json_object_set_string_member (object, "id", cookie);
    }

  if (sender)
    json_object_set_string_member (object, "name", sender);

  send_json_object (self, object);
  json_object_unref (object);
}

static gboolean
parse_json_publish (CockpitChannel *channel,
                    JsonNode *node,
                    const gchar **object_path,
                    const gchar **interface_name)
{
  JsonArray *array;

  g_assert (object_path != NULL);
  g_assert (interface_name != NULL);

  *object_path = NULL;
  *interface_name = NULL;

  if (!JSON_NODE_HOLDS_ARRAY (node))
    {
      cockpit_channel_fail (channel, "protocol-error",
                            "invalid publish field in dbus command");
      return FALSE;
    }

  array = json_node_get_array (node);
  *object_path = array_string_element (array, 0);
  *interface_name = array_string_element (array, 1);

  if (!*object_path && !g_variant_is_object_path (*object_path))
    {
      cockpit_channel_fail (channel, "protocol-error",
                            "publish dbus object path is not valid: %s", *object_path);
    }
  else if (!*interface_name && !g_dbus_is_interface_name (*interface_name))
    {
      cockpit_channel_fail (channel, "protocol-error",
                            "publish dbus interface name is not valid: %s", *interface_name);
    }
  else
    {
      return TRUE;
    }

  return FALSE;
}

static void
handle_dbus_publish (CockpitDBusJson *self,
                     JsonObject *object)
{
  CockpitChannel *channel = COCKPIT_CHANNEL (self);
  const gchar *interface_name = NULL;
  const gchar *object_path = NULL;
  GHashTable *registered = NULL;
  const gchar *cookie = NULL;
  GDBusInterfaceInfo *iface;
  guint registration_id;
  GError *error = NULL;
  JsonNode *node;
  gchar *key;

  static const GDBusInterfaceVTable vtable = {
      on_method_invocation,
      NULL, NULL /* Property access handled as invocations */
  };

  node = json_object_get_member (object, "publish");
  g_return_if_fail (node != NULL);

  if (!parse_json_publish (channel, node, &object_path, &interface_name))
    return;

  iface = g_hash_table_lookup (self->interface_info, interface_name);
  if (!iface)
    {
      cockpit_channel_fail (channel, "protocol-error",
                            "no \"meta\" introspection data available for interface: %s", interface_name);
      return;
    }

  /* Start up the necessary hash tables for exporting objects */
  registered = g_object_get_data (G_OBJECT (self->connection), "registered-objects");
  if (!registered)
    {
      registered = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
      g_object_set_data_full (G_OBJECT (self->connection), "registered-objects",
                              registered, (GDestroyNotify)g_hash_table_unref);
    }
  if (!self->registered)
    self->registered = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
  if (!self->invocations)
    self->invocations = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);

  /* If there's an old object registered here, unregister it */
  key = g_strdup_printf ("%s\n%s", object_path, interface_name);
  registration_id = GPOINTER_TO_UINT (g_hash_table_lookup (registered, key));
  if (registration_id)
    g_dbus_connection_unregister_object (self->connection, registration_id);

  /* Register the object */
  registration_id = g_dbus_connection_register_object (self->connection, object_path,
                                                       iface, &vtable, self, NULL, &error);

  if (error)
    {
      cockpit_channel_fail (channel, "protocol-error",
                            "could not publish interface %s at %s: %s",
                            interface_name, object_path, error->message);
      g_error_free (error);
      g_free (key);
      return;
    }

  /* All done mark this so we're able to later unpublish */
  g_hash_table_insert (registered, g_strdup (key), GUINT_TO_POINTER (registration_id));
  g_hash_table_insert (self->registered, key, GUINT_TO_POINTER (registration_id));

  /* Caller wants a reply to know when published */
  if (cockpit_json_get_string (object, "id", NULL, &cookie))
    {
      object = json_object_new ();
      json_object_set_array_member (object, "reply", json_array_new ());
      json_object_set_string_member (object, "id", cookie);
      send_json_object (self, object);
      json_object_unref (object);
    }
}

static void
handle_dbus_unpublish (CockpitDBusJson *self,
                       JsonObject *object)
{
  CockpitChannel *channel = COCKPIT_CHANNEL (self);
  const gchar *interface_name = NULL;
  const gchar *object_path = NULL;
  GHashTable *registered;
  JsonNode *node;
  gchar *key;
  guint id = 0;

  node = json_object_get_member (object, "unpublish");
  g_return_if_fail (node != NULL);

  if (!parse_json_publish (channel, node, &object_path, &interface_name))
    return;

  key = g_strdup_printf ("%s\n%s", object_path, interface_name);
  if (self->registered)
    {
      id = GPOINTER_TO_UINT (g_hash_table_lookup (self->registered, key));
      g_hash_table_remove (self->registered, key);

      /* A per connection list of all interfaces and objects registered */
      registered = g_object_get_data (G_OBJECT (self->connection), "registered-objects");
      g_hash_table_remove (registered, key);
    }
  g_free (key);

  if (id != 0)
    g_dbus_connection_unregister_object (self->connection, id);
}


static void
cockpit_dbus_json_recv (CockpitChannel *channel,
                        GBytes *message)
{
  CockpitDBusJson *self = COCKPIT_DBUS_JSON (channel);
  GError *error = NULL;
  JsonObject *object = NULL;

  object = cockpit_json_parse_bytes (message, &error);
  if (!object)
    {
      cockpit_channel_fail (channel, "protocol-error", "failed to parse dbus request: %s", error->message);
      g_clear_error (&error);
      return;
    }

  if (json_object_has_member (object, "call"))
    handle_dbus_call (self, object);
  else if (json_object_has_member (object, "signal"))
    handle_dbus_signal (self, object);
  else if (json_object_has_member (object, "add-match"))
    handle_dbus_add_match (self, object);
  else if (json_object_has_member (object, "remove-match"))
    handle_dbus_remove_match (self, object);
  else if (json_object_has_member (object, "watch"))
    handle_dbus_watch (self, object);
  else if (json_object_has_member (object, "unwatch"))
    handle_dbus_unwatch (self, object);
  else if (json_object_has_member (object, "meta"))
    handle_dbus_meta (self, object);
  else if (json_object_has_member (object, "error"))
    handle_dbus_error (self, object);
  else if (json_object_has_member (object, "reply"))
    handle_dbus_reply (self, object);
  else if (json_object_has_member (object, "publish"))
    handle_dbus_publish (self, object);
  else if (json_object_has_member (object, "unpublish"))
    handle_dbus_unpublish (self, object);
  else
    {
      cockpit_channel_fail (channel, "protocol-error", "got unsupported dbus command");
    }

  json_object_unref (object);
}

static void
on_signal_message (GDBusConnection *connection,
                   const gchar *sender,
                   const gchar *path,
                   const gchar *interface,
                   const gchar *signal,
                   GVariant *parameters,
                   gpointer user_data)
{
  /*
   * HACK: There is no way we can access the original GDBusMessage. This
   * means things like flags and byte order are lost here.
   *
   * We cannot use a GDBusMessageFilterFunction and use that to subsribe
   * to signals, because then the ordering guarantees are out the window.
   */

  CockpitDBusPeer *peer = user_data;
  const gchar *arg0 = NULL;
  JsonObject *object;

  /* Unfortunately we also have to recalculate this */
  if (parameters &&
      g_variant_is_of_type (parameters, G_VARIANT_TYPE_TUPLE) &&
      g_variant_n_children (parameters) > 0)
    {
      GVariant *item;
      item = g_variant_get_child_value (parameters, 0);
      if (g_variant_is_of_type (item, G_VARIANT_TYPE_STRING))
        arg0 = g_variant_get_string (item, NULL);
      g_variant_unref (item);
    }

  if (cockpit_dbus_rules_match (peer->rules, path, interface, signal, arg0))
    {
      object = build_json_signal (path, interface, signal, parameters);
      cockpit_dbus_cache_poke (peer->cache, path, interface);
      maybe_include_name (peer->dbus_json, object, peer->name);
      send_with_barrier (peer->dbus_json, peer, object);
      json_object_unref (object);
    }
}

static void
cockpit_dbus_json_closed (CockpitChannel *channel,
                          const gchar *problem)
{
  /* When closed disconnect from everything */
  g_object_run_dispose (G_OBJECT (channel));
}

static void
cockpit_dbus_json_init (CockpitDBusJson *self)
{
  self->cancellable = g_cancellable_new ();

  self->peers = g_hash_table_new (g_str_hash, g_str_equal);
  self->interface_info = cockpit_dbus_interface_info_new ();
}

static void
send_owned (CockpitDBusJson *self,
            const gchar *name,
            const gchar *owner)
{
  JsonObject *object;
  object = json_object_new ();
  maybe_include_name (self, object, name);
  json_object_set_string_member (object, "owner", owner);
  send_json_object (self, object);
  json_object_unref (object);
}

static void
send_ready (CockpitDBusJson *self)
{
  CockpitChannel *channel = COCKPIT_CHANNEL (self);
  const gchar *unique_name = NULL;
  JsonObject *message;

  message = json_object_new ();
  unique_name = g_dbus_connection_get_unique_name (self->connection);
  if (unique_name)
    json_object_set_string_member (message, "unique-name", unique_name);

  cockpit_channel_ready (channel, message);
  json_object_unref (message);
}

static void
on_name_appeared (GDBusConnection *connection,
                  const gchar *name,
                  const gchar *name_owner,
                  gpointer user_data)
{
  CockpitDBusJson *self = COCKPIT_DBUS_JSON (user_data);

  g_object_ref (self);

  CockpitDBusPeer *peer = g_hash_table_lookup (self->peers, name);
  if (peer)
    cockpit_dbus_cache_set_name_owner (peer->cache, name_owner);

  if (!self->default_appeared)
    {
      self->default_appeared = TRUE;
      send_ready (self);
    }

  send_owned (self, name, name_owner);

  g_object_unref (self);
}

static void
on_name_vanished (GDBusConnection *connection,
                  const gchar *name,
                  gpointer user_data)
{
  CockpitDBusJson *self = COCKPIT_DBUS_JSON (user_data);
  CockpitChannel *channel = COCKPIT_CHANNEL (self);

  send_owned (self, name, NULL);

  CockpitDBusPeer *peer = g_hash_table_lookup (self->peers, name);
  if (peer)
    {
      // Using the empty string as the name owner means that the cache
      // will not process any signals until we set a real name owner
      // again in on_name_appeared.
      //
      cockpit_dbus_cache_set_name_owner (peer->cache, "");
    }

  if (!G_IS_DBUS_CONNECTION (connection) || g_dbus_connection_is_closed (connection))
    cockpit_channel_close (channel, "disconnected");
  else if (!self->default_appeared)
    cockpit_channel_close (channel, "not-found");
}

static CockpitDBusPeer *
ensure_peer (CockpitDBusJson *self,
             const gchar *name)
{
  CockpitDBusPeer *peer;

  if (!name)
    name = self->default_name;

  peer = g_hash_table_lookup (self->peers, name ? name : "");
  if (!peer)
    {
      peer = g_new0 (CockpitDBusPeer, 1);
      peer->name = g_strdup (name);
      peer->dbus_json = self;
      peer->cache = cockpit_dbus_cache_new (self->connection, name, self->logname, self->interface_info);
      peer->meta_sig = g_signal_connect (peer->cache, "meta", G_CALLBACK (on_cache_meta), peer);
      peer->update_sig = g_signal_connect (peer->cache, "update", G_CALLBACK (on_cache_update), peer);
      peer->rules = cockpit_dbus_rules_new ();

      peer->subscribe_id = g_dbus_connection_signal_subscribe (self->connection,
                                                               name,
                                                               NULL, /* interface */
                                                               NULL, /* member */
                                                               NULL, /* object_path */
                                                               NULL, /* arg0 */
                                                               G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
                                                               on_signal_message, peer, NULL);

      g_hash_table_insert (self->peers, peer->name ? peer->name : "", peer);
    }

  return peer;
}

static void
subscribe_and_cache (CockpitDBusJson *self)
{
  g_dbus_connection_set_exit_on_close (self->connection, FALSE);
  if (self->default_name || self->bus_type == G_BUS_TYPE_NONE)
    ensure_peer (self, self->default_name);
}

static void
process_connection (CockpitDBusJson *self,
                    GError *error)
{
  CockpitChannel *channel = COCKPIT_CHANNEL (self);
  GBusNameWatcherFlags flags;
  if (!self->connection)
    {
      if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) ||
          g_cancellable_is_cancelled (self->cancellable))
        {
          g_debug ("%s", error ? error->message : "(error is NULL)");
        }
      else
        {
          cockpit_channel_fail (channel, "internal-error", "%s", error ? error->message : "(error is NULL)");
        }
    }
  else
    {
      /* Yup, we don't want this */
      g_dbus_connection_set_exit_on_close (self->connection, FALSE);
      if (self->default_name)
        {
          flags = G_BUS_NAME_WATCHER_FLAGS_AUTO_START;
          self->default_watch = g_bus_watch_name_on_connection (self->connection,
                                                                self->default_name, flags,
                                                                on_name_appeared,
                                                                on_name_vanished,
                                                                self, NULL);
          self->default_watched = TRUE;
          subscribe_and_cache (self);

          /* Stop the cache from processing signals until we know its
             bus name.
           */
          CockpitDBusPeer *peer = g_hash_table_lookup (self->peers, self->default_name);
          if (peer)
            cockpit_dbus_cache_set_name_owner (peer->cache, "");
        }
      else
        {
          subscribe_and_cache (self);
          send_ready (self);
        }
    }
}

static void
group_connections_weak_notify (gpointer data,
                               GObject *where_the_object_was)
{
  const gchar *key = data;

  if (group_connections)
    {
      g_hash_table_remove (group_connections, key);
      if (g_hash_table_size (group_connections) == 0)
          g_clear_pointer (&group_connections, g_hash_table_unref);
    }
}

static void
on_connection_ready (GObject *source,
                     GAsyncResult *result,
                     gpointer user_data)
{
  CockpitDBusJson *self = COCKPIT_DBUS_JSON (user_data);
  g_autoptr(GError) error = NULL;

  self->connection = g_dbus_connection_new_for_address_finish (result,
                                                               &error);
  process_connection (self, error);
  g_object_unref (self);
}

static void
on_shared_connection_ready (GObject *source,
                            GAsyncResult *result,
                            gpointer user_data)
{
  gchar *key = user_data;
  GDBusConnection *connection;
  g_autoptr(GError) error = NULL;
  GPtrArray *subscribers;

  connection = g_dbus_connection_new_for_address_finish (result, &error);

  /*
   * Notify all CockpitDBusJson objects from the same group that were
   * also waiting for this connection
   */
  subscribers = g_hash_table_lookup (pending_group_connections, key);
  for (guint i = 0; i < subscribers->len; i++)
    {
      CockpitDBusJson *channel = g_ptr_array_index (subscribers, i);

      if (connection)
        channel->connection = g_object_ref (connection);

      process_connection (channel, error);
    }

  g_hash_table_remove (pending_group_connections, key);
  if (g_hash_table_size (pending_group_connections) == 0)
    g_clear_pointer (&pending_group_connections, g_hash_table_unref);

  if (connection)
    {
      if (!group_connections)
        group_connections = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);

      /*
       * Ensure that the key is removed when the last CockpitDBusJson
       * object in its group drops the connection. group_connections
       * takes ownership of 'key'.
       */
      g_object_weak_ref (G_OBJECT (connection), group_connections_weak_notify, key);

      g_hash_table_insert (group_connections, key, connection);

      g_object_unref (connection);
    }
  else
    {
      g_free (key);
    }
}

static void
cockpit_dbus_json_prepare (CockpitChannel *channel)
{
  CockpitDBusJson *self = COCKPIT_DBUS_JSON (channel);
  JsonObject *options;
  const gchar *bus;
  const gchar *address;
  const gchar *group;
  gboolean internal = FALSE;

  COCKPIT_CHANNEL_CLASS (cockpit_dbus_json_parent_class)->prepare (channel);

  options = cockpit_channel_get_options (channel);
  if (!cockpit_json_get_string (options, "bus", NULL, &bus))
    {
      cockpit_channel_fail (channel, "protocol-error", "invalid \"bus\" option in dbus channel");
      return;
    }
  if (!cockpit_json_get_string (options, "address", NULL, &address))
    {
      cockpit_channel_fail (channel, "protocol-error", "invalid \"address\" option in dbus channel");
      return;
    }
  if (!cockpit_json_get_string (options, "group", "default", &group))
    {
      cockpit_channel_fail (channel, "protocol-error", "invalid \"group\" option in dbus channel");
      return;
    }

  /*
   * The default bus is the "user" bus which doesn't exist in many
   * places yet, so use the session bus for now.
   */
  self->bus_type = G_BUS_TYPE_SESSION;
  if (bus == NULL || g_str_equal (bus, "system"))
    {
      self->bus_type = G_BUS_TYPE_SYSTEM;
    }
  else if (g_str_equal (bus, "session") ||
           g_str_equal (bus, "user"))
    {
      self->bus_type = G_BUS_TYPE_SESSION;
    }
  else if (g_str_equal (bus, "none"))
    {
      self->bus_type = G_BUS_TYPE_NONE;
      if (address == NULL || g_str_equal (address, "internal"))
          internal = TRUE;
    }
  else if (g_str_equal (bus, "internal"))
    {
      self->bus_type = G_BUS_TYPE_NONE;
      internal = TRUE;
    }
  else
    {
      cockpit_channel_fail (channel, "protocol-error", "invalid \"bus\" option in dbus channel: %s", bus);
      return;
    }

  if (!internal && !cockpit_dbus_json_allow_external)
    {
      cockpit_channel_close (channel, "not-supported");
      return;
    }

  /* An internal peer to peer connection to cockpit-bridge */
  if (internal)
    {
      if (!cockpit_json_get_null (options, "name", NULL))
        {
          cockpit_channel_fail (channel, "protocol-error",
                                "do not specify \"name\" option in dbus channel when \"internal\"");
          return;
        }

      self->connection = cockpit_dbus_internal_client ();
      if (self->connection == NULL)
        {
          cockpit_channel_fail (channel, "internal-error", "no internal DBus connection");
          return;
        }

      self->default_name = cockpit_dbus_internal_name ();
      self->logname = "internal";

      subscribe_and_cache (self);
      send_ready (self);
    }
  else
    {
      GDBusConnectionFlags flags = G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT;

      if (!cockpit_json_get_string (options, "name", NULL, &self->default_name))
        {
          self->default_name = NULL;
          if (!cockpit_json_get_null (options, "name", NULL))
            {
              cockpit_channel_fail (channel, "protocol-error", "invalid \"name\" option in dbus channel");
              return;
            }
        }
      else if (self->default_name != NULL && !g_dbus_is_name (self->default_name))
        {
          cockpit_channel_fail (channel, "protocol-error",
                                "bad \"name\" option in dbus channel: %s", self->default_name);
          return;
        }

      if (self->bus_type == G_BUS_TYPE_NONE && !g_dbus_is_address (address))
        {
          cockpit_channel_fail (channel, "protocol-error",
                                "bad \"address\" option in dbus channel: %s", address);
          return;
        }

      if (self->default_name != NULL)
        self->logname = self->default_name;
      else if (address != NULL)
        self->logname = address;
      else
        self->logname = bus;

      if (self->bus_type == G_BUS_TYPE_NONE)
        {
          if (self->default_name)
            flags = flags | G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION;

          g_dbus_connection_new_for_address (address,
                                             flags,
                                             NULL,
                                             self->cancellable,
                                             on_connection_ready,
                                             g_object_ref (self));
        }
      else
        {
          gchar *bus_address = NULL;
          gchar *key = NULL;
          GError *error = NULL;
          GDBusConnection *connection = NULL;

          bus_address = g_dbus_address_get_for_bus_sync (self->bus_type, self->cancellable, &error);

          if (bus_address == NULL)
            {
              cockpit_channel_fail (channel, "protocol-error",
                                    "failed to connect to %s bus: %s",
                                    self->bus_type == G_BUS_TYPE_SESSION ? "session" : "system",
                                    error->message);
              g_clear_error (&error);
              return;
            }

          key = g_strdup_printf ("%s:%s", group, bus_address);

          if (group_connections)
            connection = g_hash_table_lookup (group_connections, key);

          if (connection)
            {
              self->connection = g_object_ref (connection);
              process_connection (self, NULL);
            }
          else
            {
              GPtrArray *subscribers = NULL;

              if (!pending_group_connections)
                {
                  pending_group_connections = g_hash_table_new_full (g_str_hash,
                                                                     g_str_equal,
                                                                     g_free,
                                                                     (GDestroyNotify) g_ptr_array_unref);
                }
              else
                {
                  subscribers = g_hash_table_lookup (pending_group_connections, key);
                }

              if (!subscribers)
                {
                  subscribers = g_ptr_array_new_with_free_func (g_object_unref);
                  g_hash_table_insert (pending_group_connections, g_strdup (key), subscribers);

                  g_dbus_connection_new_for_address (bus_address,
                                                     flags | G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION,
                                                     NULL,
                                                     self->cancellable,
                                                     on_shared_connection_ready,
                                                     g_strdup (key));
                }

              g_ptr_array_add (subscribers, g_object_ref (self));
            }

          g_free (key);
          g_free (bus_address);
        }
    }
}

static void
cockpit_dbus_json_dispose (GObject *object)
{
  CockpitDBusJson *self = COCKPIT_DBUS_JSON (object);
  GHashTable *registered = NULL;
  CockpitDBusPeer *peer;
  GHashTableIter iter;
  gpointer value;
  gpointer key;
  GList *l;

  g_cancellable_cancel (self->cancellable);

  if (self->default_watched)
    {
      g_bus_unwatch_name (self->default_watch);
      self->default_watched = FALSE;
    }

  g_hash_table_iter_init (&iter, self->peers);
  while (g_hash_table_iter_next (&iter, NULL, &value))
    {
      g_hash_table_iter_remove (&iter);
      peer = value;

      g_free (peer->name);

      if (peer->cache)
        {
          g_signal_handler_disconnect (peer->cache, peer->meta_sig);
          g_signal_handler_disconnect (peer->cache, peer->update_sig);
          g_object_run_dispose (G_OBJECT (peer->cache));
          g_object_unref (peer->cache);
        }

      cockpit_dbus_rules_free (peer->rules);

      if (self->connection)
        g_dbus_connection_signal_unsubscribe (self->connection, peer->subscribe_id);
      g_free (peer);
    }

  /* Remove and cancel all pending invocations */
  if (self->connection)
    registered = g_object_get_data (G_OBJECT (self->connection), "registered-objects");
  if (self->registered)
    {
      g_hash_table_iter_init (&iter, self->registered);
      while (g_hash_table_iter_next (&iter, &key, &value))
        {
          if (registered)
            g_hash_table_remove (registered, key);
          g_dbus_connection_unregister_object (self->connection, GPOINTER_TO_UINT (value));
          g_hash_table_iter_remove (&iter);
        }
    }
  if (self->invocations)
    {
      g_hash_table_iter_init (&iter, self->invocations);
      while (g_hash_table_iter_next (&iter, NULL, &value))
        {
          g_hash_table_iter_remove (&iter);
          g_dbus_method_invocation_return_dbus_error (value, "org.freedesktop.DBus.Error.Failed",
                                                      "The DBus interface for this method has been disconnected");
        }
    }

  if (self->fd_channel_ids)
    {
      while (!g_queue_is_empty (self->fd_channel_ids))
        {
          gchar *id;

          id = (gchar *)g_queue_pop_head (self->fd_channel_ids);
          cockpit_pipe_channel_remove_internal_fd (id);

          g_free (id);
        }

      g_queue_free (self->fd_channel_ids);
      self->fd_channel_ids = NULL;
    }

  /* Divorce ourselves the outstanding calls */
  for (l = self->active_calls; l != NULL; l = g_list_next (l))
    ((CallData *)l->data)->dbus_json = NULL;
  g_list_free (self->active_calls);
  self->active_calls = NULL;

  G_OBJECT_CLASS (cockpit_dbus_json_parent_class)->dispose (object);
}

static void
cockpit_dbus_json_finalize (GObject *object)
{
  CockpitDBusJson *self = COCKPIT_DBUS_JSON (object);

  g_clear_object (&self->connection);
  g_hash_table_unref (self->interface_info);
  g_hash_table_unref (self->peers);
  if (self->registered)
    g_hash_table_unref (self->registered);
  if (self->invocations)
    g_hash_table_unref (self->invocations);
  g_object_unref (self->cancellable);

  G_OBJECT_CLASS (cockpit_dbus_json_parent_class)->finalize (object);
}

static void
cockpit_dbus_json_constructed (GObject *object)
{
  const gchar *caps[] = { "address", NULL };

  G_OBJECT_CLASS (cockpit_dbus_json_parent_class)->constructed (object);

  g_object_set (object, "capabilities", &caps, NULL);
}

static void
cockpit_dbus_json_class_init (CockpitDBusJsonClass *klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
  CockpitChannelClass *channel_class = COCKPIT_CHANNEL_CLASS (klass);

  gobject_class->dispose = cockpit_dbus_json_dispose;
  gobject_class->finalize = cockpit_dbus_json_finalize;
  gobject_class->constructed = cockpit_dbus_json_constructed;

  channel_class->prepare = cockpit_dbus_json_prepare;
  channel_class->recv = cockpit_dbus_json_recv;
  channel_class->closed = cockpit_dbus_json_closed;
}

/**
 * cockpit_dbus_json_open:
 * @transport: transport to send messages on
 * @channel_id: the channel id
 * @dbus_service: the DBus service name to talk to
 *
 * This function is mainly used by tests. The normal way to open
 * channels is cockpit_channel_open().
 *
 * Guarantee: channel will not close immediately, even on invalid input.
 *
 * Returns: (transfer full): a new channel
 */
CockpitChannel *
cockpit_dbus_json_open (CockpitTransport *transport,
                        const gchar *channel_id,
                        const gchar *dbus_service)
{
  CockpitChannel *channel;
  JsonObject *options;

  g_return_val_if_fail (channel_id != NULL, NULL);

  options = json_object_new ();
  json_object_set_string_member (options, "bus", "session");
  json_object_set_string_member (options, "service", dbus_service);
  json_object_set_string_member (options, "payload", "dbus-json3");

  channel = g_object_new (COCKPIT_TYPE_DBUS_JSON,
                          "transport", transport,
                          "id", channel_id,
                          "options", options,
                          NULL);

  json_object_unref (options);
  return channel;
}
