/* -*- 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 "spice-channel-priv.h" #include "spice-session-priv.h" #include "spice-marshal.h" #include #include #include #include #include #include #ifdef HAVE_SYS_SOCKET_H #include #endif #ifdef HAVE_NETINET_IN_H #include #endif #ifdef HAVE_ARPA_INET_H #include #endif #include #include "gio-coroutine.h" static void spice_channel_send_msg(SpiceChannel *channel, spice_msg_out *out, gboolean buffered); static void spice_channel_send_link(SpiceChannel *channel); static void channel_disconnect(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, spice_channel)) G_DEFINE_TYPE(SpiceChannel, spice_channel, G_TYPE_OBJECT); /* Properties */ enum { PROP_0, PROP_SESSION, PROP_CHANNEL_TYPE, PROP_CHANNEL_ID, }; /* Signals */ enum { SPICE_CHANNEL_EVENT, SPICE_CHANNEL_OPEN_FD, SPICE_CHANNEL_LAST_SIGNAL, }; static guint signals[SPICE_CHANNEL_LAST_SIGNAL]; static const char *channel_desc[] = { [ SPICE_CHANNEL_MAIN ] = "main", [ SPICE_CHANNEL_DISPLAY ] = "display", [ SPICE_CHANNEL_CURSOR ] = "cursor", [ SPICE_CHANNEL_INPUTS ] = "inputs", [ SPICE_CHANNEL_RECORD ] = "record", [ SPICE_CHANNEL_PLAYBACK ] = "playback", [ SPICE_CHANNEL_TUNNEL ] = "tunnel", }; static void spice_channel_iterate_write(SpiceChannel *channel); static void spice_channel_iterate_read(SpiceChannel *channel); static void spice_channel_init(SpiceChannel *channel) { spice_channel *c; c = channel->priv = SPICE_CHANNEL_GET_PRIVATE(channel); c->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); } static void spice_channel_constructed(GObject *gobject) { SpiceChannel *channel = SPICE_CHANNEL(gobject); spice_channel *c = channel->priv; const char *desc = NULL; if (c->channel_type < SPICE_N_ELEMENTS(channel_desc)) desc = channel_desc[c->channel_type]; snprintf(c->name, sizeof(c->name), "%s-%d:%d", desc ? desc : "unknown", c->channel_type, c->channel_id); SPICE_DEBUG("%s: %s", c->name, __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); spice_channel *c = channel->priv; SPICE_DEBUG("%s: %s %p", c->name, __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); spice_channel *c = channel->priv; SPICE_DEBUG("%s: %s %p", c->name, __FUNCTION__, gobject); 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); spice_channel *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; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec); break; } } G_GNUC_INTERNAL gint spice_channel_get_channel_id(SpiceChannel *channel) { spice_channel *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) { spice_channel *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); spice_channel *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; 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; 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_NAME | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB)); 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_NAME | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB)); 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_NAME | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB)); /** * 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__INT, G_TYPE_NONE, 1, G_TYPE_INT); /** * 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(spice_channel)); SSL_library_init(); SSL_load_error_strings(); } /* ---------------------------------------------------------------- */ /* private msg api */ G_GNUC_INTERNAL spice_msg_in *spice_msg_in_new(SpiceChannel *channel) { spice_msg_in *in; g_return_val_if_fail(channel != NULL, NULL); in = spice_new0(spice_msg_in, 1); in->refcount = 1; in->channel = channel; return in; } G_GNUC_INTERNAL spice_msg_in *spice_msg_in_sub_new(SpiceChannel *channel, spice_msg_in *parent, SpiceSubMessage *sub) { spice_msg_in *in; g_return_val_if_fail(channel != NULL, NULL); in = spice_msg_in_new(channel); in->header.type = sub->type; in->header.size = 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(spice_msg_in *in) { g_return_if_fail(in != NULL); in->refcount++; } G_GNUC_INTERNAL void spice_msg_in_unref(spice_msg_in *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(spice_msg_in *in) { g_return_val_if_fail(in != NULL, -1); return in->header.type; } G_GNUC_INTERNAL void *spice_msg_in_parsed(spice_msg_in *in) { g_return_val_if_fail(in != NULL, NULL); return in->parsed; } G_GNUC_INTERNAL void *spice_msg_in_raw(spice_msg_in *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(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(spice_msg_in *in) { spice_channel *c = in->channel->priv; fprintf(stderr, "--\n<< hdr: %s serial %" PRIu64 " type %d size %d sub-list %d\n", c->name, in->header.serial, in->header.type, in->header.size, in->header.sub_list); hexdump("<< msg", in->data, in->dpos); } G_GNUC_INTERNAL void spice_msg_out_hexdump(spice_msg_out *out, unsigned char *data, int len) { spice_channel *c = out->channel->priv; fprintf(stderr, "--\n>> hdr: %s serial %" PRIu64 " type %d size %d sub-list %d\n", c->name, out->header->serial, out->header->type, out->header->size, out->header->sub_list); hexdump(">> msg", data, len); } G_GNUC_INTERNAL spice_msg_out *spice_msg_out_new(SpiceChannel *channel, int type) { spice_channel *c = channel->priv; spice_msg_out *out; g_return_val_if_fail(c != NULL, NULL); out = spice_new0(spice_msg_out, 1); out->refcount = 1; out->channel = channel; out->marshallers = c->marshallers; out->marshaller = spice_marshaller_new(); out->header = (SpiceDataHeader *) spice_marshaller_reserve_space(out->marshaller, sizeof(SpiceDataHeader)); spice_marshaller_set_base(out->marshaller, sizeof(SpiceDataHeader)); out->header->serial = c->serial++; out->header->type = type; out->header->sub_list = 0; return out; } G_GNUC_INTERNAL void spice_msg_out_ref(spice_msg_out *out) { g_return_if_fail(out != NULL); out->refcount++; } G_GNUC_INTERNAL void spice_msg_out_unref(spice_msg_out *out) { g_return_if_fail(out != NULL); out->refcount--; if (out->refcount > 0) return; spice_marshaller_destroy(out->marshaller); free(out); } /* system context */ G_GNUC_INTERNAL void spice_msg_out_send(spice_msg_out *out) { g_return_if_fail(out != NULL); out->header->size = spice_marshaller_get_total_size(out->marshaller) - sizeof(SpiceDataHeader); spice_channel_send_msg(out->channel, out, TRUE); /* TODO: we currently flush/wakeup immediately all buffered messages */ spice_channel_wakeup(out->channel); } /* coroutine context */ G_GNUC_INTERNAL void spice_msg_out_send_internal(spice_msg_out *out) { g_return_if_fail(out != NULL); out->header->size = spice_marshaller_get_total_size(out->marshaller) - sizeof(SpiceDataHeader); spice_channel_send_msg(out->channel, out, FALSE); } /* ---------------------------------------------------------------- */ 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) { spice_channel *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 { SPICE_DEBUG("Send error %s", error->message); } g_clear_error(&error); ret = -1; } } if (ret == -1) { if (cond != 0) { g_io_wait(c->sock, cond); } else { SPICE_DEBUG("Closing the channel: spice_channel_flush %d", errno); c->has_error = TRUE; return; } } if (ret == 0) { SPICE_DEBUG("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) { spice_channel *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; } //SPICE_DEBUG("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 spice_channel *c = channel->priv; if (c->sasl_conn) spice_channel_flush_sasl(channel, data, len); else #endif spice_channel_flush_wire(channel, data, len); } /* * 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) { spice_channel *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 { SPICE_DEBUG("Read error %s", error->message); } g_clear_error(&error); ret = -1; } } if (ret == -1) { if (cond != 0) { if (c->wait_interruptable) { if (!g_io_wait_interruptable(&c->wait, c->sock, cond)) { // SPICE_DEBUG("Read blocking interrupted %d", priv->has_error); return -EAGAIN; } } else { g_io_wait(c->sock, cond); } goto reread; } else { c->has_error = TRUE; return -errno; } } if (ret == 0) { SPICE_DEBUG("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) { spice_channel *c = channel->priv; /* SPICE_DEBUG("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) { spice_channel *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) SPICE_DEBUG("still needs %" G_GSIZE_FORMAT, len); #endif } return length; } /* coroutine context */ static void spice_channel_send_spice_ticket(SpiceChannel *channel) { spice_channel *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); BIO_free(bioKey); g_free(password); } /* coroutine context */ static void spice_channel_recv_auth(SpiceChannel *channel) { spice_channel *c = channel->priv; uint32_t link_res; int rc; rc = spice_channel_read(channel, &link_res, sizeof(link_res)); if (rc != sizeof(link_res)) { g_critical("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) { g_critical("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_MIGRATING) spice_channel_up(channel); } G_GNUC_INTERNAL void spice_channel_up(SpiceChannel *channel) { spice_channel *c = channel->priv; SPICE_DEBUG("%s: channel up, state %d", c->name, 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) { spice_channel *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); } spice_channel_write(channel, buffer, p - buffer); } /* coroutine context */ static void spice_channel_recv_link_hdr(SpiceChannel *channel) { spice_channel *c = channel->priv; int rc; rc = spice_channel_read(channel, &c->peer_hdr, sizeof(c->peer_hdr)); if (rc != sizeof(c->peer_hdr)) { g_critical("incomplete link header (%d/%" G_GSIZE_FORMAT ")", rc, sizeof(c->peer_hdr)); goto error; } if (c->peer_hdr.magic != SPICE_MAGIC) { g_critical("invalid SPICE_MAGIC!"); goto error; } if (c->peer_hdr.major_version != c->link_hdr.major_version) { if (c->peer_hdr.major_version == 1) { /* enter spice 0.4 mode */ g_object_set(c->session, "protocol", 1, NULL); SPICE_DEBUG("%s: switching to protocol 1 (spice 0.4)", c->name); SPICE_CHANNEL_GET_CLASS(channel)->channel_disconnect(channel); spice_channel_connect(channel); return; } g_critical("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); c->state = SPICE_CHANNEL_STATE_LINK_MSG; return; error: 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) { spice_channel *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; } } SPICE_DEBUG("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) { spice_channel *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; 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); SPICE_DEBUG("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; /* 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; SPICE_DEBUG("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 */ SPICE_DEBUG("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)) { g_critical("Failed to collect auth credentials"); goto error; } goto restart; } SPICE_DEBUG("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; SPICE_DEBUG("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; SPICE_DEBUG("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)) { g_critical("%s", "Failed to collect auth credentials"); goto error; } goto restep; } if (serverin) { g_free(serverin); serverin = NULL; } SPICE_DEBUG("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; SPICE_DEBUG("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; SPICE_DEBUG("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 && err == SASL_OK) { g_free(serverin); serverin = NULL; 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; SPICE_DEBUG("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; } } SPICE_DEBUG("%s", "SASL authentication complete"); spice_channel_read(channel, &len, sizeof(len)); 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: if (saslconn) sasl_dispose(&saslconn); if (!c->has_error) emit_main_context(channel, SPICE_CHANNEL_EVENT, SPICE_CHANNEL_ERROR_AUTH); c->has_error = TRUE; return FALSE; } #endif /* HAVE_SASL */ /* coroutine context */ static void spice_channel_recv_link_msg(SpiceChannel *channel) { spice_channel *c; int rc, num_caps, i; 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: c->tls = true; SPICE_DEBUG("%s: switching to tls", c->name); SPICE_CHANNEL_GET_CLASS(channel)->channel_disconnect(channel); spice_channel_connect(channel); 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; SPICE_DEBUG("%s: %s: %d caps", c->name, __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); */ uint32_t *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; SPICE_DEBUG("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; SPICE_DEBUG("got channel caps %u:0x%X", i, *caps); } c->state = SPICE_CHANNEL_STATE_AUTH; if (!spice_channel_test_common_capability(channel, SPICE_COMMON_CAP_PROTOCOL_AUTH_SELECTION)) { SPICE_DEBUG("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)) { SPICE_DEBUG("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; } } return; error: SPICE_CHANNEL_GET_CLASS(channel)->channel_disconnect(channel); emit_main_context(channel, SPICE_CHANNEL_EVENT, SPICE_CHANNEL_ERROR_LINK); } /* system context */ static void spice_channel_buffered_write(SpiceChannel *channel, const void *data, size_t size) { spice_channel *c = channel->priv; size_t left; left = c->xmit_buffer_capacity - c->xmit_buffer_size; if (left < size) { c->xmit_buffer_capacity += size + 4095; c->xmit_buffer_capacity &= ~4095; c->xmit_buffer = g_realloc(c->xmit_buffer, c->xmit_buffer_capacity); } memcpy(&c->xmit_buffer[c->xmit_buffer_size], data, size); c->xmit_buffer_size += size; } /* system context */ /* TODO: we currently flush/wakeup immediately all buffered messages */ G_GNUC_INTERNAL void spice_channel_wakeup(SpiceChannel *channel) { spice_channel *c = channel->priv; g_io_wakeup(&c->wait); } /* coroutine context if @buffered is TRUE, system context if @buffered is FALSE */ static void spice_channel_send_msg(SpiceChannel *channel, spice_msg_out *out, gboolean buffered) { uint8_t *data; int free_data; size_t len; g_return_if_fail(channel != NULL); g_return_if_fail(out != NULL); data = spice_marshaller_linearize(out->marshaller, 0, &len, &free_data); /* spice_msg_out_hexdump(out, data, len); */ if (buffered) spice_channel_buffered_write(channel, data, len); else spice_channel_write(channel, data, len); if (free_data) { free(data); } } /* coroutine context */ G_GNUC_INTERNAL void spice_channel_recv_msg(SpiceChannel *channel, handler_msg_in msg_handler, gpointer data) { spice_channel *c = channel->priv; spice_msg_in *in; int rc; if (!c->msg_in) { c->msg_in = spice_msg_in_new(channel); } in = c->msg_in; /* receive message */ if (in->hpos < sizeof(in->header)) { rc = spice_channel_read(channel, (uint8_t*)&in->header + in->hpos, sizeof(in->header) - in->hpos); if (rc < 0) { g_critical("recv hdr: %s", strerror(errno)); return; } in->hpos += rc; if (in->hpos < sizeof(in->header)) return; in->data = spice_malloc(in->header.size); } if (in->dpos < in->header.size) { rc = spice_channel_read(channel, in->data + in->dpos, in->header.size - in->dpos); if (rc < 0) { g_critical("recv msg: %s", strerror(errno)); return; } in->dpos += rc; if (in->dpos < in->header.size) return; } if (in->header.sub_list) { SpiceSubMessageList *sub_list; SpiceSubMessage *sub; spice_msg_in *sub_in; int i; sub_list = (SpiceSubMessageList *)(in->data + in->header.sub_list); 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, sub_in->header.type, 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, sub_in->header.type); 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) { spice_msg_out *out = spice_msg_out_new(channel, SPICE_MSGC_ACK); spice_msg_out_send_internal(out); spice_msg_out_unref(out); c->message_ack_count = c->message_ack_window; } } /* parse message */ in->parsed = c->parser(in->data, in->data + in->dpos, in->header.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, in->header.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: /* release message */ c->msg_in = NULL; spice_msg_in_unref(in); } /** * spice_channel_new: * @s: the @SpiceSession the channel is linked to * @type: the requested SPICE_CHANNEL type * @id: the channel-id * * Create a new #SpiceChannel of type @type, and channel ID @id. * * Returns: a #SpiceChannel **/ 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: gtype = SPICE_TYPE_PLAYBACK_CHANNEL; break; case SPICE_CHANNEL_RECORD: gtype = SPICE_TYPE_RECORD_CHANNEL; break; default: 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); SPICE_DEBUG("channel destroy"); spice_channel_disconnect(channel, SPICE_CHANNEL_NONE); g_object_unref(channel); } /* coroutine context */ static void spice_channel_iterate_write(SpiceChannel *channel) { spice_channel *c = channel->priv; if (c->xmit_buffer_size) { spice_channel_write(channel, c->xmit_buffer, c->xmit_buffer_size); c->xmit_buffer_size = 0; } } /* coroutine context */ static void spice_channel_iterate_read(SpiceChannel *channel) { spice_channel *c = channel->priv; /* TODO: get rid of state, and use coroutine state */ switch (c->state) { case SPICE_CHANNEL_STATE_LINK_HDR: spice_channel_recv_link_hdr(channel); break; case SPICE_CHANNEL_STATE_LINK_MSG: spice_channel_recv_link_msg(channel); break; case SPICE_CHANNEL_STATE_AUTH: spice_channel_recv_auth(channel); break; case SPICE_CHANNEL_STATE_READY: spice_channel_recv_msg(channel, (handler_msg_in)SPICE_CHANNEL_GET_CLASS(channel)->handle_msg, NULL); break; default: g_critical("unknown state %d", c->state); } } /* coroutine context */ static gboolean spice_channel_iterate(SpiceChannel *channel) { spice_channel *c = channel->priv; GIOCondition ret; do { while (c->state == SPICE_CHANNEL_STATE_MIGRATING) { /* freeze coroutine */ coroutine_yield(NULL); g_return_val_if_fail(c->state != SPICE_CHANNEL_STATE_MIGRATING, FALSE); } if (c->has_error) { SPICE_DEBUG("channel has error, breaking loop"); return FALSE; } SPICE_CHANNEL_GET_CLASS(channel)->iterate_write(channel); ret = g_io_wait_interruptable(&c->wait, c->sock, G_IO_IN); #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_ERR|G_IO_HUP)) { SPICE_DEBUG("got socket error before read(): %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; } do SPICE_CHANNEL_GET_CLASS(channel)->iterate_read(channel); #if HAVE_SASL while (c->sasl_decoded != NULL); #else while (FALSE); #endif 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); spice_channel *c = channel->priv; g_return_val_if_fail(channel != NULL, FALSE); SPICE_DEBUG("Delayed unref channel %s %p", c->name, channel); g_return_val_if_fail(c->coroutine.exited == TRUE, FALSE); g_object_unref(G_OBJECT(data)); return FALSE; } /* coroutine context */ static void *spice_channel_coroutine(void *data) { SpiceChannel *channel = SPICE_CHANNEL(data); spice_channel *c = channel->priv; guint verify; SPICE_DEBUG("Started background coroutine %p", &c->coroutine); if (spice_session_get_client_provided_socket(c->session)) { if (c->fd < 0) { g_critical("fd not provided!"); goto cleanup; } if (!(c->sock = g_socket_new_from_fd(c->fd, NULL))) { SPICE_DEBUG("Failed to open socket from fd %d", c->fd); return FALSE; } g_socket_set_blocking(c->sock, FALSE); goto connected; } reconnect: c->sock = spice_session_channel_open_host(c->session, c->tls); if (c->sock == NULL) { if (!c->tls) { SPICE_DEBUG("connection failed, trying with TLS port"); c->tls = true; /* FIXME: does that really work with provided fd */ goto reconnect; } else { SPICE_DEBUG("Connect error"); emit_main_context(channel, SPICE_CHANNEL_EVENT, SPICE_CHANNEL_ERROR_CONNECT); goto cleanup; } } c->has_error = FALSE; if (c->tls) { int rc; c->ctx = SSL_CTX_new(TLSv1_method()); if (c->ctx == NULL) { g_critical("SSL_CTX_new failed"); goto cleanup; } verify = spice_session_get_verify(c->session); if (verify & (SPICE_SESSION_VERIFY_PUBKEY | SPICE_SESSION_VERIFY_HOSTNAME)) { gchar *ca_file; g_object_get(c->session, "ca-file", &ca_file, NULL); if (ca_file) { rc = SSL_CTX_load_verify_locations(c->ctx, ca_file, NULL); if (rc != 1) g_warning("loading ca certs from %s failed", ca_file); g_free(ca_file); if (rc != 1) { if (verify & SPICE_SESSION_VERIFY_PUBKEY) { g_warning("only pubkey active"); verify = SPICE_SESSION_VERIFY_PUBKEY; } else goto cleanup; } } } c->ssl = SSL_new(c->ctx); if (c->ssl == NULL) { g_critical("SSL_new failed"); goto cleanup; } rc = SSL_set_fd(c->ssl, g_socket_get_fd(c->sock)); if (rc <= 0) { g_critical("SSL_set_fd failed"); goto cleanup; } { 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_io_wait(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: c->state = SPICE_CHANNEL_STATE_LINK_HDR; spice_channel_send_link(channel); while (spice_channel_iterate(channel)) ; /* TODO: improve it, this is a bit hairy, c->coroutine will be overwritten on (re)connect, so we skip the normal cleanup path. Ideally, we shouldn't use the same channel structure? */ if (c->state == SPICE_CHANNEL_STATE_CONNECTING) { g_object_unref(channel); goto end; } cleanup: SPICE_DEBUG("Coroutine exit"); SPICE_CHANNEL_GET_CLASS(channel)->channel_disconnect(channel); g_idle_add(spice_channel_delayed_unref, data); end: /* 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; spice_channel *c = channel->priv; struct coroutine *co; SPICE_DEBUG("Open coroutine starting %p", channel); c->connect_delayed_id = 0; co = &c->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) { spice_channel *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; 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) { return channel_connect(channel); } /** * spice_channel_open_fd: * @channel: * @fd: a file descriptor (socket) * * Connect the channel using @fd socket. * * Returns: %TRUE on success. **/ gboolean spice_channel_open_fd(SpiceChannel *channel, int fd) { spice_channel *c = SPICE_CHANNEL_GET_PRIVATE(channel); g_return_val_if_fail(c != NULL, FALSE); g_return_val_if_fail(fd >= 0, FALSE); c->fd = fd; return channel_connect(channel); } /* system or coroutine context */ static void channel_disconnect(SpiceChannel *channel) { spice_channel *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->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_socket_close(c->sock, NULL); g_object_unref(c->sock); c->sock = NULL; } c->fd = -1; free(c->peer_msg); c->peer_msg = NULL; c->peer_pos = 0; if (c->xmit_buffer) { g_free(c->xmit_buffer); c->xmit_buffer = NULL; c->xmit_buffer_size = 0; c->xmit_buffer_capacity = 0; } 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); g_array_set_size(c->caps, 0); 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_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) { spice_channel *c = SPICE_CHANNEL_GET_PRIVATE(channel); 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; coroutine_yieldto(&c->coroutine, NULL); } else spice_channel_wakeup(channel); 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: * @self: * @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) { spice_channel *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: * @self: * @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) { spice_channel *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. **/ /* FIXME: we may want to make caps read only from outside */ void spice_channel_set_capability(SpiceChannel *channel, guint32 cap) { spice_channel *c; g_return_if_fail(SPICE_IS_CHANNEL(channel)); c = channel->priv; set_capability(c->caps, cap); } G_GNUC_INTERNAL void spice_channel_set_common_capability(SpiceChannel *channel, guint32 cap) { spice_channel *c; g_return_if_fail(SPICE_IS_CHANNEL(channel)); c = channel->priv; set_capability(c->common_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 void spice_channel_swap(SpiceChannel *channel, SpiceChannel *swap) { spice_channel *c = SPICE_CHANNEL_GET_PRIVATE(channel); spice_channel *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); { GSocket *sock = c->sock; SSL_CTX *ctx = c->ctx; SSL *ssl = c->ssl; SpiceOpenSSLVerify *sslverify = c->sslverify; c->sock = s->sock; c->ctx = s->ctx; c->ssl = s->ssl; c->sslverify = s->sslverify; s->sock = sock; s->ctx = ctx; s->ssl = ssl; s->sslverify = sslverify; } #if HAVE_SASL { sasl_conn_t *sasl_conn = c->sasl_conn; const char *sasl_decoded = c->sasl_decoded; unsigned int sasl_decoded_length = c->sasl_decoded_length; unsigned int sasl_decoded_offset = c->sasl_decoded_offset; c->sasl_conn = s->sasl_conn; c->sasl_decoded = s->sasl_decoded; c->sasl_decoded_length = s->sasl_decoded_length; c->sasl_decoded_offset = s->sasl_decoded_offset; s->sasl_conn = sasl_conn; s->sasl_decoded = sasl_decoded; s->sasl_decoded_length = sasl_decoded_length; s->sasl_decoded_offset = sasl_decoded_offset; } #endif }