We do this by auto detecting the inbound http(s) 'GET' and probing for a well formulated WebSocket binary connection, such as used by the spice-html5 client. If detected, we implement a set of cover functions that abstract the read/write/writev functions, in a fashion similar to the SASL implementation. This includes a limited implementation of the WebSocket protocol, sufficient for our purposes. Signed-off-by: Jeremy White <jwhite@xxxxxxxxxxxxxxx> --- Resend; there was discussion without clear consensus. Daniel expressed concern: https://lists.freedesktop.org/archives/spice-devel/2015-November/023046.html I felt his concern was outweighed by the benefit. I would continue to argue that, as evidenced by the trickle of people failing to easily use spice-html5. Changes since v2: - Handle a prior partial write in our cover function more correctly - Fix a slight typo in the commit comment s/implemented/implementation/. Changes since v1 (discussed): - Added comments around the invocation of reds_stream_is_websocket to explain the rationale for reading the SpiceLinkHeader first - Added a TODO/comment in the websocket reading code explaining the theoretical flaw, that I've chosen not to address. - Switch reds_stream_is_websocket to a bool - Make websocket_create_reply a void - Free the new ws pointer on stream free - Move constants from public websocket.h to private websocket.c - Adjust a few callback prototypes to match the ones in reds_stream.c Changes since v1 (new): - Fix a bug in websocket_write, as it did not store a remainder when the write did not fully complete --- server/Makefile.am | 2 + server/reds-stream.c | 116 +++++++++++++ server/reds-stream.h | 2 + server/reds.c | 13 ++ server/websocket.c | 458 +++++++++++++++++++++++++++++++++++++++++++++++++++ server/websocket.h | 46 ++++++ 6 files changed, 637 insertions(+) create mode 100644 server/websocket.c create mode 100644 server/websocket.h diff --git a/server/Makefile.am b/server/Makefile.am index 921b082..efc85da 100644 --- a/server/Makefile.am +++ b/server/Makefile.am @@ -124,6 +124,8 @@ libserver_la_SOURCES = \ reds-private.h \ reds-stream.c \ reds-stream.h \ + websocket.c \ + websocket.h \ sw-canvas.c \ sound.c \ sound.h \ diff --git a/server/reds-stream.c b/server/reds-stream.c index 9896eab..09f8d51 100644 --- a/server/reds-stream.c +++ b/server/reds-stream.c @@ -32,6 +32,7 @@ #include <fcntl.h> #include <glib.h> +#include "websocket.h" #include <openssl/err.h> @@ -76,6 +77,17 @@ typedef struct RedsSASL { } RedsSASL; #endif +typedef struct { + int closed; + + websocket_frame_t read_frame; + guint64 write_remainder; + + ssize_t (*raw_read)(RedsStream *s, void *buf, size_t nbyte); + ssize_t (*raw_write)(RedsStream *s, const void *buf, size_t nbyte); + ssize_t (*raw_writev)(RedsStream *s, const struct iovec *iov, int iovcnt); +} RedsWebSocket; + struct RedsStreamPrivate { SSL *ssl; @@ -85,6 +97,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 @@ -340,6 +354,8 @@ void reds_stream_free(RedsStream *s) SSL_free(s->priv->ssl); } + free(s->priv->ws); + reds_stream_remove_watch(s); spice_info("close socket fd %d", s->socket); close(s->socket); @@ -1131,3 +1147,103 @@ error: return FALSE; } #endif + +static ssize_t stream_websocket_read(RedsStream *s, void *buf, size_t size) +{ + int rc; + + if (s->priv->ws->closed) + return 0; + + rc = websocket_read((void *)s, buf, size, &s->priv->ws->read_frame, + (websocket_write_cb_t) s->priv->ws->raw_read, + (websocket_write_cb_t) s->priv->ws->raw_write); + + if (rc == 0) + s->priv->ws->closed = 1; + + return rc; +} + +static ssize_t stream_websocket_write(RedsStream *s, const void *buf, size_t size) +{ + if (s->priv->ws->closed) { + errno = EPIPE; + return -1; + } + return websocket_write((void *)s, buf, size, &s->priv->ws->write_remainder, + (websocket_write_cb_t) s->priv->ws->raw_write); +} + +static ssize_t stream_websocket_writev(RedsStream *s, const struct iovec *iov, int iovcnt) +{ + if (s->priv->ws->closed) { + errno = EPIPE; + return -1; + } + return websocket_writev((void *)s, (struct iovec *) iov, iovcnt, &s->priv->ws->write_remainder, + (websocket_writev_cb_t) s->priv->ws->raw_writev); +} + +/* + If we detect that a newly opened stream appears to be using + the WebSocket protocol, we will put in place cover functions + that will speak WebSocket to the client, but allow the server + to continue to use normal stream read/write/writev semantics. +*/ +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; + + /* TODO: this has a theoretical flaw around packet buffering + that is not likely to occur in practice. That is, + to be fully correct, we should repeatedly read bytes until + either we get the end of the GET header (\r\n\r\n), or until + an amount of time has passed. Instead, we just read for + 16 bytes, and then read up to the sizeof rbuf. So if the + GET request is only partially complete at this point we + will fail. + + A typical GET request is 520 bytes, and it's difficult to + imagine a real world case where that will come in fragmented + such that we trigger this failure. Further, the spice reds + code has no real mechanism to do variable length/time based reads, + so it seems wisest to live with this theoretical flaw. + */ + + 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_write = stream->priv->write; + + stream->priv->read = stream_websocket_read; + stream->priv->write = stream_websocket_write; + + if (stream->priv->writev) { + stream->priv->ws->raw_writev = stream->priv->writev; + stream->priv->writev = stream_websocket_writev; + } + + return TRUE; + } + } + + return FALSE; +} diff --git a/server/reds-stream.h b/server/reds-stream.h index a8d1736..3959a27 100644 --- a/server/reds-stream.h +++ b/server/reds-stream.h @@ -76,6 +76,8 @@ int reds_stream_get_family(const RedsStream *stream); int reds_stream_is_plain_unix(const RedsStream *stream); int reds_stream_send_msgfd(RedsStream *stream, int fd); +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/reds.c b/server/reds.c index fdf62e5..d8bc661 100644 --- a/server/reds.c +++ b/server/reds.c @@ -2338,6 +2338,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; @@ -2348,6 +2349,18 @@ static void reds_handle_read_header_done(void *opaque) header->size = GUINT32_FROM_LE(header->size); if (header->magic != SPICE_MAGIC) { + /* Attempt to detect and support a WebSocket connection, + which will be proceeded by a variable length GET style request. + We cannot know we are dealing with a WebSocket connection + until we have read at least 3 bytes, and we will have to + read many more bytes than are contained in a SpiceLinkHeader. + So we may as well read a SpiceLinkHeader's worth of data, and if it's + clear that a WebSocket connection was requested, we switch + before proceeding further. */ + 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/websocket.c b/server/websocket.c new file mode 100644 index 0000000..be4331e --- /dev/null +++ b/server/websocket.c @@ -0,0 +1,458 @@ +/* -*- 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 <stdbool.h> +#include <string.h> +#include <ctype.h> +#include <errno.h> + +#include <sys/types.h> +#include <sys/socket.h> +#include <unistd.h> + +#include <glib.h> + +#include "websocket.h" +#include "common/log.h" + + +/* Constants / masks all from RFC 6455 */ + +#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" + +/* 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, + Always updates the output 'used' variable to the number of bytes + required to extract the length; useful for tracking where the + mask will be. +*/ +static guint64 extract_length(guint8 *buf, int *used) +{ + int i; + guint64 outlen = (*buf++) & LENGTH_MASK; + + (*used)++; + + switch(outlen) { + case LENGTH_64BIT: + *used += 8; + outlen = 0; + for (i = 56; i >= 0; i -= 8) + outlen |= (*buf++) << i; + break; + + case LENGTH_16BIT: + *used += 2; + outlen = ((*buf) << 8) | *(buf + 1); + break; + + default: + break; + } + return outlen; +} + +static int frame_bytes_needed(websocket_frame_t *frame) +{ + int needed = 2; + if (frame->header_pos < needed) + return needed - frame->header_pos; + + switch(frame->header[1] & LENGTH_MASK) { + case LENGTH_64BIT: + needed += 8; + break; + case LENGTH_16BIT: + needed += 2; + break; + } + + if (frame->header[1] & MASK_FLAG) { + needed += 4; + } + + return needed - frame->header_pos; +} + +/* +* 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; +} + + +static void websocket_clear_frame(websocket_frame_t *frame) +{ + memset(frame, 0, sizeof(*frame)); +} + +/* Extract a frame header of data from a set of data transmitted by + a WebSocket client. Returns bytes consumed if a frame + is available, otherwise returns 0 */ +static int websocket_get_frame_header(websocket_frame_t *frame) +{ + int fin; + int used = 0; + + if (frame->header_pos < frame_bytes_needed(frame)) + return 0; + + fin = frame->header[0] & FIN_FLAG; + frame->type = frame->header[0] & TYPE_MASK; + used++; + + frame->masked = frame->header[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->expected_len = extract_length(frame->header + used, &used); + + if (frame->masked) { + memcpy(frame->mask, frame->header + used, 4); + used += 4; + } + + frame->relayed = 0; + frame->frame_ready = 1; + + return used; +} + +static int relay_data(guint8* buf, size_t size, websocket_frame_t *frame) +{ + int i; + int n = MIN(size, frame->expected_len - frame->relayed); + + if (frame->masked) { + for (i = 0; i < n && frame->relayed < frame->expected_len; i++, frame->relayed++) + *buf++ ^= frame->mask[frame->relayed % 4]; + } + + return n; +} + +int websocket_read(void *opaque, guchar *buf, int size, websocket_frame_t *frame, + websocket_read_cb_t read_cb, + websocket_write_cb_t write_cb) +{ + int n = 0; + int rc; + + while (size > 0) { + if (! frame->frame_ready) { + rc = read_cb(opaque, frame->header + frame->header_pos, frame_bytes_needed(frame)); + if (rc <= 0) { + if (n > 0 && rc == -1 && (errno == EINTR || errno == EAGAIN)) + return n; + + return rc; + } + frame->header_pos += rc; + + websocket_get_frame_header(frame); + } + else if (frame->type == CLOSE_FRAME) { + websocket_ack_close(opaque, write_cb); + websocket_clear_frame(frame); + return 0; + } + else if (frame->type == BINARY_FRAME) { + rc = read_cb(opaque, buf, MIN(size, frame->expected_len - frame->relayed)); + if (rc <= 0) { + if (n > 0 && rc == -1 && (errno == EINTR || errno == EAGAIN)) + return n; + + return rc; + } + + rc = relay_data(buf, rc, frame); + n += rc; + buf += rc; + size -= rc; + if (frame->relayed >= frame->expected_len) + websocket_clear_frame(frame); + + } + else { + /* TODO - We don't handle PING at this point */ + spice_warning("Unexpected WebSocket frame.type %d. Failure now likely.", frame->type); + websocket_clear_frame(frame); + continue; + } + } + + return n; +} + +static int fill_header(guint8 *header, guint64 len) +{ + guint64 shiftlen; + int used = 0; + int i; + + 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; + + return used; +} + +static void constrain_iov(struct iovec *iov, int iovcnt, + struct iovec **iov_out, int *iov_out_cnt, + guint64 maxlen) +{ + int i, j; + + *iov_out = iov; + *iov_out_cnt = iovcnt; + + for (i = 0; i < iovcnt && maxlen > 0; i++) { + if (iov[i].iov_len > maxlen) { + /* TODO - This code has never triggered afaik... */ + *iov_out_cnt = i + 1; + *iov_out = malloc((*iov_out_cnt) * sizeof (**iov_out)); + for (j = 0; j < i; j++) { + (*iov_out)[j].iov_base = iov[j].iov_base; + (*iov_out)[j].iov_len = iov[j].iov_len; + } + (*iov_out)[j].iov_base = iov[j].iov_base; + (*iov_out)[j].iov_len = maxlen; + break; + } + maxlen -= iov[i].iov_len; + } +} + + +/* Write a WebSocket frame with the enclosed data out. */ +int websocket_writev(void *opaque, struct iovec *iov, int iovcnt, guint64 *remainder, + websocket_writev_cb_t writev_cb) +{ + guint8 header[WEBSOCKET_MAX_HEADER_SIZE]; + guint64 len; + int rc = -1; + struct iovec *iov_out; + int iov_out_cnt; + int i; + int header_len; + + if (*remainder > 0) { + constrain_iov(iov, iovcnt, &iov_out, &iov_out_cnt, *remainder); + rc = writev_cb(opaque, iov_out, iov_out_cnt); + if (iov_out != iov) + free(iov_out); + + if (rc <= 0) + return rc; + + *remainder -= rc; + return rc; + } + + iov_out_cnt = iovcnt + 1; + iov_out = malloc(iov_out_cnt * sizeof(*iov_out)); + if (! iov_out) + return -1; + + for (i = 0, len = 0; i < iovcnt; i++) { + len += iov[i].iov_len; + iov_out[i + 1] = iov[i]; + } + + memset(header, 0, sizeof(header)); + header_len = fill_header(header, len); + iov_out[0].iov_len = header_len; + iov_out[0].iov_base = header; + rc = writev_cb(opaque, iov_out, iov_out_cnt); + free(iov_out); + if (rc <= 0) + return rc; + + rc -= header_len; + + spice_assert(rc >= 0); + + /* Key point: if we did not write out all the data, remember how + much more data the client is expecting, and write that data without + a header of any kind the next time around */ + *remainder = len - rc; + + return rc; +} + +int websocket_write(void *opaque, const guchar *buf, int len, guint64 *remainder, + websocket_write_cb_t write_cb) +{ + guint8 header[WEBSOCKET_MAX_HEADER_SIZE]; + int rc; + int header_len; + + memset(header, 0, sizeof(header)); + header_len = fill_header(header, len); + + if (*remainder == 0) { + rc = write_cb(opaque, header, header_len); + if (rc <= 0) + return rc; + + if (rc != header_len) { + /* TODO - In theory, we can handle this case. In practice, + it does not occur, and does not seem to be worth + the code complexity */ + errno = EPIPE; + return -1; + } + } + else + len = MIN(*remainder, len); + + rc = write_cb(opaque, buf, len); + if (rc <= 0) + *remainder = len; + else + *remainder = len - rc; + return rc; +} + +void websocket_ack_close(void *opaque, websocket_write_cb_t write_cb) +{ + unsigned char header[2]; + + header[0] = FIN_FLAG | CLOSE_FRAME; + header[1] = 0; + + write_cb(opaque, header, sizeof(header)); +} + +bool websocket_is_start(gchar *buf, int len) +{ + if (len < 4) + return FALSE; + + 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 TRUE; + + return FALSE; +} + +void 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); +} diff --git a/server/websocket.h b/server/websocket.h new file mode 100644 index 0000000..3561279 --- /dev/null +++ b/server/websocket.h @@ -0,0 +1,46 @@ +/* + * 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 WEBSOCKET_MAX_HEADER_SIZE (1 + 9 + 4) + +typedef struct +{ + int type; + int masked; + guint8 header[WEBSOCKET_MAX_HEADER_SIZE]; + int header_pos; + int frame_ready:1; + guint8 mask[4]; + guint64 relayed; + guint64 expected_len; +} websocket_frame_t; + +typedef ssize_t (*websocket_read_cb_t)(void *opaque, const void *buf, size_t nbyte); +typedef ssize_t (*websocket_write_cb_t)(void *opaque, const void *buf, size_t nbyte); +typedef ssize_t (*websocket_writev_cb_t)(void *opaque, struct iovec *iov, int iovcnt); + +bool websocket_is_start(gchar *buf, int len); +void websocket_create_reply(gchar *buf, int len, gchar *outbuf); +int websocket_read(void *opaque, guchar *buf, int len, websocket_frame_t *frame, + websocket_read_cb_t read_cb, + websocket_write_cb_t write_cb); +int websocket_write(void *opaque, const guchar *buf, int len, guint64 *remainder, + websocket_write_cb_t write_cb); +int websocket_writev(void *opaque, struct iovec *iov, int iovcnt, guint64 *remainder, + websocket_writev_cb_t writev_cb); +void websocket_ack_close(void *opaque, websocket_write_cb_t write_cb); + -- 2.1.4 _______________________________________________ Spice-devel mailing list Spice-devel@xxxxxxxxxxxxxxxxxxxxx https://lists.freedesktop.org/mailman/listinfo/spice-devel