/*
* 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 .
*/
#include "config.h"
#include "websocketclient.h"
#include "websocketprivate.h"
#include
enum {
PROP_0,
PROP_ORIGIN,
PROP_PROTOCOLS,
};
struct _WebSocketClient
{
WebSocketConnection parent;
gboolean handshake_started;
gchar *origin;
gchar **possible_protocols;
gpointer accept_key;
GHashTable *include_headers;
GHashTable *response_headers;
GCancellable *cancellable;
GSource *idle_start;
};
struct _WebSocketClientClass
{
WebSocketConnectionClass parent;
};
G_DEFINE_TYPE (WebSocketClient, web_socket_client, WEB_SOCKET_TYPE_CONNECTION);
static void
web_socket_client_init (WebSocketClient *self)
{
}
static void
protocol_error_and_close (WebSocketConnection *conn)
{
GError *error = g_error_new_literal (WEB_SOCKET_ERROR,
WEB_SOCKET_CLOSE_PROTOCOL,
"Received invalid WebSocket handshake from the server");
_web_socket_connection_error_and_close (conn, error, TRUE);
}
static gboolean
verify_handshake_rfc6455 (WebSocketClient *self,
WebSocketConnection *conn,
GHashTable *headers)
{
const gchar *value;
/*
* This is a client verifying a handshake response it's received
* from the server.
*/
if (!_web_socket_util_header_equals (headers, "Upgrade", "websocket") ||
!_web_socket_util_header_contains (headers, "Connection", "upgrade") ||
!_web_socket_connection_choose_protocol (conn, (const gchar **)self->possible_protocols,
g_hash_table_lookup (headers, "Sec-Websocket-Protocol")) ||
!_web_socket_util_header_empty (headers, "Sec-WebSocket-Extensions"))
{
protocol_error_and_close (conn);
return FALSE;
}
/*
* We filled in accept_key when we did a handshake request
* earlier in request_handshake_rfc6455().
*/
value = g_hash_table_lookup (headers, "Sec-WebSocket-Accept");
if (value == NULL || self->accept_key == NULL ||
g_ascii_strcasecmp (self->accept_key, value))
{
g_message ("received invalid or missing Sec-WebSocket-Accept header: %s", value);
protocol_error_and_close (conn);
return FALSE;
}
g_debug ("verified rfc6455 handshake");
return TRUE;
}
static gboolean
parse_handshake_response (WebSocketClient *self,
WebSocketConnection *conn,
GByteArray *incoming)
{
GHashTable *headers;
gchar *reason;
gboolean verified;
guint status;
GError *error = NULL;
gssize in1, in2;
gssize consumed;
/* Parse the handshake response received from the server */
in1 = web_socket_util_parse_status_line ((const gchar *)incoming->data,
incoming->len, NULL, &status, &reason);
if (in1 < 0)
{
g_message ("received invalid status line");
protocol_error_and_close (conn);
}
else if (in1 == 0)
g_debug ("waiting for more handshake data");
if (in1 <= 0)
return FALSE;
in2 = web_socket_util_parse_headers ((const gchar *)incoming->data + in1,
incoming->len - in1, &headers);
if (in2 < 0)
{
g_message ("received invalid response headers");
protocol_error_and_close (conn);
}
else if (in2 == 0)
g_debug ("waiting for more handshake data");
if (in2 <= 0)
{
g_free (reason);
return FALSE;
}
consumed = in1 + in2;
if (self->response_headers)
g_hash_table_unref (self->response_headers);
self->response_headers = headers;
/*
* TODO: We could handle the following codes here:
* 401: authentication
* 3xx: redirect
*/
if (status == 101)
{
verified = verify_handshake_rfc6455 (self, conn, headers);
if (verified)
{
/* Handshake is successful */
g_debug ("open: handshake completed");
}
}
else
{
verified = FALSE;
g_message ("received unexpected status: %d %s", status, reason);
if (reason == NULL)
error = g_error_new (WEB_SOCKET_ERROR,
WEB_SOCKET_CLOSE_PROTOCOL,
"Handshake failed: %u", status);
else
error = g_error_new (WEB_SOCKET_ERROR,
WEB_SOCKET_CLOSE_PROTOCOL,
"%s", reason);
_web_socket_connection_error_and_close (conn, error, FALSE);
}
g_free (reason);
if (consumed > 0)
g_byte_array_remove_range (incoming, 0, consumed);
return verified;
}
static void
include_custom_headers (WebSocketClient *self,
GString *handshake)
{
GHashTableIter iter;
gpointer name;
gpointer value;
if (!self->include_headers)
return;
g_hash_table_iter_init (&iter, self->include_headers);
while (g_hash_table_iter_next (&iter, &name, &value))
{
if (value == NULL)
continue;
g_debug ("including custom header: %s: %s", (gchar *)name, (gchar *)value);
g_string_append_printf (handshake, "%s: %s\r\n",
(gchar *)name, (gchar *)value);
}
}
static void
request_handshake_rfc6455 (WebSocketClient *self,
WebSocketConnection *conn,
const gchar *host,
const gchar *path)
{
gchar *key;
gchar *protocols;
GString *handshake;
guint32 raw[4];
gsize len;
raw[0] = g_random_int ();
raw[1] = g_random_int ();
raw[2] = g_random_int ();
raw[3] = g_random_int ();
G_STATIC_ASSERT (sizeof (raw) == 16);
key = g_base64_encode ((const guchar *)raw, sizeof (raw));
/* Save this for verify_handshake_rfc6455() */
g_free (self->accept_key);
self->accept_key = _web_socket_complete_accept_key_rfc6455 (key);
handshake = g_string_new ("");
g_string_printf (handshake, "GET %s HTTP/1.1\r\n"
"Host: %s\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Key: %s\r\n"
"Sec-WebSocket-Version: 13\r\n",
path, host, key);
/* RFC 6454 talks about 'null' */
g_string_append_printf (handshake, "Origin: %s\r\n", self->origin ? self->origin : "null");
if (self->possible_protocols)
{
protocols = g_strjoinv (", ", self->possible_protocols);
g_string_append_printf (handshake, "Sec-WebSocket-Protocol: %s\r\n", protocols);
g_free (protocols);
}
include_custom_headers (self, handshake);
g_string_append (handshake, "\r\n");
g_free (key);
len = handshake->len;
_web_socket_connection_queue (conn, WEB_SOCKET_QUEUE_URGENT,
g_string_free (handshake, FALSE), len, 0);
g_debug ("queued rfc6455 handshake request");
}
static void
request_handshake (WebSocketClient *self,
WebSocketConnection *conn)
{
GError *error = NULL;
const gchar *url;
gchar *host;
gchar *path;
self->handshake_started = TRUE;
url = web_socket_connection_get_url (conn);
if (!_web_socket_util_parse_url (url, NULL, &host, &path, &error))
{
_web_socket_connection_error_and_close (conn, error, TRUE);
return;
}
request_handshake_rfc6455 (self, conn, host, path);
g_free (host);
g_free (path);
}
static gpointer
on_idle_do_handshake (gpointer user_data)
{
WebSocketClient *self = WEB_SOCKET_CLIENT (user_data);
WebSocketConnection *conn = WEB_SOCKET_CONNECTION (user_data);
g_source_unref (self->idle_start);
self->idle_start = NULL;
request_handshake (self, conn);
return FALSE;
}
static void
on_connect_to_uri (GObject *source,
GAsyncResult *result,
gpointer user_data)
{
WebSocketClient *self = WEB_SOCKET_CLIENT (user_data);
WebSocketConnection *conn = WEB_SOCKET_CONNECTION (user_data);
GSocketConnection *connection;
GError *error = NULL;
connection = g_socket_client_connect_to_uri_finish (G_SOCKET_CLIENT (source),
result, &error);
if (error == NULL)
{
_web_socket_connection_take_io_stream (conn, G_IO_STREAM (connection));
request_handshake (self, conn);
}
else if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
{
g_error_free (error);
}
else
{
_web_socket_connection_error_and_close (conn, error, TRUE);
}
g_object_unref (self);
}
static void
web_socket_client_constructed (GObject *object)
{
WebSocketClient *self = WEB_SOCKET_CLIENT (object);
WebSocketConnection *conn = WEB_SOCKET_CONNECTION (object);
GSocketClient *client;
const gchar *url;
guint16 default_port;
gchar *scheme;
G_OBJECT_CLASS (web_socket_client_parent_class)->constructed (object);
if (web_socket_connection_get_io_stream (conn))
{
/* Start handshake from the main context */
self->idle_start = g_idle_source_new ();
g_source_set_priority (self->idle_start, G_PRIORITY_HIGH);
g_source_set_callback (self->idle_start, (GSourceFunc)on_idle_do_handshake,
self, NULL);
g_source_attach (self->idle_start, _web_socket_connection_get_main_context (conn));
}
else
{
client = g_socket_client_new ();
self->cancellable = g_cancellable_new ();
url = web_socket_connection_get_url (WEB_SOCKET_CONNECTION (self));
scheme = g_uri_parse_scheme (url);
if (scheme && (g_str_equal (scheme, "wss") || g_str_equal (scheme, "https")))
{
g_socket_client_set_tls (client, TRUE);
default_port = 443;
}
else
{
default_port = 80;
}
g_free (scheme);
g_socket_client_connect_to_uri_async (client, url, default_port,
self->cancellable, on_connect_to_uri,
g_object_ref (self));
g_object_unref (client);
}
}
static gboolean
web_socket_client_handshake (WebSocketConnection *conn,
GByteArray *incoming)
{
WebSocketClient *self = WEB_SOCKET_CLIENT (conn);
return parse_handshake_response (self, conn, incoming);
}
static void
web_socket_client_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
WebSocketClient *self = WEB_SOCKET_CLIENT (object);
switch (prop_id)
{
case PROP_ORIGIN:
g_return_if_fail (self->origin == NULL);
self->origin = g_value_dup_string (value);
break;
case PROP_PROTOCOLS:
g_return_if_fail (self->handshake_started == FALSE);
g_strfreev (self->possible_protocols);
self->possible_protocols = g_value_dup_boxed (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
web_socket_client_close (WebSocketConnection *conn)
{
WebSocketClient *self = WEB_SOCKET_CLIENT (conn);
if (self->cancellable)
g_cancellable_cancel (self->cancellable);
if (self->idle_start)
{
g_source_destroy (self->idle_start);
g_source_unref (self->idle_start);
}
}
static void
web_socket_client_finalize (GObject *object)
{
WebSocketClient *self = WEB_SOCKET_CLIENT (object);
g_strfreev (self->possible_protocols);
g_free (self->origin);
g_free (self->accept_key);
if (self->include_headers)
g_hash_table_unref (self->include_headers);
if (self->response_headers)
g_hash_table_unref (self->response_headers);
if (self->cancellable)
g_object_unref (self->cancellable);
g_assert (self->idle_start == NULL);
G_OBJECT_CLASS (web_socket_client_parent_class)->finalize (object);
}
static void
web_socket_client_class_init (WebSocketClientClass *klass)
{
WebSocketConnectionClass *conn_class = WEB_SOCKET_CONNECTION_CLASS (klass);
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->constructed = web_socket_client_constructed;
object_class->set_property = web_socket_client_set_property;
object_class->finalize = web_socket_client_finalize;
conn_class->server_behavior = FALSE;
conn_class->handshake = web_socket_client_handshake;
conn_class->close = web_socket_client_close;
/**
* WebSocketClient:origin:
*
* The WebSocket origin. Client WebSockets will send this to the server. If
* set on a server, then only clients with the matching origin will be accepted.
*/
g_object_class_install_property (object_class, PROP_ORIGIN,
g_param_spec_string ("origin", "Origin", "The WebSocket origin", NULL,
G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
/**
* WebSocketClient:protocols:
*
* The possible protocols to negotiate with the peer.
*/
g_object_class_install_property (object_class, PROP_PROTOCOLS,
g_param_spec_boxed ("protocols", "Protocol", "The desired WebSocket protocols", G_TYPE_STRV,
G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
}
/**
* web_socket_client_new:
* @url: the url address of the WebSocket
* @origin: (allow-none): the origin to report to server
* @protocols: (allow-none): possible protocols to negotiate
*
* Create a new client side WebSocket connection to communicate with a server.
*
* The WebSocket will establish a connection to the server using HTTP
* or HTTPS at the address specified in the @url.
*
* If @protocols are specified then these are used to negotiate a protocol
* with the client.
*
* Returns: (transfer full): a new WebSocket
*/
WebSocketConnection *
web_socket_client_new (const gchar *url,
const gchar *origin,
const gchar **protocols)
{
return g_object_new (WEB_SOCKET_TYPE_CLIENT,
"url", url,
"origin", origin,
"protocols", protocols,
NULL);
}
/**
* web_socket_client_new_for_stream:
* @url: the url address of the WebSocket
* @origin: (allow-none): the origin to report to server
* @protocols: (allow-none): possible protocols to negotiate
* @io_stream: the IO stream to communicate over
*
* Create a new client side WebSocket connection to communicate with a server.
*
* Use this function if you've already opened up a IO stream to the server
* and now wish to communicate over it. The input and output streams of the
* @io_stream must be pollable.
*
* If @protocols are specified then these are used to negotiate a protocol
* with the client.
*
* Returns: (transfer full): a new WebSocket
*/
WebSocketConnection *
web_socket_client_new_for_stream (const gchar *url,
const gchar *origin,
const gchar **protocols,
GIOStream *io_stream)
{
return g_object_new (WEB_SOCKET_TYPE_CLIENT,
"url", url,
"origin", origin,
"protocols", protocols,
"io-stream", io_stream,
NULL);
}
/**
* web_socket_client_include_header:
* @self: the client
* @name: the header name
* @value: the header value
*
* Add an HTTP header (eg: for authentication) to the
* HTTP request.
*/
void
web_socket_client_include_header (WebSocketClient *self,
const gchar *name,
const gchar *value)
{
g_return_if_fail (WEB_SOCKET_IS_CLIENT (self));
g_return_if_fail (self->handshake_started == FALSE);
if (!self->include_headers)
self->include_headers = web_socket_util_new_headers ();
g_hash_table_insert (self->include_headers,
g_strdup (name), g_strdup (value));
}
GHashTable *
web_socket_client_get_headers (WebSocketClient *self)
{
g_return_val_if_fail (WEB_SOCKET_IS_CLIENT (self), NULL);
return self->response_headers;
}