Commit dfc36f5f authored by Dan Winship's avatar Dan Winship
Browse files

websockets: add WebSocket support

Add functions to create and parse WebSocket handshakes, and to
communicate on a WebSocket connection.

Based on code originally from the Cockpit project, and on earlier work
by Lionel Landwerlin to merge that into libsoup.

https://bugzilla.gnome.org/show_bug.cgi?id=627738
parent cbe988d9
......@@ -63,6 +63,7 @@
<xi:include href="xml/soup-form.xml"/>
<xi:include href="xml/soup-xmlrpc.xml"/>
<xi:include href="xml/soup-value-utils.xml"/>
<xi:include href="xml/soup-websocket.xml"/>
</chapter>
<chapter>
......
......@@ -1300,3 +1300,51 @@ SOUP_ENCODE_VERSION
SOUP_VERSION_CUR_STABLE
SOUP_VERSION_PREV_STABLE
</SECTION>
<SECTION>
<FILE>soup-websocket</FILE>
<TITLE>WebSockets</TITLE>
<SUBSECTION>
soup_websocket_client_prepare_handshake
soup_websocket_client_verify_handshake
<SUBSECTION>
soup_websocket_server_check_handshake
soup_websocket_server_process_handshake
<SUBSECTION>
SoupWebsocketConnection
SoupWebsocketConnectionType
soup_websocket_connection_new
soup_websocket_connection_get_io_stream
soup_websocket_connection_get_connection_type
soup_websocket_connection_get_uri
soup_websocket_connection_get_origin
soup_websocket_connection_get_protocol
SoupWebsocketState
soup_websocket_connection_get_state
SoupWebsocketDataType
soup_websocket_connection_send_text
soup_websocket_connection_send_binary
SoupWebsocketCloseCode
soup_websocket_connection_close
soup_websocket_connection_get_close_code
soup_websocket_connection_get_close_data
<SUBSECTION>
SoupWebsocketError
SOUP_WEBSOCKET_ERROR
<SUBSECTION Private>
SoupWebsocketConnectionClass
SoupWebsocketConnectionPrivate
SOUP_IS_WEBSOCKET_CONNECTION
SOUP_IS_WEBSOCKET_CONNECTION_CLASS
SOUP_TYPE_WEBSOCKET_CONNECTION
SOUP_WEBSOCKET_CONNECTION
SOUP_WEBSOCKET_CONNECTION_CLASS
SOUP_WEBSOCKET_CONNECTION_GET_CLASS
soup_websocket_close_code_get_type
soup_websocket_connection_get_type
soup_websocket_connection_type_get_type
soup_websocket_data_type_get_type
soup_websocket_error_get_quark
soup_websocket_error_get_type
soup_websocket_state_get_type
</SECTION>
......@@ -68,6 +68,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 = \
......@@ -186,6 +188,8 @@ libsoup_2_4_la_SOURCES = \
soup-uri.c \
soup-value-utils.c \
soup-version.c \
soup-websocket.c \
soup-websocket-connection.c \
soup-xmlrpc.c
# TLD rules
......
......@@ -513,6 +513,29 @@ soup_value_hash_lookup
soup_value_hash_lookup_vals
soup_value_hash_new
soup_value_hash_new_with_vals
soup_websocket_client_prepare_handshake
soup_websocket_client_verify_handshake
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_connection_type
soup_websocket_connection_get_io_stream
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_new
soup_websocket_connection_send_binary
soup_websocket_connection_send_text
soup_websocket_connection_type_get_type
soup_websocket_data_type_get_type
soup_websocket_error_get_quark
soup_websocket_error_get_type
soup_websocket_server_check_handshake
soup_websocket_server_process_handshake
soup_websocket_state_get_type
soup_xmlrpc_build_fault
soup_xmlrpc_build_method_call
soup_xmlrpc_build_method_response
......
......@@ -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;
......
This diff is collapsed.
/* -*- 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 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_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 (* closed) (SoupWebsocketConnection *self);
} SoupWebsocketConnectionClass;
SOUP_AVAILABLE_IN_2_50
GType soup_websocket_connection_get_type (void) G_GNUC_CONST;
SoupWebsocketConnection *soup_websocket_connection_new (GIOStream *stream,
SoupURI *uri,
SoupWebsocketConnectionType type,
const char *origin,
const char *protocol);
SOUP_AVAILABLE_IN_2_50
GIOStream * soup_websocket_connection_get_io_stream (SoupWebsocketConnection *self);
SOUP_AVAILABLE_IN_2_50
SoupWebsocketConnectionType soup_websocket_connection_get_connection_type (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
void soup_websocket_connection_send_text (SoupWebsocketConnection *self,
const char *text);
SOUP_AVAILABLE_IN_2_50
void soup_websocket_connection_send_binary (SoupWebsocketConnection *self,
gconstpointer data,
gsize 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__ */
/* -*- 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 <glib/gi18n-lib.h>
#include "soup-websocket.h"
#include "soup-headers.h"
#include "soup-message.h"
/**
* SECTION:soup-websocket
* @short_description: The WebSocket Protocol
*
* #SoupWebsocketConnection provides support for the <ulink
* url="http://tools.ietf.org/html/rfc6455">WebSocket</ulink> protocol.
*
* soup_websocket_client_prepare_handshake() and
* soup_websocket_client_verify_handshake() are low-level functions
* for handling the client side of the WebSocket handshake.
* soup_websocket_server_process_handshake() is the low-level function
* for handling the server side.
*
* After completing a handshake, you can wrap the underlying
* #GIOStream in a #SoupWebsocketConnection, which handles the details
* of WebSocket communication. You can then use
* soup_websocket_connection_send_text() and
* soup_websocket_connection_send_binary() to send data, and the
* #SoupWebsocketConnection::message signal to receive data.
* (#SoupWebsocketConnection currently only supports asynchronous
* I/O.)
*
* Since: 2.50
*/
/**
* SOUP_WEBSOCKET_ERROR:
*
* A #GError domain for WebSocket-related errors. Used with
* #SoupWebsocketError.
*
* Since: 2.50
*/
/**
* SoupWebsocketError:
* @SOUP_WEBSOCKET_ERROR_FAILED: a generic error
* @SOUP_WEBSOCKET_ERROR_NOT_WEBSOCKET: attempted to handshake with a
* server that does not appear to understand WebSockets.
* @SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE: the WebSocket handshake failed
* because some detail was invalid (eg, incorrect accept key).
* @SOUP_WEBSOCKET_ERROR_BAD_ORIGIN: the WebSocket handshake failed
* because the "Origin" header was not an allowed value.
*
* WebSocket-related errors.
*
* Since: 2.50
*/
/**
* SoupWebsocketConnectionType:
* @SOUP_WEBSOCKET_CONNECTION_UNKNOWN: unknown/invalid connection
* @SOUP_WEBSOCKET_CONNECTION_CLIENT: a client-side connection
* @SOUP_WEBSOCKET_CONNECTION_SERVER: a server-side connection
*
* The type of a #SoupWebsocketConnection.
*
* Since: 2.50
*/
/**
* SoupWebsocketDataType:
* @SOUP_WEBSOCKET_DATA_TEXT: UTF-8 text
* @SOUP_WEBSOCKET_DATA_BINARY: binary data
*
* The type of data contained in a #SoupWebsocketConnection::message
* signal.
*
* Since: 2.50
*/
/**
* SoupWebsocketCloseCode:
* @SOUP_WEBSOCKET_CLOSE_NORMAL: a normal, non-error close
* @SOUP_WEBSOCKET_CLOSE_GOING_AWAY: the client/server is going away
* @SOUP_WEBSOCKET_CLOSE_PROTOCOL_ERROR: a protocol error occurred
* @SOUP_WEBSOCKET_CLOSE_UNSUPPORTED_DATA: the endpoint received data
* of a type that it does not support.
* @SOUP_WEBSOCKET_CLOSE_NO_STATUS: reserved value indicating that
* no close code was present; must not be sent.
* @SOUP_WEBSOCKET_CLOSE_ABNORMAL: reserved value indicating that
* the connection was closed abnormally; must not be sent.
* @SOUP_WEBSOCKET_CLOSE_BAD_DATA: the endpoint received data that
* was invalid (eg, non-UTF-8 data in a text message).
* @SOUP_WEBSOCKET_CLOSE_POLICY_VIOLATION: generic error code
* indicating some sort of policy violation.
* @SOUP_WEBSOCKET_CLOSE_TOO_BIG: the endpoint received a message
* that is too big to process.
* @SOUP_WEBSOCKET_CLOSE_NO_EXTENSION: the client is closing the
* connection because the server failed to negotiate a required
* extension.
* @SOUP_WEBSOCKET_CLOSE_SERVER_ERROR: the server is closing the
* connection because it was unable to fulfill the request.
* @SOUP_WEBSOCKET_CLOSE_TLS_HANDSHAKE: reserved value indicating that
* the TLS handshake failed; must not be sent.
*
* Pre-defined close codes that can be passed to
* soup_websocket_connection_close() or received from
* soup_websocket_connection_get_close_code(). (However, other codes
* are also allowed.)
*
* Since: 2.50
*/
/**
* SoupWebsocketState:
* @SOUP_WEBSOCKET_STATE_OPEN: the connection is ready to send messages
* @SOUP_WEBSOCKET_STATE_CLOSING: the connection is in the process of
* closing down; messages may be received, but not sent
* @SOUP_WEBSOCKET_STATE_CLOSED: the connection is completely closed down
*
* The state of the WebSocket connection.
*
* Since: 2.50
*/
GQuark
soup_websocket_error_get_quark (void)
{
return g_quark_from_static_string ("web-socket-error-quark");
}
static gboolean
validate_key (const char *key)
{
guchar buf[18];
int state = 0;
guint save = 0;
/* The spec requires us to check that the key is "a
* base64-encoded value that, when decoded, is 16 bytes in
* length".
*/
if (strlen (key) != 24)
return FALSE;
if (g_base64_decode_step (key, 24, buf, &state, &save) != 16)
return FALSE;
return TRUE;
}
static char *
compute_accept_key (const char *key)
{
gsize digest_len = 20;
guchar digest[digest_len];
GChecksum *checksum;
if (!key)
return 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);
}
static gboolean
choose_subprotocol (SoupMessage *msg,
const char **server_protocols,
const char **chosen_protocol)
{
const char *client_protocols_str;
char **client_protocols;
int i, j;
if (chosen_protocol)
*chosen_protocol = NULL;
if (!server_protocols)
return TRUE;
client_protocols_str = soup_message_headers_get_one (msg->request_headers,
"Sec-Websocket-Protocol");
if (!client_protocols_str)
return TRUE;
client_protocols = g_strsplit_set (client_protocols_str, ", ", -1);
if (!client_protocols || !client_protocols[0]) {
g_strfreev (client_protocols);
return TRUE;
}
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_strfreev (client_protocols);
if (chosen_protocol)
*chosen_protocol = server_protocols[i];
return TRUE;
}
}
}
g_strfreev (client_protocols);
return FALSE;
}
/**
* soup_websocket_client_prepare_handshake:
* @msg: a #SoupMessage
* @origin: (allow-none): the "Origin" header to set
* @protocols: (allow-none) (array zero-terminated=1): list of
* protocols to offer
*
* Adds the necessary headers to @msg to request a WebSocket
* handshake. The message body and non-WebSocket-related headers are
* not modified.
*
* Since: 2.50
*/
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);
}
}
/**
* soup_websocket_server_check_handshake:
* @msg: #SoupMessage containing the client side of a WebSocket handshake
* @origin: (allow-none): expected Origin header
* @protocols: (allow-none) (array zero-terminated=1): allowed WebSocket
* protocols.
* @error: return location for a #GError
*
* Examines the method and request headers in @msg and determines
* whether @msg contains a valid handshake request.
*
* If @origin is non-%NULL, then only requests containing a matching
* "Origin" header will be accepted. If @protocols is non-%NULL, then
* only requests containing a compatible "Sec-WebSocket-Protocols"
* header will be accepted.
*
* Returns: %TRUE if @msg contained a valid WebSocket handshake,
* %FALSE and an error if not.
*
* Since: 2.50
*/
gboolean
soup_websocket_server_check_handshake (SoupMessage *msg,
const char *expected_origin,
char **protocols,
GError **error)
{
const char *origin;
const char *key;
if (msg->method != SOUP_METHOD_GET) {
g_set_error_literal (error,
SOUP_WEBSOCKET_ERROR,
SOUP_WEBSOCKET_ERROR_NOT_WEBSOCKET,
_("WebSocket handshake expected"));
return FALSE;
}
if (!soup_message_headers_header_equals (msg->request_headers, "Upgrade", "websocket") ||
!soup_message_headers_header_contains (msg->request_headers, "Connection", "upgrade")) {
g_set_error_literal (error,
SOUP_WEBSOCKET_ERROR,
SOUP_WEBSOCKET_ERROR_NOT_WEBSOCKET,
_("WebSocket handshake expected"));
return FALSE;
}
if (!soup_message_headers_header_equals (msg->request_headers, "Sec-WebSocket-Version", "13")) {
g_set_error_literal (error,
SOUP_WEBSOCKET_ERROR,
SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE,
_("Unsupported WebSocket version"));
return FALSE;
}
key = soup_message_headers_get_one (msg->request_headers, "Sec-WebSocket-Key");
if (key == NULL || !validate_key (key)) {
g_set_error_literal (error,
SOUP_WEBSOCKET_ERROR,
SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE,
_("Invalid WebSocket key"));
return FALSE;
}
if (expected_origin) {
origin = soup_message_headers_get_one (msg->request_headers, "Origin");
if (!origin || g_ascii_strcasecmp (origin, expected_origin) != 0) {
g_set_error (error,
SOUP_WEBSOCKET_ERROR,
SOUP_WEBSOCKET_ERROR_BAD_ORIGIN,
_("Incorrect WebSocket \"%s\" header"), "Origin");
return FALSE;