/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
/*
Copyright (C) 2010 Red Hat, Inc.
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 .
*/
#include "spice-client.h"
#include "spice-common.h"
#include "glib-compat.h"
#include "spice-channel-priv.h"
#include "spice-session-priv.h"
#include "spice-marshal.h"
#include "bio-gsocket.h"
#include
#include
#include
#include
#include
#include
#ifdef HAVE_SYS_SOCKET_H
#include
#endif
#ifdef HAVE_NETINET_IN_H
#include
#include // TCP_NODELAY
#endif
#ifdef HAVE_ARPA_INET_H
#include
#endif
#include
#include "gio-coroutine.h"
static void spice_channel_handle_msg(SpiceChannel *channel, SpiceMsgIn *msg);
static void spice_channel_write_msg(SpiceChannel *channel, SpiceMsgOut *out);
static void spice_channel_send_link(SpiceChannel *channel);
static void channel_disconnect(SpiceChannel *channel);
static void channel_reset(SpiceChannel *channel, gboolean migrating);
static void spice_channel_reset_capabilities(SpiceChannel *channel);
static void spice_channel_send_migration_handshake(SpiceChannel *channel);
/**
* SECTION:spice-channel
* @short_description: the base channel class
* @title: Spice Channel
* @section_id:
* @see_also: #SpiceSession, #SpiceMainChannel and other channels
* @stability: Stable
* @include: spice-channel.h
*
* #SpiceChannel is the base class for the different kind of Spice
* channel connections, such as #SpiceMainChannel, or
* #SpiceInputsChannel.
*/
/* ------------------------------------------------------------------ */
/* gobject glue */
#define SPICE_CHANNEL_GET_PRIVATE(obj) \
(G_TYPE_INSTANCE_GET_PRIVATE ((obj), SPICE_TYPE_CHANNEL, SpiceChannelPrivate))
G_DEFINE_TYPE(SpiceChannel, spice_channel, G_TYPE_OBJECT);
/* Properties */
enum {
PROP_0,
PROP_SESSION,
PROP_CHANNEL_TYPE,
PROP_CHANNEL_ID,
PROP_TOTAL_READ_BYTES,
};
/* Signals */
enum {
SPICE_CHANNEL_EVENT,
SPICE_CHANNEL_OPEN_FD,
SPICE_CHANNEL_LAST_SIGNAL,
};
static guint signals[SPICE_CHANNEL_LAST_SIGNAL];
static void spice_channel_iterate_write(SpiceChannel *channel);
static void spice_channel_iterate_read(SpiceChannel *channel);
static void spice_channel_init(SpiceChannel *channel)
{
SpiceChannelPrivate *c;
c = channel->priv = SPICE_CHANNEL_GET_PRIVATE(channel);
c->out_serial = 1;
c->in_serial = 1;
c->fd = -1;
strcpy(c->name, "?");
c->caps = g_array_new(FALSE, TRUE, sizeof(guint32));
c->common_caps = g_array_new(FALSE, TRUE, sizeof(guint32));
c->remote_caps = g_array_new(FALSE, TRUE, sizeof(guint32));
c->remote_common_caps = g_array_new(FALSE, TRUE, sizeof(guint32));
spice_channel_set_common_capability(channel, SPICE_COMMON_CAP_PROTOCOL_AUTH_SELECTION);
spice_channel_set_common_capability(channel, SPICE_COMMON_CAP_MINI_HEADER);
g_queue_init(&c->xmit_queue);
STATIC_MUTEX_INIT(c->xmit_queue_lock);
}
static void spice_channel_constructed(GObject *gobject)
{
SpiceChannel *channel = SPICE_CHANNEL(gobject);
SpiceChannelPrivate *c = channel->priv;
const char *desc = spice_channel_type_to_string(c->channel_type);
snprintf(c->name, sizeof(c->name), "%s-%d:%d",
desc ? desc : "unknown", c->channel_type, c->channel_id);
CHANNEL_DEBUG(channel, "%s", __FUNCTION__);
c->connection_id = spice_session_get_connection_id(c->session);
spice_session_channel_new(c->session, channel);
/* Chain up to the parent class */
if (G_OBJECT_CLASS(spice_channel_parent_class)->constructed)
G_OBJECT_CLASS(spice_channel_parent_class)->constructed(gobject);
}
static void spice_channel_dispose(GObject *gobject)
{
SpiceChannel *channel = SPICE_CHANNEL(gobject);
SpiceChannelPrivate *c = channel->priv;
CHANNEL_DEBUG(channel, "%s %p", __FUNCTION__, gobject);
if (c->session)
spice_session_channel_destroy(c->session, channel);
spice_channel_disconnect(channel, SPICE_CHANNEL_CLOSED);
if (c->session) {
g_object_unref(c->session);
c->session = NULL;
}
/* Chain up to the parent class */
if (G_OBJECT_CLASS(spice_channel_parent_class)->dispose)
G_OBJECT_CLASS(spice_channel_parent_class)->dispose(gobject);
}
static void spice_channel_finalize(GObject *gobject)
{
SpiceChannel *channel = SPICE_CHANNEL(gobject);
SpiceChannelPrivate *c = channel->priv;
CHANNEL_DEBUG(channel, "%s %p", __FUNCTION__, gobject);
g_idle_remove_by_data(gobject);
STATIC_MUTEX_CLEAR(c->xmit_queue_lock);
if (c->caps)
g_array_free(c->caps, TRUE);
if (c->common_caps)
g_array_free(c->common_caps, TRUE);
if (c->remote_caps)
g_array_free(c->remote_caps, TRUE);
if (c->remote_common_caps)
g_array_free(c->remote_common_caps, TRUE);
/* Chain up to the parent class */
if (G_OBJECT_CLASS(spice_channel_parent_class)->finalize)
G_OBJECT_CLASS(spice_channel_parent_class)->finalize(gobject);
}
static void spice_channel_get_property(GObject *gobject,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
SpiceChannel *channel = SPICE_CHANNEL(gobject);
SpiceChannelPrivate *c = channel->priv;
switch (prop_id) {
case PROP_SESSION:
g_value_set_object(value, c->session);
break;
case PROP_CHANNEL_TYPE:
g_value_set_int(value, c->channel_type);
break;
case PROP_CHANNEL_ID:
g_value_set_int(value, c->channel_id);
break;
case PROP_TOTAL_READ_BYTES:
g_value_set_ulong(value, c->total_read_bytes);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
break;
}
}
G_GNUC_INTERNAL
gint spice_channel_get_channel_id(SpiceChannel *channel)
{
SpiceChannelPrivate *c = SPICE_CHANNEL_GET_PRIVATE(channel);
g_return_val_if_fail(c != NULL, 0);
return c->channel_id;
}
G_GNUC_INTERNAL
gint spice_channel_get_channel_type(SpiceChannel *channel)
{
SpiceChannelPrivate *c = SPICE_CHANNEL_GET_PRIVATE(channel);
g_return_val_if_fail(c != NULL, 0);
return c->channel_type;
}
static void spice_channel_set_property(GObject *gobject,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
SpiceChannel *channel = SPICE_CHANNEL(gobject);
SpiceChannelPrivate *c = channel->priv;
switch (prop_id) {
case PROP_SESSION:
c->session = g_value_dup_object(value);
break;
case PROP_CHANNEL_TYPE:
c->channel_type = g_value_get_int(value);
break;
case PROP_CHANNEL_ID:
c->channel_id = g_value_get_int(value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
break;
}
}
static void spice_channel_class_init(SpiceChannelClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
klass->iterate_write = spice_channel_iterate_write;
klass->iterate_read = spice_channel_iterate_read;
klass->channel_disconnect = channel_disconnect;
klass->channel_reset = channel_reset;
gobject_class->constructed = spice_channel_constructed;
gobject_class->dispose = spice_channel_dispose;
gobject_class->finalize = spice_channel_finalize;
gobject_class->get_property = spice_channel_get_property;
gobject_class->set_property = spice_channel_set_property;
klass->handle_msg = spice_channel_handle_msg;
g_object_class_install_property
(gobject_class, PROP_SESSION,
g_param_spec_object("spice-session",
"Spice session",
"",
SPICE_TYPE_SESSION,
G_PARAM_READWRITE |
G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_STRINGS));
g_object_class_install_property
(gobject_class, PROP_CHANNEL_TYPE,
g_param_spec_int("channel-type",
"Channel type",
"",
-1, INT_MAX, -1,
G_PARAM_READWRITE |
G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_STRINGS));
g_object_class_install_property
(gobject_class, PROP_CHANNEL_ID,
g_param_spec_int("channel-id",
"Channel ID",
"",
-1, INT_MAX, -1,
G_PARAM_READWRITE |
G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_STRINGS));
g_object_class_install_property
(gobject_class, PROP_TOTAL_READ_BYTES,
g_param_spec_ulong("total-read-bytes",
"Total read bytes",
"",
0, G_MAXULONG, 0,
G_PARAM_READABLE |
G_PARAM_STATIC_STRINGS));
/**
* SpiceChannel::channel-event:
* @channel: the channel that emitted the signal
* @event: a #SpiceChannelEvent
*
* The #SpiceChannel::channel-event signal is emitted when the
* state of the connection change.
**/
signals[SPICE_CHANNEL_EVENT] =
g_signal_new("channel-event",
G_OBJECT_CLASS_TYPE(gobject_class),
G_SIGNAL_RUN_FIRST,
G_STRUCT_OFFSET(SpiceChannelClass, channel_event),
NULL, NULL,
g_cclosure_marshal_VOID__ENUM,
G_TYPE_NONE,
1,
SPICE_TYPE_CHANNEL_EVENT);
/**
* SpiceChannel::open-fd:
* @channel: the channel that emitted the signal
* @with_tls: wether TLS connection is requested
*
* The #SpiceChannel::open-fd signal is emitted when a new
* connection is requested. This signal is emitted when the
* connection is made with spice_session_open_fd().
**/
signals[SPICE_CHANNEL_OPEN_FD] =
g_signal_new("open-fd",
G_OBJECT_CLASS_TYPE(gobject_class),
G_SIGNAL_RUN_FIRST,
G_STRUCT_OFFSET(SpiceChannelClass, open_fd),
NULL, NULL,
g_cclosure_marshal_VOID__INT,
G_TYPE_NONE,
1,
G_TYPE_INT);
g_type_class_add_private(klass, sizeof(SpiceChannelPrivate));
SSL_library_init();
SSL_load_error_strings();
}
/* ---------------------------------------------------------------- */
/* private header api */
static inline void spice_header_set_msg_type(uint8_t *header, gboolean is_mini_header,
uint16_t type)
{
if (is_mini_header) {
((SpiceMiniDataHeader *)header)->type = type;
} else {
((SpiceDataHeader *)header)->type = type;
}
}
static inline void spice_header_set_msg_size(uint8_t *header, gboolean is_mini_header,
uint32_t size)
{
if (is_mini_header) {
((SpiceMiniDataHeader *)header)->size = size;
} else {
((SpiceDataHeader *)header)->size = size;
}
}
G_GNUC_INTERNAL
uint16_t spice_header_get_msg_type(uint8_t *header, gboolean is_mini_header)
{
if (is_mini_header) {
return ((SpiceMiniDataHeader *)header)->type;
} else {
return ((SpiceDataHeader *)header)->type;
}
}
G_GNUC_INTERNAL
uint32_t spice_header_get_msg_size(uint8_t *header, gboolean is_mini_header)
{
if (is_mini_header) {
return ((SpiceMiniDataHeader *)header)->size;
} else {
return ((SpiceDataHeader *)header)->size;
}
}
static inline int spice_header_get_header_size(gboolean is_mini_header)
{
return is_mini_header ? sizeof(SpiceMiniDataHeader) : sizeof(SpiceDataHeader);
}
static inline void spice_header_set_msg_serial(uint8_t *header, gboolean is_mini_header,
uint64_t serial)
{
if (!is_mini_header) {
((SpiceDataHeader *)header)->serial = serial;
}
}
static inline void spice_header_reset_msg_sub_list(uint8_t *header, gboolean is_mini_header)
{
if (!is_mini_header) {
((SpiceDataHeader *)header)->sub_list = 0;
}
}
static inline uint64_t spice_header_get_in_msg_serial(SpiceMsgIn *in)
{
SpiceChannelPrivate *c = in->channel->priv;
uint8_t *header = in->header;
if (c->use_mini_header) {
return c->in_serial;
} else {
return ((SpiceDataHeader *)header)->serial;
}
}
static inline uint64_t spice_header_get_out_msg_serial(SpiceMsgOut *out)
{
SpiceChannelPrivate *c = out->channel->priv;
if (c->use_mini_header) {
return c->out_serial;
} else {
return ((SpiceDataHeader *)out->header)->serial;
}
}
static inline uint32_t spice_header_get_msg_sub_list(uint8_t *header, gboolean is_mini_header)
{
if (is_mini_header) {
return 0;
} else {
return ((SpiceDataHeader *)header)->sub_list;
}
}
/* ---------------------------------------------------------------- */
/* private msg api */
G_GNUC_INTERNAL
SpiceMsgIn *spice_msg_in_new(SpiceChannel *channel)
{
SpiceMsgIn *in;
g_return_val_if_fail(channel != NULL, NULL);
in = spice_new0(SpiceMsgIn, 1);
in->refcount = 1;
in->channel = channel;
return in;
}
G_GNUC_INTERNAL
SpiceMsgIn *spice_msg_in_sub_new(SpiceChannel *channel, SpiceMsgIn *parent,
SpiceSubMessage *sub)
{
SpiceMsgIn *in;
g_return_val_if_fail(channel != NULL, NULL);
in = spice_msg_in_new(channel);
spice_header_set_msg_type(in->header, channel->priv->use_mini_header, sub->type);
spice_header_set_msg_size(in->header, channel->priv->use_mini_header, sub->size);
in->data = (uint8_t*)(sub+1);
in->dpos = sub->size;
in->parent = parent;
spice_msg_in_ref(parent);
return in;
}
G_GNUC_INTERNAL
void spice_msg_in_ref(SpiceMsgIn *in)
{
g_return_if_fail(in != NULL);
in->refcount++;
}
G_GNUC_INTERNAL
void spice_msg_in_unref(SpiceMsgIn *in)
{
g_return_if_fail(in != NULL);
in->refcount--;
if (in->refcount > 0)
return;
if (in->parsed)
in->pfree(in->parsed);
if (in->parent) {
spice_msg_in_unref(in->parent);
} else {
free(in->data);
}
free(in);
}
G_GNUC_INTERNAL
int spice_msg_in_type(SpiceMsgIn *in)
{
g_return_val_if_fail(in != NULL, -1);
return spice_header_get_msg_type(in->header, in->channel->priv->use_mini_header);
}
G_GNUC_INTERNAL
void *spice_msg_in_parsed(SpiceMsgIn *in)
{
g_return_val_if_fail(in != NULL, NULL);
return in->parsed;
}
G_GNUC_INTERNAL
void *spice_msg_in_raw(SpiceMsgIn *in, int *len)
{
g_return_val_if_fail(in != NULL, NULL);
g_return_val_if_fail(len != NULL, NULL);
*len = in->dpos;
return in->data;
}
static void hexdump(const char *prefix, unsigned char *data, int len)
{
int i;
for (i = 0; i < len; i++) {
if (i % 16 == 0)
fprintf(stderr, "%s:", prefix);
if (i % 4 == 0)
fprintf(stderr, " ");
fprintf(stderr, " %02x", data[i]);
if (i % 16 == 15)
fprintf(stderr, "\n");
}
if (i % 16 != 0)
fprintf(stderr, "\n");
}
G_GNUC_INTERNAL
void spice_msg_in_hexdump(SpiceMsgIn *in)
{
SpiceChannelPrivate *c = in->channel->priv;
fprintf(stderr, "--\n<< hdr: %s serial %" PRIu64 " type %d size %d sub-list %d\n",
c->name, spice_header_get_in_msg_serial(in),
spice_header_get_msg_type(in->header, c->use_mini_header),
spice_header_get_msg_size(in->header, c->use_mini_header),
spice_header_get_msg_sub_list(in->header, c->use_mini_header));
hexdump("<< msg", in->data, in->dpos);
}
G_GNUC_INTERNAL
void spice_msg_out_hexdump(SpiceMsgOut *out, unsigned char *data, int len)
{
SpiceChannelPrivate *c = out->channel->priv;
fprintf(stderr, "--\n>> hdr: %s serial %" PRIu64 " type %d size %d sub-list %d\n",
c->name,
spice_header_get_out_msg_serial(out),
spice_header_get_msg_type(out->header, c->use_mini_header),
spice_header_get_msg_size(out->header, c->use_mini_header),
spice_header_get_msg_sub_list(out->header, c->use_mini_header));
hexdump(">> msg", data, len);
}
static gboolean msg_check_read_only (int channel_type, int msg_type)
{
if (msg_type < 100) // those are the common messages
return FALSE;
switch (channel_type) {
/* messages allowed to be sent in read-only mode */
case SPICE_CHANNEL_MAIN:
switch (msg_type) {
case SPICE_MSGC_MAIN_CLIENT_INFO:
case SPICE_MSGC_MAIN_MIGRATE_CONNECTED:
case SPICE_MSGC_MAIN_MIGRATE_CONNECT_ERROR:
case SPICE_MSGC_MAIN_ATTACH_CHANNELS:
case SPICE_MSGC_MAIN_MIGRATE_END:
return FALSE;
}
break;
case SPICE_CHANNEL_DISPLAY:
return FALSE;
}
return TRUE;
}
G_GNUC_INTERNAL
SpiceMsgOut *spice_msg_out_new(SpiceChannel *channel, int type)
{
SpiceChannelPrivate *c = channel->priv;
SpiceMsgOut *out;
g_return_val_if_fail(c != NULL, NULL);
out = spice_new0(SpiceMsgOut, 1);
out->refcount = 1;
out->channel = channel;
out->ro_check = msg_check_read_only(c->channel_type, type);
out->marshallers = c->marshallers;
out->marshaller = spice_marshaller_new();
out->header = spice_marshaller_reserve_space(out->marshaller,
spice_header_get_header_size(c->use_mini_header));
spice_marshaller_set_base(out->marshaller, spice_header_get_header_size(c->use_mini_header));
spice_header_set_msg_type(out->header, c->use_mini_header, type);
spice_header_set_msg_serial(out->header, c->use_mini_header, c->out_serial);
spice_header_reset_msg_sub_list(out->header, c->use_mini_header);
c->out_serial++;
return out;
}
G_GNUC_INTERNAL
void spice_msg_out_ref(SpiceMsgOut *out)
{
g_return_if_fail(out != NULL);
out->refcount++;
}
G_GNUC_INTERNAL
void spice_msg_out_unref(SpiceMsgOut *out)
{
g_return_if_fail(out != NULL);
out->refcount--;
if (out->refcount > 0)
return;
spice_marshaller_destroy(out->marshaller);
free(out);
}
/* system context */
static gboolean spice_channel_idle_wakeup(gpointer user_data)
{
SpiceChannel *channel = SPICE_CHANNEL(user_data);
SpiceChannelPrivate *c = channel->priv;
/*
* Note:
*
* - This must be done before the wakeup as that may eventually
* call channel_reset() which checks this.
* - The lock calls are really necessary, this fixes the following race:
* 1) usb-event-thread calls spice_msg_out_send()
* 2) spice_msg_out_send calls g_timeout_add_full(...)
* 3) we run, set xmit_queue_wakeup_id to 0
* 4) spice_msg_out_send stores the result of g_timeout_add_full() in
* xmit_queue_wakeup_id, overwriting the 0 we just stored
* 5) xmit_queue_wakeup_id now says there is a wakeup pending which is
* false
*/
STATIC_MUTEX_LOCK(c->xmit_queue_lock);
c->xmit_queue_wakeup_id = 0;
STATIC_MUTEX_UNLOCK(c->xmit_queue_lock);
spice_channel_wakeup(channel, FALSE);
return FALSE;
}
/* any context (system/co-routine/usb-event-thread) */
G_GNUC_INTERNAL
void spice_msg_out_send(SpiceMsgOut *out)
{
SpiceChannelPrivate *c;
gboolean was_empty;
g_return_if_fail(out != NULL);
g_return_if_fail(out->channel != NULL);
c = out->channel->priv;
STATIC_MUTEX_LOCK(c->xmit_queue_lock);
if (c->xmit_queue_blocked) {
g_warning("message queue is blocked, dropping message");
goto end;
}
was_empty = g_queue_is_empty(&c->xmit_queue);
g_queue_push_tail(&c->xmit_queue, out);
/* One wakeup is enough to empty the entire queue -> only do a wakeup
if the queue was empty, and there isn't one pending already. */
if (was_empty && !c->xmit_queue_wakeup_id) {
c->xmit_queue_wakeup_id =
/* Use g_timeout_add_full so that can specify the priority */
g_timeout_add_full(G_PRIORITY_HIGH, 0,
spice_channel_idle_wakeup,
out->channel, NULL);
}
end:
STATIC_MUTEX_UNLOCK(c->xmit_queue_lock);
}
/* coroutine context */
G_GNUC_INTERNAL
void spice_msg_out_send_internal(SpiceMsgOut *out)
{
g_return_if_fail(out != NULL);
spice_channel_write_msg(out->channel, out);
}
/* ---------------------------------------------------------------- */
struct SPICE_CHANNEL_EVENT {
SpiceChannelEvent event;
};
/* main context */
static void do_emit_main_context(GObject *object, int signum, gpointer params)
{
switch (signum) {
case SPICE_CHANNEL_EVENT: {
struct SPICE_CHANNEL_EVENT *p = params;
g_signal_emit(object, signals[signum], 0, p->event);
break;
}
case SPICE_CHANNEL_OPEN_FD:
g_warning("this signal is only sent directly from main context");
break;
default:
g_warn_if_reached();
}
}
/*
* Write all 'data' of length 'datalen' bytes out to
* the wire
*/
/* coroutine context */
static void spice_channel_flush_wire(SpiceChannel *channel,
const void *data,
size_t datalen)
{
SpiceChannelPrivate *c = channel->priv;
const char *ptr = data;
size_t offset = 0;
GIOCondition cond;
while (offset < datalen) {
int ret;
if (c->has_error) return;
cond = 0;
if (c->tls) {
ret = SSL_write(c->ssl, ptr+offset, datalen-offset);
if (ret < 0) {
ret = SSL_get_error(c->ssl, ret);
if (ret == SSL_ERROR_WANT_READ)
cond |= G_IO_IN;
if (ret == SSL_ERROR_WANT_WRITE)
cond |= G_IO_OUT;
ret = -1;
}
} else {
GError *error = NULL;
ret = g_socket_send(c->sock, ptr+offset, datalen-offset,
NULL, &error);
if (ret < 0) {
if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) {
cond = G_IO_OUT;
} else {
CHANNEL_DEBUG(channel, "Send error %s", error->message);
}
g_clear_error(&error);
ret = -1;
}
}
if (ret == -1) {
if (cond != 0) {
g_coroutine_socket_wait(&c->coroutine, c->sock, cond);
continue;
} else {
CHANNEL_DEBUG(channel, "Closing the channel: spice_channel_flush %d", errno);
c->has_error = TRUE;
return;
}
}
if (ret == 0) {
CHANNEL_DEBUG(channel, "Closing the connection: spice_channel_flush");
c->has_error = TRUE;
return;
}
offset += ret;
}
}
#if HAVE_SASL
/*
* Encode all buffered data, write all encrypted data out
* to the wire
*/
static void spice_channel_flush_sasl(SpiceChannel *channel, const void *data, size_t len)
{
SpiceChannelPrivate *c = channel->priv;
const char *output;
unsigned int outputlen;
int err;
err = sasl_encode(c->sasl_conn, data, len, &output, &outputlen);
if (err != SASL_OK) {
g_warning ("Failed to encode SASL data %s",
sasl_errstring(err, NULL, NULL));
c->has_error = TRUE;
return;
}
//CHANNEL_DEBUG(channel, "Flush SASL %d: %p %d", len, output, outputlen);
spice_channel_flush_wire(channel, output, outputlen);
}
#endif
/* coroutine context */
static void spice_channel_write(SpiceChannel *channel, const void *data, size_t len)
{
#if HAVE_SASL
SpiceChannelPrivate *c = channel->priv;
if (c->sasl_conn)
spice_channel_flush_sasl(channel, data, len);
else
#endif
spice_channel_flush_wire(channel, data, len);
}
/* coroutine context */
static void spice_channel_write_msg(SpiceChannel *channel, SpiceMsgOut *out)
{
uint8_t *data;
int free_data;
size_t len;
uint32_t msg_size;
g_return_if_fail(channel != NULL);
g_return_if_fail(out != NULL);
g_return_if_fail(channel == out->channel);
if (out->ro_check &&
spice_channel_get_read_only(channel)) {
g_warning("Try to send message while read-only. Please report a bug.");
return;
}
msg_size = spice_marshaller_get_total_size(out->marshaller) -
spice_header_get_header_size(channel->priv->use_mini_header);
spice_header_set_msg_size(out->header, channel->priv->use_mini_header, msg_size);
data = spice_marshaller_linearize(out->marshaller, 0, &len, &free_data);
/* spice_msg_out_hexdump(out, data, len); */
spice_channel_write(channel, data, len);
if (free_data)
free(data);
spice_msg_out_unref(out);
}
/*
* Read at least 1 more byte of data straight off the wire
* into the requested buffer.
*/
/* coroutine context */
static int spice_channel_read_wire(SpiceChannel *channel, void *data, size_t len)
{
SpiceChannelPrivate *c = channel->priv;
int ret;
GIOCondition cond;
reread:
if (c->has_error) return 0; /* has_error is set by disconnect(), return no error */
cond = 0;
if (c->tls) {
ret = SSL_read(c->ssl, data, len);
if (ret < 0) {
ret = SSL_get_error(c->ssl, ret);
if (ret == SSL_ERROR_WANT_READ)
cond |= G_IO_IN;
if (ret == SSL_ERROR_WANT_WRITE)
cond |= G_IO_OUT;
ret = -1;
}
} else {
GError *error = NULL;
ret = g_socket_receive(c->sock, data, len, NULL, &error);
if (ret < 0) {
if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) {
cond = G_IO_IN;
} else {
CHANNEL_DEBUG(channel, "Read error %s", error->message);
}
g_clear_error(&error);
ret = -1;
}
}
if (ret == -1) {
if (cond != 0) {
g_coroutine_socket_wait(&c->coroutine, c->sock, cond);
goto reread;
} else {
c->has_error = TRUE;
return -errno;
}
}
if (ret == 0) {
CHANNEL_DEBUG(channel, "Closing the connection: spice_channel_read() - ret=0");
c->has_error = TRUE;
return 0;
}
return ret;
}
#if HAVE_SASL
/*
* Read at least 1 more byte of data out of the SASL decrypted
* data buffer, into the internal read buffer
*/
static int spice_channel_read_sasl(SpiceChannel *channel, void *data, size_t len)
{
SpiceChannelPrivate *c = channel->priv;
/* CHANNEL_DEBUG(channel, "Read %lu SASL %p size %d offset %d", len, c->sasl_decoded, */
/* c->sasl_decoded_length, c->sasl_decoded_offset); */
if (c->sasl_decoded == NULL || c->sasl_decoded_length == 0) {
char encoded[8192]; /* should stay lower than maxbufsize */
int err, ret;
g_warn_if_fail(c->sasl_decoded_offset == 0);
ret = spice_channel_read_wire(channel, encoded, sizeof(encoded));
if (ret < 0)
return ret;
err = sasl_decode(c->sasl_conn, encoded, ret,
&c->sasl_decoded, &c->sasl_decoded_length);
if (err != SASL_OK) {
g_warning("Failed to decode SASL data %s",
sasl_errstring(err, NULL, NULL));
c->has_error = TRUE;
return -EINVAL;
}
c->sasl_decoded_offset = 0;
}
if (c->sasl_decoded_length == 0)
return 0;
len = MIN(c->sasl_decoded_length - c->sasl_decoded_offset, len);
memcpy(data, c->sasl_decoded + c->sasl_decoded_offset, len);
c->sasl_decoded_offset += len;
if (c->sasl_decoded_offset == c->sasl_decoded_length) {
c->sasl_decoded_length = c->sasl_decoded_offset = 0;
c->sasl_decoded = NULL;
}
return len;
}
#endif
/*
* Fill the 'data' buffer up with exactly 'len' bytes worth of data
*/
/* coroutine context */
static int spice_channel_read(SpiceChannel *channel, void *data, size_t length)
{
SpiceChannelPrivate *c = channel->priv;
gsize len = length;
int ret;
while (len > 0) {
if (c->has_error) return 0; /* has_error is set by disconnect(), return no error */
#if HAVE_SASL
if (c->sasl_conn)
ret = spice_channel_read_sasl(channel, data, len);
else
#endif
ret = spice_channel_read_wire(channel, data, len);
if (ret < 0)
return ret;
g_assert(ret <= len);
len -= ret;
data = ((char*)data) + ret;
#if DEBUG
if (len > 0)
CHANNEL_DEBUG(channel, "still needs %" G_GSIZE_FORMAT, len);
#endif
}
c->total_read_bytes += length;
return length;
}
/* coroutine context */
static void spice_channel_send_spice_ticket(SpiceChannel *channel)
{
SpiceChannelPrivate *c = channel->priv;
EVP_PKEY *pubkey;
int nRSASize;
BIO *bioKey;
RSA *rsa;
char *password;
uint8_t *encrypted;
int rc;
bioKey = BIO_new(BIO_s_mem());
g_return_if_fail(bioKey != NULL);
BIO_write(bioKey, c->peer_msg->pub_key, SPICE_TICKET_PUBKEY_BYTES);
pubkey = d2i_PUBKEY_bio(bioKey, NULL);
g_return_if_fail(pubkey != NULL);
rsa = pubkey->pkey.rsa;
nRSASize = RSA_size(rsa);
encrypted = g_alloca(nRSASize);
/*
The use of RSA encryption limit the potential maximum password length.
for RSA_PKCS1_OAEP_PADDING it is RSA_size(rsa) - 41.
*/
g_object_get(c->session, "password", &password, NULL);
if (password == NULL)
password = g_strdup("");
rc = RSA_public_encrypt(strlen(password) + 1, (uint8_t*)password,
encrypted, rsa, RSA_PKCS1_OAEP_PADDING);
g_warn_if_fail(rc > 0);
spice_channel_write(channel, encrypted, nRSASize);
memset(encrypted, 0, nRSASize);
EVP_PKEY_free(pubkey);
BIO_free(bioKey);
g_free(password);
}
/* coroutine context */
static void spice_channel_recv_auth(SpiceChannel *channel)
{
SpiceChannelPrivate *c = channel->priv;
uint32_t link_res;
int rc;
rc = spice_channel_read(channel, &link_res, sizeof(link_res));
if (rc != sizeof(link_res)) {
CHANNEL_DEBUG(channel, "incomplete auth reply (%d/%" G_GSIZE_FORMAT ")",
rc, sizeof(link_res));
emit_main_context(channel, SPICE_CHANNEL_EVENT, SPICE_CHANNEL_ERROR_LINK);
return;
}
if (link_res != SPICE_LINK_ERR_OK) {
CHANNEL_DEBUG(channel, "link result: reply %d", link_res);
emit_main_context(channel, SPICE_CHANNEL_EVENT, SPICE_CHANNEL_ERROR_AUTH);
return;
}
c->state = SPICE_CHANNEL_STATE_READY;
emit_main_context(channel, SPICE_CHANNEL_EVENT, SPICE_CHANNEL_OPENED);
if (c->state == SPICE_CHANNEL_STATE_MIGRATION_HANDSHAKE) {
spice_channel_send_migration_handshake(channel);
}
if (c->state != SPICE_CHANNEL_STATE_MIGRATING)
spice_channel_up(channel);
}
G_GNUC_INTERNAL
void spice_channel_up(SpiceChannel *channel)
{
SpiceChannelPrivate *c = channel->priv;
CHANNEL_DEBUG(channel, "channel up, state %d", c->state);
if (SPICE_CHANNEL_GET_CLASS(channel)->channel_up)
SPICE_CHANNEL_GET_CLASS(channel)->channel_up(channel);
}
/* coroutine context */
static void spice_channel_send_link(SpiceChannel *channel)
{
SpiceChannelPrivate *c = channel->priv;
uint8_t *buffer, *p;
int protocol, i;
c->link_hdr.magic = SPICE_MAGIC;
c->link_hdr.size = sizeof(c->link_msg);
g_object_get(c->session, "protocol", &protocol, NULL);
switch (protocol) {
case 1: /* protocol 1 == major 1, old 0.4 protocol, last active minor */
c->link_hdr.major_version = 1;
c->link_hdr.minor_version = 3;
c->parser = spice_get_server_channel_parser1(c->channel_type, NULL);
c->marshallers = spice_message_marshallers_get1();
break;
case SPICE_VERSION_MAJOR: /* protocol 2 == current */
c->link_hdr.major_version = SPICE_VERSION_MAJOR;
c->link_hdr.minor_version = SPICE_VERSION_MINOR;
c->parser = spice_get_server_channel_parser(c->channel_type, NULL);
c->marshallers = spice_message_marshallers_get();
break;
default:
g_critical("unknown major %d", protocol);
return;
}
c->link_msg.connection_id = c->connection_id;
c->link_msg.channel_type = c->channel_type;
c->link_msg.channel_id = c->channel_id;
c->link_msg.caps_offset = sizeof(c->link_msg);
c->link_msg.num_common_caps = c->common_caps->len;
c->link_msg.num_channel_caps = c->caps->len;
c->link_hdr.size += (c->link_msg.num_common_caps +
c->link_msg.num_channel_caps) * sizeof(uint32_t);
buffer = spice_malloc(sizeof(c->link_hdr) + c->link_hdr.size);
p = buffer;
memcpy(p, &c->link_hdr, sizeof(c->link_hdr)); p += sizeof(c->link_hdr);
memcpy(p, &c->link_msg, sizeof(c->link_msg)); p += sizeof(c->link_msg);
for (i = 0; i < c->common_caps->len; i++) {
*(uint32_t *)p = g_array_index(c->common_caps, uint32_t, i);
p += sizeof(uint32_t);
}
for (i = 0; i < c->caps->len; i++) {
*(uint32_t *)p = g_array_index(c->caps, uint32_t, i);
p += sizeof(uint32_t);
}
CHANNEL_DEBUG(channel, "channel type %d id %d num common caps %d num caps %d",
c->link_msg.channel_type,
c->link_msg.channel_id,
c->link_msg.num_common_caps,
c->link_msg.num_channel_caps);
spice_channel_write(channel, buffer, p - buffer);
free(buffer);
}
static void spice_channel_switch_protocol(SpiceChannel *channel, gint version)
{
SpiceChannelPrivate *c = channel->priv;
g_object_set(c->session, "protocol", version, NULL);
SPICE_CHANNEL_GET_CLASS(channel)->channel_disconnect(channel);
spice_channel_connect(channel);
}
/* coroutine context */
static void spice_channel_recv_link_hdr(SpiceChannel *channel)
{
SpiceChannelPrivate *c = channel->priv;
int rc;
rc = spice_channel_read(channel, &c->peer_hdr, sizeof(c->peer_hdr));
if (rc != sizeof(c->peer_hdr)) {
g_warning("incomplete link header (%d/%" G_GSIZE_FORMAT ")",
rc, sizeof(c->peer_hdr));
goto error;
}
if (c->peer_hdr.magic != SPICE_MAGIC) {
g_warning("invalid SPICE_MAGIC!");
goto error;
}
CHANNEL_DEBUG(channel, "Peer version: %d:%d", c->peer_hdr.major_version, c->peer_hdr.minor_version);
if (c->peer_hdr.major_version != c->link_hdr.major_version) {
g_warning("major mismatch (got %d, expected %d)",
c->peer_hdr.major_version, c->link_hdr.major_version);
goto error;
}
c->peer_msg = spice_malloc(c->peer_hdr.size);
if (c->peer_msg == NULL) {
g_warning("invalid peer header size: %u", c->peer_hdr.size);
goto error;
}
return;
error:
/* Windows socket seems to give early CONNRESET errors. The server
does not linger when closing the socket if the protocol is
incompatible. Try with the oldest protocol in this case: */
if (c->link_hdr.major_version != 1) {
SPICE_DEBUG("%s: error, switching to protocol 1 (spice 0.4)", c->name);
spice_channel_switch_protocol(channel, 1);
return;
}
emit_main_context(channel, SPICE_CHANNEL_EVENT, SPICE_CHANNEL_ERROR_LINK);
}
#if HAVE_SASL
/*
* NB, keep in sync with similar method in spice/server/reds.c
*/
static gchar *addr_to_string(GSocketAddress *addr)
{
GInetSocketAddress *iaddr = G_INET_SOCKET_ADDRESS(addr);
guint16 port;
GInetAddress *host;
gchar *hoststr;
gchar *ret;
host = g_inet_socket_address_get_address(iaddr);
port = g_inet_socket_address_get_port(iaddr);
hoststr = g_inet_address_to_string(host);
ret = g_strdup_printf("%s;%hu", hoststr, port);
g_free(hoststr);
return ret;
}
static gboolean
spice_channel_gather_sasl_credentials(SpiceChannel *channel,
sasl_interact_t *interact)
{
SpiceChannelPrivate *c;
int ninteract;
g_return_val_if_fail(channel != NULL, FALSE);
g_return_val_if_fail(channel->priv != NULL, FALSE);
c = channel->priv;
/* FIXME: we could keep connection open and ask connection details if missing */
for (ninteract = 0 ; interact[ninteract].id != 0 ; ninteract++) {
switch (interact[ninteract].id) {
case SASL_CB_AUTHNAME:
case SASL_CB_USER:
g_warn_if_reached();
break;
case SASL_CB_PASS:
if (spice_session_get_password(c->session) == NULL)
return FALSE;
interact[ninteract].result = spice_session_get_password(c->session);
interact[ninteract].len = strlen(interact[ninteract].result);
break;
}
}
CHANNEL_DEBUG(channel, "Filled SASL interact");
return TRUE;
}
/*
*
* Init msg from server
*
* u32 mechlist-length
* u8-array mechlist-string
*
* Start msg to server
*
* u32 mechname-length
* u8-array mechname-string
* u32 clientout-length
* u8-array clientout-string
*
* Start msg from server
*
* u32 serverin-length
* u8-array serverin-string
* u8 continue
*
* Step msg to server
*
* u32 clientout-length
* u8-array clientout-string
*
* Step msg from server
*
* u32 serverin-length
* u8-array serverin-string
* u8 continue
*/
#define SASL_MAX_MECHLIST_LEN 300
#define SASL_MAX_MECHNAME_LEN 100
#define SASL_MAX_DATA_LEN (1024 * 1024)
/* Perform the SASL authentication process
*/
static gboolean spice_channel_perform_auth_sasl(SpiceChannel *channel)
{
SpiceChannelPrivate *c;
sasl_conn_t *saslconn = NULL;
sasl_security_properties_t secprops;
const char *clientout;
char *serverin = NULL;
unsigned int clientoutlen;
int err;
char *localAddr = NULL, *remoteAddr = NULL;
const void *val;
sasl_ssf_t ssf;
sasl_callback_t saslcb[] = {
{ .id = SASL_CB_PASS },
{ .id = 0 },
};
sasl_interact_t *interact = NULL;
guint32 len;
char *mechlist;
const char *mechname;
gboolean ret = FALSE;
GSocketAddress *addr = NULL;
guint8 complete;
g_return_val_if_fail(channel != NULL, FALSE);
g_return_val_if_fail(channel->priv != NULL, FALSE);
c = channel->priv;
/* Sets up the SASL library as a whole */
err = sasl_client_init(NULL);
CHANNEL_DEBUG(channel, "Client initialize SASL authentication %d", err);
if (err != SASL_OK) {
g_critical("failed to initialize SASL library: %d (%s)",
err, sasl_errstring(err, NULL, NULL));
goto error;
}
/* Get local address in form IPADDR:PORT */
addr = g_socket_get_local_address(c->sock, NULL);
if (!addr) {
g_critical("failed to get local address");
goto error;
}
if ((g_socket_address_get_family(addr) == G_SOCKET_FAMILY_IPV4 ||
g_socket_address_get_family(addr) == G_SOCKET_FAMILY_IPV6) &&
(localAddr = addr_to_string(addr)) == NULL)
goto error;
g_clear_object(&addr);
/* Get remote address in form IPADDR:PORT */
addr = g_socket_get_remote_address(c->sock, NULL);
if (!addr) {
g_critical("failed to get peer address");
goto error;
}
if ((g_socket_address_get_family(addr) == G_SOCKET_FAMILY_IPV4 ||
g_socket_address_get_family(addr) == G_SOCKET_FAMILY_IPV6) &&
(remoteAddr = addr_to_string(addr)) == NULL)
goto error;
g_clear_object(&addr);
CHANNEL_DEBUG(channel, "Client SASL new host:'%s' local:'%s' remote:'%s'",
spice_session_get_host(c->session), localAddr, remoteAddr);
/* Setup a handle for being a client */
err = sasl_client_new("spice",
spice_session_get_host(c->session),
localAddr,
remoteAddr,
saslcb,
SASL_SUCCESS_DATA,
&saslconn);
g_free(localAddr);
g_free(remoteAddr);
if (err != SASL_OK) {
g_critical("Failed to create SASL client context: %d (%s)",
err, sasl_errstring(err, NULL, NULL));
goto error;
}
if (c->ssl) {
sasl_ssf_t ssf;
ssf = SSL_get_cipher_bits(c->ssl, NULL);
err = sasl_setprop(saslconn, SASL_SSF_EXTERNAL, &ssf);
if (err != SASL_OK) {
g_critical("cannot set SASL external SSF %d (%s)",
err, sasl_errstring(err, NULL, NULL));
goto error;
}
}
memset(&secprops, 0, sizeof secprops);
/* If we've got TLS, we don't care about SSF */
secprops.min_ssf = c->ssl ? 0 : 56; /* Equiv to DES supported by all Kerberos */
secprops.max_ssf = c->ssl ? 0 : 100000; /* Very strong ! AES == 256 */
secprops.maxbufsize = 100000;
/* If we're not TLS, then forbid any anonymous or trivially crackable auth */
secprops.security_flags = c->ssl ? 0 :
SASL_SEC_NOANONYMOUS | SASL_SEC_NOPLAINTEXT;
err = sasl_setprop(saslconn, SASL_SEC_PROPS, &secprops);
if (err != SASL_OK) {
g_critical("cannot set security props %d (%s)",
err, sasl_errstring(err, NULL, NULL));
goto error;
}
/* Get the supported mechanisms from the server */
spice_channel_read(channel, &len, sizeof(len));
if (c->has_error)
goto error;
if (len > SASL_MAX_MECHLIST_LEN) {
g_critical("mechlistlen %d too long", len);
goto error;
}
mechlist = g_malloc(len + 1);
spice_channel_read(channel, mechlist, len);
mechlist[len] = '\0';
if (c->has_error) {
g_free(mechlist);
mechlist = NULL;
goto error;
}
restart:
/* Start the auth negotiation on the client end first */
CHANNEL_DEBUG(channel, "Client start negotiation mechlist '%s'", mechlist);
err = sasl_client_start(saslconn,
mechlist,
&interact,
&clientout,
&clientoutlen,
&mechname);
if (err != SASL_OK && err != SASL_CONTINUE && err != SASL_INTERACT) {
g_critical("Failed to start SASL negotiation: %d (%s)",
err, sasl_errdetail(saslconn));
g_free(mechlist);
mechlist = NULL;
goto error;
}
/* Need to gather some credentials from the client */
if (err == SASL_INTERACT) {
if (!spice_channel_gather_sasl_credentials(channel, interact)) {
CHANNEL_DEBUG(channel, "Failed to collect auth credentials");
goto error;
}
goto restart;
}
CHANNEL_DEBUG(channel, "Server start negotiation with mech %s. Data %d bytes %p '%s'",
mechname, clientoutlen, clientout, clientout);
if (clientoutlen > SASL_MAX_DATA_LEN) {
g_critical("SASL negotiation data too long: %d bytes",
clientoutlen);
goto error;
}
/* Send back the chosen mechname */
len = strlen(mechname);
spice_channel_write(channel, &len, sizeof(guint32));
spice_channel_write(channel, mechname, len);
/* NB, distinction of NULL vs "" is *critical* in SASL */
if (clientout) {
len += clientoutlen + 1;
spice_channel_write(channel, &len, sizeof(guint32));
spice_channel_write(channel, clientout, len);
} else {
len = 0;
spice_channel_write(channel, &len, sizeof(guint32));
}
if (c->has_error)
goto error;
CHANNEL_DEBUG(channel, "Getting sever start negotiation reply");
/* Read the 'START' message reply from server */
spice_channel_read(channel, &len, sizeof(len));
if (c->has_error)
goto error;
if (len > SASL_MAX_DATA_LEN) {
g_critical("SASL negotiation data too long: %d bytes",
len);
goto error;
}
/* NB, distinction of NULL vs "" is *critical* in SASL */
if (len > 0) {
serverin = g_malloc(len);
spice_channel_read(channel, serverin, len);
serverin[len - 1] = '\0';
len--;
} else {
serverin = NULL;
}
spice_channel_read(channel, &complete, sizeof(guint8));
if (c->has_error)
goto error;
CHANNEL_DEBUG(channel, "Client start result complete: %d. Data %d bytes %p '%s'",
complete, len, serverin, serverin);
/* Loop-the-loop...
* Even if the server has completed, the client must *always* do at least one step
* in this loop to verify the server isn't lying about something. Mutual auth */
for (;;) {
restep:
err = sasl_client_step(saslconn,
serverin,
len,
&interact,
&clientout,
&clientoutlen);
if (err != SASL_OK && err != SASL_CONTINUE && err != SASL_INTERACT) {
g_critical("Failed SASL step: %d (%s)",
err, sasl_errdetail(saslconn));
goto error;
}
/* Need to gather some credentials from the client */
if (err == SASL_INTERACT) {
if (!spice_channel_gather_sasl_credentials(channel,
interact)) {
CHANNEL_DEBUG(channel, "%s", "Failed to collect auth credentials");
goto error;
}
goto restep;
}
if (serverin) {
g_free(serverin);
serverin = NULL;
}
CHANNEL_DEBUG(channel, "Client step result %d. Data %d bytes %p '%s'", err, clientoutlen, clientout, clientout);
/* Previous server call showed completion & we're now locally complete too */
if (complete && err == SASL_OK)
break;
/* Not done, prepare to talk with the server for another iteration */
/* NB, distinction of NULL vs "" is *critical* in SASL */
if (clientout) {
len = clientoutlen + 1;
spice_channel_write(channel, &len, sizeof(guint32));
spice_channel_write(channel, clientout, len);
} else {
len = 0;
spice_channel_write(channel, &len, sizeof(guint32));
}
if (c->has_error)
goto error;
CHANNEL_DEBUG(channel, "Server step with %d bytes %p", clientoutlen, clientout);
spice_channel_read(channel, &len, sizeof(guint32));
if (c->has_error)
goto error;
if (len > SASL_MAX_DATA_LEN) {
g_critical("SASL negotiation data too long: %d bytes", len);
goto error;
}
/* NB, distinction of NULL vs "" is *critical* in SASL */
if (len) {
serverin = g_malloc(len);
spice_channel_read(channel, serverin, len);
serverin[len - 1] = '\0';
len--;
} else {
serverin = NULL;
}
spice_channel_read(channel, &complete, sizeof(guint8));
if (c->has_error)
goto error;
CHANNEL_DEBUG(channel, "Client step result complete: %d. Data %d bytes %p '%s'",
complete, len, serverin, serverin);
/* This server call shows complete, and earlier client step was OK */
if (complete) {
g_free(serverin);
serverin = NULL;
if (err == SASL_CONTINUE) /* something went wrong */
goto complete;
break;
}
}
/* Check for suitable SSF if non-TLS */
if (!c->ssl) {
err = sasl_getprop(saslconn, SASL_SSF, &val);
if (err != SASL_OK) {
g_critical("cannot query SASL ssf on connection %d (%s)",
err, sasl_errstring(err, NULL, NULL));
goto error;
}
ssf = *(const int *)val;
CHANNEL_DEBUG(channel, "SASL SSF value %d", ssf);
if (ssf < 56) { /* 56 == DES level, good for Kerberos */
g_critical("negotiation SSF %d was not strong enough", ssf);
goto error;
}
}
complete:
CHANNEL_DEBUG(channel, "%s", "SASL authentication complete");
spice_channel_read(channel, &len, sizeof(len));
if (len != SPICE_LINK_ERR_OK)
emit_main_context(channel, SPICE_CHANNEL_EVENT, SPICE_CHANNEL_ERROR_AUTH);
ret = len == SPICE_LINK_ERR_OK;
/* This must come *after* check-auth-result, because the former
* is defined to be sent unencrypted, and setting saslconn turns
* on the SSF layer encryption processing */
c->sasl_conn = saslconn;
return ret;
error:
g_clear_object(&addr);
if (saslconn)
sasl_dispose(&saslconn);
emit_main_context(channel, SPICE_CHANNEL_EVENT, SPICE_CHANNEL_ERROR_AUTH);
c->has_error = TRUE; /* force disconnect */
return FALSE;
}
#endif /* HAVE_SASL */
/* coroutine context */
static void spice_channel_recv_link_msg(SpiceChannel *channel, gboolean *switch_tls)
{
SpiceChannelPrivate *c;
int rc, num_caps, i;
uint32_t *caps;
g_return_if_fail(channel != NULL);
g_return_if_fail(channel->priv != NULL);
c = channel->priv;
rc = spice_channel_read(channel, (uint8_t*)c->peer_msg + c->peer_pos,
c->peer_hdr.size - c->peer_pos);
c->peer_pos += rc;
if (c->peer_pos != c->peer_hdr.size) {
g_critical("%s: %s: incomplete link reply (%d/%d)",
c->name, __FUNCTION__, rc, c->peer_hdr.size);
emit_main_context(channel, SPICE_CHANNEL_EVENT, SPICE_CHANNEL_ERROR_LINK);
return;
}
switch (c->peer_msg->error) {
case SPICE_LINK_ERR_OK:
/* nothing */
break;
case SPICE_LINK_ERR_NEED_SECURED:
*switch_tls = true;
CHANNEL_DEBUG(channel, "switching to tls");
return;
default:
g_warning("%s: %s: unhandled error %d",
c->name, __FUNCTION__, c->peer_msg->error);
goto error;
}
num_caps = c->peer_msg->num_channel_caps + c->peer_msg->num_common_caps;
CHANNEL_DEBUG(channel, "%s: %d caps", __FUNCTION__, num_caps);
/* see original spice/client code: */
/* g_return_if_fail(c->peer_msg + c->peer_msg->caps_offset * sizeof(uint32_t) > c->peer_msg + c->peer_hdr.size); */
caps = (uint32_t *)((uint8_t *)c->peer_msg + c->peer_msg->caps_offset);
g_array_set_size(c->remote_common_caps, c->peer_msg->num_common_caps);
for (i = 0; i < c->peer_msg->num_common_caps; i++, caps++) {
g_array_index(c->remote_common_caps, uint32_t, i) = *caps;
CHANNEL_DEBUG(channel, "got common caps %u:0x%X", i, *caps);
}
g_array_set_size(c->remote_caps, c->peer_msg->num_channel_caps);
for (i = 0; i < c->peer_msg->num_channel_caps; i++, caps++) {
g_array_index(c->remote_caps, uint32_t, i) = *caps;
CHANNEL_DEBUG(channel, "got channel caps %u:0x%X", i, *caps);
}
if (!spice_channel_test_common_capability(channel,
SPICE_COMMON_CAP_PROTOCOL_AUTH_SELECTION)) {
CHANNEL_DEBUG(channel, "Server supports spice ticket auth only");
spice_channel_send_spice_ticket(channel);
} else {
SpiceLinkAuthMechanism auth = { 0, };
#if HAVE_SASL
if (spice_channel_test_common_capability(channel, SPICE_COMMON_CAP_AUTH_SASL)) {
CHANNEL_DEBUG(channel, "Choosing SASL mechanism");
auth.auth_mechanism = SPICE_COMMON_CAP_AUTH_SASL;
spice_channel_write(channel, &auth, sizeof(auth));
spice_channel_perform_auth_sasl(channel);
} else
#endif
if (spice_channel_test_common_capability(channel, SPICE_COMMON_CAP_AUTH_SPICE)) {
auth.auth_mechanism = SPICE_COMMON_CAP_AUTH_SPICE;
spice_channel_write(channel, &auth, sizeof(auth));
spice_channel_send_spice_ticket(channel);
} else {
g_warning("No compatible AUTH mechanism");
goto error;
}
}
c->use_mini_header = spice_channel_test_common_capability(channel,
SPICE_COMMON_CAP_MINI_HEADER);
CHANNEL_DEBUG(channel, "use mini header: %d", c->use_mini_header);
return;
error:
SPICE_CHANNEL_GET_CLASS(channel)->channel_disconnect(channel);
emit_main_context(channel, SPICE_CHANNEL_EVENT, SPICE_CHANNEL_ERROR_LINK);
}
/* system context */
G_GNUC_INTERNAL
void spice_channel_wakeup(SpiceChannel *channel, gboolean cancel)
{
GCoroutine *c = &channel->priv->coroutine;
if (cancel)
g_coroutine_condition_cancel(c);
g_coroutine_wakeup(c);
}
G_GNUC_INTERNAL
gboolean spice_channel_get_read_only(SpiceChannel *channel)
{
return spice_session_get_read_only(channel->priv->session);
}
/* coroutine context */
G_GNUC_INTERNAL
void spice_channel_recv_msg(SpiceChannel *channel,
handler_msg_in msg_handler, gpointer data)
{
SpiceChannelPrivate *c = channel->priv;
SpiceMsgIn *in;
int header_size;
int msg_size;
int msg_type;
int sub_list_offset = 0;
int rc;
if (!c->msg_in) {
c->msg_in = spice_msg_in_new(channel);
}
in = c->msg_in;
header_size = spice_header_get_header_size(c->use_mini_header);
/* receive message */
if (in->hpos < header_size) {
rc = spice_channel_read(channel, in->header + in->hpos,
header_size - in->hpos);
if (rc < 0) {
g_critical("recv hdr: %s", strerror(errno));
return;
}
in->hpos += rc;
if (in->hpos < header_size)
return;
in->data = spice_malloc(spice_header_get_msg_size(in->header, c->use_mini_header));
}
msg_size = spice_header_get_msg_size(in->header, c->use_mini_header);
if (in->dpos < msg_size) {
rc = spice_channel_read(channel, in->data + in->dpos,
msg_size - in->dpos);
if (rc < 0) {
g_critical("recv msg: %s", strerror(errno));
return;
}
in->dpos += rc;
if (in->dpos < msg_size)
return;
}
msg_type = spice_header_get_msg_type(in->header, c->use_mini_header);
sub_list_offset = spice_header_get_msg_sub_list(in->header, c->use_mini_header);
if (msg_type == SPICE_MSG_LIST || sub_list_offset) {
SpiceSubMessageList *sub_list;
SpiceSubMessage *sub;
SpiceMsgIn *sub_in;
int i;
sub_list = (SpiceSubMessageList *)(in->data + sub_list_offset);
for (i = 0; i < sub_list->size; i++) {
sub = (SpiceSubMessage *)(in->data + sub_list->sub_messages[i]);
sub_in = spice_msg_in_sub_new(channel, in, sub);
sub_in->parsed = c->parser(sub_in->data, sub_in->data + sub_in->dpos,
spice_header_get_msg_type(sub_in->header,
c->use_mini_header),
c->peer_hdr.minor_version,
&sub_in->psize, &sub_in->pfree);
if (sub_in->parsed == NULL) {
g_critical("failed to parse sub-message: %s type %d",
c->name, spice_header_get_msg_type(sub_in->header, c->use_mini_header));
return;
}
msg_handler(channel, sub_in, data);
spice_msg_in_unref(sub_in);
}
}
/* ack message */
if (c->message_ack_count) {
c->message_ack_count--;
if (!c->message_ack_count) {
SpiceMsgOut *out = spice_msg_out_new(channel, SPICE_MSGC_ACK);
spice_msg_out_send_internal(out);
c->message_ack_count = c->message_ack_window;
}
}
if (msg_type == SPICE_MSG_LIST) {
goto end;
}
/* parse message */
in->parsed = c->parser(in->data, in->data + in->dpos, msg_type,
c->peer_hdr.minor_version, &in->psize, &in->pfree);
if (in->parsed == NULL) {
g_critical("failed to parse message: %s type %d",
c->name, msg_type);
goto end;
}
/* process message */
c->msg_in = NULL; /* the function is reentrant, reset state */
/* spice_msg_in_hexdump(in); */
msg_handler(channel, in, data);
end:
/* If the server uses full header, the serial is not necessarily equal
* to c->in_serial (the server can sometimes skip serials) */
c->last_message_serial = spice_header_get_in_msg_serial(in);
c->in_serial++;
/* release message */
c->msg_in = NULL;
spice_msg_in_unref(in);
}
const gchar* spice_channel_type_to_string(gint type)
{
static const char *to_string[] = {
NULL,
[ SPICE_CHANNEL_MAIN ] = "main",
[ SPICE_CHANNEL_DISPLAY ] = "display",
[ SPICE_CHANNEL_INPUTS ] = "inputs",
[ SPICE_CHANNEL_CURSOR ] = "cursor",
[ SPICE_CHANNEL_PLAYBACK ] = "playback",
[ SPICE_CHANNEL_RECORD ] = "record",
[ SPICE_CHANNEL_TUNNEL ] = "tunnel",
[ SPICE_CHANNEL_SMARTCARD ] = "smartcard",
[ SPICE_CHANNEL_USBREDIR ] = "usbredir",
[ SPICE_CHANNEL_PORT ] = "port",
};
const char *str = NULL;
if (type >= 0 && type < G_N_ELEMENTS(to_string)) {
str = to_string[type];
}
return str ? str : "unknown channel type";
}
/**
* spice_channel_new:
* @s: the @SpiceSession the channel is linked to
* @type: the requested SPICECHANNELPRIVATE type
* @id: the channel-id
*
* Create a new #SpiceChannel of type @type, and channel ID @id.
*
* Returns: a weak reference to #SpiceChannel, the session owns the reference
**/
SpiceChannel *spice_channel_new(SpiceSession *s, int type, int id)
{
SpiceChannel *channel;
GType gtype = 0;
g_return_val_if_fail(s != NULL, NULL);
switch (type) {
case SPICE_CHANNEL_MAIN:
gtype = SPICE_TYPE_MAIN_CHANNEL;
break;
case SPICE_CHANNEL_DISPLAY:
gtype = SPICE_TYPE_DISPLAY_CHANNEL;
break;
case SPICE_CHANNEL_CURSOR:
gtype = SPICE_TYPE_CURSOR_CHANNEL;
break;
case SPICE_CHANNEL_INPUTS:
gtype = SPICE_TYPE_INPUTS_CHANNEL;
break;
case SPICE_CHANNEL_PLAYBACK:
case SPICE_CHANNEL_RECORD: {
if (!s->priv->audio) {
g_debug("audio channel is disabled, not creating it");
return NULL;
}
gtype = type == SPICE_CHANNEL_RECORD ?
SPICE_TYPE_RECORD_CHANNEL : SPICE_TYPE_PLAYBACK_CHANNEL;
break;
}
#ifdef USE_SMARTCARD
case SPICE_CHANNEL_SMARTCARD: {
if (!s->priv->smartcard) {
g_debug("smartcard channel is disabled, not creating it");
return NULL;
}
gtype = SPICE_TYPE_SMARTCARD_CHANNEL;
break;
}
#endif
#ifdef USE_USBREDIR
case SPICE_CHANNEL_USBREDIR: {
if (!s->priv->usbredir) {
g_debug("usbredir channel is disabled, not creating it");
return NULL;
}
gtype = SPICE_TYPE_USBREDIR_CHANNEL;
break;
}
#endif
case SPICE_CHANNEL_PORT:
gtype = SPICE_TYPE_PORT_CHANNEL;
break;
default:
g_debug("unsupported channel kind: %s: %d",
spice_channel_type_to_string(type), type);
return NULL;
}
channel = SPICE_CHANNEL(g_object_new(gtype,
"spice-session", s,
"channel-type", type,
"channel-id", id,
NULL));
return channel;
}
/**
* spice_channel_destroy:
* @channel:
*
* Disconnect and unref the @channel. Called by @spice_session_disconnect()
*
**/
void spice_channel_destroy(SpiceChannel *channel)
{
g_return_if_fail(channel != NULL);
CHANNEL_DEBUG(channel, "channel destroy");
spice_channel_disconnect(channel, SPICE_CHANNEL_NONE);
g_object_unref(channel);
}
/* any context */
static void spice_channel_flushed(SpiceChannel *channel, gboolean success)
{
SpiceChannelPrivate *c = channel->priv;
GSList *l;
for (l = c->flushing; l != NULL; l = l->next) {
GSimpleAsyncResult *result = G_SIMPLE_ASYNC_RESULT(l->data);
g_simple_async_result_set_op_res_gboolean(result, success);
g_simple_async_result_complete_in_idle(result);
}
g_slist_free_full(c->flushing, g_object_unref);
c->flushing = NULL;
}
/* coroutine context */
static void spice_channel_iterate_write(SpiceChannel *channel)
{
SpiceChannelPrivate *c = channel->priv;
SpiceMsgOut *out;
do {
STATIC_MUTEX_LOCK(c->xmit_queue_lock);
out = g_queue_pop_head(&c->xmit_queue);
STATIC_MUTEX_UNLOCK(c->xmit_queue_lock);
if (out)
spice_channel_write_msg(channel, out);
} while (out);
spice_channel_flushed(channel, TRUE);
}
/* coroutine context */
static void spice_channel_iterate_read(SpiceChannel *channel)
{
SpiceChannelPrivate *c = channel->priv;
g_return_if_fail(c->state != SPICE_CHANNEL_STATE_MIGRATING);
spice_channel_recv_msg(channel,
(handler_msg_in)SPICE_CHANNEL_GET_CLASS(channel)->handle_msg, NULL);
}
static gboolean wait_migration(gpointer data)
{
SpiceChannel *channel = SPICE_CHANNEL(data);
SpiceChannelPrivate *c = channel->priv;
if (c->state != SPICE_CHANNEL_STATE_MIGRATING) {
CHANNEL_DEBUG(channel, "unfreeze channel");
return TRUE;
}
return FALSE;
}
/* coroutine context */
static gboolean spice_channel_iterate(SpiceChannel *channel)
{
SpiceChannelPrivate *c = channel->priv;
GIOCondition ret;
do {
if (c->state == SPICE_CHANNEL_STATE_MIGRATING &&
!g_coroutine_condition_wait(&c->coroutine, wait_migration, channel))
CHANNEL_DEBUG(channel, "migration wait cancelled");
if (c->has_error) {
CHANNEL_DEBUG(channel, "channel has error, breaking loop");
return FALSE;
}
SPICE_CHANNEL_GET_CLASS(channel)->iterate_write(channel);
ret = g_coroutine_socket_wait(&c->coroutine, c->sock,
c->state != SPICE_CHANNEL_STATE_MIGRATING ? G_IO_IN : 0);
#ifdef WIN32
/* FIXME: windows gsocket is buggy, it doesn't return correct condition... */
ret = g_socket_condition_check(c->sock, G_IO_IN);
#endif
} while (ret == 0); /* ret == 0 means no IO condition, but woken up */
if (ret & G_IO_IN) {
do
SPICE_CHANNEL_GET_CLASS(channel)->iterate_read(channel);
#if HAVE_SASL
while (c->sasl_decoded != NULL);
#else
while (FALSE);
#endif
}
if (c->state > SPICE_CHANNEL_STATE_CONNECTING &&
ret & (G_IO_ERR|G_IO_HUP)) {
SPICE_DEBUG("got socket error: %d", ret);
emit_main_context(channel, SPICE_CHANNEL_EVENT,
c->state == SPICE_CHANNEL_STATE_READY ?
SPICE_CHANNEL_ERROR_IO : SPICE_CHANNEL_ERROR_LINK);
c->has_error = TRUE;
return FALSE;
}
return TRUE;
}
/* we use an idle function to allow the coroutine to exit before we actually
* unref the object since the coroutine's state is part of the object */
static gboolean spice_channel_delayed_unref(gpointer data)
{
SpiceChannel *channel = SPICE_CHANNEL(data);
SpiceChannelPrivate *c = channel->priv;
g_return_val_if_fail(channel != NULL, FALSE);
CHANNEL_DEBUG(channel, "Delayed unref channel %p", channel);
g_return_val_if_fail(c->coroutine.coroutine.exited == TRUE, FALSE);
g_object_unref(G_OBJECT(data));
return FALSE;
}
static X509_LOOKUP_METHOD spice_x509_mem_lookup = {
"spice_x509_mem_lookup",
0
};
static int spice_channel_load_ca(SpiceChannel *channel)
{
SpiceChannelPrivate *c = channel->priv;
STACK_OF(X509_INFO) *inf;
X509_INFO *itmp;
X509_LOOKUP *lookup;
BIO *in;
int i, count = 0;
guint8 *ca;
guint size;
const gchar *ca_file;
g_return_val_if_fail(c->ctx != NULL, 0);
lookup = X509_STORE_add_lookup(c->ctx->cert_store, &spice_x509_mem_lookup);
ca_file = spice_session_get_ca_file(c->session);
spice_session_get_ca(c->session, &ca, &size);
CHANNEL_DEBUG(channel, "Load CA, file: %s, data: %p", ca_file, ca);
g_warn_if_fail(ca_file || ca);
if (ca != NULL) {
in = BIO_new_mem_buf(ca, size);
inf = PEM_X509_INFO_read_bio(in, NULL, NULL, NULL);
BIO_free(in);
for (i = 0; i < sk_X509_INFO_num(inf); i++) {
itmp = sk_X509_INFO_value(inf, i);
if (itmp->x509) {
X509_STORE_add_cert(lookup->store_ctx, itmp->x509);
count++;
}
if (itmp->crl) {
X509_STORE_add_crl(lookup->store_ctx, itmp->crl);
count++;
}
}
sk_X509_INFO_pop_free(inf, X509_INFO_free);
}
if (ca_file != NULL) {
int rc = SSL_CTX_load_verify_locations(c->ctx, ca_file, NULL);
if (rc != 1)
g_warning("loading ca certs from %s failed", ca_file);
else
count++;
}
return count;
}
/* coroutine context */
static void *spice_channel_coroutine(void *data)
{
SpiceChannel *channel = SPICE_CHANNEL(data);
SpiceChannelPrivate *c = channel->priv;
guint verify;
int rc, delay_val = 1;
gboolean switch_tls = FALSE;
CHANNEL_DEBUG(channel, "Started background coroutine %p", &c->coroutine);
if (spice_session_get_client_provided_socket(c->session)) {
if (c->fd < 0) {
g_critical("fd not provided!");
emit_main_context(channel, SPICE_CHANNEL_EVENT, SPICE_CHANNEL_ERROR_CONNECT);
goto cleanup;
}
if (!(c->sock = g_socket_new_from_fd(c->fd, NULL))) {
CHANNEL_DEBUG(channel, "Failed to open socket from fd %d", c->fd);
emit_main_context(channel, SPICE_CHANNEL_EVENT, SPICE_CHANNEL_ERROR_CONNECT);
goto cleanup;
}
g_socket_set_blocking(c->sock, FALSE);
g_socket_set_keepalive(c->sock, TRUE);
goto connected;
}
reconnect:
c->sock = spice_session_channel_open_host(c->session, channel, c->tls);
if (c->sock == NULL) {
if (!c->tls) {
CHANNEL_DEBUG(channel, "trying with TLS port");
c->tls = true; /* FIXME: does that really work with provided fd */
goto reconnect;
} else {
CHANNEL_DEBUG(channel, "Connect error");
emit_main_context(channel, SPICE_CHANNEL_EVENT, SPICE_CHANNEL_ERROR_CONNECT);
goto cleanup;
}
}
c->has_error = FALSE;
if (c->tls) {
c->ctx = SSL_CTX_new(TLSv1_method());
if (c->ctx == NULL) {
g_critical("SSL_CTX_new failed");
emit_main_context(channel, SPICE_CHANNEL_EVENT, SPICE_CHANNEL_ERROR_TLS);
goto cleanup;
}
verify = spice_session_get_verify(c->session);
if (verify &
(SPICE_SESSION_VERIFY_SUBJECT | SPICE_SESSION_VERIFY_HOSTNAME)) {
rc = spice_channel_load_ca(channel);
if (rc == 0) {
g_warning("no cert loaded");
if (verify & SPICE_SESSION_VERIFY_PUBKEY) {
g_warning("only pubkey active");
verify = SPICE_SESSION_VERIFY_PUBKEY;
} else {
emit_main_context(channel, SPICE_CHANNEL_EVENT, SPICE_CHANNEL_ERROR_TLS);
goto cleanup;
}
}
}
{
const gchar *ciphers = spice_session_get_ciphers(c->session);
if (ciphers != NULL) {
rc = SSL_CTX_set_cipher_list(c->ctx, ciphers);
if (rc != 1)
g_warning("loading cipher list %s failed", ciphers);
}
}
c->ssl = SSL_new(c->ctx);
if (c->ssl == NULL) {
g_critical("SSL_new failed");
emit_main_context(channel, SPICE_CHANNEL_EVENT, SPICE_CHANNEL_ERROR_TLS);
goto cleanup;
}
BIO *bio = bio_new_gsocket(c->sock);
SSL_set_bio(c->ssl, bio, bio);
{
guint8 *pubkey;
guint pubkey_len;
spice_session_get_pubkey(c->session, &pubkey, &pubkey_len);
c->sslverify = spice_openssl_verify_new(c->ssl, verify,
spice_session_get_host(c->session),
(char*)pubkey, pubkey_len,
spice_session_get_cert_subject(c->session));
}
ssl_reconnect:
rc = SSL_connect(c->ssl);
if (rc <= 0) {
rc = SSL_get_error(c->ssl, rc);
if (rc == SSL_ERROR_WANT_READ || rc == SSL_ERROR_WANT_WRITE) {
g_coroutine_socket_wait(&c->coroutine, c->sock, G_IO_OUT|G_IO_ERR|G_IO_HUP);
goto ssl_reconnect;
} else {
g_warning("%s: SSL_connect: %s",
c->name, ERR_error_string(rc, NULL));
emit_main_context(channel, SPICE_CHANNEL_EVENT, SPICE_CHANNEL_ERROR_TLS);
goto cleanup;
}
}
}
connected:
rc = setsockopt(g_socket_get_fd(c->sock), IPPROTO_TCP, TCP_NODELAY,
(const char*)&delay_val, sizeof(delay_val));
if ((rc != 0)
#ifdef ENOTSUP
&& (errno != ENOTSUP)
#endif
) {
g_warning("%s: could not set sockopt TCP_NODELAY: %s", c->name,
strerror(errno));
}
spice_channel_send_link(channel);
spice_channel_recv_link_hdr(channel);
spice_channel_recv_link_msg(channel, &switch_tls);
if (switch_tls)
goto cleanup;
spice_channel_recv_auth(channel);
while (spice_channel_iterate(channel))
;
cleanup:
CHANNEL_DEBUG(channel, "Coroutine exit %s", c->name);
SPICE_CHANNEL_GET_CLASS(channel)->channel_disconnect(channel);
if (switch_tls) {
c->tls = true;
spice_channel_connect(channel);
g_object_unref(channel);
} else
g_idle_add(spice_channel_delayed_unref, data);
/* Co-routine exits now - the SpiceChannel object may no longer exist,
so don't do anything else now unless you like SEGVs */
return NULL;
}
static gboolean connect_delayed(gpointer data)
{
SpiceChannel *channel = data;
SpiceChannelPrivate *c = channel->priv;
struct coroutine *co;
CHANNEL_DEBUG(channel, "Open coroutine starting %p", channel);
c->connect_delayed_id = 0;
co = &c->coroutine.coroutine;
co->stack_size = 16 << 20; /* 16Mb */
co->entry = spice_channel_coroutine;
co->release = NULL;
coroutine_init(co);
coroutine_yieldto(co, channel);
return FALSE;
}
static gboolean channel_connect(SpiceChannel *channel)
{
SpiceChannelPrivate *c = channel->priv;
g_return_val_if_fail(c != NULL, FALSE);
if (c->session == NULL || c->channel_type == -1 || c->channel_id == -1) {
/* unset properties or unknown channel type */
g_warning("%s: channel setup incomplete", __FUNCTION__);
return false;
}
if (c->state != SPICE_CHANNEL_STATE_UNCONNECTED) {
g_warning("Invalid channel_connect state: %d", c->state);
return true;
}
if (spice_session_get_client_provided_socket(c->session)) {
if (c->fd == -1) {
g_signal_emit(channel, signals[SPICE_CHANNEL_OPEN_FD], 0, c->tls);
return true;
}
}
c->state = SPICE_CHANNEL_STATE_CONNECTING;
c->xmit_queue_blocked = FALSE;
g_return_val_if_fail(c->sock == NULL, FALSE);
g_object_ref(G_OBJECT(channel)); /* Unref'd when co-routine exits */
/* we connect in idle, to let previous coroutine exit, if present */
c->connect_delayed_id = g_idle_add(connect_delayed, channel);
return true;
}
/**
* spice_channel_connect:
* @channel:
*
* Connect the channel, using #SpiceSession connection informations
*
* Returns: %TRUE on success.
**/
gboolean spice_channel_connect(SpiceChannel *channel)
{
g_return_val_if_fail(SPICE_IS_CHANNEL(channel), FALSE);
SpiceChannelPrivate *c = channel->priv;
if (c->state >= SPICE_CHANNEL_STATE_CONNECTING)
return TRUE;
return channel_connect(channel);
}
/**
* spice_channel_open_fd:
* @channel:
* @fd: a file descriptor (socket) or -1.
* request mechanism
*
* Connect the channel using @fd socket.
*
* If @fd is -1, a valid fd will be requested later via the
* SpiceChannel::open-fd signal.
*
* Returns: %TRUE on success.
**/
gboolean spice_channel_open_fd(SpiceChannel *channel, int fd)
{
SpiceChannelPrivate *c = SPICE_CHANNEL_GET_PRIVATE(channel);
g_return_val_if_fail(c != NULL, FALSE);
g_return_val_if_fail(fd >= -1, FALSE);
c->fd = fd;
return channel_connect(channel);
}
/* system or coroutine context */
static void channel_reset(SpiceChannel *channel, gboolean migrating)
{
SpiceChannelPrivate *c = SPICE_CHANNEL_GET_PRIVATE(channel);
if (c->connect_delayed_id) {
g_source_remove(c->connect_delayed_id);
c->connect_delayed_id = 0;
}
#if HAVE_SASL
if (c->sasl_conn) {
sasl_dispose(&c->sasl_conn);
c->sasl_conn = NULL;
c->sasl_decoded_offset = c->sasl_decoded_length = 0;
}
#endif
spice_openssl_verify_free(c->sslverify);
c->sslverify = NULL;
if (c->ssl) {
SSL_free(c->ssl);
c->ssl = NULL;
}
if (c->ctx) {
SSL_CTX_free(c->ctx);
c->ctx = NULL;
}
if (c->sock) {
g_object_unref(c->sock);
c->sock = NULL;
}
c->fd = -1;
free(c->peer_msg);
c->peer_msg = NULL;
c->peer_pos = 0;
STATIC_MUTEX_LOCK(c->xmit_queue_lock);
c->xmit_queue_blocked = TRUE; /* Disallow queuing new messages */
gboolean was_empty = g_queue_is_empty(&c->xmit_queue);
g_queue_foreach(&c->xmit_queue, (GFunc)spice_msg_out_unref, NULL);
g_queue_clear(&c->xmit_queue);
if (c->xmit_queue_wakeup_id) {
g_source_remove(c->xmit_queue_wakeup_id);
c->xmit_queue_wakeup_id = 0;
}
STATIC_MUTEX_UNLOCK(c->xmit_queue_lock);
spice_channel_flushed(channel, was_empty);
g_array_set_size(c->remote_common_caps, 0);
g_array_set_size(c->remote_caps, 0);
g_array_set_size(c->common_caps, 0);
/* Restore our default capabilities in case the channel gets re-used */
spice_channel_set_common_capability(channel, SPICE_COMMON_CAP_PROTOCOL_AUTH_SELECTION);
spice_channel_set_common_capability(channel, SPICE_COMMON_CAP_MINI_HEADER);
spice_channel_reset_capabilities(channel);
}
/* system or coroutine context */
G_GNUC_INTERNAL
void spice_channel_reset(SpiceChannel *channel, gboolean migrating)
{
SPICE_CHANNEL_GET_CLASS(channel)->channel_reset(channel, migrating);
}
/* system or coroutine context */
static void channel_disconnect(SpiceChannel *channel)
{
SpiceChannelPrivate *c = SPICE_CHANNEL_GET_PRIVATE(channel);
g_return_if_fail(c != NULL);
if (c->state == SPICE_CHANNEL_STATE_UNCONNECTED)
return;
c->has_error = TRUE; /* break the loop */
if (c->state == SPICE_CHANNEL_STATE_READY)
emit_main_context(channel, SPICE_CHANNEL_EVENT, SPICE_CHANNEL_CLOSED);
c->state = SPICE_CHANNEL_STATE_UNCONNECTED;
spice_channel_reset(channel, FALSE);
g_return_if_fail(SPICE_IS_CHANNEL(channel));
}
/**
* spice_channel_disconnect:
* @channel:
* @reason: a channel event emitted on main context (or #SPICE_CHANNEL_NONE)
*
* Close the socket and reset connection specific data. Finally, emit
* @reason #SpiceChannel::channel-event on main context if not
* #SPICE_CHANNEL_NONE.
**/
void spice_channel_disconnect(SpiceChannel *channel, SpiceChannelEvent reason)
{
SpiceChannelPrivate *c = SPICE_CHANNEL_GET_PRIVATE(channel);
CHANNEL_DEBUG(channel, "channel disconnect %d", reason);
g_return_if_fail(c != NULL);
if (c->state == SPICE_CHANNEL_STATE_UNCONNECTED)
return;
if (reason == SPICE_CHANNEL_SWITCHING)
c->state = SPICE_CHANNEL_STATE_SWITCHING;
c->has_error = TRUE; /* break the loop */
if (c->state == SPICE_CHANNEL_STATE_MIGRATING) {
c->state = SPICE_CHANNEL_STATE_READY;
} else
spice_channel_wakeup(channel, TRUE);
if (reason != SPICE_CHANNEL_NONE)
g_signal_emit(G_OBJECT(channel), signals[SPICE_CHANNEL_EVENT], 0, reason);
}
static gboolean test_capability(GArray *caps, guint32 cap)
{
guint32 c, word_index = cap / 32;
gboolean ret;
if (caps == NULL)
return FALSE;
if (caps->len < word_index + 1)
return FALSE;
c = g_array_index(caps, guint32, word_index);
ret = (c & (1 << (cap % 32))) != 0;
SPICE_DEBUG("test cap %d in 0x%X: %s", cap, c, ret ? "yes" : "no");
return ret;
}
/**
* spice_channel_test_capability:
* @channel:
* @cap:
*
* Test availability of remote "channel kind capability".
*
* Returns: %TRUE if @cap (channel kind capability) is available.
**/
gboolean spice_channel_test_capability(SpiceChannel *self, guint32 cap)
{
SpiceChannelPrivate *c;
g_return_val_if_fail(SPICE_IS_CHANNEL(self), FALSE);
c = self->priv;
return test_capability(c->remote_caps, cap);
}
/**
* spice_channel_test_common_capability:
* @channel:
* @cap:
*
* Test availability of remote "common channel capability".
*
* Returns: %TRUE if @cap (common channel capability) is available.
**/
gboolean spice_channel_test_common_capability(SpiceChannel *self, guint32 cap)
{
SpiceChannelPrivate *c;
g_return_val_if_fail(SPICE_IS_CHANNEL(self), FALSE);
c = self->priv;
return test_capability(c->remote_common_caps, cap);
}
static void set_capability(GArray *caps, guint32 cap)
{
guint word_index = cap / 32;
g_return_if_fail(caps != NULL);
if (caps->len <= word_index)
g_array_set_size(caps, word_index + 1);
g_array_index(caps, guint32, word_index) =
g_array_index(caps, guint32, word_index) | (1 << (cap % 32));
}
/**
* spice_channel_set_capability:
* @channel:
* @cap: a capability
*
* Enable specific channel-kind capability.
* Deprecated: 0.13: this function has been removed
**/
#undef spice_channel_set_capability
void spice_channel_set_capability(SpiceChannel *channel, guint32 cap)
{
SpiceChannelPrivate *c;
g_return_if_fail(SPICE_IS_CHANNEL(channel));
c = channel->priv;
set_capability(c->caps, cap);
}
G_GNUC_INTERNAL
void spice_caps_set(GArray *caps, guint32 cap, const gchar *desc)
{
g_return_if_fail(caps != NULL);
g_return_if_fail(desc != NULL);
if (g_strcmp0(g_getenv(desc), "0") == 0)
return;
set_capability(caps, cap);
}
G_GNUC_INTERNAL
SpiceSession* spice_channel_get_session(SpiceChannel *channel)
{
g_return_val_if_fail(SPICE_IS_CHANNEL(channel), NULL);
return channel->priv->session;
}
G_GNUC_INTERNAL
enum spice_channel_state spice_channel_get_state(SpiceChannel *channel)
{
g_return_val_if_fail(SPICE_IS_CHANNEL(channel),
SPICE_CHANNEL_STATE_UNCONNECTED);
return channel->priv->state;
}
G_GNUC_INTERNAL
void spice_channel_swap(SpiceChannel *channel, SpiceChannel *swap, gboolean swap_msgs)
{
SpiceChannelPrivate *c = SPICE_CHANNEL_GET_PRIVATE(channel);
SpiceChannelPrivate *s = SPICE_CHANNEL_GET_PRIVATE(swap);
g_return_if_fail(c != NULL);
g_return_if_fail(s != NULL);
g_return_if_fail(s->session != NULL);
g_return_if_fail(s->sock != NULL);
#define SWAP(Field) ({ \
typeof (c->Field) Field = c->Field; \
c->Field = s->Field; \
s->Field = Field; \
})
/* TODO: split channel in 2 objects: a controller and a swappable
state object */
SWAP(sock);
SWAP(ctx);
SWAP(ssl);
SWAP(sslverify);
SWAP(use_mini_header);
if (swap_msgs) {
SWAP(xmit_queue);
SWAP(xmit_queue_blocked);
SWAP(in_serial);
SWAP(out_serial);
}
SWAP(caps);
SWAP(common_caps);
SWAP(remote_caps);
SWAP(remote_common_caps);
#if HAVE_SASL
SWAP(sasl_conn);
SWAP(sasl_decoded);
SWAP(sasl_decoded_length);
SWAP(sasl_decoded_offset);
#endif
}
static const spice_msg_handler base_handlers[] = {
[ SPICE_MSG_SET_ACK ] = spice_channel_handle_set_ack,
[ SPICE_MSG_PING ] = spice_channel_handle_ping,
[ SPICE_MSG_NOTIFY ] = spice_channel_handle_notify,
[ SPICE_MSG_DISCONNECTING ] = spice_channel_handle_disconnect,
[ SPICE_MSG_WAIT_FOR_CHANNELS ] = spice_channel_handle_wait_for_channels,
[ SPICE_MSG_MIGRATE ] = spice_channel_handle_migrate,
};
/* coroutine context */
static void spice_channel_handle_msg(SpiceChannel *channel, SpiceMsgIn *msg)
{
int type = spice_msg_in_type(msg);
g_return_if_fail(type < SPICE_N_ELEMENTS(base_handlers));
g_return_if_fail(base_handlers[type] != NULL);
base_handlers[type](channel, msg);
}
static void spice_channel_reset_capabilities(SpiceChannel *channel)
{
SpiceChannelPrivate *c = SPICE_CHANNEL_GET_PRIVATE(channel);
g_array_set_size(c->caps, 0);
if (SPICE_CHANNEL_GET_CLASS(channel)->channel_reset_capabilities) {
SPICE_CHANNEL_GET_CLASS(channel)->channel_reset_capabilities(channel);
}
}
static void spice_channel_send_migration_handshake(SpiceChannel *channel)
{
SpiceChannelPrivate *c = SPICE_CHANNEL_GET_PRIVATE(channel);
if (SPICE_CHANNEL_GET_CLASS(channel)->channel_send_migration_handshake) {
SPICE_CHANNEL_GET_CLASS(channel)->channel_send_migration_handshake(channel);
} else {
c->state = SPICE_CHANNEL_STATE_MIGRATING;
}
}
/**
* spice_channel_flush_async:
* @channel: a #SpiceChannel
* @cancellable: (allow-none): optional GCancellable object, %NULL to ignore
* @callback: (scope async): callback to call when the request is satisfied
* @user_data: (closure): the data to pass to callback function
*
* Forces an asynchronous write of all user-space buffered data for
* the given channel.
*
* When the operation is finished callback will be called. You can
* then call spice_channel_flush_finish() to get the result of the
* operation.
*
* Since: 0.15
**/
void spice_channel_flush_async(SpiceChannel *self, GCancellable *cancellable,
GAsyncReadyCallback callback, gpointer user_data)
{
GSimpleAsyncResult *simple;
SpiceChannelPrivate *c;
gboolean was_empty;
g_return_if_fail(SPICE_IS_CHANNEL(self));
c = self->priv;
if (c->state != SPICE_CHANNEL_STATE_READY) {
g_simple_async_report_error_in_idle(G_OBJECT(self), callback, user_data,
SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
"The channel is not ready yet");
return;
}
simple = g_simple_async_result_new(G_OBJECT(self), callback, user_data,
spice_channel_flush_async);
STATIC_MUTEX_LOCK(c->xmit_queue_lock);
was_empty = g_queue_is_empty(&c->xmit_queue);
STATIC_MUTEX_UNLOCK(c->xmit_queue_lock);
if (was_empty) {
g_simple_async_result_set_op_res_gboolean(simple, TRUE);
g_simple_async_result_complete_in_idle(simple);
g_object_unref(simple);
return;
}
c->flushing = g_slist_append(c->flushing, simple);
}
/**
* spice_channel_flush_finish:
* @channel: a #SpiceChannel
* @result: a #GAsyncResult
* @error: a #GError location to store the error occurring, or %NULL
* to ignore.
*
* Finishes flushing a channel.
*
* Returns: %TRUE if flush operation succeeded, %FALSE otherwise.
* Since: 0.15
**/
gboolean spice_channel_flush_finish(SpiceChannel *self, GAsyncResult *result,
GError **error)
{
GSimpleAsyncResult *simple;
g_return_val_if_fail(SPICE_IS_CHANNEL(self), FALSE);
g_return_val_if_fail(result != NULL, FALSE);
simple = (GSimpleAsyncResult *)result;
if (g_simple_async_result_propagate_error(simple, error))
return -1;
g_return_val_if_fail(g_simple_async_result_is_valid(result, G_OBJECT(self),
spice_channel_flush_async), FALSE);
CHANNEL_DEBUG(self, "flushed finished!");
return g_simple_async_result_get_op_res_gboolean(simple);
}