diff options
author | Dan Winship <danw@gnome.org> | 2014-11-30 10:26:23 -0500 |
---|---|---|
committer | Dan Winship <danw@gnome.org> | 2015-02-27 17:59:04 -0500 |
commit | 53ac6213a8aa886c5ace1dc0ba8268f245aa0f3b (patch) | |
tree | 66d40d5d4a31a4fa229ffd1ee5729c64c64a1863 | |
parent | ba3cebdc08b9b9d5addccc179c5e42da4c28fa45 (diff) | |
download | libsoup-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.sgml | 1 | ||||
-rw-r--r-- | docs/reference/libsoup-2.4-sections.txt | 47 | ||||
-rw-r--r-- | libsoup/Makefile.am | 4 | ||||
-rw-r--r-- | libsoup/libsoup-2.4.sym | 23 | ||||
-rw-r--r-- | libsoup/soup-types.h | 34 | ||||
-rw-r--r-- | libsoup/soup-websocket-connection.c | 1560 | ||||
-rw-r--r-- | libsoup/soup-websocket-connection.h | 109 | ||||
-rw-r--r-- | libsoup/soup-websocket.c | 456 | ||||
-rw-r--r-- | libsoup/soup-websocket.h | 93 | ||||
-rw-r--r-- | libsoup/soup.h | 2 | ||||
-rw-r--r-- | po/POTFILES.in | 1 | ||||
-rw-r--r-- | tests/Makefile.am | 1 | ||||
-rw-r--r-- | tests/header-parsing.c | 63 | ||||
-rw-r--r-- | tests/websocket-test.c | 695 |
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; +} |