diff options
author | Patrick Griffis <pgriffis@igalia.com> | 2020-03-10 13:58:38 -0700 |
---|---|---|
committer | Patrick Griffis <pgriffis@igalia.com> | 2020-09-19 15:41:24 -0700 |
commit | 5dcc1a9cffb351eec1e882b82c4dcf08145fa280 (patch) | |
tree | 0a08ae0d46408914453892d26fff7f9e410587a2 /libsoup/websocket | |
parent | 1b3b31371f01af8b6081e5358273b4b28ff3489b (diff) | |
download | libsoup-5dcc1a9cffb351eec1e882b82c4dcf08145fa280.tar.gz |
Reorganize source tree
Diffstat (limited to 'libsoup/websocket')
-rw-r--r-- | libsoup/websocket/soup-websocket-connection.c | 2214 | ||||
-rw-r--r-- | libsoup/websocket/soup-websocket-connection.h | 141 | ||||
-rw-r--r-- | libsoup/websocket/soup-websocket-extension-deflate.c | 497 | ||||
-rw-r--r-- | libsoup/websocket/soup-websocket-extension-deflate.h | 49 | ||||
-rw-r--r-- | libsoup/websocket/soup-websocket-extension-manager-private.h | 30 | ||||
-rw-r--r-- | libsoup/websocket/soup-websocket-extension-manager.c | 180 | ||||
-rw-r--r-- | libsoup/websocket/soup-websocket-extension-manager.h | 50 | ||||
-rw-r--r-- | libsoup/websocket/soup-websocket-extension.c | 221 | ||||
-rw-r--r-- | libsoup/websocket/soup-websocket-extension.h | 100 | ||||
-rw-r--r-- | libsoup/websocket/soup-websocket.c | 1030 | ||||
-rw-r--r-- | libsoup/websocket/soup-websocket.h | 117 |
11 files changed, 4629 insertions, 0 deletions
diff --git a/libsoup/websocket/soup-websocket-connection.c b/libsoup/websocket/soup-websocket-connection.c new file mode 100644 index 00000000..a4095e1c --- /dev/null +++ b/libsoup/websocket/soup-websocket-connection.c @@ -0,0 +1,2214 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * soup-websocket-connection.c: This file was originally part of Cockpit. + * + * Copyright 2013, 2014 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 this library; If not, see <http://www.gnu.org/licenses/>. + */ + +#include "config.h" + +#include <string.h> + +#include "soup-websocket-connection.h" +#include "soup-enum-types.h" +#include "soup-io-stream.h" +#include "soup-uri.h" +#include "soup-websocket-extension.h" + +/* + * SECTION:websocketconnection + * @title: SoupWebsocketConnection + * @short_description: A WebSocket connection + * + * A #SoupWebsocketConnection is a WebSocket connection to a peer. + * This API is modeled after the W3C API for interacting with + * WebSockets. + * + * The #SoupWebsocketConnection:state property will indicate the + * state of the connection. + * + * Use soup_websocket_connection_send() to send a message to the peer. + * When a message is received the #SoupWebsocketConnection::message + * signal will fire. + * + * The soup_websocket_connection_close() function will perform an + * orderly close of the connection. The + * #SoupWebsocketConnection::closed signal will fire once the + * connection closes, whether it was initiated by this side or the + * peer. + * + * Connect to the #SoupWebsocketConnection::closing signal to detect + * when either peer begins closing the connection. + */ + +/** + * SoupWebsocketConnection: + * + * A class representing a WebSocket connection. + * + * Since: 2.50 + */ + +/** + * SoupWebsocketConnectionClass: + * @message: default handler for the #SoupWebsocketConnection::message signal + * @error: default handler for the #SoupWebsocketConnection::error signal + * @closing: the default handler for the #SoupWebsocketConnection:closing signal + * @closed: default handler for the #SoupWebsocketConnection::closed signal + * @pong: default handler for the #SoupWebsocketConnection::pong signal + * + * The abstract base class for #SoupWebsocketConnection + * + * Since: 2.50 + */ + +enum { + PROP_0, + PROP_IO_STREAM, + PROP_CONNECTION_TYPE, + PROP_URI, + PROP_ORIGIN, + PROP_PROTOCOL, + PROP_STATE, + PROP_MAX_INCOMING_PAYLOAD_SIZE, + PROP_KEEPALIVE_INTERVAL, + PROP_EXTENSIONS +}; + +enum { + MESSAGE, + ERROR, + CLOSING, + CLOSED, + PONG, + NUM_SIGNALS +}; + +static guint signals[NUM_SIGNALS] = { 0, }; + +typedef enum { + SOUP_WEBSOCKET_QUEUE_NORMAL = 0, + SOUP_WEBSOCKET_QUEUE_URGENT = 1 << 0, + SOUP_WEBSOCKET_QUEUE_LAST = 1 << 1, +} SoupWebsocketQueueFlags; + +typedef struct { + GBytes *data; + gsize sent; + gsize amount; + SoupWebsocketQueueFlags flags; + gboolean pending; +} Frame; + +struct _SoupWebsocketConnectionPrivate { + GIOStream *io_stream; + SoupWebsocketConnectionType connection_type; + SoupURI *uri; + char *origin; + char *protocol; + guint64 max_incoming_payload_size; + guint keepalive_interval; + + gushort peer_close_code; + char *peer_close_data; + gboolean close_sent; + gboolean close_received; + gboolean dirty_close; + GSource *close_timeout; + + GMainContext *main_context; + + gboolean io_closing; + gboolean io_closed; + + GPollableInputStream *input; + GSource *input_source; + GByteArray *incoming; + + GPollableOutputStream *output; + GSource *output_source; + GQueue outgoing; + + /* Current message being assembled */ + guint8 message_opcode; + GByteArray *message_data; + + GSource *keepalive_timeout; + + GList *extensions; +}; + +#define MAX_INCOMING_PAYLOAD_SIZE_DEFAULT 128 * 1024 +#define READ_BUFFER_SIZE 1024 +#define MASK_LENGTH 4 + +G_DEFINE_TYPE_WITH_PRIVATE (SoupWebsocketConnection, soup_websocket_connection, G_TYPE_OBJECT) + +static void queue_frame (SoupWebsocketConnection *self, SoupWebsocketQueueFlags flags, + gpointer data, gsize len, gsize amount); + +static void emit_error_and_close (SoupWebsocketConnection *self, + GError *error, gboolean prejudice); + +static void protocol_error_and_close (SoupWebsocketConnection *self); + +static gboolean on_web_socket_input (GObject *pollable_stream, + gpointer user_data); +static gboolean on_web_socket_output (GObject *pollable_stream, + gpointer user_data); + +/* Code below is based on g_utf8_validate() implementation, + * but handling NULL characters as valid, as expected by + * WebSockets and compliant with RFC 3629. + */ +#define VALIDATE_BYTE(mask, expect) \ + G_STMT_START { \ + if (G_UNLIKELY((*(guchar *)p & (mask)) != (expect))) \ + return FALSE; \ + } G_STMT_END + +/* see IETF RFC 3629 Section 4 */ +static gboolean +utf8_validate (const char *str, + gsize max_len) + +{ + const gchar *p; + + for (p = str; ((p - str) < max_len); p++) { + if (*(guchar *)p < 128) + /* done */; + else { + if (*(guchar *)p < 0xe0) { /* 110xxxxx */ + if (G_UNLIKELY (max_len - (p - str) < 2)) + return FALSE; + + if (G_UNLIKELY (*(guchar *)p < 0xc2)) + return FALSE; + } else { + if (*(guchar *)p < 0xf0) { /* 1110xxxx */ + if (G_UNLIKELY (max_len - (p - str) < 3)) + return FALSE; + + switch (*(guchar *)p++ & 0x0f) { + case 0: + VALIDATE_BYTE(0xe0, 0xa0); /* 0xa0 ... 0xbf */ + break; + case 0x0d: + VALIDATE_BYTE(0xe0, 0x80); /* 0x80 ... 0x9f */ + break; + default: + VALIDATE_BYTE(0xc0, 0x80); /* 10xxxxxx */ + } + } else if (*(guchar *)p < 0xf5) { /* 11110xxx excluding out-of-range */ + if (G_UNLIKELY (max_len - (p - str) < 4)) + return FALSE; + + switch (*(guchar *)p++ & 0x07) { + case 0: + VALIDATE_BYTE(0xc0, 0x80); /* 10xxxxxx */ + if (G_UNLIKELY((*(guchar *)p & 0x30) == 0)) + return FALSE; + break; + case 4: + VALIDATE_BYTE(0xf0, 0x80); /* 0x80 ... 0x8f */ + break; + default: + VALIDATE_BYTE(0xc0, 0x80); /* 10xxxxxx */ + } + p++; + VALIDATE_BYTE(0xc0, 0x80); /* 10xxxxxx */ + } else { + return FALSE; + } + } + + p++; + VALIDATE_BYTE(0xc0, 0x80); /* 10xxxxxx */ + } + } + + return TRUE; +} + +#undef VALIDATE_BYTE + +static void +frame_free (gpointer data) +{ + Frame *frame = data; + + if (frame) { + g_bytes_unref (frame->data); + g_slice_free (Frame, frame); + } +} + +static void +soup_websocket_connection_init (SoupWebsocketConnection *self) +{ + SoupWebsocketConnectionPrivate *pv; + + pv = self->pv = soup_websocket_connection_get_instance_private (self); + + pv->incoming = g_byte_array_sized_new (1024); + g_queue_init (&pv->outgoing); + pv->main_context = g_main_context_ref_thread_default (); +} + +static void +on_iostream_closed (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + SoupWebsocketConnection *self = user_data; + SoupWebsocketConnectionPrivate *pv = self->pv; + GError *error = NULL; + + /* We treat connection as closed even if close fails */ + pv->io_closed = TRUE; + g_io_stream_close_finish (pv->io_stream, result, &error); + + if (error) { + g_debug ("error closing web socket stream: %s", error->message); + if (!pv->dirty_close) + g_signal_emit (self, signals[ERROR], 0, error); + pv->dirty_close = TRUE; + g_error_free (error); + } + + g_assert (soup_websocket_connection_get_state (self) == SOUP_WEBSOCKET_STATE_CLOSED); + g_debug ("closed: completed io stream close"); + g_signal_emit (self, signals[CLOSED], 0); + + g_object_unref (self); +} + +static void +soup_websocket_connection_start_input_source (SoupWebsocketConnection *self) +{ + SoupWebsocketConnectionPrivate *pv = self->pv; + + if (pv->input_source) + return; + + pv->input_source = g_pollable_input_stream_create_source (pv->input, NULL); + g_source_set_callback (pv->input_source, (GSourceFunc)on_web_socket_input, self, NULL); + g_source_attach (pv->input_source, pv->main_context); +} + +static void +soup_websocket_connection_stop_input_source (SoupWebsocketConnection *self) +{ + SoupWebsocketConnectionPrivate *pv = self->pv; + + if (pv->input_source) { + g_debug ("stopping input source"); + g_source_destroy (pv->input_source); + g_source_unref (pv->input_source); + pv->input_source = NULL; + } +} + +static void +soup_websocket_connection_start_output_source (SoupWebsocketConnection *self) +{ + SoupWebsocketConnectionPrivate *pv = self->pv; + + if (pv->output_source) + return; + + pv->output_source = g_pollable_output_stream_create_source (pv->output, NULL); + g_source_set_callback (pv->output_source, (GSourceFunc)on_web_socket_output, self, NULL); + g_source_attach (pv->output_source, pv->main_context); +} + +static void +soup_websocket_connection_stop_output_source (SoupWebsocketConnection *self) +{ + SoupWebsocketConnectionPrivate *pv = self->pv; + + if (pv->output_source) { + g_debug ("stopping output source"); + g_source_destroy (pv->output_source); + g_source_unref (pv->output_source); + pv->output_source = NULL; + } +} + +static void +keepalive_stop_timeout (SoupWebsocketConnection *self) +{ + SoupWebsocketConnectionPrivate *pv = self->pv; + + if (pv->keepalive_timeout) { + g_source_destroy (pv->keepalive_timeout); + g_source_unref (pv->keepalive_timeout); + pv->keepalive_timeout = NULL; + } +} + +static void +close_io_stop_timeout (SoupWebsocketConnection *self) +{ + SoupWebsocketConnectionPrivate *pv = self->pv; + + if (pv->close_timeout) { + g_source_destroy (pv->close_timeout); + g_source_unref (pv->close_timeout); + pv->close_timeout = NULL; + } +} + +static void +close_io_stream (SoupWebsocketConnection *self) +{ + SoupWebsocketConnectionPrivate *pv = self->pv; + + keepalive_stop_timeout (self); + close_io_stop_timeout (self); + + if (!pv->io_closing) { + soup_websocket_connection_stop_input_source (self); + soup_websocket_connection_stop_output_source (self); + pv->io_closing = TRUE; + g_debug ("closing io stream"); + g_io_stream_close_async (pv->io_stream, G_PRIORITY_DEFAULT, + NULL, on_iostream_closed, g_object_ref (self)); + } + + g_object_notify (G_OBJECT (self), "state"); +} + +static void +shutdown_wr_io_stream (SoupWebsocketConnection *self) +{ + SoupWebsocketConnectionPrivate *pv = self->pv; + GSocket *socket; + GIOStream *base_iostream; + GError *error = NULL; + + soup_websocket_connection_stop_output_source (self); + + base_iostream = SOUP_IS_IO_STREAM (pv->io_stream) ? + soup_io_stream_get_base_iostream (SOUP_IO_STREAM (pv->io_stream)) : + pv->io_stream; + + if (G_IS_SOCKET_CONNECTION (base_iostream)) { + socket = g_socket_connection_get_socket (G_SOCKET_CONNECTION (base_iostream)); + g_socket_shutdown (socket, FALSE, TRUE, &error); + if (error != NULL) { + g_debug ("error shutting down io stream: %s", error->message); + g_error_free (error); + } + } + + g_object_notify (G_OBJECT (self), "state"); +} + +static gboolean +on_timeout_close_io (gpointer user_data) +{ + SoupWebsocketConnection *self = SOUP_WEBSOCKET_CONNECTION (user_data); + SoupWebsocketConnectionPrivate *pv = self->pv; + + pv->close_timeout = 0; + + g_debug ("peer did not close io when expected"); + close_io_stream (self); + + return FALSE; +} + +static void +close_io_after_timeout (SoupWebsocketConnection *self) +{ + SoupWebsocketConnectionPrivate *pv = self->pv; + const int timeout = 5; + + if (pv->close_timeout) + return; + + g_debug ("waiting %d seconds for peer to close io", timeout); + pv->close_timeout = g_timeout_source_new_seconds (timeout); + g_source_set_callback (pv->close_timeout, on_timeout_close_io, self, NULL); + g_source_attach (pv->close_timeout, pv->main_context); +} + +static void +xor_with_mask (const guint8 *mask, + guint8 *data, + gsize len) +{ + gsize n; + + /* Do the masking */ + for (n = 0; n < len; n++) + data[n] ^= mask[n & 3]; +} + +static void +send_message (SoupWebsocketConnection *self, + SoupWebsocketQueueFlags flags, + guint8 opcode, + const guint8 *data, + gsize length) +{ + gsize buffered_amount; + GByteArray *bytes; + gsize frame_len; + guint8 *outer; + guint8 mask_offset; + GBytes *filtered_bytes; + GList *l; + GError *error = NULL; + + if (!(soup_websocket_connection_get_state (self) == SOUP_WEBSOCKET_STATE_OPEN)) { + g_debug ("Ignoring message since the connection is closed or is closing"); + return; + } + + bytes = g_byte_array_sized_new (14 + length); + outer = bytes->data; + outer[0] = 0x80 | opcode; + + filtered_bytes = g_bytes_new_static (data, length); + for (l = self->pv->extensions; l != NULL; l = g_list_next (l)) { + SoupWebsocketExtension *extension; + + extension = (SoupWebsocketExtension *)l->data; + filtered_bytes = soup_websocket_extension_process_outgoing_message (extension, outer, filtered_bytes, &error); + if (error) { + g_byte_array_free (bytes, TRUE); + emit_error_and_close (self, error, FALSE); + return; + } + } + + data = g_bytes_get_data (filtered_bytes, &length); + buffered_amount = length; + + /* If control message, check payload size */ + if (opcode & 0x08) { + if (length > 125) { + g_warning ("WebSocket control message payload exceeds size limit"); + protocol_error_and_close (self); + g_byte_array_free (bytes, TRUE); + g_bytes_unref (filtered_bytes); + return; + } + + buffered_amount = 0; + } + + if (length < 126) { + outer[1] = (0xFF & length); /* mask | 7-bit-len */ + bytes->len = 2; + } else if (length < 65536) { + outer[1] = 126; /* mask | 16-bit-len */ + outer[2] = (length >> 8) & 0xFF; + outer[3] = (length >> 0) & 0xFF; + bytes->len = 4; + } else { + outer[1] = 127; /* mask | 64-bit-len */ +#if GLIB_SIZEOF_SIZE_T > 4 + outer[2] = (length >> 56) & 0xFF; + outer[3] = (length >> 48) & 0xFF; + outer[4] = (length >> 40) & 0xFF; + outer[5] = (length >> 32) & 0xFF; +#else + outer[2] = outer[3] = outer[4] = outer[5] = 0; +#endif + outer[6] = (length >> 24) & 0xFF; + outer[7] = (length >> 16) & 0xFF; + outer[8] = (length >> 8) & 0xFF; + outer[9] = (length >> 0) & 0xFF; + bytes->len = 10; + } + + /* The server side doesn't need to mask, so we don't. There's + * probably a client somewhere that's not expecting it. + */ + if (self->pv->connection_type == SOUP_WEBSOCKET_CONNECTION_CLIENT) { + guint32 rnd = g_random_int (); + outer[1] |= 0x80; + mask_offset = bytes->len; + memcpy (outer + mask_offset, &rnd, sizeof (rnd)); + bytes->len += MASK_LENGTH; + } + + g_byte_array_append (bytes, data, length); + + if (self->pv->connection_type == SOUP_WEBSOCKET_CONNECTION_CLIENT) + xor_with_mask (bytes->data + mask_offset, bytes->data + mask_offset + MASK_LENGTH, length); + + frame_len = bytes->len; + queue_frame (self, flags, g_byte_array_free (bytes, FALSE), + frame_len, buffered_amount); + g_bytes_unref (filtered_bytes); + g_debug ("queued %d frame of len %u", (int)opcode, (guint)frame_len); +} + +static void +send_close (SoupWebsocketConnection *self, + SoupWebsocketQueueFlags flags, + gushort code, + const char *reason) +{ + /* Note that send_message truncates as expected */ + char buffer[128]; + gsize len = 0; + + if (code != 0) { + buffer[len++] = code >> 8; + buffer[len++] = code & 0xFF; + if (reason) + len += g_strlcpy (buffer + len, reason, sizeof (buffer) - len); + } + + send_message (self, flags, 0x08, (guint8 *)buffer, len); + self->pv->close_sent = TRUE; + + keepalive_stop_timeout (self); +} + +static void +emit_error_and_close (SoupWebsocketConnection *self, + GError *error, + gboolean prejudice) +{ + gboolean ignore = FALSE; + gushort code; + + if (soup_websocket_connection_get_state (self) == SOUP_WEBSOCKET_STATE_CLOSED) { + g_error_free (error); + return; + } + + if (error && error->domain == SOUP_WEBSOCKET_ERROR) + code = error->code; + else + code = SOUP_WEBSOCKET_CLOSE_GOING_AWAY; + + self->pv->dirty_close = TRUE; + g_signal_emit (self, signals[ERROR], 0, error); + g_error_free (error); + + /* If already closing, just ignore this stuff */ + switch (soup_websocket_connection_get_state (self)) { + case SOUP_WEBSOCKET_STATE_CLOSED: + ignore = TRUE; + break; + case SOUP_WEBSOCKET_STATE_CLOSING: + ignore = !prejudice; + break; + default: + break; + } + + if (ignore) { + g_debug ("already closing/closed, ignoring error"); + } else if (prejudice) { + g_debug ("forcing close due to error"); + close_io_stream (self); + } else { + g_debug ("requesting close due to error"); + send_close (self, SOUP_WEBSOCKET_QUEUE_URGENT | SOUP_WEBSOCKET_QUEUE_LAST, code, NULL); + } +} + +static void +protocol_error_and_close_full (SoupWebsocketConnection *self, + gboolean prejudice) +{ + GError *error; + + error = g_error_new_literal (SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_CLOSE_PROTOCOL_ERROR, + self->pv->connection_type == SOUP_WEBSOCKET_CONNECTION_SERVER ? + "Received invalid WebSocket response from the client" : + "Received invalid WebSocket response from the server"); + emit_error_and_close (self, error, prejudice); +} + +static void +protocol_error_and_close (SoupWebsocketConnection *self) +{ + protocol_error_and_close_full (self, FALSE); +} + +static void +bad_data_error_and_close (SoupWebsocketConnection *self) +{ + GError *error; + + error = g_error_new_literal (SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_CLOSE_BAD_DATA, + self->pv->connection_type == SOUP_WEBSOCKET_CONNECTION_SERVER ? + "Received invalid WebSocket data from the client" : + "Received invalid WebSocket data from the server"); + emit_error_and_close (self, error, FALSE); +} + +static void +too_big_error_and_close (SoupWebsocketConnection *self, + guint64 payload_len) +{ + GError *error; + + error = g_error_new_literal (SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_CLOSE_TOO_BIG, + self->pv->connection_type == SOUP_WEBSOCKET_CONNECTION_SERVER ? + "Received extremely large WebSocket data from the client" : + "Received extremely large WebSocket data from the server"); + g_debug ("%s is trying to frame of size %" G_GUINT64_FORMAT " or greater, but max supported size is %" G_GUINT64_FORMAT, + self->pv->connection_type == SOUP_WEBSOCKET_CONNECTION_SERVER ? "server" : "client", + payload_len, self->pv->max_incoming_payload_size); + emit_error_and_close (self, error, TRUE); +} + +static void +close_connection (SoupWebsocketConnection *self, + gushort code, + const char *data) +{ + SoupWebsocketQueueFlags flags; + SoupWebsocketConnectionPrivate *pv; + + pv = self->pv; + + if (pv->close_sent) { + g_debug ("close code already sent"); + return; + } + + /* Validate the closing code received by the peer */ + switch (code) { + case SOUP_WEBSOCKET_CLOSE_NORMAL: + case SOUP_WEBSOCKET_CLOSE_GOING_AWAY: + case SOUP_WEBSOCKET_CLOSE_PROTOCOL_ERROR: + case SOUP_WEBSOCKET_CLOSE_UNSUPPORTED_DATA: + case SOUP_WEBSOCKET_CLOSE_BAD_DATA: + case SOUP_WEBSOCKET_CLOSE_POLICY_VIOLATION: + case SOUP_WEBSOCKET_CLOSE_TOO_BIG: + break; + case SOUP_WEBSOCKET_CLOSE_NO_EXTENSION: + if (pv->connection_type == SOUP_WEBSOCKET_CONNECTION_SERVER) { + g_debug ("Wrong closing code %d received for a server connection", + code); + } + break; + case SOUP_WEBSOCKET_CLOSE_SERVER_ERROR: + if (pv->connection_type != SOUP_WEBSOCKET_CONNECTION_SERVER) { + g_debug ("Wrong closing code %d received for a non server connection", + code); + } + break; + case SOUP_WEBSOCKET_CLOSE_NO_STATUS: + /* This is special case to send a close message with no body */ + code = 0; + break; + default: + if (code < 3000) { + g_debug ("Wrong closing code %d received", code); + protocol_error_and_close (self); + return; + } + } + + g_signal_emit (self, signals[CLOSING], 0); + + if (pv->close_received) + g_debug ("responding to close request"); + + flags = 0; + if (pv->close_received) + flags |= SOUP_WEBSOCKET_QUEUE_LAST; + send_close (self, flags, code, data); + close_io_after_timeout (self); +} + +static void +receive_close (SoupWebsocketConnection *self, + const guint8 *data, + gsize len) +{ + SoupWebsocketConnectionPrivate *pv = self->pv; + + pv->peer_close_code = 0; + g_free (pv->peer_close_data); + pv->peer_close_data = NULL; + pv->close_received = TRUE; + + switch (len) { + case 0: + /* Send a clean close when having an empty payload */ + pv->peer_close_code = SOUP_WEBSOCKET_CLOSE_NO_STATUS; + close_connection (self, 1000, NULL); + return; + case 1: + /* Send a protocol error since the close code is incomplete */ + protocol_error_and_close (self); + return; + default: + /* Store the code/data payload */ + pv->peer_close_code = (guint16)data[0] << 8 | data[1]; + break; + } + + if (len > 2) { + data += 2; + len -= 2; + + if (!utf8_validate ((const char *)data, len)) { + g_debug ("received non-UTF8 close data: %d '%.*s' %d", (int)len, (int)len, (char *)data, (int)data[0]); + protocol_error_and_close (self); + return; + } + + pv->peer_close_data = g_strndup ((char *)data, len); + } + + /* Once we receive close response on server, close immediately */ + if (pv->close_sent) { + shutdown_wr_io_stream (self); + if (pv->connection_type == SOUP_WEBSOCKET_CONNECTION_SERVER) + close_io_stream (self); + } else { + close_connection (self, pv->peer_close_code, pv->peer_close_data); + } +} + +static void +receive_ping (SoupWebsocketConnection *self, + const guint8 *data, + gsize len) +{ + /* Send back a pong with same data */ + g_debug ("received ping, responding"); + send_message (self, SOUP_WEBSOCKET_QUEUE_URGENT, 0x0A, data, len); +} + +static void +receive_pong (SoupWebsocketConnection *self, + const guint8 *data, + gsize len) +{ + GByteArray *bytes; + + g_debug ("received pong message"); + + bytes = g_byte_array_sized_new (len + 1); + g_byte_array_append (bytes, data, len); + /* Always null terminate, as a convenience */ + g_byte_array_append (bytes, (guchar *)"\0", 1); + /* But don't include the null terminator in the byte count */ + bytes->len--; + + g_signal_emit (self, signals[PONG], 0, bytes); + g_byte_array_unref (bytes); + +} + +static void +process_contents (SoupWebsocketConnection *self, + gboolean control, + gboolean fin, + guint8 opcode, + GBytes *payload_data) +{ + SoupWebsocketConnectionPrivate *pv = self->pv; + GBytes *message; + gconstpointer payload; + gsize payload_len; + + payload = g_bytes_get_data (payload_data, &payload_len); + + if (pv->close_sent && pv->close_received) + return; + + if (control) { + /* Control frames must never be fragmented */ + if (!fin) { + g_debug ("received fragmented control frame"); + protocol_error_and_close (self); + return; + } + + g_debug ("received control frame %d with %d payload", (int)opcode, (int)payload_len); + + switch (opcode) { + case 0x08: + receive_close (self, payload, payload_len); + break; + case 0x09: + receive_ping (self, payload, payload_len); + break; + case 0x0A: + receive_pong (self, payload, payload_len); + break; + default: + g_debug ("received unsupported control frame: %d", (int)opcode); + protocol_error_and_close (self); + return; + } + } else if (pv->close_received) { + g_debug ("received message after close was received"); + } else { + /* A message frame */ + + if (!fin && opcode) { + /* Initial fragment of a message */ + if (pv->message_data) { + g_debug ("received out of order initial message fragment"); + protocol_error_and_close (self); + return; + } + g_debug ("received initial fragment frame %d with %d payload", (int)opcode, (int)payload_len); + } else if (!fin && !opcode) { + /* Middle fragment of a message */ + if (!pv->message_data) { + g_debug ("received out of order middle message fragment"); + protocol_error_and_close (self); + return; + } + g_debug ("received middle fragment frame with %d payload", (int)payload_len); + } else if (fin && !opcode) { + /* Last fragment of a message */ + if (!pv->message_data) { + g_debug ("received out of order ending message fragment"); + protocol_error_and_close (self); + return; + } + g_debug ("received last fragment frame with %d payload", (int)payload_len); + } else { + /* An unfragmented message */ + g_assert (opcode != 0); + if (pv->message_data) { + g_debug ("received unfragmented message when fragment was expected"); + protocol_error_and_close (self); + return; + } + g_debug ("received frame %d with %d payload", (int)opcode, (int)payload_len); + } + + if (opcode) { + pv->message_opcode = opcode; + pv->message_data = g_byte_array_sized_new (payload_len + 1); + } + + switch (pv->message_opcode) { + case 0x01: + case 0x02: + g_byte_array_append (pv->message_data, payload, payload_len); + break; + default: + g_debug ("received unknown data frame: %d", (int)opcode); + protocol_error_and_close (self); + return; + } + + /* Actually deliver the message? */ + if (fin) { + if (pv->message_opcode == 0x01 && + !utf8_validate((const char *)pv->message_data->data, + pv->message_data->len)) { + + g_debug ("received invalid non-UTF8 text data"); + + /* Discard the entire message */ + g_byte_array_unref (pv->message_data); + pv->message_data = NULL; + pv->message_opcode = 0; + + bad_data_error_and_close (self); + return; + } + + /* Always null terminate, as a convenience */ + g_byte_array_append (pv->message_data, (guchar *)"\0", 1); + + /* But don't include the null terminator in the byte count */ + pv->message_data->len--; + + opcode = pv->message_opcode; + message = g_byte_array_free_to_bytes (pv->message_data); + pv->message_data = NULL; + pv->message_opcode = 0; + g_debug ("message: delivering %d with %d length", + (int)opcode, (int)g_bytes_get_size (message)); + g_signal_emit (self, signals[MESSAGE], 0, (int)opcode, message); + g_bytes_unref (message); + } + } +} + +static gboolean +process_frame (SoupWebsocketConnection *self) +{ + guint8 *header; + guint8 *payload; + guint64 payload_len; + guint8 *mask; + gboolean fin; + gboolean control; + gboolean masked; + guint8 opcode; + gsize len; + gsize at; + GBytes *filtered_bytes; + GList *l; + GError *error = NULL; + + len = self->pv->incoming->len; + if (len < 2) + return FALSE; /* need more data */ + + header = self->pv->incoming->data; + fin = ((header[0] & 0x80) != 0); + control = header[0] & 0x08; + opcode = header[0] & 0x0f; + masked = ((header[1] & 0x80) != 0); + + if (self->pv->connection_type == SOUP_WEBSOCKET_CONNECTION_CLIENT && masked) { + /* A server MUST NOT mask any frames that it sends to the client. + * A client MUST close a connection if it detects a masked frame. + */ + g_debug ("A server must not mask any frames that it sends to the client."); + protocol_error_and_close (self); + return FALSE; + } + + if (self->pv->connection_type == SOUP_WEBSOCKET_CONNECTION_SERVER && !masked) { + /* The server MUST close the connection upon receiving a frame + * that is not masked. + */ + g_debug ("The client should always mask frames"); + protocol_error_and_close (self); + return FALSE; + } + + switch (header[1] & 0x7f) { + case 126: + /* If 126, the following 2 bytes interpreted as a 16-bit + * unsigned integer are the payload length. + */ + at = 4; + if (len < at) + return FALSE; /* need more data */ + payload_len = (((guint16)header[2] << 8) | + ((guint16)header[3] << 0)); + + /* The minimal number of bytes MUST be used to encode the length. */ + if (payload_len <= 125) { + protocol_error_and_close (self); + return FALSE; + } + break; + case 127: + /* If 127, the following 8 bytes interpreted as a 64-bit + * unsigned integer (the most significant bit MUST be 0) + * are the payload length. + */ + at = 10; + if (len < at) + return FALSE; /* need more data */ + payload_len = (((guint64)header[2] << 56) | + ((guint64)header[3] << 48) | + ((guint64)header[4] << 40) | + ((guint64)header[5] << 32) | + ((guint64)header[6] << 24) | + ((guint64)header[7] << 16) | + ((guint64)header[8] << 8) | + ((guint64)header[9] << 0)); + + /* The minimal number of bytes MUST be used to encode the length. */ + if (payload_len <= G_MAXUINT16) { + protocol_error_and_close (self); + return FALSE; + } + break; + default: + payload_len = header[1] & 0x7f; + at = 2; + break; + } + + /* Safety valve */ + if (self->pv->max_incoming_payload_size > 0 && + payload_len >= self->pv->max_incoming_payload_size) { + too_big_error_and_close (self, payload_len); + return FALSE; + } + + if (len < at + payload_len) + return FALSE; /* need more data */ + + payload = header + at; + + if (masked) { + mask = header + at; + payload += 4; + at += 4; + + if (len < at + payload_len) + return FALSE; /* need more data */ + + xor_with_mask (mask, payload, payload_len); + } + + filtered_bytes = g_bytes_new_static (payload, payload_len); + for (l = self->pv->extensions; l != NULL; l = g_list_next (l)) { + SoupWebsocketExtension *extension; + + extension = (SoupWebsocketExtension *)l->data; + filtered_bytes = soup_websocket_extension_process_incoming_message (extension, self->pv->incoming->data, filtered_bytes, &error); + if (error) { + emit_error_and_close (self, error, FALSE); + return FALSE; + } + } + + /* After being processed by extensions reserved bits must be 0 */ + if (header[0] & 0x70) { + protocol_error_and_close (self); + g_bytes_unref (filtered_bytes); + + return FALSE; + } + + /* Note that now that we've unmasked, we've modified the buffer, we can + * only return below via discarding or processing the message + */ + process_contents (self, control, fin, opcode, filtered_bytes); + g_bytes_unref (filtered_bytes); + + /* Move past the parsed frame */ + g_byte_array_remove_range (self->pv->incoming, 0, at + payload_len); + + return TRUE; +} + +static void +process_incoming (SoupWebsocketConnection *self) +{ + while (process_frame (self)) + ; +} + +static void +soup_websocket_connection_read (SoupWebsocketConnection *self) +{ + SoupWebsocketConnectionPrivate *pv = self->pv; + GError *error = NULL; + gboolean end = FALSE; + gssize count; + gsize len; + + soup_websocket_connection_stop_input_source (self); + + do { + len = pv->incoming->len; + g_byte_array_set_size (pv->incoming, len + READ_BUFFER_SIZE); + + count = g_pollable_input_stream_read_nonblocking (pv->input, + pv->incoming->data + len, + READ_BUFFER_SIZE, NULL, &error); + if (count < 0) { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) { + g_error_free (error); + count = 0; + } else { + emit_error_and_close (self, error, TRUE); + return; + } + } else if (count == 0) { + end = TRUE; + } + + pv->incoming->len = len + count; + } while (count > 0); + + process_incoming (self); + + if (end) { + if (!pv->close_sent || !pv->close_received) { + pv->dirty_close = TRUE; + g_debug ("connection unexpectedly closed by peer"); + } else { + g_debug ("peer has closed socket"); + } + + close_io_stream (self); + return; + } + + if (!pv->io_closing) + soup_websocket_connection_start_input_source (self); +} + +static gboolean +on_web_socket_input (GObject *pollable_stream, + gpointer user_data) +{ + soup_websocket_connection_read (SOUP_WEBSOCKET_CONNECTION (user_data)); + + return G_SOURCE_REMOVE; +} + +static void +soup_websocket_connection_write (SoupWebsocketConnection *self) +{ + SoupWebsocketConnectionPrivate *pv = self->pv; + const guint8 *data; + GError *error = NULL; + Frame *frame; + gssize count; + gsize len; + + soup_websocket_connection_stop_output_source (self); + + if (soup_websocket_connection_get_state (self) == SOUP_WEBSOCKET_STATE_CLOSED) { + g_debug ("Ignoring message since the connection is closed"); + return; + } + + frame = g_queue_peek_head (&pv->outgoing); + + /* No more frames to send */ + if (frame == NULL) + return; + + data = g_bytes_get_data (frame->data, &len); + g_assert (len > 0); + g_assert (len > frame->sent); + + count = g_pollable_output_stream_write_nonblocking (pv->output, + data + frame->sent, + len - frame->sent, + NULL, &error); + + if (count < 0) { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) { + g_clear_error (&error); + count = 0; + + g_debug ("failed to send frame because it would block, marking as pending"); + frame->pending = TRUE; + } else { + emit_error_and_close (self, error, TRUE); + return; + } + } + + frame->sent += count; + if (frame->sent >= len) { + g_debug ("sent frame"); + g_queue_pop_head (&pv->outgoing); + + if (frame->flags & SOUP_WEBSOCKET_QUEUE_LAST) { + if (pv->connection_type == SOUP_WEBSOCKET_CONNECTION_SERVER) { + close_io_stream (self); + } else { + shutdown_wr_io_stream (self); + close_io_after_timeout (self); + } + } + frame_free (frame); + + if (g_queue_is_empty (&pv->outgoing)) + return; + } + + soup_websocket_connection_start_output_source (self); +} + +static gboolean +on_web_socket_output (GObject *pollable_stream, + gpointer user_data) +{ + soup_websocket_connection_write (SOUP_WEBSOCKET_CONNECTION (user_data)); + + return G_SOURCE_REMOVE; +} + +static void +queue_frame (SoupWebsocketConnection *self, + SoupWebsocketQueueFlags flags, + gpointer data, + gsize len, + gsize amount) +{ + SoupWebsocketConnectionPrivate *pv = self->pv; + Frame *frame; + + g_return_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self)); + g_return_if_fail (pv->close_sent == FALSE); + g_return_if_fail (data != NULL); + g_return_if_fail (len > 0); + + frame = g_slice_new0 (Frame); + frame->data = g_bytes_new_take (data, len); + frame->amount = amount; + frame->flags = flags; + + /* If urgent put at front of queue */ + if (flags & SOUP_WEBSOCKET_QUEUE_URGENT) { + GList *l; + + /* Find out the first frame that is not urgent or partially sent or pending */ + for (l = g_queue_peek_head_link (&pv->outgoing); l != NULL; l = l->next) { + Frame *prev = l->data; + + if (!(prev->flags & SOUP_WEBSOCKET_QUEUE_URGENT) && + prev->sent == 0 && !prev->pending) + break; + } + + g_queue_insert_before (&pv->outgoing, l, frame); + } else { + g_queue_push_tail (&pv->outgoing, frame); + } + + soup_websocket_connection_write (self); +} + +static void +soup_websocket_connection_constructed (GObject *object) +{ + SoupWebsocketConnection *self = SOUP_WEBSOCKET_CONNECTION (object); + SoupWebsocketConnectionPrivate *pv = self->pv; + GInputStream *is; + GOutputStream *os; + + G_OBJECT_CLASS (soup_websocket_connection_parent_class)->constructed (object); + + g_return_if_fail (pv->io_stream != NULL); + + is = g_io_stream_get_input_stream (pv->io_stream); + g_return_if_fail (G_IS_POLLABLE_INPUT_STREAM (is)); + pv->input = G_POLLABLE_INPUT_STREAM (is); + g_return_if_fail (g_pollable_input_stream_can_poll (pv->input)); + + os = g_io_stream_get_output_stream (pv->io_stream); + g_return_if_fail (G_IS_POLLABLE_OUTPUT_STREAM (os)); + pv->output = G_POLLABLE_OUTPUT_STREAM (os); + g_return_if_fail (g_pollable_output_stream_can_poll (pv->output)); + + soup_websocket_connection_start_input_source (self); +} + +static void +soup_websocket_connection_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + SoupWebsocketConnection *self = SOUP_WEBSOCKET_CONNECTION (object); + SoupWebsocketConnectionPrivate *pv = self->pv; + + switch (prop_id) { + case PROP_IO_STREAM: + g_value_set_object (value, soup_websocket_connection_get_io_stream (self)); + break; + + case PROP_CONNECTION_TYPE: + g_value_set_enum (value, soup_websocket_connection_get_connection_type (self)); + break; + + case PROP_URI: + g_value_set_boxed (value, soup_websocket_connection_get_uri (self)); + break; + + case PROP_ORIGIN: + g_value_set_string (value, soup_websocket_connection_get_origin (self)); + break; + + case PROP_PROTOCOL: + g_value_set_string (value, soup_websocket_connection_get_protocol (self)); + break; + + case PROP_STATE: + g_value_set_enum (value, soup_websocket_connection_get_state (self)); + break; + + case PROP_MAX_INCOMING_PAYLOAD_SIZE: + g_value_set_uint64 (value, pv->max_incoming_payload_size); + break; + + case PROP_KEEPALIVE_INTERVAL: + g_value_set_uint (value, pv->keepalive_interval); + break; + + case PROP_EXTENSIONS: + g_value_set_pointer (value, pv->extensions); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +soup_websocket_connection_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + SoupWebsocketConnection *self = SOUP_WEBSOCKET_CONNECTION (object); + SoupWebsocketConnectionPrivate *pv = self->pv; + + switch (prop_id) { + case PROP_IO_STREAM: + g_return_if_fail (pv->io_stream == NULL); + pv->io_stream = g_value_dup_object (value); + break; + + case PROP_CONNECTION_TYPE: + pv->connection_type = g_value_get_enum (value); + break; + + case PROP_URI: + g_return_if_fail (pv->uri == NULL); + pv->uri = g_value_dup_boxed (value); + break; + + case PROP_ORIGIN: + g_return_if_fail (pv->origin == NULL); + pv->origin = g_value_dup_string (value); + break; + + case PROP_PROTOCOL: + g_return_if_fail (pv->protocol == NULL); + pv->protocol = g_value_dup_string (value); + break; + + case PROP_MAX_INCOMING_PAYLOAD_SIZE: + pv->max_incoming_payload_size = g_value_get_uint64 (value); + break; + + case PROP_KEEPALIVE_INTERVAL: + soup_websocket_connection_set_keepalive_interval (self, + g_value_get_uint (value)); + break; + + case PROP_EXTENSIONS: + pv->extensions = g_value_get_pointer (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +soup_websocket_connection_dispose (GObject *object) +{ + SoupWebsocketConnection *self = SOUP_WEBSOCKET_CONNECTION (object); + + self->pv->dirty_close = TRUE; + close_io_stream (self); + + G_OBJECT_CLASS (soup_websocket_connection_parent_class)->dispose (object); +} + +static void +soup_websocket_connection_finalize (GObject *object) +{ + SoupWebsocketConnection *self = SOUP_WEBSOCKET_CONNECTION (object); + SoupWebsocketConnectionPrivate *pv = self->pv; + + g_free (pv->peer_close_data); + + g_main_context_unref (pv->main_context); + + if (pv->incoming) + g_byte_array_free (pv->incoming, TRUE); + while (!g_queue_is_empty (&pv->outgoing)) + frame_free (g_queue_pop_head (&pv->outgoing)); + + g_clear_object (&pv->io_stream); + g_assert (!pv->input_source); + g_assert (!pv->output_source); + g_assert (pv->io_closing); + g_assert (pv->io_closed); + g_assert (!pv->close_timeout); + g_assert (!pv->keepalive_timeout); + + if (pv->message_data) + g_byte_array_free (pv->message_data, TRUE); + + if (pv->uri) + soup_uri_free (pv->uri); + g_free (pv->origin); + g_free (pv->protocol); + + g_list_free_full (pv->extensions, g_object_unref); + + G_OBJECT_CLASS (soup_websocket_connection_parent_class)->finalize (object); +} + +static void +soup_websocket_connection_class_init (SoupWebsocketConnectionClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->constructed = soup_websocket_connection_constructed; + gobject_class->get_property = soup_websocket_connection_get_property; + gobject_class->set_property = soup_websocket_connection_set_property; + gobject_class->dispose = soup_websocket_connection_dispose; + gobject_class->finalize = soup_websocket_connection_finalize; + + /** + * SoupWebsocketConnection:io-stream: + * + * The underlying IO stream the WebSocket is communicating + * over. + * + * The input and output streams must be pollable streams. + * + * Since: 2.50 + */ + g_object_class_install_property (gobject_class, PROP_IO_STREAM, + g_param_spec_object ("io-stream", + "I/O Stream", + "Underlying I/O stream", + G_TYPE_IO_STREAM, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + /** + * SoupWebsocketConnection:connection-type: + * + * The type of connection (client/server). + * + * Since: 2.50 + */ + g_object_class_install_property (gobject_class, PROP_CONNECTION_TYPE, + g_param_spec_enum ("connection-type", + "Connection type", + "Connection type (client/server)", + SOUP_TYPE_WEBSOCKET_CONNECTION_TYPE, + SOUP_WEBSOCKET_CONNECTION_UNKNOWN, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + /** + * SoupWebsocketConnection:uri: + * + * The URI of the WebSocket. + * + * For servers this represents the address of the WebSocket, + * and for clients it is the address connected to. + * + * Since: 2.50 + */ + g_object_class_install_property (gobject_class, PROP_URI, + g_param_spec_boxed ("uri", + "URI", + "The WebSocket URI", + SOUP_TYPE_URI, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + /** + * SoupWebsocketConnection:origin: + * + * The client's Origin. + * + * Since: 2.50 + */ + g_object_class_install_property (gobject_class, PROP_ORIGIN, + g_param_spec_string ("origin", + "Origin", + "The WebSocket origin", + NULL, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + /** + * SoupWebsocketConnection:protocol: + * + * The chosen protocol, or %NULL if a protocol was not agreed + * upon. + * + * Since: 2.50 + */ + g_object_class_install_property (gobject_class, PROP_PROTOCOL, + g_param_spec_string ("protocol", + "Protocol", + "The chosen WebSocket protocol", + NULL, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + /** + * SoupWebsocketConnection:state: + * + * The current state of the WebSocket. + * + * Since: 2.50 + */ + g_object_class_install_property (gobject_class, PROP_STATE, + g_param_spec_enum ("state", + "State", + "State ", + SOUP_TYPE_WEBSOCKET_STATE, + SOUP_WEBSOCKET_STATE_OPEN, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + + /** + * SoupWebsocketConnection:max-incoming-payload-size: + * + * The maximum payload size for incoming packets the protocol expects + * or 0 to not limit it. + * + * Since: 2.56 + */ + g_object_class_install_property (gobject_class, PROP_MAX_INCOMING_PAYLOAD_SIZE, + g_param_spec_uint64 ("max-incoming-payload-size", + "Max incoming payload size", + "Max incoming payload size ", + 0, + G_MAXUINT64, + MAX_INCOMING_PAYLOAD_SIZE_DEFAULT, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + + /** + * SoupWebsocketConnection:keepalive-interval: + * + * Interval in seconds on when to send a ping message which will + * serve as a keepalive message. If set to 0 the keepalive message is + * disabled. + * + * Since: 2.58 + */ + g_object_class_install_property (gobject_class, PROP_KEEPALIVE_INTERVAL, + g_param_spec_uint ("keepalive-interval", + "Keepalive interval", + "Keepalive interval", + 0, + G_MAXUINT, + 0, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + + /** + * SoupWebsocketConnection:extensions: + * + * List of #SoupWebsocketExtension objects that are active in the connection. + * + * Since: 2.68 + */ + g_object_class_install_property (gobject_class, PROP_EXTENSIONS, + g_param_spec_pointer ("extensions", + "Active extensions", + "The list of active extensions", + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + /** + * SoupWebsocketConnection::message: + * @self: the WebSocket + * @type: the type of message contents + * @message: the message data + * + * Emitted when we receive a message from the peer. + * + * As a convenience, the @message data will always be + * NUL-terminated, but the NUL byte will not be included in + * the length count. + * + * Since: 2.50 + */ + signals[MESSAGE] = g_signal_new ("message", + SOUP_TYPE_WEBSOCKET_CONNECTION, + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (SoupWebsocketConnectionClass, message), + NULL, NULL, g_cclosure_marshal_generic, + G_TYPE_NONE, 2, G_TYPE_INT, G_TYPE_BYTES); + + /** + * SoupWebsocketConnection::error: + * @self: the WebSocket + * @error: the error that occured + * + * Emitted when an error occurred on the WebSocket. This may + * be fired multiple times. Fatal errors will be followed by + * the #SoupWebsocketConnection::closed signal being emitted. + * + * Since: 2.50 + */ + signals[ERROR] = g_signal_new ("error", + SOUP_TYPE_WEBSOCKET_CONNECTION, + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (SoupWebsocketConnectionClass, error), + NULL, NULL, g_cclosure_marshal_generic, + G_TYPE_NONE, 1, G_TYPE_ERROR); + + /** + * SoupWebsocketConnection::closing: + * @self: the WebSocket + * + * This signal will be emitted during an orderly close. + * + * Since: 2.50 + */ + signals[CLOSING] = g_signal_new ("closing", + SOUP_TYPE_WEBSOCKET_CONNECTION, + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (SoupWebsocketConnectionClass, closing), + NULL, NULL, g_cclosure_marshal_generic, + G_TYPE_NONE, 0); + + /** + * SoupWebsocketConnection::closed: + * @self: the WebSocket + * + * Emitted when the connection has completely closed, either + * due to an orderly close from the peer, one initiated via + * soup_websocket_connection_close() or a fatal error + * condition that caused a close. + * + * This signal will be emitted once. + * + * Since: 2.50 + */ + signals[CLOSED] = g_signal_new ("closed", + SOUP_TYPE_WEBSOCKET_CONNECTION, + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (SoupWebsocketConnectionClass, closed), + NULL, NULL, g_cclosure_marshal_generic, + G_TYPE_NONE, 0); + + /** + * SoupWebsocketConnection::pong: + * @self: the WebSocket + * @message: the application data (if any) + * + * Emitted when we receive a Pong frame (solicited or + * unsolicited) from the peer. + * + * As a convenience, the @message data will always be + * NUL-terminated, but the NUL byte will not be included in + * the length count. + * + * Since: 2.60 + */ + signals[PONG] = g_signal_new ("pong", + SOUP_TYPE_WEBSOCKET_CONNECTION, + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (SoupWebsocketConnectionClass, pong), + NULL, NULL, g_cclosure_marshal_generic, + G_TYPE_NONE, 1, G_TYPE_BYTES); +} + +/** + * soup_websocket_connection_new: + * @stream: a #GIOStream connected to the WebSocket server + * @uri: the URI of the connection + * @type: the type of connection (client/side) + * @origin: (allow-none): the Origin of the client + * @protocol: (allow-none): the subprotocol in use + * + * Creates a #SoupWebsocketConnection on @stream. This should be + * called after completing the handshake to begin using the WebSocket + * protocol. + * + * Returns: a new #SoupWebsocketConnection + * + * Since: 2.50 + */ +SoupWebsocketConnection * +soup_websocket_connection_new (GIOStream *stream, + SoupURI *uri, + SoupWebsocketConnectionType type, + const char *origin, + const char *protocol) +{ + return soup_websocket_connection_new_with_extensions (stream, uri, type, origin, protocol, NULL); +} + +/** + * soup_websocket_connection_new_with_extensions: + * @stream: a #GIOStream connected to the WebSocket server + * @uri: the URI of the connection + * @type: the type of connection (client/side) + * @origin: (allow-none): the Origin of the client + * @protocol: (allow-none): the subprotocol in use + * @extensions: (element-type SoupWebsocketExtension) (transfer full): a #GList of #SoupWebsocketExtension objects + * + * Creates a #SoupWebsocketConnection on @stream with the given active @extensions. + * This should be called after completing the handshake to begin using the WebSocket + * protocol. + * + * Returns: a new #SoupWebsocketConnection + * + * Since: 2.68 + */ +SoupWebsocketConnection * +soup_websocket_connection_new_with_extensions (GIOStream *stream, + SoupURI *uri, + SoupWebsocketConnectionType type, + const char *origin, + const char *protocol, + GList *extensions) +{ + g_return_val_if_fail (G_IS_IO_STREAM (stream), NULL); + g_return_val_if_fail (uri != NULL, NULL); + g_return_val_if_fail (type != SOUP_WEBSOCKET_CONNECTION_UNKNOWN, NULL); + + return g_object_new (SOUP_TYPE_WEBSOCKET_CONNECTION, + "io-stream", stream, + "uri", uri, + "connection-type", type, + "origin", origin, + "protocol", protocol, + "extensions", extensions, + NULL); +} + +/** + * soup_websocket_connection_get_io_stream: + * @self: the WebSocket + * + * Get the I/O stream the WebSocket is communicating over. + * + * Returns: (transfer none): the WebSocket's I/O stream. + * + * Since: 2.50 + */ +GIOStream * +soup_websocket_connection_get_io_stream (SoupWebsocketConnection *self) +{ + g_return_val_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self), NULL); + + return self->pv->io_stream; +} + +/** + * soup_websocket_connection_get_connection_type: + * @self: the WebSocket + * + * Get the connection type (client/server) of the connection. + * + * Returns: the connection type + * + * Since: 2.50 + */ +SoupWebsocketConnectionType +soup_websocket_connection_get_connection_type (SoupWebsocketConnection *self) +{ + g_return_val_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self), SOUP_WEBSOCKET_CONNECTION_UNKNOWN); + + return self->pv->connection_type; +} + +/** + * soup_websocket_connection_get_uri: + * @self: the WebSocket + * + * Get the URI of the WebSocket. + * + * For servers this represents the address of the WebSocket, and + * for clients it is the address connected to. + * + * Returns: (transfer none): the URI + * + * Since: 2.50 + */ +SoupURI * +soup_websocket_connection_get_uri (SoupWebsocketConnection *self) +{ + g_return_val_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self), NULL); + + return self->pv->uri; +} + +/** + * soup_websocket_connection_get_origin: + * @self: the WebSocket + * + * Get the origin of the WebSocket. + * + * Returns: (nullable): the origin, or %NULL + * + * Since: 2.50 + */ +const char * +soup_websocket_connection_get_origin (SoupWebsocketConnection *self) +{ + g_return_val_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self), NULL); + + return self->pv->origin; +} + +/** + * soup_websocket_connection_get_protocol: + * @self: the WebSocket + * + * Get the protocol chosen via negotiation with the peer. + * + * Returns: (nullable): the chosen protocol, or %NULL + * + * Since: 2.50 + */ +const char * +soup_websocket_connection_get_protocol (SoupWebsocketConnection *self) +{ + g_return_val_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self), NULL); + + return self->pv->protocol; +} + +/** + * soup_websocket_connection_get_extensions: + * @self: the WebSocket + * + * Get the extensions chosen via negotiation with the peer. + * + * Returns: (element-type SoupWebsocketExtension) (transfer none): a #GList of #SoupWebsocketExtension objects + * + * Since: 2.68 + */ +GList * +soup_websocket_connection_get_extensions (SoupWebsocketConnection *self) +{ + g_return_val_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self), NULL); + + return self->pv->extensions; +} + +/** + * soup_websocket_connection_get_state: + * @self: the WebSocket + * + * Get the current state of the WebSocket. + * + * Returns: the state + * + * Since: 2.50 + */ +SoupWebsocketState +soup_websocket_connection_get_state (SoupWebsocketConnection *self) +{ + g_return_val_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self), 0); + + if (self->pv->io_closed) + return SOUP_WEBSOCKET_STATE_CLOSED; + else if (self->pv->io_closing || self->pv->close_sent) + return SOUP_WEBSOCKET_STATE_CLOSING; + else + return SOUP_WEBSOCKET_STATE_OPEN; +} + +/** + * soup_websocket_connection_get_close_code: + * @self: the WebSocket + * + * Get the close code received from the WebSocket peer. + * + * This only becomes valid once the WebSocket is in the + * %SOUP_WEBSOCKET_STATE_CLOSED state. The value will often be in the + * #SoupWebsocketCloseCode enumeration, but may also be an application + * defined close code. + * + * Returns: the close code or zero. + * + * Since: 2.50 + */ +gushort +soup_websocket_connection_get_close_code (SoupWebsocketConnection *self) +{ + g_return_val_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self), 0); + + return self->pv->peer_close_code; +} + +/** + * soup_websocket_connection_get_close_data: + * @self: the WebSocket + * + * Get the close data received from the WebSocket peer. + * + * This only becomes valid once the WebSocket is in the + * %SOUP_WEBSOCKET_STATE_CLOSED state. The data may be freed once + * the main loop is run, so copy it if you need to keep it around. + * + * Returns: the close data or %NULL + * + * Since: 2.50 + */ +const char * +soup_websocket_connection_get_close_data (SoupWebsocketConnection *self) +{ + g_return_val_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self), NULL); + + return self->pv->peer_close_data; +} + +/** + * soup_websocket_connection_send_text: + * @self: the WebSocket + * @text: the message contents + * + * Send a %NULL-terminated text (UTF-8) message to the peer. If you need + * to send text messages containing %NULL characters use + * soup_websocket_connection_send_message() instead. + * + * The message is queued to be sent and will be sent when the main loop + * is run. + * + * Since: 2.50 + */ +void +soup_websocket_connection_send_text (SoupWebsocketConnection *self, + const char *text) +{ + gsize length; + + g_return_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self)); + g_return_if_fail (soup_websocket_connection_get_state (self) == SOUP_WEBSOCKET_STATE_OPEN); + g_return_if_fail (text != NULL); + + length = strlen (text); + g_return_if_fail (utf8_validate (text, length)); + + send_message (self, SOUP_WEBSOCKET_QUEUE_NORMAL, 0x01, (const guint8 *) text, length); +} + +/** + * soup_websocket_connection_send_binary: + * @self: the WebSocket + * @data: (array length=length) (element-type guint8) (nullable): the message contents + * @length: the length of @data + * + * Send a binary message to the peer. If @length is 0, @data may be %NULL. + * + * The message is queued to be sent and will be sent when the main loop + * is run. + * + * Since: 2.50 + */ +void +soup_websocket_connection_send_binary (SoupWebsocketConnection *self, + gconstpointer data, + gsize length) +{ + g_return_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self)); + g_return_if_fail (soup_websocket_connection_get_state (self) == SOUP_WEBSOCKET_STATE_OPEN); + g_return_if_fail (data != NULL || length == 0); + + send_message (self, SOUP_WEBSOCKET_QUEUE_NORMAL, 0x02, data, length); +} + +/** + * soup_websocket_connection_send_message: + * @self: the WebSocket + * @type: the type of message contents + * @message: the message data as #GBytes + * + * Send a message of the given @type to the peer. Note that this method, + * allows to send text messages containing %NULL characters. + * + * The message is queued to be sent and will be sent when the main loop + * is run. + * + * Since: 2.68 + */ +void +soup_websocket_connection_send_message (SoupWebsocketConnection *self, + SoupWebsocketDataType type, + GBytes *message) +{ + gconstpointer data; + gsize length; + + g_return_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self)); + g_return_if_fail (soup_websocket_connection_get_state (self) == SOUP_WEBSOCKET_STATE_OPEN); + g_return_if_fail (message != NULL); + + data = g_bytes_get_data (message, &length); + g_return_if_fail (type != SOUP_WEBSOCKET_DATA_TEXT || utf8_validate ((const char *)data, length)); + + send_message (self, SOUP_WEBSOCKET_QUEUE_NORMAL, (int)type, data, length); +} + +/** + * soup_websocket_connection_close: + * @self: the WebSocket + * @code: close code + * @data: (allow-none): close data + * + * Close the connection in an orderly fashion. + * + * Note that until the #SoupWebsocketConnection::closed signal fires, the connection + * is not yet completely closed. The close message is not even sent until the + * main loop runs. + * + * The @code and @data are sent to the peer along with the close request. + * If @code is %SOUP_WEBSOCKET_CLOSE_NO_STATUS a close message with no body + * (without code and data) is sent. + * Note that the @data must be UTF-8 valid. + * + * Since: 2.50 + */ +void +soup_websocket_connection_close (SoupWebsocketConnection *self, + gushort code, + const char *data) +{ + SoupWebsocketConnectionPrivate *pv; + + g_return_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self)); + pv = self->pv; + g_return_if_fail (!pv->close_sent); + + g_return_if_fail (code != SOUP_WEBSOCKET_CLOSE_ABNORMAL && + code != SOUP_WEBSOCKET_CLOSE_TLS_HANDSHAKE); + if (pv->connection_type == SOUP_WEBSOCKET_CONNECTION_SERVER) + g_return_if_fail (code != SOUP_WEBSOCKET_CLOSE_NO_EXTENSION); + else + g_return_if_fail (code != SOUP_WEBSOCKET_CLOSE_SERVER_ERROR); + + close_connection (self, code, data); +} + +/** + * soup_websocket_connection_get_max_incoming_payload_size: + * @self: the WebSocket + * + * Gets the maximum payload size allowed for incoming packets. + * + * Returns: the maximum payload size. + * + * Since: 2.56 + */ +guint64 +soup_websocket_connection_get_max_incoming_payload_size (SoupWebsocketConnection *self) +{ + SoupWebsocketConnectionPrivate *pv; + + g_return_val_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self), MAX_INCOMING_PAYLOAD_SIZE_DEFAULT); + pv = self->pv; + + return pv->max_incoming_payload_size; +} + +/** + * soup_websocket_connection_set_max_incoming_payload_size: + * @self: the WebSocket + * @max_incoming_payload_size: the maximum payload size + * + * Sets the maximum payload size allowed for incoming packets. It + * does not limit the outgoing packet size. + * + * Since: 2.56 + */ +void +soup_websocket_connection_set_max_incoming_payload_size (SoupWebsocketConnection *self, + guint64 max_incoming_payload_size) +{ + SoupWebsocketConnectionPrivate *pv; + + g_return_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self)); + pv = self->pv; + + if (pv->max_incoming_payload_size != max_incoming_payload_size) { + pv->max_incoming_payload_size = max_incoming_payload_size; + g_object_notify (G_OBJECT (self), "max-incoming-payload-size"); + } +} + +/** + * soup_websocket_connection_get_keepalive_interval: + * @self: the WebSocket + * + * Gets the keepalive interval in seconds or 0 if disabled. + * + * Returns: the keepalive interval. + * + * Since: 2.58 + */ +guint +soup_websocket_connection_get_keepalive_interval (SoupWebsocketConnection *self) +{ + SoupWebsocketConnectionPrivate *pv; + + g_return_val_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self), 0); + pv = self->pv; + + return pv->keepalive_interval; +} + +static gboolean +on_queue_ping (gpointer user_data) +{ + SoupWebsocketConnection *self = SOUP_WEBSOCKET_CONNECTION (user_data); + static const char ping_payload[] = "libsoup"; + + g_debug ("sending ping message"); + + send_message (self, SOUP_WEBSOCKET_QUEUE_NORMAL, 0x09, + (guint8 *) ping_payload, strlen(ping_payload)); + + return G_SOURCE_CONTINUE; +} + +/** + * soup_websocket_connection_set_keepalive_interval: + * @self: the WebSocket + * @interval: the interval to send a ping message or 0 to disable it + * + * Sets the interval in seconds on when to send a ping message which will serve + * as a keepalive message. If set to 0 the keepalive message is disabled. + * + * Since: 2.58 + */ +void +soup_websocket_connection_set_keepalive_interval (SoupWebsocketConnection *self, + guint interval) +{ + SoupWebsocketConnectionPrivate *pv; + + g_return_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self)); + pv = self->pv; + + if (pv->keepalive_interval != interval) { + pv->keepalive_interval = interval; + g_object_notify (G_OBJECT (self), "keepalive-interval"); + + keepalive_stop_timeout (self); + + if (interval > 0) { + pv->keepalive_timeout = g_timeout_source_new_seconds (interval); + g_source_set_callback (pv->keepalive_timeout, on_queue_ping, self, NULL); + g_source_attach (pv->keepalive_timeout, pv->main_context); + } + } +} diff --git a/libsoup/websocket/soup-websocket-connection.h b/libsoup/websocket/soup-websocket-connection.h new file mode 100644 index 00000000..b5e90fb8 --- /dev/null +++ b/libsoup/websocket/soup-websocket-connection.h @@ -0,0 +1,141 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * soup-websocket-connection.h: This file was originally part of Cockpit. + * + * Copyright 2013, 2014 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 this library; If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef __SOUP_WEBSOCKET_CONNECTION_H__ +#define __SOUP_WEBSOCKET_CONNECTION_H__ + +#include "soup-types.h" +#include "soup-websocket.h" + +G_BEGIN_DECLS + +#define SOUP_TYPE_WEBSOCKET_CONNECTION (soup_websocket_connection_get_type ()) +#define SOUP_WEBSOCKET_CONNECTION(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SOUP_TYPE_WEBSOCKET_CONNECTION, SoupWebsocketConnection)) +#define SOUP_IS_WEBSOCKET_CONNECTION(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SOUP_TYPE_WEBSOCKET_CONNECTION)) +#define SOUP_WEBSOCKET_CONNECTION_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), SOUP_TYPE_WEBSOCKET_CONNECTION, SoupWebsocketConnectionClass)) +#define SOUP_WEBSOCKET_CONNECTION_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), SOUP_TYPE_WEBSOCKET_CONNECTION, SoupWebsocketConnectionClass)) +#define SOUP_IS_WEBSOCKET_CONNECTION_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), SOUP_TYPE_WEBSOCKET_CONNECTION)) + +typedef struct _SoupWebsocketConnectionPrivate SoupWebsocketConnectionPrivate; + +struct _SoupWebsocketConnection { + GObject parent; + + /*< private >*/ + SoupWebsocketConnectionPrivate *pv; +}; + +typedef struct { + GObjectClass parent; + + /* signals */ + void (* message) (SoupWebsocketConnection *self, + SoupWebsocketDataType type, + GBytes *message); + + void (* error) (SoupWebsocketConnection *self, + GError *error); + + void (* closing) (SoupWebsocketConnection *self); + + void (* closed) (SoupWebsocketConnection *self); + + void (* pong) (SoupWebsocketConnection *self, + GBytes *message); +} SoupWebsocketConnectionClass; + +SOUP_AVAILABLE_IN_2_50 +GType soup_websocket_connection_get_type (void) G_GNUC_CONST; + +SOUP_AVAILABLE_IN_2_50 +SoupWebsocketConnection *soup_websocket_connection_new (GIOStream *stream, + SoupURI *uri, + SoupWebsocketConnectionType type, + const char *origin, + const char *protocol); +SOUP_AVAILABLE_IN_2_68 +SoupWebsocketConnection *soup_websocket_connection_new_with_extensions (GIOStream *stream, + SoupURI *uri, + SoupWebsocketConnectionType type, + const char *origin, + const char *protocol, + GList *extensions); + +SOUP_AVAILABLE_IN_2_50 +GIOStream * soup_websocket_connection_get_io_stream (SoupWebsocketConnection *self); + +SOUP_AVAILABLE_IN_2_50 +SoupWebsocketConnectionType soup_websocket_connection_get_connection_type (SoupWebsocketConnection *self); + +SOUP_AVAILABLE_IN_2_50 +SoupURI * soup_websocket_connection_get_uri (SoupWebsocketConnection *self); + +SOUP_AVAILABLE_IN_2_50 +const char * soup_websocket_connection_get_origin (SoupWebsocketConnection *self); + +SOUP_AVAILABLE_IN_2_50 +const char * soup_websocket_connection_get_protocol (SoupWebsocketConnection *self); + +SOUP_AVAILABLE_IN_2_68 +GList * soup_websocket_connection_get_extensions (SoupWebsocketConnection *self); + +SOUP_AVAILABLE_IN_2_50 +SoupWebsocketState soup_websocket_connection_get_state (SoupWebsocketConnection *self); + +SOUP_AVAILABLE_IN_2_50 +gushort soup_websocket_connection_get_close_code (SoupWebsocketConnection *self); + +SOUP_AVAILABLE_IN_2_50 +const char * soup_websocket_connection_get_close_data (SoupWebsocketConnection *self); + +SOUP_AVAILABLE_IN_2_50 +void soup_websocket_connection_send_text (SoupWebsocketConnection *self, + const char *text); +SOUP_AVAILABLE_IN_2_50 +void soup_websocket_connection_send_binary (SoupWebsocketConnection *self, + gconstpointer data, + gsize length); +SOUP_AVAILABLE_IN_2_68 +void soup_websocket_connection_send_message (SoupWebsocketConnection *self, + SoupWebsocketDataType type, + GBytes *message); + +SOUP_AVAILABLE_IN_2_50 +void soup_websocket_connection_close (SoupWebsocketConnection *self, + gushort code, + const char *data); + +SOUP_AVAILABLE_IN_2_56 +guint64 soup_websocket_connection_get_max_incoming_payload_size (SoupWebsocketConnection *self); + +SOUP_AVAILABLE_IN_2_56 +void soup_websocket_connection_set_max_incoming_payload_size (SoupWebsocketConnection *self, + guint64 max_incoming_payload_size); + +SOUP_AVAILABLE_IN_2_58 +guint soup_websocket_connection_get_keepalive_interval (SoupWebsocketConnection *self); + +SOUP_AVAILABLE_IN_2_58 +void soup_websocket_connection_set_keepalive_interval (SoupWebsocketConnection *self, + guint interval); + +G_END_DECLS + +#endif /* __SOUP_WEBSOCKET_CONNECTION_H__ */ diff --git a/libsoup/websocket/soup-websocket-extension-deflate.c b/libsoup/websocket/soup-websocket-extension-deflate.c new file mode 100644 index 00000000..c7864426 --- /dev/null +++ b/libsoup/websocket/soup-websocket-extension-deflate.c @@ -0,0 +1,497 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * soup-websocket-extension-deflate.c + * + * Copyright (C) 2019 Igalia S.L. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "soup-websocket-extension-deflate.h" +#include <zlib.h> + +typedef struct { + z_stream zstream; + gboolean no_context_takeover; +} Deflater; + +typedef struct { + z_stream zstream; + gboolean uncompress_ongoing; +} Inflater; + +#define BUFFER_SIZE 4096 + +typedef enum { + PARAM_SERVER_NO_CONTEXT_TAKEOVER = 1 << 0, + PARAM_CLIENT_NO_CONTEXT_TAKEOVER = 1 << 1, + PARAM_SERVER_MAX_WINDOW_BITS = 1 << 2, + PARAM_CLIENT_MAX_WINDOW_BITS = 1 << 3 +} ParamFlags; + +typedef struct { + ParamFlags flags; + gushort server_max_window_bits; + gushort client_max_window_bits; +} Params; + +typedef struct { + Params params; + + gboolean enabled; + + Deflater deflater; + Inflater inflater; +} SoupWebsocketExtensionDeflatePrivate; + +/* + * SECTION:soup-websocket-extension-deflate + * @title: SoupWebsocketExtensionDeflate + * @short_description: A permessage-deflate WebSocketExtension + * @see_also: #SoupWebsocketExtension + * + * A SoupWebsocketExtensionDeflate is a #SoupWebsocketExtension + * implementing permessage-deflate (RFC 7692). + * + * This extension is used by default in a #SoupSession when #SoupWebsocketExtensionManager + * feature is present, and always used by #SoupServer. + * + * Since: 2.68 + */ + +/** + * SOUP_TYPE_WEBSOCKET_EXTENSION_DEFLATE: + * + * A #GType corresponding to permessage-deflate WebSocket extension. + * + * Since: 2.68 + */ + +G_DEFINE_TYPE_WITH_PRIVATE (SoupWebsocketExtensionDeflate, soup_websocket_extension_deflate, SOUP_TYPE_WEBSOCKET_EXTENSION) + +static void +soup_websocket_extension_deflate_init (SoupWebsocketExtensionDeflate *basic) +{ +} + +static void +soup_websocket_extension_deflate_finalize (GObject *object) +{ + SoupWebsocketExtensionDeflatePrivate *priv = soup_websocket_extension_deflate_get_instance_private (SOUP_WEBSOCKET_EXTENSION_DEFLATE (object)); + + if (priv->enabled) { + deflateEnd (&priv->deflater.zstream); + inflateEnd (&priv->inflater.zstream); + } + + G_OBJECT_CLASS (soup_websocket_extension_deflate_parent_class)->finalize (object); +} + +static gboolean +parse_window_bits (const char *value, + gushort *out) +{ + guint64 int_value; + char *end = NULL; + + if (!value || !*value) + return FALSE; + + int_value = g_ascii_strtoull (value, &end, 10); + if (*end != '\0') + return FALSE; + + if (int_value < 8 || int_value > 15) + return FALSE; + + *out = (gushort)int_value; + return TRUE; +} + +static gboolean +return_invalid_param_error (GError **error, + const char *param) +{ + g_set_error (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, + "Invalid parameter '%s' in permessage-deflate extension header", + param); + return FALSE; +} + +static gboolean +return_invalid_param_value_error (GError **error, + const char *param) +{ + g_set_error (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, + "Invalid value of parameter '%s' in permessage-deflate extension header", + param); + return FALSE; +} + +static gboolean +parse_params (GHashTable *params, + Params *out, + GError **error) +{ + GHashTableIter iter; + gpointer key, value; + + g_hash_table_iter_init (&iter, params); + while (g_hash_table_iter_next (&iter, &key, &value)) { + if (g_str_equal ((char *)key, "server_no_context_takeover")) { + if (value) + return return_invalid_param_value_error(error, "server_no_context_takeover"); + + out->flags |= PARAM_SERVER_NO_CONTEXT_TAKEOVER; + } else if (g_str_equal ((char *)key, "client_no_context_takeover")) { + if (value) + return return_invalid_param_value_error(error, "client_no_context_takeover"); + + out->flags |= PARAM_CLIENT_NO_CONTEXT_TAKEOVER; + } else if (g_str_equal ((char *)key, "server_max_window_bits")) { + if (!parse_window_bits ((char *)value, &out->server_max_window_bits)) + return return_invalid_param_value_error(error, "server_max_window_bits"); + + out->flags |= PARAM_SERVER_MAX_WINDOW_BITS; + } else if (g_str_equal ((char *)key, "client_max_window_bits")) { + if (value) { + if (!parse_window_bits ((char *)value, &out->client_max_window_bits)) + return return_invalid_param_value_error(error, "client_max_window_bits"); + } else { + out->client_max_window_bits = 15; + } + out->flags |= PARAM_CLIENT_MAX_WINDOW_BITS; + } else { + return return_invalid_param_error (error, (char *)key); + } + } + + return TRUE; +} + +static gboolean +soup_websocket_extension_deflate_configure (SoupWebsocketExtension *extension, + SoupWebsocketConnectionType connection_type, + GHashTable *params, + GError **error) +{ + gushort deflater_max_window_bits; + gushort inflater_max_window_bits; + SoupWebsocketExtensionDeflatePrivate *priv; + + priv = soup_websocket_extension_deflate_get_instance_private (SOUP_WEBSOCKET_EXTENSION_DEFLATE (extension)); + + if (params && !parse_params (params, &priv->params, error)) + return FALSE; + + switch (connection_type) { + case SOUP_WEBSOCKET_CONNECTION_CLIENT: + priv->deflater.no_context_takeover = priv->params.flags & PARAM_CLIENT_NO_CONTEXT_TAKEOVER; + deflater_max_window_bits = priv->params.flags & PARAM_CLIENT_MAX_WINDOW_BITS ? priv->params.client_max_window_bits : 15; + inflater_max_window_bits = priv->params.flags & PARAM_SERVER_MAX_WINDOW_BITS ? priv->params.server_max_window_bits : 15; + break; + case SOUP_WEBSOCKET_CONNECTION_SERVER: + priv->deflater.no_context_takeover = priv->params.flags & PARAM_SERVER_NO_CONTEXT_TAKEOVER; + deflater_max_window_bits = priv->params.flags & PARAM_SERVER_MAX_WINDOW_BITS ? priv->params.server_max_window_bits : 15; + inflater_max_window_bits = priv->params.flags & PARAM_CLIENT_MAX_WINDOW_BITS ? priv->params.client_max_window_bits : 15; + break; + default: + g_assert_not_reached (); + } + + /* zlib is unable to compress with window_bits=8, so use 9 + * instead. This is compatible with decompressing using + * window_bits=8. + */ + deflater_max_window_bits = MAX (deflater_max_window_bits, 9); + + /* In case of failing to initialize zlib deflater/inflater, + * we return TRUE without setting enabled = TRUE, so that the + * hanshake doesn't fail. + */ + if (deflateInit2 (&priv->deflater.zstream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -deflater_max_window_bits, 8, Z_DEFAULT_STRATEGY) != Z_OK) + return TRUE; + + if (inflateInit2 (&priv->inflater.zstream, -inflater_max_window_bits) != Z_OK) { + deflateEnd (&priv->deflater.zstream); + return TRUE; + } + + priv->enabled = TRUE; + + return TRUE; +} + +static char * +soup_websocket_extension_deflate_get_request_params (SoupWebsocketExtension *extension) +{ + return g_strdup ("; client_max_window_bits"); +} + +static char * +soup_websocket_extension_deflate_get_response_params (SoupWebsocketExtension *extension) +{ + GString *params; + SoupWebsocketExtensionDeflatePrivate *priv; + + priv = soup_websocket_extension_deflate_get_instance_private (SOUP_WEBSOCKET_EXTENSION_DEFLATE (extension)); + if (!priv->enabled) + return NULL; + + if (priv->params.flags == 0) + return NULL; + + params = g_string_new (NULL); + + if (priv->params.flags & PARAM_SERVER_NO_CONTEXT_TAKEOVER) + params = g_string_append (params, "; server_no_context_takeover"); + if (priv->params.flags & PARAM_CLIENT_NO_CONTEXT_TAKEOVER) + params = g_string_append (params, "; client_no_context_takeover"); + if (priv->params.flags & PARAM_SERVER_MAX_WINDOW_BITS) + g_string_append_printf (params, "; server_max_window_bits=%u", priv->params.server_max_window_bits); + if (priv->params.flags & PARAM_CLIENT_MAX_WINDOW_BITS) + g_string_append_printf (params, "; client_max_window_bits=%u", priv->params.client_max_window_bits); + + return g_string_free (params, FALSE); +} + +static void +deflater_reset (Deflater *deflater) +{ + if (deflater->no_context_takeover) + deflateReset (&deflater->zstream); +} + +static GBytes * +soup_websocket_extension_deflate_process_outgoing_message (SoupWebsocketExtension *extension, + guint8 *header, + GBytes *payload, + GError **error) +{ + const guint8 *payload_data; + gsize payload_length; + guint max_length; + gboolean control; + GByteArray *buffer; + gsize bytes_written; + int result; + gboolean in_sync_flush; + SoupWebsocketExtensionDeflatePrivate *priv; + + priv = soup_websocket_extension_deflate_get_instance_private (SOUP_WEBSOCKET_EXTENSION_DEFLATE (extension)); + + if (!priv->enabled) + return payload; + + control = header[0] & 0x08; + + /* Do not compress control frames */ + if (control) + return payload; + + payload_data = g_bytes_get_data (payload, &payload_length); + if (payload_length == 0) + return payload; + + /* Mark the frame as compressed using reserved bit 1 (0x40) */ + header[0] |= 0x40; + + buffer = g_byte_array_new (); + max_length = deflateBound(&priv->deflater.zstream, payload_length); + + priv->deflater.zstream.next_in = (void *)payload_data; + priv->deflater.zstream.avail_in = payload_length; + + bytes_written = 0; + priv->deflater.zstream.avail_out = 0; + + do { + gsize write_remaining; + + if (priv->deflater.zstream.avail_out == 0) { + guint write_position; + + priv->deflater.zstream.avail_out = max_length; + write_position = buffer->len; + g_byte_array_set_size (buffer, buffer->len + max_length); + priv->deflater.zstream.next_out = buffer->data + write_position; + + /* Use a fixed value for buffer increments */ + max_length = BUFFER_SIZE; + } + + write_remaining = buffer->len - bytes_written; + in_sync_flush = priv->deflater.zstream.avail_in == 0; + result = deflate (&priv->deflater.zstream, in_sync_flush ? Z_SYNC_FLUSH : Z_NO_FLUSH); + bytes_written += write_remaining - priv->deflater.zstream.avail_out; + } while (result == Z_OK); + + g_bytes_unref (payload); + + if (result != Z_BUF_ERROR || bytes_written < 4) { + g_set_error_literal (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_CLOSE_PROTOCOL_ERROR, + "Failed to compress outgoing frame"); + g_byte_array_unref (buffer); + deflater_reset (&priv->deflater); + return NULL; + } + + /* Remove 4 octets (that are 0x00 0x00 0xff 0xff) from the tail end. */ + g_byte_array_set_size (buffer, bytes_written - 4); + + deflater_reset (&priv->deflater); + + return g_byte_array_free_to_bytes (buffer); +} + +static GBytes * +soup_websocket_extension_deflate_process_incoming_message (SoupWebsocketExtension *extension, + guint8 *header, + GBytes *payload, + GError **error) +{ + const guint8 *payload_data; + gsize payload_length; + gboolean fin, control, compressed; + GByteArray *buffer; + gsize bytes_read, bytes_written; + int result; + gboolean tail_added = FALSE; + SoupWebsocketExtensionDeflatePrivate *priv; + + priv = soup_websocket_extension_deflate_get_instance_private (SOUP_WEBSOCKET_EXTENSION_DEFLATE (extension)); + + if (!priv->enabled) + return payload; + + control = header[0] & 0x08; + + /* Do not uncompress control frames */ + if (control) + return payload; + + compressed = header[0] & 0x40; + if (!priv->inflater.uncompress_ongoing && !compressed) + return payload; + + if (priv->inflater.uncompress_ongoing && compressed) { + g_set_error_literal (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_CLOSE_PROTOCOL_ERROR, + "Received a non-first frame with RSV1 flag set"); + g_bytes_unref (payload); + return NULL; + } + + /* Remove the compressed flag */ + header[0] &= ~0x40; + + fin = header[0] & 0x80; + payload_data = g_bytes_get_data (payload, &payload_length); + if (payload_length == 0 && ((!priv->inflater.uncompress_ongoing && fin) || (priv->inflater.uncompress_ongoing && !fin))) + return payload; + + priv->inflater.uncompress_ongoing = !fin; + + buffer = g_byte_array_new (); + + bytes_read = 0; + priv->inflater.zstream.next_in = (void *)payload_data; + priv->inflater.zstream.avail_in = payload_length; + + bytes_written = 0; + priv->inflater.zstream.avail_out = 0; + + do { + gsize read_remaining; + gsize write_remaining; + + if (priv->inflater.zstream.avail_out == 0) { + guint current_position; + + priv->inflater.zstream.avail_out = BUFFER_SIZE; + current_position = buffer->len; + g_byte_array_set_size (buffer, buffer->len + BUFFER_SIZE); + priv->inflater.zstream.next_out = buffer->data + current_position; + } + + if (priv->inflater.zstream.avail_in == 0 && !tail_added && fin) { + /* Append 4 octets of 0x00 0x00 0xff 0xff to the tail end */ + priv->inflater.zstream.next_in = (void *)"\x00\x00\xff\xff"; + priv->inflater.zstream.avail_in = 4; + bytes_read = 0; + tail_added = TRUE; + } + + read_remaining = tail_added ? 4 : payload_length - bytes_read; + write_remaining = buffer->len - bytes_written; + result = inflate (&priv->inflater.zstream, tail_added ? Z_FINISH : Z_NO_FLUSH); + bytes_read += read_remaining - priv->inflater.zstream.avail_in; + bytes_written += write_remaining - priv->inflater.zstream.avail_out; + if (!tail_added && result == Z_STREAM_END) { + /* Received a block with BFINAL set to 1. Reset decompression state. */ + result = inflateReset (&priv->inflater.zstream); + } + + if ((!fin && bytes_read == payload_length) || (fin && tail_added && bytes_read == 4)) + break; + } while (result == Z_OK || result == Z_BUF_ERROR); + + g_bytes_unref (payload); + + if (result != Z_OK && result != Z_BUF_ERROR) { + priv->inflater.uncompress_ongoing = FALSE; + g_set_error_literal (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_CLOSE_PROTOCOL_ERROR, + "Failed to uncompress incoming frame"); + g_byte_array_unref (buffer); + + return NULL; + } + + g_byte_array_set_size (buffer, bytes_written); + + return g_byte_array_free_to_bytes (buffer); +} + +static void +soup_websocket_extension_deflate_class_init (SoupWebsocketExtensionDeflateClass *klass) +{ + SoupWebsocketExtensionClass *extension_class = SOUP_WEBSOCKET_EXTENSION_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + extension_class->name = "permessage-deflate"; + + extension_class->configure = soup_websocket_extension_deflate_configure; + extension_class->get_request_params = soup_websocket_extension_deflate_get_request_params; + extension_class->get_response_params = soup_websocket_extension_deflate_get_response_params; + extension_class->process_outgoing_message = soup_websocket_extension_deflate_process_outgoing_message; + extension_class->process_incoming_message = soup_websocket_extension_deflate_process_incoming_message; + + object_class->finalize = soup_websocket_extension_deflate_finalize; +} diff --git a/libsoup/websocket/soup-websocket-extension-deflate.h b/libsoup/websocket/soup-websocket-extension-deflate.h new file mode 100644 index 00000000..e353965d --- /dev/null +++ b/libsoup/websocket/soup-websocket-extension-deflate.h @@ -0,0 +1,49 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * soup-websocket-extension-deflate.h + * + * Copyright (C) 2019 Igalia S.L. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __SOUP_WEBSOCKET_EXTENSION_DEFLATE_H__ +#define __SOUP_WEBSOCKET_EXTENSION_DEFLATE_H__ 1 + +#include "soup-websocket-extension.h" + +#define SOUP_TYPE_WEBSOCKET_EXTENSION_DEFLATE (soup_websocket_extension_deflate_get_type ()) +#define SOUP_WEBSOCKET_EXTENSION_DEFLATE(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), SOUP_TYPE_WEBSOCKET_EXTENSION_DEFLATE, SoupWebsocketExtensionDeflate)) +#define SOUP_IS_WEBSOCKET_EXTENSION_DEFLATE(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), SOUP_TYPE_WEBSOCKET_EXTENSION_DEFLATE)) +#define SOUP_WEBSOCKET_EXTENSION_DEFLATE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SOUP_TYPE_WEBSOCKET_EXTENSION_DEFLATE, SoupWebsocketExtensionDeflateClass)) +#define SOUP_IS_WEBSOCKET_EXTENSION_DEFLATE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SOUP_TYPE_WEBSOCKET_EXTENSION_DEFLATE)) +#define SOUP_WEBSOCKET_EXTENSION_DEFLATE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SOUP_TYPE_WEBSOCKET_EXTENSION_DEFLATE, SoupWebsocketExtensionDeflateClass)) + +typedef struct _SoupWebsocketExtensionDeflate SoupWebsocketExtensionDeflate; +typedef struct _SoupWebsocketExtensionDeflateClass SoupWebsocketExtensionDeflateClass; + +struct _SoupWebsocketExtensionDeflate { + SoupWebsocketExtension parent; +}; + +struct _SoupWebsocketExtensionDeflateClass { + SoupWebsocketExtensionClass parent_class; +}; + +SOUP_AVAILABLE_IN_2_68 +GType soup_websocket_extension_deflate_get_type (void); + +#endif /* __SOUP_WEBSOCKET_EXTENSION_DEFLATE_H__ */ diff --git a/libsoup/websocket/soup-websocket-extension-manager-private.h b/libsoup/websocket/soup-websocket-extension-manager-private.h new file mode 100644 index 00000000..b7ff618d --- /dev/null +++ b/libsoup/websocket/soup-websocket-extension-manager-private.h @@ -0,0 +1,30 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * soup-websocket-extension-manager-private.h + * + * Copyright (C) 2019 Igalia S.L. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __SOUP_WEBSOCKET_EXTENSION_MANAGER_PRIVATE_H__ +#define __SOUP_WEBSOCKET_EXTENSION_MANAGER_PRIVATE_H__ 1 + +#include "soup-websocket-extension-manager.h" + +GPtrArray *soup_websocket_extension_manager_get_supported_extensions (SoupWebsocketExtensionManager *manager); + +#endif /* __SOUP_WEBSOCKET_EXTENSION_MANAGER_PRIVATE_H__ */ diff --git a/libsoup/websocket/soup-websocket-extension-manager.c b/libsoup/websocket/soup-websocket-extension-manager.c new file mode 100644 index 00000000..69c4fd4a --- /dev/null +++ b/libsoup/websocket/soup-websocket-extension-manager.c @@ -0,0 +1,180 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * soup-websocket-extension-manager.c + * + * Copyright (C) 2019 Igalia S.L. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "soup-websocket-extension-manager.h" +#include "soup-headers.h" +#include "soup-session-feature.h" +#include "soup-websocket.h" +#include "soup-websocket-extension.h" +#include "soup-websocket-extension-deflate.h" +#include "soup-websocket-extension-manager-private.h" + +/** + * SECTION:soup-websocket-extension-manager + * @title: SoupWebsocketExtensionManager + * @short_description: WebSocket extensions manager + * @see_also: #SoupSession, #SoupWebsocketExtension + * + * SoupWebsocketExtensionManager is the #SoupSessionFeature that handles WebSockets + * extensions for a #SoupSession. + * + * A SoupWebsocketExtensionManager is added to the session by default, and normally + * you don't need to worry about it at all. However, if you want to + * disable WebSocket extensions, you can remove the feature from the + * session with soup_session_remove_feature_by_type(), or disable it on + * individual requests with soup_message_disable_feature(). + * + * Since: 2.68 + **/ + +/** + * SOUP_TYPE_WEBSOCKET_EXTENSION_MANAGER: + * + * The #GType of #SoupWebsocketExtensionManager; you can use this with + * soup_session_remove_feature_by_type() or + * soup_message_disable_feature(). + * + * Since: 2.68 + */ + +static void soup_websocket_extension_manager_session_feature_init (SoupSessionFeatureInterface *feature_interface, gpointer interface_data); + +typedef struct { + GPtrArray *extension_types; +} SoupWebsocketExtensionManagerPrivate; + +G_DEFINE_TYPE_WITH_CODE (SoupWebsocketExtensionManager, soup_websocket_extension_manager, G_TYPE_OBJECT, + G_ADD_PRIVATE (SoupWebsocketExtensionManager) + G_IMPLEMENT_INTERFACE (SOUP_TYPE_SESSION_FEATURE, + soup_websocket_extension_manager_session_feature_init)) + +static void +soup_websocket_extension_manager_init (SoupWebsocketExtensionManager *manager) +{ + SoupWebsocketExtensionManagerPrivate *priv = soup_websocket_extension_manager_get_instance_private (manager); + + priv->extension_types = g_ptr_array_new_with_free_func ((GDestroyNotify)g_type_class_unref); + + /* Use permessage-deflate extension by default */ + soup_session_feature_add_feature (SOUP_SESSION_FEATURE (manager), SOUP_TYPE_WEBSOCKET_EXTENSION_DEFLATE); +} + +static void +soup_websocket_extension_manager_finalize (GObject *object) +{ + SoupWebsocketExtensionManagerPrivate *priv; + + priv = soup_websocket_extension_manager_get_instance_private (SOUP_WEBSOCKET_EXTENSION_MANAGER (object)); + g_ptr_array_free (priv->extension_types, TRUE); + + G_OBJECT_CLASS (soup_websocket_extension_manager_parent_class)->finalize (object); +} + +static void +soup_websocket_extension_manager_class_init (SoupWebsocketExtensionManagerClass *websocket_extension_manager_class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (websocket_extension_manager_class); + + object_class->finalize = soup_websocket_extension_manager_finalize; +} + +static gboolean +soup_websocket_extension_manager_add_feature (SoupSessionFeature *feature, GType type) +{ + SoupWebsocketExtensionManagerPrivate *priv; + + if (!g_type_is_a (type, SOUP_TYPE_WEBSOCKET_EXTENSION)) + return FALSE; + + priv = soup_websocket_extension_manager_get_instance_private (SOUP_WEBSOCKET_EXTENSION_MANAGER (feature)); + g_ptr_array_add (priv->extension_types, g_type_class_ref (type)); + + return TRUE; +} + +static gboolean +soup_websocket_extension_manager_remove_feature (SoupSessionFeature *feature, GType type) +{ + SoupWebsocketExtensionManagerPrivate *priv; + SoupWebsocketExtensionClass *extension_class; + guint i; + + if (!g_type_is_a (type, SOUP_TYPE_WEBSOCKET_EXTENSION)) + return FALSE; + + priv = soup_websocket_extension_manager_get_instance_private (SOUP_WEBSOCKET_EXTENSION_MANAGER (feature)); + extension_class = g_type_class_peek (type); + + for (i = 0; i < priv->extension_types->len; i++) { + if (priv->extension_types->pdata[i] == (gpointer)extension_class) { + g_ptr_array_remove_index (priv->extension_types, i); + return TRUE; + } + } + + return FALSE; +} + +static gboolean +soup_websocket_extension_manager_has_feature (SoupSessionFeature *feature, GType type) +{ + SoupWebsocketExtensionManagerPrivate *priv; + SoupWebsocketExtensionClass *extension_class; + guint i; + + if (!g_type_is_a (type, SOUP_TYPE_WEBSOCKET_EXTENSION)) + return FALSE; + + priv = soup_websocket_extension_manager_get_instance_private (SOUP_WEBSOCKET_EXTENSION_MANAGER (feature)); + extension_class = g_type_class_peek (type); + + for (i = 0; i < priv->extension_types->len; i++) { + if (priv->extension_types->pdata[i] == (gpointer)extension_class) + return TRUE; + } + + return FALSE; +} + +static void +soup_websocket_extension_manager_session_feature_init (SoupSessionFeatureInterface *feature_interface, + gpointer interface_data) +{ + feature_interface->add_feature = soup_websocket_extension_manager_add_feature; + feature_interface->remove_feature = soup_websocket_extension_manager_remove_feature; + feature_interface->has_feature = soup_websocket_extension_manager_has_feature; +} + +GPtrArray * +soup_websocket_extension_manager_get_supported_extensions (SoupWebsocketExtensionManager *manager) +{ + SoupWebsocketExtensionManagerPrivate *priv; + + g_return_val_if_fail (SOUP_IS_WEBSOCKET_EXTENSION_MANAGER (manager), NULL); + + priv = soup_websocket_extension_manager_get_instance_private (manager); + return priv->extension_types; +} diff --git a/libsoup/websocket/soup-websocket-extension-manager.h b/libsoup/websocket/soup-websocket-extension-manager.h new file mode 100644 index 00000000..0940a53e --- /dev/null +++ b/libsoup/websocket/soup-websocket-extension-manager.h @@ -0,0 +1,50 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * soup-websocket-extension-manager.h + * + * Copyright (C) 2019 Igalia S.L. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __SOUP_WEBSOCKET_EXTENSION_MANAGER_H__ +#define __SOUP_WEBSOCKET_EXTENSION_MANAGER_H__ 1 + +#include "soup-types.h" + +G_BEGIN_DECLS + +#define SOUP_TYPE_WEBSOCKET_EXTENSION_MANAGER (soup_websocket_extension_manager_get_type ()) +#define SOUP_WEBSOCKET_EXTENSION_MANAGER(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), SOUP_TYPE_WEBSOCKET_EXTENSION_MANAGER, SoupWebsocketExtensionManager)) +#define SOUP_IS_WEBSOCKET_EXTENSION_MANAGER(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), SOUP_TYPE_WEBSOCKET_EXTENSION_MANAGER)) +#define SOUP_WEBSOCKET_EXTENSION_MANAGER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SOUP_TYPE_WEBSOCKET_EXTENSION_MANAGER, SoupWebsocketExtensionManagerClass)) +#define SOUP_IS_WEBSOCKET_EXTENSION_MANAGER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SOUP_TYPE_WEBSOCKET_EXTENSION_MANAGER)) +#define SOUP_WEBSOCKET_EXTENSION_MANAGER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SOUP_TYPE_WEBSOCKET_EXTENSION_MANAGER, SoupWebsocketExtensionManagerClass)) + +typedef struct { + GObject parent; +} SoupWebsocketExtensionManager; + +typedef struct { + GObjectClass parent_class; +} SoupWebsocketExtensionManagerClass; + +SOUP_AVAILABLE_IN_2_68 +GType soup_websocket_extension_manager_get_type (void); + +G_END_DECLS + +#endif /* __SOUP_WEBSOCKET_EXTENSION_MANAGER_H__ */ diff --git a/libsoup/websocket/soup-websocket-extension.c b/libsoup/websocket/soup-websocket-extension.c new file mode 100644 index 00000000..91ae6951 --- /dev/null +++ b/libsoup/websocket/soup-websocket-extension.c @@ -0,0 +1,221 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * soup-websocket-extension.c + * + * Copyright (C) 2019 Igalia S.L. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "soup-websocket-extension.h" + +/** + * SECTION:soup-websocket-extension + * @short_description: a WebSocket extension + * @see_also: #SoupSession, #SoupWebsocketExtensionManager + * + * SoupWebsocketExtension is the base class for WebSocket extension objects. + * + * Since: 2.68 + */ + +/** + * SoupWebsocketExtensionClass: + * @parent_class: the parent class + * @configure: called to configure the extension with the given parameters + * @get_request_params: called by the client to build the request header. + * It should include the parameters string starting with ';' + * @get_response_params: called by the server to build the response header. + * It should include the parameters string starting with ';' + * @process_outgoing_message: called to process the payload data of a message + * before it's sent. Reserved bits of the header should be changed. + * @process_incoming_message: called to process the payload data of a message + * after it's received. Reserved bits of the header should be cleared. + * + * The class structure for the SoupWebsocketExtension. + * + * Since: 2.68 + */ + +G_DEFINE_ABSTRACT_TYPE (SoupWebsocketExtension, soup_websocket_extension, G_TYPE_OBJECT) + +static void +soup_websocket_extension_init (SoupWebsocketExtension *extension) +{ +} + +static void +soup_websocket_extension_class_init (SoupWebsocketExtensionClass *auth_class) +{ +} + +/** + * soup_websocket_extension_configure: + * @extension: a #SoupWebsocketExtension + * @connection_type: either %SOUP_WEBSOCKET_CONNECTION_CLIENT or %SOUP_WEBSOCKET_CONNECTION_SERVER + * @params: (nullable): the parameters, or %NULL + * @error: return location for a #GError + * + * Configures @extension with the given @params + * + * Return value: %TRUE if extension could be configured with the given parameters, or %FALSE otherwise + */ +gboolean +soup_websocket_extension_configure (SoupWebsocketExtension *extension, + SoupWebsocketConnectionType connection_type, + GHashTable *params, + GError **error) +{ + SoupWebsocketExtensionClass *klass; + + g_return_val_if_fail (SOUP_IS_WEBSOCKET_EXTENSION (extension), FALSE); + g_return_val_if_fail (connection_type != SOUP_WEBSOCKET_CONNECTION_UNKNOWN, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + klass = SOUP_WEBSOCKET_EXTENSION_GET_CLASS (extension); + if (!klass->configure) + return TRUE; + + return klass->configure (extension, connection_type, params, error); +} + +/** + * soup_websocket_extension_get_request_params: + * @extension: a #SoupWebsocketExtension + * + * Get the parameters strings to be included in the request header. If the extension + * doesn't include any parameter in the request, this function returns %NULL. + * + * Returns: (nullable) (transfer full): a new allocated string with the parameters + * + * Since: 2.68 + */ +char * +soup_websocket_extension_get_request_params (SoupWebsocketExtension *extension) +{ + SoupWebsocketExtensionClass *klass; + + g_return_val_if_fail (SOUP_IS_WEBSOCKET_EXTENSION (extension), NULL); + + klass = SOUP_WEBSOCKET_EXTENSION_GET_CLASS (extension); + if (!klass->get_request_params) + return NULL; + + return klass->get_request_params (extension); +} + +/** + * soup_websocket_extension_get_response_params: + * @extension: a #SoupWebsocketExtension + * + * Get the parameters strings to be included in the response header. If the extension + * doesn't include any parameter in the response, this function returns %NULL. + * + * Returns: (nullable) (transfer full): a new allocated string with the parameters + * + * Since: 2.68 + */ +char * +soup_websocket_extension_get_response_params (SoupWebsocketExtension *extension) +{ + SoupWebsocketExtensionClass *klass; + + g_return_val_if_fail (SOUP_IS_WEBSOCKET_EXTENSION (extension), NULL); + + klass = SOUP_WEBSOCKET_EXTENSION_GET_CLASS (extension); + if (!klass->get_response_params) + return NULL; + + return klass->get_response_params (extension); +} + +/** + * soup_websocket_extension_process_outgoing_message: + * @extension: a #SoupWebsocketExtension + * @header: (inout): the message header + * @payload: (transfer full): the payload data + * @error: return location for a #GError + * + * Process a message before it's sent. If the payload isn't changed the given + * @payload is just returned, otherwise g_bytes_unref() is called on the given + * @payload and a new #GBytes is returned with the new data. + * + * Extensions using reserved bits of the header will change them in @header. + * + * Returns: (transfer full): the message payload data, or %NULL in case of error + * + * Since: 2.68 + */ +GBytes * +soup_websocket_extension_process_outgoing_message (SoupWebsocketExtension *extension, + guint8 *header, + GBytes *payload, + GError **error) +{ + SoupWebsocketExtensionClass *klass; + + g_return_val_if_fail (SOUP_IS_WEBSOCKET_EXTENSION (extension), NULL); + g_return_val_if_fail (header != NULL, NULL); + g_return_val_if_fail (payload != NULL, NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + klass = SOUP_WEBSOCKET_EXTENSION_GET_CLASS (extension); + if (!klass->process_outgoing_message) + return payload; + + return klass->process_outgoing_message (extension, header, payload, error); +} + +/** + * soup_websocket_extension_process_incoming_message: + * @extension: a #SoupWebsocketExtension + * @header: (inout): the message header + * @payload: (transfer full): the payload data + * @error: return location for a #GError + * + * Process a message after it's received. If the payload isn't changed the given + * @payload is just returned, otherwise g_bytes_unref() is called on the given + * @payload and a new #GBytes is returned with the new data. + * + * Extensions using reserved bits of the header will reset them in @header. + * + * Returns: (transfer full): the message payload data, or %NULL in case of error + * + * Since: 2.68 + */ +GBytes * +soup_websocket_extension_process_incoming_message (SoupWebsocketExtension *extension, + guint8 *header, + GBytes *payload, + GError **error) +{ + SoupWebsocketExtensionClass *klass; + + g_return_val_if_fail (SOUP_IS_WEBSOCKET_EXTENSION (extension), NULL); + g_return_val_if_fail (header != NULL, NULL); + g_return_val_if_fail (payload != NULL, NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + klass = SOUP_WEBSOCKET_EXTENSION_GET_CLASS (extension); + if (!klass->process_incoming_message) + return payload; + + return klass->process_incoming_message (extension, header, payload, error); +} diff --git a/libsoup/websocket/soup-websocket-extension.h b/libsoup/websocket/soup-websocket-extension.h new file mode 100644 index 00000000..e8f345a0 --- /dev/null +++ b/libsoup/websocket/soup-websocket-extension.h @@ -0,0 +1,100 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * soup-websocket-extension.h + * + * Copyright (C) 2019 Igalia S.L. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __SOUP_WEBSOCKET_EXTENSION_H__ +#define __SOUP_WEBSOCKET_EXTENSION_H__ 1 + +#include "soup-types.h" +#include "soup-websocket.h" + +G_BEGIN_DECLS + +#define SOUP_TYPE_WEBSOCKET_EXTENSION (soup_websocket_extension_get_type ()) +#define SOUP_WEBSOCKET_EXTENSION(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SOUP_TYPE_WEBSOCKET_EXTENSION, SoupWebsocketExtension)) +#define SOUP_IS_WEBSOCKET_EXTENSION(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SOUP_TYPE_WEBSOCKET_EXTENSION)) +#define SOUP_WEBSOCKET_EXTENSION_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SOUP_TYPE_WEBSOCKET_EXTENSION, SoupWebsocketExtensionClass)) +#define SOUP_IS_WEBSOCKET_EXTENSION_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((obj), SOUP_TYPE_WEBSOCKET_EXTENSION)) +#define SOUP_WEBSOCKET_EXTENSION_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SOUP_TYPE_WEBSOCKET_EXTENSION, SoupWebsocketExtensionClass)) + +struct _SoupWebsocketExtension { + GObject parent; +}; + +typedef struct { + GObjectClass parent_class; + + const char *name; + + gboolean (* configure) (SoupWebsocketExtension *extension, + SoupWebsocketConnectionType connection_type, + GHashTable *params, + GError **error); + + char *(* get_request_params) (SoupWebsocketExtension *extension); + + char *(* get_response_params) (SoupWebsocketExtension *extension); + + GBytes *(* process_outgoing_message) (SoupWebsocketExtension *extension, + guint8 *header, + GBytes *payload, + GError **error); + + GBytes *(* process_incoming_message) (SoupWebsocketExtension *extension, + guint8 *header, + GBytes *payload, + GError **error); + + /* Padding for future expansion */ + void (*_libsoup_reserved1) (void); + void (*_libsoup_reserved2) (void); + void (*_libsoup_reserved3) (void); + void (*_libsoup_reserved4) (void); +} SoupWebsocketExtensionClass; + +SOUP_AVAILABLE_IN_2_68 +GType soup_websocket_extension_get_type (void); + +SOUP_AVAILABLE_IN_2_68 +gboolean soup_websocket_extension_configure (SoupWebsocketExtension *extension, + SoupWebsocketConnectionType connection_type, + GHashTable *params, + GError **error); +SOUP_AVAILABLE_IN_2_68 +char *soup_websocket_extension_get_request_params (SoupWebsocketExtension *extension); + +SOUP_AVAILABLE_IN_2_68 +char *soup_websocket_extension_get_response_params (SoupWebsocketExtension *extension); + +SOUP_AVAILABLE_IN_2_68 +GBytes *soup_websocket_extension_process_outgoing_message (SoupWebsocketExtension *extension, + guint8 *header, + GBytes *payload, + GError **error); +SOUP_AVAILABLE_IN_2_68 +GBytes *soup_websocket_extension_process_incoming_message (SoupWebsocketExtension *extension, + guint8 *header, + GBytes *payload, + GError **error); + +G_END_DECLS + +#endif /* __SOUP_WEBSOCKET_EXTENSION_H__ */ diff --git a/libsoup/websocket/soup-websocket.c b/libsoup/websocket/soup-websocket.c new file mode 100644 index 00000000..13b56474 --- /dev/null +++ b/libsoup/websocket/soup-websocket.c @@ -0,0 +1,1030 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * soup-websocket.c: This file was originally part of Cockpit. + * + * Copyright 2013, 2014 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 this library; If not, see <http://www.gnu.org/licenses/>. + */ + +#include "config.h" + +#include <stdlib.h> +#include <string.h> +#include <glib/gi18n-lib.h> + +#include "soup-websocket.h" +#include "soup-headers.h" +#include "soup-message-private.h" +#include "soup-websocket-extension.h" + +#define FIXED_DIGEST_LEN 20 + +/** + * SECTION:soup-websocket + * @short_description: The WebSocket Protocol + * @see_also: soup_session_websocket_connect_async(), + * soup_server_add_websocket_handler() + * + * #SoupWebsocketConnection provides support for the <ulink + * url="http://tools.ietf.org/html/rfc6455">WebSocket</ulink> protocol. + * + * To connect to a WebSocket server, create a #SoupSession and call + * soup_session_websocket_connect_async(). To accept WebSocket + * connections, create a #SoupServer and add a handler to it with + * soup_server_add_websocket_handler(). + * + * (Lower-level support is available via + * soup_websocket_client_prepare_handshake() and + * soup_websocket_client_verify_handshake(), for handling the client + * side of the WebSocket handshake, and + * soup_websocket_server_process_handshake() for handling the server + * side.) + * + * #SoupWebsocketConnection handles the details of WebSocket + * communication. You can use soup_websocket_connection_send_text() + * and soup_websocket_connection_send_binary() to send data, and the + * #SoupWebsocketConnection::message signal to receive data. + * (#SoupWebsocketConnection currently only supports asynchronous + * I/O.) + * + * Since: 2.50 + */ + +/** + * SOUP_WEBSOCKET_ERROR: + * + * A #GError domain for WebSocket-related errors. Used with + * #SoupWebsocketError. + * + * Since: 2.50 + */ + +/** + * SoupWebsocketError: + * @SOUP_WEBSOCKET_ERROR_FAILED: a generic error + * @SOUP_WEBSOCKET_ERROR_NOT_WEBSOCKET: attempted to handshake with a + * server that does not appear to understand WebSockets. + * @SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE: the WebSocket handshake failed + * because some detail was invalid (eg, incorrect accept key). + * @SOUP_WEBSOCKET_ERROR_BAD_ORIGIN: the WebSocket handshake failed + * because the "Origin" header was not an allowed value. + * + * WebSocket-related errors. + * + * Since: 2.50 + */ + +/** + * SoupWebsocketConnectionType: + * @SOUP_WEBSOCKET_CONNECTION_UNKNOWN: unknown/invalid connection + * @SOUP_WEBSOCKET_CONNECTION_CLIENT: a client-side connection + * @SOUP_WEBSOCKET_CONNECTION_SERVER: a server-side connection + * + * The type of a #SoupWebsocketConnection. + * + * Since: 2.50 + */ + +/** + * SoupWebsocketDataType: + * @SOUP_WEBSOCKET_DATA_TEXT: UTF-8 text + * @SOUP_WEBSOCKET_DATA_BINARY: binary data + * + * The type of data contained in a #SoupWebsocketConnection::message + * signal. + * + * Since: 2.50 + */ + +/** + * SoupWebsocketCloseCode: + * @SOUP_WEBSOCKET_CLOSE_NORMAL: a normal, non-error close + * @SOUP_WEBSOCKET_CLOSE_GOING_AWAY: the client/server is going away + * @SOUP_WEBSOCKET_CLOSE_PROTOCOL_ERROR: a protocol error occurred + * @SOUP_WEBSOCKET_CLOSE_UNSUPPORTED_DATA: the endpoint received data + * of a type that it does not support. + * @SOUP_WEBSOCKET_CLOSE_NO_STATUS: reserved value indicating that + * no close code was present; must not be sent. + * @SOUP_WEBSOCKET_CLOSE_ABNORMAL: reserved value indicating that + * the connection was closed abnormally; must not be sent. + * @SOUP_WEBSOCKET_CLOSE_BAD_DATA: the endpoint received data that + * was invalid (eg, non-UTF-8 data in a text message). + * @SOUP_WEBSOCKET_CLOSE_POLICY_VIOLATION: generic error code + * indicating some sort of policy violation. + * @SOUP_WEBSOCKET_CLOSE_TOO_BIG: the endpoint received a message + * that is too big to process. + * @SOUP_WEBSOCKET_CLOSE_NO_EXTENSION: the client is closing the + * connection because the server failed to negotiate a required + * extension. + * @SOUP_WEBSOCKET_CLOSE_SERVER_ERROR: the server is closing the + * connection because it was unable to fulfill the request. + * @SOUP_WEBSOCKET_CLOSE_TLS_HANDSHAKE: reserved value indicating that + * the TLS handshake failed; must not be sent. + * + * Pre-defined close codes that can be passed to + * soup_websocket_connection_close() or received from + * soup_websocket_connection_get_close_code(). (However, other codes + * are also allowed.) + * + * Since: 2.50 + */ + +/** + * SoupWebsocketState: + * @SOUP_WEBSOCKET_STATE_OPEN: the connection is ready to send messages + * @SOUP_WEBSOCKET_STATE_CLOSING: the connection is in the process of + * closing down; messages may be received, but not sent + * @SOUP_WEBSOCKET_STATE_CLOSED: the connection is completely closed down + * + * The state of the WebSocket connection. + * + * Since: 2.50 + */ + +GQuark +soup_websocket_error_get_quark (void) +{ + return g_quark_from_static_string ("web-socket-error-quark"); +} + +static gboolean +validate_key (const char *key) +{ + guchar buf[18]; + int state = 0; + guint save = 0; + + /* The spec requires us to check that the key is "a + * base64-encoded value that, when decoded, is 16 bytes in + * length". + */ + if (strlen (key) != 24) + return FALSE; + if (g_base64_decode_step (key, 24, buf, &state, &save) != 16) + return FALSE; + return TRUE; +} + +static char * +compute_accept_key (const char *key) +{ + gsize digest_len = FIXED_DIGEST_LEN; + guchar digest[FIXED_DIGEST_LEN]; + GChecksum *checksum; + + if (!key) + return NULL; + + checksum = g_checksum_new (G_CHECKSUM_SHA1); + g_return_val_if_fail (checksum != NULL, NULL); + + g_checksum_update (checksum, (guchar *)key, -1); + + /* magic from: http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17 */ + g_checksum_update (checksum, (guchar *)"258EAFA5-E914-47DA-95CA-C5AB0DC85B11", -1); + + g_checksum_get_digest (checksum, digest, &digest_len); + g_checksum_free (checksum); + + g_assert (digest_len == FIXED_DIGEST_LEN); + + return g_base64_encode (digest, digest_len); +} + +static gboolean +choose_subprotocol (SoupMessage *msg, + const char **server_protocols, + const char **chosen_protocol) +{ + const char *client_protocols_str; + char **client_protocols; + int i, j; + + if (chosen_protocol) + *chosen_protocol = NULL; + + if (!server_protocols) + return TRUE; + + client_protocols_str = soup_message_headers_get_one (msg->request_headers, + "Sec-Websocket-Protocol"); + if (!client_protocols_str) + return TRUE; + + client_protocols = g_strsplit_set (client_protocols_str, ", ", -1); + if (!client_protocols || !client_protocols[0]) { + g_strfreev (client_protocols); + return TRUE; + } + + for (i = 0; server_protocols[i] != NULL; i++) { + for (j = 0; client_protocols[j] != NULL; j++) { + if (g_str_equal (server_protocols[i], client_protocols[j])) { + g_strfreev (client_protocols); + if (chosen_protocol) + *chosen_protocol = server_protocols[i]; + return TRUE; + } + } + } + + g_strfreev (client_protocols); + return FALSE; +} + +/** + * soup_websocket_client_prepare_handshake: + * @msg: a #SoupMessage + * @origin: (allow-none): the "Origin" header to set + * @protocols: (allow-none) (array zero-terminated=1): list of + * protocols to offer + * + * Adds the necessary headers to @msg to request a WebSocket + * handshake. The message body and non-WebSocket-related headers are + * not modified. + * + * Use soup_websocket_client_prepare_handshake_with_extensions() if you + * want to include "Sec-WebSocket-Extensions" header in the request. + * + * This is a low-level function; if you use + * soup_session_websocket_connect_async() to create a WebSocket + * connection, it will call this for you. + * + * Since: 2.50 + */ +void +soup_websocket_client_prepare_handshake (SoupMessage *msg, + const char *origin, + char **protocols) +{ + soup_websocket_client_prepare_handshake_with_extensions (msg, origin, protocols, NULL); +} + +/** + * soup_websocket_client_prepare_handshake_with_extensions: + * @msg: a #SoupMessage + * @origin: (nullable): the "Origin" header to set + * @protocols: (nullable) (array zero-terminated=1): list of + * protocols to offer + * @supported_extensions: (nullable) (element-type GObject.TypeClass): list + * of supported extension types + * + * Adds the necessary headers to @msg to request a WebSocket + * handshake including supported WebSocket extensions. + * The message body and non-WebSocket-related headers are + * not modified. + * + * This is a low-level function; if you use + * soup_session_websocket_connect_async() to create a WebSocket + * connection, it will call this for you. + * + * Since: 2.68 + */ +void +soup_websocket_client_prepare_handshake_with_extensions (SoupMessage *msg, + const char *origin, + char **protocols, + GPtrArray *supported_extensions) +{ + guint32 raw[4]; + char *key; + + g_return_if_fail (SOUP_IS_MESSAGE (msg)); + + soup_message_headers_replace (msg->request_headers, "Upgrade", "websocket"); + soup_message_headers_append (msg->request_headers, "Connection", "Upgrade"); + + raw[0] = g_random_int (); + raw[1] = g_random_int (); + raw[2] = g_random_int (); + raw[3] = g_random_int (); + key = g_base64_encode ((const guchar *)raw, sizeof (raw)); + soup_message_headers_replace (msg->request_headers, "Sec-WebSocket-Key", key); + g_free (key); + + soup_message_headers_replace (msg->request_headers, "Sec-WebSocket-Version", "13"); + + if (origin) + soup_message_headers_replace (msg->request_headers, "Origin", origin); + + if (protocols) { + char *protocols_str; + + protocols_str = g_strjoinv (", ", protocols); + soup_message_headers_replace (msg->request_headers, + "Sec-WebSocket-Protocol", protocols_str); + g_free (protocols_str); + } + + if (supported_extensions && supported_extensions->len > 0) { + guint i; + GString *extensions; + + extensions = g_string_new (NULL); + + for (i = 0; i < supported_extensions->len; i++) { + SoupWebsocketExtensionClass *extension_class = (SoupWebsocketExtensionClass *)supported_extensions->pdata[i]; + + if (soup_message_is_feature_disabled (msg, G_TYPE_FROM_CLASS (extension_class))) + continue; + + if (i != 0) + extensions = g_string_append (extensions, ", "); + extensions = g_string_append (extensions, extension_class->name); + + if (extension_class->get_request_params) { + SoupWebsocketExtension *websocket_extension; + gchar *params; + + websocket_extension = g_object_new (G_TYPE_FROM_CLASS (extension_class), NULL); + params = soup_websocket_extension_get_request_params (websocket_extension); + if (params) { + extensions = g_string_append (extensions, params); + g_free (params); + } + g_object_unref (websocket_extension); + } + } + + if (extensions->len > 0) { + soup_message_headers_replace (msg->request_headers, + "Sec-WebSocket-Extensions", + extensions->str); + } else { + soup_message_headers_remove (msg->request_headers, + "Sec-WebSocket-Extensions"); + } + g_string_free (extensions, TRUE); + } +} + +/** + * soup_websocket_server_check_handshake: + * @msg: #SoupMessage containing the client side of a WebSocket handshake + * @origin: (allow-none): expected Origin header + * @protocols: (allow-none) (array zero-terminated=1): allowed WebSocket + * protocols. + * @error: return location for a #GError + * + * Examines the method and request headers in @msg and determines + * whether @msg contains a valid handshake request. + * + * If @origin is non-%NULL, then only requests containing a matching + * "Origin" header will be accepted. If @protocols is non-%NULL, then + * only requests containing a compatible "Sec-WebSocket-Protocols" + * header will be accepted. + * + * Requests containing "Sec-WebSocket-Extensions" header will be + * accepted even if the header is not valid. To check a request + * with extensions you need to use + * soup_websocket_server_check_handshake_with_extensions() and provide + * the list of supported extension types. + * + * Normally soup_websocket_server_process_handshake() will take care + * of this for you, and if you use soup_server_add_websocket_handler() + * to handle accepting WebSocket connections, it will call that for + * you. However, this function may be useful if you need to perform + * more complicated validation; eg, accepting multiple different Origins, + * or handling different protocols depending on the path. + * + * Returns: %TRUE if @msg contained a valid WebSocket handshake, + * %FALSE and an error if not. + * + * Since: 2.50 + */ +gboolean +soup_websocket_server_check_handshake (SoupMessage *msg, + const char *expected_origin, + char **protocols, + GError **error) +{ + return soup_websocket_server_check_handshake_with_extensions (msg, expected_origin, protocols, NULL, error); +} + +static gboolean +websocket_extension_class_equal (gconstpointer a, + gconstpointer b) +{ + return g_str_equal (((const SoupWebsocketExtensionClass *)a)->name, (const char *)b); +} + +static GHashTable * +extract_extension_names_from_request (SoupMessage *msg) +{ + const char *extensions; + GSList *extension_list, *l; + GHashTable *return_value = NULL; + + extensions = soup_message_headers_get_list (msg->request_headers, "Sec-WebSocket-Extensions"); + if (!extensions || !*extensions) + return NULL; + + extension_list = soup_header_parse_list (extensions); + for (l = extension_list; l != NULL; l = g_slist_next (l)) { + char *extension = (char *)l->data; + char *p, *end; + + while (g_ascii_isspace (*extension)) + extension++; + + if (!*extension) + continue; + + p = strstr (extension, ";"); + end = p ? p : extension + strlen (extension); + while (end > extension && g_ascii_isspace (*(end - 1))) + end--; + *end = '\0'; + + if (!return_value) + return_value = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + g_hash_table_add (return_value, g_strdup (extension)); + } + + soup_header_free_list (extension_list); + + return return_value; +} + +static gboolean +process_extensions (SoupMessage *msg, + const char *extensions, + gboolean is_server, + GPtrArray *supported_extensions, + GList **accepted_extensions, + GError **error) +{ + GSList *extension_list, *l; + GHashTable *requested_extensions = NULL; + + if (!supported_extensions || supported_extensions->len == 0) { + if (is_server) + return TRUE; + + g_set_error_literal (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, + _("Server requested unsupported extension")); + return FALSE; + } + + if (!is_server) + requested_extensions = extract_extension_names_from_request (msg); + + extension_list = soup_header_parse_list (extensions); + for (l = extension_list; l != NULL; l = g_slist_next (l)) { + char *extension = (char *)l->data; + char *p, *end; + guint index; + GHashTable *params = NULL; + SoupWebsocketExtension *websocket_extension; + + while (g_ascii_isspace (*extension)) + extension++; + + if (!*extension) { + g_set_error (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, + is_server ? + _("Incorrect WebSocket “%s” header") : + _("Server returned incorrect “%s” key"), + "Sec-WebSocket-Extensions"); + if (accepted_extensions) + g_list_free_full (*accepted_extensions, g_object_unref); + g_clear_pointer (&requested_extensions, g_hash_table_destroy); + soup_header_free_list (extension_list); + + return FALSE; + } + + p = strstr (extension, ";"); + end = p ? p : extension + strlen (extension); + while (end > extension && g_ascii_isspace (*(end - 1))) + end--; + *end = '\0'; + + if (requested_extensions && !g_hash_table_contains (requested_extensions, extension)) { + g_set_error_literal (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, + _("Server requested unsupported extension")); + if (accepted_extensions) + g_list_free_full (*accepted_extensions, g_object_unref); + g_clear_pointer (&requested_extensions, g_hash_table_destroy); + soup_header_free_list (extension_list); + + return FALSE; + } + + if (!g_ptr_array_find_with_equal_func (supported_extensions, extension, websocket_extension_class_equal, &index)) { + if (is_server) + continue; + + g_set_error_literal (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, + _("Server requested unsupported extension")); + if (accepted_extensions) + g_list_free_full (*accepted_extensions, g_object_unref); + g_clear_pointer (&requested_extensions, g_hash_table_destroy); + soup_header_free_list (extension_list); + + return FALSE; + } + + /* If we are just checking headers in server side + * and there's no parameters, it's enough to know + * the extension is supported. + */ + if (is_server && !accepted_extensions && !p) + continue; + + websocket_extension = g_object_new (G_TYPE_FROM_CLASS (supported_extensions->pdata[index]), NULL); + if (accepted_extensions) + *accepted_extensions = g_list_prepend (*accepted_extensions, websocket_extension); + + if (p) { + params = soup_header_parse_semi_param_list_strict (p + 1); + if (!params) { + g_set_error (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, + is_server ? + _("Duplicated parameter in “%s” WebSocket extension header") : + _("Server returned a duplicated parameter in “%s” WebSocket extension header"), + extension); + if (accepted_extensions) + g_list_free_full (*accepted_extensions, g_object_unref); + else + g_object_unref (websocket_extension); + g_clear_pointer (&requested_extensions, g_hash_table_destroy); + soup_header_free_list (extension_list); + + return FALSE; + } + } + + if (!soup_websocket_extension_configure (websocket_extension, + is_server ? SOUP_WEBSOCKET_CONNECTION_SERVER : SOUP_WEBSOCKET_CONNECTION_CLIENT, + params, + error)) { + g_clear_pointer (¶ms, g_hash_table_destroy); + if (accepted_extensions) + g_list_free_full (*accepted_extensions, g_object_unref); + else + g_object_unref (websocket_extension); + g_clear_pointer (&requested_extensions, g_hash_table_destroy); + soup_header_free_list (extension_list); + + return FALSE; + } + g_clear_pointer (¶ms, g_hash_table_destroy); + if (!accepted_extensions) + g_object_unref (websocket_extension); + } + + soup_header_free_list (extension_list); + g_clear_pointer (&requested_extensions, g_hash_table_destroy); + + if (accepted_extensions) + *accepted_extensions = g_list_reverse (*accepted_extensions); + + return TRUE; +} + +/** + * soup_websocket_server_check_handshake_with_extensions: + * @msg: #SoupMessage containing the client side of a WebSocket handshake + * @origin: (nullable): expected Origin header + * @protocols: (nullable) (array zero-terminated=1): allowed WebSocket + * protocols. + * @supported_extensions: (nullable) (element-type GObject.TypeClass): list + * of supported extension types + * @error: return location for a #GError + * + * Examines the method and request headers in @msg and determines + * whether @msg contains a valid handshake request. + * + * If @origin is non-%NULL, then only requests containing a matching + * "Origin" header will be accepted. If @protocols is non-%NULL, then + * only requests containing a compatible "Sec-WebSocket-Protocols" + * header will be accepted. If @supported_extensions is non-%NULL, then + * only requests containing valid supported extensions in + * "Sec-WebSocket-Extensions" header will be accepted. + * + * Normally soup_websocket_server_process_handshake_with_extensioins() + * will take care of this for you, and if you use + * soup_server_add_websocket_handler() to handle accepting WebSocket + * connections, it will call that for you. However, this function may + * be useful if you need to perform more complicated validation; eg, + * accepting multiple different Origins, or handling different protocols + * depending on the path. + * + * Returns: %TRUE if @msg contained a valid WebSocket handshake, + * %FALSE and an error if not. + * + * Since: 2.68 + */ +gboolean +soup_websocket_server_check_handshake_with_extensions (SoupMessage *msg, + const char *expected_origin, + char **protocols, + GPtrArray *supported_extensions, + GError **error) +{ + const char *origin; + const char *key; + const char *extensions; + + g_return_val_if_fail (SOUP_IS_MESSAGE (msg), FALSE); + + if (msg->method != SOUP_METHOD_GET) { + g_set_error_literal (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_ERROR_NOT_WEBSOCKET, + _("WebSocket handshake expected")); + return FALSE; + } + + if (!soup_message_headers_header_equals (msg->request_headers, "Upgrade", "websocket") || + !soup_message_headers_header_contains (msg->request_headers, "Connection", "upgrade")) { + g_set_error_literal (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_ERROR_NOT_WEBSOCKET, + _("WebSocket handshake expected")); + return FALSE; + } + + if (!soup_message_headers_header_equals (msg->request_headers, "Sec-WebSocket-Version", "13")) { + g_set_error_literal (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, + _("Unsupported WebSocket version")); + return FALSE; + } + + key = soup_message_headers_get_one (msg->request_headers, "Sec-WebSocket-Key"); + if (key == NULL || !validate_key (key)) { + g_set_error_literal (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, + _("Invalid WebSocket key")); + return FALSE; + } + + if (expected_origin) { + origin = soup_message_headers_get_one (msg->request_headers, "Origin"); + if (!origin || g_ascii_strcasecmp (origin, expected_origin) != 0) { + g_set_error (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_ERROR_BAD_ORIGIN, + _("Incorrect WebSocket “%s” header"), "Origin"); + return FALSE; + } + } + + if (!choose_subprotocol (msg, (const char **) protocols, NULL)) { + g_set_error_literal (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, + _("Unsupported WebSocket subprotocol")); + return FALSE; + } + + extensions = soup_message_headers_get_list (msg->request_headers, "Sec-WebSocket-Extensions"); + if (extensions && *extensions) { + if (!process_extensions (msg, extensions, TRUE, supported_extensions, NULL, error)) + return FALSE; + } + + return TRUE; +} + +#define RESPONSE_FORBIDDEN "<html><head><title>400 Forbidden</title></head>\r\n" \ + "<body>Received invalid WebSocket request</body></html>\r\n" + +static void +respond_handshake_forbidden (SoupMessage *msg) +{ + soup_message_set_status (msg, SOUP_STATUS_FORBIDDEN); + soup_message_headers_append (msg->response_headers, "Connection", "close"); + soup_message_set_response (msg, "text/html", SOUP_MEMORY_COPY, + RESPONSE_FORBIDDEN, strlen (RESPONSE_FORBIDDEN)); +} + +#define RESPONSE_BAD "<html><head><title>400 Bad Request</title></head>\r\n" \ + "<body>Received invalid WebSocket request: %s</body></html>\r\n" + +static void +respond_handshake_bad (SoupMessage *msg, const char *why) +{ + char *text; + + text = g_strdup_printf (RESPONSE_BAD, why); + soup_message_set_status (msg, SOUP_STATUS_BAD_REQUEST); + soup_message_headers_append (msg->response_headers, "Connection", "close"); + soup_message_set_response (msg, "text/html", SOUP_MEMORY_TAKE, + text, strlen (text)); +} + +/** + * soup_websocket_server_process_handshake: + * @msg: #SoupMessage containing the client side of a WebSocket handshake + * @expected_origin: (allow-none): expected Origin header + * @protocols: (allow-none) (array zero-terminated=1): allowed WebSocket + * protocols. + * + * Examines the method and request headers in @msg and (assuming @msg + * contains a valid handshake request), fills in the handshake + * response. + * + * If @expected_origin is non-%NULL, then only requests containing a matching + * "Origin" header will be accepted. If @protocols is non-%NULL, then + * only requests containing a compatible "Sec-WebSocket-Protocols" + * header will be accepted. + * + * Requests containing "Sec-WebSocket-Extensions" header will be + * accepted even if the header is not valid. To process a request + * with extensions you need to use + * soup_websocket_server_process_handshake_with_extensions() and provide + * the list of supported extension types. + * + * This is a low-level function; if you use + * soup_server_add_websocket_handler() to handle accepting WebSocket + * connections, it will call this for you. + * + * Returns: %TRUE if @msg contained a valid WebSocket handshake + * request and was updated to contain a handshake response. %FALSE if not. + * + * Since: 2.50 + */ +gboolean +soup_websocket_server_process_handshake (SoupMessage *msg, + const char *expected_origin, + char **protocols) +{ + return soup_websocket_server_process_handshake_with_extensions (msg, expected_origin, protocols, NULL, NULL); +} + +/** + * soup_websocket_server_process_handshake_with_extensions: + * @msg: #SoupMessage containing the client side of a WebSocket handshake + * @expected_origin: (nullable): expected Origin header + * @protocols: (nullable) (array zero-terminated=1): allowed WebSocket + * protocols. + * @supported_extensions: (nullable) (element-type GObject.TypeClass): list + * of supported extension types + * @accepted_extensions: (out) (optional) (element-type SoupWebsocketExtension): a + * #GList of #SoupWebsocketExtension objects + * + * Examines the method and request headers in @msg and (assuming @msg + * contains a valid handshake request), fills in the handshake + * response. + * + * If @expected_origin is non-%NULL, then only requests containing a matching + * "Origin" header will be accepted. If @protocols is non-%NULL, then + * only requests containing a compatible "Sec-WebSocket-Protocols" + * header will be accepted. If @supported_extensions is non-%NULL, then + * only requests containing valid supported extensions in + * "Sec-WebSocket-Extensions" header will be accepted. The accepted extensions + * will be returned in @accepted_extensions parameter if non-%NULL. + * + * This is a low-level function; if you use + * soup_server_add_websocket_handler() to handle accepting WebSocket + * connections, it will call this for you. + * + * Returns: %TRUE if @msg contained a valid WebSocket handshake + * request and was updated to contain a handshake response. %FALSE if not. + * + * Since: 2.68 + */ +gboolean +soup_websocket_server_process_handshake_with_extensions (SoupMessage *msg, + const char *expected_origin, + char **protocols, + GPtrArray *supported_extensions, + GList **accepted_extensions) +{ + const char *chosen_protocol = NULL; + const char *key; + const char *extensions; + char *accept_key; + GError *error = NULL; + + g_return_val_if_fail (accepted_extensions == NULL || *accepted_extensions == NULL, FALSE); + + if (!soup_websocket_server_check_handshake_with_extensions (msg, expected_origin, protocols, supported_extensions, &error)) { + if (g_error_matches (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_ERROR_BAD_ORIGIN)) + respond_handshake_forbidden (msg); + else + respond_handshake_bad (msg, error->message); + g_error_free (error); + return FALSE; + } + + soup_message_set_status (msg, SOUP_STATUS_SWITCHING_PROTOCOLS); + soup_message_headers_replace (msg->response_headers, "Upgrade", "websocket"); + soup_message_headers_append (msg->response_headers, "Connection", "Upgrade"); + + key = soup_message_headers_get_one (msg->request_headers, "Sec-WebSocket-Key"); + accept_key = compute_accept_key (key); + soup_message_headers_append (msg->response_headers, "Sec-WebSocket-Accept", accept_key); + g_free (accept_key); + + choose_subprotocol (msg, (const char **) protocols, &chosen_protocol); + if (chosen_protocol) + soup_message_headers_append (msg->response_headers, "Sec-WebSocket-Protocol", chosen_protocol); + + extensions = soup_message_headers_get_list (msg->request_headers, "Sec-WebSocket-Extensions"); + if (extensions && *extensions) { + GList *websocket_extensions = NULL; + GList *l; + + process_extensions (msg, extensions, TRUE, supported_extensions, &websocket_extensions, NULL); + if (websocket_extensions) { + GString *response_extensions; + + response_extensions = g_string_new (NULL); + + for (l = websocket_extensions; l && l->data; l = g_list_next (l)) { + SoupWebsocketExtension *websocket_extension; + gchar *params; + + websocket_extension = (SoupWebsocketExtension *)l->data; + if (response_extensions->len > 0) + response_extensions = g_string_append (response_extensions, ", "); + response_extensions = g_string_append (response_extensions, SOUP_WEBSOCKET_EXTENSION_GET_CLASS (websocket_extension)->name); + params = soup_websocket_extension_get_response_params (websocket_extension); + if (params) { + response_extensions = g_string_append (response_extensions, params); + g_free (params); + } + } + + if (response_extensions->len > 0) { + soup_message_headers_replace (msg->response_headers, + "Sec-WebSocket-Extensions", + response_extensions->str); + } else { + soup_message_headers_remove (msg->response_headers, + "Sec-WebSocket-Extensions"); + } + g_string_free (response_extensions, TRUE); + + if (accepted_extensions) + *accepted_extensions = websocket_extensions; + else + g_list_free_full (websocket_extensions, g_object_unref); + } + } + + return TRUE; +} + +/** + * soup_websocket_client_verify_handshake: + * @msg: #SoupMessage containing both client and server sides of a + * WebSocket handshake + * @error: return location for a #GError + * + * Looks at the response status code and headers in @msg and + * determines if they contain a valid WebSocket handshake response + * (given the handshake request in @msg's request headers). + * + * If the response contains the "Sec-WebSocket-Extensions" header, + * the handshake will be considered invalid. You need to use + * soup_websocket_client_verify_handshake_with_extensions() to handle + * responses with extensions. + * + * This is a low-level function; if you use + * soup_session_websocket_connect_async() to create a WebSocket + * connection, it will call this for you. + * + * Returns: %TRUE if @msg contains a completed valid WebSocket + * handshake, %FALSE and an error if not. + * + * Since: 2.50 + */ +gboolean +soup_websocket_client_verify_handshake (SoupMessage *msg, + GError **error) +{ + return soup_websocket_client_verify_handshake_with_extensions (msg, NULL, NULL, error); +} + +/** + * soup_websocket_client_verify_handshake_with_extensions: + * @msg: #SoupMessage containing both client and server sides of a + * WebSocket handshake + * @supported_extensions: (nullable) (element-type GObject.TypeClass): list + * of supported extension types + * @accepted_extensions: (out) (optional) (element-type SoupWebsocketExtension): a + * #GList of #SoupWebsocketExtension objects + * @error: return location for a #GError + * + * Looks at the response status code and headers in @msg and + * determines if they contain a valid WebSocket handshake response + * (given the handshake request in @msg's request headers). + * + * If @supported_extensions is non-%NULL, extensions included in the + * response "Sec-WebSocket-Extensions" are verified too. Accepted + * extensions are returned in @accepted_extensions parameter if non-%NULL. + * + * This is a low-level function; if you use + * soup_session_websocket_connect_async() to create a WebSocket + * connection, it will call this for you. + * + * Returns: %TRUE if @msg contains a completed valid WebSocket + * handshake, %FALSE and an error if not. + * + * Since: 2.68 + */ +gboolean +soup_websocket_client_verify_handshake_with_extensions (SoupMessage *msg, + GPtrArray *supported_extensions, + GList **accepted_extensions, + GError **error) +{ + const char *protocol, *request_protocols, *extensions, *accept_key; + char *expected_accept_key; + gboolean key_ok; + + g_return_val_if_fail (SOUP_IS_MESSAGE (msg), FALSE); + g_return_val_if_fail (accepted_extensions == NULL || *accepted_extensions == NULL, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + if (msg->status_code == SOUP_STATUS_BAD_REQUEST) { + g_set_error_literal (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, + _("Server rejected WebSocket handshake")); + return FALSE; + } + + if (msg->status_code != SOUP_STATUS_SWITCHING_PROTOCOLS) { + g_set_error_literal (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_ERROR_NOT_WEBSOCKET, + _("Server ignored WebSocket handshake")); + return FALSE; + } + + if (!soup_message_headers_header_equals (msg->response_headers, "Upgrade", "websocket") || + !soup_message_headers_header_contains (msg->response_headers, "Connection", "upgrade")) { + g_set_error_literal (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_ERROR_NOT_WEBSOCKET, + _("Server ignored WebSocket handshake")); + return FALSE; + } + + protocol = soup_message_headers_get_one (msg->response_headers, "Sec-WebSocket-Protocol"); + if (protocol) { + request_protocols = soup_message_headers_get_one (msg->request_headers, "Sec-WebSocket-Protocol"); + if (!request_protocols || + !soup_header_contains (request_protocols, protocol)) { + g_set_error_literal (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, + _("Server requested unsupported protocol")); + return FALSE; + } + } + + extensions = soup_message_headers_get_list (msg->response_headers, "Sec-WebSocket-Extensions"); + if (extensions && *extensions) { + if (!process_extensions (msg, extensions, FALSE, supported_extensions, accepted_extensions, error)) + return FALSE; + } + + accept_key = soup_message_headers_get_one (msg->response_headers, "Sec-WebSocket-Accept"); + expected_accept_key = compute_accept_key (soup_message_headers_get_one (msg->request_headers, "Sec-WebSocket-Key")); + key_ok = (accept_key && expected_accept_key && + !g_ascii_strcasecmp (accept_key, expected_accept_key)); + g_free (expected_accept_key); + if (!key_ok) { + g_set_error (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, + _("Server returned incorrect “%s” key"), + "Sec-WebSocket-Accept"); + return FALSE; + } + + return TRUE; +} diff --git a/libsoup/websocket/soup-websocket.h b/libsoup/websocket/soup-websocket.h new file mode 100644 index 00000000..b96dec2b --- /dev/null +++ b/libsoup/websocket/soup-websocket.h @@ -0,0 +1,117 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * soup-websocket.h: This file was originally part of Cockpit. + * + * Copyright 2013, 2014 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 this library; If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef __SOUP_WEBSOCKET_H__ +#define __SOUP_WEBSOCKET_H__ + +#include "soup-types.h" + +G_BEGIN_DECLS + +#define SOUP_WEBSOCKET_ERROR (soup_websocket_error_get_quark ()) +SOUP_AVAILABLE_IN_2_50 +GQuark soup_websocket_error_get_quark (void) G_GNUC_CONST; + +typedef enum { + SOUP_WEBSOCKET_ERROR_FAILED, + SOUP_WEBSOCKET_ERROR_NOT_WEBSOCKET, + SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, + SOUP_WEBSOCKET_ERROR_BAD_ORIGIN, +} SoupWebsocketError; + +typedef enum { + SOUP_WEBSOCKET_CONNECTION_UNKNOWN, + SOUP_WEBSOCKET_CONNECTION_CLIENT, + SOUP_WEBSOCKET_CONNECTION_SERVER +} SoupWebsocketConnectionType; + +typedef enum { + SOUP_WEBSOCKET_DATA_TEXT = 0x01, + SOUP_WEBSOCKET_DATA_BINARY = 0x02, +} SoupWebsocketDataType; + +typedef enum { + SOUP_WEBSOCKET_CLOSE_NORMAL = 1000, + SOUP_WEBSOCKET_CLOSE_GOING_AWAY = 1001, + SOUP_WEBSOCKET_CLOSE_PROTOCOL_ERROR = 1002, + SOUP_WEBSOCKET_CLOSE_UNSUPPORTED_DATA = 1003, + SOUP_WEBSOCKET_CLOSE_NO_STATUS = 1005, + SOUP_WEBSOCKET_CLOSE_ABNORMAL = 1006, + SOUP_WEBSOCKET_CLOSE_BAD_DATA = 1007, + SOUP_WEBSOCKET_CLOSE_POLICY_VIOLATION = 1008, + SOUP_WEBSOCKET_CLOSE_TOO_BIG = 1009, + SOUP_WEBSOCKET_CLOSE_NO_EXTENSION = 1010, + SOUP_WEBSOCKET_CLOSE_SERVER_ERROR = 1011, + SOUP_WEBSOCKET_CLOSE_TLS_HANDSHAKE = 1015, +} SoupWebsocketCloseCode; + +typedef enum { + SOUP_WEBSOCKET_STATE_OPEN = 1, + SOUP_WEBSOCKET_STATE_CLOSING = 2, + SOUP_WEBSOCKET_STATE_CLOSED = 3, +} SoupWebsocketState; + +SOUP_AVAILABLE_IN_2_50 +void soup_websocket_client_prepare_handshake (SoupMessage *msg, + const char *origin, + char **protocols); +SOUP_AVAILABLE_IN_2_68 +void soup_websocket_client_prepare_handshake_with_extensions (SoupMessage *msg, + const char *origin, + char **protocols, + GPtrArray *supported_extensions); + +SOUP_AVAILABLE_IN_2_50 +gboolean soup_websocket_client_verify_handshake (SoupMessage *msg, + GError **error); +SOUP_AVAILABLE_IN_2_68 +gboolean soup_websocket_client_verify_handshake_with_extensions (SoupMessage *msg, + GPtrArray *supported_extensions, + GList **accepted_extensions, + GError **error); + +SOUP_AVAILABLE_IN_2_50 +gboolean soup_websocket_server_check_handshake (SoupMessage *msg, + const char *origin, + char **protocols, + GError **error); +SOUP_AVAILABLE_IN_2_68 +gboolean +soup_websocket_server_check_handshake_with_extensions (SoupMessage *msg, + const char *origin, + char **protocols, + GPtrArray *supported_extensions, + GError **error); + +SOUP_AVAILABLE_IN_2_50 +gboolean soup_websocket_server_process_handshake (SoupMessage *msg, + const char *expected_origin, + char **protocols); +SOUP_AVAILABLE_IN_2_68 +gboolean +soup_websocket_server_process_handshake_with_extensions (SoupMessage *msg, + const char *expected_origin, + char **protocols, + GPtrArray *supported_extensions, + GList **accepted_extensions); + +G_END_DECLS + +#endif /* __SOUP_WEBSOCKET_H__ */ |