/*
* This file is part of Cockpit.
*
* Copyright (C) 2015 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 .
*/
#include "config.h"
#include "cockpitwebsocketstream.h"
#include "cockpitchannel.h"
#include "cockpitconnect.h"
#include "cockpitstream.h"
#include "common/cockpitjson.h"
#include "websocket/websocket.h"
#include
/**
* CockpitWebSocketStream:
*
* A #CockpitChannel that represents a WebSocket client
*
* The payload type for this channel is 'websocket-stream1'.
*/
#define COCKPIT_WEB_SOCKET_STREAM(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_WEB_SOCKET_STREAM, CockpitWebSocketStream))
typedef struct _CockpitWebSocketStream {
CockpitChannel parent;
/* The nickname for debugging and logging */
gchar *url;
gchar *origin;
/* The connection */
WebSocketConnection *client;
gulong sig_open;
gulong sig_message;
gulong sig_closing;
gulong sig_close;
gulong sig_error;
gulong sig_accept_cert;
gboolean binary;
gboolean closed;
gushort last_error_code;
} CockpitWebSocketStream;
typedef struct {
CockpitChannelClass parent_class;
} CockpitWebSocketStreamClass;
G_DEFINE_TYPE (CockpitWebSocketStream, cockpit_web_socket_stream, COCKPIT_TYPE_CHANNEL);
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 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
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_init (CockpitWebSocketStream *self)
{
}
static gboolean
on_rejected_certificate (GTlsConnection *conn,
GTlsCertificate *peer_cert,
GTlsCertificateFlags errors,
gpointer user_data)
{
CockpitChannel *channel = user_data;
JsonObject *close_options = NULL; // owned by channel
gchar *pem_data = NULL;
g_return_val_if_fail (peer_cert != NULL, FALSE);
g_object_get (peer_cert, "certificate-pem", &pem_data, NULL);
close_options = cockpit_channel_close_options (channel);
json_object_set_string_member (close_options, "rejected-certificate", pem_data);
g_free (pem_data);
return FALSE;
}
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
on_web_socket_message (WebSocketConnection *connection,
WebSocketDataType type,
GBytes *message,
gpointer user_data)
{
CockpitChannel *channel = COCKPIT_CHANNEL (user_data);
cockpit_channel_send (channel, message, type == WEB_SOCKET_DATA_TEXT);
}
static gboolean
on_web_socket_closing (WebSocketConnection *connection,
gpointer user_data)
{
CockpitChannel *channel = COCKPIT_CHANNEL (user_data);
cockpit_channel_control (channel, "done", NULL);
return TRUE;
}
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 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
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);
}
static void
cockpit_web_socket_stream_prepare (CockpitChannel *channel)
{
CockpitWebSocketStream *self = COCKPIT_WEB_SOCKET_STREAM (channel);
CockpitConnectable *connectable = NULL;
JsonObject *options;
const gchar *path;
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))
{
cockpit_channel_fail (channel, "protocol-error",
"%s: bad \"path\" field in WebSocket stream request", self->origin);
goto out;
}
else if (path == NULL || path[0] != '/')
{
cockpit_channel_fail (channel, "protocol-error",
"%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));
out:
if (connectable)
cockpit_connectable_unref (connectable);
}
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 void
cockpit_web_socket_stream_constructed (GObject *object)
{
static const gchar *caps[] = { "tls-certificates", "address", NULL };
G_OBJECT_CLASS (cockpit_web_socket_stream_parent_class)->constructed (object);
g_object_set (object, "capabilities", &caps, NULL);
}
static void
cockpit_web_socket_stream_class_init (CockpitWebSocketStreamClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
CockpitChannelClass *channel_class = COCKPIT_CHANNEL_CLASS (klass);
gobject_class->dispose = cockpit_web_socket_stream_dispose;
gobject_class->finalize = cockpit_web_socket_stream_finalize;
gobject_class->constructed = cockpit_web_socket_stream_constructed;
channel_class->prepare = cockpit_web_socket_stream_prepare;
channel_class->control = cockpit_web_socket_stream_control;
channel_class->recv = cockpit_web_socket_stream_recv;
channel_class->close = cockpit_web_socket_stream_close;
}