[Spice-devel] [PATCH spice-server 03/23] Add support for clients connecting with the WebSocket protocol.
Frediano Ziglio
fziglio at redhat.com
Tue Jun 25 16:11:27 UTC 2019
From: Jeremy White <jwhite at codeweavers.com>
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 at codeweavers.com>
---
server/Makefile.am | 2 +
server/meson.build | 2 +
server/red-stream.c | 118 ++++++++++++
server/red-stream.h | 2 +
server/reds.c | 13 ++
server/websocket.c | 457 ++++++++++++++++++++++++++++++++++++++++++++
server/websocket.h | 44 +++++
7 files changed, 638 insertions(+)
create mode 100644 server/websocket.c
create mode 100644 server/websocket.h
diff --git a/server/Makefile.am b/server/Makefile.am
index 20adf65f0..71c7e1732 100644
--- a/server/Makefile.am
+++ b/server/Makefile.am
@@ -177,6 +177,8 @@ libserver_la_SOURCES = \
video-encoder.h \
video-stream.c \
video-stream.h \
+ websocket.c \
+ websocket.h \
zlib-encoder.c \
zlib-encoder.h \
$(NULL)
diff --git a/server/meson.build b/server/meson.build
index a7676f7e3..395811c80 100644
--- a/server/meson.build
+++ b/server/meson.build
@@ -144,6 +144,8 @@ spice_server_sources = [
'video-encoder.h',
'video-stream.c',
'video-stream.h',
+ 'websocket.c',
+ 'websocket.h',
'zlib-encoder.c',
'zlib-encoder.h',
]
diff --git a/server/red-stream.c b/server/red-stream.c
index 77fed097e..4b8992411 100644
--- a/server/red-stream.c
+++ b/server/red-stream.c
@@ -39,6 +39,7 @@
#include "red-common.h"
#include "red-stream.h"
#include "reds.h"
+#include "websocket.h"
// compatibility for *BSD systems
#if !defined(TCP_CORK) && !defined(_WIN32)
@@ -77,6 +78,17 @@ typedef struct RedSASL {
} RedSASL;
#endif
+typedef struct {
+ int closed;
+
+ websocket_frame_t read_frame;
+ uint64_t write_remainder;
+
+ ssize_t (*raw_read)(RedStream *s, void *buf, size_t nbyte);
+ ssize_t (*raw_write)(RedStream *s, const void *buf, size_t nbyte);
+ ssize_t (*raw_writev)(RedStream *s, const struct iovec *iov, int iovcnt);
+} RedsWebSocket;
+
struct RedStreamPrivate {
SSL *ssl;
@@ -86,6 +98,8 @@ struct RedStreamPrivate {
AsyncRead async_read;
+ RedsWebSocket *ws;
+
/* life time of info:
* allocated when creating RedStream.
* deallocated when main_dispatcher handles the SPICE_CHANNEL_EVENT_DISCONNECTED
@@ -433,6 +447,8 @@ void red_stream_free(RedStream *s)
SSL_free(s->priv->ssl);
}
+ g_free(s->priv->ws);
+
red_stream_remove_watch(s);
socket_close(s->socket);
@@ -1155,3 +1171,105 @@ error:
return false;
}
#endif
+
+static ssize_t stream_websocket_read(RedStream *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_read_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(RedStream *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(RedStream *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 red_stream_is_websocket(RedStream *stream, const void *buf, size_t 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 - 1);
+ if (rc <= 0) {
+ return false;
+ }
+ len += rc;
+ rbuf[len] = 0;
+
+ /* 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)) {
+ char outbuf[1024];
+
+ websocket_create_reply(rbuf, outbuf);
+ rc = stream->priv->write(stream, outbuf, strlen(outbuf));
+ if (rc == strlen(outbuf)) {
+ stream->priv->ws = g_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/red-stream.h b/server/red-stream.h
index ca6dc71a9..a191dd42b 100644
--- a/server/red-stream.h
+++ b/server/red-stream.h
@@ -91,6 +91,8 @@ bool red_stream_set_auto_flush(RedStream *stream, bool auto_flush);
*/
void red_stream_flush(RedStream *stream);
+bool red_stream_is_websocket(RedStream *stream, const void *buf, size_t len);
+
typedef enum {
RED_SASL_ERROR_OK,
RED_SASL_ERROR_GENERIC,
diff --git a/server/reds.c b/server/reds.c
index b4061fbc3..671e0a86d 100644
--- a/server/reds.c
+++ b/server/reds.c
@@ -2418,6 +2418,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;
@@ -2460,6 +2461,18 @@ static void reds_handle_read_magic_done(void *opaque)
const SpiceLinkHeader *header = &link->link_header;
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 (red_stream_is_websocket(link->stream, &header->magic, sizeof(header->magic))) {
+ 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 000000000..58f36da0a
--- /dev/null
+++ b/server/websocket.c
@@ -0,0 +1,457 @@
+/* -*- 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/>.
+*/
+#define _GNU_SOURCE
+#include <config.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <errno.h>
+
+#include <sys/types.h>
+#ifndef _WIN32
+#include <sys/socket.h>
+#include <unistd.h>
+#endif
+
+#include <glib.h>
+
+#include <common/log.h>
+#include <common/mem.h>
+
+#include "sys-socket.h"
+#include "websocket.h"
+
+#ifdef _WIN32
+#include <shlwapi.h>
+#define strcasestr(haystack, needle) StrStrIA(haystack, needle)
+#endif
+
+/* Constants / masks all from RFC 6455 */
+
+#define FIN_FLAG 0x80
+#define TYPE_MASK 0x0F
+
+#define CONTINUATION_FRAME 0x0
+#define TEXT_FRAME 0x1
+#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 insensitive search for needle in haystack.
+ If found, return a pointer to the byte after the end of needle.
+ Otherwise, return NULL */
+static const char *find_str(const char *haystack, const char *needle)
+{
+ const char *s = strcasestr(haystack, needle);
+
+ if (s) {
+ return s + 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 uint64_t extract_length(const uint8_t *buf, int *used)
+{
+ int i;
+ uint64_t 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 char *generate_reply_key(char *buf)
+{
+ GChecksum *checksum;
+ char *b64 = NULL;
+ uint8_t *sha1;
+ size_t sha1_size;
+ const char *key;
+ const char *p;
+ char *k;
+
+ key = find_str(buf, "\nSec-WebSocket-Key:");
+ if (key) {
+ p = strchr(key, '\r');
+ if (p) {
+ k = g_strndup(key, p - key);
+ k = g_strstrip(k);
+ checksum = g_checksum_new(G_CHECKSUM_SHA1);
+ g_checksum_update(checksum, (uint8_t *) k, strlen(k));
+ g_checksum_update(checksum, (uint8_t *) 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. */
+static void websocket_get_frame_header(websocket_frame_t *frame)
+{
+ int fin;
+ int used = 0;
+
+ if (frame_bytes_needed(frame) > 0) {
+ return;
+ }
+
+ 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 == CONTINUATION_FRAME) {
+ frame->type = BINARY_FRAME;
+ }
+
+ frame->expected_len = extract_length(frame->header + used, &used);
+
+ if (frame->masked) {
+ memcpy(frame->mask, frame->header + used, 4);
+ }
+
+ frame->relayed = 0;
+ frame->frame_ready = 1;
+}
+
+static int relay_data(uint8_t* 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; i++, frame->relayed++) {
+ *buf++ ^= frame->mask[frame->relayed % 4];
+ }
+ }
+
+ return n;
+}
+
+int websocket_read(void *opaque, uint8_t *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) {
+ // make sure we have a proper frame ready
+ if (!frame->frame_ready) {
+ rc = read_cb(opaque, frame->header + frame->header_pos, frame_bytes_needed(frame));
+ if (rc <= 0) {
+ goto read_error;
+ }
+ 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) {
+ goto read_error;
+ }
+
+ 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;
+
+read_error:
+ if (n > 0 && rc == -1 && (errno == EINTR || errno == EAGAIN)) {
+ return n;
+ }
+ return rc;
+}
+
+static int fill_header(uint8_t *header, uint64_t len)
+{
+ int used = 0;
+ int i;
+
+ header[0] = FIN_FLAG | BINARY_FRAME;
+ used++;
+
+ header[1] = 0;
+ used++;
+ if (len > 65535) {
+ header[1] |= LENGTH_64BIT;
+ for (i = 9; i >= 2; i--) {
+ header[i] = len & 0xFF;
+ len >>= 8;
+ }
+ used += 8;
+ } else if (len >= LENGTH_16BIT) {
+ 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,
+ uint64_t 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 = g_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, uint64_t *remainder,
+ websocket_writev_cb_t writev_cb)
+{
+ uint8_t header[WEBSOCKET_MAX_HEADER_SIZE];
+ uint64_t 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) {
+ g_free(iov_out);
+ }
+ if (rc <= 0) {
+ return rc;
+ }
+ *remainder -= rc;
+ return rc;
+ }
+
+ iov_out_cnt = iovcnt + 1;
+ iov_out = g_malloc(iov_out_cnt * sizeof(*iov_out));
+
+ 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);
+ g_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 uint8_t *buf, int len, uint64_t *remainder,
+ websocket_write_cb_t write_cb)
+{
+ uint8_t header[WEBSOCKET_MAX_HEADER_SIZE];
+ int rc;
+ int header_len;
+
+ if (*remainder == 0) {
+ header_len = fill_header(header, len);
+ 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(char *buf)
+{
+ if (strncmp(buf, "GET ", 4) == 0 &&
+ // TODO strip, do not assume a single space
+ find_str(buf, "\nSec-WebSocket-Protocol: binary") &&
+ find_str(buf, "\nSec-WebSocket-Key:") &&
+ g_str_has_suffix(buf, "\r\n\r\n")) {
+ return true;
+ }
+
+ return false;
+}
+
+void websocket_create_reply(char *buf, char *outbuf)
+{
+ char *key;
+
+ key = generate_reply_key(buf);
+ 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 000000000..63d7b10c2
--- /dev/null
+++ b/server/websocket.h
@@ -0,0 +1,44 @@
+/*
+ * 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;
+ uint8_t header[WEBSOCKET_MAX_HEADER_SIZE];
+ int header_pos;
+ int frame_ready:1;
+ uint8_t mask[4];
+ uint64_t relayed;
+ uint64_t expected_len;
+} websocket_frame_t;
+
+typedef ssize_t (*websocket_read_cb_t)(void *opaque, 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(char *buf);
+void websocket_create_reply(char *buf, char *outbuf);
+int websocket_read(void *opaque, uint8_t *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 uint8_t *buf, int len, uint64_t *remainder,
+ websocket_write_cb_t write_cb);
+int websocket_writev(void *opaque, struct iovec *iov, int iovcnt, uint64_t *remainder,
+ websocket_writev_cb_t writev_cb);
+void websocket_ack_close(void *opaque, websocket_write_cb_t write_cb);
--
2.20.1
More information about the Spice-devel
mailing list