summaryrefslogtreecommitdiff
path: root/libsoup/websocket
diff options
context:
space:
mode:
authorPatrick Griffis <pgriffis@igalia.com>2020-03-10 13:58:38 -0700
committerPatrick Griffis <pgriffis@igalia.com>2020-09-19 15:41:24 -0700
commit5dcc1a9cffb351eec1e882b82c4dcf08145fa280 (patch)
tree0a08ae0d46408914453892d26fff7f9e410587a2 /libsoup/websocket
parent1b3b31371f01af8b6081e5358273b4b28ff3489b (diff)
downloadlibsoup-5dcc1a9cffb351eec1e882b82c4dcf08145fa280.tar.gz
Reorganize source tree
Diffstat (limited to 'libsoup/websocket')
-rw-r--r--libsoup/websocket/soup-websocket-connection.c2214
-rw-r--r--libsoup/websocket/soup-websocket-connection.h141
-rw-r--r--libsoup/websocket/soup-websocket-extension-deflate.c497
-rw-r--r--libsoup/websocket/soup-websocket-extension-deflate.h49
-rw-r--r--libsoup/websocket/soup-websocket-extension-manager-private.h30
-rw-r--r--libsoup/websocket/soup-websocket-extension-manager.c180
-rw-r--r--libsoup/websocket/soup-websocket-extension-manager.h50
-rw-r--r--libsoup/websocket/soup-websocket-extension.c221
-rw-r--r--libsoup/websocket/soup-websocket-extension.h100
-rw-r--r--libsoup/websocket/soup-websocket.c1030
-rw-r--r--libsoup/websocket/soup-websocket.h117
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 (&params, 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 (&params, 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__ */