[Spice-devel] [RFC spice] Add support for clients connecting with the WebSocket protocol.
Jeremy White
jwhite at codeweavers.com
Fri Oct 16 13:19:48 PDT 2015
I am revisiting the effort involved in adding direct support for the WebSocket
protcol to the Spice server. Alon worked on this back in 2012:
http://lists.freedesktop.org/archives/spice-devel/2012-October/011262.html
I have taken a different approach than Alon.
That is, rather than requiring any additional configuration, this patch
simply auto detects the inbound http 'GET' that signals a WebSocket
connection, and then puts a layer of websocket control handling ahead of the
regular stream code in reds_stream.c.
I believe it is fairly low risk, as this code path should only trigger
for connections that do not send the SPICE_MAGIC bytes on connection.
I would appreciate comments on the general approach.
The code does work, although I have only does the most basic tests.
Signed-off-by: Jeremy White <jwhite at codeweavers.com>
---
server/Makefile.am | 2 +
server/reds.c | 6 ++
server/reds_stream.c | 110 +++++++++++++++++++++
server/reds_stream.h | 2 +
server/websocket.c | 272 +++++++++++++++++++++++++++++++++++++++++++++++++++
server/websocket.h | 52 ++++++++++
6 files changed, 444 insertions(+)
create mode 100644 server/websocket.c
create mode 100644 server/websocket.h
diff --git a/server/Makefile.am b/server/Makefile.am
index fad1cbc..a072141 100644
--- a/server/Makefile.am
+++ b/server/Makefile.am
@@ -113,6 +113,8 @@ libspice_server_la_SOURCES = \
reds-private.h \
reds_stream.c \
reds_stream.h \
+ websocket.c \
+ websocket.h \
reds_sw_canvas.c \
reds_sw_canvas.h \
snd_worker.c \
diff --git a/server/reds.c b/server/reds.c
index 5d2ad9b..f8730df 100644
--- a/server/reds.c
+++ b/server/reds.c
@@ -2172,6 +2172,7 @@ static void reds_handle_link_error(void *opaque, int err)
reds_link_free(link);
}
+static void reds_handle_new_link(RedLinkInfo *link);
static void reds_handle_read_header_done(void *opaque)
{
RedLinkInfo *link = (RedLinkInfo *)opaque;
@@ -2182,6 +2183,11 @@ static void reds_handle_read_header_done(void *opaque)
header->size = GUINT32_FROM_LE(header->size);
if (header->magic != SPICE_MAGIC) {
+ if (reds_stream_is_websocket(link->stream,
+ (guchar *) header, sizeof(*header))) {
+ reds_handle_new_link(link);
+ return;
+ }
reds_send_link_error(link, SPICE_LINK_ERR_INVALID_MAGIC);
reds_link_free(link);
return;
diff --git a/server/reds_stream.c b/server/reds_stream.c
index 6dc41ca..0ebca75 100644
--- a/server/reds_stream.c
+++ b/server/reds_stream.c
@@ -30,6 +30,7 @@
#include <sys/socket.h>
#include <glib.h>
+#include "websocket.h"
#include <openssl/err.h>
@@ -76,6 +77,14 @@ typedef struct RedsSASL {
} RedsSASL;
#endif
+typedef struct {
+ SpiceBuffer rawbuffer;
+ SpiceBuffer inbuffer;
+
+ ssize_t (*raw_read)(RedsStream *s, void *buf, size_t nbyte);
+ ssize_t (*raw_writev)(RedsStream *s, const struct iovec *iov, int iovcnt);
+} RedsWebSocket;
+
struct RedsStreamPrivate {
SSL *ssl;
@@ -85,6 +94,8 @@ struct RedsStreamPrivate {
AsyncRead async_read;
+ RedsWebSocket *ws;
+
/* life time of info:
* allocated when creating RedsStream.
* deallocated when main_dispatcher handles the SPICE_CHANNEL_EVENT_DISCONNECTED
@@ -1068,3 +1079,102 @@ error:
return FALSE;
}
#endif
+
+static int pull_frames(SpiceBuffer *raw, SpiceBuffer *in)
+{
+ int count = 0;
+ int rc;
+ websocket_frame_t frame;
+
+ while(1) {
+ rc = websocket_get_frame(raw->buffer, raw->offset, &frame);
+ if (rc <= 0)
+ break;
+
+ spice_buffer_append(in, frame.payload, frame.len);
+ websocket_free_frame(&frame);
+ spice_buffer_remove(raw, rc);
+ count++;
+ }
+ return count;
+}
+
+
+static ssize_t stream_websocket_read(RedsStream *s, void *buf, size_t size)
+{
+ SpiceBuffer *in = &s->priv->ws->inbuffer;
+ SpiceBuffer *raw = &s->priv->ws->rawbuffer;
+ int rc;
+
+ /* TODO - probably have an issue with watches not
+ being 'live' because we read too far
+ We can probably 'fake' it. */
+ while (1) {
+ if (in->offset >= size) {
+ spice_buffer_copy(in, buf, size);
+ spice_buffer_remove(in, size);
+ return size;
+ }
+
+ if (raw->offset > 0)
+ if (pull_frames(raw, in))
+ continue;
+
+ spice_buffer_reserve(raw, 4096);
+ rc = s->priv->ws->raw_read(s, raw->buffer + raw->offset,
+ raw->capacity - raw->offset);
+ if (rc <= 0)
+ return rc;
+
+ raw->offset += rc;
+ }
+}
+
+static ssize_t stream_websocket_write(RedsStream *s, const void *buf, size_t size)
+{
+ return websocket_write((void *)s, buf, size,
+ (websocket_writev_cb_t) s->priv->ws->raw_writev);
+}
+
+static ssize_t stream_websocket_writev(RedsStream *s, const struct iovec *iov, int iovcnt)
+{
+ return websocket_writev((void *)s, iov, iovcnt,
+ (websocket_writev_cb_t) s->priv->ws->raw_writev);
+}
+
+bool reds_stream_is_websocket(RedsStream *stream, unsigned char *buf, int len)
+{
+ char rbuf[4096];
+ int rc;
+
+ if (stream->priv->ws) {
+ return FALSE;
+ }
+
+ memcpy(rbuf, buf, len);
+ rc = stream->priv->read(stream, rbuf + len, sizeof(rbuf) - len);
+ if (rc <= 0)
+ return FALSE;
+ len += rc;
+
+ if (websocket_is_start(rbuf, len)) {
+ char outbuf[1024];
+
+ websocket_create_reply(rbuf, len, outbuf);
+ rc = stream->priv->write(stream, outbuf, strlen(outbuf));
+ if (rc == strlen(outbuf)) {
+ stream->priv->ws = spice_malloc0(sizeof(*stream->priv->ws));
+
+ stream->priv->ws->raw_read = stream->priv->read;
+ stream->priv->ws->raw_writev = stream->priv->writev;
+
+ stream->priv->read = stream_websocket_read;
+ stream->priv->write = stream_websocket_write;
+ stream->priv->writev = stream_websocket_writev;
+
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
diff --git a/server/reds_stream.h b/server/reds_stream.h
index b5889e3..3e64500 100644
--- a/server/reds_stream.h
+++ b/server/reds_stream.h
@@ -74,6 +74,8 @@ int reds_stream_enable_ssl(RedsStream *stream, SSL_CTX *ctx);
void reds_stream_set_info_flag(RedsStream *stream, unsigned int flag);
int reds_stream_get_family(RedsStream *stream);
+bool reds_stream_is_websocket(RedsStream *stream, unsigned char *buf, int len);
+
typedef enum {
REDS_SASL_ERROR_OK,
REDS_SASL_ERROR_GENERIC,
diff --git a/server/websocket.c b/server/websocket.c
new file mode 100644
index 0000000..3eca8dd
--- /dev/null
+++ b/server/websocket.c
@@ -0,0 +1,272 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2015 Jeremy White
+
+ This library 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.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ 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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <unistd.h>
+
+#include <glib.h>
+
+#include "websocket.h"
+
+/* Perform a case insenstive search for needle in haystack.
+ If found, return a pointer to the byte after the end of needle.
+ Otherwise, return NULL */
+static gchar *find_str(gchar *haystack, const char *needle, int n)
+{
+ int i;
+
+ for (i = 0; i < n; i++) {
+ if ((n - i) < strlen(needle))
+ break;
+
+ if (g_ascii_strncasecmp(haystack + i, needle, strlen(needle)) == 0)
+ return haystack + i + strlen(needle);
+ }
+ return NULL;
+}
+
+/* Extract WebSocket style length. Returns 0 if not enough data present */
+static guint64 extract_length(guint8 *buf, int len, int *used)
+{
+ int i;
+ guint64 outlen = (*buf++) & LENGTH_MASK;
+
+ (*used)++;
+
+ switch(outlen) {
+ case LENGTH_64BIT:
+ *used += 8;
+ if (len < 8)
+ return 0;
+ outlen = 0;
+ for (i = 56; i >= 0; i -= 8)
+ outlen |= (*buf++) << i;
+ break;
+
+ case LENGTH_16BIT:
+ *used += 2;
+ if (len < 2)
+ return 0;
+ outlen = ((*buf) << 8) | *(buf + 1);
+ break;
+
+ default:
+ break;
+ }
+ return outlen;
+}
+
+/*
+* Generate WebSocket style response key, based on the
+* original key sent to us
+* If non null, caller must free returned key string.
+*/
+static gchar *generate_reply_key(gchar *buf, int len)
+{
+ GChecksum *checksum = NULL;
+ gchar *b64 = NULL;
+ guint8 *sha1;
+ gsize sha1_size;
+ gchar *key;
+ gchar *p;
+ gchar *k;
+
+ key = find_str(buf, "Sec-WebSocket-Key:", len);
+ if (key) {
+ p = strchr(key, '\r');
+ if (p && p - buf < len) {
+ k = g_strndup(key, p - key);
+ k = g_strstrip(k);
+ checksum = g_checksum_new(G_CHECKSUM_SHA1);
+ g_checksum_update(checksum, (guchar *) k, strlen(k));
+ g_checksum_update(checksum, (guchar *) WEBSOCKET_GUID, strlen(WEBSOCKET_GUID));
+ g_free(k);
+
+ sha1_size = g_checksum_type_get_length(G_CHECKSUM_SHA1);
+ sha1 = g_malloc(sha1_size);
+
+ g_checksum_get_digest(checksum, sha1, &sha1_size);
+
+ b64 = g_base64_encode(sha1, sha1_size);
+
+ g_checksum_free(checksum);
+ g_free(sha1);
+ }
+ }
+
+ return b64;
+}
+
+
+/* Extract a frame of data from a set of data transmitted by
+ a WebSocket client. Returns the number of bytes consumed
+ if a complete frame is available, otherwise returns 0 */
+int websocket_get_frame(guint8 *buf, int len, websocket_frame_t *frame)
+{
+ int fin = *buf & FIN_FLAG;
+ int used = 0;
+ int i;
+
+ if (len < 2)
+ return 0;
+
+ frame->type = *buf & TYPE_MASK;
+ used++;
+
+ frame->masked = *(buf + 1) & MASK_FLAG;
+
+ /* This is a Spice specific optimization. We don't really
+ care about assembling frames fully, so we treat
+ a frame in process as a finished frame and pass it along. */
+ if (!fin && frame->type == 0)
+ frame->type = BINARY_FRAME;
+
+ frame->len = extract_length(buf + used, len - used, &used);
+ if (frame->masked) {
+ if (len - used < 4)
+ return 0;
+
+ memcpy(frame->mask, buf + used, 4);
+ used += 4;
+ }
+
+ if (len - used < frame->len)
+ return 0;
+
+ frame->payload = g_malloc(frame->len);
+ memcpy(frame->payload, buf + used, frame->len);
+ if (frame->masked) {
+ for (i = 0; i < frame->len; i++)
+ frame->payload[i] ^= frame->mask[i % 4];
+ }
+
+ used += frame->len;
+
+ return used;
+}
+
+/* Write a WebSocket frame with the enclosed data out. */
+int websocket_writev(void *opaque, const struct iovec *iov, int iovcnt,
+ websocket_writev_cb_t writev_cb)
+{
+ guint8 header[1 + 9];
+ int used = 0;
+ guint64 len = 0;
+ guint64 shiftlen;
+ int rc;
+ int i;
+
+ struct iovec *iov_out;
+
+ iov_out = malloc((iovcnt + 1) * sizeof(*iov_out));
+ if (! iov_out)
+ return -1;
+
+ for (i = 0; i < iovcnt; i++) {
+ iov_out[i + 1] = iov[i];
+ len += iov[i].iov_len;
+ }
+
+ memset(header, 0, sizeof(header));
+ header[0] = FIN_FLAG | BINARY_FRAME;
+ used++;
+
+ header[1] = 0;
+ used++;
+ shiftlen = len;
+ if (len > 65535) {
+ header[1] |= LENGTH_64BIT;
+ for (i = 9; i >= 2; i--) {
+ header[i] = shiftlen & 0xFF;
+ shiftlen = shiftlen >> 8;
+ }
+ used += 8;
+ }
+
+ else if (len > 125) {
+ header[1] |= LENGTH_16BIT;
+ header[2] = len >> 8;
+ header[3] = len & 0xFF;
+ used += 2;
+ }
+
+ else
+ header[1] |= len;
+
+ iov_out[0].iov_base = header;
+ iov_out[0].iov_len = used;
+
+ rc = (*writev_cb)(opaque, iov_out, iovcnt + 1);
+
+ free(iov_out);
+
+ if (rc <= 0)
+ return rc;
+
+ return rc - used;
+}
+
+int websocket_write(void *opaque, const guchar *buf, int len,
+ websocket_writev_cb_t writev_cb)
+{
+ struct iovec iov;
+
+ iov.iov_base = (char *) buf;
+ iov.iov_len = len;
+
+ return websocket_writev(opaque, &iov, 1, writev_cb);
+}
+
+void websocket_free_frame(websocket_frame_t *frame)
+{
+ g_free(frame->payload);
+ frame->payload = NULL;
+}
+
+int websocket_is_start(gchar *buf, int len)
+{
+ if (len < 4)
+ return 0;
+
+ if (find_str(buf, "GET", len) == (buf + 3) &&
+ find_str(buf, "Sec-WebSocket-Protocol: binary", len) &&
+ find_str(buf, "Sec-WebSocket-Key:", len) &&
+ buf[len - 2] == '\r' && buf[len - 1] == '\n')
+ return 1;
+
+ return 0;
+}
+
+int websocket_create_reply(gchar *buf, int len, gchar *outbuf)
+{
+ gchar *key;
+
+ key = generate_reply_key(buf, len);
+ sprintf(outbuf, "HTTP/1.1 101 Switching Protocols\r\n"
+ "Upgrade: websocket\r\n"
+ "Connection: Upgrade\r\n"
+ "Sec-WebSocket-Accept: %s\r\n"
+ "Sec-WebSocket-Protocol: binary\r\n\r\n", key);
+ g_free(key);
+ return 0;
+}
diff --git a/server/websocket.h b/server/websocket.h
new file mode 100644
index 0000000..3f52887
--- /dev/null
+++ b/server/websocket.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2015 Jeremy White
+ *
+ * This library 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.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * 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/>.
+ */
+
+#define FIN_FLAG 0x80
+#define TYPE_MASK 0x0F
+
+#define BINARY_FRAME 0x2
+#define CLOSE_FRAME 0x8
+#define PING_FRAME 0x9
+#define PONG_FRAME 0xA
+
+#define LENGTH_MASK 0x7F
+#define LENGTH_16BIT 0x7E
+#define LENGTH_64BIT 0x7F
+
+#define MASK_FLAG 0x80
+
+#define WEBSOCKET_GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
+
+typedef struct
+{
+ int type;
+ int masked;
+ guint8 mask[4];
+ guint64 len;
+ guchar *payload;
+} websocket_frame_t;
+
+typedef size_t (*websocket_writev_cb_t)(void *opaque, struct iovec *iov, int iovcnt);
+
+int websocket_get_frame(guint8 *buf, int len, websocket_frame_t *frame);
+void websocket_free_frame(websocket_frame_t *frame);
+int websocket_is_start(gchar *buf, int len);
+int websocket_create_reply(gchar *buf, int len, gchar *outbuf);
+int websocket_write(void *opaque, const guchar *buf, int len,
+ websocket_writev_cb_t writev_cb);
+int websocket_writev(void *opaque, const struct iovec *iov, int iovcnt,
+ websocket_writev_cb_t writev_cb);
--
2.1.4
More information about the Spice-devel
mailing list