static void
on_web_socket_open (WebSocketConnection *connection,
                    gpointer user_data)
{
  CockpitWebSocketStream *self = COCKPIT_WEB_SOCKET_STREAM (user_data);
  CockpitChannel *channel = COCKPIT_CHANNEL (user_data);
  JsonObject *object;
  JsonObject *headers;
  GHashTableIter iter;
  gpointer key, value;

  headers = json_object_new ();

  g_hash_table_iter_init (&iter, web_socket_client_get_headers (WEB_SOCKET_CLIENT (self->client)));
  while (g_hash_table_iter_next (&iter, &key, &value))
    json_object_set_string_member (headers, key, value);

  object = json_object_new ();
  json_object_set_object_member (object, "headers", headers);

  cockpit_channel_control (channel, "response", object);
  json_object_unref (object);

  cockpit_channel_ready (channel, NULL);
}
static void
cockpit_web_socket_stream_dispose (GObject *object)
{
  CockpitWebSocketStream *self = COCKPIT_WEB_SOCKET_STREAM (object);
  GIOStream *io = NULL; // Owned by self->client;

  if (self->client)
    {
      if (web_socket_connection_get_ready_state (self->client) < WEB_SOCKET_STATE_CLOSING)
        web_socket_connection_close (self->client, WEB_SOCKET_CLOSE_GOING_AWAY, "disconnected");
      g_signal_handler_disconnect (self->client, self->sig_open);
      g_signal_handler_disconnect (self->client, self->sig_message);
      g_signal_handler_disconnect (self->client, self->sig_closing);
      g_signal_handler_disconnect (self->client, self->sig_close);
      g_signal_handler_disconnect (self->client, self->sig_error);

      io = web_socket_connection_get_io_stream (self->client);
      if (io != NULL && self->sig_accept_cert)
        g_signal_handler_disconnect (io, self->sig_accept_cert);

      g_object_unref (self->client);
      self->client = NULL;
    }

  G_OBJECT_CLASS (cockpit_web_socket_stream_parent_class)->dispose (object);
}
static void
cockpit_web_socket_stream_finalize (GObject *object)
{
  CockpitWebSocketStream *self = COCKPIT_WEB_SOCKET_STREAM (object);

  g_free (self->url);
  g_free (self->origin);
  g_assert (self->client == NULL);

  G_OBJECT_CLASS (cockpit_web_socket_stream_parent_class)->finalize (object);
}
static gboolean
on_web_socket_error (WebSocketConnection *ws,
                     GError *error,
                     gpointer user_data)
{
  CockpitWebSocketStream *self = COCKPIT_WEB_SOCKET_STREAM (user_data);
  self->last_error_code = 0;
  if (error && error->domain == WEB_SOCKET_ERROR)
    self->last_error_code = error->code;

  return TRUE;
}
static gboolean
cockpit_web_socket_stream_control (CockpitChannel *channel,
                                   const gchar *command,
                                   JsonObject *options)
{
  CockpitWebSocketStream *self = COCKPIT_WEB_SOCKET_STREAM (channel);

  if (!g_str_equal (command, "done"))
    return FALSE;

  if (self->client && web_socket_connection_get_ready_state (self->client) == WEB_SOCKET_STATE_OPEN)
    web_socket_connection_close (self->client, WEB_SOCKET_CLOSE_NORMAL, "disconnected");

  return TRUE;
}
static void
on_web_socket_close (WebSocketConnection *connection,
                     gpointer user_data)
{
  CockpitWebSocketStream *self = COCKPIT_WEB_SOCKET_STREAM (user_data);
  CockpitChannel *channel = COCKPIT_CHANNEL (user_data);
  const gchar *problem;
  gushort code;

  code = web_socket_connection_get_close_code (connection);
  problem = web_socket_connection_get_close_data (connection);

  if (code == WEB_SOCKET_CLOSE_NORMAL || code == WEB_SOCKET_CLOSE_GOING_AWAY)
    {
      problem = NULL;
    }
  else if (problem == NULL || !problem[0])
    {
      /* If we don't have a code but have a last error
       * use it's code */
      if (code == 0)
        code = self->last_error_code;

      switch (code)
        {
        case WEB_SOCKET_CLOSE_NO_STATUS:
        case WEB_SOCKET_CLOSE_ABNORMAL:
          problem = "disconnected";
          break;
        case WEB_SOCKET_CLOSE_PROTOCOL:
        case WEB_SOCKET_CLOSE_UNSUPPORTED_DATA:
        case WEB_SOCKET_CLOSE_BAD_DATA:
        case WEB_SOCKET_CLOSE_POLICY_VIOLATION:
        case WEB_SOCKET_CLOSE_TOO_BIG:
        case WEB_SOCKET_CLOSE_TLS_HANDSHAKE:
          problem = "protocol-error";
          break;
        case WEB_SOCKET_CLOSE_NO_EXTENSION:
          problem = "unsupported";
          break;
        default:
          problem = "internal-error";
          break;
        }
    }

  cockpit_channel_close (channel, problem);
}
static void
cockpit_web_socket_stream_close (CockpitChannel *channel,
                                 const gchar *problem)
{
  CockpitWebSocketStream *self = COCKPIT_WEB_SOCKET_STREAM (channel);

  self->closed = TRUE;
  if (self->client && web_socket_connection_get_ready_state (self->client) < WEB_SOCKET_STATE_CLOSING)
    {
      if (problem)
        web_socket_connection_close (self->client, WEB_SOCKET_CLOSE_ABNORMAL, problem);
      else
        web_socket_connection_close (self->client, WEB_SOCKET_CLOSE_NORMAL, "disconnected");
    }
  COCKPIT_CHANNEL_CLASS (cockpit_web_socket_stream_parent_class)->close (channel, problem);
}
static void
cockpit_web_socket_stream_prepare (CockpitChannel *channel)
{
  CockpitWebSocketStream *self = COCKPIT_WEB_SOCKET_STREAM (channel);
  CockpitConnectable *connectable = NULL;
  JsonObject *options;
  const gchar *path;
  gboolean started = FALSE;

  COCKPIT_CHANNEL_CLASS (cockpit_web_socket_stream_parent_class)->prepare (channel);

  if (self->closed)
    goto out;

  connectable = cockpit_channel_parse_stream (channel);
  if (!connectable)
    goto out;

  options = cockpit_channel_get_options (channel);
  if (!cockpit_json_get_string (options, "path", NULL, &path))
    {
      g_warning ("%s: bad \"path\" field in WebSocket stream request", self->origin);
      goto out;
    }
  else if (path == NULL || path[0] != '/')
    {
      g_warning ("%s: invalid or missing \"path\" field in WebSocket stream request", self->origin);
      goto out;
    }

  self->url = g_strdup_printf ("%s://%s%s", connectable->tls ? "wss" : "ws", connectable->name, path);
  self->origin = g_strdup_printf ("%s://%s", connectable->tls ? "https" : "http", connectable->name);

  /* Parsed elsewhere */
  self->binary = json_object_has_member (options, "binary");

  cockpit_connect_stream_full (connectable, NULL, on_socket_connect, g_object_ref (self));
  started = TRUE;

out:
  if (connectable)
    {
      cockpit_connectable_unref (connectable);
      if (!started)
        cockpit_channel_close (channel, "protocol-error");
    }
}
static void
cockpit_web_socket_stream_recv (CockpitChannel *channel,
                                GBytes *message)
{
  CockpitWebSocketStream *self = COCKPIT_WEB_SOCKET_STREAM (channel);
  WebSocketDataType type;
  WebSocketState state;

  /* Should never be called before cockpit_channel_ready() */
  g_return_if_fail (self->client != NULL);

  state = web_socket_connection_get_ready_state (self->client);
  g_return_if_fail (state >= WEB_SOCKET_STATE_OPEN);

  if (state == WEB_SOCKET_STATE_OPEN)
    {
      type = self->binary ? WEB_SOCKET_DATA_BINARY : WEB_SOCKET_DATA_TEXT;
      web_socket_connection_send (self->client, type, NULL, message);
    }
}
static void
on_socket_connect (GObject *object,
                   GAsyncResult *result,
                   gpointer user_data)
{
  CockpitWebSocketStream *self = COCKPIT_WEB_SOCKET_STREAM (user_data);
  CockpitChannel *channel = COCKPIT_CHANNEL (self);
  const gchar *problem = "protocol-error";
  gchar **protocols = NULL;
  GList *l, *names = NULL;
  GError *error = NULL;
  JsonObject *options;
  JsonObject *headers;
  const gchar *value;
  JsonNode *node;
  GIOStream *io;

  io = cockpit_connect_stream_finish (result, &error);
  if (error)
    {
      problem = cockpit_stream_problem (error, self->origin, "couldn't connect",
                                        cockpit_channel_close_options (channel));
      cockpit_channel_close (channel, problem);
      goto out;
    }

  options = cockpit_channel_get_options (channel);

  if (!cockpit_json_get_strv (options, "protocols", NULL, &protocols))
    {
      cockpit_channel_fail (channel, "protocol-error",
                            "%s: invalid \"protocol\" value in WebSocket stream request", self->origin);
      goto out;
    }

  if (G_IS_TLS_CONNECTION (io))
    {
      self->sig_accept_cert =  g_signal_connect (G_TLS_CONNECTION (io),
                                                 "accept-certificate",
                                                 G_CALLBACK (on_rejected_certificate),
                                                 self);
    }
  else
    {
      self->sig_accept_cert = 0;
    }

  self->client = web_socket_client_new_for_stream (self->url, self->origin, (const gchar **)protocols, io);

  node = json_object_get_member (options, "headers");
  if (node)
    {
      if (!JSON_NODE_HOLDS_OBJECT (node))
        {
          cockpit_channel_fail (channel, "protocol-error",
                                "%s: invalid \"headers\" field in WebSocket stream request", self->origin);
          goto out;
        }

      headers = json_node_get_object (node);
      names = json_object_get_members (headers);
      for (l = names; l != NULL; l = g_list_next (l))
        {
          node = json_object_get_member (headers, l->data);
          if (!node || !JSON_NODE_HOLDS_VALUE (node) || json_node_get_value_type (node) != G_TYPE_STRING)
            {
              cockpit_channel_fail (channel, "protocol-error",
                                    "%s: invalid header value in WebSocket stream request: %s",
                                    self->origin, (gchar *)l->data);
              goto out;
            }
          value = json_node_get_string (node);

          g_debug ("%s: sending header: %s %s", self->origin, (gchar *)l->data, value);
          web_socket_client_include_header (WEB_SOCKET_CLIENT (self->client), l->data, value);
        }
    }

  self->sig_open = g_signal_connect (self->client, "open", G_CALLBACK (on_web_socket_open), self);
  self->sig_message = g_signal_connect (self->client, "message", G_CALLBACK (on_web_socket_message), self);
  self->sig_closing = g_signal_connect (self->client, "closing", G_CALLBACK (on_web_socket_closing), self);
  self->sig_close = g_signal_connect (self->client, "close", G_CALLBACK (on_web_socket_close), self);
  self->sig_error = g_signal_connect (self->client, "error", G_CALLBACK (on_web_socket_error), self);

  problem = NULL;

out:
  g_clear_error (&error);
  g_strfreev (protocols);
  if (io)
    g_object_unref (io);
  g_list_free (names);
}