summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCarlos Garcia Campos <cgarcia@igalia.com>2019-07-04 09:31:39 +0200
committerCarlos Garcia Campos <carlosgc@gnome.org>2019-07-31 14:09:00 +0200
commit13cea0fdf2c935af4e38849c8cd550e9b654b2b9 (patch)
tree6410cc559e45dd7bf5ab426b042861dd168a8f66
parent5c45253243a2deca4880d4f614f2932373445cf9 (diff)
downloadlibsoup-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.
-rw-r--r--docs/reference/libsoup-2.4-sections.txt44
-rw-r--r--docs/reference/meson.build1
-rw-r--r--libsoup/meson.build8
-rw-r--r--libsoup/soup-message-private.h2
-rw-r--r--libsoup/soup-message.c17
-rw-r--r--libsoup/soup-server.c157
-rw-r--r--libsoup/soup-server.h9
-rw-r--r--libsoup/soup-session.c45
-rw-r--r--libsoup/soup-types.h2
-rw-r--r--libsoup/soup-websocket-connection.c164
-rw-r--r--libsoup/soup-websocket-connection.h10
-rw-r--r--libsoup/soup-websocket-extension-deflate.c503
-rw-r--r--libsoup/soup-websocket-extension-deflate.h49
-rw-r--r--libsoup/soup-websocket-extension-manager-private.h30
-rw-r--r--libsoup/soup-websocket-extension-manager.c180
-rw-r--r--libsoup/soup-websocket-extension-manager.h50
-rw-r--r--libsoup/soup-websocket-extension.c221
-rw-r--r--libsoup/soup-websocket-extension.h100
-rw-r--r--libsoup/soup-websocket.c476
-rw-r--r--libsoup/soup-websocket.h24
-rw-r--r--libsoup/soup.h3
-rw-r--r--meson.build15
-rw-r--r--subprojects/zlib.wrap10
-rw-r--r--tests/meson.build85
-rw-r--r--tests/websocket-test.c556
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 (&params, g_hash_table_destroy);
+ if (accepted_extensions)
+ g_list_free_full (*accepted_extensions, g_object_unref);
+ else
+ g_object_unref (websocket_extension);
+ g_clear_pointer (&requested_extensions, g_hash_table_destroy);
+ soup_header_free_list (extension_list);
+
+ return FALSE;
+ }
+ g_clear_pointer (&params, g_hash_table_destroy);
+ if (!accepted_extensions)
+ g_object_unref (websocket_extension);
+ }
+
+ soup_header_free_list (extension_list);
+ g_clear_pointer (&requested_extensions, g_hash_table_destroy);
+
+ if (accepted_extensions)
+ *accepted_extensions = g_list_reverse (*accepted_extensions);
+
+ return TRUE;
+}
+
+/**
+ * soup_websocket_server_check_handshake_with_extensions:
+ * @msg: #SoupMessage containing the client side of a WebSocket handshake
+ * @origin: (nullable): expected Origin header
+ * @protocols: (nullable) (array zero-terminated=1): allowed WebSocket
+ * protocols.
+ * @supported_extensions: (nullable) (element-type GObject.TypeClass): list
+ * of supported extension types
+ * @error: return location for a #GError
+ *
+ * Examines the method and request headers in @msg and determines
+ * whether @msg contains a valid handshake request.
+ *
+ * If @origin is non-%NULL, then only requests containing a matching
+ * "Origin" header will be accepted. If @protocols is non-%NULL, then
+ * only requests containing a compatible "Sec-WebSocket-Protocols"
+ * header will be accepted. If @supported_extensions is non-%NULL, then
+ * only requests containing valid supported extensions in
+ * "Sec-WebSocket-Extensions" header will be accepted.
+ *
+ * Normally soup_websocket_server_process_handshake_with_extensioins()
+ * will take care of this for you, and if you use
+ * soup_server_add_websocket_handler() to handle accepting WebSocket
+ * connections, it will call that for you. However, this function may
+ * be useful if you need to perform more complicated validation; eg,
+ * accepting multiple different Origins, or handling different protocols
+ * depending on the path.
+ *
+ * Returns: %TRUE if @msg contained a valid WebSocket handshake,
+ * %FALSE and an error if not.
+ *
+ * Since: 2.68
+ */
+gboolean
+soup_websocket_server_check_handshake_with_extensions (SoupMessage *msg,
+ const char *expected_origin,
+ char **protocols,
+ GPtrArray *supported_extensions,
+ GError **error)
+{
const char *origin;
const char *key;
+ const char *extensions;
+
+ g_return_val_if_fail (SOUP_IS_MESSAGE (msg), FALSE);
if (msg->method != SOUP_METHOD_GET) {
g_set_error_literal (error,
@@ -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,