[libsoup] websockets: add WebSocket support to SoupSession and SoupServer



commit bad410554310c94ce2a758b9e8d3ca2c0e39903b
Author: Dan Winship <danw gnome org>
Date:   Sun Nov 30 10:26:23 2014 -0500

    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

 docs/reference/libsoup-2.4-sections.txt |    6 +
 libsoup/libsoup-2.4.sym                 |    3 +
 libsoup/soup-server.c                   |  266 ++++++++++++++++++++++++-------
 libsoup/soup-server.h                   |   15 ++
 libsoup/soup-session.c                  |  131 +++++++++++++++
 libsoup/soup-session.h                  |   15 ++
 libsoup/soup-websocket.c                |   45 ++++--
 tests/websocket-test.c                  |  214 +++++++++++++++++++++++++
 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 293c12f..92a3563 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 4f52b84..69d46ef 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 2807439..84d5bc0 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 3cabc03..36a9490 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 a9b1497..bc08c9c 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 5dcd747..4c64ce4 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 e04030f..8fe8562 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 cc9bdcd..aee8bfc 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,



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]