summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDan Winship <danw@gnome.org>2014-11-30 10:26:23 -0500
committerDan Winship <danw@gnome.org>2015-03-01 10:36:55 -0500
commitbad410554310c94ce2a758b9e8d3ca2c0e39903b (patch)
tree9ac960779ea0a070ed75d345f08f2cd1e19955d9
parentdfc36f5feefcbc20fe2f61682519ba5373b03a84 (diff)
downloadlibsoup-bad410554310c94ce2a758b9e8d3ca2c0e39903b.tar.gz
websockets: add WebSocket support to SoupSession and SoupServer
Add new SoupSession and SoupServer API to use the new WebSocket support. Based on code originally from the Cockpit project, and on earlier work by Lionel Landwerlin to merge that into libsoup. https://bugzilla.gnome.org/show_bug.cgi?id=627738
-rw-r--r--docs/reference/libsoup-2.4-sections.txt6
-rw-r--r--libsoup/libsoup-2.4.sym3
-rw-r--r--libsoup/soup-server.c266
-rw-r--r--libsoup/soup-server.h15
-rw-r--r--libsoup/soup-session.c131
-rw-r--r--libsoup/soup-session.h15
-rw-r--r--libsoup/soup-websocket.c45
-rw-r--r--tests/websocket-test.c214
8 files changed, 624 insertions, 71 deletions
diff --git a/docs/reference/libsoup-2.4-sections.txt b/docs/reference/libsoup-2.4-sections.txt
index 293c12f8..92a35637 100644
--- a/docs/reference/libsoup-2.4-sections.txt
+++ b/docs/reference/libsoup-2.4-sections.txt
@@ -239,6 +239,9 @@ soup_server_add_handler
soup_server_add_early_handler
soup_server_remove_handler
<SUBSECTION>
+SoupServerWebsocketCallback
+soup_server_add_websocket_handler
+<SUBSECTION>
SoupClientContext
soup_client_context_get_local_address
soup_client_context_get_remote_address
@@ -445,6 +448,9 @@ soup_session_send
soup_session_send_async
soup_session_send_finish
<SUBSECTION>
+soup_session_websocket_connect_async
+soup_session_websocket_connect_finish
+<SUBSECTION>
soup_session_prefetch_dns
soup_session_prepare_for_uri
soup_session_abort
diff --git a/libsoup/libsoup-2.4.sym b/libsoup/libsoup-2.4.sym
index 4f52b842..69d46efb 100644
--- a/libsoup/libsoup-2.4.sym
+++ b/libsoup/libsoup-2.4.sym
@@ -373,6 +373,7 @@ soup_server_accept_iostream
soup_server_add_auth_domain
soup_server_add_early_handler
soup_server_add_handler
+soup_server_add_websocket_handler
soup_server_disconnect
soup_server_get_async_context
soup_server_get_listener
@@ -438,6 +439,8 @@ soup_session_sync_get_type
soup_session_sync_new
soup_session_sync_new_with_options
soup_session_unpause_message
+soup_session_websocket_connect_async
+soup_session_websocket_connect_finish
soup_session_would_redirect
soup_socket_connect_async
soup_socket_connect_sync
diff --git a/libsoup/soup-server.c b/libsoup/soup-server.c
index 2807439d..84d5bc0a 100644
--- a/libsoup/soup-server.c
+++ b/libsoup/soup-server.c
@@ -19,6 +19,8 @@
#include "soup-misc-private.h"
#include "soup-path-map.h"
#include "soup-socket-private.h"
+#include "soup-websocket.h"
+#include "soup-websocket-connection.h"
/**
* SECTION:soup-server
@@ -68,16 +70,26 @@
* #SoupServer will return a %SOUP_STATUS_CONTINUE status before
* continuing.)
*
- * The server will then read in the response body (if present), and
- * then (assuming no previous step assigned a status to the message)
- * call any "normal" handler (added with soup_server_add_handler())
- * for the message's Request-URI.
+ * The server will then read in the response body (if present). At
+ * this point, if there are no handlers at all defined for the
+ * Request-URI, then the server will return %SOUP_STATUS_NOT_FOUND to
+ * the client.
*
- * If the message still has no status code after this step (and has
- * not been paused with soup_server_pause_message()), then it will
- * automatically be given a status of %SOUP_STATUS_NOT_FOUND (if there
- * was no handler for the path) or %SOUP_STATUS_INTERNAL_SERVER_ERROR
- * (if a handler ran but did not assign a status).
+ * Otherwise (assuming no previous step assigned a status to the
+ * message) any "normal" handlers (added with
+ * soup_server_add_handler()) for the message's Request-URI will be
+ * run.
+ *
+ * Then, if the path has a WebSocket handler registered (and has
+ * not yet been assigned a status), #SoupServer will attempt to
+ * validate the WebSocket handshake, filling in the response and
+ * setting a status of %SOUP_STATUS_SWITCHING_PROTOCOLS or
+ * %SOUP_STATUS_BAD_REQUEST accordingly.
+ *
+ * If the message still has no status code at this point (and has not
+ * been paused with soup_server_pause_message()), then it will be
+ * given a status of %SOUP_STATUS_INTERNAL_SERVER_ERROR (because at
+ * least one handler ran, but returned without assigning a status).
*
* Finally, the server will emit #SoupServer::request-finished (or
* #SoupServer::request-aborted if an I/O error occurred before
@@ -141,6 +153,12 @@ typedef struct {
SoupServerCallback callback;
GDestroyNotify destroy;
gpointer user_data;
+
+ char *websocket_origin;
+ char **websocket_protocols;
+ SoupServerWebsocketCallback websocket_callback;
+ GDestroyNotify websocket_destroy;
+ gpointer websocket_user_data;
} SoupServerHandler;
typedef struct {
@@ -157,7 +175,7 @@ typedef struct {
gboolean raw_paths;
SoupPathMap *handlers;
-
+
GSList *auth_domains;
char **http_aliases, **https_aliases;
@@ -198,10 +216,14 @@ static void
free_handler (SoupServerHandler *handler)
{
g_free (handler->path);
+ g_free (handler->websocket_origin);
+ g_strfreev (handler->websocket_protocols);
if (handler->early_destroy)
handler->early_destroy (handler->early_user_data);
if (handler->destroy)
handler->destroy (handler->user_data);
+ if (handler->websocket_destroy)
+ handler->websocket_destroy (handler->websocket_user_data);
g_slice_free (SoupServerHandler, handler);
}
@@ -1201,28 +1223,33 @@ request_finished (SoupMessage *msg, SoupMessageIOCompletion completion, gpointer
*/
#define NORMALIZED_PATH(path) ((path) && *(path) ? (path) : "/")
-/* Returns TRUE if a handler exists, FALSE if not */
-static gboolean
-call_handler (SoupServer *server, SoupClientContext *client,
- SoupMessage *msg, gboolean early)
+static SoupServerHandler *
+get_handler (SoupServer *server, SoupMessage *msg)
{
SoupServerPrivate *priv = SOUP_SERVER_GET_PRIVATE (server);
- SoupServerHandler *handler;
SoupURI *uri;
- GHashTable *form_data_set;
uri = soup_message_get_uri (msg);
- handler = soup_path_map_lookup (priv->handlers, NORMALIZED_PATH (uri->path));
- if (!handler)
- return FALSE;
- else if (early && !handler->early_callback)
- return TRUE;
+ return soup_path_map_lookup (priv->handlers, NORMALIZED_PATH (uri->path));
+}
+
+static void
+call_handler (SoupServer *server, SoupServerHandler *handler,
+ SoupClientContext *client, SoupMessage *msg,
+ gboolean early)
+{
+ GHashTable *form_data_set;
+ SoupURI *uri;
+
+ if (early && !handler->early_callback)
+ return;
else if (!early && !handler->callback)
- return TRUE;
+ return;
if (msg->status_code != 0)
- return TRUE;
+ return;
+ uri = soup_message_get_uri (msg);
if (uri->query)
form_data_set = soup_form_decode (uri->query);
else
@@ -1240,8 +1267,6 @@ call_handler (SoupServer *server, SoupClientContext *client,
if (form_data_set)
g_hash_table_unref (form_data_set);
-
- return TRUE;
}
static void
@@ -1249,6 +1274,7 @@ got_headers (SoupMessage *msg, SoupClientContext *client)
{
SoupServer *server = client->server;
SoupServerPrivate *priv = SOUP_SERVER_GET_PRIVATE (server);
+ SoupServerHandler *handler;
SoupURI *uri;
SoupDate *date;
char *date_string;
@@ -1324,22 +1350,67 @@ got_headers (SoupMessage *msg, SoupClientContext *client)
}
/* Otherwise, call the early handlers. */
- call_handler (server, client, msg, TRUE);
+ handler = get_handler (server, msg);
+ if (handler)
+ call_handler (server, handler, client, msg, TRUE);
+}
+
+static void
+complete_websocket_upgrade (SoupMessage *msg, gpointer user_data)
+{
+ SoupClientContext *client = user_data;
+ SoupServer *server = client->server;
+ SoupURI *uri = soup_message_get_uri (msg);
+ SoupServerHandler *handler;
+ GIOStream *stream;
+ SoupWebsocketConnection *conn;
+
+ handler = get_handler (server, msg);
+ if (!handler || !handler->websocket_callback)
+ return;
+
+ 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"));
+ g_object_unref (stream);
+ soup_client_context_unref (client);
+
+ (*handler->websocket_callback) (server, conn, uri->path, client,
+ handler->websocket_user_data);
+ g_object_unref (conn);
}
static void
got_body (SoupMessage *msg, SoupClientContext *client)
{
SoupServer *server = client->server;
+ SoupServerHandler *handler;
g_signal_emit (server, signals[REQUEST_READ], 0, msg, client);
if (msg->status_code != 0)
return;
- if (!call_handler (server, client, msg, FALSE)) {
- if (msg->status_code == 0)
- soup_message_set_status (msg, SOUP_STATUS_NOT_FOUND);
+ handler = get_handler (server, msg);
+ if (!handler) {
+ soup_message_set_status (msg, SOUP_STATUS_NOT_FOUND);
+ return;
+ }
+
+ call_handler (server, handler, client, msg, FALSE);
+ if (msg->status_code != 0)
+ return;
+
+ if (handler->websocket_callback && msg->status_code == 0) {
+ if (soup_websocket_server_process_handshake (msg,
+ handler->websocket_origin,
+ handler->websocket_protocols)) {
+ g_signal_connect (msg, "wrote-informational",
+ G_CALLBACK (complete_websocket_upgrade),
+ soup_client_context_ref (client));
+ }
}
}
@@ -2395,40 +2466,23 @@ soup_client_context_steal_connection (SoupClientContext *client)
* for details of what handlers can/should do.
**/
-static void
-add_handler_internal (SoupServer *server,
- const char *path,
- gboolean early,
- SoupServerCallback callback,
- gpointer user_data,
- GDestroyNotify destroy)
+static SoupServerHandler *
+get_or_create_handler (SoupServer *server, const char *exact_path)
{
SoupServerPrivate *priv = SOUP_SERVER_GET_PRIVATE (server);
SoupServerHandler *handler;
- path = NORMALIZED_PATH (path);
+ exact_path = NORMALIZED_PATH (exact_path);
- handler = soup_path_map_lookup (priv->handlers, path);
- if (handler && !strcmp (handler->path, path)) {
- if (early && handler->early_destroy)
- handler->early_destroy (handler->early_user_data);
- else if (!early && handler->destroy)
- handler->destroy (handler->user_data);
- } else {
- handler = g_slice_new0 (SoupServerHandler);
- handler->path = g_strdup (path);
- soup_path_map_add (priv->handlers, path, handler);
- }
+ handler = soup_path_map_lookup (priv->handlers, exact_path);
+ if (handler && !strcmp (handler->path, exact_path))
+ return handler;
- if (early) {
- handler->early_callback = callback;
- handler->early_destroy = destroy;
- handler->early_user_data = user_data;
- } else {
- handler->callback = callback;
- handler->destroy = destroy;
- handler->user_data = user_data;
- }
+ handler = g_slice_new0 (SoupServerHandler);
+ handler->path = g_strdup (exact_path);
+ soup_path_map_add (priv->handlers, exact_path, handler);
+
+ return handler;
}
/**
@@ -2484,11 +2538,18 @@ soup_server_add_handler (SoupServer *server,
gpointer user_data,
GDestroyNotify destroy)
{
+ SoupServerHandler *handler;
+
g_return_if_fail (SOUP_IS_SERVER (server));
g_return_if_fail (callback != NULL);
- add_handler_internal (server, path, FALSE,
- callback, user_data, destroy);
+ handler = get_or_create_handler (server, path);
+ if (handler->destroy)
+ handler->destroy (handler->user_data);
+
+ handler->callback = callback;
+ handler->destroy = destroy;
+ handler->user_data = user_data;
}
/**
@@ -2536,11 +2597,94 @@ soup_server_add_early_handler (SoupServer *server,
gpointer user_data,
GDestroyNotify destroy)
{
+ SoupServerHandler *handler;
+
+ g_return_if_fail (SOUP_IS_SERVER (server));
+ g_return_if_fail (callback != NULL);
+
+ handler = get_or_create_handler (server, path);
+ if (handler->early_destroy)
+ handler->early_destroy (handler->early_user_data);
+
+ handler->early_callback = callback;
+ handler->early_destroy = destroy;
+ handler->early_user_data = user_data;
+}
+
+/**
+ * SoupServerWebsocketCallback:
+ * @server: the #SoupServer
+ * @path: the path component of @msg's Request-URI
+ * @connection: the newly created WebSocket connection
+ * @client: additional contextual information about the client
+ * @user_data: the data passed to @soup_server_add_handler
+ *
+ * A callback used to handle WebSocket requests to a #SoupServer. The
+ * callback will be invoked after sending the handshake response back
+ * to the client (and is only invoked if the handshake was
+ * successful).
+ *
+ * @path contains the path of the Request-URI, subject to the same
+ * rules as #SoupServerCallback (qv).
+ **/
+
+/**
+ * soup_server_add_websocket_handler:
+ * @server: a #SoupServer
+ * @path: (allow-none): the toplevel path for the handler
+ * @origin: (allow-none): the origin of the connection
+ * @protocols: (allow-none) (array zero-terminated=1): the protocols
+ * supported by this handler
+ * @callback: callback to invoke for successful WebSocket requests under @path
+ * @user_data: data for @callback
+ * @destroy: destroy notifier to free @user_data
+ *
+ * Adds a WebSocket handler to @server for requests under @path. (If
+ * @path is %NULL or "/", then this will be the default handler for
+ * all requests that don't have a more specific handler.)
+ *
+ * When a path has a WebSocket handler registered, @server will check
+ * incoming requests for WebSocket handshakes after all other handlers
+ * have run (unless some earlier handler has already set a status code
+ * on the message), and update the request's status, response headers,
+ * and response body accordingly.
+ *
+ * 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. More complicated requirements can be
+ * handled by adding a normal handler to @path, and having it perform
+ * whatever checks are needed (possibly calling
+ * soup_server_check_websocket_handshake() one or more times), and
+ * setting a failure status code if the handshake should be rejected.
+ **/
+void
+soup_server_add_websocket_handler (SoupServer *server,
+ const char *path,
+ const char *origin,
+ char **protocols,
+ SoupServerWebsocketCallback callback,
+ gpointer user_data,
+ GDestroyNotify destroy)
+{
+ SoupServerHandler *handler;
+
g_return_if_fail (SOUP_IS_SERVER (server));
g_return_if_fail (callback != NULL);
- add_handler_internal (server, path, TRUE,
- callback, user_data, destroy);
+ handler = get_or_create_handler (server, path);
+ if (handler->websocket_destroy)
+ handler->websocket_destroy (handler->websocket_user_data);
+ if (handler->websocket_origin)
+ g_free (handler->websocket_origin);
+ if (handler->websocket_protocols)
+ g_strfreev (handler->websocket_protocols);
+
+ 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);
}
/**
diff --git a/libsoup/soup-server.h b/libsoup/soup-server.h
index 3cabc037..36a94907 100644
--- a/libsoup/soup-server.h
+++ b/libsoup/soup-server.h
@@ -8,6 +8,7 @@
#include <libsoup/soup-types.h>
#include <libsoup/soup-uri.h>
+#include <libsoup/soup-websocket-connection.h>
G_BEGIN_DECLS
@@ -131,6 +132,20 @@ void soup_server_add_early_handler (SoupServer *server,
gpointer user_data,
GDestroyNotify destroy);
+typedef void (*SoupServerWebsocketCallback) (SoupServer *server,
+ SoupWebsocketConnection *connection,
+ const char *path,
+ SoupClientContext *client,
+ gpointer user_data);
+SOUP_AVAILABLE_IN_2_50
+void soup_server_add_websocket_handler (SoupServer *server,
+ const char *path,
+ const char *origin,
+ char **protocols,
+ SoupServerWebsocketCallback callback,
+ gpointer user_data,
+ GDestroyNotify destroy);
+
void soup_server_remove_handler (SoupServer *server,
const char *path);
diff --git a/libsoup/soup-session.c b/libsoup/soup-session.c
index a9b14976..bc08c9c0 100644
--- a/libsoup/soup-session.c
+++ b/libsoup/soup-session.c
@@ -22,6 +22,8 @@
#include "soup-proxy-resolver-wrapper.h"
#include "soup-session-private.h"
#include "soup-socket-private.h"
+#include "soup-websocket.h"
+#include "soup-websocket-connection.h"
#define HOST_KEEP_ALIVE 5 * 60 * 1000 /* 5 min in msecs */
@@ -4724,3 +4726,132 @@ soup_session_steal_connection (SoupSession *session,
soup_message_queue_item_unref (item);
return stream;
}
+
+static void websocket_connect_async_stop (SoupMessage *msg, gpointer user_data);
+
+static void
+websocket_connect_async_complete (SoupSession *session, SoupMessage *msg, gpointer user_data)
+{
+ GTask *task = user_data;
+
+ g_signal_handlers_disconnect_by_func (msg, G_CALLBACK (websocket_connect_async_stop), task);
+
+ g_task_return_new_error (task,
+ SOUP_WEBSOCKET_ERROR, SOUP_WEBSOCKET_ERROR_NOT_WEBSOCKET,
+ "%s", _("The server did not accept the WebSocket handshake."));
+ g_object_unref (task);
+}
+
+static void
+websocket_connect_async_stop (SoupMessage *msg, gpointer user_data)
+{
+ GTask *task = user_data;
+ SoupMessageQueueItem *item = g_task_get_task_data (task);
+ GIOStream *stream;
+ SoupWebsocketConnection *client;
+ GError *error = NULL;
+
+ g_signal_handlers_disconnect_by_func (msg, G_CALLBACK (websocket_connect_async_stop), task);
+
+ if (soup_websocket_client_verify_handshake (item->msg, &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"));
+ g_object_unref (stream);
+
+ g_task_return_pointer (task, client, g_object_unref);
+ } else
+ g_task_return_error (task, error);
+ g_object_unref (task);
+}
+
+/**
+ * soup_session_websocket_connect_async:
+ * @session: a #SoupSession
+ * @msg: #SoupMessage indicating the WebSocket server to connect to
+ * @origin: (allow-none): origin of the connection
+ * @protocols: (allow-none) (array zero-terminated=1): a
+ * %NULL-terminated array of protocols supported
+ * @cancellable: a #GCancellable
+ * @callback: the callback to invoke
+ * @user_data: data for @callback
+ *
+ * Asynchronously creates a #SoupWebsocketConnection to communicate
+ * with a remote server.
+ *
+ * All necessary WebSocket-related headers will be added to @msg, and
+ * it will then be sent and asynchronously processed normally
+ * (including handling of redirection and HTTP authentication).
+ *
+ * If the server returns "101 Switching Protocols", then @msg's status
+ * code and response headers will be updated, and then the WebSocket
+ * handshake will be completed. On success,
+ * soup_websocket_connect_finish() will return a new
+ * #SoupWebsocketConnection. On failure it will return a #GError.
+ *
+ * If the server returns a status other than "101 Switching
+ * Protocols", then @msg will contain the complete response headers
+ * and body from the server's response, and
+ * soup_websocket_connect_finish() will return
+ * %SOUP_WEBSOCKET_ERROR_NOT_WEBSOCKET.
+ *
+ * Since: 2.50
+ */
+void
+soup_session_websocket_connect_async (SoupSession *session,
+ SoupMessage *msg,
+ const char *origin,
+ char **protocols,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ SoupMessageQueueItem *item;
+ GTask *task;
+
+ g_return_if_fail (SOUP_IS_SESSION (session));
+ g_return_if_fail (SOUP_SESSION_GET_PRIVATE (session)->use_thread_context);
+ g_return_if_fail (SOUP_IS_MESSAGE (msg));
+
+ soup_websocket_client_prepare_handshake (msg, origin, protocols);
+
+ task = g_task_new (session, cancellable, callback, user_data);
+ item = soup_session_append_queue_item (session, msg, TRUE, FALSE,
+ websocket_connect_async_complete, task);
+ g_task_set_task_data (task, item, (GDestroyNotify) soup_message_queue_item_unref);
+
+ soup_message_add_status_code_handler (msg, "got-informational",
+ SOUP_STATUS_SWITCHING_PROTOCOLS,
+ G_CALLBACK (websocket_connect_async_stop), task);
+ soup_session_kick_queue (session);
+}
+
+/**
+ * soup_session_websocket_connect_finish:
+ * @session: a #SoupSession
+ * @result: the #GAsyncResult passed to your callback
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets the #SoupWebsocketConnection response to a
+ * soup_session_websocket_connect_async() call and (if successful),
+ * returns a #SoupWebsockConnection that can be used to communicate
+ * with the server.
+ *
+ * Return value: (transfer full): a new #SoupWebsocketConnection, or
+ * %NULL on error.
+ *
+ * Since: 2.50
+ */
+SoupWebsocketConnection *
+soup_session_websocket_connect_finish (SoupSession *session,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (SOUP_IS_SESSION (session), NULL);
+ g_return_val_if_fail (g_task_is_valid (result, session), NULL);
+
+ return g_task_propagate_pointer (G_TASK (result), error);
+}
diff --git a/libsoup/soup-session.h b/libsoup/soup-session.h
index 5dcd747c..4c64ce43 100644
--- a/libsoup/soup-session.h
+++ b/libsoup/soup-session.h
@@ -9,6 +9,7 @@
#include <libsoup/soup-types.h>
#include <libsoup/soup-address.h>
#include <libsoup/soup-message.h>
+#include <libsoup/soup-websocket-connection.h>
G_BEGIN_DECLS
@@ -210,6 +211,20 @@ SOUP_AVAILABLE_IN_2_50
GIOStream *soup_session_steal_connection (SoupSession *session,
SoupMessage *msg);
+SOUP_AVAILABLE_IN_2_50
+void soup_session_websocket_connect_async (SoupSession *session,
+ SoupMessage *msg,
+ const char *origin,
+ char **protocols,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+SOUP_AVAILABLE_IN_2_50
+SoupWebsocketConnection *soup_session_websocket_connect_finish (SoupSession *session,
+ GAsyncResult *result,
+ GError **error);
+
G_END_DECLS
#endif /* SOUP_SESSION_H */
diff --git a/libsoup/soup-websocket.c b/libsoup/soup-websocket.c
index e04030f4..8fe8562c 100644
--- a/libsoup/soup-websocket.c
+++ b/libsoup/soup-websocket.c
@@ -31,21 +31,27 @@
/**
* SECTION:soup-websocket
* @short_description: The WebSocket Protocol
+ * @see_also: soup_session_websocket_connect_async(),
+ * soup_server_add_websocket_handler()
*
* #SoupWebsocketConnection provides support for the <ulink
* url="http://tools.ietf.org/html/rfc6455">WebSocket</ulink> protocol.
*
+ * To connect to a WebSocket server, create a #SoupSession and call
+ * soup_session_websocket_connect_async(). To accept WebSocket
+ * connections, create a #SoupServer and add a handler to it with
+ * soup_server_add_websocket_handler().
+ *
+ * (Lower-level support is available via
* soup_websocket_client_prepare_handshake() and
- * soup_websocket_client_verify_handshake() are low-level functions
- * for handling the client side of the WebSocket handshake.
- * soup_websocket_server_process_handshake() is the low-level function
- * for handling the server side.
- *
- * After completing a handshake, you can wrap the underlying
- * #GIOStream in a #SoupWebsocketConnection, which handles the details
- * of WebSocket communication. You can then use
- * soup_websocket_connection_send_text() and
- * soup_websocket_connection_send_binary() to send data, and the
+ * soup_websocket_client_verify_handshake(), for handling the client
+ * side of the WebSocket handshake, and
+ * soup_websocket_server_process_handshake() for handling the server
+ * side.)
+ *
+ * #SoupWebsocketConnection handles the details of WebSocket
+ * communication. You can use soup_websocket_connection_send_text()
+ * and soup_websocket_connection_send_binary() to send data, and the
* #SoupWebsocketConnection::message signal to receive data.
* (#SoupWebsocketConnection currently only supports asynchronous
* I/O.)
@@ -246,6 +252,10 @@ choose_subprotocol (SoupMessage *msg,
* handshake. 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.50
*/
void
@@ -298,6 +308,13 @@ soup_websocket_client_prepare_handshake (SoupMessage *msg,
* only requests containing a compatible "Sec-WebSocket-Protocols"
* header will be accepted.
*
+ * Normally soup_websocket_server_process_handshake() will take care
+ * of this for you, and if you use soup_server_add_websocket_handler()
+ * to handle accepting WebSocket connections, it will call that for
+ * you. However, this function may be useful if you need to perform
+ * more complicated validation; eg, accepting multiple different Origins,
+ * or handling different protocols depending on the path.
+ *
* Returns: %TRUE if @msg contained a valid WebSocket handshake,
* %FALSE and an error if not.
*
@@ -412,6 +429,10 @@ respond_handshake_bad (SoupMessage *msg, const char *why)
* only requests containing a compatible "Sec-WebSocket-Protocols"
* header will be accepted.
*
+ * 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
* and an error if not.
@@ -465,6 +486,10 @@ 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).
*
+ * 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.
*
diff --git a/tests/websocket-test.c b/tests/websocket-test.c
index cc9bdcd2..aee8bfc6 100644
--- a/tests/websocket-test.c
+++ b/tests/websocket-test.c
@@ -26,7 +26,12 @@ typedef struct {
GSocket *listener;
gushort port;
+ SoupSession *session;
+ SoupMessage *msg;
SoupWebsocketConnection *client;
+ GError *client_error;
+
+ SoupServer *soup_server;
SoupWebsocketConnection *server;
gboolean no_server;
@@ -184,6 +189,96 @@ teardown_direct_connection (Test *test,
}
static void
+setup_soup_server (Test *test,
+ const char *origin,
+ const char **protocols,
+ SoupServerWebsocketCallback callback,
+ gpointer user_data)
+{
+ GError *error = NULL;
+
+ setup_listener (test);
+
+ test->soup_server = soup_test_server_new (SOUP_TEST_SERVER_IN_THREAD);
+ soup_server_listen_socket (test->soup_server, test->listener, 0, &error);
+ g_assert_no_error (error);
+
+ soup_server_add_websocket_handler (test->soup_server, "/unix",
+ origin, (char **) protocols,
+ callback, user_data, NULL);
+}
+
+static void
+client_connect (Test *test,
+ const char *origin,
+ const char **protocols,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ char *url;
+
+ if (!test->session)
+ test->session = soup_test_session_new (SOUP_TYPE_SESSION, NULL);
+
+ url = g_strdup_printf ("ws://127.0.0.1:%u/unix", test->port);
+ test->msg = soup_message_new ("GET", url);
+ g_free (url);
+
+ soup_session_websocket_connect_async (test->session, test->msg,
+ origin, (char **) protocols,
+ NULL, callback, user_data);
+}
+
+static void
+got_server_connection (SoupServer *server,
+ SoupWebsocketConnection *connection,
+ const char *path,
+ SoupClientContext *client,
+ gpointer user_data)
+{
+ Test *test = user_data;
+
+ test->server = g_object_ref (connection);
+}
+
+static void
+got_client_connection (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ Test *test = user_data;
+
+ test->client = soup_session_websocket_connect_finish (SOUP_SESSION (object),
+ result, &test->client_error);
+}
+
+static void
+setup_soup_connection (Test *test,
+ gconstpointer data)
+{
+ 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);
+}
+
+static void
+teardown_soup_connection (Test *test,
+ gconstpointer data)
+{
+ g_clear_object (&test->client);
+ g_clear_error (&test->client_error);
+
+ if (test->session)
+ soup_test_session_abort_unref (test->session);
+
+ if (test->soup_server)
+ soup_test_server_quit_unref (test->soup_server);
+}
+
+
+static void
on_text_message (SoupWebsocketConnection *ws,
SoupWebsocketDataType type,
GBytes *message,
@@ -210,6 +305,14 @@ on_close_set_flag (SoupWebsocketConnection *ws,
}
+static void
+test_handshake (Test *test,
+ gconstpointer data)
+{
+ g_assert_cmpint (soup_websocket_connection_get_state (test->client), ==, SOUP_WEBSOCKET_STATE_OPEN);
+ g_assert_cmpint (soup_websocket_connection_get_state (test->server), ==, SOUP_WEBSOCKET_STATE_OPEN);
+}
+
#define TEST_STRING "this is a test"
static void
@@ -348,6 +451,20 @@ test_protocol_negotiate_direct (Test *test,
g_object_unref (msg);
}
+static void
+test_protocol_negotiate_soup (Test *test,
+ gconstpointer unused)
+{
+ setup_soup_server (test, NULL, negotiate_server_protocols, got_server_connection, test);
+ client_connect (test, NULL, negotiate_client_protocols, 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_websocket_connection_get_protocol (test->client), ==, negotiated_protocol);
+ g_assert_cmpstr (soup_websocket_connection_get_protocol (test->server), ==, negotiated_protocol);
+}
+
static const char *mismatch_client_protocols[] = { "ddd", NULL };
static const char *mismatch_server_protocols[] = { "aaa", "bbb", "ccc", NULL };
@@ -387,6 +504,17 @@ test_protocol_mismatch_direct (Test *test,
g_object_unref (msg);
}
+static void
+test_protocol_mismatch_soup (Test *test,
+ gconstpointer unused)
+{
+ setup_soup_server (test, NULL, mismatch_server_protocols, got_server_connection, test);
+ client_connect (test, NULL, mismatch_client_protocols, got_client_connection, test);
+ WAIT_UNTIL (test->client_error != NULL);
+
+ g_assert_error (test->client_error, SOUP_WEBSOCKET_ERROR, SOUP_WEBSOCKET_ERROR_NOT_WEBSOCKET);
+}
+
static const char *all_protocols[] = { "aaa", "bbb", "ccc", NULL };
static void
@@ -419,6 +547,21 @@ test_protocol_server_any_direct (Test *test,
}
static void
+test_protocol_server_any_soup (Test *test,
+ gconstpointer unused)
+{
+ setup_soup_server (test, NULL, NULL, got_server_connection, test);
+ client_connect (test, NULL, all_protocols, 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_websocket_connection_get_protocol (test->client), ==, NULL);
+ g_assert_cmpstr (soup_websocket_connection_get_protocol (test->server), ==, NULL);
+ g_assert_cmpstr (soup_message_headers_get_one (test->msg->response_headers, "Sec-WebSocket-Protocol"), ==, NULL);
+}
+
+static void
test_protocol_client_any_direct (Test *test,
gconstpointer unused)
{
@@ -448,6 +591,21 @@ test_protocol_client_any_direct (Test *test,
}
static void
+test_protocol_client_any_soup (Test *test,
+ gconstpointer unused)
+{
+ setup_soup_server (test, NULL, all_protocols, 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_websocket_connection_get_protocol (test->client), ==, NULL);
+ g_assert_cmpstr (soup_websocket_connection_get_protocol (test->server), ==, NULL);
+ g_assert_cmpstr (soup_message_headers_get_one (test->msg->response_headers, "Sec-WebSocket-Protocol"), ==, NULL);
+}
+
+static void
test_close_clean_client (Test *test,
gconstpointer data)
{
@@ -631,47 +789,103 @@ main (int argc,
test_init (argc, argv, NULL);
+ g_test_add ("/websocket/soup/handshake", Test, NULL,
+ setup_soup_connection,
+ test_handshake,
+ teardown_soup_connection);
+
g_test_add ("/websocket/direct/send-client-to-server", Test, NULL,
setup_direct_connection,
test_send_client_to_server,
teardown_direct_connection);
+ g_test_add ("/websocket/soup/send-client-to-server", Test, NULL,
+ setup_soup_connection,
+ test_send_client_to_server,
+ teardown_soup_connection);
+
g_test_add ("/websocket/direct/send-server-to-client", Test, NULL,
setup_direct_connection,
test_send_server_to_client,
teardown_direct_connection);
+ g_test_add ("/websocket/soup/send-server-to-client", Test, NULL,
+ setup_soup_connection,
+ test_send_server_to_client,
+ teardown_soup_connection);
+
g_test_add ("/websocket/direct/send-big-packets", Test, NULL,
setup_direct_connection,
test_send_big_packets,
teardown_direct_connection);
+ g_test_add ("/websocket/soup/send-big-packets", Test, NULL,
+ setup_soup_connection,
+ test_send_big_packets,
+ teardown_soup_connection);
+
g_test_add ("/websocket/direct/send-bad-data", Test, NULL,
setup_direct_connection,
test_send_bad_data,
teardown_direct_connection);
+ g_test_add ("/websocket/soup/send-bad-data", Test, NULL,
+ setup_soup_connection,
+ test_send_bad_data,
+ teardown_soup_connection);
+
g_test_add ("/websocket/direct/close-clean-client", Test, NULL,
setup_direct_connection,
test_close_clean_client,
teardown_direct_connection);
+ g_test_add ("/websocket/soup/close-clean-client", Test, NULL,
+ setup_soup_connection,
+ test_close_clean_client,
+ teardown_soup_connection);
+
g_test_add ("/websocket/direct/close-clean-server", Test, NULL,
setup_direct_connection,
test_close_clean_server,
teardown_direct_connection);
+ g_test_add ("/websocket/soup/close-clean-server", Test, NULL,
+ setup_soup_connection,
+ test_close_clean_server,
+ teardown_soup_connection);
+
g_test_add ("/websocket/direct/message-after-closing", Test, NULL,
setup_direct_connection,
test_message_after_closing,
teardown_direct_connection);
+ g_test_add ("/websocket/soup/message-after-closing", Test, NULL,
+ setup_soup_connection,
+ test_message_after_closing,
+ teardown_soup_connection);
+
g_test_add ("/websocket/direct/protocol-negotiate", Test, NULL, NULL,
test_protocol_negotiate_direct,
NULL);
+ g_test_add ("/websocket/soup/protocol-negotiate", Test, NULL, NULL,
+ test_protocol_negotiate_soup,
+ teardown_soup_connection);
+
g_test_add ("/websocket/direct/protocol-mismatch", Test, NULL, NULL,
test_protocol_mismatch_direct,
NULL);
+ g_test_add ("/websocket/soup/protocol-mismatch", Test, NULL, NULL,
+ test_protocol_mismatch_soup,
+ teardown_soup_connection);
+
g_test_add ("/websocket/direct/protocol-server-any", Test, NULL, NULL,
test_protocol_server_any_direct,
NULL);
+ g_test_add ("/websocket/soup/protocol-server-any", Test, NULL, NULL,
+ test_protocol_server_any_soup,
+ teardown_soup_connection);
+
g_test_add ("/websocket/direct/protocol-client-any", Test, NULL, NULL,
test_protocol_client_any_direct,
NULL);
+ g_test_add ("/websocket/soup/protocol-client-any", Test, NULL, NULL,
+ test_protocol_client_any_soup,
+ teardown_soup_connection);
+
g_test_add ("/websocket/direct/receive-fragmented", Test, NULL,
setup_half_direct_connection,