summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDan Winship <danw@gnome.org>2014-11-30 10:26:23 -0500
committerDan Winship <danw@gnome.org>2015-02-27 17:59:04 -0500
commit53ac6213a8aa886c5ace1dc0ba8268f245aa0f3b (patch)
tree66d40d5d4a31a4fa229ffd1ee5729c64c64a1863
parentba3cebdc08b9b9d5addccc179c5e42da4c28fa45 (diff)
downloadlibsoup-53ac6213a8aa886c5ace1dc0ba8268f245aa0f3b.tar.gz
websockets: add WebSocket support
Add functions to create and parse WebSocket handshakes, and to communicate on a WebSocket connection. Based on code originally from the Cockpit project, and on earlier work by Lionel Landwerlin to merge that into libsoup.
-rw-r--r--docs/reference/libsoup-2.4-docs.sgml1
-rw-r--r--docs/reference/libsoup-2.4-sections.txt47
-rw-r--r--libsoup/Makefile.am4
-rw-r--r--libsoup/libsoup-2.4.sym23
-rw-r--r--libsoup/soup-types.h34
-rw-r--r--libsoup/soup-websocket-connection.c1560
-rw-r--r--libsoup/soup-websocket-connection.h109
-rw-r--r--libsoup/soup-websocket.c456
-rw-r--r--libsoup/soup-websocket.h93
-rw-r--r--libsoup/soup.h2
-rw-r--r--po/POTFILES.in1
-rw-r--r--tests/Makefile.am1
-rw-r--r--tests/header-parsing.c63
-rw-r--r--tests/websocket-test.c695
14 files changed, 3073 insertions, 16 deletions
diff --git a/docs/reference/libsoup-2.4-docs.sgml b/docs/reference/libsoup-2.4-docs.sgml
index ba33142a..713f78ab 100644
--- a/docs/reference/libsoup-2.4-docs.sgml
+++ b/docs/reference/libsoup-2.4-docs.sgml
@@ -63,6 +63,7 @@
<xi:include href="xml/soup-form.xml"/>
<xi:include href="xml/soup-xmlrpc.xml"/>
<xi:include href="xml/soup-value-utils.xml"/>
+ <xi:include href="xml/soup-websockets.xml"/>
</chapter>
<chapter>
diff --git a/docs/reference/libsoup-2.4-sections.txt b/docs/reference/libsoup-2.4-sections.txt
index 239f1eb9..68c9470f 100644
--- a/docs/reference/libsoup-2.4-sections.txt
+++ b/docs/reference/libsoup-2.4-sections.txt
@@ -1290,3 +1290,50 @@ SOUP_ENCODE_VERSION
SOUP_VERSION_CUR_STABLE
SOUP_VERSION_PREV_STABLE
</SECTION>
+
+<SECTION>
+<FILE>soup-websocket</FILE>
+<TITLE>WebSockets</TITLE>
+<SUBSECTION>
+soup_websocket_client_prepare_handshake
+soup_websocket_client_verify_handshake
+<SUBSECTION>
+soup_websocket_server_check_handshake
+soup_websocket_server_process_handshake
+<SUBSECTION>
+SoupWebsocketConnection
+SoupWebsocketConnectionType
+soup_websocket_connection_new
+soup_websocket_connection_get_io_stream
+soup_websocket_connection_get_connection_type
+soup_websocket_connection_get_uri
+soup_websocket_connection_get_origin
+soup_websocket_connection_get_protocol
+SoupWebsocketState
+soup_websocket_connection_get_state
+SoupWebsocketDataType
+soup_websocket_connection_send_text
+soup_websocket_connection_send_binary
+SoupWebsocketCloseCode
+soup_websocket_connection_close
+soup_websocket_connection_get_close_code
+soup_websocket_connection_get_close_data
+<SUBSECTION>
+SoupWebsocketError
+SOUP_WEBSOCKET_ERROR
+<SUBSECTION Private>
+SoupWebsocketConnectionClass
+SOUP_IS_WEBSOCKET_CONNECTION
+SOUP_IS_WEBSOCKET_CONNECTION_CLASS
+SOUP_TYPE_WEBSOCKET_CONNECTION
+SOUP_WEBSOCKET_CONNECTION
+SOUP_WEBSOCKET_CONNECTION_CLASS
+SOUP_WEBSOCKET_CONNECTION_GET_CLASS
+soup_websocket_close_code_get_type
+soup_websocket_connection_get_type
+soup_websocket_connection_type_get_type
+soup_websocket_data_type_get_type
+soup_websocket_error_get_quark
+soup_websocket_error_get_type
+soup_websocket_state_get_type
+</SECTION>
diff --git a/libsoup/Makefile.am b/libsoup/Makefile.am
index 3c321527..584f24ad 100644
--- a/libsoup/Makefile.am
+++ b/libsoup/Makefile.am
@@ -68,6 +68,8 @@ soup_headers = \
soup-types.h \
soup-uri.h \
soup-value-utils.h \
+ soup-websocket.h \
+ soup-websocket-connection.h \
soup-xmlrpc.h
libsoupinclude_HEADERS = \
@@ -186,6 +188,8 @@ libsoup_2_4_la_SOURCES = \
soup-uri.c \
soup-value-utils.c \
soup-version.c \
+ soup-websocket.c \
+ soup-websocket-connection.c \
soup-xmlrpc.c
# TLD rules
diff --git a/libsoup/libsoup-2.4.sym b/libsoup/libsoup-2.4.sym
index 08892f8f..4f52b842 100644
--- a/libsoup/libsoup-2.4.sym
+++ b/libsoup/libsoup-2.4.sym
@@ -513,6 +513,29 @@ soup_value_hash_lookup
soup_value_hash_lookup_vals
soup_value_hash_new
soup_value_hash_new_with_vals
+soup_websocket_client_prepare_handshake
+soup_websocket_client_verify_handshake
+soup_websocket_close_code_get_type
+soup_websocket_connection_close
+soup_websocket_connection_get_close_code
+soup_websocket_connection_get_close_data
+soup_websocket_connection_get_connection_type
+soup_websocket_connection_get_io_stream
+soup_websocket_connection_get_origin
+soup_websocket_connection_get_protocol
+soup_websocket_connection_get_state
+soup_websocket_connection_get_type
+soup_websocket_connection_get_uri
+soup_websocket_connection_new
+soup_websocket_connection_send_binary
+soup_websocket_connection_send_text
+soup_websocket_connection_type_get_type
+soup_websocket_data_type_get_type
+soup_websocket_error_get_quark
+soup_websocket_error_get_type
+soup_websocket_server_check_handshake
+soup_websocket_server_process_handshake
+soup_websocket_state_get_type
soup_xmlrpc_build_fault
soup_xmlrpc_build_method_call
soup_xmlrpc_build_method_response
diff --git a/libsoup/soup-types.h b/libsoup/soup-types.h
index 0776bdbf..e020de7d 100644
--- a/libsoup/soup-types.h
+++ b/libsoup/soup-types.h
@@ -13,22 +13,24 @@
G_BEGIN_DECLS
-typedef struct _SoupAddress SoupAddress;
-typedef struct _SoupAuth SoupAuth;
-typedef struct _SoupAuthDomain SoupAuthDomain;
-typedef struct _SoupCookie SoupCookie;
-typedef struct _SoupCookieJar SoupCookieJar;
-typedef struct _SoupDate SoupDate;
-typedef struct _SoupMessage SoupMessage;
-typedef struct _SoupRequest SoupRequest;
-typedef struct _SoupRequestHTTP SoupRequestHTTP;
-typedef struct _SoupServer SoupServer;
-typedef struct _SoupSession SoupSession;
-typedef struct _SoupSessionAsync SoupSessionAsync;
-typedef struct _SoupSessionFeature SoupSessionFeature;
-typedef struct _SoupSessionSync SoupSessionSync;
-typedef struct _SoupSocket SoupSocket;
-typedef struct _SoupURI SoupURI;
+typedef struct _SoupAddress SoupAddress;
+typedef struct _SoupAuth SoupAuth;
+typedef struct _SoupAuthDomain SoupAuthDomain;
+typedef struct _SoupCookie SoupCookie;
+typedef struct _SoupCookieJar SoupCookieJar;
+typedef struct _SoupDate SoupDate;
+typedef struct _SoupMessage SoupMessage;
+typedef struct _SoupRequest SoupRequest;
+typedef struct _SoupRequestHTTP SoupRequestHTTP;
+typedef struct _SoupServer SoupServer;
+typedef struct _SoupSession SoupSession;
+typedef struct _SoupSessionAsync SoupSessionAsync;
+typedef struct _SoupSessionFeature SoupSessionFeature;
+typedef struct _SoupSessionSync SoupSessionSync;
+typedef struct _SoupSocket SoupSocket;
+typedef struct _SoupURI SoupURI;
+typedef struct _SoupWebsocketConnection SoupWebsocketConnection;
+
/*< private >*/
typedef struct _SoupConnection SoupConnection;
diff --git a/libsoup/soup-websocket-connection.c b/libsoup/soup-websocket-connection.c
new file mode 100644
index 00000000..515d942f
--- /dev/null
+++ b/libsoup/soup-websocket-connection.c
@@ -0,0 +1,1560 @@
+/* -*- 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-message.h"
+#include "soup-uri.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
+ *
+ * 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,
+};
+
+enum {
+ MESSAGE,
+ ERROR,
+ CLOSING,
+ CLOSED,
+ NUM_SIGNALS
+};
+
+static guint signals[NUM_SIGNALS] = { 0, };
+
+typedef struct {
+ GBytes *data;
+ gboolean last;
+ gsize sent;
+ gsize amount;
+} Frame;
+
+struct _SoupWebsocketConnectionPrivate {
+ GIOStream *io_stream;
+ SoupWebsocketConnectionType connection_type;
+ SoupURI *uri;
+ char *origin;
+ char *protocol;
+
+ 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;
+};
+
+#define MAX_PAYLOAD 128 * 1024
+
+G_DEFINE_TYPE (SoupWebsocketConnection, soup_websocket_connection, G_TYPE_OBJECT)
+
+typedef enum {
+ SOUP_WEBSOCKET_QUEUE_NORMAL = 0,
+ SOUP_WEBSOCKET_QUEUE_URGENT = 1 << 0,
+ SOUP_WEBSOCKET_QUEUE_LAST = 1 << 1,
+} SoupWebsocketQueueFlags;
+
+static void queue_frame (SoupWebsocketConnection *self, SoupWebsocketQueueFlags flags,
+ gpointer data, gsize len, gsize amount);
+
+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 = G_TYPE_INSTANCE_GET_PRIVATE (self, SOUP_TYPE_WEBSOCKET_CONNECTION,
+ SoupWebsocketConnectionPrivate);
+
+ 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
+stop_input (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
+stop_output (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
+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;
+
+ close_io_stop_timeout (self);
+
+ if (!pv->io_closing) {
+ stop_input (self);
+ stop_output (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;
+ GError *error = NULL;
+
+ stop_output (self);
+
+ if (G_IS_SOCKET_CONNECTION (pv->io_stream)) {
+ socket = g_socket_connection_get_socket (G_SOCKET_CONNECTION (pv->io_stream));
+ 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 = length;
+ GByteArray *bytes;
+ gsize frame_len;
+ guint8 *outer;
+ guint8 *mask = 0;
+ guint8 *at;
+
+ bytes = g_byte_array_sized_new (14 + length);
+ outer = bytes->data;
+ outer[0] = 0x80 | opcode;
+
+ /* If control message, truncate payload */
+ if (opcode & 0x08) {
+ if (length > 125) {
+ g_warning ("Truncating WebSocket control message payload");
+ length = 125;
+ }
+
+ 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 */
+ outer[2] = (length >> 56) & 0xFF;
+ outer[3] = (length >> 48) & 0xFF;
+ outer[4] = (length >> 40) & 0xFF;
+ outer[5] = (length >> 32) & 0xFF;
+ 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) {
+ outer[1] |= 0x80;
+ mask = outer + bytes->len;
+ * ((guint32 *)mask) = g_random_int ();
+ bytes->len += 4;
+ }
+
+ at = bytes->data + bytes->len;
+ g_byte_array_append (bytes, data, length);
+
+ if (self->pv->connection_type == SOUP_WEBSOCKET_CONNECTION_CLIENT)
+ xor_with_mask (mask, at, length);
+
+ frame_len = bytes->len;
+ queue_frame (self, flags, g_byte_array_free (bytes, FALSE),
+ frame_len, buffered_amount);
+ 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;
+}
+
+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,
+ 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,
+ gsize 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 128KiB",
+ self->pv->connection_type == SOUP_WEBSOCKET_CONNECTION_SERVER ? "server" : "client",
+ payload_len);
+ emit_error_and_close (self, error, TRUE);
+
+ /* The input is in an invalid state now */
+ stop_input (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;
+
+ /* Store the code/data payload */
+ if (len >= 2) {
+ pv->peer_close_code = (guint16)data[0] << 8 | data[1];
+ }
+ if (len > 2) {
+ data += 2;
+ len -= 2;
+ if (g_utf8_validate ((char *)data, len, NULL))
+ pv->peer_close_data = g_strndup ((char *)data, len);
+ else
+ g_debug ("received non-UTF8 close data: %d '%.*s' %d", (int)len, (int)len, (char *)data, (int)data[0]);
+ }
+
+ /* 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 {
+ /* Send back the response */
+ soup_websocket_connection_close (self, pv->peer_close_code, NULL);
+ }
+}
+
+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
+process_contents (SoupWebsocketConnection *self,
+ gboolean control,
+ gboolean fin,
+ guint8 opcode,
+ gconstpointer payload,
+ gsize payload_len)
+{
+ SoupWebsocketConnectionPrivate *pv = self->pv;
+ GBytes *message;
+
+ 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:
+ break;
+ default:
+ g_debug ("received unsupported control frame: %d", (int)opcode);
+ break;
+ }
+ } 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 inital message fragment");
+ protocol_error_and_close (self);
+ return;
+ }
+ g_debug ("received inital 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);
+ }
+
+ switch (pv->message_opcode) {
+ case 0x01:
+ if (!g_utf8_validate ((char *)payload, payload_len, NULL)) {
+ 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;
+ }
+ /* fall through */
+ case 0x02:
+ g_byte_array_append (pv->message_data, payload, payload_len);
+ break;
+ default:
+ g_debug ("received unknown data frame: %d", (int)opcode);
+ break;
+ }
+
+ /* Actually deliver the message? */
+ if (fin) {
+ /* 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;
+
+ 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);
+
+ switch (header[1] & 0x7f) {
+ case 126:
+ at = 4;
+ if (len < at)
+ return FALSE; /* need more data */
+ payload_len = (((guint16)header[2] << 8) |
+ ((guint16)header[3] << 0));
+ break;
+ case 127:
+ 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));
+ break;
+ default:
+ payload_len = header[1] & 0x7f;
+ at = 2;
+ break;
+ }
+
+ /* Safety valve */
+ if (payload_len >= MAX_PAYLOAD) {
+ 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);
+ }
+
+ /* 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, payload, payload_len);
+
+ /* 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 gboolean
+on_web_socket_input (GObject *pollable_stream,
+ gpointer user_data)
+{
+ SoupWebsocketConnection *self = SOUP_WEBSOCKET_CONNECTION (user_data);
+ SoupWebsocketConnectionPrivate *pv = self->pv;
+ GError *error = NULL;
+ gboolean end = FALSE;
+ gssize count;
+ gsize len;
+
+ do {
+ len = pv->incoming->len;
+ g_byte_array_set_size (pv->incoming, len + 1024);
+
+ count = g_pollable_input_stream_read_nonblocking (pv->input,
+ pv->incoming->data + len,
+ 1024, 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 TRUE;
+ }
+ } 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 TRUE;
+}
+
+static gboolean
+on_web_socket_output (GObject *pollable_stream,
+ gpointer user_data)
+{
+ SoupWebsocketConnection *self = SOUP_WEBSOCKET_CONNECTION (user_data);
+ SoupWebsocketConnectionPrivate *pv = self->pv;
+ const guint8 *data;
+ GError *error = NULL;
+ Frame *frame;
+ gssize count;
+ gsize len;
+
+ frame = g_queue_peek_head (&pv->outgoing);
+
+ /* No more frames to send */
+ if (frame == NULL) {
+ stop_output (self);
+ return TRUE;
+ }
+
+ 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;
+ } else {
+ emit_error_and_close (self, error, TRUE);
+ return FALSE;
+ }
+ }
+
+ frame->sent += count;
+ if (frame->sent >= len) {
+ g_debug ("sent frame");
+ g_queue_pop_head (&pv->outgoing);
+
+ if (frame->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);
+ }
+
+ return TRUE;
+}
+
+static void
+start_output (SoupWebsocketConnection *self)
+{
+ SoupWebsocketConnectionPrivate *pv = self->pv;
+
+ if (pv->output_source)
+ return;
+
+ g_debug ("starting output source");
+ 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
+queue_frame (SoupWebsocketConnection *self,
+ SoupWebsocketQueueFlags flags,
+ gpointer data,
+ gsize len,
+ gsize amount)
+{
+ SoupWebsocketConnectionPrivate *pv = self->pv;
+ Frame *frame;
+ Frame *prev;
+
+ 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->last = (flags & SOUP_WEBSOCKET_QUEUE_LAST) ? TRUE : FALSE;
+
+ /* If urgent put at front of queue */
+ if (flags & SOUP_WEBSOCKET_QUEUE_URGENT) {
+ /* But we can't interrupt a message already partially sent */
+ prev = g_queue_pop_head (&pv->outgoing);
+ if (prev == NULL) {
+ g_queue_push_head (&pv->outgoing, frame);
+ } else if (prev->sent > 0) {
+ g_queue_push_head (&pv->outgoing, frame);
+ g_queue_push_head (&pv->outgoing, prev);
+ } else {
+ g_queue_push_head (&pv->outgoing, prev);
+ g_queue_push_head (&pv->outgoing, frame);
+ }
+ } else {
+ g_queue_push_tail (&pv->outgoing, frame);
+ }
+
+ start_output (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));
+
+ 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_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ SoupWebsocketConnection *self = SOUP_WEBSOCKET_CONNECTION (object);
+
+ 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;
+
+ 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;
+
+ 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);
+
+ 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_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);
+
+ g_type_class_add_private (klass, sizeof (SoupWebsocketConnectionPrivate));
+
+ 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::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);
+}
+
+/**
+ * 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)
+{
+ 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,
+ 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_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 text (UTF-8) message to the peer.
+ *
+ * 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 (g_utf8_validate (text, length, NULL));
+
+ 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): the message contents
+ * @length: the length of @data
+ *
+ * Send a binary message to the peer.
+ *
+ * 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);
+
+ send_message (self, SOUP_WEBSOCKET_QUEUE_NORMAL, 0x02, 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.
+ * Note that the @data must be UTF-8 valid.
+ *
+ * Since: 2.50
+ */
+void
+soup_websocket_connection_close (SoupWebsocketConnection *self,
+ gushort code,
+ const char *data)
+{
+ SoupWebsocketQueueFlags flags;
+ SoupWebsocketConnectionPrivate *pv;
+
+ g_return_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self));
+ pv = self->pv;
+ g_return_if_fail (!pv->close_sent);
+
+ g_signal_emit (self, signals[CLOSING], 0);
+
+ if (pv->close_received)
+ g_debug ("responding to close request");
+
+ flags = 0;
+ if (pv->connection_type == SOUP_WEBSOCKET_CONNECTION_SERVER && pv->close_received)
+ flags |= SOUP_WEBSOCKET_QUEUE_LAST;
+ send_close (self, flags, code, data);
+ close_io_after_timeout (self);
+}
diff --git a/libsoup/soup-websocket-connection.h b/libsoup/soup-websocket-connection.h
new file mode 100644
index 00000000..2f6af000
--- /dev/null
+++ b/libsoup/soup-websocket-connection.h
@@ -0,0 +1,109 @@
+/* -*- 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 <libsoup/soup-types.h>
+#include <libsoup/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);
+} SoupWebsocketConnectionClass;
+
+SOUP_AVAILABLE_IN_2_50
+GType soup_websocket_connection_get_type (void) G_GNUC_CONST;
+
+SoupWebsocketConnection *soup_websocket_connection_new (GIOStream *stream,
+ SoupURI *uri,
+ SoupWebsocketConnectionType type,
+ const char *origin,
+ const char *protocol);
+
+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_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_50
+void soup_websocket_connection_close (SoupWebsocketConnection *self,
+ gushort code,
+ const char *data);
+
+G_END_DECLS
+
+#endif /* __SOUP_WEBSOCKET_CONNECTION_H__ */
diff --git a/libsoup/soup-websocket.c b/libsoup/soup-websocket.c
new file mode 100644
index 00000000..6e54390b
--- /dev/null
+++ b/libsoup/soup-websocket.c
@@ -0,0 +1,456 @@
+/* -*- 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.h"
+
+/**
+ * SoupWebsocketState:
+ * @SOUP_WEBSOCKET_STATE_CONNECTING: the WebSocket is not yet ready to send messages
+ * @SOUP_WEBSOCKET_STATE_OPEN: the Websocket is ready to send messages
+ * @SOUP_WEBSOCKET_STATE_CLOSING: the Websocket is in the process of closing down, no further messages sent
+ * @SOUP_WEBSOCKET_STATE_CLOSED: the Websocket is completely closed down
+ *
+ * The WebSocket is in the %SOUP_WEBSOCKET_STATE_CONNECTING state during initial
+ * connection setup, and handshaking. If the handshake or connection fails it
+ * can go directly to the %SOUP_WEBSOCKET_STATE_CLOSED state from here.
+ *
+ * Once the WebSocket handshake completes successfully it will be in the
+ * %SOUP_WEBSOCKET_STATE_OPEN state. During this state, and only during this state
+ * can WebSocket messages be sent.
+ *
+ * WebSocket messages can be received during either the %SOUP_WEBSOCKET_STATE_OPEN
+ * or %SOUP_WEBSOCKET_STATE_CLOSING states.
+ *
+ * The WebSocket goes into the %SOUP_WEBSOCKET_STATE_CLOSING state once it has
+ * successfully sent a close request to the peer. If we had not yet received
+ * an earlier close request from the peer, then the WebSocket waits for a
+ * response to the close request (until a timeout).
+ *
+ * Once actually closed completely down the WebSocket state is
+ * %SOUP_WEBSOCKET_STATE_CLOSED. No communication is possible during this state.
+ *
+ * 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 = 20;
+ guchar digest[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 == 20);
+
+ 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.
+ *
+ * Since: 2.50
+ */
+void
+soup_websocket_client_prepare_handshake (SoupMessage *msg,
+ const char *origin,
+ char **protocols)
+{
+ guint32 raw[4];
+ char *key;
+
+ 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);
+ }
+}
+
+/**
+ * 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.
+ *
+ * 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)
+{
+ const char *origin;
+ const char *key;
+
+ 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;
+ }
+
+ 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
+ * @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 (assuming @msg
+ * contains a valid handshake request), fills in the handshake
+ * response.
+ *
+ * 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.
+ *
+ * Returns: %TRUE if @msg contained a valid WebSocket handshake
+ * request and was updated to contain a handshake response. %FALSE
+ * and an error if not.
+ *
+ * Since: 2.50
+ */
+gboolean
+soup_websocket_server_process_handshake (SoupMessage *msg,
+ const char *expected_origin,
+ char **protocols)
+{
+ const char *chosen_protocol = NULL;
+ const char *key;
+ char *accept_key;
+ GError *error = NULL;
+
+ if (!soup_websocket_server_check_handshake (msg, expected_origin, protocols, &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);
+
+ 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).
+ *
+ * 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)
+{
+ const char *protocol, *request_protocols, *extensions, *accept_key;
+ char *expected_accept_key;
+ gboolean key_ok;
+
+ 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) {
+ g_set_error_literal (error,
+ SOUP_WEBSOCKET_ERROR,
+ SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE,
+ _("Server requested unsupported extension"));
+ 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/soup-websocket.h b/libsoup/soup-websocket.h
new file mode 100644
index 00000000..336922cd
--- /dev/null
+++ b/libsoup/soup-websocket.h
@@ -0,0 +1,93 @@
+/* -*- 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 <libsoup/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_NO_STATUS = 1005,
+ SOUP_WEBSOCKET_CLOSE_ABNORMAL = 1006,
+ SOUP_WEBSOCKET_CLOSE_PROTOCOL = 1002,
+ SOUP_WEBSOCKET_CLOSE_UNSUPPORTED_DATA = 1003,
+ 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_50
+gboolean soup_websocket_client_verify_handshake (SoupMessage *msg,
+ 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_50
+gboolean soup_websocket_server_process_handshake (SoupMessage *msg,
+ const char *origin,
+ char **protocols);
+
+G_END_DECLS
+
+#endif /* __SOUP_WEBSOCKET_H__ */
diff --git a/libsoup/soup.h b/libsoup/soup.h
index 82a26329..7106cc5e 100644
--- a/libsoup/soup.h
+++ b/libsoup/soup.h
@@ -50,6 +50,8 @@ extern "C" {
#include <libsoup/soup-uri.h>
#include <libsoup/soup-value-utils.h>
#include <libsoup/soup-version.h>
+#include <libsoup/soup-websocket.h>
+#include <libsoup/soup-websocket-connection.h>
#include <libsoup/soup-xmlrpc.h>
#ifdef __cplusplus
diff --git a/po/POTFILES.in b/po/POTFILES.in
index ece9e952..3cd20bf4 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -8,4 +8,5 @@ libsoup/soup-request.c
libsoup/soup-server.c
libsoup/soup-session.c
libsoup/soup-socket.c
+libsoup/soup-websocket.c
libsoup/soup-tld.c
diff --git a/tests/Makefile.am b/tests/Makefile.am
index a8b9d019..662ab79d 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -41,6 +41,7 @@ test_programs = \
timeout-test \
tld-test \
uri-parsing \
+ websocket-test \
xmlrpc-server-test \
xmlrpc-test
diff --git a/tests/header-parsing.c b/tests/header-parsing.c
index fdc7885d..7bea1e95 100644
--- a/tests/header-parsing.c
+++ b/tests/header-parsing.c
@@ -608,6 +608,30 @@ static struct ResponseTest {
{ NULL } }
},
+ /********************************/
+ /*** VALID CONTINUE RESPONSES ***/
+ /********************************/
+
+ /* Tests from Cockpit project */
+
+ { "Response w/ 101 Switching Protocols + spaces after new line", NULL,
+ "HTTP/1.0 101 Switching Protocols\r\n \r\n", 38,
+ SOUP_HTTP_1_0, SOUP_STATUS_SWITCHING_PROTOCOLS, "Switching Protocols",
+ { { NULL } }
+ },
+
+ { "Response w/ 101 Switching Protocols missing \\r + spaces", NULL,
+ "HTTP/1.0 101 Switching Protocols\r\n \r\n", 40,
+ SOUP_HTTP_1_0, SOUP_STATUS_SWITCHING_PROTOCOLS, "Switching Protocols",
+ { { NULL } }
+ },
+
+ { "Response w/ 101 Switching Protocols + spaces after & before new line", NULL,
+ "HTTP/1.1 101 Switching Protocols \r\n \r\n", 42,
+ SOUP_HTTP_1_1, SOUP_STATUS_SWITCHING_PROTOCOLS, "Switching Protocols",
+ { { NULL } }
+ },
+
/*************************/
/*** INVALID RESPONSES ***/
/*************************/
@@ -689,6 +713,45 @@ static struct ResponseTest {
-1, 0, NULL,
{ { NULL } }
},
+
+ /* Failing test from Cockpit */
+
+ { "Partial response stops after HTTP/", NULL,
+ "HTTP/", -1,
+ -1, 0, NULL,
+ { { NULL } }
+ },
+
+ { "Space before HTTP/", NULL,
+ " HTTP/1.0 101 Switching Protocols\r\n ", -1,
+ -1, 0, NULL,
+ { { NULL } }
+ },
+
+ { "Missing reason", NULL,
+ "HTTP/1.0 101\r\n ", -1,
+ -1, 0, NULL,
+ { { NULL } }
+ },
+
+ { "Response code containing alphabetic character", NULL,
+ "HTTP/1.1 1A01 Switching Protocols \r\n ", -1,
+ -1, 0, NULL,
+ { { NULL } }
+ },
+
+ { "TESTONE\\r\\n", NULL,
+ "TESTONE\r\n ", -1,
+ -1, 0, NULL,
+ { { NULL } }
+ },
+
+ { "Response w/ 3 headers truncated", NULL,
+ "HTTP/1.0 200 ok\r\nHeader1: value3\r\nHeader2: field\r\nHead3: Anothe", -1,
+ -1, 0, NULL,
+ { { NULL }
+ }
+ },
};
static const int num_resptests = G_N_ELEMENTS (resptests);
diff --git a/tests/websocket-test.c b/tests/websocket-test.c
new file mode 100644
index 00000000..7e6ae05f
--- /dev/null
+++ b/tests/websocket-test.c
@@ -0,0 +1,695 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * This file was originally part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "test-utils.h"
+
+#include <sys/socket.h>
+
+/* Hack, to get compute_accept_key() */
+#include "../libsoup/soup-websocket.c"
+
+typedef struct {
+ GSocket *listener;
+ gushort port;
+
+ SoupWebsocketConnection *client;
+ SoupWebsocketConnection *server;
+
+ gboolean no_server;
+ GIOStream *raw_server;
+
+ GMutex mutex;
+} Test;
+
+#define WAIT_UNTIL(cond) \
+ G_STMT_START \
+ while (!(cond)) g_main_context_iteration (NULL, TRUE); \
+ G_STMT_END
+
+static void
+on_error_not_reached (SoupWebsocketConnection *ws,
+ GError *error,
+ gpointer user_data)
+{
+ /* At this point we know this will fail, but is informative */
+ g_assert_no_error (error);
+}
+
+static void
+on_error_copy (SoupWebsocketConnection *ws,
+ GError *error,
+ gpointer user_data)
+{
+ GError **copy = user_data;
+ g_assert (*copy == NULL);
+ *copy = g_error_copy (error);
+}
+
+static void
+setup_listener (Test *test)
+{
+ GSocketAddress *addr;
+ GError *error = NULL;
+
+ test->listener = g_socket_new (G_SOCKET_FAMILY_IPV4,
+ G_SOCKET_TYPE_STREAM,
+ G_SOCKET_PROTOCOL_TCP,
+ &error);
+ g_assert_no_error (error);
+
+ addr = g_inet_socket_address_new_from_string ("127.0.0.1", 0);
+ g_assert_no_error (error);
+
+ g_socket_bind (test->listener, addr, TRUE, &error);
+ g_assert_no_error (error);
+ g_object_unref (addr);
+
+ addr = g_socket_get_local_address (test->listener, &error);
+ g_assert_no_error (error);
+
+ test->port = g_inet_socket_address_get_port (G_INET_SOCKET_ADDRESS (addr));
+ g_object_unref (addr);
+
+ g_socket_listen (test->listener, &error);
+ g_assert_no_error (error);
+}
+
+static void
+direct_connection_complete (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ Test *test = user_data;
+ GSocketConnection *conn;
+ SoupURI *uri;
+ GError *error = NULL;
+
+ conn = g_socket_client_connect_to_host_finish (G_SOCKET_CLIENT (object),
+ result, &error);
+ g_assert_no_error (error);
+
+ uri = soup_uri_new ("http://127.0.0.1/");
+ test->client = soup_websocket_connection_new (G_IO_STREAM (conn), uri,
+ SOUP_WEBSOCKET_CONNECTION_CLIENT,
+ NULL, NULL);
+ soup_uri_free (uri);
+}
+
+static gboolean
+got_connection (GSocket *listener,
+ GIOCondition cond,
+ gpointer user_data)
+{
+ Test *test = user_data;
+ GSocket *sock;
+ GSocketConnection *conn;
+ SoupURI *uri;
+ GError *error = NULL;
+
+ sock = g_socket_accept (listener, NULL, &error);
+ g_assert_no_error (error);
+
+ conn = g_socket_connection_factory_create_connection (sock);
+ g_assert (conn != NULL);
+ g_object_unref (sock);
+
+ if (test->no_server)
+ test->raw_server = G_IO_STREAM (conn);
+ else {
+ uri = soup_uri_new ("http://127.0.0.1/");
+ test->server = soup_websocket_connection_new (G_IO_STREAM (conn), uri,
+ SOUP_WEBSOCKET_CONNECTION_SERVER,
+ NULL, NULL);
+ soup_uri_free (uri);
+ }
+
+ return FALSE;
+}
+
+static void
+setup_direct_connection (Test *test,
+ gconstpointer data)
+{
+ GSocketClient *client;
+ GSource *listen_source;
+
+ setup_listener (test);
+
+ client = g_socket_client_new ();
+ g_socket_client_connect_to_host_async (client, "127.0.0.1", test->port,
+ NULL, direct_connection_complete, test);
+
+ listen_source = g_socket_create_source (test->listener, G_IO_IN, NULL);
+ g_source_set_callback (listen_source, (GSourceFunc) got_connection, test, NULL);
+ g_source_attach (listen_source, NULL);
+
+ while (test->client == NULL || (test->server == NULL && !test->no_server))
+ g_main_context_iteration (NULL, TRUE);
+
+ g_source_destroy (listen_source);
+ g_source_unref (listen_source);
+ g_object_unref (client);
+}
+
+static void
+setup_half_direct_connection (Test *test,
+ gconstpointer data)
+{
+ test->no_server = TRUE;
+ setup_direct_connection (test, data);
+}
+
+static void
+teardown_direct_connection (Test *test,
+ gconstpointer data)
+{
+ g_clear_object (&test->listener);
+ g_clear_object (&test->client);
+ g_clear_object (&test->server);
+ g_clear_object (&test->raw_server);
+}
+
+static void
+on_text_message (SoupWebsocketConnection *ws,
+ SoupWebsocketDataType type,
+ GBytes *message,
+ gpointer user_data)
+{
+ GBytes **receive = user_data;
+
+ g_assert_cmpint (type, ==, SOUP_WEBSOCKET_DATA_TEXT);
+ g_assert (*receive == NULL);
+ g_assert (message != NULL);
+
+ *receive = g_bytes_ref (message);
+}
+
+static void
+on_close_set_flag (SoupWebsocketConnection *ws,
+ gpointer user_data)
+{
+ gboolean *flag = user_data;
+
+ g_assert (*flag == FALSE);
+
+ *flag = TRUE;
+}
+
+
+#define TEST_STRING "this is a test"
+
+static void
+test_send_client_to_server (Test *test,
+ gconstpointer data)
+{
+ GBytes *received = NULL;
+ const char *contents;
+ gsize len;
+
+ g_signal_connect (test->server, "message", G_CALLBACK (on_text_message), &received);
+
+ soup_websocket_connection_send_text (test->client, TEST_STRING);
+
+ WAIT_UNTIL (received != NULL);
+
+ /* Received messages should be null terminated (outside of len) */
+ contents = g_bytes_get_data (received, &len);
+ g_assert_cmpstr (contents, ==, TEST_STRING);
+ g_assert_cmpint (len, ==, strlen (TEST_STRING));
+
+ g_bytes_unref (received);
+}
+
+static void
+test_send_server_to_client (Test *test,
+ gconstpointer data)
+{
+ GBytes *received = NULL;
+ const char *contents;
+ gsize len;
+
+ g_signal_connect (test->client, "message", G_CALLBACK (on_text_message), &received);
+
+ soup_websocket_connection_send_text (test->server, TEST_STRING);
+
+ WAIT_UNTIL (received != NULL);
+
+ /* Received messages should be null terminated (outside of len) */
+ contents = g_bytes_get_data (received, &len);
+ g_assert_cmpstr (contents, ==, TEST_STRING);
+ g_assert_cmpint (len, ==, strlen (TEST_STRING));
+
+ g_bytes_unref (received);
+}
+
+static void
+test_send_big_packets (Test *test,
+ gconstpointer data)
+{
+ GBytes *sent = NULL;
+ GBytes *received = NULL;
+
+ g_signal_connect (test->client, "message", G_CALLBACK (on_text_message), &received);
+
+ sent = g_bytes_new_take (g_strnfill (400, '!'), 400);
+ soup_websocket_connection_send_text (test->server, g_bytes_get_data (sent, NULL));
+ WAIT_UNTIL (received != NULL);
+ g_assert (g_bytes_equal (sent, received));
+ g_bytes_unref (sent);
+ g_bytes_unref (received);
+ received = NULL;
+
+ sent = g_bytes_new_take (g_strnfill (100 * 1000, '?'), 100 * 1000);
+ soup_websocket_connection_send_text (test->server, g_bytes_get_data (sent, NULL));
+ WAIT_UNTIL (received != NULL);
+ g_assert (g_bytes_equal (sent, received));
+ g_bytes_unref (sent);
+ g_bytes_unref (received);
+}
+
+static void
+test_send_bad_data (Test *test,
+ gconstpointer unused)
+{
+ GError *error = NULL;
+ GIOStream *io;
+ gsize written;
+ const char *frame;
+
+ g_signal_handlers_disconnect_by_func (test->server, on_error_not_reached, NULL);
+ g_signal_connect (test->server, "error", G_CALLBACK (on_error_copy), &error);
+
+ io = soup_websocket_connection_get_io_stream (test->client);
+
+ /* Bad UTF-8 frame */
+ frame = "\x81\x04\xEE\xEE\xEE\xEE";
+ if (!g_output_stream_write_all (g_io_stream_get_output_stream (io),
+ frame, 6, &written, NULL, NULL))
+ g_assert_not_reached ();
+ g_assert_cmpuint (written, ==, 6);
+
+ WAIT_UNTIL (error != NULL);
+ g_assert_error (error, SOUP_WEBSOCKET_ERROR, SOUP_WEBSOCKET_CLOSE_BAD_DATA);
+ g_clear_error (&error);
+
+ WAIT_UNTIL (soup_websocket_connection_get_state (test->client) == SOUP_WEBSOCKET_STATE_CLOSED);
+
+ g_assert_cmpuint (soup_websocket_connection_get_close_code (test->client), ==, SOUP_WEBSOCKET_CLOSE_BAD_DATA);
+}
+
+static const char *negotiate_client_protocols[] = { "bbb", "ccc", NULL };
+static const char *negotiate_server_protocols[] = { "aaa", "bbb", "ccc", NULL };
+static const char *negotiated_protocol = "bbb";
+
+static void
+test_protocol_negotiate_direct (Test *test,
+ gconstpointer unused)
+{
+ SoupMessage *msg;
+ gboolean ok;
+ const char *protocol;
+ GError *error = NULL;
+
+ msg = soup_message_new ("GET", "http://127.0.0.1");
+ soup_websocket_client_prepare_handshake (msg, NULL,
+ (char **) negotiate_client_protocols);
+
+ ok = soup_websocket_server_check_handshake (msg, NULL,
+ (char **) negotiate_server_protocols,
+ &error);
+ g_assert_no_error (error);
+ g_assert_true (ok);
+
+ ok = soup_websocket_server_process_handshake (msg, NULL,
+ (char **) negotiate_server_protocols);
+ g_assert_true (ok);
+
+ protocol = soup_message_headers_get_one (msg->response_headers, "Sec-WebSocket-Protocol");
+ g_assert_cmpstr (protocol, ==, negotiated_protocol);
+
+ ok = soup_websocket_client_verify_handshake (msg, &error);
+ g_assert_no_error (error);
+ g_assert_true (ok);
+
+ g_object_unref (msg);
+}
+
+static const char *mismatch_client_protocols[] = { "ddd", NULL };
+static const char *mismatch_server_protocols[] = { "aaa", "bbb", "ccc", NULL };
+
+static void
+test_protocol_mismatch_direct (Test *test,
+ gconstpointer unused)
+{
+ SoupMessage *msg;
+ gboolean ok;
+ const char *protocol;
+ GError *error = NULL;
+
+ msg = soup_message_new ("GET", "http://127.0.0.1");
+ soup_websocket_client_prepare_handshake (msg, NULL,
+ (char **) mismatch_client_protocols);
+
+ ok = soup_websocket_server_check_handshake (msg, NULL,
+ (char **) mismatch_server_protocols,
+ &error);
+ g_assert_error (error, SOUP_WEBSOCKET_ERROR, SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE);
+ g_clear_error (&error);
+ g_assert_false (ok);
+
+ ok = soup_websocket_server_process_handshake (msg, NULL,
+ (char **) mismatch_server_protocols);
+ g_assert_false (ok);
+ soup_test_assert_message_status (msg, SOUP_STATUS_BAD_REQUEST);
+
+ protocol = soup_message_headers_get_one (msg->response_headers, "Sec-WebSocket-Protocol");
+ g_assert_cmpstr (protocol, ==, NULL);
+
+ ok = soup_websocket_client_verify_handshake (msg, &error);
+ g_assert_error (error, SOUP_WEBSOCKET_ERROR, SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE);
+ g_clear_error (&error);
+ g_assert_false (ok);
+
+ g_object_unref (msg);
+}
+
+static const char *all_protocols[] = { "aaa", "bbb", "ccc", NULL };
+
+static void
+test_protocol_server_any_direct (Test *test,
+ gconstpointer unused)
+{
+ SoupMessage *msg;
+ gboolean ok;
+ const char *protocol;
+ GError *error = NULL;
+
+ msg = soup_message_new ("GET", "http://127.0.0.1");
+ soup_websocket_client_prepare_handshake (msg, NULL, (char **) all_protocols);
+
+ ok = soup_websocket_server_check_handshake (msg, NULL, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_true (ok);
+
+ ok = soup_websocket_server_process_handshake (msg, NULL, NULL);
+ g_assert_true (ok);
+
+ protocol = soup_message_headers_get_one (msg->response_headers, "Sec-WebSocket-Protocol");
+ g_assert_cmpstr (protocol, ==, NULL);
+
+ ok = soup_websocket_client_verify_handshake (msg, &error);
+ g_assert_no_error (error);
+ g_assert_true (ok);
+
+ g_object_unref (msg);
+}
+
+static void
+test_protocol_client_any_direct (Test *test,
+ gconstpointer unused)
+{
+ SoupMessage *msg;
+ gboolean ok;
+ const char *protocol;
+ GError *error = NULL;
+
+ msg = soup_message_new ("GET", "http://127.0.0.1");
+ soup_websocket_client_prepare_handshake (msg, NULL, NULL);
+
+ ok = soup_websocket_server_check_handshake (msg, NULL, (char **) all_protocols, &error);
+ g_assert_no_error (error);
+ g_assert_true (ok);
+
+ ok = soup_websocket_server_process_handshake (msg, NULL, (char **) all_protocols);
+ g_assert_true (ok);
+
+ protocol = soup_message_headers_get_one (msg->response_headers, "Sec-WebSocket-Protocol");
+ g_assert_cmpstr (protocol, ==, NULL);
+
+ ok = soup_websocket_client_verify_handshake (msg, &error);
+ g_assert_no_error (error);
+ g_assert_true (ok);
+
+ g_object_unref (msg);
+}
+
+static void
+test_close_clean_client (Test *test,
+ gconstpointer data)
+{
+ gboolean close_event_client = FALSE;
+ gboolean close_event_server = FALSE;
+
+ g_signal_connect (test->client, "closed", G_CALLBACK (on_close_set_flag), &close_event_client);
+ g_signal_connect (test->server, "closed", G_CALLBACK (on_close_set_flag), &close_event_server);
+
+ soup_websocket_connection_close (test->client, SOUP_WEBSOCKET_CLOSE_GOING_AWAY, "give me a reason");
+ g_assert_cmpint (soup_websocket_connection_get_state (test->client), ==, SOUP_WEBSOCKET_STATE_CLOSING);
+
+ WAIT_UNTIL (soup_websocket_connection_get_state (test->server) == SOUP_WEBSOCKET_STATE_CLOSED);
+ WAIT_UNTIL (soup_websocket_connection_get_state (test->client) == SOUP_WEBSOCKET_STATE_CLOSED);
+
+ g_assert (close_event_client);
+ g_assert (close_event_server);
+
+ g_assert_cmpint (soup_websocket_connection_get_close_code (test->client), ==, SOUP_WEBSOCKET_CLOSE_GOING_AWAY);
+ g_assert_cmpint (soup_websocket_connection_get_close_code (test->server), ==, SOUP_WEBSOCKET_CLOSE_GOING_AWAY);
+ g_assert_cmpstr (soup_websocket_connection_get_close_data (test->server), ==, "give me a reason");
+}
+
+static void
+test_close_clean_server (Test *test,
+ gconstpointer data)
+{
+ gboolean close_event_client = FALSE;
+ gboolean close_event_server = FALSE;
+
+ g_signal_connect (test->client, "closed", G_CALLBACK (on_close_set_flag), &close_event_client);
+ g_signal_connect (test->server, "closed", G_CALLBACK (on_close_set_flag), &close_event_server);
+
+ soup_websocket_connection_close (test->server, SOUP_WEBSOCKET_CLOSE_GOING_AWAY, "another reason");
+ g_assert_cmpint (soup_websocket_connection_get_state (test->server), ==, SOUP_WEBSOCKET_STATE_CLOSING);
+
+ WAIT_UNTIL (soup_websocket_connection_get_state (test->server) == SOUP_WEBSOCKET_STATE_CLOSED);
+ WAIT_UNTIL (soup_websocket_connection_get_state (test->client) == SOUP_WEBSOCKET_STATE_CLOSED);
+
+ g_assert (close_event_client);
+ g_assert (close_event_server);
+
+ g_assert_cmpint (soup_websocket_connection_get_close_code (test->server), ==, SOUP_WEBSOCKET_CLOSE_GOING_AWAY);
+ g_assert_cmpint (soup_websocket_connection_get_close_code (test->client), ==, SOUP_WEBSOCKET_CLOSE_GOING_AWAY);
+ g_assert_cmpstr (soup_websocket_connection_get_close_data (test->client), ==, "another reason");
+}
+
+static gboolean
+on_closing_send_message (SoupWebsocketConnection *ws,
+ gpointer data)
+{
+ GBytes *message = data;
+
+ soup_websocket_connection_send_text (ws, g_bytes_get_data (message, NULL));
+ g_signal_handlers_disconnect_by_func (ws, on_closing_send_message, data);
+ return TRUE;
+}
+
+static void
+test_message_after_closing (Test *test,
+ gconstpointer data)
+{
+ gboolean close_event_client = FALSE;
+ gboolean close_event_server = FALSE;
+ GBytes *received = NULL;
+ GBytes *message;
+
+ message = g_bytes_new_static ("another test because", strlen ("another test because"));
+ g_signal_connect (test->client, "closed", G_CALLBACK (on_close_set_flag), &close_event_client);
+ g_signal_connect (test->client, "message", G_CALLBACK (on_text_message), &received);
+ g_signal_connect (test->server, "closed", G_CALLBACK (on_close_set_flag), &close_event_server);
+ g_signal_connect (test->server, "closing", G_CALLBACK (on_closing_send_message), message);
+
+ soup_websocket_connection_close (test->client, SOUP_WEBSOCKET_CLOSE_GOING_AWAY, "another reason");
+ g_assert_cmpint (soup_websocket_connection_get_state (test->client), ==, SOUP_WEBSOCKET_STATE_CLOSING);
+
+ WAIT_UNTIL (soup_websocket_connection_get_state (test->server) == SOUP_WEBSOCKET_STATE_CLOSED);
+ WAIT_UNTIL (soup_websocket_connection_get_state (test->client) == SOUP_WEBSOCKET_STATE_CLOSED);
+
+ g_assert (close_event_client);
+ g_assert (close_event_server);
+
+ g_assert (received != NULL);
+ g_assert (g_bytes_equal (message, received));
+
+ g_bytes_unref (received);
+ g_bytes_unref (message);
+}
+
+static gpointer
+timeout_server_thread (gpointer user_data)
+{
+ Test *test = user_data;
+ GError *error = NULL;
+
+ /* don't close until the client has timed out */
+ g_mutex_lock (&test->mutex);
+ g_mutex_unlock (&test->mutex);
+
+ g_io_stream_close (test->raw_server, NULL, &error);
+ g_assert_no_error (error);
+
+ return NULL;
+}
+
+static void
+test_close_after_timeout (Test *test,
+ gconstpointer data)
+{
+ gboolean close_event = FALSE;
+ GThread *thread;
+
+ g_mutex_lock (&test->mutex);
+
+ /* Note that no real server is around in this test, so no close happens */
+ thread = g_thread_new ("timeout-thread", timeout_server_thread, test);
+
+ g_signal_connect (test->client, "closed", G_CALLBACK (on_close_set_flag), &close_event);
+ g_signal_connect (test->client, "error", G_CALLBACK (on_error_not_reached), NULL);
+
+ /* Now try and close things */
+ soup_websocket_connection_close (test->client, 0, NULL);
+ g_assert_cmpint (soup_websocket_connection_get_state (test->client), ==, SOUP_WEBSOCKET_STATE_CLOSING);
+
+ WAIT_UNTIL (soup_websocket_connection_get_state (test->client) == SOUP_WEBSOCKET_STATE_CLOSED);
+
+ g_assert (close_event == TRUE);
+
+ /* Now actually close the server side stream */
+ g_mutex_unlock (&test->mutex);
+ g_thread_join (thread);
+}
+
+static gpointer
+send_fragments_server_thread (gpointer user_data)
+{
+ Test *test = user_data;
+ gsize written;
+ const char fragments[] = "\x01\x04""one " /* !fin | opcode */
+ "\x00\x04""two " /* !fin | no opcode */
+ "\x80\x05""three"; /* fin | no opcode */
+ GError *error = NULL;
+
+ g_output_stream_write_all (g_io_stream_get_output_stream (test->raw_server),
+ fragments, sizeof (fragments) -1, &written, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_cmpuint (written, ==, sizeof (fragments) - 1);
+ g_io_stream_close (test->raw_server, NULL, &error);
+ g_assert_no_error (error);
+
+ return NULL;
+}
+
+static void
+test_receive_fragmented (Test *test,
+ gconstpointer data)
+{
+ GThread *thread;
+ GBytes *received = NULL;
+ GBytes *expect;
+
+ thread = g_thread_new ("fragment-thread", send_fragments_server_thread, test);
+
+ g_signal_connect (test->client, "error", G_CALLBACK (on_error_not_reached), NULL);
+ g_signal_connect (test->client, "message", G_CALLBACK (on_text_message), &received);
+
+ WAIT_UNTIL (received != NULL);
+ expect = g_bytes_new ("one two three", 13);
+ g_assert (g_bytes_equal (expect, received));
+ g_bytes_unref (expect);
+ g_bytes_unref (received);
+
+ g_thread_join (thread);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ int ret;
+
+ test_init (argc, argv, NULL);
+
+ g_test_add ("/websocket/direct/send-client-to-server", Test, NULL,
+ setup_direct_connection,
+ test_send_client_to_server,
+ teardown_direct_connection);
+ g_test_add ("/websocket/direct/send-server-to-client", Test, NULL,
+ setup_direct_connection,
+ test_send_server_to_client,
+ teardown_direct_connection);
+ g_test_add ("/websocket/direct/send-big-packets", Test, NULL,
+ setup_direct_connection,
+ test_send_big_packets,
+ teardown_direct_connection);
+ g_test_add ("/websocket/direct/send-bad-data", Test, NULL,
+ setup_direct_connection,
+ test_send_bad_data,
+ teardown_direct_connection);
+ g_test_add ("/websocket/direct/close-clean-client", Test, NULL,
+ setup_direct_connection,
+ test_close_clean_client,
+ teardown_direct_connection);
+ g_test_add ("/websocket/direct/close-clean-server", Test, NULL,
+ setup_direct_connection,
+ test_close_clean_server,
+ teardown_direct_connection);
+ g_test_add ("/websocket/direct/message-after-closing", Test, NULL,
+ setup_direct_connection,
+ test_message_after_closing,
+ teardown_direct_connection);
+
+ g_test_add ("/websocket/direct/protocol-negotiate", Test, NULL, NULL,
+ test_protocol_negotiate_direct,
+ NULL);
+ g_test_add ("/websocket/direct/protocol-mismatch", Test, NULL, NULL,
+ test_protocol_mismatch_direct,
+ NULL);
+ g_test_add ("/websocket/direct/protocol-server-any", Test, NULL, NULL,
+ test_protocol_server_any_direct,
+ NULL);
+ g_test_add ("/websocket/direct/protocol-client-any", Test, NULL, NULL,
+ test_protocol_client_any_direct,
+ NULL);
+
+ g_test_add ("/websocket/direct/receive-fragmented", Test, NULL,
+ setup_half_direct_connection,
+ test_receive_fragmented,
+ teardown_direct_connection);
+
+ if (g_test_slow ()) {
+ g_test_add ("/websocket/direct/close-after-timeout", Test, NULL,
+ setup_half_direct_connection,
+ test_close_after_timeout,
+ teardown_direct_connection);
+ }
+
+ ret = g_test_run ();
+
+ test_cleanup ();
+ return ret;
+}