diff options
author | Carlos Garcia Campos <cgarcia@igalia.com> | 2019-07-04 09:31:39 +0200 |
---|---|---|
committer | Carlos Garcia Campos <carlosgc@gnome.org> | 2019-07-31 14:09:00 +0200 |
commit | 13cea0fdf2c935af4e38849c8cd550e9b654b2b9 (patch) | |
tree | 6410cc559e45dd7bf5ab426b042861dd168a8f66 | |
parent | 5c45253243a2deca4880d4f614f2932373445cf9 (diff) | |
download | libsoup-13cea0fdf2c935af4e38849c8cd550e9b654b2b9.tar.gz |
WebSockets: add support for permessage-deflate extension
Add new API to add WebSocket extensions to SoupSession and SoupServer
and include an implementation of permessage-deflate extension (see RFC
7692). In the client side, supported extensions are added to the session
as sub-features of a new session feature, SoupWebsocketExtensionManager.
In the client side, supported extensions are added/removed directly using
the new SoupServer API. All functions to negotiate the handshake
(client_prepare, client_verify, server_check and server_process) have
now a _with_extensions alternative to handle the extensions.
25 files changed, 2663 insertions, 98 deletions
diff --git a/docs/reference/libsoup-2.4-sections.txt b/docs/reference/libsoup-2.4-sections.txt index ec95d20e..b36a16e9 100644 --- a/docs/reference/libsoup-2.4-sections.txt +++ b/docs/reference/libsoup-2.4-sections.txt @@ -240,8 +240,12 @@ soup_server_add_handler soup_server_add_early_handler soup_server_remove_handler <SUBSECTION> +SOUP_SERVER_ADD_WEBSOCKET_EXTENSION +SOUP_SERVER_REMOVE_WEBSOCKET_EXTENSION SoupServerWebsocketCallback soup_server_add_websocket_handler +soup_server_add_websocket_extension +soup_server_remove_websocket_extension <SUBSECTION> SoupClientContext soup_client_context_get_local_address @@ -1304,19 +1308,25 @@ SOUP_VERSION_PREV_STABLE <TITLE>WebSockets</TITLE> <SUBSECTION> soup_websocket_client_prepare_handshake +soup_websocket_client_prepare_handshake_with_extensions soup_websocket_client_verify_handshake +soup_websocket_client_verify_handshake_with_extensions <SUBSECTION> soup_websocket_server_check_handshake +soup_websocket_server_check_handshake_with_extensions soup_websocket_server_process_handshake +soup_websocket_server_process_handshake_with_extensions <SUBSECTION> SoupWebsocketConnection SoupWebsocketConnectionType soup_websocket_connection_new +soup_websocket_connection_new_with_extensions 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 +soup_websocket_connection_get_extensions SoupWebsocketState soup_websocket_connection_get_state SoupWebsocketDataType @@ -1328,20 +1338,54 @@ soup_websocket_connection_close soup_websocket_connection_get_close_code soup_websocket_connection_get_close_data <SUBSECTION> +SoupWebsocketExtensionManager +<SUBSECTION> +SoupWebsocketExtension +SoupWebsocketExtensionDeflate +soup_websocket_extension_configure +soup_websocket_extension_get_request_params +soup_websocket_extension_get_response_params +soup_websocket_extension_process_outgoing_message +soup_websocket_extension_process_incoming_message +<SUBSECTION> SoupWebsocketError SOUP_WEBSOCKET_ERROR <SUBSECTION Private> SoupWebsocketConnectionClass SoupWebsocketConnectionPrivate +SoupWebsocketExtensionManagerClass +SoupWebsocketExtensionClass +SoupWebsocketExtensionDeflateClass 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_IS_WEBSOCKET_EXTENSION_MANAGER +SOUP_IS_WEBSOCKET_EXTENSION_MANAGER_CLASS +SOUP_TYPE_WEBSOCKET_EXTENSION_MANAGER +SOUP_WEBSOCKET_EXTENSION_MANAGER +SOUP_WEBSOCKET_EXTENSION_MANAGER_CLASS +SOUP_WEBSOCKET_EXTENSION_MANAGER_GET_CLASS +SOUP_IS_WEBSOCKET_EXTENSION +SOUP_IS_WEBSOCKET_EXTENSION_CLASS +SOUP_TYPE_WEBSOCKET_EXTENSION +SOUP_WEBSOCKET_EXTENSION +SOUP_WEBSOCKET_EXTENSION_CLASS +SOUP_WEBSOCKET_EXTENSION_GET_CLASS +SOUP_IS_WEBSOCKET_EXTENSION_DEFLATE +SOUP_IS_WEBSOCKET_EXTENSION_DEFLATE_CLASS +SOUP_TYPE_WEBSOCKET_EXTENSION_DEFLATE +SOUP_WEBSOCKET_EXTENSION_DEFLATE +SOUP_WEBSOCKET_EXTENSION_DEFLATE_CLASS +SOUP_WEBSOCKET_EXTENSION_DEFLATE_GET_CLASS soup_websocket_close_code_get_type soup_websocket_connection_get_type soup_websocket_connection_type_get_type +soup_websocket_extension_manager_get_type +soup_websocket_extension_get_type +soup_websocket_extension_deflate_get_type soup_websocket_data_type_get_type soup_websocket_error_get_quark soup_websocket_error_get_type diff --git a/docs/reference/meson.build b/docs/reference/meson.build index 86c5bda9..89413822 100644 --- a/docs/reference/meson.build +++ b/docs/reference/meson.build @@ -40,6 +40,7 @@ ignore_headers = [ 'soup-cache-client-input-stream.h', 'soup-socket-private.h', 'soup-value-utils.h', + 'soup-websocket-extension-manager-private.h', 'soup-xmlrpc-old.h' ] diff --git a/libsoup/meson.build b/libsoup/meson.build index 845e233f..73bb1188 100644 --- a/libsoup/meson.build +++ b/libsoup/meson.build @@ -75,6 +75,9 @@ soup_sources = [ 'soup-version.c', 'soup-websocket.c', 'soup-websocket-connection.c', + 'soup-websocket-extension.c', + 'soup-websocket-extension-deflate.c', + 'soup-websocket-extension-manager.c', 'soup-xmlrpc.c', 'soup-xmlrpc-old.c', ] @@ -106,6 +109,7 @@ soup_headers = [ 'soup-proxy-resolver-wrapper.h', 'soup-session-private.h', 'soup-socket-private.h', + 'soup-websocket-extension-manager-private.h', ] soup_introspection_headers = [ @@ -160,6 +164,9 @@ soup_introspection_headers = [ 'soup-value-utils.h', 'soup-websocket.h', 'soup-websocket-connection.h', + 'soup-websocket-extension.h', + 'soup-websocket-extension-deflate.h', + 'soup-websocket-extension-manager.h', 'soup-xmlrpc.h', 'soup-xmlrpc-old.h', ] @@ -234,6 +241,7 @@ deps = [ libpsl_dep, brotlidec_dep, platform_deps, + libz_dep, ] libsoup = library('soup-@0@'.format(apiversion), diff --git a/libsoup/soup-message-private.h b/libsoup/soup-message-private.h index dcca1603..dd345bd1 100644 --- a/libsoup/soup-message-private.h +++ b/libsoup/soup-message-private.h @@ -140,6 +140,8 @@ GInputStream *soup_message_io_get_response_istream (SoupMessage *msg, gboolean soup_message_disables_feature (SoupMessage *msg, gpointer feature); +gboolean soup_message_disables_feature_by_type (SoupMessage *msg, + GType feature_type); GSList *soup_message_get_disabled_features (SoupMessage *msg); diff --git a/libsoup/soup-message.c b/libsoup/soup-message.c index da6a3716..f61f58c2 100644 --- a/libsoup/soup-message.c +++ b/libsoup/soup-message.c @@ -1860,6 +1860,23 @@ soup_message_disables_feature (SoupMessage *msg, gpointer feature) return FALSE; } +gboolean +soup_message_disables_feature_by_type (SoupMessage *msg, GType feature_type) +{ + SoupMessagePrivate *priv; + GSList *f; + + g_return_val_if_fail (SOUP_IS_MESSAGE (msg), FALSE); + + priv = soup_message_get_instance_private (msg); + + for (f = priv->disabled_features; f; f = f->next) { + if (g_type_is_a ((GType)GPOINTER_TO_SIZE (f->data), feature_type)) + return TRUE; + } + return FALSE; +} + GSList * soup_message_get_disabled_features (SoupMessage *msg) { diff --git a/libsoup/soup-server.c b/libsoup/soup-server.c index 58d9574f..0bb30626 100644 --- a/libsoup/soup-server.c +++ b/libsoup/soup-server.c @@ -21,6 +21,7 @@ #include "soup-socket-private.h" #include "soup-websocket.h" #include "soup-websocket-connection.h" +#include "soup-websocket-extension-deflate.h" /** * SECTION:soup-server @@ -156,6 +157,7 @@ typedef struct { char *websocket_origin; char **websocket_protocols; + GList *websocket_extensions; SoupServerWebsocketCallback websocket_callback; GDestroyNotify websocket_destroy; gpointer websocket_user_data; @@ -183,6 +185,8 @@ typedef struct { SoupAddress *legacy_iface; int legacy_port; + GPtrArray *websocket_extension_types; + gboolean disposed; } SoupServerPrivate; @@ -204,6 +208,8 @@ enum { PROP_SERVER_HEADER, PROP_HTTP_ALIASES, PROP_HTTPS_ALIASES, + PROP_ADD_WEBSOCKET_EXTENSION, + PROP_REMOVE_WEBSOCKET_EXTENSION, LAST_PROP }; @@ -219,6 +225,7 @@ free_handler (SoupServerHandler *handler) g_free (handler->path); g_free (handler->websocket_origin); g_strfreev (handler->websocket_protocols); + g_list_free_full (handler->websocket_extensions, g_object_unref); if (handler->early_destroy) handler->early_destroy (handler->early_user_data); if (handler->destroy) @@ -240,6 +247,11 @@ soup_server_init (SoupServer *server) priv->http_aliases[1] = NULL; priv->legacy_port = -1; + + priv->websocket_extension_types = g_ptr_array_new_with_free_func ((GDestroyNotify)g_type_class_unref); + + /* Use permessage-deflate extension by default */ + g_ptr_array_add (priv->websocket_extension_types, g_type_class_ref (SOUP_TYPE_WEBSOCKET_EXTENSION_DEFLATE)); } static void @@ -278,6 +290,8 @@ soup_server_finalize (GObject *object) g_free (priv->http_aliases); g_free (priv->https_aliases); + g_ptr_array_free (priv->websocket_extension_types, TRUE); + G_OBJECT_CLASS (soup_server_parent_class)->finalize (object); } @@ -466,6 +480,12 @@ soup_server_set_property (GObject *object, guint prop_id, case PROP_HTTPS_ALIASES: set_aliases (&priv->https_aliases, g_value_get_boxed (value)); break; + case PROP_ADD_WEBSOCKET_EXTENSION: + soup_server_add_websocket_extension (server, g_value_get_gtype (value)); + break; + case PROP_REMOVE_WEBSOCKET_EXTENSION: + soup_server_remove_websocket_extension (server, g_value_get_gtype (value)); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -927,6 +947,51 @@ soup_server_class_init (SoupServerClass *server_class) "URI schemes that are considered aliases for 'https'", G_TYPE_STRV, G_PARAM_READWRITE)); + + /** + * SoupServer:add-websocket-extension: (skip) + * + * Add support for #SoupWebsocketExtension of the given type. + * (Shortcut for calling soup_server_add_websocket_extension().) + * + * Since: 2.68 + **/ + /** + * SOUP_SERVER_ADD_WEBSOCKET_EXTENSION: (skip) + * + * Alias for the #SoupServer:add-websocket-extension property, qv. + * + * Since: 2.68 + **/ + g_object_class_install_property ( + object_class, PROP_ADD_WEBSOCKET_EXTENSION, + g_param_spec_gtype (SOUP_SERVER_ADD_WEBSOCKET_EXTENSION, + "Add support for a WebSocket extension", + "Add support for a WebSocket extension of the given type", + SOUP_TYPE_WEBSOCKET_EXTENSION, + G_PARAM_WRITABLE)); + /** + * SoupServer:remove-websocket-extension: (skip) + * + * Remove support for #SoupWebsocketExtension of the given type. (Shortcut for + * calling soup_server_remove_websocket_extension().) + * + * Since: 2.68 + **/ + /** + * SOUP_SERVER_REMOVE_WEBSOCKET_EXTENSION: (skip) + * + * Alias for the #SoupServer:remove-websocket-extension property, qv. + * + * Since: 2.68 + **/ + g_object_class_install_property ( + object_class, PROP_REMOVE_WEBSOCKET_EXTENSION, + g_param_spec_gtype (SOUP_SERVER_REMOVE_WEBSOCKET_EXTENSION, + "Remove support for a WebSocket extension", + "Remove support for a WebSocket extension of the given type", + SOUP_TYPE_WEBSOCKET_EXTENSION, + G_PARAM_WRITABLE)); } /** @@ -1367,10 +1432,12 @@ complete_websocket_upgrade (SoupMessage *msg, gpointer user_data) soup_client_context_ref (client); stream = soup_client_context_steal_connection (client); - conn = soup_websocket_connection_new (stream, uri, - SOUP_WEBSOCKET_CONNECTION_SERVER, - soup_message_headers_get_one (msg->request_headers, "Origin"), - soup_message_headers_get_one (msg->response_headers, "Sec-WebSocket-Protocol")); + conn = soup_websocket_connection_new_with_extensions (stream, uri, + SOUP_WEBSOCKET_CONNECTION_SERVER, + soup_message_headers_get_one (msg->request_headers, "Origin"), + soup_message_headers_get_one (msg->response_headers, "Sec-WebSocket-Protocol"), + handler->websocket_extensions); + handler->websocket_extensions = NULL; g_object_unref (stream); soup_client_context_unref (client); @@ -1402,9 +1469,14 @@ got_body (SoupMessage *msg, SoupClientContext *client) return; if (handler->websocket_callback) { - if (soup_websocket_server_process_handshake (msg, - handler->websocket_origin, - handler->websocket_protocols)) { + SoupServerPrivate *priv; + + priv = soup_server_get_instance_private (server); + if (soup_websocket_server_process_handshake_with_extensions (msg, + handler->websocket_origin, + handler->websocket_protocols, + priv->websocket_extension_types, + &handler->websocket_extensions)) { g_signal_connect (msg, "wrote-informational", G_CALLBACK (complete_websocket_upgrade), soup_client_context_ref (client)); @@ -2696,12 +2768,14 @@ soup_server_add_websocket_handler (SoupServer *server, g_free (handler->websocket_origin); if (handler->websocket_protocols) g_strfreev (handler->websocket_protocols); + g_list_free_full (handler->websocket_extensions, g_object_unref); handler->websocket_callback = callback; handler->websocket_destroy = destroy; handler->websocket_user_data = user_data; handler->websocket_origin = g_strdup (origin); handler->websocket_protocols = g_strdupv (protocols); + handler->websocket_extensions = NULL; } /** @@ -2817,3 +2891,72 @@ soup_server_unpause_message (SoupServer *server, soup_message_io_unpause (msg); } + +/** + * soup_server_add_websocket_extension: + * @server: a #SoupServer + * @extension_type: a #GType + * + * Add support for a WebSocket extension of the given @extension_type. + * When a WebSocket client requests an extension of @extension_type, + * a new #SoupWebsocketExtension of type @extension_type will be created + * to handle the request. + * + * You can also add support for a WebSocket extension to the server at + * construct time by using the %SOUP_SERVER_ADD_WEBSOCKET_EXTENSION property. + * Note that #SoupWebsocketExtensionDeflate is supported by default, use + * soup_server_remove_websocket_extension() if you want to disable it. + * + * Since: 2.68 + */ +void +soup_server_add_websocket_extension (SoupServer *server, GType extension_type) +{ + SoupServerPrivate *priv; + + g_return_if_fail (SOUP_IS_SERVER (server)); + + priv = soup_server_get_instance_private (server); + if (!g_type_is_a (extension_type, SOUP_TYPE_WEBSOCKET_EXTENSION)) { + g_warning ("Type '%s' is not a SoupWebsocketExtension", g_type_name (extension_type)); + return; + } + + g_ptr_array_add (priv->websocket_extension_types, g_type_class_ref (extension_type)); +} + +/** + * soup_server_remove_websocket_extension: + * @server: a #SoupServer + * @extension_type: a #GType + * + * Removes support for WebSocket extension of type @extension_type (or any subclass of + * @extension_type) from @server. You can also remove extensions enabled by default + * from the server at construct time by using the %SOUP_SERVER_REMOVE_WEBSOCKET_EXTENSION + * property. + * + * Since: 2.68 + */ +void +soup_server_remove_websocket_extension (SoupServer *server, GType extension_type) +{ + SoupServerPrivate *priv; + SoupWebsocketExtensionClass *extension_class; + guint i; + + g_return_if_fail (SOUP_IS_SERVER (server)); + + priv = soup_server_get_instance_private (server); + if (!g_type_is_a (extension_type, SOUP_TYPE_WEBSOCKET_EXTENSION)) { + g_warning ("Type '%s' is not a SoupWebsocketExtension", g_type_name (extension_type)); + return; + } + + extension_class = g_type_class_peek (extension_type); + for (i = 0; i < priv->websocket_extension_types->len; i++) { + if (priv->websocket_extension_types->pdata[i] == (gpointer)extension_class) { + g_ptr_array_remove_index (priv->websocket_extension_types, i); + break; + } + } +} diff --git a/libsoup/soup-server.h b/libsoup/soup-server.h index f04e9eb1..1dc6cafe 100644 --- a/libsoup/soup-server.h +++ b/libsoup/soup-server.h @@ -138,6 +138,9 @@ void soup_server_add_early_handler (SoupServer *server, gpointer user_data, GDestroyNotify destroy); +#define SOUP_SERVER_ADD_WEBSOCKET_EXTENSION "add-websocket-extension" +#define SOUP_SERVER_REMOVE_WEBSOCKET_EXTENSION "remove-websocket-extension" + typedef void (*SoupServerWebsocketCallback) (SoupServer *server, SoupWebsocketConnection *connection, const char *path, @@ -151,6 +154,12 @@ void soup_server_add_websocket_handler (SoupServer SoupServerWebsocketCallback callback, gpointer user_data, GDestroyNotify destroy); +SOUP_AVAILABLE_IN_2_68 +void soup_server_add_websocket_extension (SoupServer *server, + GType extension_type); +SOUP_AVAILABLE_IN_2_68 +void soup_server_remove_websocket_extension (SoupServer *server, + GType extension_type); SOUP_AVAILABLE_IN_2_4 void soup_server_remove_handler (SoupServer *server, diff --git a/libsoup/soup-session.c b/libsoup/soup-session.c index 5ecae857..465cb469 100644 --- a/libsoup/soup-session.c +++ b/libsoup/soup-session.c @@ -24,6 +24,7 @@ #include "soup-socket-private.h" #include "soup-websocket.h" #include "soup-websocket-connection.h" +#include "soup-websocket-extension-manager-private.h" #define HOST_KEEP_ALIVE 5 * 60 * 1000 /* 5 min in msecs */ @@ -4775,6 +4776,19 @@ soup_session_steal_connection (SoupSession *session, return stream; } +static GPtrArray * +soup_session_get_supported_websocket_extensions_for_message (SoupSession *session, + SoupMessage *msg) +{ + SoupSessionFeature *extension_manager; + + extension_manager = soup_session_get_feature_for_message (session, SOUP_TYPE_WEBSOCKET_EXTENSION_MANAGER, msg); + if (!extension_manager) + return NULL; + + return soup_websocket_extension_manager_get_supported_extensions (SOUP_WEBSOCKET_EXTENSION_MANAGER (extension_manager)); +} + static void websocket_connect_async_stop (SoupMessage *msg, gpointer user_data); static void @@ -4799,6 +4813,9 @@ websocket_connect_async_stop (SoupMessage *msg, gpointer user_data) SoupMessageQueueItem *item = g_task_get_task_data (task); GIOStream *stream; SoupWebsocketConnection *client; + SoupSession *session = g_task_get_source_object (task); + GPtrArray *supported_extensions; + GList *accepted_extensions = NULL; GError *error = NULL; /* Disconnect websocket_connect_async_stop() handler. */ @@ -4807,20 +4824,24 @@ websocket_connect_async_stop (SoupMessage *msg, gpointer user_data) /* Ensure websocket_connect_async_complete is not called either. */ item->callback = NULL; - if (soup_websocket_client_verify_handshake (item->msg, &error)){ + supported_extensions = soup_session_get_supported_websocket_extensions_for_message (session, msg); + if (soup_websocket_client_verify_handshake_with_extensions (item->msg, supported_extensions, &accepted_extensions, &error)) { stream = soup_session_steal_connection (item->session, item->msg); - client = soup_websocket_connection_new (stream, - soup_message_get_uri (item->msg), - SOUP_WEBSOCKET_CONNECTION_CLIENT, - soup_message_headers_get_one (msg->request_headers, "Origin"), - soup_message_headers_get_one (msg->response_headers, "Sec-WebSocket-Protocol")); + client = soup_websocket_connection_new_with_extensions (stream, + soup_message_get_uri (item->msg), + SOUP_WEBSOCKET_CONNECTION_CLIENT, + soup_message_headers_get_one (msg->request_headers, "Origin"), + soup_message_headers_get_one (msg->response_headers, "Sec-WebSocket-Protocol"), + accepted_extensions); g_object_unref (stream); - g_task_return_pointer (task, client, g_object_unref); - } else { - soup_message_io_finished (item->msg); - g_task_return_error (task, error); + g_object_unref (task); + + return; } + + soup_message_io_finished (item->msg); + g_task_return_error (task, error); g_object_unref (task); } @@ -4868,12 +4889,14 @@ soup_session_websocket_connect_async (SoupSession *session, SoupSessionPrivate *priv = soup_session_get_instance_private (session); SoupMessageQueueItem *item; GTask *task; + GPtrArray *supported_extensions; g_return_if_fail (SOUP_IS_SESSION (session)); g_return_if_fail (priv->use_thread_context); g_return_if_fail (SOUP_IS_MESSAGE (msg)); - soup_websocket_client_prepare_handshake (msg, origin, protocols); + supported_extensions = soup_session_get_supported_websocket_extensions_for_message (session, msg); + soup_websocket_client_prepare_handshake_with_extensions (msg, origin, protocols, supported_extensions); task = g_task_new (session, cancellable, callback, user_data); item = soup_session_append_queue_item (session, msg, TRUE, FALSE, diff --git a/libsoup/soup-types.h b/libsoup/soup-types.h index ed593390..9e3d5788 100644 --- a/libsoup/soup-types.h +++ b/libsoup/soup-types.h @@ -32,7 +32,7 @@ typedef struct _SoupSessionSync SoupSessionSync; typedef struct _SoupSocket SoupSocket; typedef struct _SoupURI SoupURI; typedef struct _SoupWebsocketConnection SoupWebsocketConnection; - +typedef struct _SoupWebsocketExtension SoupWebsocketExtension; /*< private >*/ typedef struct _SoupConnection SoupConnection; diff --git a/libsoup/soup-websocket-connection.c b/libsoup/soup-websocket-connection.c index 32404e3b..50e67fd6 100644 --- a/libsoup/soup-websocket-connection.c +++ b/libsoup/soup-websocket-connection.c @@ -26,6 +26,7 @@ #include "soup-enum-types.h" #include "soup-io-stream.h" #include "soup-uri.h" +#include "soup-websocket-extension.h" /* * SECTION:websocketconnection @@ -84,6 +85,7 @@ enum { PROP_STATE, PROP_MAX_INCOMING_PAYLOAD_SIZE, PROP_KEEPALIVE_INTERVAL, + PROP_EXTENSIONS }; enum { @@ -145,6 +147,8 @@ struct _SoupWebsocketConnectionPrivate { GByteArray *message_data; GSource *keepalive_timeout; + + GList *extensions; }; #define MAX_INCOMING_PAYLOAD_SIZE_DEFAULT 128 * 1024 @@ -154,6 +158,9 @@ G_DEFINE_TYPE_WITH_PRIVATE (SoupWebsocketConnection, soup_websocket_connection, static void queue_frame (SoupWebsocketConnection *self, SoupWebsocketQueueFlags flags, gpointer data, gsize len, gsize amount); +static void emit_error_and_close (SoupWebsocketConnection *self, + GError *error, gboolean prejudice); + static void protocol_error_and_close (SoupWebsocketConnection *self); /* Code below is based on g_utf8_validate() implementation, @@ -427,12 +434,15 @@ send_message (SoupWebsocketConnection *self, const guint8 *data, gsize length) { - gsize buffered_amount = length; + gsize buffered_amount; GByteArray *bytes; gsize frame_len; guint8 *outer; guint8 *mask = 0; guint8 *at; + GBytes *filtered_bytes; + GList *l; + GError *error = NULL; if (!(soup_websocket_connection_get_state (self) == SOUP_WEBSOCKET_STATE_OPEN)) { g_debug ("Ignoring message since the connection is closed or is closing"); @@ -443,6 +453,21 @@ send_message (SoupWebsocketConnection *self, outer = bytes->data; outer[0] = 0x80 | opcode; + filtered_bytes = g_bytes_new_static (data, length); + for (l = self->pv->extensions; l != NULL; l = g_list_next (l)) { + SoupWebsocketExtension *extension; + + extension = (SoupWebsocketExtension *)l->data; + filtered_bytes = soup_websocket_extension_process_outgoing_message (extension, outer, filtered_bytes, &error); + if (error) { + emit_error_and_close (self, error, FALSE); + return; + } + } + + data = g_bytes_get_data (filtered_bytes, &length); + buffered_amount = length; + /* If control message, check payload size */ if (opcode & 0x08) { if (length > 125) { @@ -499,6 +524,7 @@ send_message (SoupWebsocketConnection *self, frame_len = bytes->len; queue_frame (self, flags, g_byte_array_free (bytes, FALSE), frame_len, buffered_amount); + g_bytes_unref (filtered_bytes); g_debug ("queued %d frame of len %u", (int)opcode, (guint)frame_len); } @@ -771,11 +797,14 @@ process_contents (SoupWebsocketConnection *self, gboolean control, gboolean fin, guint8 opcode, - gconstpointer payload, - gsize payload_len) + GBytes *payload_data) { SoupWebsocketConnectionPrivate *pv = self->pv; GBytes *message; + gconstpointer payload; + gsize payload_len; + + payload = g_bytes_get_data (payload_data, &payload_len); if (pv->close_sent && pv->close_received) return; @@ -909,6 +938,9 @@ process_frame (SoupWebsocketConnection *self) guint8 opcode; gsize len; gsize at; + GBytes *filtered_bytes; + GList *l; + GError *error = NULL; len = self->pv->incoming->len; if (len < 2) @@ -938,12 +970,6 @@ process_frame (SoupWebsocketConnection *self) return FALSE; } - /* We do not support extensions, reserved bits must be 0 */ - if (header[0] & 0x70) { - protocol_error_and_close (self); - return FALSE; - } - switch (header[1] & 0x7f) { case 126: /* If 126, the following 2 bytes interpreted as a 16-bit @@ -1013,13 +1039,37 @@ process_frame (SoupWebsocketConnection *self) xor_with_mask (mask, payload, payload_len); } + filtered_bytes = g_bytes_new_static (payload, payload_len); + for (l = self->pv->extensions; l != NULL; l = g_list_next (l)) { + SoupWebsocketExtension *extension; + + extension = (SoupWebsocketExtension *)l->data; + filtered_bytes = soup_websocket_extension_process_incoming_message (extension, self->pv->incoming->data, filtered_bytes, &error); + if (error) { + emit_error_and_close (self, error, FALSE); + g_bytes_unref (filtered_bytes); + + return FALSE; + } + } + + /* After being processed by extensions reserved bits must be 0 */ + if (header[0] & 0x70) { + protocol_error_and_close (self); + g_bytes_unref (filtered_bytes); + + return FALSE; + } + /* Note that now that we've unmasked, we've modified the buffer, we can * only return below via discarding or processing the message */ - process_contents (self, control, fin, opcode, payload, payload_len); + process_contents (self, control, fin, opcode, filtered_bytes); + g_bytes_unref (filtered_bytes); /* Move past the parsed frame */ g_byte_array_remove_range (self->pv->incoming, 0, at + payload_len); + return TRUE; } @@ -1271,6 +1321,10 @@ soup_websocket_connection_get_property (GObject *object, g_value_set_uint (value, pv->keepalive_interval); break; + case PROP_EXTENSIONS: + g_value_set_pointer (value, pv->extensions); + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -1320,6 +1374,10 @@ soup_websocket_connection_set_property (GObject *object, g_value_get_uint (value)); break; + case PROP_EXTENSIONS: + pv->extensions = g_value_get_pointer (value); + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -1368,6 +1426,8 @@ soup_websocket_connection_finalize (GObject *object) g_free (pv->origin); g_free (pv->protocol); + g_list_free_full (pv->extensions, g_object_unref); + G_OBJECT_CLASS (soup_websocket_connection_parent_class)->finalize (object); } @@ -1525,6 +1585,21 @@ soup_websocket_connection_class_init (SoupWebsocketConnectionClass *klass) G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS)); + /** + * SoupWebsocketConnection:extensions: + * + * List of #SoupWebsocketExtension objects that are active in the connection. + * + * Since: 2.68 + */ + g_object_class_install_property (gobject_class, PROP_EXTENSIONS, + g_param_spec_pointer ("extensions", + "Active extensions", + "The list of active extensions", + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + /** * SoupWebsocketConnection::message: * @self: the WebSocket @@ -1644,17 +1719,46 @@ soup_websocket_connection_new (GIOStream *stream, 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); + return soup_websocket_connection_new_with_extensions (stream, uri, type, origin, protocol, NULL); +} + +/** + * soup_websocket_connection_new_with_extensions: + * @stream: a #GIOStream connected to the WebSocket server + * @uri: the URI of the connection + * @type: the type of connection (client/side) + * @origin: (allow-none): the Origin of the client + * @protocol: (allow-none): the subprotocol in use + * @extensions: (element-type SoupWebsocketExtension) (transfer full): a #GList of #SoupWebsocketExtension objects + * + * Creates a #SoupWebsocketConnection on @stream with the given active @extensions. + * This should be called after completing the handshake to begin using the WebSocket + * protocol. + * + * Returns: a new #SoupWebsocketConnection + * + * Since: 2.68 + */ +SoupWebsocketConnection * +soup_websocket_connection_new_with_extensions (GIOStream *stream, + SoupURI *uri, + SoupWebsocketConnectionType type, + const char *origin, + const char *protocol, + GList *extensions) +{ + g_return_val_if_fail (G_IS_IO_STREAM (stream), NULL); + g_return_val_if_fail (uri != NULL, NULL); + g_return_val_if_fail (type != SOUP_WEBSOCKET_CONNECTION_UNKNOWN, NULL); + + return g_object_new (SOUP_TYPE_WEBSOCKET_CONNECTION, + "io-stream", stream, + "uri", uri, + "connection-type", type, + "origin", origin, + "protocol", protocol, + "extensions", extensions, + NULL); } /** @@ -1751,6 +1855,24 @@ soup_websocket_connection_get_protocol (SoupWebsocketConnection *self) } /** + * soup_websocket_connection_get_extensions: + * @self: the WebSocket + * + * Get the extensions chosen via negotiation with the peer. + * + * Returns: (element-type SoupWebsocketExtension) (transfer none): a #GList of #SoupWebsocketExtension objects + * + * Since: 2.68 + */ +GList * +soup_websocket_connection_get_extensions (SoupWebsocketConnection *self) +{ + g_return_val_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self), NULL); + + return self->pv->extensions; +} + +/** * soup_websocket_connection_get_state: * @self: the WebSocket * diff --git a/libsoup/soup-websocket-connection.h b/libsoup/soup-websocket-connection.h index d761c424..f82d723a 100644 --- a/libsoup/soup-websocket-connection.h +++ b/libsoup/soup-websocket-connection.h @@ -70,6 +70,13 @@ SoupWebsocketConnection *soup_websocket_connection_new (GIOStream SoupWebsocketConnectionType type, const char *origin, const char *protocol); +SOUP_AVAILABLE_IN_2_68 +SoupWebsocketConnection *soup_websocket_connection_new_with_extensions (GIOStream *stream, + SoupURI *uri, + SoupWebsocketConnectionType type, + const char *origin, + const char *protocol, + GList *extensions); SOUP_AVAILABLE_IN_2_50 GIOStream * soup_websocket_connection_get_io_stream (SoupWebsocketConnection *self); @@ -86,6 +93,9 @@ const char * soup_websocket_connection_get_origin (SoupWebsocketConne SOUP_AVAILABLE_IN_2_50 const char * soup_websocket_connection_get_protocol (SoupWebsocketConnection *self); +SOUP_AVAILABLE_IN_2_68 +GList * soup_websocket_connection_get_extensions (SoupWebsocketConnection *self); + SOUP_AVAILABLE_IN_2_50 SoupWebsocketState soup_websocket_connection_get_state (SoupWebsocketConnection *self); diff --git a/libsoup/soup-websocket-extension-deflate.c b/libsoup/soup-websocket-extension-deflate.c new file mode 100644 index 00000000..01faf3fc --- /dev/null +++ b/libsoup/soup-websocket-extension-deflate.c @@ -0,0 +1,503 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * soup-websocket-extension-deflate.c + * + * Copyright (C) 2019 Igalia S.L. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "soup-websocket-extension-deflate.h" +#include <zlib.h> + +typedef struct { + z_stream zstream; + gboolean no_context_takeover; +} Deflater; + +typedef struct { + z_stream zstream; + gboolean uncompress_ongoing; +} Inflater; + +#define BUFFER_SIZE 4096 + +typedef enum { + PARAM_SERVER_NO_CONTEXT_TAKEOVER = 1 << 0, + PARAM_CLIENT_NO_CONTEXT_TAKEOVER = 1 << 1, + PARAM_SERVER_MAX_WINDOW_BITS = 1 << 2, + PARAM_CLIENT_MAX_WINDOW_BITS = 1 << 3, + PARAM_CLIENT_MAX_WINDOW_BITS_VALUE = 1 << 4 +} ParamFlags; + +typedef struct { + ParamFlags flags; + gushort server_max_window_bits; + gushort client_max_window_bits; +} Params; + +typedef struct { + Params params; + + gboolean enabled; + + Deflater deflater; + Inflater inflater; +} SoupWebsocketExtensionDeflatePrivate; + +/* + * SECTION:soup-websocket-extension-deflate + * @title: SoupWebsocketExtensionDeflate + * @short_description: A permessage-deflate WebSocketExtension + * @see_also: #SoupWebsocketExtension + * + * A SoupWebsocketExtensionDeflate is a #SoupWebsocketExtension + * implementing permessage-deflate (RFC 7692). + * + * This extension is used by default in a #SoupSession when #SoupWebsocketExtensionManager + * feature is present, and always used by #SoupServer. + * + * Since: 2.68 + */ + +/** + * SOUP_TYPE_WEBSOCKET_EXTENSION_DEFLATE: + * + * A #GType corresponding to permessage-deflate WebSocket extension. + * + * Since: 2.68 + */ + +G_DEFINE_TYPE_WITH_PRIVATE (SoupWebsocketExtensionDeflate, soup_websocket_extension_deflate, SOUP_TYPE_WEBSOCKET_EXTENSION) + +static void +soup_websocket_extension_deflate_init (SoupWebsocketExtensionDeflate *basic) +{ +} + +static void +soup_websocket_extension_deflate_finalize (GObject *object) +{ + SoupWebsocketExtensionDeflatePrivate *priv = soup_websocket_extension_deflate_get_instance_private (SOUP_WEBSOCKET_EXTENSION_DEFLATE (object)); + + if (priv->enabled) { + deflateEnd (&priv->deflater.zstream); + inflateEnd (&priv->inflater.zstream); + } + + G_OBJECT_CLASS (soup_websocket_extension_deflate_parent_class)->finalize (object); +} + +static gboolean +parse_window_bits (const char *value, + gushort *out) +{ + guint64 int_value; + char *end = NULL; + + if (!value || !*value) + return FALSE; + + int_value = g_ascii_strtoull (value, &end, 10); + if (*end != '\0') + return FALSE; + + if (int_value < 8 || int_value > 15) + return FALSE; + + *out = (gushort)int_value; + return TRUE; +} + +static gboolean +return_invalid_param_error (GError **error, + const char *param) +{ + g_set_error (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, + "Invalid parameter '%s' in permessage-deflate extension header", + param); + return FALSE; +} + +static gboolean +return_invalid_param_value_error (GError **error, + const char *param) +{ + g_set_error (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, + "Invalid value of parameter '%s' in permessage-deflate extension header", + param); + return FALSE; +} + +static gboolean +parse_params (GHashTable *params, + Params *out, + GError **error) +{ + GHashTableIter iter; + gpointer key, value; + + g_hash_table_iter_init (&iter, params); + while (g_hash_table_iter_next (&iter, &key, &value)) { + if (g_str_equal ((char *)key, "server_no_context_takeover")) { + if (value) + return return_invalid_param_value_error(error, "server_no_context_takeover"); + + out->flags |= PARAM_SERVER_NO_CONTEXT_TAKEOVER; + } else if (g_str_equal ((char *)key, "client_no_context_takeover")) { + if (value) + return return_invalid_param_value_error(error, "client_no_context_takeover"); + + out->flags |= PARAM_CLIENT_NO_CONTEXT_TAKEOVER; + } else if (g_str_equal ((char *)key, "server_max_window_bits")) { + if (!parse_window_bits ((char *)value, &out->server_max_window_bits)) + return return_invalid_param_value_error(error, "server_max_window_bits"); + + out->flags |= PARAM_SERVER_MAX_WINDOW_BITS; + } else if (g_str_equal ((char *)key, "client_max_window_bits")) { + if (value) { + if (!parse_window_bits ((char *)value, &out->client_max_window_bits)) + return return_invalid_param_value_error(error, "client_max_window_bits"); + + out->flags |= PARAM_CLIENT_MAX_WINDOW_BITS_VALUE; + } else { + out->client_max_window_bits = 15; + } + out->flags |= PARAM_CLIENT_MAX_WINDOW_BITS; + } else { + return return_invalid_param_error (error, (char *)key); + } + } + + return TRUE; +} + +static gboolean +soup_websocket_extension_deflate_configure (SoupWebsocketExtension *extension, + SoupWebsocketConnectionType connection_type, + GHashTable *params, + GError **error) +{ + gushort deflater_max_window_bits; + gushort inflater_max_window_bits; + SoupWebsocketExtensionDeflatePrivate *priv; + + priv = soup_websocket_extension_deflate_get_instance_private (SOUP_WEBSOCKET_EXTENSION_DEFLATE (extension)); + + if (params && !parse_params (params, &priv->params, error)) + return FALSE; + + switch (connection_type) { + case SOUP_WEBSOCKET_CONNECTION_CLIENT: + priv->deflater.no_context_takeover = priv->params.flags & PARAM_CLIENT_NO_CONTEXT_TAKEOVER; + deflater_max_window_bits = priv->params.flags & PARAM_CLIENT_MAX_WINDOW_BITS ? priv->params.client_max_window_bits : 15; + inflater_max_window_bits = priv->params.flags & PARAM_SERVER_MAX_WINDOW_BITS ? priv->params.server_max_window_bits : 15; + break; + case SOUP_WEBSOCKET_CONNECTION_SERVER: + priv->deflater.no_context_takeover = priv->params.flags & PARAM_SERVER_NO_CONTEXT_TAKEOVER; + deflater_max_window_bits = priv->params.flags & PARAM_SERVER_MAX_WINDOW_BITS ? priv->params.server_max_window_bits : 15; + inflater_max_window_bits = priv->params.flags & PARAM_CLIENT_MAX_WINDOW_BITS ? priv->params.client_max_window_bits : 15; + break; + default: + g_assert_not_reached (); + } + + /* zlib is unable to compress with window_bits=8, so use 9 + * instead. This is compatible with decompressing using + * window_bits=8. + */ + deflater_max_window_bits = MAX (deflater_max_window_bits, 9); + + /* In case of failing to initialize zlib deflater/inflater, + * we return TRUE without setting enabled = TRUE, so that the + * hanshake doesn't fail. + */ + if (deflateInit2 (&priv->deflater.zstream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -deflater_max_window_bits, 8, Z_DEFAULT_STRATEGY) != Z_OK) + return TRUE; + + if (inflateInit2 (&priv->inflater.zstream, -inflater_max_window_bits) != Z_OK) { + deflateEnd (&priv->deflater.zstream); + return TRUE; + } + + priv->enabled = TRUE; + + return TRUE; +} + +static char * +soup_websocket_extension_deflate_get_request_params (SoupWebsocketExtension *extension) +{ + return g_strdup ("; client_max_window_bits"); +} + +static char * +soup_websocket_extension_deflate_get_response_params (SoupWebsocketExtension *extension) +{ + GString *params; + SoupWebsocketExtensionDeflatePrivate *priv; + + priv = soup_websocket_extension_deflate_get_instance_private (SOUP_WEBSOCKET_EXTENSION_DEFLATE (extension)); + if (!priv->enabled) + return NULL; + + if (priv->params.flags == 0) + return NULL; + + params = g_string_new (NULL); + + if (priv->params.flags & PARAM_SERVER_NO_CONTEXT_TAKEOVER) + params = g_string_append (params, "; server_no_context_takeover"); + if (priv->params.flags & PARAM_CLIENT_NO_CONTEXT_TAKEOVER) + params = g_string_append (params, "; client_no_context_takeover"); + if (priv->params.flags & PARAM_SERVER_MAX_WINDOW_BITS) + g_string_append_printf (params, "; server_max_window_bits=%u", priv->params.server_max_window_bits); + if (priv->params.flags & PARAM_CLIENT_MAX_WINDOW_BITS) { + if (priv->params.flags & PARAM_CLIENT_MAX_WINDOW_BITS_VALUE) + g_string_append_printf (params, "; client_max_window_bits=%u", priv->params.client_max_window_bits); + else + params = g_string_append (params, "; client_max_window_bits"); + } + + return g_string_free (params, FALSE); +} + +static void +deflater_reset (Deflater *deflater) +{ + if (deflater->no_context_takeover) + deflateReset (&deflater->zstream); +} + +static GBytes * +soup_websocket_extension_deflate_process_outgoing_message (SoupWebsocketExtension *extension, + guint8 *header, + GBytes *payload, + GError **error) +{ + const guint8 *payload_data; + gsize payload_length; + guint max_length; + gboolean control; + GByteArray *buffer; + gsize bytes_written; + int result; + gboolean in_sync_flush; + SoupWebsocketExtensionDeflatePrivate *priv; + + priv = soup_websocket_extension_deflate_get_instance_private (SOUP_WEBSOCKET_EXTENSION_DEFLATE (extension)); + + if (!priv->enabled) + return payload; + + control = header[0] & 0x08; + + /* Do not compress control frames */ + if (control) + return payload; + + payload_data = g_bytes_get_data (payload, &payload_length); + if (payload_length == 0) + return payload; + + /* Mark the frame as compressed using reserved bit 1 (0x40) */ + header[0] |= 0x40; + + buffer = g_byte_array_new (); + max_length = deflateBound(&priv->deflater.zstream, payload_length); + + priv->deflater.zstream.next_in = (void *)payload_data; + priv->deflater.zstream.avail_in = payload_length; + + bytes_written = 0; + priv->deflater.zstream.avail_out = 0; + + do { + gsize write_remaining; + + if (priv->deflater.zstream.avail_out == 0) { + guint write_position; + + priv->deflater.zstream.avail_out = max_length; + write_position = buffer->len; + g_byte_array_set_size (buffer, buffer->len + max_length); + priv->deflater.zstream.next_out = buffer->data + write_position; + + /* Use a fixed value for buffer increments */ + max_length = BUFFER_SIZE; + } + + write_remaining = buffer->len - bytes_written; + in_sync_flush = priv->deflater.zstream.avail_in == 0; + result = deflate (&priv->deflater.zstream, in_sync_flush ? Z_SYNC_FLUSH : Z_NO_FLUSH); + bytes_written += write_remaining - priv->deflater.zstream.avail_out; + } while (result == Z_OK); + + if (result != Z_BUF_ERROR || bytes_written < 4) { + g_set_error_literal (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_CLOSE_PROTOCOL_ERROR, + "Failed to compress outgoing frame"); + g_byte_array_unref (buffer); + deflater_reset (&priv->deflater); + return NULL; + } + + /* Remove 4 octets (that are 0x00 0x00 0xff 0xff) from the tail end. */ + g_byte_array_set_size (buffer, bytes_written - 4); + + g_bytes_unref (payload); + + deflater_reset (&priv->deflater); + + return g_byte_array_free_to_bytes (buffer); +} + +static GBytes * +soup_websocket_extension_deflate_process_incoming_message (SoupWebsocketExtension *extension, + guint8 *header, + GBytes *payload, + GError **error) +{ + const guint8 *payload_data; + gsize payload_length; + gboolean fin, control, compressed; + GByteArray *buffer; + gsize bytes_read, bytes_written; + int result; + gboolean tail_added = FALSE; + SoupWebsocketExtensionDeflatePrivate *priv; + + priv = soup_websocket_extension_deflate_get_instance_private (SOUP_WEBSOCKET_EXTENSION_DEFLATE (extension)); + + if (!priv->enabled) + return payload; + + control = header[0] & 0x08; + + /* Do not uncompress control frames */ + if (control) + return payload; + + compressed = header[0] & 0x40; + if (!priv->inflater.uncompress_ongoing && !compressed) + return payload; + + if (priv->inflater.uncompress_ongoing && compressed) { + g_set_error_literal (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_CLOSE_PROTOCOL_ERROR, + "Received a non-first frame with RSV1 flag set"); + return NULL; + } + + /* Remove the compressed flag */ + header[0] &= ~0x40; + + fin = header[0] & 0x80; + payload_data = g_bytes_get_data (payload, &payload_length); + if (payload_length == 0 && ((!priv->inflater.uncompress_ongoing && fin) || (priv->inflater.uncompress_ongoing && !fin))) + return payload; + + priv->inflater.uncompress_ongoing = !fin; + + buffer = g_byte_array_new (); + + bytes_read = 0; + priv->inflater.zstream.next_in = (void *)payload_data; + priv->inflater.zstream.avail_in = payload_length; + + bytes_written = 0; + priv->inflater.zstream.avail_out = 0; + + do { + gsize read_remaining; + gsize write_remaining; + + if (priv->inflater.zstream.avail_out == 0) { + guint current_position; + + priv->inflater.zstream.avail_out = BUFFER_SIZE; + current_position = buffer->len; + g_byte_array_set_size (buffer, buffer->len + BUFFER_SIZE); + priv->inflater.zstream.next_out = buffer->data + current_position; + } + + if (priv->inflater.zstream.avail_in == 0 && !tail_added && fin) { + /* Append 4 octets of 0x00 0x00 0xff 0xff to the tail end */ + priv->inflater.zstream.next_in = (void *)"\x00\x00\xff\xff"; + priv->inflater.zstream.avail_in = 4; + bytes_read = 0; + tail_added = TRUE; + } + + read_remaining = tail_added ? 4 : payload_length - bytes_read; + write_remaining = buffer->len - bytes_written; + result = inflate (&priv->inflater.zstream, tail_added ? Z_FINISH : Z_NO_FLUSH); + bytes_read += read_remaining - priv->inflater.zstream.avail_in; + bytes_written += write_remaining - priv->inflater.zstream.avail_out; + if (!tail_added && result == Z_STREAM_END) { + /* Received a block with BFINAL set to 1. Reset decompression state. */ + result = inflateReset (&priv->inflater.zstream); + } + + if ((!fin && bytes_read == payload_length) || (fin && tail_added && bytes_read == 4)) + break; + } while (result == Z_OK || result == Z_BUF_ERROR); + + if (result != Z_OK && result != Z_BUF_ERROR) { + priv->inflater.uncompress_ongoing = FALSE; + g_set_error_literal (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_CLOSE_PROTOCOL_ERROR, + "Failed to uncompress incoming frame"); + g_byte_array_unref (buffer); + + return NULL; + } + + g_byte_array_set_size (buffer, bytes_written); + + g_bytes_unref (payload); + + return g_byte_array_free_to_bytes (buffer); +} + +static void +soup_websocket_extension_deflate_class_init (SoupWebsocketExtensionDeflateClass *klass) +{ + SoupWebsocketExtensionClass *extension_class = SOUP_WEBSOCKET_EXTENSION_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + extension_class->name = "permessage-deflate"; + + extension_class->configure = soup_websocket_extension_deflate_configure; + extension_class->get_request_params = soup_websocket_extension_deflate_get_request_params; + extension_class->get_response_params = soup_websocket_extension_deflate_get_response_params; + extension_class->process_outgoing_message = soup_websocket_extension_deflate_process_outgoing_message; + extension_class->process_incoming_message = soup_websocket_extension_deflate_process_incoming_message; + + object_class->finalize = soup_websocket_extension_deflate_finalize; +} diff --git a/libsoup/soup-websocket-extension-deflate.h b/libsoup/soup-websocket-extension-deflate.h new file mode 100644 index 00000000..e353965d --- /dev/null +++ b/libsoup/soup-websocket-extension-deflate.h @@ -0,0 +1,49 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * soup-websocket-extension-deflate.h + * + * Copyright (C) 2019 Igalia S.L. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __SOUP_WEBSOCKET_EXTENSION_DEFLATE_H__ +#define __SOUP_WEBSOCKET_EXTENSION_DEFLATE_H__ 1 + +#include "soup-websocket-extension.h" + +#define SOUP_TYPE_WEBSOCKET_EXTENSION_DEFLATE (soup_websocket_extension_deflate_get_type ()) +#define SOUP_WEBSOCKET_EXTENSION_DEFLATE(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), SOUP_TYPE_WEBSOCKET_EXTENSION_DEFLATE, SoupWebsocketExtensionDeflate)) +#define SOUP_IS_WEBSOCKET_EXTENSION_DEFLATE(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), SOUP_TYPE_WEBSOCKET_EXTENSION_DEFLATE)) +#define SOUP_WEBSOCKET_EXTENSION_DEFLATE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SOUP_TYPE_WEBSOCKET_EXTENSION_DEFLATE, SoupWebsocketExtensionDeflateClass)) +#define SOUP_IS_WEBSOCKET_EXTENSION_DEFLATE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SOUP_TYPE_WEBSOCKET_EXTENSION_DEFLATE)) +#define SOUP_WEBSOCKET_EXTENSION_DEFLATE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SOUP_TYPE_WEBSOCKET_EXTENSION_DEFLATE, SoupWebsocketExtensionDeflateClass)) + +typedef struct _SoupWebsocketExtensionDeflate SoupWebsocketExtensionDeflate; +typedef struct _SoupWebsocketExtensionDeflateClass SoupWebsocketExtensionDeflateClass; + +struct _SoupWebsocketExtensionDeflate { + SoupWebsocketExtension parent; +}; + +struct _SoupWebsocketExtensionDeflateClass { + SoupWebsocketExtensionClass parent_class; +}; + +SOUP_AVAILABLE_IN_2_68 +GType soup_websocket_extension_deflate_get_type (void); + +#endif /* __SOUP_WEBSOCKET_EXTENSION_DEFLATE_H__ */ diff --git a/libsoup/soup-websocket-extension-manager-private.h b/libsoup/soup-websocket-extension-manager-private.h new file mode 100644 index 00000000..b7ff618d --- /dev/null +++ b/libsoup/soup-websocket-extension-manager-private.h @@ -0,0 +1,30 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * soup-websocket-extension-manager-private.h + * + * Copyright (C) 2019 Igalia S.L. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __SOUP_WEBSOCKET_EXTENSION_MANAGER_PRIVATE_H__ +#define __SOUP_WEBSOCKET_EXTENSION_MANAGER_PRIVATE_H__ 1 + +#include "soup-websocket-extension-manager.h" + +GPtrArray *soup_websocket_extension_manager_get_supported_extensions (SoupWebsocketExtensionManager *manager); + +#endif /* __SOUP_WEBSOCKET_EXTENSION_MANAGER_PRIVATE_H__ */ diff --git a/libsoup/soup-websocket-extension-manager.c b/libsoup/soup-websocket-extension-manager.c new file mode 100644 index 00000000..69c4fd4a --- /dev/null +++ b/libsoup/soup-websocket-extension-manager.c @@ -0,0 +1,180 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * soup-websocket-extension-manager.c + * + * Copyright (C) 2019 Igalia S.L. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "soup-websocket-extension-manager.h" +#include "soup-headers.h" +#include "soup-session-feature.h" +#include "soup-websocket.h" +#include "soup-websocket-extension.h" +#include "soup-websocket-extension-deflate.h" +#include "soup-websocket-extension-manager-private.h" + +/** + * SECTION:soup-websocket-extension-manager + * @title: SoupWebsocketExtensionManager + * @short_description: WebSocket extensions manager + * @see_also: #SoupSession, #SoupWebsocketExtension + * + * SoupWebsocketExtensionManager is the #SoupSessionFeature that handles WebSockets + * extensions for a #SoupSession. + * + * A SoupWebsocketExtensionManager is added to the session by default, and normally + * you don't need to worry about it at all. However, if you want to + * disable WebSocket extensions, you can remove the feature from the + * session with soup_session_remove_feature_by_type(), or disable it on + * individual requests with soup_message_disable_feature(). + * + * Since: 2.68 + **/ + +/** + * SOUP_TYPE_WEBSOCKET_EXTENSION_MANAGER: + * + * The #GType of #SoupWebsocketExtensionManager; you can use this with + * soup_session_remove_feature_by_type() or + * soup_message_disable_feature(). + * + * Since: 2.68 + */ + +static void soup_websocket_extension_manager_session_feature_init (SoupSessionFeatureInterface *feature_interface, gpointer interface_data); + +typedef struct { + GPtrArray *extension_types; +} SoupWebsocketExtensionManagerPrivate; + +G_DEFINE_TYPE_WITH_CODE (SoupWebsocketExtensionManager, soup_websocket_extension_manager, G_TYPE_OBJECT, + G_ADD_PRIVATE (SoupWebsocketExtensionManager) + G_IMPLEMENT_INTERFACE (SOUP_TYPE_SESSION_FEATURE, + soup_websocket_extension_manager_session_feature_init)) + +static void +soup_websocket_extension_manager_init (SoupWebsocketExtensionManager *manager) +{ + SoupWebsocketExtensionManagerPrivate *priv = soup_websocket_extension_manager_get_instance_private (manager); + + priv->extension_types = g_ptr_array_new_with_free_func ((GDestroyNotify)g_type_class_unref); + + /* Use permessage-deflate extension by default */ + soup_session_feature_add_feature (SOUP_SESSION_FEATURE (manager), SOUP_TYPE_WEBSOCKET_EXTENSION_DEFLATE); +} + +static void +soup_websocket_extension_manager_finalize (GObject *object) +{ + SoupWebsocketExtensionManagerPrivate *priv; + + priv = soup_websocket_extension_manager_get_instance_private (SOUP_WEBSOCKET_EXTENSION_MANAGER (object)); + g_ptr_array_free (priv->extension_types, TRUE); + + G_OBJECT_CLASS (soup_websocket_extension_manager_parent_class)->finalize (object); +} + +static void +soup_websocket_extension_manager_class_init (SoupWebsocketExtensionManagerClass *websocket_extension_manager_class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (websocket_extension_manager_class); + + object_class->finalize = soup_websocket_extension_manager_finalize; +} + +static gboolean +soup_websocket_extension_manager_add_feature (SoupSessionFeature *feature, GType type) +{ + SoupWebsocketExtensionManagerPrivate *priv; + + if (!g_type_is_a (type, SOUP_TYPE_WEBSOCKET_EXTENSION)) + return FALSE; + + priv = soup_websocket_extension_manager_get_instance_private (SOUP_WEBSOCKET_EXTENSION_MANAGER (feature)); + g_ptr_array_add (priv->extension_types, g_type_class_ref (type)); + + return TRUE; +} + +static gboolean +soup_websocket_extension_manager_remove_feature (SoupSessionFeature *feature, GType type) +{ + SoupWebsocketExtensionManagerPrivate *priv; + SoupWebsocketExtensionClass *extension_class; + guint i; + + if (!g_type_is_a (type, SOUP_TYPE_WEBSOCKET_EXTENSION)) + return FALSE; + + priv = soup_websocket_extension_manager_get_instance_private (SOUP_WEBSOCKET_EXTENSION_MANAGER (feature)); + extension_class = g_type_class_peek (type); + + for (i = 0; i < priv->extension_types->len; i++) { + if (priv->extension_types->pdata[i] == (gpointer)extension_class) { + g_ptr_array_remove_index (priv->extension_types, i); + return TRUE; + } + } + + return FALSE; +} + +static gboolean +soup_websocket_extension_manager_has_feature (SoupSessionFeature *feature, GType type) +{ + SoupWebsocketExtensionManagerPrivate *priv; + SoupWebsocketExtensionClass *extension_class; + guint i; + + if (!g_type_is_a (type, SOUP_TYPE_WEBSOCKET_EXTENSION)) + return FALSE; + + priv = soup_websocket_extension_manager_get_instance_private (SOUP_WEBSOCKET_EXTENSION_MANAGER (feature)); + extension_class = g_type_class_peek (type); + + for (i = 0; i < priv->extension_types->len; i++) { + if (priv->extension_types->pdata[i] == (gpointer)extension_class) + return TRUE; + } + + return FALSE; +} + +static void +soup_websocket_extension_manager_session_feature_init (SoupSessionFeatureInterface *feature_interface, + gpointer interface_data) +{ + feature_interface->add_feature = soup_websocket_extension_manager_add_feature; + feature_interface->remove_feature = soup_websocket_extension_manager_remove_feature; + feature_interface->has_feature = soup_websocket_extension_manager_has_feature; +} + +GPtrArray * +soup_websocket_extension_manager_get_supported_extensions (SoupWebsocketExtensionManager *manager) +{ + SoupWebsocketExtensionManagerPrivate *priv; + + g_return_val_if_fail (SOUP_IS_WEBSOCKET_EXTENSION_MANAGER (manager), NULL); + + priv = soup_websocket_extension_manager_get_instance_private (manager); + return priv->extension_types; +} diff --git a/libsoup/soup-websocket-extension-manager.h b/libsoup/soup-websocket-extension-manager.h new file mode 100644 index 00000000..280a5b39 --- /dev/null +++ b/libsoup/soup-websocket-extension-manager.h @@ -0,0 +1,50 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * soup-websocket-extension-manager.h + * + * Copyright (C) 2019 Igalia S.L. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __SOUP_WEBSOCKET_EXTENSION_MANAGER_H__ +#define __SOUP_WEBSOCKET_EXTENSION_MANAGER_H__ 1 + +#include <libsoup/soup-types.h> + +G_BEGIN_DECLS + +#define SOUP_TYPE_WEBSOCKET_EXTENSION_MANAGER (soup_websocket_extension_manager_get_type ()) +#define SOUP_WEBSOCKET_EXTENSION_MANAGER(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), SOUP_TYPE_WEBSOCKET_EXTENSION_MANAGER, SoupWebsocketExtensionManager)) +#define SOUP_IS_WEBSOCKET_EXTENSION_MANAGER(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), SOUP_TYPE_WEBSOCKET_EXTENSION_MANAGER)) +#define SOUP_WEBSOCKET_EXTENSION_MANAGER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SOUP_TYPE_WEBSOCKET_EXTENSION_MANAGER, SoupWebsocketExtensionManagerClass)) +#define SOUP_IS_WEBSOCKET_EXTENSION_MANAGER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SOUP_TYPE_WEBSOCKET_EXTENSION_MANAGER)) +#define SOUP_WEBSOCKET_EXTENSION_MANAGER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SOUP_TYPE_WEBSOCKET_EXTENSION_MANAGER, SoupWebsocketExtensionManagerClass)) + +typedef struct { + GObject parent; +} SoupWebsocketExtensionManager; + +typedef struct { + GObjectClass parent_class; +} SoupWebsocketExtensionManagerClass; + +SOUP_AVAILABLE_IN_2_68 +GType soup_websocket_extension_manager_get_type (void); + +G_END_DECLS + +#endif /* __SOUP_WEBSOCKET_EXTENSION_MANAGER_H__ */ diff --git a/libsoup/soup-websocket-extension.c b/libsoup/soup-websocket-extension.c new file mode 100644 index 00000000..b04a4fd8 --- /dev/null +++ b/libsoup/soup-websocket-extension.c @@ -0,0 +1,221 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * soup-websocket-extension.c + * + * Copyright (C) 2019 Igalia S.L. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "soup-websocket-extension.h" + +/** + * SECTION:soup-websocket-extension + * @short_description: a WebSocket extension + * @see_also: #SoupSession, #SoupWebsocketExtensionManager + * + * SoupWebsocketExtension is the base class for WebSocket extension objects. + * + * Since: 2.68 + */ + +/** + * SoupWebsocketExtensionClass: + * @parent_class: the parent class + * @configure: called to configure the extension with the given parameters + * @get_request_params: called by the client to build the request header. + * It should include the parameters string starting with ';' + * @get_response_params: called by the server to build the response header. + * It should include the parameters string starting with ';' + * @process_outgoing_message: called to process the payload data of a message + * before it's sent. Reserved bits of the header should be changed. + * @process_incoming_message: called to process the payload data of a message + * after it's received. Reserved bits of the header should be cleared. + * + * The class structure for the SoupWebsocketExtension. + * + * Since: 2.68 + */ + +G_DEFINE_ABSTRACT_TYPE (SoupWebsocketExtension, soup_websocket_extension, G_TYPE_OBJECT) + +static void +soup_websocket_extension_init (SoupWebsocketExtension *extension) +{ +} + +static void +soup_websocket_extension_class_init (SoupWebsocketExtensionClass *auth_class) +{ +} + +/** + * soup_websocket_extension_configure: + * @extension: a #SoupWebsocketExtension + * @connection_type: either %SOUP_WEBSOCKET_CONNECTION_CLIENT or %SOUP_WEBSOCKET_CONNECTION_SERVER + * @params: (nullable): the parameters, or %NULL + * @error: return location for a #GError + * + * Configures @extension with the given @params + * + * Return value: %TRUE if extension could be configured with the given parameters, or %FALSE otherwise + */ +gboolean +soup_websocket_extension_configure (SoupWebsocketExtension *extension, + SoupWebsocketConnectionType connection_type, + GHashTable *params, + GError **error) +{ + SoupWebsocketExtensionClass *klass; + + g_return_val_if_fail (SOUP_IS_WEBSOCKET_EXTENSION (extension), FALSE); + g_return_val_if_fail (connection_type != SOUP_WEBSOCKET_CONNECTION_UNKNOWN, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + klass = SOUP_WEBSOCKET_EXTENSION_GET_CLASS (extension); + if (!klass->configure) + return TRUE; + + return klass->configure (extension, connection_type, params, error); +} + +/** + * soup_websocket_extension_get_request_params: + * @extension: a #SoupWebsocketExtension + * + * Get the parameters strings to be included in the request header. If the extension + * doesn't include any parameter in the request, this function returns %NULL. + * + * Returns: (nullable) (transfer full): a new allocated string with the parameters + * + * Since: 2.68 + */ +char * +soup_websocket_extension_get_request_params (SoupWebsocketExtension *extension) +{ + SoupWebsocketExtensionClass *klass; + + g_return_val_if_fail (SOUP_IS_WEBSOCKET_EXTENSION (extension), NULL); + + klass = SOUP_WEBSOCKET_EXTENSION_GET_CLASS (extension); + if (!klass->get_request_params) + return NULL; + + return klass->get_request_params (extension); +} + +/** + * soup_websocket_extension_get_response_params: + * @extension: a #SoupWebsocketExtension + * + * Get the parameters strings to be included in the response header. If the extension + * doesn't include any parameter in the response, this function returns %NULL. + * + * Returns: (nullable) (transfer full): a new allocated string with the parameters + * + * Since: 2.68 + */ +char * +soup_websocket_extension_get_response_params (SoupWebsocketExtension *extension) +{ + SoupWebsocketExtensionClass *klass; + + g_return_val_if_fail (SOUP_IS_WEBSOCKET_EXTENSION (extension), NULL); + + klass = SOUP_WEBSOCKET_EXTENSION_GET_CLASS (extension); + if (!klass->get_response_params) + return NULL; + + return klass->get_response_params (extension); +} + +/** + * soup_websocket_extension_process_outgoing_message: + * @extension: a #SoupWebsocketExtension + * @header: (inout): the message header + * @payload: (transfer full): the payload data + * @error: return location for a #GError + * + * Process a message before it's sent. If the payload isn't changed the given + * @payload is just returned, otherwise g_bytes_unref() is called on the given + * @payload and a new #GBytes is returned with the new data. + * + * Extensions using reserved bits of the header will change them in @header. + * + * Returns: (transfer full): the message payload data + * + * Since: 2.68 + */ +GBytes * +soup_websocket_extension_process_outgoing_message (SoupWebsocketExtension *extension, + guint8 *header, + GBytes *payload, + GError **error) +{ + SoupWebsocketExtensionClass *klass; + + g_return_val_if_fail (SOUP_IS_WEBSOCKET_EXTENSION (extension), NULL); + g_return_val_if_fail (header != NULL, NULL); + g_return_val_if_fail (payload != NULL, NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + klass = SOUP_WEBSOCKET_EXTENSION_GET_CLASS (extension); + if (!klass->process_outgoing_message) + return payload; + + return klass->process_outgoing_message (extension, header, payload, error); +} + +/** + * soup_websocket_extension_process_incoming_message: + * @extension: a #SoupWebsocketExtension + * @header: (inout): the message header + * @payload: (transfer full): the payload data + * @error: return location for a #GError + * + * Process a message after it's received. If the payload isn't changed the given + * @payload is just returned, otherwise g_bytes_unref() is called on the given + * @payload and a new #GBytes is returned with the new data. + * + * Extensions using reserved bits of the header will reset them in @header. + * + * Returns: (transfer full): the message payload data + * + * Since: 2.68 + */ +GBytes * +soup_websocket_extension_process_incoming_message (SoupWebsocketExtension *extension, + guint8 *header, + GBytes *payload, + GError **error) +{ + SoupWebsocketExtensionClass *klass; + + g_return_val_if_fail (SOUP_IS_WEBSOCKET_EXTENSION (extension), NULL); + g_return_val_if_fail (header != NULL, NULL); + g_return_val_if_fail (payload != NULL, NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + klass = SOUP_WEBSOCKET_EXTENSION_GET_CLASS (extension); + if (!klass->process_incoming_message) + return payload; + + return klass->process_incoming_message (extension, header, payload, error); +} diff --git a/libsoup/soup-websocket-extension.h b/libsoup/soup-websocket-extension.h new file mode 100644 index 00000000..4461cfa7 --- /dev/null +++ b/libsoup/soup-websocket-extension.h @@ -0,0 +1,100 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * soup-websocket-extension.h + * + * Copyright (C) 2019 Igalia S.L. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __SOUP_WEBSOCKET_EXTENSION_H__ +#define __SOUP_WEBSOCKET_EXTENSION_H__ 1 + +#include <libsoup/soup-types.h> +#include <libsoup/soup-websocket.h> + +G_BEGIN_DECLS + +#define SOUP_TYPE_WEBSOCKET_EXTENSION (soup_websocket_extension_get_type ()) +#define SOUP_WEBSOCKET_EXTENSION(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SOUP_TYPE_WEBSOCKET_EXTENSION, SoupWebsocketExtension)) +#define SOUP_IS_WEBSOCKET_EXTENSION(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SOUP_TYPE_WEBSOCKET_EXTENSION)) +#define SOUP_WEBSOCKET_EXTENSION_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SOUP_TYPE_WEBSOCKET_EXTENSION, SoupWebsocketExtensionClass)) +#define SOUP_IS_WEBSOCKET_EXTENSION_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((obj), SOUP_TYPE_WEBSOCKET_EXTENSION)) +#define SOUP_WEBSOCKET_EXTENSION_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SOUP_TYPE_WEBSOCKET_EXTENSION, SoupWebsocketExtensionClass)) + +struct _SoupWebsocketExtension { + GObject parent; +}; + +typedef struct { + GObjectClass parent_class; + + const char *name; + + gboolean (* configure) (SoupWebsocketExtension *extension, + SoupWebsocketConnectionType connection_type, + GHashTable *params, + GError **error); + + char *(* get_request_params) (SoupWebsocketExtension *extension); + + char *(* get_response_params) (SoupWebsocketExtension *extension); + + GBytes *(* process_outgoing_message) (SoupWebsocketExtension *extension, + guint8 *header, + GBytes *payload, + GError **error); + + GBytes *(* process_incoming_message) (SoupWebsocketExtension *extension, + guint8 *header, + GBytes *payload, + GError **error); + + /* Padding for future expansion */ + void (*_libsoup_reserved1) (void); + void (*_libsoup_reserved2) (void); + void (*_libsoup_reserved3) (void); + void (*_libsoup_reserved4) (void); +} SoupWebsocketExtensionClass; + +SOUP_AVAILABLE_IN_2_68 +GType soup_websocket_extension_get_type (void); + +SOUP_AVAILABLE_IN_2_68 +gboolean soup_websocket_extension_configure (SoupWebsocketExtension *extension, + SoupWebsocketConnectionType connection_type, + GHashTable *params, + GError **error); +SOUP_AVAILABLE_IN_2_68 +char *soup_websocket_extension_get_request_params (SoupWebsocketExtension *extension); + +SOUP_AVAILABLE_IN_2_68 +char *soup_websocket_extension_get_response_params (SoupWebsocketExtension *extension); + +SOUP_AVAILABLE_IN_2_68 +GBytes *soup_websocket_extension_process_outgoing_message (SoupWebsocketExtension *extension, + guint8 *header, + GBytes *payload, + GError **error); +SOUP_AVAILABLE_IN_2_68 +GBytes *soup_websocket_extension_process_incoming_message (SoupWebsocketExtension *extension, + guint8 *header, + GBytes *payload, + GError **error); + +G_END_DECLS + +#endif /* __SOUP_WEBSOCKET_EXTENSION_H__ */ diff --git a/libsoup/soup-websocket.c b/libsoup/soup-websocket.c index 5038041d..d7011b49 100644 --- a/libsoup/soup-websocket.c +++ b/libsoup/soup-websocket.c @@ -26,7 +26,8 @@ #include "soup-websocket.h" #include "soup-headers.h" -#include "soup-message.h" +#include "soup-message-private.h" +#include "soup-websocket-extension.h" #define FIXED_DIGEST_LEN 20 @@ -254,6 +255,9 @@ choose_subprotocol (SoupMessage *msg, * handshake. The message body and non-WebSocket-related headers are * not modified. * + * Use soup_websocket_client_prepare_handshake_with_extensions() if you + * want to include "Sec-WebSocket-Extensions" header in the request. + * * This is a low-level function; if you use * soup_session_websocket_connect_async() to create a WebSocket * connection, it will call this for you. @@ -265,9 +269,40 @@ soup_websocket_client_prepare_handshake (SoupMessage *msg, const char *origin, char **protocols) { + soup_websocket_client_prepare_handshake_with_extensions (msg, origin, protocols, NULL); +} + +/** + * soup_websocket_client_prepare_handshake_with_extensions: + * @msg: a #SoupMessage + * @origin: (nullable): the "Origin" header to set + * @protocols: (nullable) (array zero-terminated=1): list of + * protocols to offer + * @supported_extensions: (nullable) (element-type GObject.TypeClass): list + * of supported extension types + * + * Adds the necessary headers to @msg to request a WebSocket + * handshake including supported WebSocket extensions. + * The message body and non-WebSocket-related headers are + * not modified. + * + * This is a low-level function; if you use + * soup_session_websocket_connect_async() to create a WebSocket + * connection, it will call this for you. + * + * Since: 2.68 + */ +void +soup_websocket_client_prepare_handshake_with_extensions (SoupMessage *msg, + const char *origin, + char **protocols, + GPtrArray *supported_extensions) +{ guint32 raw[4]; char *key; + g_return_if_fail (SOUP_IS_MESSAGE (msg)); + soup_message_headers_replace (msg->request_headers, "Upgrade", "websocket"); soup_message_headers_append (msg->request_headers, "Connection", "Upgrade"); @@ -292,6 +327,47 @@ soup_websocket_client_prepare_handshake (SoupMessage *msg, "Sec-WebSocket-Protocol", protocols_str); g_free (protocols_str); } + + if (supported_extensions && supported_extensions->len > 0) { + guint i; + GString *extensions; + + extensions = g_string_new (NULL); + + for (i = 0; i < supported_extensions->len; i++) { + SoupWebsocketExtensionClass *extension_class = (SoupWebsocketExtensionClass *)supported_extensions->pdata[i]; + + if (soup_message_disables_feature_by_type (msg, G_TYPE_FROM_CLASS (extension_class))) + continue; + + if (i != 0) + extensions = g_string_append (extensions, ", "); + extensions = g_string_append (extensions, extension_class->name); + + if (extension_class->get_request_params) { + SoupWebsocketExtension *websocket_extension; + gchar *params; + + websocket_extension = g_object_new (G_TYPE_FROM_CLASS (extension_class), NULL); + params = soup_websocket_extension_get_request_params (websocket_extension); + if (params) { + extensions = g_string_append (extensions, params); + g_free (params); + } + g_object_unref (websocket_extension); + } + } + + if (extensions->len > 0) { + soup_message_headers_replace (msg->request_headers, + "Sec-WebSocket-Extensions", + extensions->str); + } else { + soup_message_headers_remove (msg->request_headers, + "Sec-WebSocket-Extensions"); + } + g_string_free (extensions, TRUE); + } } /** @@ -310,6 +386,12 @@ soup_websocket_client_prepare_handshake (SoupMessage *msg, * only requests containing a compatible "Sec-WebSocket-Protocols" * header will be accepted. * + * Requests containing "Sec-WebSocket-Extensions" header will be + * accepted even if the header is not valid. To check a request + * with extensions you need to use + * soup_websocket_server_check_handshake_with_extensions() and provide + * the list of supported extension types. + * * Normally soup_websocket_server_process_handshake() will take care * of this for you, and if you use soup_server_add_websocket_handler() * to handle accepting WebSocket connections, it will call that for @@ -328,8 +410,246 @@ soup_websocket_server_check_handshake (SoupMessage *msg, char **protocols, GError **error) { + return soup_websocket_server_check_handshake_with_extensions (msg, expected_origin, protocols, NULL, error); +} + +static gboolean +websocket_extension_class_equal (gconstpointer a, + gconstpointer b) +{ + return g_str_equal (((const SoupWebsocketExtensionClass *)a)->name, (const char *)b); +} + +static GHashTable * +extract_extension_names_from_request (SoupMessage *msg) +{ + const char *extensions; + GSList *extension_list, *l; + GHashTable *return_value = NULL; + + extensions = soup_message_headers_get_list (msg->request_headers, "Sec-WebSocket-Extensions"); + if (!extensions || !*extensions) + return NULL; + + extension_list = soup_header_parse_list (extensions); + for (l = extension_list; l != NULL; l = g_slist_next (l)) { + char *extension = (char *)l->data; + char *p, *end; + + while (g_ascii_isspace (*extension)) + extension++; + + if (!*extension) + continue; + + p = strstr (extension, ";"); + end = p ? p : extension + strlen (extension); + while (end > extension && g_ascii_isspace (*(end - 1))) + end--; + *end = '\0'; + + if (!return_value) + return_value = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + g_hash_table_add (return_value, g_strdup (extension)); + } + + soup_header_free_list (extension_list); + + return return_value; +} + +static gboolean +process_extensions (SoupMessage *msg, + const char *extensions, + gboolean is_server, + GPtrArray *supported_extensions, + GList **accepted_extensions, + GError **error) +{ + GSList *extension_list, *l; + GHashTable *requested_extensions = NULL; + + if (!supported_extensions || supported_extensions->len == 0) { + if (is_server) + return TRUE; + + g_set_error_literal (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, + _("Server requested unsupported extension")); + return FALSE; + } + + if (!is_server) + requested_extensions = extract_extension_names_from_request (msg); + + extension_list = soup_header_parse_list (extensions); + for (l = extension_list; l != NULL; l = g_slist_next (l)) { + char *extension = (char *)l->data; + char *p, *end; + guint index; + GHashTable *params = NULL; + SoupWebsocketExtension *websocket_extension; + + while (g_ascii_isspace (*extension)) + extension++; + + if (!*extension) { + g_set_error (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, + is_server ? + _("Incorrect WebSocket “%s” header") : + _("Server returned incorrect “%s” key"), + "Sec-WebSocket-Extensions"); + if (accepted_extensions) + g_list_free_full (*accepted_extensions, g_object_unref); + g_clear_pointer (&requested_extensions, g_hash_table_destroy); + soup_header_free_list (extension_list); + + return FALSE; + } + + p = strstr (extension, ";"); + end = p ? p : extension + strlen (extension); + while (end > extension && g_ascii_isspace (*(end - 1))) + end--; + *end = '\0'; + + if (requested_extensions && !g_hash_table_contains (requested_extensions, extension)) { + g_set_error_literal (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, + _("Server requested unsupported extension")); + if (accepted_extensions) + g_list_free_full (*accepted_extensions, g_object_unref); + g_clear_pointer (&requested_extensions, g_hash_table_destroy); + soup_header_free_list (extension_list); + + return FALSE; + } + + if (!g_ptr_array_find_with_equal_func (supported_extensions, extension, websocket_extension_class_equal, &index)) { + if (is_server) + continue; + + g_set_error_literal (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, + _("Server requested unsupported extension")); + if (accepted_extensions) + g_list_free_full (*accepted_extensions, g_object_unref); + g_clear_pointer (&requested_extensions, g_hash_table_destroy); + soup_header_free_list (extension_list); + + return FALSE; + } + + /* If we are just checking headers in server side + * and there's no parameters, it's enough to know + * the extension is supported. + */ + if (is_server && !accepted_extensions && !p) + continue; + + websocket_extension = g_object_new (G_TYPE_FROM_CLASS (supported_extensions->pdata[index]), NULL); + if (accepted_extensions) + *accepted_extensions = g_list_prepend (*accepted_extensions, websocket_extension); + + if (p) { + params = soup_header_parse_semi_param_list_strict (p + 1); + if (!params) { + g_set_error (error, + SOUP_WEBSOCKET_ERROR, + SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, + is_server ? + _("Duplicated parameter in “%s” WebSocket extension header") : + _("Server returned a duplicated parameter in “%s” WebSocket extension header"), + extension); + if (accepted_extensions) + g_list_free_full (*accepted_extensions, g_object_unref); + else + g_object_unref (websocket_extension); + g_clear_pointer (&requested_extensions, g_hash_table_destroy); + soup_header_free_list (extension_list); + + return FALSE; + } + } + + if (!soup_websocket_extension_configure (websocket_extension, + is_server ? SOUP_WEBSOCKET_CONNECTION_SERVER : SOUP_WEBSOCKET_CONNECTION_CLIENT, + params, + error)) { + g_clear_pointer (¶ms, g_hash_table_destroy); + if (accepted_extensions) + g_list_free_full (*accepted_extensions, g_object_unref); + else + g_object_unref (websocket_extension); + g_clear_pointer (&requested_extensions, g_hash_table_destroy); + soup_header_free_list (extension_list); + + return FALSE; + } + g_clear_pointer (¶ms, g_hash_table_destroy); + if (!accepted_extensions) + g_object_unref (websocket_extension); + } + + soup_header_free_list (extension_list); + g_clear_pointer (&requested_extensions, g_hash_table_destroy); + + if (accepted_extensions) + *accepted_extensions = g_list_reverse (*accepted_extensions); + + return TRUE; +} + +/** + * soup_websocket_server_check_handshake_with_extensions: + * @msg: #SoupMessage containing the client side of a WebSocket handshake + * @origin: (nullable): expected Origin header + * @protocols: (nullable) (array zero-terminated=1): allowed WebSocket + * protocols. + * @supported_extensions: (nullable) (element-type GObject.TypeClass): list + * of supported extension types + * @error: return location for a #GError + * + * Examines the method and request headers in @msg and determines + * whether @msg contains a valid handshake request. + * + * If @origin is non-%NULL, then only requests containing a matching + * "Origin" header will be accepted. If @protocols is non-%NULL, then + * only requests containing a compatible "Sec-WebSocket-Protocols" + * header will be accepted. If @supported_extensions is non-%NULL, then + * only requests containing valid supported extensions in + * "Sec-WebSocket-Extensions" header will be accepted. + * + * Normally soup_websocket_server_process_handshake_with_extensioins() + * will take care of this for you, and if you use + * soup_server_add_websocket_handler() to handle accepting WebSocket + * connections, it will call that for you. However, this function may + * be useful if you need to perform more complicated validation; eg, + * accepting multiple different Origins, or handling different protocols + * depending on the path. + * + * Returns: %TRUE if @msg contained a valid WebSocket handshake, + * %FALSE and an error if not. + * + * Since: 2.68 + */ +gboolean +soup_websocket_server_check_handshake_with_extensions (SoupMessage *msg, + const char *expected_origin, + char **protocols, + GPtrArray *supported_extensions, + GError **error) +{ const char *origin; const char *key; + const char *extensions; + + g_return_val_if_fail (SOUP_IS_MESSAGE (msg), FALSE); if (msg->method != SOUP_METHOD_GET) { g_set_error_literal (error, @@ -384,6 +704,12 @@ soup_websocket_server_check_handshake (SoupMessage *msg, return FALSE; } + extensions = soup_message_headers_get_list (msg->request_headers, "Sec-WebSocket-Extensions"); + if (extensions && *extensions) { + if (!process_extensions (msg, extensions, TRUE, supported_extensions, NULL, error)) + return FALSE; + } + return TRUE; } @@ -430,6 +756,12 @@ respond_handshake_bad (SoupMessage *msg, const char *why) * only requests containing a compatible "Sec-WebSocket-Protocols" * header will be accepted. * + * Requests containing "Sec-WebSocket-Extensions" header will be + * accepted even if the header is not valid. To process a request + * with extensions you need to use + * soup_websocket_server_process_handshake_with_extensions() and provide + * the list of supported extension types. + * * This is a low-level function; if you use * soup_server_add_websocket_handler() to handle accepting WebSocket * connections, it will call this for you. @@ -444,12 +776,57 @@ soup_websocket_server_process_handshake (SoupMessage *msg, const char *expected_origin, char **protocols) { + return soup_websocket_server_process_handshake_with_extensions (msg, expected_origin, protocols, NULL, NULL); +} + +/** + * soup_websocket_server_process_handshake_with_extensions: + * @msg: #SoupMessage containing the client side of a WebSocket handshake + * @expected_origin: (nullable): expected Origin header + * @protocols: (nullable) (array zero-terminated=1): allowed WebSocket + * protocols. + * @supported_extensions: (nullable) (element-type GObject.TypeClass): list + * of supported extension types + * @accepted_extensions: (out) (optional) (element-type SoupWebsocketExtension): a + * #GList of #SoupWebsocketExtension objects + * + * Examines the method and request headers in @msg and (assuming @msg + * contains a valid handshake request), fills in the handshake + * response. + * + * If @expected_origin is non-%NULL, then only requests containing a matching + * "Origin" header will be accepted. If @protocols is non-%NULL, then + * only requests containing a compatible "Sec-WebSocket-Protocols" + * header will be accepted. If @supported_extensions is non-%NULL, then + * only requests containing valid supported extensions in + * "Sec-WebSocket-Extensions" header will be accepted. The accepted extensions + * will be returned in @accepted_extensions parameter if non-%NULL. + * + * This is a low-level function; if you use + * soup_server_add_websocket_handler() to handle accepting WebSocket + * connections, it will call this for you. + * + * Returns: %TRUE if @msg contained a valid WebSocket handshake + * request and was updated to contain a handshake response. %FALSE if not. + * + * Since: 2.68 + */ +gboolean +soup_websocket_server_process_handshake_with_extensions (SoupMessage *msg, + const char *expected_origin, + char **protocols, + GPtrArray *supported_extensions, + GList **accepted_extensions) +{ const char *chosen_protocol = NULL; const char *key; + const char *extensions; char *accept_key; GError *error = NULL; - if (!soup_websocket_server_check_handshake (msg, expected_origin, protocols, &error)) { + g_return_val_if_fail (accepted_extensions == NULL || *accepted_extensions == NULL, FALSE); + + if (!soup_websocket_server_check_handshake_with_extensions (msg, expected_origin, protocols, supported_extensions, &error)) { if (g_error_matches (error, SOUP_WEBSOCKET_ERROR, SOUP_WEBSOCKET_ERROR_BAD_ORIGIN)) @@ -473,6 +850,49 @@ soup_websocket_server_process_handshake (SoupMessage *msg, if (chosen_protocol) soup_message_headers_append (msg->response_headers, "Sec-WebSocket-Protocol", chosen_protocol); + extensions = soup_message_headers_get_list (msg->request_headers, "Sec-WebSocket-Extensions"); + if (extensions && *extensions) { + GList *websocket_extensions = NULL; + GList *l; + + process_extensions (msg, extensions, TRUE, supported_extensions, &websocket_extensions, NULL); + if (websocket_extensions) { + GString *response_extensions; + + response_extensions = g_string_new (NULL); + + for (l = websocket_extensions; l && l->data; l = g_list_next (l)) { + SoupWebsocketExtension *websocket_extension; + gchar *params; + + websocket_extension = (SoupWebsocketExtension *)l->data; + if (response_extensions->len > 0) + response_extensions = g_string_append (response_extensions, ", "); + response_extensions = g_string_append (response_extensions, SOUP_WEBSOCKET_EXTENSION_GET_CLASS (websocket_extension)->name); + params = soup_websocket_extension_get_response_params (websocket_extension); + if (params) { + response_extensions = g_string_append (response_extensions, params); + g_free (params); + } + } + + if (response_extensions->len > 0) { + soup_message_headers_replace (msg->response_headers, + "Sec-WebSocket-Extensions", + response_extensions->str); + } else { + soup_message_headers_remove (msg->response_headers, + "Sec-WebSocket-Extensions"); + } + g_string_free (response_extensions, TRUE); + + if (accepted_extensions) + *accepted_extensions = websocket_extensions; + else + g_list_free_full (websocket_extensions, g_object_unref); + } + } + return TRUE; } @@ -486,6 +906,11 @@ soup_websocket_server_process_handshake (SoupMessage *msg, * determines if they contain a valid WebSocket handshake response * (given the handshake request in @msg's request headers). * + * If the response contains the "Sec-WebSocket-Extensions" header, + * the handshake will be considered invalid. You need to use + * soup_websocket_client_verify_handshake_with_extensions() to handle + * responses with extensions. + * * This is a low-level function; if you use * soup_session_websocket_connect_async() to create a WebSocket * connection, it will call this for you. @@ -499,10 +924,50 @@ gboolean soup_websocket_client_verify_handshake (SoupMessage *msg, GError **error) { + return soup_websocket_client_verify_handshake_with_extensions (msg, NULL, NULL, error); +} + +/** + * soup_websocket_client_verify_handshake_with_extensions: + * @msg: #SoupMessage containing both client and server sides of a + * WebSocket handshake + * @supported_extensions: (nullable) (element-type GObject.TypeClass): list + * of supported extension types + * @accepted_extensions: (out) (optional) (element-type SoupWebsocketExtension): a + * #GList of #SoupWebsocketExtension objects + * @error: return location for a #GError + * + * Looks at the response status code and headers in @msg and + * determines if they contain a valid WebSocket handshake response + * (given the handshake request in @msg's request headers). + * + * If @supported_extensions is non-%NULL, extensions included in the + * response "Sec-WebSocket-Extensions" are verified too. Accepted + * extensions are returned in @accepted_extensions parameter if non-%NULL. + * + * This is a low-level function; if you use + * soup_session_websocket_connect_async() to create a WebSocket + * connection, it will call this for you. + * + * Returns: %TRUE if @msg contains a completed valid WebSocket + * handshake, %FALSE and an error if not. + * + * Since: 2.68 + */ +gboolean +soup_websocket_client_verify_handshake_with_extensions (SoupMessage *msg, + GPtrArray *supported_extensions, + GList **accepted_extensions, + GError **error) +{ const char *protocol, *request_protocols, *extensions, *accept_key; char *expected_accept_key; gboolean key_ok; + g_return_val_if_fail (SOUP_IS_MESSAGE (msg), FALSE); + g_return_val_if_fail (accepted_extensions == NULL || *accepted_extensions == NULL, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + if (msg->status_code == SOUP_STATUS_BAD_REQUEST) { g_set_error_literal (error, SOUP_WEBSOCKET_ERROR, @@ -543,11 +1008,8 @@ soup_websocket_client_verify_handshake (SoupMessage *msg, 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; + if (!process_extensions (msg, extensions, FALSE, supported_extensions, accepted_extensions, error)) + return FALSE; } accept_key = soup_message_headers_get_one (msg->response_headers, "Sec-WebSocket-Accept"); diff --git a/libsoup/soup-websocket.h b/libsoup/soup-websocket.h index 20584982..c5dd31ab 100644 --- a/libsoup/soup-websocket.h +++ b/libsoup/soup-websocket.h @@ -72,21 +72,45 @@ SOUP_AVAILABLE_IN_2_50 void soup_websocket_client_prepare_handshake (SoupMessage *msg, const char *origin, char **protocols); +SOUP_AVAILABLE_IN_2_68 +void soup_websocket_client_prepare_handshake_with_extensions (SoupMessage *msg, + const char *origin, + char **protocols, + GPtrArray *supported_extensions); SOUP_AVAILABLE_IN_2_50 gboolean soup_websocket_client_verify_handshake (SoupMessage *msg, GError **error); +SOUP_AVAILABLE_IN_2_68 +gboolean soup_websocket_client_verify_handshake_with_extensions (SoupMessage *msg, + GPtrArray *supported_extensions, + GList **accepted_extensions, + GError **error); SOUP_AVAILABLE_IN_2_50 gboolean soup_websocket_server_check_handshake (SoupMessage *msg, const char *origin, char **protocols, GError **error); +SOUP_AVAILABLE_IN_2_68 +gboolean +soup_websocket_server_check_handshake_with_extensions (SoupMessage *msg, + const char *origin, + char **protocols, + GPtrArray *supported_extensions, + GError **error); SOUP_AVAILABLE_IN_2_50 gboolean soup_websocket_server_process_handshake (SoupMessage *msg, const char *expected_origin, char **protocols); +SOUP_AVAILABLE_IN_2_68 +gboolean +soup_websocket_server_process_handshake_with_extensions (SoupMessage *msg, + const char *expected_origin, + char **protocols, + GPtrArray *supported_extensions, + GList **accepted_extensions); G_END_DECLS diff --git a/libsoup/soup.h b/libsoup/soup.h index 46ca6acf..48b75f0c 100644 --- a/libsoup/soup.h +++ b/libsoup/soup.h @@ -58,6 +58,9 @@ extern "C" { #include <libsoup/soup-version.h> #include <libsoup/soup-websocket.h> #include <libsoup/soup-websocket-connection.h> +#include <libsoup/soup-websocket-extension.h> +#include <libsoup/soup-websocket-extension-deflate.h> +#include <libsoup/soup-websocket-extension-manager.h> #include <libsoup/soup-xmlrpc.h> #include <libsoup/soup-xmlrpc-old.h> diff --git a/meson.build b/meson.build index 94098645..99a1ea5f 100644 --- a/meson.build +++ b/meson.build @@ -159,6 +159,21 @@ if enable_tls_check endif endif +libz_dep = dependency('zlib', required : false) +if not libz_dep.found() + if cc.get_id() != 'msvc' + libz_dep = cc.find_library('z', required : false) + else + libz_dep = cc.find_library('zlib1', required : false) + if not libz_dep.found() + libz_dep = cc.find_library('zlib', required : false) + endif + endif + if not libz_dep.found() or not cc.has_header('zlib.h') + libz_dep = subproject('zlib').get_variable('zlib_dep') + endif +endif + ################################# # Regression tests dependencies # ################################# diff --git a/subprojects/zlib.wrap b/subprojects/zlib.wrap new file mode 100644 index 00000000..6aff13ff --- /dev/null +++ b/subprojects/zlib.wrap @@ -0,0 +1,10 @@ +[wrap-file] +directory = zlib-1.2.11 + +source_url = https://zlib.net/fossils/zlib-1.2.11.tar.gz +source_filename = zlib-1.2.11.tar.gz +source_hash = c3e5e9fdd5004dcb542feda5ee4f0ff0744628baf8ed2dd5d66f8ca1197cb1a1 + +patch_url = https://wrapdb.mesonbuild.com/v1/projects/zlib/1.2.11/3/get_zip +patch_filename = zlib-1.2.11-3-wrap.zip +patch_hash = f07dc491ab3d05daf00632a0591e2ae61b470615b5b73bcf9b3f061fff65cff0 diff --git a/tests/meson.build b/tests/meson.build index ba78944c..e6742b45 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -12,53 +12,53 @@ test_resources = gnome.compile_resources('soup-tests', 'soup-tests.gresource.xml', gresource_bundle : true) -# ['name', is_parallel] +# ['name', is_parallel, extra_deps] tests = [ - ['cache', true], - ['chunk', true], - ['chunk-io', true], - ['coding', true], - ['context', true], - ['continue', true], - ['cookies', true], - ['date', true], - ['forms', true], - ['header-parsing', true], - ['hsts', true], - ['hsts-db', true], - ['misc', true], - ['multipart', true], - ['no-ssl', true], - ['ntlm', true], - ['redirect', true], - ['requester', true], - ['resource', true], - ['session', true], - ['server-auth', true], - ['server', true], - ['sniffing', true], - ['socket', true], - ['ssl', true], - ['streaming', true], - ['timeout', true], - ['tld', true], - ['uri-parsing', true], - ['websocket', true] + ['cache', true, []], + ['chunk', true, []], + ['chunk-io', true, []], + ['coding', true, []], + ['context', true, []], + ['continue', true, []], + ['cookies', true, []], + ['date', true, []], + ['forms', true, []], + ['header-parsing', true, []], + ['hsts', true, []], + ['hsts-db', true, []], + ['misc', true, []], + ['multipart', true, []], + ['no-ssl', true, []], + ['ntlm', true, []], + ['redirect', true, []], + ['requester', true, []], + ['resource', true, []], + ['session', true, []], + ['server-auth', true, []], + ['server', true, []], + ['sniffing', true, []], + ['socket', true, []], + ['ssl', true, []], + ['streaming', true, []], + ['timeout', true, []], + ['tld', true, []], + ['uri-parsing', true, []], + ['websocket', true, [libz_dep]] ] if brotlidec_dep.found() tests += [ - ['brotli-decompressor', true], + ['brotli-decompressor', true, []], ] endif if have_apache tests += [ - ['auth', false], - ['connection', false], - ['range', false], - ['proxy', false], - ['pull-api', false], + ['auth', false, []], + ['connection', false, []], + ['range', false, []], + ['proxy', false, []], + ['pull-api', false, []], ] configure_file(output : 'httpd.conf', @@ -90,10 +90,10 @@ endif if have_php_xmlrpc tests += [ - ['xmlrpc-old-server', true, have_php_xmlrpc], - ['xmlrpc-old', false, have_php_xmlrpc], - ['xmlrpc-server', true, have_php_xmlrpc], - ['xmlrpc', false, have_php_xmlrpc] + ['xmlrpc-old-server', true, []], + ['xmlrpc-old', false, []], + ['xmlrpc-server', true, []], + ['xmlrpc', false, []] ] configure_file(input : 'xmlrpc-server.php', @@ -113,10 +113,11 @@ env.set('MALLOC_PERTURB_', '') foreach test: tests test_name = '@0@-test'.format(test[0]) + test_deps = [ libsoup_dep ] + test[2] test_target = executable(test_name, sources : [ test_name + '.c', test_resources ], link_with : test_utils, - dependencies : libsoup_dep) + dependencies : test_deps) # Increase the timeout as on some architectures the tests could be slower # than the default 30 seconds. test(test_name, test_target, env : env, is_parallel : test[1], timeout : 60) diff --git a/tests/websocket-test.c b/tests/websocket-test.c index 89e329dd..146fdf82 100644 --- a/tests/websocket-test.c +++ b/tests/websocket-test.c @@ -20,6 +20,8 @@ #include "test-utils.h" +#include <zlib.h> + typedef struct { GSocket *listener; gushort port; @@ -35,6 +37,9 @@ typedef struct { gboolean no_server; GIOStream *raw_server; + gboolean enable_extensions; + gboolean disable_deflate_in_message; + GMutex mutex; } Test; @@ -100,15 +105,26 @@ direct_connection_complete (GObject *object, GSocketConnection *conn; SoupURI *uri; GError *error = NULL; + GList *extensions = 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); + if (test->enable_extensions) { + SoupWebsocketExtension *extension; + + extension = g_object_new (SOUP_TYPE_WEBSOCKET_EXTENSION_DEFLATE, NULL); + g_assert_true (soup_websocket_extension_configure (extension, + SOUP_WEBSOCKET_CONNECTION_CLIENT, + NULL, NULL)); + extensions = g_list_prepend (extensions, extension); + } + test->client = soup_websocket_connection_new_with_extensions (G_IO_STREAM (conn), uri, + SOUP_WEBSOCKET_CONNECTION_CLIENT, + NULL, NULL, + extensions); soup_uri_free (uri); g_object_unref (conn); } @@ -122,6 +138,7 @@ got_connection (GSocket *listener, GSocket *sock; GSocketConnection *conn; SoupURI *uri; + GList *extensions = NULL; GError *error = NULL; sock = g_socket_accept (listener, NULL, &error); @@ -135,9 +152,19 @@ got_connection (GSocket *listener, 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); + if (test->enable_extensions) { + SoupWebsocketExtension *extension; + + extension = g_object_new (SOUP_TYPE_WEBSOCKET_EXTENSION_DEFLATE, NULL); + g_assert_true (soup_websocket_extension_configure (extension, + SOUP_WEBSOCKET_CONNECTION_SERVER, + NULL, NULL)); + extensions = g_list_prepend (extensions, extension); + } + test->server = soup_websocket_connection_new_with_extensions (G_IO_STREAM (conn), uri, + SOUP_WEBSOCKET_CONNECTION_SERVER, + NULL, NULL, + extensions); soup_uri_free (uri); g_object_unref (conn); } @@ -171,6 +198,14 @@ setup_direct_connection (Test *test, } static void +setup_direct_connection_with_extensions (Test *test, + gconstpointer data) +{ + test->enable_extensions = TRUE; + setup_direct_connection (test, data); +} + +static void setup_half_direct_connection (Test *test, gconstpointer data) { @@ -179,6 +214,14 @@ setup_half_direct_connection (Test *test, } static void +setup_half_direct_connection_with_extensions (Test *test, + gconstpointer data) +{ + test->no_server = TRUE; + setup_direct_connection_with_extensions (test, data); +} + +static void teardown_direct_connection (Test *test, gconstpointer data) { @@ -200,6 +243,8 @@ setup_soup_server (Test *test, setup_listener (test); test->soup_server = soup_test_server_new (SOUP_TEST_SERVER_IN_THREAD); + if (!test->enable_extensions) + soup_server_remove_websocket_extension (test->soup_server, SOUP_TYPE_WEBSOCKET_EXTENSION_DEFLATE); soup_server_listen_socket (test->soup_server, test->listener, 0, &error); g_assert_no_error (error); @@ -217,11 +262,14 @@ client_connect (Test *test, { char *url; - if (!test->session) - test->session = soup_test_session_new (SOUP_TYPE_SESSION, NULL); + test->session = soup_test_session_new (SOUP_TYPE_SESSION, NULL); + if (test->enable_extensions) + soup_session_add_feature_by_type (test->session, SOUP_TYPE_WEBSOCKET_EXTENSION_MANAGER); url = g_strdup_printf ("ws://127.0.0.1:%u/unix", test->port); test->msg = soup_message_new ("GET", url); + if (test->disable_deflate_in_message) + soup_message_disable_feature (test->msg, SOUP_TYPE_WEBSOCKET_EXTENSION_DEFLATE); g_free (url); soup_session_websocket_connect_async (test->session, test->msg, @@ -264,6 +312,14 @@ setup_soup_connection (Test *test, } static void +setup_soup_connection_with_extensions (Test *test, + gconstpointer data) +{ + test->enable_extensions = TRUE; + setup_soup_connection (test, data); +} + +static void teardown_soup_connection (Test *test, gconstpointer data) { @@ -323,7 +379,27 @@ test_handshake (Test *test, gconstpointer data) { g_assert_cmpint (soup_websocket_connection_get_state (test->client), ==, SOUP_WEBSOCKET_STATE_OPEN); + if (test->enable_extensions) { + GList *extensions = soup_websocket_connection_get_extensions (test->client); + + g_assert_nonnull (extensions); + g_assert_cmpuint (g_list_length (extensions), ==, 1); + g_assert (SOUP_IS_WEBSOCKET_EXTENSION_DEFLATE (extensions->data)); + } else { + g_assert_null (soup_websocket_connection_get_extensions (test->client)); + } + g_assert_cmpint (soup_websocket_connection_get_state (test->server), ==, SOUP_WEBSOCKET_STATE_OPEN); + if (test->enable_extensions) { + GList *extensions = soup_websocket_connection_get_extensions (test->server); + + g_assert_nonnull (extensions); + g_assert_cmpuint (g_list_length (extensions), ==, 1); + g_assert (SOUP_IS_WEBSOCKET_EXTENSION_DEFLATE (extensions->data)); + } else { + g_assert_null (soup_websocket_connection_get_extensions (test->server)); + } + } static void @@ -1041,6 +1117,78 @@ send_fragments_server_thread (gpointer user_data) } static void +do_deflate (z_stream *zstream, + const char *str, + guint8 *buffer, + gsize *length) +{ + zstream->next_in = (void *)str; + zstream->avail_in = strlen (str); + zstream->next_out = buffer; + zstream->avail_out = 512; + + g_assert_cmpint (deflate(zstream, Z_NO_FLUSH), ==, Z_OK); + g_assert_cmpint (zstream->avail_in, ==, 0); + g_assert_cmpint (deflate(zstream, Z_SYNC_FLUSH), ==, Z_OK); + g_assert_cmpint (deflate(zstream, Z_SYNC_FLUSH), ==, Z_BUF_ERROR); + + *length = 512 - zstream->avail_out; + g_assert_cmpuint (*length, <, 126); +} + +static gpointer +send_compressed_fragments_server_thread (gpointer user_data) +{ + Test *test = user_data; + gsize written; + z_stream zstream; + GByteArray *data; + guint8 byte; + guint8 buffer[512]; + gsize buffer_length; + GError *error = NULL; + + memset (&zstream, 0, sizeof(z_stream)); + g_assert (deflateInit2 (&zstream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -15, 8, Z_DEFAULT_STRATEGY) == Z_OK); + + data = g_byte_array_new (); + + do_deflate (&zstream, "one ", buffer, &buffer_length); + byte = 0x00 | 0x01 | 0x40; /* !fin | opcode | compressed */ + data = g_byte_array_append (data, &byte, 1); + byte = (0xFF & buffer_length); /* mask | 7-bit-len */ + data = g_byte_array_append (data, &byte, 1); + data = g_byte_array_append (data, buffer, buffer_length); + + do_deflate (&zstream, "two ", buffer, &buffer_length); + byte = 0x00; /* !fin | no opcode */ + data = g_byte_array_append (data, &byte, 1); + byte = (0xFF & buffer_length); /* mask | 7-bit-len */ + data = g_byte_array_append (data, &byte, 1); + data = g_byte_array_append (data, buffer, buffer_length); + + do_deflate (&zstream, "three", buffer, &buffer_length); + g_assert_cmpuint (buffer_length, >=, 4); + buffer_length -= 4; + byte = 0x80; /* fin | no opcode */ + data = g_byte_array_append (data, &byte, 1); + byte = (0xFF & buffer_length); /* mask | 7-bit-len */ + data = g_byte_array_append (data, &byte, 1); + data = g_byte_array_append (data, buffer, buffer_length); + + g_output_stream_write_all (g_io_stream_get_output_stream (test->raw_server), + data->data, data->len, &written, NULL, &error); + g_assert_no_error (error); + g_assert_cmpuint (written, ==, data->len); + g_io_stream_close (test->raw_server, NULL, &error); + g_assert_no_error (error); + + deflateEnd (&zstream); + + return NULL; +} + +static void test_receive_fragmented (Test *test, gconstpointer data) { @@ -1048,7 +1196,11 @@ test_receive_fragmented (Test *test, GBytes *received = NULL; GBytes *expect; - thread = g_thread_new ("fragment-thread", send_fragments_server_thread, test); + thread = g_thread_new ("fragment-thread", + test->enable_extensions ? + send_compressed_fragments_server_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); @@ -1281,6 +1433,331 @@ test_client_context (Test *test, g_assert_no_error (test->client_error); } +static struct { + const char *client_extension; + gboolean expected_prepare_result; + gboolean server_supports_extensions; + gboolean expected_check_result; + gboolean expected_accepted_extension; + gboolean expected_verify_result; + const char *server_extension; +} deflate_negotiate_tests[] = { + { "permessage-deflate", + /* prepare supported check accepted verify */ + TRUE, TRUE, TRUE, TRUE, TRUE, + "permessage-deflate" + }, + { "permessage-deflate", + /* prepare supported check accepted verify */ + TRUE, FALSE, TRUE, FALSE, TRUE, + "permessage-deflate" + }, + { "permessage-deflate; server_no_context_takeover", + /* prepare supported check accepted verify */ + TRUE, TRUE, TRUE, TRUE, TRUE, + "permessage-deflate; server_no_context_takeover" + }, + { "permessage-deflate; client_no_context_takeover", + /* prepare supported check accepted verify */ + TRUE, TRUE, TRUE, TRUE, TRUE, + "permessage-deflate; client_no_context_takeover" + }, + { "permessage-deflate; server_max_window_bits=8", + /* prepare supported check accepted verify */ + TRUE, TRUE, TRUE, TRUE, TRUE, + "permessage-deflate; server_max_window_bits=8" + }, + { "permessage-deflate; client_max_window_bits", + /* prepare supported check accepted verify */ + TRUE, TRUE, TRUE, TRUE, TRUE, + "permessage-deflate; client_max_window_bits" + }, + { "permessage-deflate; client_max_window_bits=10", + /* prepare supported check accepted verify */ + TRUE, TRUE, TRUE, TRUE, TRUE, + "permessage-deflate; client_max_window_bits=10" + }, + { "permessage-deflate; client_no_context_takeover; server_max_window_bits=10", + /* prepare supported check accepted verify */ + TRUE, TRUE, TRUE, TRUE, TRUE, + "permessage-deflate; client_no_context_takeover; server_max_window_bits=10" + }, + { "permessage-deflate; unknown_parameter", + /* prepare supported check accepted verify */ + TRUE, TRUE, FALSE, FALSE, FALSE, + NULL + }, + { "permessage-deflate; client_no_context_takeover; client_no_context_takeover", + /* prepare supported check accepted verify */ + TRUE, TRUE, FALSE, FALSE, FALSE, + NULL + }, + { "permessage-deflate; server_max_window_bits=10; server_max_window_bits=15", + /* prepare supported check accepted verify */ + TRUE, TRUE, FALSE, FALSE, FALSE, + NULL + }, + { "permessage-deflate; client_no_context_takeover=15", + /* prepare supported check accepted verify */ + TRUE, TRUE, FALSE, FALSE, FALSE, + NULL + }, + { "permessage-deflate; server_no_context_takeover=15", + /* prepare supported check accepted verify */ + TRUE, TRUE, FALSE, FALSE, FALSE, + NULL + }, + { "permessage-deflate; server_max_window_bits", + /* prepare supported check accepted verify */ + TRUE, TRUE, FALSE, FALSE, FALSE, + NULL + }, + { "permessage-deflate; server_max_window_bits=7", + /* prepare supported check accepted verify */ + TRUE, TRUE, FALSE, FALSE, FALSE, + NULL + }, + { "permessage-deflate; server_max_window_bits=16", + /* prepare supported check accepted verify */ + TRUE, TRUE, FALSE, FALSE, FALSE, + NULL + }, + { "permessage-deflate; client_max_window_bits=7", + /* prepare supported check accepted verify */ + TRUE, TRUE, FALSE, FALSE, FALSE, + NULL + }, + { "permessage-deflate; client_max_window_bits=16", + /* prepare supported check accepted verify */ + TRUE, TRUE, FALSE, FALSE, FALSE, + NULL + }, + { "permessage-deflate; client_max_window_bits=foo", + /* prepare supported check accepted verify */ + TRUE, TRUE, FALSE, FALSE, FALSE, + NULL + }, + { "permessage-deflate; server_max_window_bits=bar", + /* prepare supported check accepted verify */ + TRUE, TRUE, FALSE, FALSE, FALSE, + NULL + }, + { "permessage-deflate; client_max_window_bits=15foo", + /* prepare supported check accepted verify */ + TRUE, TRUE, FALSE, FALSE, FALSE, + NULL + }, + { "permessage-deflate; server_max_window_bits=10bar", + /* prepare supported check accepted verify */ + TRUE, TRUE, FALSE, FALSE, FALSE, + NULL + }, +}; + +static void +test_deflate_negotiate_direct (Test *test, + gconstpointer unused) +{ + GPtrArray *supported_extensions; + guint i; + + supported_extensions = g_ptr_array_new_full (1, g_type_class_unref); + g_ptr_array_add (supported_extensions, g_type_class_ref (SOUP_TYPE_WEBSOCKET_EXTENSION_DEFLATE)); + + for (i = 0; i < G_N_ELEMENTS (deflate_negotiate_tests); i++) { + SoupMessage *msg; + gboolean result; + GList *accepted_extensions = NULL; + GError *error = NULL; + + msg = soup_message_new ("GET", "http://127.0.0.1"); + + soup_websocket_client_prepare_handshake (msg, NULL, NULL); + soup_message_headers_append (msg->request_headers, "Sec-WebSocket-Extensions", deflate_negotiate_tests[i].client_extension); + result = soup_websocket_server_check_handshake_with_extensions (msg, NULL, NULL, + deflate_negotiate_tests[i].server_supports_extensions ? + supported_extensions : NULL, + &error); + g_assert (result == deflate_negotiate_tests[i].expected_check_result); + if (result) { + g_assert_no_error (error); + } else { + g_assert_error (error, SOUP_WEBSOCKET_ERROR, SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE); + g_clear_error (&error); + } + + result = soup_websocket_server_process_handshake_with_extensions (msg, NULL, NULL, + deflate_negotiate_tests[i].server_supports_extensions ? + supported_extensions : NULL, + &accepted_extensions); + g_assert (result == deflate_negotiate_tests[i].expected_check_result); + if (deflate_negotiate_tests[i].expected_accepted_extension) { + const char *extension; + + extension = soup_message_headers_get_one (msg->response_headers, "Sec-WebSocket-Extensions"); + g_assert_cmpstr (extension, ==, deflate_negotiate_tests[i].server_extension); + g_assert_nonnull (accepted_extensions); + g_assert_cmpuint (g_list_length (accepted_extensions), ==, 1); + g_assert (SOUP_IS_WEBSOCKET_EXTENSION_DEFLATE (accepted_extensions->data)); + g_list_free_full (accepted_extensions, g_object_unref); + accepted_extensions = NULL; + } else { + g_assert_null (accepted_extensions); + } + + result = soup_websocket_client_verify_handshake_with_extensions (msg, supported_extensions, &accepted_extensions, &error); + g_assert (result == deflate_negotiate_tests[i].expected_verify_result); + if (result) { + g_assert_no_error (error); + } else { + g_assert_error (error, SOUP_WEBSOCKET_ERROR, SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE); + g_clear_error (&error); + } + if (deflate_negotiate_tests[i].expected_accepted_extension) { + g_assert_nonnull (accepted_extensions); + g_assert_cmpuint (g_list_length (accepted_extensions), ==, 1); + g_assert (SOUP_IS_WEBSOCKET_EXTENSION_DEFLATE (accepted_extensions->data)); + g_list_free_full (accepted_extensions, g_object_unref); + accepted_extensions = NULL; + } else { + g_assert_null (accepted_extensions); + } + + g_object_unref (msg); + } + + g_ptr_array_unref (supported_extensions); +} + +static void +test_deflate_disabled_in_message_direct (Test *test, + gconstpointer unused) +{ + SoupMessage *msg; + GPtrArray *supported_extensions; + GList *accepted_extensions = NULL; + GError *error = NULL; + + supported_extensions = g_ptr_array_new_full (1, g_type_class_unref); + g_ptr_array_add (supported_extensions, g_type_class_ref (SOUP_TYPE_WEBSOCKET_EXTENSION_DEFLATE)); + + msg = soup_message_new ("GET", "http://127.0.0.1"); + soup_message_disable_feature (msg, SOUP_TYPE_WEBSOCKET_EXTENSION_DEFLATE); + soup_websocket_client_prepare_handshake_with_extensions (msg, NULL, NULL, supported_extensions); + g_assert_cmpstr (soup_message_headers_get_one (msg->request_headers, "Sec-WebSocket-Extensions"), ==, NULL); + + g_assert_true (soup_websocket_server_check_handshake_with_extensions (msg, NULL, NULL, supported_extensions, &error)); + g_assert_no_error (error); + + g_assert_true (soup_websocket_server_process_handshake_with_extensions (msg, NULL, NULL, supported_extensions, &accepted_extensions)); + g_assert_null (accepted_extensions); + g_assert_cmpstr (soup_message_headers_get_one (msg->response_headers, "Sec-WebSocket-Extensions"), ==, NULL); + + g_assert_true (soup_websocket_client_verify_handshake_with_extensions (msg, supported_extensions, &accepted_extensions, &error)); + g_assert_no_error (error); + g_assert_null (accepted_extensions); + + g_object_unref (msg); + g_ptr_array_unref (supported_extensions); +} + +static void +test_deflate_disabled_in_message_soup (Test *test, + gconstpointer unused) +{ + test->enable_extensions = TRUE; + test->disable_deflate_in_message = TRUE; + setup_soup_server (test, NULL, NULL, got_server_connection, test); + client_connect (test, NULL, NULL, got_client_connection, test); + WAIT_UNTIL (test->server != NULL); + WAIT_UNTIL (test->client != NULL || test->client_error != NULL); + g_assert_no_error (test->client_error); + + g_assert_cmpstr (soup_message_headers_get_one (test->msg->request_headers, "Sec-WebSocket-Extensions"), ==, NULL); + g_assert_cmpstr (soup_message_headers_get_one (test->msg->response_headers, "Sec-WebSocket-Extensions"), ==, NULL); +} + +static gpointer +send_compressed_fragments_error_server_thread (gpointer user_data) +{ + Test *test = user_data; + gsize written; + z_stream zstream; + GByteArray *data; + guint8 byte; + guint8 buffer[512]; + gsize buffer_length; + GError *error = NULL; + + memset (&zstream, 0, sizeof(z_stream)); + g_assert (deflateInit2 (&zstream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -15, 8, Z_DEFAULT_STRATEGY) == Z_OK); + + data = g_byte_array_new (); + + do_deflate (&zstream, "one ", buffer, &buffer_length); + byte = 0x00 | 0x01 | 0x40; /* !fin | opcode | compressed */ + data = g_byte_array_append (data, &byte, 1); + byte = (0xFF & buffer_length); /* mask | 7-bit-len */ + data = g_byte_array_append (data, &byte, 1); + data = g_byte_array_append (data, buffer, buffer_length); + + /* Only the first fragment should include the compressed bit set. */ + do_deflate (&zstream, "two ", buffer, &buffer_length); + byte = 0x00 | 0x00 | 0x40; /* !fin | no opcode | compressed */ + data = g_byte_array_append (data, &byte, 1); + byte = (0xFF & buffer_length); /* mask | 7-bit-len */ + data = g_byte_array_append (data, &byte, 1); + data = g_byte_array_append (data, buffer, buffer_length); + + do_deflate (&zstream, "three", buffer, &buffer_length); + g_assert_cmpuint (buffer_length, >=, 4); + buffer_length -= 4; + byte = 0x80; /* fin | no opcode */ + data = g_byte_array_append (data, &byte, 1); + byte = (0xFF & buffer_length); /* mask | 7-bit-len */ + data = g_byte_array_append (data, &byte, 1); + data = g_byte_array_append (data, buffer, buffer_length); + + g_output_stream_write_all (g_io_stream_get_output_stream (test->raw_server), + data->data, data->len, &written, NULL, &error); + g_assert_no_error (error); + g_assert_cmpuint (written, ==, data->len); + g_io_stream_close (test->raw_server, NULL, &error); + g_assert_no_error (error); + + deflateEnd (&zstream); + + return NULL; +} + +static void +test_deflate_receive_fragmented_error (Test *test, + gconstpointer data) +{ + GThread *thread; + GBytes *received = NULL; + gboolean close_event = FALSE; + GError *error = NULL; + + thread = g_thread_new ("deflate-fragment-error-thread", + send_compressed_fragments_error_server_thread, + test); + + g_signal_connect (test->client, "error", G_CALLBACK (on_error_copy), &error); + g_signal_connect (test->client, "message", G_CALLBACK (on_text_message), &received); + g_signal_connect (test->client, "closed", G_CALLBACK (on_close_set_flag), &close_event); + + WAIT_UNTIL (error != NULL || received != NULL); + g_assert_error (error, SOUP_WEBSOCKET_ERROR, SOUP_WEBSOCKET_CLOSE_PROTOCOL_ERROR); + g_clear_error (&error); + g_assert_null (received); + + g_thread_join (thread); + + WAIT_UNTIL (soup_websocket_connection_get_state (test->client) == SOUP_WEBSOCKET_STATE_CLOSED); + g_assert (close_event); +} + int main (int argc, char *argv[]) @@ -1430,6 +1907,67 @@ main (int argc, test_server_receive_unmasked_frame, teardown_soup_connection); + g_test_add ("/websocket/soup/deflate-handshake", Test, NULL, + setup_soup_connection_with_extensions, + test_handshake, + teardown_soup_connection); + + g_test_add ("/websocket/direct/deflate-negotiate", Test, NULL, NULL, + test_deflate_negotiate_direct, + NULL); + + g_test_add ("/websocket/direct/deflate-disabled-in-message", Test, NULL, NULL, + test_deflate_disabled_in_message_direct, + NULL); + g_test_add ("/websocket/soup/deflate-disabled-in-message", Test, NULL, NULL, + test_deflate_disabled_in_message_soup, + teardown_soup_connection); + + g_test_add ("/websocket/direct/deflate-send-client-to-server", Test, NULL, + setup_direct_connection_with_extensions, + test_send_client_to_server, + teardown_direct_connection); + g_test_add ("/websocket/soup/deflate-send-client-to-server", Test, NULL, + setup_soup_connection_with_extensions, + test_send_client_to_server, + teardown_soup_connection); + + g_test_add ("/websocket/direct/deflate-send-server-to-client", Test, NULL, + setup_direct_connection_with_extensions, + test_send_server_to_client, + teardown_direct_connection); + g_test_add ("/websocket/soup/deflate-send-server-to-client", Test, NULL, + setup_soup_connection_with_extensions, + test_send_server_to_client, + teardown_soup_connection); + + g_test_add ("/websocket/direct/deflate-send-big-packets", Test, NULL, + setup_direct_connection_with_extensions, + test_send_big_packets, + teardown_direct_connection); + g_test_add ("/websocket/soup/deflate-send-big-packets", Test, NULL, + setup_soup_connection_with_extensions, + test_send_big_packets, + teardown_soup_connection); + + g_test_add ("/websocket/direct/deflate-send-empty-packets", Test, NULL, + setup_direct_connection_with_extensions, + test_send_empty_packets, + teardown_direct_connection); + g_test_add ("/websocket/soup/deflate-send-empty-packets", Test, NULL, + setup_soup_connection_with_extensions, + test_send_empty_packets, + teardown_soup_connection); + + g_test_add ("/websocket/direct/deflate-receive-fragmented", Test, NULL, + setup_half_direct_connection_with_extensions, + test_receive_fragmented, + teardown_direct_connection); + g_test_add ("/websocket/direct/deflate-receive-fragmented-error", Test, NULL, + setup_half_direct_connection_with_extensions, + test_deflate_receive_fragmented_error, + teardown_direct_connection); + if (g_test_slow ()) { g_test_add ("/websocket/direct/close-after-timeout", Test, NULL, setup_half_direct_connection, |