[libsoup/websocket: 9/10] websockets: add WebSockets support to SoupSession and SoupServer



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

    websockets: add WebSockets support to SoupSession and SoupServer
    
    Add support to SoupSession for connecting to WebSocket servers, and
    likewise add support to SoupServer for accepting WebSocket
    connections.
    
    Based on code originally from the Cockpit project, and on earlier work
    by Lionel Landwerlin to merge that into libsoup.

 libsoup/Makefile.am                 |    9 +
 libsoup/libsoup-2.4.sym             |   20 +
 libsoup/soup-server.c               |  225 ++++++-
 libsoup/soup-server.h               |   19 +-
 libsoup/soup-session.c              |  126 +++
 libsoup/soup-session.h              |   15 +
 libsoup/soup-types.h                |   34 +-
 libsoup/soup-websocket-client.c     |  155 ++++
 libsoup/soup-websocket-client.h     |   46 ++
 libsoup/soup-websocket-connection.c | 1493 +++++++++++++++++++++++++++++++++++
 libsoup/soup-websocket-connection.h |  101 +++
 libsoup/soup-websocket-private.h    |   32 +
 libsoup/soup-websocket-server.c     |  182 +++++
 libsoup/soup-websocket-server.h     |   48 ++
 libsoup/soup-websocket.c            |   87 ++
 libsoup/soup-websocket.h            |   66 ++
 libsoup/soup.h                      |    2 +
 po/POTFILES.in                      |    1 +
 tests/Makefile.am                   |    1 +
 tests/header-parsing.c              |   63 ++
 tests/websocket-test.c              |  709 +++++++++++++++++
 21 files changed, 3415 insertions(+), 19 deletions(-)
---
diff --git a/libsoup/Makefile.am b/libsoup/Makefile.am
index 431c99e..3513be4 100644
--- a/libsoup/Makefile.am
+++ b/libsoup/Makefile.am
@@ -67,6 +67,8 @@ soup_headers =                        \
        soup-types.h            \
        soup-uri.h              \
        soup-value-utils.h      \
+       soup-websocket.h        \
+       soup-websocket-connection.h     \
        soup-xmlrpc.h
 
 libsoupinclude_HEADERS =       \
@@ -184,6 +186,13 @@ libsoup_2_4_la_SOURCES =           \
        soup-uri.c                      \
        soup-value-utils.c              \
        soup-version.c                  \
+       soup-websocket.c                \
+       soup-websocket-client.h         \
+       soup-websocket-client.c         \
+       soup-websocket-connection.c     \
+       soup-websocket-private.h        \
+       soup-websocket-server.h         \
+       soup-websocket-server.c         \
        soup-xmlrpc.c
 
 # TLD rules
diff --git a/libsoup/libsoup-2.4.sym b/libsoup/libsoup-2.4.sym
index 2ee33fb..5f47bd4 100644
--- a/libsoup/libsoup-2.4.sym
+++ b/libsoup/libsoup-2.4.sym
@@ -371,6 +371,7 @@ soup_requester_request
 soup_requester_request_uri
 soup_server_add_auth_domain
 soup_server_add_handler
+soup_server_add_websocket_handler
 soup_server_disconnect
 soup_server_get_async_context
 soup_server_get_listener
@@ -390,6 +391,7 @@ soup_server_pause_message
 soup_server_quit
 soup_server_remove_auth_domain
 soup_server_remove_handler
+soup_server_remove_websocket_handler
 soup_server_run
 soup_server_run_async
 soup_server_set_ssl_cert_file
@@ -436,6 +438,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
@@ -511,6 +515,22 @@ soup_value_hash_lookup
 soup_value_hash_lookup_vals
 soup_value_hash_new
 soup_value_hash_new_with_vals
+soup_websocket_close_code_get_type
+soup_websocket_connection_close
+soup_websocket_connection_get_close_code
+soup_websocket_connection_get_close_data
+soup_websocket_connection_get_io_stream
+soup_websocket_connection_get_message
+soup_websocket_connection_get_origin
+soup_websocket_connection_get_protocol
+soup_websocket_connection_get_state
+soup_websocket_connection_get_type
+soup_websocket_connection_get_uri
+soup_websocket_connection_send
+soup_websocket_data_type_get_type
+soup_websocket_error_get_quark
+soup_websocket_error_get_type
+soup_websocket_state_get_type
 soup_xmlrpc_build_fault
 soup_xmlrpc_build_method_call
 soup_xmlrpc_build_method_response
diff --git a/libsoup/soup-server.c b/libsoup/soup-server.c
index ce481d5..488bd4e 100644
--- a/libsoup/soup-server.c
+++ b/libsoup/soup-server.c
@@ -19,6 +19,7 @@
 #include "soup-misc-private.h"
 #include "soup-path-map.h" 
 #include "soup-socket-private.h"
+#include "soup-websocket-server.h"
 
 /**
  * SECTION:soup-server
@@ -110,6 +111,16 @@ typedef struct {
 } SoupServerHandler;
 
 typedef struct {
+       char     *path;
+       char     *origin;
+       char    **protocols;
+
+       SoupServerWebsocketCallback   callback;
+       GDestroyNotify                destroy;
+       gpointer                      user_data;
+} SoupServerWebsocketHandler;
+
+typedef struct {
        GSList            *listeners;
        GSList            *clients;
 
@@ -124,7 +135,10 @@ typedef struct {
        gboolean           raw_paths;
        SoupPathMap       *handlers;
        SoupServerHandler *default_handler;
-       
+
+       SoupPathMap                     *websocket_handlers;
+       SoupServerWebsocketHandler      *default_websocket_handler;
+
        GSList            *auth_domains;
 
        char             **http_aliases, **https_aliases;
@@ -169,11 +183,21 @@ free_handler (SoupServerHandler *hand)
 }
 
 static void
+free_websocket_handler (SoupServerWebsocketHandler *hand)
+{
+       g_free (hand->path);
+       g_free (hand->origin);
+       g_strfreev (hand->protocols);
+       g_slice_free (SoupServerWebsocketHandler, hand);
+}
+
+static void
 soup_server_init (SoupServer *server)
 {
        SoupServerPrivate *priv = SOUP_SERVER_GET_PRIVATE (server);
 
        priv->handlers = soup_path_map_new ((GDestroyNotify)free_handler);
+       priv->websocket_handlers = soup_path_map_new ((GDestroyNotify)free_websocket_handler);
 
        priv->http_aliases = g_new (char *, 2);
        priv->http_aliases[0] = (char *)g_intern_string ("*");
@@ -235,6 +259,9 @@ soup_server_finalize (GObject *object)
        g_clear_pointer (&priv->default_handler, free_handler);
        soup_path_map_free (priv->handlers);
 
+       g_clear_pointer (&priv->default_websocket_handler, free_websocket_handler);
+       soup_path_map_free (priv->websocket_handlers);
+
        g_slist_free_full (priv->auth_domains, g_object_unref);
 
        g_clear_pointer (&priv->loop, g_main_loop_unref);
@@ -1178,6 +1205,73 @@ soup_server_get_handler (SoupServer *server, const char *path)
        return priv->default_handler;
 }
 
+static SoupServerWebsocketHandler *
+soup_server_get_websocket_handler (SoupServer *server, const char *path)
+{
+       SoupServerPrivate *priv;
+       SoupServerWebsocketHandler *hand;
+
+       g_return_val_if_fail (SOUP_IS_SERVER (server), NULL);
+       priv = SOUP_SERVER_GET_PRIVATE (server);
+
+       if (path) {
+               hand = soup_path_map_lookup (priv->websocket_handlers, path);
+               if (hand)
+                       return hand;
+               if (!strcmp (path, "*"))
+                       return NULL;
+       }
+       return priv->default_websocket_handler;
+}
+
+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);
+       SoupServerWebsocketHandler *hand;
+       GIOStream *stream;
+       SoupWebsocketConnection *conn;
+
+       hand = soup_server_get_websocket_handler (server, uri->path);
+       if (!hand)
+               return;
+
+       stream = soup_client_context_steal_connection (client);
+       conn = g_object_new (SOUP_TYPE_WEBSOCKET_SERVER,
+                            "message", msg,
+                            "io-stream", stream,
+                            NULL);
+       g_object_unref (stream);
+       soup_client_context_unref (client);
+
+       (*hand->callback) (server, conn, uri->path, client, hand->user_data);
+       g_object_unref (conn);
+}
+
+static void
+call_websocket_handler (SoupMessage *msg, SoupClientContext *client)
+{
+       SoupServer *server = client->server;
+       SoupURI *uri = soup_message_get_uri (msg);
+       SoupServerWebsocketHandler *hand;
+
+       hand = soup_server_get_websocket_handler (server, uri->path);
+       if (!hand) {
+               // FIXME
+               soup_message_set_status (msg, SOUP_STATUS_NOT_FOUND);
+               return;
+       }
+
+       if (soup_websocket_server_process_handshake (msg, hand->origin,
+                                                    hand->protocols)) {
+               g_signal_connect (msg, "wrote-informational",
+                                 G_CALLBACK (complete_websocket_upgrade),
+                                 soup_client_context_ref (client));
+       }
+}
+
 static void
 got_headers (SoupMessage *msg, SoupClientContext *client)
 {
@@ -1191,6 +1285,12 @@ got_headers (SoupMessage *msg, SoupClientContext *client)
        gboolean rejected = FALSE;
        char *auth_user;
 
+       if (soup_message_headers_header_contains (msg->request_headers, "Connection", "upgrade") &&
+           soup_message_headers_header_equals (msg->request_headers, "Upgrade", "websocket")) {
+               call_websocket_handler (msg, client);
+               return;
+       }
+
        uri = soup_message_get_uri (msg);
        if ((soup_socket_is_ssl (client->sock) && !soup_uri_is_https (uri, priv->https_aliases)) ||
            (!soup_socket_is_ssl (client->sock) && !soup_uri_is_http (uri, priv->http_aliases))) {
@@ -1222,7 +1322,7 @@ got_headers (SoupMessage *msg, SoupClientContext *client)
                                      date_string);
        g_free (date_string);
        soup_date_free (date);
-       
+
        /* Now handle authentication. (We do this here so that if
         * the request uses "Expect: 100-continue", we can reject it
         * immediately rather than waiting for the request body to
@@ -2383,6 +2483,127 @@ soup_server_remove_handler (SoupServer *server, const char *path)
 }
 
 /**
+ * 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 receiving the request headers.
+ *
+ * @path contains the path of the Request-URI, subject to certain
+ * assumptions. By default, #SoupServer decodes all percent-encoding
+ * in the URI path, such that "/foo%<!-- -->2Fbar" is treated the same
+ * as "/foo/bar". If your server is serving resources in some
+ * non-POSIX-filesystem namespace, you may want to distinguish those
+ * as two distinct paths. In that case, you can set the
+ * %SOUP_SERVER_RAW_PATHS property when creating the #SoupServer, and
+ * it will leave those characters undecoded. (You may want to call
+ * soup_uri_normalize() to decode any percent-encoded characters that
+ * you aren't handling specially.)
+ **/
+
+/**
+ * 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): the protocols supported by this handler
+ * @callback: callback to invoke for 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. See
+ * the documentation for #SoupServerWebsocketCallback for information
+ * about how callbacks should behave.
+ *
+ * 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 @path is %NULL or "/", then this will be the default handler for
+ * all requests that don't have a more specific handler. Note though
+ * that if you want to handle requests to the special "*" URI, you
+ * must explicitly register a handler for "*"; the default handler
+ * will not be used for that case.
+ **/
+void
+soup_server_add_websocket_handler (SoupServer                  *server,
+                                  const char                   *path,
+                                  const char                   *origin,
+                                  char                        **protocols,
+                                  SoupServerWebsocketCallback   callback,
+                                  gpointer                      user_data,
+                                  GDestroyNotify                destroy)
+{
+       SoupServerPrivate *priv;
+       SoupServerWebsocketHandler *hand;
+
+       g_return_if_fail (SOUP_IS_SERVER (server));
+       g_return_if_fail (callback != NULL);
+       priv = SOUP_SERVER_GET_PRIVATE (server);
+
+       if (path && (!*path || !strcmp (path, "/")))
+               return;
+
+       hand = g_slice_new0 (SoupServerWebsocketHandler);
+       hand->path       = g_strdup (path);
+       hand->origin     = g_strdup (origin);
+       hand->protocols  = g_strdupv (protocols);
+       hand->callback   = callback;
+       hand->destroy    = destroy;
+       hand->user_data  = user_data;
+
+       soup_server_remove_websocket_handler (server, path);
+       if (path)
+               soup_path_map_add (priv->websocket_handlers, path, hand);
+       else
+               priv->default_websocket_handler = hand;
+}
+
+static void
+unregister_websocket_handler (SoupServerWebsocketHandler *handler)
+{
+       if (handler->destroy)
+               handler->destroy (handler->user_data);
+}
+
+/**
+ * soup_server_remove_websocket_handler:
+ * @server: a #SoupServer
+ * @path: the toplevel path for the handler
+ *
+ * Removes the WebSocket handler registered at @path.
+ **/
+void
+soup_server_remove_websocket_handler (SoupServer *server, const char *path)
+{
+       SoupServerPrivate *priv;
+       SoupServerWebsocketHandler *hand;
+
+       g_return_if_fail (SOUP_IS_SERVER (server));
+       priv = SOUP_SERVER_GET_PRIVATE (server);
+
+       if (!path || !*path || !strcmp (path, "/")) {
+               if (priv->default_websocket_handler) {
+                       unregister_websocket_handler (priv->default_websocket_handler);
+                       free_websocket_handler (priv->default_websocket_handler);
+                       priv->default_websocket_handler = NULL;
+               }
+               return;
+       }
+
+       hand = soup_path_map_lookup (priv->websocket_handlers, path);
+       if (hand && !strcmp (path, hand->path)) {
+               unregister_websocket_handler (hand);
+               soup_path_map_remove (priv->websocket_handlers, path);
+       }
+}
+
+/**
  * soup_server_add_auth_domain:
  * @server: a #SoupServer
  * @auth_domain: a #SoupAuthDomain
diff --git a/libsoup/soup-server.h b/libsoup/soup-server.h
index b7ac129..09dfd77 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
 
@@ -56,12 +57,18 @@ typedef struct {
 GType soup_server_get_type (void);
 
 typedef void (*SoupServerCallback) (SoupServer        *server,
-                                   SoupMessage       *msg, 
+                                   SoupMessage       *msg,
                                    const char        *path,
                                    GHashTable        *query,
                                    SoupClientContext *client,
                                    gpointer           user_data);
 
+typedef void (*SoupServerWebsocketCallback) (SoupServer              *server,
+                                            SoupWebsocketConnection *connection,
+                                            const char              *path,
+                                            SoupClientContext       *client,
+                                            gpointer                 user_data);
+
 #define SOUP_SERVER_TLS_CERTIFICATE "tls-certificate"
 #define SOUP_SERVER_RAW_PATHS       "raw-paths"
 #define SOUP_SERVER_SERVER_HEADER   "server-header"
@@ -126,6 +133,16 @@ void            soup_server_add_auth_domain    (SoupServer         *server,
 void            soup_server_remove_auth_domain (SoupServer         *server,
                                                SoupAuthDomain     *auth_domain);
 
+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_websocket_handler (SoupServer                   *server,
+                                                     const char                   *path);
+
 /* I/O */
 
 void            soup_server_pause_message   (SoupServer  *server,
diff --git a/libsoup/soup-session.c b/libsoup/soup-session.c
index 353ac59..1582959 100644
--- a/libsoup/soup-session.c
+++ b/libsoup/soup-session.c
@@ -22,6 +22,7 @@
 #include "soup-proxy-resolver-wrapper.h"
 #include "soup-session-private.h"
 #include "soup-socket-private.h"
+#include "soup-websocket-client.h"
 
 #define HOST_KEEP_ALIVE 5 * 60 * 1000 /* 5 min in msecs */
 
@@ -4717,3 +4718,128 @@ 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);
+
+       stream = soup_session_steal_connection (item->session, item->msg);
+       client = soup_websocket_client_new (msg, stream, &error);
+       g_object_unref (stream);
+
+       if (client)
+               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: (transfer none) (allow-none): 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..1fab65b 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-types.h b/libsoup/soup-types.h
index 0776bdb..e020de7 100644
--- a/libsoup/soup-types.h
+++ b/libsoup/soup-types.h
@@ -13,22 +13,24 @@
 
 G_BEGIN_DECLS
 
-typedef struct _SoupAddress           SoupAddress;
-typedef struct _SoupAuth              SoupAuth;
-typedef struct _SoupAuthDomain        SoupAuthDomain;
-typedef struct _SoupCookie            SoupCookie;
-typedef struct _SoupCookieJar         SoupCookieJar;
-typedef struct _SoupDate              SoupDate;
-typedef struct _SoupMessage           SoupMessage;
-typedef struct _SoupRequest           SoupRequest;
-typedef struct _SoupRequestHTTP       SoupRequestHTTP;
-typedef struct _SoupServer            SoupServer;
-typedef struct _SoupSession           SoupSession;
-typedef struct _SoupSessionAsync      SoupSessionAsync;
-typedef struct _SoupSessionFeature    SoupSessionFeature;
-typedef struct _SoupSessionSync       SoupSessionSync;
-typedef struct _SoupSocket            SoupSocket;
-typedef struct _SoupURI               SoupURI;
+typedef struct _SoupAddress             SoupAddress;
+typedef struct _SoupAuth                SoupAuth;
+typedef struct _SoupAuthDomain          SoupAuthDomain;
+typedef struct _SoupCookie              SoupCookie;
+typedef struct _SoupCookieJar           SoupCookieJar;
+typedef struct _SoupDate                SoupDate;
+typedef struct _SoupMessage             SoupMessage;
+typedef struct _SoupRequest             SoupRequest;
+typedef struct _SoupRequestHTTP         SoupRequestHTTP;
+typedef struct _SoupServer              SoupServer;
+typedef struct _SoupSession             SoupSession;
+typedef struct _SoupSessionAsync        SoupSessionAsync;
+typedef struct _SoupSessionFeature      SoupSessionFeature;
+typedef struct _SoupSessionSync         SoupSessionSync;
+typedef struct _SoupSocket              SoupSocket;
+typedef struct _SoupURI                 SoupURI;
+typedef struct _SoupWebsocketConnection SoupWebsocketConnection;
+
 
 /*< private >*/
 typedef struct _SoupConnection        SoupConnection;
diff --git a/libsoup/soup-websocket-client.c b/libsoup/soup-websocket-client.c
new file mode 100644
index 0000000..493d810
--- /dev/null
+++ b/libsoup/soup-websocket-client.c
@@ -0,0 +1,155 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * soup-websocket-client.c: This file was originally part of Cockpit.
+ *
+ * Copyright 2013, 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <glib/gi18n-lib.h>
+
+#include "soup-websocket-client.h"
+#include "soup-websocket-private.h"
+#include "soup-headers.h"
+#include "soup-message.h"
+
+typedef SoupWebsocketConnection SoupWebsocketClient;
+typedef SoupWebsocketConnectionClass SoupWebsocketClientClass;
+
+static void soup_websocket_client_initable_interface_init (GInitableIface *initable_interface);
+
+G_DEFINE_TYPE_WITH_CODE (SoupWebsocketClient, soup_websocket_client, SOUP_TYPE_WEBSOCKET_CONNECTION,
+                        G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE,
+                                               soup_websocket_client_initable_interface_init))
+
+static void
+soup_websocket_client_init (SoupWebsocketClient *self)
+{
+}
+
+static gboolean
+verify_handshake (SoupMessage *msg)
+{
+       const char *protocol, *request_protocols, *extensions, *accept_key;
+       char *expected_accept_key;
+
+       if (msg->status_code != SOUP_STATUS_SWITCHING_PROTOCOLS)
+               return FALSE;
+
+       if (!soup_message_headers_header_equals (msg->response_headers, "Upgrade", "websocket") ||
+           !soup_message_headers_header_contains (msg->response_headers, "Connection", "upgrade"))
+               return FALSE;
+
+       protocol = soup_message_headers_get_one (msg->response_headers, "Sec-WebSocket-Protocol");
+       request_protocols = soup_message_headers_get_one (msg->request_headers, "Sec-WebSocket-Protocol");
+       if (request_protocols && protocol &&
+           !soup_header_contains (request_protocols, protocol))
+               return FALSE;
+
+       extensions = soup_message_headers_get_list (msg->response_headers, "Sec-WebSocket-Extensions");
+       if (extensions && *extensions)
+               return FALSE;
+
+       expected_accept_key = soup_websocket_get_accept_key (soup_message_headers_get_one 
(msg->request_headers, "Sec-WebSocket-Key"));
+       accept_key = soup_message_headers_get_one (msg->response_headers, "Sec-WebSocket-Accept");
+       if (!accept_key || g_ascii_strcasecmp (accept_key, expected_accept_key)) {
+               g_free (expected_accept_key);
+               g_debug ("received invalid or missing Sec-WebSocket-Accept header: %s", accept_key);
+               return FALSE;
+       }
+
+       g_free (expected_accept_key);
+       return TRUE;
+}
+
+static gboolean
+soup_websocket_client_initable_init (GInitable     *initable,
+                                    GCancellable  *cancellable,
+                                    GError       **error)
+{
+       SoupMessage *msg;
+
+       msg = soup_websocket_connection_get_message (SOUP_WEBSOCKET_CONNECTION (initable));
+       g_return_val_if_fail (msg != NULL, FALSE);
+
+       if (!verify_handshake (msg)) {
+               g_set_error_literal (error,
+                                    SOUP_WEBSOCKET_ERROR, SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE,
+                                    _("Received invalid WebSocket handshake from server"));
+               return FALSE;
+       }
+
+       return TRUE;
+}
+
+static void
+soup_websocket_client_class_init (SoupWebsocketClientClass *klass)
+{
+}
+
+static void
+soup_websocket_client_initable_interface_init (GInitableIface *initable_interface)
+{
+       initable_interface->init = soup_websocket_client_initable_init;
+}
+
+SoupWebsocketConnection *
+soup_websocket_client_new (SoupMessage  *msg,
+                          GIOStream    *stream,
+                          GError      **error)
+{
+       return g_initable_new (SOUP_TYPE_WEBSOCKET_CLIENT, NULL, error,
+                              "message", msg,
+                              "io-stream", stream,
+                              NULL);
+}
+
+
+void
+soup_websocket_client_prepare_handshake (SoupMessage  *msg,
+                                        const char   *origin,
+                                        char        **protocols)
+{
+       guint32 raw[4];
+       char *key;
+
+       soup_message_headers_replace (msg->request_headers, "Upgrade", "websocket");
+       soup_message_headers_append (msg->request_headers, "Connection", "Upgrade");
+
+       raw[0] = g_random_int ();
+       raw[1] = g_random_int ();
+       raw[2] = g_random_int ();
+       raw[3] = g_random_int ();
+       key = g_base64_encode ((const guchar *)raw, sizeof (raw));
+       soup_message_headers_replace (msg->request_headers, "Sec-WebSocket-Key", key);
+       g_free (key);
+
+       soup_message_headers_replace (msg->request_headers, "Sec-WebSocket-Version", "13");
+
+       if (origin)
+               soup_message_headers_replace (msg->request_headers, "Origin", origin);
+
+       if (protocols) {
+               char *protocols_str;
+
+               protocols_str = g_strjoinv (", ", protocols);
+               soup_message_headers_replace (msg->request_headers,
+                                             "Sec-WebSocket-Protocol", protocols_str);
+               g_free (protocols_str);
+       }
+}
diff --git a/libsoup/soup-websocket-client.h b/libsoup/soup-websocket-client.h
new file mode 100644
index 0000000..49cec64
--- /dev/null
+++ b/libsoup/soup-websocket-client.h
@@ -0,0 +1,46 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * soup-websocket-client.h: This file was originally part of Cockpit.
+ *
+ * Copyright 2013, 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __SOUP_WEBSOCKET_CLIENT_H__
+#define __SOUP_WEBSOCKET_CLIENT_H__
+
+#include <libsoup/soup-websocket-connection.h>
+
+G_BEGIN_DECLS
+
+#define SOUP_TYPE_WEBSOCKET_CLIENT         (soup_websocket_client_get_type ())
+#define SOUP_WEBSOCKET_CLIENT(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), SOUP_TYPE_WEBSOCKET_CLIENT, 
SoupWebsocketClient))
+#define SOUP_IS_WEBSOCKET_CLIENT(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), SOUP_TYPE_WEBSOCKET_CLIENT))
+#define SOUP_WEBSOCKET_CLIENT_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), SOUP_TYPE_WEBSOCKET_CLIENT, 
SoupWebsocketClientClass))
+#define SOUP_IS_WEBSOCKET_CLIENT_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), SOUP_TYPE_WEBSOCKET_CLIENT))
+
+GType soup_websocket_client_get_type (void) G_GNUC_CONST;
+
+void soup_websocket_client_prepare_handshake (SoupMessage  *msg,
+                                             const char   *origin,
+                                             char        **protocols);
+
+SoupWebsocketConnection *soup_websocket_client_new (SoupMessage  *msg,
+                                                   GIOStream    *stream,
+                                                   GError      **error);
+
+G_END_DECLS
+
+#endif /* __SOUP_WEBSOCKET_CLIENT_H__ */
diff --git a/libsoup/soup-websocket-connection.c b/libsoup/soup-websocket-connection.c
new file mode 100644
index 0000000..c3f3117
--- /dev/null
+++ b/libsoup/soup-websocket-connection.c
@@ -0,0 +1,1493 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * soup-websocket-connection.c: This file was originally part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <string.h>
+
+#include "soup-websocket-connection.h"
+#include "soup-websocket-server.h"
+#include "soup-enum-types.h"
+#include "soup-message.h"
+#include "soup-uri.h"
+
+/*
+ * SECTION:websocketconnection
+ * @title: SoupWebsocketConnection
+ * @short_description: A WebSocket connection
+ *
+ * A #SoupWebsocketConnection is a WebSocket connection to a peer.
+ * This API is modeled after the W3C API for interacting with
+ * WebSockets.
+ *
+ * The #SoupWebsocketConnection:state property will indicate the
+ * state of the connection.
+ *
+ * Use soup_websocket_connection_send() to send a message to the peer.
+ * When a message is received the #SoupWebsocketConnection::message
+ * signal will fire.
+ *
+ * The soup_websocket_connection_close() function will perform an
+ * orderly close of the connection. The
+ * #SoupWebsocketConnection::close signal will fire once the
+ * connection closes, whether it was initiated by this side or the
+ * peer.
+ *
+ * Connect to the #SoupWebsocketConnection::closing signal to detect
+ * when either peer begins closing the connection.
+ */
+
+/**
+ * SoupWebsocketConnection:
+ *
+ * A class representing a WebSocket connection.
+ *
+ * Since: 2.50
+ */
+
+/**
+ * SoupWebsocketConnectionClass:
+ * @message: default handler for the #SoupWebsocketConnection::message signal
+ * @error: default handler for the #SoupWebsocketConnection::error signal
+ * @closing: the default handler for the #SoupWebsocketConnection:closing signal
+ * @close: default handler for the #SoupWebsocketConnection::close signal
+ *
+ * The abstract base class for #SoupWebsocketConnection
+ *
+ * Since: 2.50
+ */
+
+enum {
+       PROP_0,
+       PROP_MESSAGE,
+       PROP_URI,
+       PROP_PROTOCOL,
+       PROP_ORIGIN,
+       PROP_STATE,
+       PROP_IO_STREAM,
+};
+
+enum {
+       MESSAGE,
+       ERROR,
+       CLOSING,
+       CLOSE,
+       NUM_SIGNALS
+};
+
+static guint signals[NUM_SIGNALS] = { 0, };
+
+typedef struct {
+       GBytes *data;
+       gboolean last;
+       gsize sent;
+       gsize amount;
+} Frame;
+
+struct _SoupWebsocketConnectionPrivate {
+       SoupMessage *msg;
+
+       gushort peer_close_code;
+       char *peer_close_data;
+       gboolean close_sent;
+       gboolean close_received;
+       gboolean dirty_close;
+       GSource *close_timeout;
+
+       GMainContext *main_context;
+
+       GIOStream *io_stream;
+       gboolean io_closing;
+       gboolean io_closed;
+
+       GPollableInputStream *input;
+       GSource *input_source;
+       GByteArray *incoming;
+
+       GPollableOutputStream *output;
+       GSource *output_source;
+       GQueue outgoing;
+
+       /* Current message being assembled */
+       guint8 message_opcode;
+       GByteArray *message_data;
+};
+
+#define MAX_PAYLOAD   128 * 1024
+
+G_DEFINE_ABSTRACT_TYPE (SoupWebsocketConnection, soup_websocket_connection, G_TYPE_OBJECT);
+
+typedef enum {
+       SOUP_WEBSOCKET_QUEUE_NORMAL = 0,
+       SOUP_WEBSOCKET_QUEUE_URGENT = 1 << 0,
+       SOUP_WEBSOCKET_QUEUE_LAST = 1 << 1,
+} SoupWebsocketQueueFlags;
+
+static void queue_frame (SoupWebsocketConnection *self, SoupWebsocketQueueFlags flags,
+                        gpointer data, gsize len, gsize amount);
+
+static void
+frame_free (gpointer data)
+{
+       Frame *frame = data;
+
+       if (frame) {
+               g_bytes_unref (frame->data);
+               g_slice_free (Frame, frame);
+       }
+}
+
+static void
+soup_websocket_connection_init (SoupWebsocketConnection *self)
+{
+       SoupWebsocketConnectionPrivate *pv;
+
+       pv = self->pv = G_TYPE_INSTANCE_GET_PRIVATE (self, SOUP_TYPE_WEBSOCKET_CONNECTION,
+                                                    SoupWebsocketConnectionPrivate);
+
+       pv->incoming = g_byte_array_sized_new (1024);
+       g_queue_init (&pv->outgoing);
+       pv->main_context = g_main_context_ref_thread_default ();
+}
+
+static void
+on_iostream_closed (GObject *source,
+                    GAsyncResult *result,
+                    gpointer user_data)
+{
+       SoupWebsocketConnection *self = user_data;
+       SoupWebsocketConnectionPrivate *pv = self->pv;
+       GError *error = NULL;
+
+       /* We treat connection as closed even if close fails */
+       pv->io_closed = TRUE;
+       g_io_stream_close_finish (pv->io_stream, result, &error);
+
+       if (error) {
+               g_debug ("error closing web socket stream: %s", error->message);
+               if (!pv->dirty_close)
+                       g_signal_emit (self, signals[ERROR], 0, error);
+               pv->dirty_close = TRUE;
+               g_error_free (error);
+       }
+
+       g_assert (soup_websocket_connection_get_state (self) == SOUP_WEBSOCKET_STATE_CLOSED);
+       g_debug ("closed: completed io stream close");
+       g_signal_emit (self, signals[CLOSE], 0);
+
+       g_object_unref (self);
+}
+
+static void
+stop_input (SoupWebsocketConnection *self)
+{
+       SoupWebsocketConnectionPrivate *pv = self->pv;
+
+       if (pv->input_source) {
+               g_debug ("stopping input source");
+               g_source_destroy (pv->input_source);
+               g_source_unref (pv->input_source);
+               pv->input_source = NULL;
+       }
+}
+
+static void
+stop_output (SoupWebsocketConnection *self)
+{
+       SoupWebsocketConnectionPrivate *pv = self->pv;
+
+       if (pv->output_source) {
+               g_debug ("stopping output source");
+               g_source_destroy (pv->output_source);
+               g_source_unref (pv->output_source);
+               pv->output_source = NULL;
+       }
+}
+
+static void
+close_io_stop_timeout (SoupWebsocketConnection *self)
+{
+       SoupWebsocketConnectionPrivate *pv = self->pv;
+
+       if (pv->close_timeout) {
+               g_source_destroy (pv->close_timeout);
+               g_source_unref (pv->close_timeout);
+               pv->close_timeout = NULL;
+       }
+}
+
+static void
+close_io_stream (SoupWebsocketConnection *self)
+{
+       SoupWebsocketConnectionPrivate *pv = self->pv;
+
+       close_io_stop_timeout (self);
+
+       if (!pv->io_closing) {
+               stop_input (self);
+               stop_output (self);
+               pv->io_closing = TRUE;
+               g_debug ("closing io stream");
+               g_io_stream_close_async (pv->io_stream, G_PRIORITY_DEFAULT,
+                                        NULL, on_iostream_closed, g_object_ref (self));
+       }
+
+       g_object_notify (G_OBJECT (self), "state");
+}
+
+static void
+shutdown_wr_io_stream (SoupWebsocketConnection *self)
+{
+       SoupWebsocketConnectionPrivate *pv = self->pv;
+       GSocket *socket;
+       GError *error = NULL;
+
+       stop_output (self);
+
+       if (G_IS_SOCKET_CONNECTION (pv->io_stream)) {
+               socket = g_socket_connection_get_socket (G_SOCKET_CONNECTION (pv->io_stream));
+               g_socket_shutdown (socket, FALSE, TRUE, &error);
+               if (error != NULL) {
+                       g_debug ("error shutting down io stream: %s", error->message);
+                       g_error_free (error);
+               }
+       }
+
+       g_object_notify (G_OBJECT (self), "state");
+}
+
+static gboolean
+on_timeout_close_io (gpointer user_data)
+{
+       SoupWebsocketConnection *self = SOUP_WEBSOCKET_CONNECTION (user_data);
+       SoupWebsocketConnectionPrivate *pv = self->pv;
+
+       pv->close_timeout = 0;
+
+       g_debug ("peer did not close io when expected");
+       close_io_stream (self);
+
+       return FALSE;
+}
+
+static void
+close_io_after_timeout (SoupWebsocketConnection *self)
+{
+       SoupWebsocketConnectionPrivate *pv = self->pv;
+       const int timeout = 5;
+
+       if (pv->close_timeout)
+               return;
+
+       g_debug ("waiting %d seconds for peer to close io", timeout);
+       pv->close_timeout = g_timeout_source_new_seconds (timeout);
+       g_source_set_callback (pv->close_timeout, on_timeout_close_io, self, NULL);
+       g_source_attach (pv->close_timeout, pv->main_context);
+}
+
+static void
+xor_with_mask (const guint8 *mask,
+              guint8 *data,
+              gsize len)
+{
+       gsize n;
+
+       /* Do the masking */
+       for (n = 0; n < len; n++)
+               data[n] ^= mask[n & 3];
+}
+
+static void
+send_message (SoupWebsocketConnection *self,
+             SoupWebsocketQueueFlags flags,
+             guint8 opcode,
+             const guint8 *data,
+             gsize length)
+{
+       gsize buffered_amount = length;
+       GByteArray *bytes;
+       gsize frame_len;
+       guint8 *outer;
+       guint8 *mask = 0;
+       guint8 *at;
+
+       bytes = g_byte_array_sized_new (14 + length);
+       outer = bytes->data;
+       outer[0] = 0x80 | opcode;
+
+       /* If control message, truncate payload */
+       if (opcode & 0x08) {
+               if (length > 125) {
+                       g_warning ("Truncating WebSocket control message payload");
+                       length = 125;
+               }
+
+               buffered_amount = 0;
+       }
+
+       if (length < 126) {
+               outer[1] = (0xFF & length); /* mask | 7-bit-len */
+               bytes->len = 2;
+       } else if (length < 65536) {
+               outer[1] = 126; /* mask | 16-bit-len */
+               outer[2] = (length >> 8) & 0xFF;
+               outer[3] = (length >> 0) & 0xFF;
+               bytes->len = 4;
+       } else {
+               outer[1] = 127; /* mask | 64-bit-len */
+               outer[2] = (length >> 56) & 0xFF;
+               outer[3] = (length >> 48) & 0xFF;
+               outer[4] = (length >> 40) & 0xFF;
+               outer[5] = (length >> 32) & 0xFF;
+               outer[6] = (length >> 24) & 0xFF;
+               outer[7] = (length >> 16) & 0xFF;
+               outer[8] = (length >> 8) & 0xFF;
+               outer[9] = (length >> 0) & 0xFF;
+               bytes->len = 10;
+       }
+
+       /* The server side doesn't need to mask, so we don't. There's
+        * probably a client somewhere that's not expecting it.
+        */
+       if (!SOUP_IS_WEBSOCKET_SERVER (self)) {
+               outer[1] |= 0x80;
+               mask = outer + bytes->len;
+               * ((guint32 *)mask) = g_random_int ();
+               bytes->len += 4;
+       }
+
+       at = bytes->data + bytes->len;
+       g_byte_array_append (bytes, data, length);
+
+       if (!SOUP_IS_WEBSOCKET_SERVER (self))
+               xor_with_mask (mask, at, length);
+
+       frame_len = bytes->len;
+       queue_frame (self, flags, g_byte_array_free (bytes, FALSE),
+                    frame_len, buffered_amount);
+       g_debug ("queued %d frame of len %u", (int)opcode, (guint)frame_len);
+}
+
+static void
+send_close (SoupWebsocketConnection *self,
+           SoupWebsocketQueueFlags flags,
+           gushort code,
+           const char *reason)
+{
+       /* Note that send_message truncates as expected */
+       char buffer[128];
+       gsize len = 0;
+
+       if (code != 0) {
+               buffer[len++] = code >> 8;
+               buffer[len++] = code & 0xFF;
+               if (reason)
+                       len += g_strlcpy (buffer + len, reason, sizeof (buffer) - len);
+       }
+
+       send_message (self, flags, 0x08, (guint8 *)buffer, len);
+       self->pv->close_sent = TRUE;
+}
+
+static void
+emit_error_and_close (SoupWebsocketConnection *self,
+                     GError *error,
+                     gboolean prejudice)
+{
+       gboolean ignore = FALSE;
+       gushort code;
+
+       if (soup_websocket_connection_get_state (self) == SOUP_WEBSOCKET_STATE_CLOSED) {
+               g_error_free (error);
+               return;
+       }
+
+       if (error && error->domain == SOUP_WEBSOCKET_ERROR)
+               code = error->code;
+       else
+               code = SOUP_WEBSOCKET_CLOSE_GOING_AWAY;
+
+       self->pv->dirty_close = TRUE;
+       g_signal_emit (self, signals[ERROR], 0, error);
+       g_error_free (error);
+
+       /* If already closing, just ignore this stuff */
+       switch (soup_websocket_connection_get_state (self)) {
+       case SOUP_WEBSOCKET_STATE_CLOSED:
+               ignore = TRUE;
+               break;
+       case SOUP_WEBSOCKET_STATE_CLOSING:
+               ignore = !prejudice;
+               break;
+       default:
+               break;
+       }
+
+       if (ignore) {
+               g_debug ("already closing/closed, ignoring error");
+       } else if (prejudice) {
+               g_debug ("forcing close due to error");
+               close_io_stream (self);
+       } else {
+               g_debug ("requesting close due to error");
+               send_close (self, SOUP_WEBSOCKET_QUEUE_URGENT | SOUP_WEBSOCKET_QUEUE_LAST, code, NULL);
+       }
+}
+
+static void
+protocol_error_and_close_full (SoupWebsocketConnection *self,
+                               gboolean prejudice)
+{
+       GError *error;
+
+       error = g_error_new_literal (SOUP_WEBSOCKET_ERROR,
+                                    SOUP_WEBSOCKET_CLOSE_PROTOCOL,
+                                    SOUP_IS_WEBSOCKET_SERVER (self) ?
+                                    "Received invalid WebSocket response from the client" :
+                                    "Received invalid WebSocket response from the server");
+       emit_error_and_close (self, error, prejudice);
+}
+
+static void
+protocol_error_and_close (SoupWebsocketConnection *self)
+{
+       protocol_error_and_close_full (self, FALSE);
+}
+
+static void
+bad_data_error_and_close (SoupWebsocketConnection *self)
+{
+       GError *error;
+
+       error = g_error_new_literal (SOUP_WEBSOCKET_ERROR,
+                                    SOUP_WEBSOCKET_CLOSE_BAD_DATA,
+                                    SOUP_IS_WEBSOCKET_SERVER (self) ?
+                                    "Received invalid WebSocket data from the client" :
+                                    "Received invalid WebSocket data from the server");
+       emit_error_and_close (self, error, FALSE);
+}
+
+static void
+too_big_error_and_close (SoupWebsocketConnection *self,
+                         gsize payload_len)
+{
+       GError *error;
+
+       error = g_error_new_literal (SOUP_WEBSOCKET_ERROR,
+                                    SOUP_WEBSOCKET_CLOSE_TOO_BIG,
+                                    SOUP_IS_WEBSOCKET_SERVER (self) ?
+                                    "Received extremely large WebSocket data from the client" :
+                                    "Received extremely large WebSocket data from the server");
+       g_debug ("%s is trying to frame of size %" G_GUINT64_FORMAT " or greater, but max supported size is 
128KiB",
+                SOUP_IS_WEBSOCKET_SERVER (self) ? "server" : "client", payload_len);
+       emit_error_and_close (self, error, TRUE);
+
+       /* The input is in an invalid state now */
+       stop_input (self);
+}
+
+static void
+receive_close (SoupWebsocketConnection *self,
+              const guint8 *data,
+              gsize len)
+{
+       SoupWebsocketConnectionPrivate *pv = self->pv;
+
+       pv->peer_close_code = 0;
+       g_free (pv->peer_close_data);
+       pv->peer_close_data = NULL;
+       pv->close_received = TRUE;
+
+       /* Store the code/data payload */
+       if (len >= 2) {
+               pv->peer_close_code = (guint16)data[0] << 8 | data[1];
+       }
+       if (len > 2) {
+               data += 2;
+               len -= 2;
+               if (g_utf8_validate ((char *)data, len, NULL))
+                       pv->peer_close_data = g_strndup ((char *)data, len);
+               else
+                       g_debug ("received non-UTF8 close data: %d '%.*s' %d", (int)len, (int)len, (char 
*)data, (int)data[0]);
+       }
+
+       /* Once we receive close response on server, close immediately */
+       if (pv->close_sent) {
+               shutdown_wr_io_stream (self);
+               if (SOUP_IS_WEBSOCKET_SERVER (self))
+                       close_io_stream (self);
+       } else {
+               /* Send back the response */
+               soup_websocket_connection_close (self, pv->peer_close_code, NULL);
+       }
+}
+
+static void
+receive_ping (SoupWebsocketConnection *self,
+                      const guint8 *data,
+                      gsize len)
+{
+       /* Send back a pong with same data */
+       g_debug ("received ping, responding");
+       send_message (self, SOUP_WEBSOCKET_QUEUE_URGENT, 0x0A, data, len);
+}
+
+static void
+process_contents (SoupWebsocketConnection *self,
+                 gboolean control,
+                 gboolean fin,
+                 guint8 opcode,
+                 gconstpointer payload,
+                 gsize payload_len)
+{
+       SoupWebsocketConnectionPrivate *pv = self->pv;
+       GBytes *message;
+
+       if (control) {
+               /* Control frames must never be fragmented */
+               if (!fin) {
+                       g_debug ("received fragmented control frame");
+                       protocol_error_and_close (self);
+                       return;
+               }
+
+               g_debug ("received control frame %d with %d payload", (int)opcode, (int)payload_len);
+
+               switch (opcode) {
+               case 0x08:
+                       receive_close (self, payload, payload_len);
+                       break;
+               case 0x09:
+                       receive_ping (self, payload, payload_len);
+                       break;
+               case 0x0A:
+                       break;
+               default:
+                       g_debug ("received unsupported control frame: %d", (int)opcode);
+                       break;
+               }
+       } else if (pv->close_received) {
+               g_debug ("received message after close was received");
+       } else {
+               /* A message frame */
+
+               if (!fin && opcode) {
+                       /* Initial fragment of a message */
+                       if (pv->message_data) {
+                               g_debug ("received out of order inital message fragment");
+                               protocol_error_and_close (self);
+                               return;
+                       }
+                       g_debug ("received inital fragment frame %d with %d payload", (int)opcode, 
(int)payload_len);
+               } else if (!fin && !opcode) {
+                       /* Middle fragment of a message */
+                       if (!pv->message_data) {
+                               g_debug ("received out of order middle message fragment");
+                               protocol_error_and_close (self);
+                               return;
+                       }
+                       g_debug ("received middle fragment frame with %d payload", (int)payload_len);
+               } else if (fin && !opcode) {
+                       /* Last fragment of a message */
+                       if (!pv->message_data) {
+                               g_debug ("received out of order ending message fragment");
+                               protocol_error_and_close (self);
+                               return;
+                       }
+                       g_debug ("received last fragment frame with %d payload", (int)payload_len);
+               } else {
+                       /* An unfragmented message */
+                       g_assert (opcode != 0);
+                       if (pv->message_data) {
+                               g_debug ("received unfragmented message when fragment was expected");
+                               protocol_error_and_close (self);
+                               return;
+                       }
+                       g_debug ("received frame %d with %d payload", (int)opcode, (int)payload_len);
+               }
+
+               if (opcode) {
+                       pv->message_opcode = opcode;
+                       pv->message_data = g_byte_array_sized_new (payload_len);
+               }
+
+               switch (pv->message_opcode) {
+               case 0x01:
+                       if (!g_utf8_validate ((char *)payload, payload_len, NULL)) {
+                               g_debug ("received invalid non-UTF8 text data");
+
+                               /* Discard the entire message */
+                               g_byte_array_unref (pv->message_data);
+                               pv->message_data = NULL;
+                               pv->message_opcode = 0;
+
+                               bad_data_error_and_close (self);
+                               return;
+                       }
+                       /* fall through */
+               case 0x02:
+                       g_byte_array_append (pv->message_data, payload, payload_len);
+                       break;
+               default:
+                       g_debug ("received unknown data frame: %d", (int)opcode);
+                       break;
+               }
+
+               /* Actually deliver the message? */
+               if (fin) {
+                       /* Always null terminate, as a convenience */
+                       g_byte_array_append (pv->message_data, (guchar *)"\0", 1);
+
+                       /* But don't include the null terminator in the byte count */
+                       pv->message_data->len--;
+
+                       opcode = pv->message_opcode;
+                       message = g_byte_array_free_to_bytes (pv->message_data);
+                       pv->message_data = NULL;
+                       pv->message_opcode = 0;
+                       g_debug ("message: delivering %d with %d length",
+                                (int)opcode, (int)g_bytes_get_size (message));
+                       g_signal_emit (self, signals[MESSAGE], 0, (int)opcode, message);
+                       g_bytes_unref (message);
+               }
+       }
+}
+
+static gboolean
+process_frame (SoupWebsocketConnection *self)
+{
+       guint8 *header;
+       guint8 *payload;
+       guint64 payload_len;
+       guint8 *mask;
+       gboolean fin;
+       gboolean control;
+       gboolean masked;
+       guint8 opcode;
+       gsize len;
+       gsize at;
+
+       len = self->pv->incoming->len;
+       if (len < 2)
+               return FALSE; /* need more data */
+
+       header = self->pv->incoming->data;
+       fin = ((header[0] & 0x80) != 0);
+       control = header[0] & 0x08;
+       opcode = header[0] & 0x0f;
+       masked = ((header[1] & 0x80) != 0);
+
+       switch (header[1] & 0x7f) {
+       case 126:
+               at = 4;
+               if (len < at)
+                       return FALSE; /* need more data */
+               payload_len = (((guint16)header[2] << 8) |
+                              ((guint16)header[3] << 0));
+               break;
+       case 127:
+               at = 10;
+               if (len < at)
+                       return FALSE; /* need more data */
+               payload_len = (((guint64)header[2] << 56) |
+                              ((guint64)header[3] << 48) |
+                              ((guint64)header[4] << 40) |
+                              ((guint64)header[5] << 32) |
+                              ((guint64)header[6] << 24) |
+                              ((guint64)header[7] << 16) |
+                              ((guint64)header[8] << 8) |
+                              ((guint64)header[9] << 0));
+               break;
+       default:
+               payload_len = header[1] & 0x7f;
+               at = 2;
+               break;
+       }
+
+       /* Safety valve */
+       if (payload_len >= MAX_PAYLOAD) {
+               too_big_error_and_close (self, payload_len);
+               return FALSE;
+       }
+
+       if (len < at + payload_len)
+               return FALSE; /* need more data */
+
+       payload = header + at;
+
+       if (masked) {
+               mask = header + at;
+               payload += 4;
+               at += 4;
+
+               if (len < at + payload_len)
+                       return FALSE; /* need more data */
+
+               xor_with_mask (mask, payload, payload_len);
+       }
+
+       /* Note that now that we've unmasked, we've modified the buffer, we can
+        * only return below via discarding or processing the message
+        */
+       process_contents (self, control, fin, opcode, payload, payload_len);
+
+       /* Move past the parsed frame */
+       g_byte_array_remove_range (self->pv->incoming, 0, at + payload_len);
+       return TRUE;
+}
+
+static void
+process_incoming (SoupWebsocketConnection *self)
+{
+       while (process_frame (self))
+               ;
+}
+
+static gboolean
+on_web_socket_input (GObject *pollable_stream,
+                    gpointer user_data)
+{
+       SoupWebsocketConnection *self = SOUP_WEBSOCKET_CONNECTION (user_data);
+       SoupWebsocketConnectionPrivate *pv = self->pv;
+       GError *error = NULL;
+       gboolean end = FALSE;
+       gssize count;
+       gsize len;
+
+       do {
+               len = pv->incoming->len;
+               g_byte_array_set_size (pv->incoming, len + 1024);
+
+               count = g_pollable_input_stream_read_nonblocking (pv->input,
+                                                                 pv->incoming->data + len,
+                                                                 1024, NULL, &error);
+
+               if (count < 0) {
+                       if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) {
+                               g_error_free (error);
+                               count = 0;
+                       } else {
+                               emit_error_and_close (self, error, TRUE);
+                               return TRUE;
+                       }
+               } else if (count == 0) {
+                       end = TRUE;
+               }
+
+               pv->incoming->len = len + count;
+       } while (count > 0);
+
+       process_incoming (self);
+
+       if (end) {
+               if (!pv->close_sent || !pv->close_received) {
+                       pv->dirty_close = TRUE;
+                       g_debug ("connection unexpectedly closed by peer");
+               } else {
+                       g_debug ("peer has closed socket");
+               }
+
+               close_io_stream (self);
+       }
+
+       return TRUE;
+}
+
+static gboolean
+on_web_socket_output (GObject *pollable_stream,
+                     gpointer user_data)
+{
+       SoupWebsocketConnection *self = SOUP_WEBSOCKET_CONNECTION (user_data);
+       SoupWebsocketConnectionPrivate *pv = self->pv;
+       const guint8 *data;
+       GError *error = NULL;
+       Frame *frame;
+       gssize count;
+       gsize len;
+
+       frame = g_queue_peek_head (&pv->outgoing);
+
+       /* No more frames to send */
+       if (frame == NULL) {
+               stop_output (self);
+               return TRUE;
+       }
+
+       data = g_bytes_get_data (frame->data, &len);
+       g_assert (len > 0);
+       g_assert (len > frame->sent);
+
+       count = g_pollable_output_stream_write_nonblocking (pv->output,
+                                                           data + frame->sent,
+                                                           len - frame->sent,
+                                                           NULL, &error);
+
+       if (count < 0) {
+               if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) {
+                       g_clear_error (&error);
+                       count = 0;
+               } else {
+                       emit_error_and_close (self, error, TRUE);
+                       return FALSE;
+               }
+       }
+
+       frame->sent += count;
+       if (frame->sent >= len) {
+               g_debug ("sent frame");
+               g_queue_pop_head (&pv->outgoing);
+
+               if (frame->last) {
+                       if (SOUP_IS_WEBSOCKET_SERVER (self)) {
+                               close_io_stream (self);
+                       } else {
+                               shutdown_wr_io_stream (self);
+                               close_io_after_timeout (self);
+                       }
+               }
+               frame_free (frame);
+       }
+
+       return TRUE;
+}
+
+static void
+start_output (SoupWebsocketConnection *self)
+{
+       SoupWebsocketConnectionPrivate *pv = self->pv;
+
+       if (pv->output_source)
+               return;
+
+       g_debug ("starting output source");
+       pv->output_source = g_pollable_output_stream_create_source (pv->output, NULL);
+       g_source_set_callback (pv->output_source, (GSourceFunc)on_web_socket_output, self, NULL);
+       g_source_attach (pv->output_source, pv->main_context);
+}
+
+static void
+queue_frame (SoupWebsocketConnection *self,
+            SoupWebsocketQueueFlags flags,
+            gpointer data,
+            gsize len,
+            gsize amount)
+{
+       SoupWebsocketConnectionPrivate *pv = self->pv;
+       Frame *frame;
+       Frame *prev;
+
+       g_return_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self));
+       g_return_if_fail (pv->close_sent == FALSE);
+       g_return_if_fail (data != NULL);
+       g_return_if_fail (len > 0);
+
+       frame = g_slice_new0 (Frame);
+       frame->data = g_bytes_new_take (data, len);
+       frame->amount = amount;
+       frame->last = (flags & SOUP_WEBSOCKET_QUEUE_LAST) ? TRUE : FALSE;
+
+       /* If urgent put at front of queue */
+       if (flags & SOUP_WEBSOCKET_QUEUE_URGENT) {
+               /* But we can't interrupt a message already partially sent */
+               prev = g_queue_pop_head (&pv->outgoing);
+               if (prev == NULL) {
+                       g_queue_push_head (&pv->outgoing, frame);
+               } else if (prev->sent > 0) {
+                       g_queue_push_head (&pv->outgoing, frame);
+                       g_queue_push_head (&pv->outgoing, prev);
+               } else {
+                       g_queue_push_head (&pv->outgoing, prev);
+                       g_queue_push_head (&pv->outgoing, frame);
+               }
+       } else {
+               g_queue_push_tail (&pv->outgoing, frame);
+       }
+
+       start_output (self);
+}
+
+static void
+soup_websocket_connection_constructed (GObject *object)
+{
+       SoupWebsocketConnection *self = SOUP_WEBSOCKET_CONNECTION (object);
+       SoupWebsocketConnectionPrivate *pv = self->pv;
+       GInputStream *is;
+       GOutputStream *os;
+
+       G_OBJECT_CLASS (soup_websocket_connection_parent_class)->constructed (object);
+
+       g_return_if_fail (pv->io_stream != NULL);
+
+       is = g_io_stream_get_input_stream (pv->io_stream);
+       g_return_if_fail (G_IS_POLLABLE_INPUT_STREAM (is));
+       pv->input = G_POLLABLE_INPUT_STREAM (is);
+       g_return_if_fail (g_pollable_input_stream_can_poll (pv->input));
+
+       os = g_io_stream_get_output_stream (pv->io_stream);
+       g_return_if_fail (G_IS_POLLABLE_OUTPUT_STREAM (os));
+       pv->output = G_POLLABLE_OUTPUT_STREAM (os);
+       g_return_if_fail (g_pollable_output_stream_can_poll (pv->output));
+
+       pv->input_source = g_pollable_input_stream_create_source (pv->input, NULL);
+       g_source_set_callback (pv->input_source, (GSourceFunc)on_web_socket_input, self, NULL);
+       g_source_attach (pv->input_source, pv->main_context);
+}
+
+static void
+soup_websocket_connection_get_property (GObject *object,
+                                       guint prop_id,
+                                       GValue *value,
+                                       GParamSpec *pspec)
+{
+       SoupWebsocketConnection *self = SOUP_WEBSOCKET_CONNECTION (object);
+
+       switch (prop_id) {
+       case PROP_MESSAGE:
+               g_value_set_object (value, soup_websocket_connection_get_message (self));
+               break;
+
+       case PROP_URI:
+               g_value_set_boxed (value, soup_websocket_connection_get_uri (self));
+               break;
+
+       case PROP_ORIGIN:
+               g_value_set_string (value, soup_websocket_connection_get_origin (self));
+               break;
+
+       case PROP_PROTOCOL:
+               g_value_set_string (value, soup_websocket_connection_get_protocol (self));
+               break;
+
+       case PROP_STATE:
+               g_value_set_enum (value, soup_websocket_connection_get_state (self));
+               break;
+
+       case PROP_IO_STREAM:
+               g_value_set_object (value, soup_websocket_connection_get_io_stream (self));
+               break;
+
+       default:
+               G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+               break;
+       }
+}
+
+static void
+soup_websocket_connection_set_property (GObject *object,
+                                       guint prop_id,
+                                       const GValue *value,
+                                       GParamSpec *pspec)
+{
+       SoupWebsocketConnection *self = SOUP_WEBSOCKET_CONNECTION (object);
+       SoupWebsocketConnectionPrivate *pv = self->pv;
+
+       switch (prop_id) {
+       case PROP_MESSAGE:
+               g_return_if_fail (pv->msg == NULL);
+               pv->msg = g_value_dup_object (value);
+               break;
+
+       case PROP_IO_STREAM:
+               g_return_if_fail (pv->io_stream == NULL);
+               pv->io_stream = g_value_dup_object (value);
+               break;
+
+       default:
+               G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+               break;
+       }
+}
+
+static void
+soup_websocket_connection_dispose (GObject *object)
+{
+       SoupWebsocketConnection *self = SOUP_WEBSOCKET_CONNECTION (object);
+
+       self->pv->dirty_close = TRUE;
+       close_io_stream (self);
+
+       G_OBJECT_CLASS (soup_websocket_connection_parent_class)->dispose (object);
+}
+
+static void
+soup_websocket_connection_finalize (GObject *object)
+{
+       SoupWebsocketConnection *self = SOUP_WEBSOCKET_CONNECTION (object);
+       SoupWebsocketConnectionPrivate *pv = self->pv;
+
+       g_free (pv->peer_close_data);
+
+       g_main_context_unref (pv->main_context);
+
+       if (pv->incoming)
+               g_byte_array_free (pv->incoming, TRUE);
+       while (!g_queue_is_empty (&pv->outgoing))
+               frame_free (g_queue_pop_head (&pv->outgoing));
+
+       g_clear_object (&pv->io_stream);
+       g_assert (!pv->input_source);
+       g_assert (!pv->output_source);
+       g_assert (pv->io_closing);
+       g_assert (pv->io_closed);
+       g_assert (!pv->close_timeout);
+
+       if (pv->message_data)
+               g_byte_array_free (pv->message_data, TRUE);
+
+       G_OBJECT_CLASS (soup_websocket_connection_parent_class)->finalize (object);
+}
+
+static void
+soup_websocket_connection_class_init (SoupWebsocketConnectionClass *klass)
+{
+       GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+       g_type_class_add_private (klass, sizeof (SoupWebsocketConnectionPrivate));
+
+       gobject_class->constructed = soup_websocket_connection_constructed;
+       gobject_class->get_property = soup_websocket_connection_get_property;
+       gobject_class->set_property = soup_websocket_connection_set_property;
+       gobject_class->dispose = soup_websocket_connection_dispose;
+       gobject_class->finalize = soup_websocket_connection_finalize;
+
+       /**
+        * SoupWebsocketConnection:message:
+        *
+        * The #SoupMessage used to create the WebSocket connection.
+        *
+        * Since: 2.50
+        */
+       g_object_class_install_property (gobject_class, PROP_MESSAGE,
+                                        g_param_spec_object ("message",
+                                                             "Message",
+                                                             "The SoupMessage used to create the connection",
+                                                             SOUP_TYPE_MESSAGE,
+                                                             G_PARAM_READWRITE |
+                                                             G_PARAM_CONSTRUCT_ONLY |
+                                                             G_PARAM_STATIC_STRINGS));
+
+       /**
+        * SoupWebsocketConnection:uri:
+        *
+        * The URI of the WebSocket.
+        *
+        * For servers this represents the address of the WebSocket,
+        * and for clients it is the address connected to.
+        *
+        * Since: 2.50
+        */
+       g_object_class_install_property (gobject_class, PROP_URI,
+                                        g_param_spec_boxed ("uri",
+                                                            "URI",
+                                                            "The WebSocket URI",
+                                                            SOUP_TYPE_URI,
+                                                            G_PARAM_READABLE |
+                                                            G_PARAM_STATIC_STRINGS));
+
+       /**
+        * SoupWebsocketConnection:protocol:
+        *
+        * The chosen protocol, or %NULL if a protocol was not agreed
+        * upon.
+        *
+        * Since: 2.50
+        */
+       g_object_class_install_property (gobject_class, PROP_PROTOCOL,
+                                        g_param_spec_string ("protocol",
+                                                             "Protocol",
+                                                             "The chosen WebSocket protocol",
+                                                             NULL,
+                                                             G_PARAM_READABLE |
+                                                             G_PARAM_STATIC_STRINGS));
+
+       /**
+        * SoupWebsocketConnection:origin:
+        *
+        * The client's Origin.
+        *
+        * Since: 2.50
+        */
+       g_object_class_install_property (gobject_class, PROP_ORIGIN,
+                                        g_param_spec_string ("origin",
+                                                             "Origin",
+                                                             "The WebSocket origin",
+                                                             NULL,
+                                                             G_PARAM_READABLE |
+                                                             G_PARAM_STATIC_STRINGS));
+
+       /**
+        * SoupWebsocketConnection:state:
+        *
+        * The current state of the WebSocket.
+        *
+        * Since: 2.50
+        */
+       g_object_class_install_property (gobject_class, PROP_STATE,
+                                        g_param_spec_enum ("state",
+                                                           "State",
+                                                           "State ",
+                                                           SOUP_TYPE_WEBSOCKET_STATE,
+                                                           SOUP_WEBSOCKET_STATE_OPEN,
+                                                           G_PARAM_READABLE |
+                                                           G_PARAM_STATIC_STRINGS));
+
+       /**
+        * SoupWebsocketConnection:io-stream:
+        *
+        * The underlying IO stream the WebSocket is communicating
+        * over.
+        *
+        * The input and output streams must be pollable streams.
+        *
+        * Since: 2.50
+        */
+       g_object_class_install_property (gobject_class, PROP_IO_STREAM,
+                                        g_param_spec_object ("io-stream",
+                                                             "I/O Stream",
+                                                             "Underlying I/O stream",
+                                                             G_TYPE_IO_STREAM,
+                                                             G_PARAM_READWRITE |
+                                                             G_PARAM_CONSTRUCT_ONLY |
+                                                             G_PARAM_STATIC_STRINGS));
+
+       /**
+        * SoupWebsocketConnection::message:
+        * @self: the WebSocket
+        * @type: the type of message contents
+        * @message: the message data
+        *
+        * Emitted when we receive a message from the peer.
+        *
+        * As a convenience, the @message data will always be
+        * null-terminated, but the null-terminator will not be
+        * included in the length count. This signal may emitted
+        * multiple times.
+        *
+        * Since: 2.50
+        */
+       signals[MESSAGE] = g_signal_new ("message",
+                                        SOUP_TYPE_WEBSOCKET_CONNECTION,
+                                        G_SIGNAL_RUN_FIRST,
+                                        G_STRUCT_OFFSET (SoupWebsocketConnectionClass, message),
+                                        NULL, NULL, g_cclosure_marshal_generic,
+                                        G_TYPE_NONE, 2, G_TYPE_INT, G_TYPE_BYTES);
+
+       /**
+        * SoupWebsocketConnection::error:
+        * @self: the WebSocket
+        * @error: the error that occured
+        *
+        * Emitted when an error occurred on the WebSocket. This may
+        * be fired multiple times. Fatal errors will be followed by
+        * the #SoupWebsocketConnection::close signal being emitted.
+        *
+        * Since: 2.50
+        */
+       signals[ERROR] = g_signal_new ("error",
+                                      SOUP_TYPE_WEBSOCKET_CONNECTION,
+                                      G_SIGNAL_RUN_FIRST,
+                                      G_STRUCT_OFFSET (SoupWebsocketConnectionClass, error),
+                                      NULL, NULL, g_cclosure_marshal_generic,
+                                      G_TYPE_NONE, 1, G_TYPE_ERROR);
+
+       /**
+        * SoupWebsocketConnection::closing:
+        * @self: the WebSocket
+        *
+        * This signal will be emitted during an orderly close
+        *
+        * Since: 2.50
+        */
+       signals[CLOSING] = g_signal_new ("closing",
+                                        SOUP_TYPE_WEBSOCKET_CONNECTION,
+                                        G_SIGNAL_RUN_LAST,
+                                        G_STRUCT_OFFSET (SoupWebsocketConnectionClass, closing),
+                                        NULL, NULL, g_cclosure_marshal_generic,
+                                        G_TYPE_NONE, 0);
+
+       /**
+        * SoupWebsocketConnection::close:
+        * @self: the WebSocket
+        *
+        * Emitted when the connection has completely closed, either
+        * due to an orderly close from the peer, one initiated via
+        * soup_websocket_connection_close() or a fatal error
+        * condition that caused a close.
+        *
+        * This signal will be emitted once.
+        *
+        * Since: 2.50
+        */
+       signals[CLOSE] = g_signal_new ("close",
+                                      SOUP_TYPE_WEBSOCKET_CONNECTION,
+                                      G_SIGNAL_RUN_FIRST,
+                                      G_STRUCT_OFFSET (SoupWebsocketConnectionClass, close),
+                                      NULL, NULL, g_cclosure_marshal_generic,
+                                      G_TYPE_NONE, 0);
+}
+
+/**
+ * soup_websocket_connection_get_message:
+ * @self: the WebSocket
+ *
+ * Get the #SoupMessage that was used to create the WebSocket
+ * connection.
+ *
+ * Returns: the #SoupMessage
+ *
+ * Since: 2.50
+ */
+SoupMessage *
+soup_websocket_connection_get_message (SoupWebsocketConnection *self)
+{
+       g_return_val_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self), NULL);
+
+       return self->pv->msg;
+}
+
+/**
+ * soup_websocket_connection_get_uri:
+ * @self: the WebSocket
+ *
+ * Get the URI of the WebSocket.
+ *
+ * For servers this represents the address of the WebSocket, and
+ * for clients it is the address connected to.
+ *
+ * Returns: (transfer none): the URI
+ *
+ * Since: 2.50
+ */
+SoupURI *
+soup_websocket_connection_get_uri (SoupWebsocketConnection *self)
+{
+       g_return_val_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self), NULL);
+
+       return soup_message_get_uri (self->pv->msg);
+}
+
+/**
+ * soup_websocket_connection_get_origin:
+ * @self: the WebSocket
+ *
+ * Get the origin of the WebSocket.
+ *
+ * Returns: the origin
+ *
+ * Since: 2.50
+ */
+const char *
+soup_websocket_connection_get_origin (SoupWebsocketConnection *self)
+{
+       g_return_val_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self), NULL);
+
+       return soup_message_headers_get_one (self->pv->msg->request_headers, "Origin");
+}
+
+/**
+ * soup_websocket_connection_get_protocol:
+ * @self: the WebSocket
+ *
+ * Get the protocol chosen via negotiation with the peer.
+ *
+ * Returns: the chosen protocol or %NULL
+ *
+ * Since: 2.50
+ */
+const char *
+soup_websocket_connection_get_protocol (SoupWebsocketConnection *self)
+{
+       g_return_val_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self), NULL);
+
+       return soup_message_headers_get_one (self->pv->msg->response_headers, "Sec-WebSocket-Protocol");
+}
+
+/**
+ * soup_websocket_connection_get_state:
+ * @self: the WebSocket
+ *
+ * Get the current state of the WebSocket.
+ *
+ * Returns: the state
+ *
+ * Since: 2.50
+ */
+SoupWebsocketState
+soup_websocket_connection_get_state (SoupWebsocketConnection *self)
+{
+       g_return_val_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self), 0);
+
+       if (self->pv->io_closed)
+               return SOUP_WEBSOCKET_STATE_CLOSED;
+       else if (self->pv->io_closing || self->pv->close_sent)
+               return SOUP_WEBSOCKET_STATE_CLOSING;
+       else
+               return SOUP_WEBSOCKET_STATE_OPEN;
+}
+
+/**
+ * soup_websocket_connection_get_io_stream:
+ * @self: the WebSocket
+ *
+ * Get the I/O stream the WebSocket is communicating over.
+ *
+ * Returns: (transfer none): the WebSocket's I/O stream.
+ *
+ * Since: 2.50
+ */
+GIOStream *
+soup_websocket_connection_get_io_stream (SoupWebsocketConnection *self)
+{
+       g_return_val_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self), NULL);
+
+       return self->pv->io_stream;
+}
+
+/**
+ * soup_websocket_connection_get_close_code:
+ * @self: the WebSocket
+ *
+ * Get the close code received from the WebSocket peer.
+ *
+ * This only becomes valid once the WebSocket is in the
+ * %SOUP_WEBSOCKET_STATE_CLOSED state. The value will often be in the
+ * #SoupWebsocketCloseCode enumeration, but may also be an application
+ * defined close code.
+ *
+ * Returns: the close code or zero.
+ *
+ * Since: 2.50
+ */
+gushort
+soup_websocket_connection_get_close_code (SoupWebsocketConnection *self)
+{
+       g_return_val_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self), 0);
+
+       return self->pv->peer_close_code;
+}
+
+/**
+ * soup_websocket_connection_get_close_data:
+ * @self: the WebSocket
+ *
+ * Get the close data received from the WebSocket peer.
+ *
+ * This only becomes valid once the WebSocket is in the
+ * %SOUP_WEBSOCKET_STATE_CLOSED state. The data may be freed once
+ * the main loop is run, so copy it if you need to keep it around.
+ *
+ * Returns: the close data or %NULL
+ *
+ * Since: 2.50
+ */
+const char *
+soup_websocket_connection_get_close_data (SoupWebsocketConnection *self)
+{
+       g_return_val_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self), NULL);
+
+       return self->pv->peer_close_data;
+}
+
+/**
+ * soup_websocket_connection_send:
+ * @self: the WebSocket
+ * @type: the data type of message
+ * @data: the message contents
+ * @length: the length of @data, or -1 if @data is a NUL-terminated string
+ *
+ * Send a message to the peer.
+ *
+ * The @type parameter describes whether this is a binary or text message.
+ * If a text message then the contents must be valid UTF-8.
+ *
+ * The message is queued to be sent and will be sent when the main loop
+ * is run.
+ *
+ * Since: 2.50
+ */
+void
+soup_websocket_connection_send (SoupWebsocketConnection *self,
+                               SoupWebsocketDataType type,
+                               gconstpointer data,
+                               gssize length)
+{
+       guint8 opcode;
+
+       g_return_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self));
+       g_return_if_fail (soup_websocket_connection_get_state (self) == SOUP_WEBSOCKET_STATE_OPEN);
+       g_return_if_fail (data != NULL);
+
+       if (length == -1)
+               length = strlen (data);
+
+       switch (type) {
+       case SOUP_WEBSOCKET_DATA_TEXT:
+               g_return_if_fail (g_utf8_validate (data, length, NULL));
+               opcode = 0x01;
+               break;
+       case SOUP_WEBSOCKET_DATA_BINARY:
+               opcode = 0x02;
+               break;
+       default:
+               g_return_if_reached ();
+               return;
+       }
+
+       send_message (self, SOUP_WEBSOCKET_QUEUE_NORMAL, opcode, data, length);
+}
+
+/**
+ * soup_websocket_connection_close:
+ * @self: the WebSocket
+ * @code: close code
+ * @data: (allow-none): close data
+ *
+ * Close the connection in an orderly fashion.
+ *
+ * Note that until the #SoupWebsocketConnection::close signal fires, the connection
+ * is not yet completely closed. The close message is not even sent until the
+ * main loop runs.
+ *
+ * The @code and @data are sent to the peer along with the close request.
+ * Note that the @data must be UTF-8 valid.
+ *
+ * Since: 2.50
+ */
+void
+soup_websocket_connection_close (SoupWebsocketConnection *self,
+                                gushort code,
+                                const char *data)
+{
+       SoupWebsocketQueueFlags flags;
+
+       g_return_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self));
+       g_return_if_fail (!self->pv->close_sent);
+
+       g_signal_emit (self, signals[CLOSING], 0);
+
+       if (self->pv->close_received)
+               g_debug ("responding to close request");
+
+       flags = 0;
+       if (SOUP_IS_WEBSOCKET_SERVER (self) && self->pv->close_received)
+               flags |= SOUP_WEBSOCKET_QUEUE_LAST;
+       send_close (self, flags, code, data);
+       close_io_after_timeout (self);
+}
diff --git a/libsoup/soup-websocket-connection.h b/libsoup/soup-websocket-connection.h
new file mode 100644
index 0000000..211ae23
--- /dev/null
+++ b/libsoup/soup-websocket-connection.h
@@ -0,0 +1,101 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * soup-websocket-connection.h: This file was originally part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __SOUP_WEBSOCKET_CONNECTION_H__
+#define __SOUP_WEBSOCKET_CONNECTION_H__
+
+#include <libsoup/soup-types.h>
+#include <libsoup/soup-websocket.h>
+
+G_BEGIN_DECLS
+
+#define SOUP_TYPE_WEBSOCKET_CONNECTION         (soup_websocket_connection_get_type ())
+#define SOUP_WEBSOCKET_CONNECTION(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), 
SOUP_TYPE_WEBSOCKET_CONNECTION, SoupWebsocketConnection))
+#define SOUP_IS_WEBSOCKET_CONNECTION(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), 
SOUP_TYPE_WEBSOCKET_CONNECTION))
+#define SOUP_WEBSOCKET_CONNECTION_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST ((k), 
SOUP_TYPE_WEBSOCKET_CONNECTION, SoupWebsocketConnectionClass))
+#define SOUP_WEBSOCKET_CONNECTION_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), 
SOUP_TYPE_WEBSOCKET_CONNECTION, SoupWebsocketConnectionClass))
+#define SOUP_IS_WEBSOCKET_CONNECTION_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), 
SOUP_TYPE_WEBSOCKET_CONNECTION))
+
+typedef struct _SoupWebsocketConnectionPrivate  SoupWebsocketConnectionPrivate;
+
+struct _SoupWebsocketConnection {
+       GObject parent;
+
+       /*< private >*/
+       SoupWebsocketConnectionPrivate *pv;
+};
+
+typedef struct {
+       GObjectClass parent;
+
+       /* signals */
+       void      (* message)     (SoupWebsocketConnection *self,
+                                  SoupWebsocketDataType type,
+                                  GBytes *message);
+
+       void      (* error)       (SoupWebsocketConnection *self,
+                                  GError *error);
+
+       void      (* closing)     (SoupWebsocketConnection *self);
+
+       void      (* close)       (SoupWebsocketConnection *self);
+} SoupWebsocketConnectionClass;
+
+SOUP_AVAILABLE_IN_2_50
+GType soup_websocket_connection_get_type (void) G_GNUC_CONST;
+
+SOUP_AVAILABLE_IN_2_50
+SoupMessage *       soup_websocket_connection_get_message         (SoupWebsocketConnection *self);
+
+SOUP_AVAILABLE_IN_2_50
+SoupURI *           soup_websocket_connection_get_uri             (SoupWebsocketConnection *self);
+
+SOUP_AVAILABLE_IN_2_50
+const char *        soup_websocket_connection_get_origin          (SoupWebsocketConnection *self);
+
+SOUP_AVAILABLE_IN_2_50
+const char *        soup_websocket_connection_get_protocol        (SoupWebsocketConnection *self);
+
+SOUP_AVAILABLE_IN_2_50
+SoupWebsocketState  soup_websocket_connection_get_state           (SoupWebsocketConnection *self);
+
+SOUP_AVAILABLE_IN_2_50
+gushort             soup_websocket_connection_get_close_code      (SoupWebsocketConnection *self);
+
+SOUP_AVAILABLE_IN_2_50
+const char *        soup_websocket_connection_get_close_data      (SoupWebsocketConnection *self);
+
+SOUP_AVAILABLE_IN_2_50
+GIOStream *         soup_websocket_connection_get_io_stream       (SoupWebsocketConnection *self);
+
+SOUP_AVAILABLE_IN_2_50
+void                soup_websocket_connection_send                (SoupWebsocketConnection *self,
+                                                                  SoupWebsocketDataType type,
+                                                                  gconstpointer data,
+                                                                  gssize length);
+
+SOUP_AVAILABLE_IN_2_50
+void                soup_websocket_connection_close               (SoupWebsocketConnection *self,
+                                                                  gushort code,
+                                                                  const char *data);
+
+G_END_DECLS
+
+#endif /* __SOUP_WEBSOCKET_CONNECTION_H__ */
diff --git a/libsoup/soup-websocket-private.h b/libsoup/soup-websocket-private.h
new file mode 100644
index 0000000..d592775
--- /dev/null
+++ b/libsoup/soup-websocket-private.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * soup-websocket-private.h: This file was originally part of Cockpit.
+ *
+ * Copyright 2013, 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __SOUP_WEBSOCKET_PRIVATE_H__
+#define __SOUP_WEBSOCKET_PRIVATE_H__
+
+#include <libsoup/soup-types.h>
+
+G_BEGIN_DECLS
+
+char *soup_websocket_get_accept_key (const char *key);
+
+G_END_DECLS
+
+#endif /* __SOUP_WEBSOCKET_PRIVATE_H__ */
diff --git a/libsoup/soup-websocket-server.c b/libsoup/soup-websocket-server.c
new file mode 100644
index 0000000..23c99ad
--- /dev/null
+++ b/libsoup/soup-websocket-server.c
@@ -0,0 +1,182 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * soup-websocket-server.c: This file was originally part of Cockpit.
+ *
+ * Copyright 2013, 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <string.h>
+
+#include "soup-websocket-server.h"
+#include "soup-websocket-private.h"
+#include "soup-headers.h"
+#include "soup-uri.h"
+
+typedef SoupWebsocketConnection SoupWebsocketServer;
+typedef SoupWebsocketConnectionClass SoupWebsocketServerClass;
+
+G_DEFINE_TYPE (SoupWebsocketServer, soup_websocket_server, SOUP_TYPE_WEBSOCKET_CONNECTION)
+
+static void
+soup_websocket_server_init (SoupWebsocketServer *self)
+{
+}
+
+static void
+soup_websocket_server_class_init (SoupWebsocketServerClass *klass)
+{
+}
+
+SoupWebsocketConnection *
+soup_websocket_server_new (SoupMessage  *msg,
+                          GIOStream    *stream)
+{
+       return g_object_new (SOUP_TYPE_WEBSOCKET_SERVER,
+                            "message", msg,
+                            "io-stream", stream,
+                            NULL);
+}
+
+#define RESPONSE_FORBIDDEN "<html><head><title>400 Forbidden</title></head>\r\n" \
+       "<body>Received invalid WebSocket request</body></html>\r\n"
+
+static gboolean
+respond_handshake_forbidden (SoupMessage *msg)
+{
+       soup_message_set_status (msg, SOUP_STATUS_FORBIDDEN);
+       soup_message_headers_append (msg->response_headers, "Connection", "close");
+       soup_message_set_response (msg, "text/html", SOUP_MEMORY_COPY,
+                                  RESPONSE_FORBIDDEN, strlen (RESPONSE_FORBIDDEN));
+
+       return FALSE;
+}
+
+#define RESPONSE_BAD "<html><head><title>400 Bad Request</title></head>\r\n" \
+       "<body>Received invalid WebSocket request: %s</body></html>\r\n"
+
+static gboolean
+respond_handshake_bad (SoupMessage *msg, const char *why)
+{
+       char *text;
+
+       text = g_strdup_printf (RESPONSE_BAD, why);
+       soup_message_set_status (msg, SOUP_STATUS_BAD_REQUEST);
+       soup_message_headers_append (msg->response_headers, "Connection", "close");
+       soup_message_set_response (msg, "text/html", SOUP_MEMORY_TAKE,
+                                  text, strlen (text));
+
+       return FALSE;
+}
+
+static gboolean
+validate_websocket_key (const char *key)
+{
+       /* The key must be 16 bytes, base64 encoded, meaning 22 bytes of base64
+        * data followed by 2 bytes of padding.
+        */
+       if (strlen (key) != 24 || key[21] == '=' || key[22] != '=' || key[23] != '=')
+               return FALSE;
+       return TRUE;
+}
+
+static const char *
+choose_protocol (const char *client_protocols_str,
+                const char **server_protocols)
+{
+       char **client_protocols;
+       int i, j;
+
+       if (!client_protocols_str)
+               return NULL;
+       client_protocols = g_strsplit_set (client_protocols_str, ", ", -1);
+       if (!client_protocols || !client_protocols[0]) {
+               g_strfreev (client_protocols);
+               return NULL;
+       }
+
+       for (i = 0; server_protocols[i] != NULL; i++) {
+               for (j = 0; client_protocols[j] != NULL; j++) {
+                       if (g_str_equal (server_protocols[i], client_protocols[j])) {
+                               g_debug ("agreed on protocol: %s", server_protocols[i]);
+                               g_strfreev (client_protocols);
+                               return server_protocols[i];
+                       }
+               }
+       }
+
+       g_debug ("Unable to find a matching protocol");
+       g_strfreev (client_protocols);
+       return NULL;
+}
+
+gboolean
+soup_websocket_server_process_handshake (SoupMessage  *msg,
+                                        const char   *expected_origin,
+                                        char        **protocols)
+{
+       const char *client_protocols, *chosen_protocol = NULL;
+       const char *origin, *extensions;
+       char *accept_key;
+       const char *key;
+
+       if (msg->method != SOUP_METHOD_GET)
+               return respond_handshake_bad (msg, "method was not GET");
+
+       if (!soup_message_headers_header_equals (msg->request_headers, "Upgrade", "websocket") ||
+           !soup_message_headers_header_contains (msg->request_headers, "Connection", "upgrade"))
+               return respond_handshake_bad (msg, "not a WebSocket request");
+
+       if (!soup_message_headers_header_equals (msg->request_headers, "Sec-WebSocket-Version", "13"))
+               return respond_handshake_bad (msg, "bad WebSocket version");
+
+       extensions = soup_message_headers_get_list (msg->request_headers, "Sec-WebSocket-Extensions");
+       if (extensions && *extensions)
+               return respond_handshake_bad (msg, "unsupported extension");
+
+       key = soup_message_headers_get_one (msg->request_headers, "Sec-WebSocket-Key");
+       if (key == NULL || !validate_websocket_key (key))
+               return respond_handshake_bad (msg, "bad key");
+
+       if (expected_origin) {
+               origin = soup_message_headers_get_one (msg->request_headers, "Origin");
+               if (!origin)
+                       return respond_handshake_forbidden (msg);
+               if (g_ascii_strcasecmp (origin, expected_origin) != 0)
+                       return respond_handshake_forbidden (msg);
+       }
+
+       client_protocols = soup_message_headers_get_one (msg->request_headers, "Sec-Websocket-Protocol");
+       if (protocols && protocols[0] && client_protocols) {
+               chosen_protocol = choose_protocol (client_protocols, (const char **) protocols);
+               if (!chosen_protocol)
+                       return respond_handshake_bad (msg, "unsupported protocol");
+       }
+
+       soup_message_set_status (msg, SOUP_STATUS_SWITCHING_PROTOCOLS);
+       soup_message_headers_replace (msg->response_headers, "Upgrade", "websocket");
+       soup_message_headers_append (msg->response_headers, "Connection", "Upgrade");
+
+       accept_key = soup_websocket_get_accept_key (key);
+       soup_message_headers_append (msg->response_headers, "Sec-WebSocket-Accept", accept_key);
+       g_free (accept_key);
+
+       if (chosen_protocol)
+               soup_message_headers_append (msg->response_headers, "Sec-WebSocket-Protocol", 
chosen_protocol);
+
+       return TRUE;
+}
diff --git a/libsoup/soup-websocket-server.h b/libsoup/soup-websocket-server.h
new file mode 100644
index 0000000..ea7fb9a
--- /dev/null
+++ b/libsoup/soup-websocket-server.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * soup-websocket-server.h: This file was originally part of Cockpit.
+ *
+ * Copyright 2013, 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __SOUP_WEBSOCKET_SERVER_H__
+#define __SOUP_WEBSOCKET_SERVER_H__
+
+#include <gio/gio.h>
+
+#include <libsoup/soup-websocket-connection.h>
+#include <libsoup/soup-message-headers.h>
+
+G_BEGIN_DECLS
+
+#define SOUP_TYPE_WEBSOCKET_SERVER         (soup_websocket_server_get_type ())
+#define SOUP_WEBSOCKET_SERVER(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), SOUP_TYPE_WEBSOCKET_SERVER, 
SoupWebsocketServer))
+#define SOUP_IS_WEBSOCKET_SERVER(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), SOUP_TYPE_WEBSOCKET_SERVER))
+#define SOUP_WEBSOCKET_SERVER_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), SOUP_TYPE_WEBSOCKET_SERVER, 
SoupWebsocketServerClass))
+#define SOUP_IS_WEBSOCKET_SERVER_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), SOUP_TYPE_WEBSOCKET_SERVER))
+
+GType soup_websocket_server_get_type (void) G_GNUC_CONST;
+
+gboolean soup_websocket_server_process_handshake (SoupMessage  *msg,
+                                                 const char   *expected_origin,
+                                                 char        **protocols);
+
+SoupWebsocketConnection *soup_websocket_server_new (SoupMessage  *msg,
+                                                   GIOStream    *stream);
+
+G_END_DECLS
+
+#endif /* __SOUP_WEBSOCKET_SERVER_H__ */
diff --git a/libsoup/soup-websocket.c b/libsoup/soup-websocket.c
new file mode 100644
index 0000000..3dce5a5
--- /dev/null
+++ b/libsoup/soup-websocket.c
@@ -0,0 +1,87 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * soup-websocket.c: This file was originally part of Cockpit.
+ *
+ * Copyright 2013, 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "soup-websocket.h"
+#include "soup-websocket-private.h"
+
+/**
+ * SoupWebsocketState:
+ * @SOUP_WEBSOCKET_STATE_CONNECTING: the WebSocket is not yet ready to send messages
+ * @SOUP_WEBSOCKET_STATE_OPEN: the Websocket is ready to send messages
+ * @SOUP_WEBSOCKET_STATE_CLOSING: the Websocket is in the process of closing down, no further messages sent
+ * @SOUP_WEBSOCKET_STATE_CLOSED: the Websocket is completely closed down
+ *
+ * The WebSocket is in the %SOUP_WEBSOCKET_STATE_CONNECTING state during initial
+ * connection setup, and handshaking. If the handshake or connection fails it
+ * can go directly to the %SOUP_WEBSOCKET_STATE_CLOSED state from here.
+ *
+ * Once the WebSocket handshake completes successfully it will be in the
+ * %SOUP_WEBSOCKET_STATE_OPEN state. During this state, and only during this state
+ * can WebSocket messages be sent.
+ *
+ * WebSocket messages can be received during either the %SOUP_WEBSOCKET_STATE_OPEN
+ * or %SOUP_WEBSOCKET_STATE_CLOSING states.
+ *
+ * The WebSocket goes into the %SOUP_WEBSOCKET_STATE_CLOSING state once it has
+ * successfully sent a close request to the peer. If we had not yet received
+ * an earlier close request from the peer, then the WebSocket waits for a
+ * response to the close request (until a timeout).
+ *
+ * Once actually closed completely down the WebSocket state is
+ * %SOUP_WEBSOCKET_STATE_CLOSED. No communication is possible during this state.
+ *
+ * Since: 2.50
+ */
+
+GQuark
+soup_websocket_error_get_quark (void)
+{
+       return g_quark_from_static_string ("web-socket-error-quark");
+}
+
+char *
+soup_websocket_get_accept_key (const char *key)
+{
+       gsize digest_len = 20;
+       guchar digest[digest_len];
+       GChecksum *checksum;
+
+       g_return_val_if_fail (key != NULL, NULL);
+
+       checksum = g_checksum_new (G_CHECKSUM_SHA1);
+       g_return_val_if_fail (checksum != NULL, NULL);
+
+       g_checksum_update (checksum, (guchar *)key, -1);
+
+       /* magic from: http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17 */
+       g_checksum_update (checksum, (guchar *)"258EAFA5-E914-47DA-95CA-C5AB0DC85B11", -1);
+
+       g_checksum_get_digest (checksum, digest, &digest_len);
+       g_checksum_free (checksum);
+
+       g_assert (digest_len == 20);
+
+       return g_base64_encode (digest, digest_len);
+}
diff --git a/libsoup/soup-websocket.h b/libsoup/soup-websocket.h
new file mode 100644
index 0000000..bf0e98f
--- /dev/null
+++ b/libsoup/soup-websocket.h
@@ -0,0 +1,66 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * soup-websocket.h: This file was originally part of Cockpit.
+ *
+ * Copyright 2013, 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __SOUP_WEBSOCKET_H__
+#define __SOUP_WEBSOCKET_H__
+
+#include <libsoup/soup-types.h>
+
+G_BEGIN_DECLS
+
+#define SOUP_WEBSOCKET_ERROR (soup_websocket_error_get_quark ())
+SOUP_AVAILABLE_IN_2_50
+GQuark soup_websocket_error_get_quark (void) G_GNUC_CONST;
+
+typedef enum {
+       SOUP_WEBSOCKET_ERROR_FAILED,
+       SOUP_WEBSOCKET_ERROR_NOT_WEBSOCKET,
+       SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE
+} SoupWebsocketError;
+
+typedef enum {
+       SOUP_WEBSOCKET_DATA_TEXT = 0x01,
+       SOUP_WEBSOCKET_DATA_BINARY = 0x02,
+} SoupWebsocketDataType;
+
+typedef enum {
+       SOUP_WEBSOCKET_CLOSE_NORMAL = 1000,
+       SOUP_WEBSOCKET_CLOSE_GOING_AWAY = 1001,
+       SOUP_WEBSOCKET_CLOSE_NO_STATUS = 1005,
+       SOUP_WEBSOCKET_CLOSE_ABNORMAL = 1006,
+       SOUP_WEBSOCKET_CLOSE_PROTOCOL = 1002,
+       SOUP_WEBSOCKET_CLOSE_UNSUPPORTED_DATA = 1003,
+       SOUP_WEBSOCKET_CLOSE_BAD_DATA = 1007,
+       SOUP_WEBSOCKET_CLOSE_POLICY_VIOLATION = 1008,
+       SOUP_WEBSOCKET_CLOSE_TOO_BIG = 1009,
+       SOUP_WEBSOCKET_CLOSE_NO_EXTENSION = 1010,
+       SOUP_WEBSOCKET_CLOSE_SERVER_ERROR = 1011,
+       SOUP_WEBSOCKET_CLOSE_TLS_HANDSHAKE = 1015,
+} SoupWebsocketCloseCode;
+
+typedef enum {
+       SOUP_WEBSOCKET_STATE_OPEN = 1,
+       SOUP_WEBSOCKET_STATE_CLOSING = 2,
+       SOUP_WEBSOCKET_STATE_CLOSED = 3,
+} SoupWebsocketState;
+
+G_END_DECLS
+
+#endif /* __SOUP_WEBSOCKET_H__ */
diff --git a/libsoup/soup.h b/libsoup/soup.h
index 82a2632..7106cc5 100644
--- a/libsoup/soup.h
+++ b/libsoup/soup.h
@@ -50,6 +50,8 @@ extern "C" {
 #include <libsoup/soup-uri.h>
 #include <libsoup/soup-value-utils.h>
 #include <libsoup/soup-version.h>
+#include <libsoup/soup-websocket.h>
+#include <libsoup/soup-websocket-connection.h>
 #include <libsoup/soup-xmlrpc.h>
 
 #ifdef __cplusplus
diff --git a/po/POTFILES.in b/po/POTFILES.in
index ece9e95..92609bd 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -8,4 +8,5 @@ libsoup/soup-request.c
 libsoup/soup-server.c
 libsoup/soup-session.c
 libsoup/soup-socket.c
+libsoup/soup-websocket-client.c
 libsoup/soup-tld.c
diff --git a/tests/Makefile.am b/tests/Makefile.am
index a8b9d01..662ab79 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -41,6 +41,7 @@ test_programs =                       \
        timeout-test            \
        tld-test                \
        uri-parsing             \
+       websocket-test          \
        xmlrpc-server-test      \
        xmlrpc-test
 
diff --git a/tests/header-parsing.c b/tests/header-parsing.c
index fdc7885..7bea1e9 100644
--- a/tests/header-parsing.c
+++ b/tests/header-parsing.c
@@ -608,6 +608,30 @@ static struct ResponseTest {
            { NULL } }
        },
 
+       /********************************/
+       /*** VALID CONTINUE RESPONSES ***/
+       /********************************/
+
+       /* Tests from Cockpit project */
+
+       { "Response w/ 101 Switching Protocols + spaces after new line", NULL,
+         "HTTP/1.0 101 Switching Protocols\r\n  \r\n", 38,
+         SOUP_HTTP_1_0, SOUP_STATUS_SWITCHING_PROTOCOLS, "Switching Protocols",
+         { { NULL } }
+       },
+
+       { "Response w/ 101 Switching Protocols missing \\r + spaces", NULL,
+         "HTTP/1.0  101  Switching Protocols\r\n  \r\n", 40,
+         SOUP_HTTP_1_0, SOUP_STATUS_SWITCHING_PROTOCOLS, "Switching Protocols",
+         { { NULL } }
+       },
+
+       { "Response w/ 101 Switching Protocols + spaces after & before new line", NULL,
+         "HTTP/1.1  101  Switching Protocols  \r\n  \r\n", 42,
+         SOUP_HTTP_1_1, SOUP_STATUS_SWITCHING_PROTOCOLS, "Switching Protocols",
+         { { NULL } }
+       },
+
        /*************************/
        /*** INVALID RESPONSES ***/
        /*************************/
@@ -689,6 +713,45 @@ static struct ResponseTest {
          -1, 0, NULL,
          { { NULL } }
        },
+
+       /* Failing test from Cockpit */
+
+       { "Partial response stops after HTTP/", NULL,
+         "HTTP/", -1,
+         -1, 0, NULL,
+         { { NULL } }
+       },
+
+       { "Space before HTTP/", NULL,
+         " HTTP/1.0 101 Switching Protocols\r\n  ", -1,
+         -1, 0, NULL,
+         { { NULL } }
+       },
+
+       { "Missing reason", NULL,
+         "HTTP/1.0  101\r\n  ", -1,
+         -1, 0, NULL,
+         { { NULL } }
+       },
+
+       { "Response code containing alphabetic character", NULL,
+         "HTTP/1.1  1A01  Switching Protocols  \r\n  ", -1,
+         -1, 0, NULL,
+         { { NULL } }
+       },
+
+       { "TESTONE\\r\\n", NULL,
+         "TESTONE\r\n  ", -1,
+         -1, 0, NULL,
+         { { NULL } }
+       },
+
+       { "Response w/ 3 headers truncated", NULL,
+         "HTTP/1.0 200 ok\r\nHeader1: value3\r\nHeader2:  field\r\nHead3:  Anothe", -1,
+         -1, 0, NULL,
+         { { NULL }
+         }
+       },
 };
 static const int num_resptests = G_N_ELEMENTS (resptests);
 
diff --git a/tests/websocket-test.c b/tests/websocket-test.c
new file mode 100644
index 0000000..75628da
--- /dev/null
+++ b/tests/websocket-test.c
@@ -0,0 +1,709 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * This file was originally part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "test-utils.h"
+
+#include <sys/socket.h>
+
+/* Hack, to get soup_websocket_get_accept_key() */
+#include "../libsoup/soup-websocket.c"
+
+typedef struct {
+       GSocket *listener;
+       gushort port;
+
+       SoupSession *session;
+       SoupWebsocketConnection *client;
+       SoupMessage *msg;
+       GError *client_error;
+
+       SoupServer *soup_server;
+       SoupWebsocketConnection *server;
+
+       GMutex mutex;
+} Test;
+
+#define WAIT_UNTIL(cond)                                       \
+       G_STMT_START                                            \
+       while (!(cond)) g_main_context_iteration (NULL, TRUE);  \
+       G_STMT_END
+
+static void
+on_error_not_reached (SoupWebsocketConnection *ws,
+                      GError *error,
+                      gpointer user_data)
+{
+       /* At this point we know this will fail, but is informative */
+       g_assert_no_error (error);
+}
+
+static void
+on_error_copy (SoupWebsocketConnection *ws,
+               GError *error,
+               gpointer user_data)
+{
+       GError **copy = user_data;
+       g_assert (*copy == NULL);
+       *copy = g_error_copy (error);
+}
+
+static void
+setup (Test *test,
+       gconstpointer data)
+{
+       GSocketAddress *addr;
+       GError *error = NULL;
+
+       test->listener = g_socket_new (G_SOCKET_FAMILY_IPV4,
+                                      G_SOCKET_TYPE_STREAM,
+                                      G_SOCKET_PROTOCOL_TCP,
+                                      &error);
+       g_assert_no_error (error);
+
+       addr = g_inet_socket_address_new_from_string ("127.0.0.1", 0);
+       g_assert_no_error (error);
+
+       g_socket_bind (test->listener, addr, TRUE, &error);
+       g_assert_no_error (error);
+       g_object_unref (addr);
+
+       addr = g_socket_get_local_address (test->listener, &error);
+       g_assert_no_error (error);
+
+       test->port = g_inet_socket_address_get_port (G_INET_SOCKET_ADDRESS (addr));
+       g_object_unref (addr);
+
+       g_socket_listen (test->listener, &error);
+       g_assert_no_error (error);
+}
+
+static void
+teardown (Test *test,
+          gconstpointer data)
+{
+       g_clear_object (&test->listener);
+       g_clear_object (&test->client);
+       g_clear_object (&test->msg);
+       g_clear_object (&test->server);
+       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
+setup_soup_server (Test *test,
+                  const char *origin,
+                  const char **protocols,
+                  SoupServerWebsocketCallback callback,
+                  gpointer user_data)
+{
+       GError *error = NULL;
+
+       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
+on_text_message (SoupWebsocketConnection *ws,
+                 SoupWebsocketDataType type,
+                 GBytes *message,
+                 gpointer user_data)
+{
+       GBytes **receive = user_data;
+       g_assert_cmpint (type, ==, SOUP_WEBSOCKET_DATA_TEXT);
+       g_assert (*receive == NULL);
+       g_assert (message != NULL);
+       *receive = g_bytes_ref (message);
+}
+
+static void
+on_close_set_flag (SoupWebsocketConnection *ws,
+                   gpointer user_data)
+{
+       gboolean *flag = user_data;
+       g_assert (*flag == FALSE);
+       *flag = TRUE;
+}
+
+
+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_simple (Test *test)
+{
+       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
+test_handshake (Test *test,
+                gconstpointer data)
+{
+       setup_simple (test);
+
+       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
+test_send_client_to_server (Test *test,
+                            gconstpointer data)
+{
+       GBytes *received = NULL;
+       const char *contents;
+       gsize len;
+
+       setup_simple (test);
+
+       g_signal_connect (test->server, "message", G_CALLBACK (on_text_message), &received);
+
+       soup_websocket_connection_send (test->client, SOUP_WEBSOCKET_DATA_TEXT,
+                                       TEST_STRING, -1);
+
+       WAIT_UNTIL (received != NULL);
+
+       /* Received messages should be null terminated (outside of len) */
+       contents = g_bytes_get_data (received, &len);
+       g_assert_cmpstr (contents, ==, TEST_STRING);
+       g_assert_cmpint (len, ==, strlen (TEST_STRING));
+
+       g_bytes_unref (received);
+}
+
+static void
+test_send_server_to_client (Test *test,
+                            gconstpointer data)
+{
+       GBytes *received = NULL;
+       const char *contents;
+       gsize len;
+
+       setup_simple (test);
+
+       g_signal_connect (test->client, "message", G_CALLBACK (on_text_message), &received);
+
+       soup_websocket_connection_send (test->server, SOUP_WEBSOCKET_DATA_TEXT,
+                                       TEST_STRING, -1);
+
+       WAIT_UNTIL (received != NULL);
+
+       /* Received messages should be null terminated (outside of len) */
+       contents = g_bytes_get_data (received, &len);
+       g_assert_cmpstr (contents, ==, TEST_STRING);
+       g_assert_cmpint (len, ==, strlen (TEST_STRING));
+
+       g_bytes_unref (received);
+}
+
+static void
+test_send_big_packets (Test *test,
+                       gconstpointer data)
+{
+       GBytes *sent = NULL;
+       GBytes *received = NULL;
+
+       setup_simple (test);
+
+       g_signal_connect (test->client, "message", G_CALLBACK (on_text_message), &received);
+
+       sent = g_bytes_new_take (g_strnfill (400, '!'), 400);
+       soup_websocket_connection_send (test->server, SOUP_WEBSOCKET_DATA_TEXT,
+                                       g_bytes_get_data (sent, NULL),
+                                       g_bytes_get_size (sent));
+       WAIT_UNTIL (received != NULL);
+       g_assert (g_bytes_equal (sent, received));
+       g_bytes_unref (sent);
+       g_bytes_unref (received);
+       received = NULL;
+
+       sent = g_bytes_new_take (g_strnfill (100 * 1000, '?'), 100 * 1000);
+       soup_websocket_connection_send (test->server, SOUP_WEBSOCKET_DATA_TEXT,
+                                       g_bytes_get_data (sent, NULL),
+                                       g_bytes_get_size (sent));
+       WAIT_UNTIL (received != NULL);
+       g_assert (g_bytes_equal (sent, received));
+       g_bytes_unref (sent);
+       g_bytes_unref (received);
+}
+
+static void
+test_send_bad_data (Test *test,
+                    gconstpointer unused)
+{
+       GError *error = NULL;
+       GIOStream *io;
+       gsize written;
+       const char *frame;
+
+       setup_simple (test);
+
+       g_signal_handlers_disconnect_by_func (test->server, on_error_not_reached, NULL);
+       g_signal_connect (test->server, "error", G_CALLBACK (on_error_copy), &error);
+
+       io = soup_websocket_connection_get_io_stream (test->client);
+
+       /* Bad UTF-8 frame */
+       frame = "\x81\x04\xEE\xEE\xEE\xEE";
+       if (!g_output_stream_write_all (g_io_stream_get_output_stream (io),
+                                       frame, 6, &written, NULL, NULL))
+               g_assert_not_reached ();
+       g_assert_cmpuint (written, ==, 6);
+
+       WAIT_UNTIL (error != NULL);
+       g_assert_error (error, SOUP_WEBSOCKET_ERROR, SOUP_WEBSOCKET_CLOSE_BAD_DATA);
+
+       WAIT_UNTIL (soup_websocket_connection_get_state (test->client) == SOUP_WEBSOCKET_STATE_CLOSED);
+
+       g_assert_cmpuint (soup_websocket_connection_get_close_code (test->client), ==, 
SOUP_WEBSOCKET_CLOSE_BAD_DATA);
+
+       g_error_free (error);
+}
+
+static void
+test_protocol_negotiate (Test *test,
+                         gconstpointer unused)
+{
+       const char *server_protocols[] = { "aaa", "bbb", "ccc", NULL };
+       const char *client_protocols[] = { "bbb", "ccc", NULL };
+
+       setup_soup_server (test, NULL, server_protocols, got_server_connection, test);
+       client_connect (test, NULL, 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), ==, "bbb");
+       g_assert_cmpstr (soup_websocket_connection_get_protocol (test->server), ==, "bbb");
+}
+
+static void
+test_protocol_mismatch (Test *test,
+                        gconstpointer unused)
+{
+       const char *server_protocols[] = { "aaa", "bbb", "ccc", NULL };
+       const char *client_protocols[] = { "ddd", NULL };
+
+       setup_soup_server (test, NULL, server_protocols, got_server_connection, test);
+       client_connect (test, NULL, 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 void
+test_protocol_server_any (Test *test,
+                          gconstpointer unused)
+{
+       const char *client_protocols[] = { "aaa", "bbb", "ccc", NULL };
+
+       setup_soup_server (test, NULL, NULL, got_server_connection, test);
+       client_connect (test, NULL, 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), ==, 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 (Test *test,
+                          gconstpointer unused)
+{
+       const char *server_protocols[] = { "aaa", "bbb", "ccc", NULL };
+
+       setup_soup_server (test, NULL, server_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)
+{
+       gboolean close_event_client = FALSE;
+       gboolean close_event_server = FALSE;
+
+       setup_simple (test);
+
+       g_signal_connect (test->client, "close", G_CALLBACK (on_close_set_flag), &close_event_client);
+       g_signal_connect (test->server, "close", G_CALLBACK (on_close_set_flag), &close_event_server);
+
+       soup_websocket_connection_close (test->client, SOUP_WEBSOCKET_CLOSE_GOING_AWAY, "give me a reason");
+       g_assert_cmpint (soup_websocket_connection_get_state (test->client), ==, 
SOUP_WEBSOCKET_STATE_CLOSING);
+
+       WAIT_UNTIL (soup_websocket_connection_get_state (test->server) == SOUP_WEBSOCKET_STATE_CLOSED);
+       WAIT_UNTIL (soup_websocket_connection_get_state (test->client) == SOUP_WEBSOCKET_STATE_CLOSED);
+
+       g_assert (close_event_client);
+       g_assert (close_event_server);
+
+       g_assert_cmpint (soup_websocket_connection_get_close_code (test->client), ==, 
SOUP_WEBSOCKET_CLOSE_GOING_AWAY);
+       g_assert_cmpint (soup_websocket_connection_get_close_code (test->server), ==, 
SOUP_WEBSOCKET_CLOSE_GOING_AWAY);
+       g_assert_cmpstr (soup_websocket_connection_get_close_data (test->server), ==, "give me a reason");
+}
+
+static void
+test_close_clean_server (Test *test,
+                         gconstpointer data)
+{
+       gboolean close_event_client = FALSE;
+       gboolean close_event_server = FALSE;
+
+       setup_simple (test);
+
+       g_signal_connect (test->client, "close", G_CALLBACK (on_close_set_flag), &close_event_client);
+       g_signal_connect (test->server, "close", G_CALLBACK (on_close_set_flag), &close_event_server);
+
+       soup_websocket_connection_close (test->server, SOUP_WEBSOCKET_CLOSE_GOING_AWAY, "another reason");
+       g_assert_cmpint (soup_websocket_connection_get_state (test->server), ==, 
SOUP_WEBSOCKET_STATE_CLOSING);
+
+       WAIT_UNTIL (soup_websocket_connection_get_state (test->server) == SOUP_WEBSOCKET_STATE_CLOSED);
+       WAIT_UNTIL (soup_websocket_connection_get_state (test->client) == SOUP_WEBSOCKET_STATE_CLOSED);
+
+       g_assert (close_event_client);
+       g_assert (close_event_server);
+
+       g_assert_cmpint (soup_websocket_connection_get_close_code (test->server), ==, 
SOUP_WEBSOCKET_CLOSE_GOING_AWAY);
+       g_assert_cmpint (soup_websocket_connection_get_close_code (test->client), ==, 
SOUP_WEBSOCKET_CLOSE_GOING_AWAY);
+       g_assert_cmpstr (soup_websocket_connection_get_close_data (test->client), ==, "another reason");
+}
+
+static gboolean
+on_closing_send_message (SoupWebsocketConnection *ws,
+                         gpointer data)
+{
+       GBytes *message = data;
+
+       soup_websocket_connection_send (ws, SOUP_WEBSOCKET_DATA_TEXT,
+                                       g_bytes_get_data (message, NULL),
+                                       g_bytes_get_size (message));
+       g_signal_handlers_disconnect_by_func (ws, on_closing_send_message, data);
+       return TRUE;
+}
+
+static void
+test_message_after_closing (Test *test,
+                            gconstpointer data)
+{
+       gboolean close_event_client = FALSE;
+       gboolean close_event_server = FALSE;
+       GBytes *received = NULL;
+       GBytes *message;
+
+       setup_simple (test);
+
+       message = g_bytes_new ("another test because", 20);
+       g_signal_connect (test->client, "close", G_CALLBACK (on_close_set_flag), &close_event_client);
+       g_signal_connect (test->client, "message", G_CALLBACK (on_text_message), &received);
+       g_signal_connect (test->server, "close", G_CALLBACK (on_close_set_flag), &close_event_server);
+       g_signal_connect (test->server, "closing", G_CALLBACK (on_closing_send_message), message);
+
+       soup_websocket_connection_close (test->client, SOUP_WEBSOCKET_CLOSE_GOING_AWAY, "another reason");
+       g_assert_cmpint (soup_websocket_connection_get_state (test->client), ==, 
SOUP_WEBSOCKET_STATE_CLOSING);
+
+       WAIT_UNTIL (soup_websocket_connection_get_state (test->server) == SOUP_WEBSOCKET_STATE_CLOSED);
+       WAIT_UNTIL (soup_websocket_connection_get_state (test->client) == SOUP_WEBSOCKET_STATE_CLOSED);
+
+       g_assert (close_event_client);
+       g_assert (close_event_server);
+
+       g_assert (received != NULL);
+       g_assert (g_bytes_equal (message, received));
+
+       g_bytes_unref (received);
+       g_bytes_unref (message);
+}
+
+static GIOStream *
+mock_accept (GSocket *listener)
+{
+       GSocket *sock;
+       GSocketConnection *conn;
+       GError *error = NULL;
+
+       sock = g_socket_accept (listener, NULL, &error);
+       g_assert_no_error (error);
+
+       conn = g_socket_connection_factory_create_connection (sock);
+       g_assert (conn != NULL);
+       g_object_unref (sock);
+
+       return G_IO_STREAM (conn);
+}
+
+static void
+mock_perform_handshake (GIOStream *io)
+{
+       SoupMessageHeaders *headers;
+       char buffer[1024], *headers_end;
+       gssize count;
+       const char *key;
+       char *accept;
+       gsize written;
+
+       /* Assumes client codes sends headers as a single write() */
+       count = g_input_stream_read (g_io_stream_get_input_stream (io),
+                                    buffer, sizeof (buffer), NULL, NULL);
+       g_assert (count > 0);
+
+       /* Parse the incoming request */
+       headers_end = g_strstr_len (buffer, sizeof (buffer), "\n\r\n");
+       g_assert (headers_end != NULL);
+       headers_end += 3;
+
+       headers = soup_message_headers_new (SOUP_MESSAGE_HEADERS_REQUEST);
+       g_assert (soup_headers_parse_request (buffer, headers_end - buffer, headers, NULL, NULL, NULL));
+       g_assert_cmpuint (headers_end - buffer, <, sizeof (buffer));
+
+       key = soup_message_headers_get_one (headers, "Sec-WebSocket-Key");
+       accept = soup_websocket_get_accept_key (key);
+
+       count = g_snprintf (buffer, sizeof (buffer),
+                           "HTTP/1.1 101 Switching Protocols\r\n"
+                           "Upgrade: websocket\r\n"
+                           "Connection: Upgrade\r\n"
+                           "Sec-WebSocket-Accept: %s\r\n"
+                           "\r\n", accept);
+       g_free (accept);
+
+       if (!g_output_stream_write_all (g_io_stream_get_output_stream (io),
+                                       buffer, count, &written, NULL, NULL))
+               g_assert_not_reached ();
+       g_assert_cmpuint (count, ==, written);
+
+       soup_message_headers_free (headers);
+}
+
+static gpointer
+handshake_then_timeout_server_thread (gpointer user_data)
+{
+       Test *test = user_data;
+       GIOStream *io;
+
+       io = mock_accept (test->listener);
+       mock_perform_handshake (io);
+
+       /* don't close until the client has timed out */
+       g_mutex_lock (&test->mutex);
+       g_mutex_unlock (&test->mutex);
+
+       g_object_unref (io);
+       return NULL;
+}
+
+static void
+test_close_after_timeout (Test *test,
+                         gconstpointer data)
+{
+       gboolean close_event = FALSE;
+       GThread *thread;
+
+       g_mutex_lock (&test->mutex);
+
+       /* Note that no server is around in this test, so no close happens */
+       thread = g_thread_new ("timeout-thread", handshake_then_timeout_server_thread, test);
+
+       client_connect (test, NULL, NULL, got_client_connection, test);
+       WAIT_UNTIL (test->client != NULL || test->client_error != NULL);
+       g_assert_no_error (test->client_error);
+
+       g_signal_connect (test->client, "close", G_CALLBACK (on_close_set_flag), &close_event);
+       g_signal_connect (test->client, "error", G_CALLBACK (on_error_not_reached), NULL);
+       WAIT_UNTIL (soup_websocket_connection_get_state (test->client) == SOUP_WEBSOCKET_STATE_OPEN);
+
+       /* Now try and close things */
+       soup_websocket_connection_close (test->client, 0, NULL);
+       g_assert_cmpint (soup_websocket_connection_get_state (test->client), ==, 
SOUP_WEBSOCKET_STATE_CLOSING);
+
+       WAIT_UNTIL (soup_websocket_connection_get_state (test->client) == SOUP_WEBSOCKET_STATE_CLOSED);
+
+       g_assert (close_event == TRUE);
+
+       /* Now actually close the server side stream */
+       g_mutex_unlock (&test->mutex);
+       g_thread_join (thread);
+}
+
+static gpointer
+send_fragments_server_thread (gpointer user_data)
+{
+       Test *test = user_data;
+       GIOStream *io;
+       gsize written;
+
+       const char fragments[] = "\x01\x04""one "   /* !fin | opcode */
+               "\x00\x04""two "   /* !fin | no opcode */
+               "\x80\x05""three"; /* fin  | no opcode */
+
+       io = mock_accept (test->listener);
+       mock_perform_handshake (io);
+
+       /* synchronize... */
+       g_mutex_lock (&test->mutex);
+       g_mutex_unlock (&test->mutex);
+
+       if (!g_output_stream_write_all (g_io_stream_get_output_stream (io),
+                                       fragments, sizeof (fragments) -1, &written, NULL, NULL))
+               g_assert_not_reached ();
+       g_assert_cmpuint (written, ==, sizeof (fragments) - 1);
+       g_object_unref (io);
+
+       return NULL;
+}
+
+static void
+test_receive_fragmented (Test *test,
+                        gconstpointer data)
+{
+       GThread *thread;
+       GBytes *received = NULL;
+       GBytes *expect;
+
+       g_mutex_lock (&test->mutex);
+
+       /* Note that no server is around in this test, so no close happens */
+       thread = g_thread_new ("fragment-thread", send_fragments_server_thread, test);
+
+       client_connect (test, NULL, NULL, got_client_connection, test);
+       WAIT_UNTIL (test->client != NULL || test->client_error != NULL);
+       g_assert_no_error (test->client_error);
+
+       g_mutex_unlock (&test->mutex);
+
+       g_signal_connect (test->client, "error", G_CALLBACK (on_error_not_reached), NULL);
+       g_signal_connect (test->client, "message", G_CALLBACK (on_text_message), &received);
+
+       WAIT_UNTIL (received != NULL);
+       expect = g_bytes_new ("one two three", 13);
+       g_assert (g_bytes_equal (expect, received));
+       g_bytes_unref (expect);
+       g_bytes_unref (received);
+
+       g_thread_join (thread);
+}
+
+int
+main (int argc,
+      char *argv[])
+{
+       int ret;
+
+       test_init (argc, argv, NULL);
+
+       g_test_add ("/websocket/handshake", Test, NULL, setup,
+                   test_handshake, teardown);
+       g_test_add ("/websocket/send-client-to-server", Test, NULL, setup,
+                   test_send_client_to_server, teardown);
+       g_test_add ("/websocket/send-server-to-client", Test, NULL, setup,
+                   test_send_server_to_client, teardown);
+       g_test_add ("/websocket/send-big-packets", Test, NULL, setup,
+                   test_send_big_packets, teardown);
+       g_test_add ("/websocket/send-bad-data", Test, NULL, setup,
+                   test_send_bad_data, teardown);
+       g_test_add ("/websocket/protocol-negotiate", Test, NULL, setup,
+                   test_protocol_negotiate, teardown);
+       g_test_add ("/websocket/protocol-mismatch", Test, NULL, setup,
+                   test_protocol_mismatch, teardown);
+       g_test_add ("/websocket/protocol-server-any", Test, NULL, setup,
+                   test_protocol_server_any, teardown);
+       g_test_add ("/websocket/protocol-client-any", Test, NULL, setup,
+                   test_protocol_client_any, teardown);
+       g_test_add ("/websocket/close-clean-client", Test, NULL, setup,
+                   test_close_clean_client, teardown);
+       g_test_add ("/websocket/close-clean-server", Test, NULL, setup,
+                   test_close_clean_server, teardown);
+       g_test_add ("/websocket/receive-fragmented", Test, NULL, setup,
+                   test_receive_fragmented, teardown);
+       g_test_add ("/websocket/message-after-closing", Test, NULL, setup,
+                   test_message_after_closing, teardown);
+
+       if (g_test_slow ()) {
+               g_test_add ("/websocket/close-after-timeout", Test, NULL, setup,
+                           test_close_after_timeout, teardown);
+       }
+
+       ret = g_test_run ();
+
+       test_cleanup ();
+       return ret;
+}


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