/* -*- 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
#include
#include "spice-client.h"
#include "spice-common.h"
#include "spice-channel-priv.h"
#include "spice-util-priv.h"
#include "spice-session-priv.h"
/* spice/common */
#include "ring.h"
#include "gio-coroutine.h"
#include "glib-compat.h"
struct channel {
SpiceChannel *channel;
RingItem link;
};
/**
* SECTION:spice-session
* @short_description: handles connection details, and active channels
* @title: Spice Session
* @section_id:
* @see_also: #SpiceChannel, and the GTK widget #SpiceDisplay
* @stability: Stable
* @include: spice-session.h
*
* The #SpiceSession class handles all the #SpiceChannel connections.
* It's also the class that contains connections informations, such as
* #SpiceSession:host and #SpiceSession:port.
*
* You can simply set the property #SpiceSession:uri to something like
* "spice://127.0.0.1?port=5930" to configure your connection details.
*
* You may want to connect to #SpiceSession::channel-new signal, to be
* informed of the availability of channels and to interact with
* them.
*
* For example, when the #SpiceInputsChannel is available and get the
* event #SPICE_CHANNEL_OPENED, you can send key events with
* spice_inputs_key_press(). When the #SpiceMainChannel is available,
* you can start sharing the clipboard... .
*
*
* Once #SpiceSession properties set, you can call
* spice_session_connect() to start connecting and communicating with
* a Spice server.
*/
/* ------------------------------------------------------------------ */
/* gobject glue */
#define SPICE_SESSION_GET_PRIVATE(obj) \
(G_TYPE_INSTANCE_GET_PRIVATE ((obj), SPICE_TYPE_SESSION, SpiceSessionPrivate))
G_DEFINE_TYPE (SpiceSession, spice_session, G_TYPE_OBJECT);
/* Properties */
enum {
PROP_0,
PROP_HOST,
PROP_PORT,
PROP_TLS_PORT,
PROP_PASSWORD,
PROP_CA_FILE,
PROP_CIPHERS,
PROP_IPV4,
PROP_IPV6,
PROP_PROTOCOL,
PROP_URI,
PROP_CLIENT_SOCKETS,
PROP_PUBKEY,
PROP_CERT_SUBJECT,
PROP_VERIFY,
PROP_MIGRATION_STATE,
PROP_AUDIO,
PROP_SMARTCARD,
PROP_SMARTCARD_CERTIFICATES,
PROP_SMARTCARD_DB,
PROP_USBREDIR,
PROP_INHIBIT_KEYBOARD_GRAB,
PROP_DISABLE_EFFECTS,
PROP_COLOR_DEPTH,
PROP_READ_ONLY,
};
/* signals */
enum {
SPICE_SESSION_CHANNEL_NEW,
SPICE_SESSION_CHANNEL_DESTROY,
SPICE_SESSION_LAST_SIGNAL,
};
static guint signals[SPICE_SESSION_LAST_SIGNAL];
static void spice_session_init(SpiceSession *session)
{
SpiceSessionPrivate *s;
SPICE_DEBUG("New session (compiled from package " PACKAGE_STRING ")");
s = session->priv = SPICE_SESSION_GET_PRIVATE(session);
ring_init(&s->channels);
cache_init(&s->images, "image");
cache_init(&s->palettes, "palette");
s->glz_window = glz_decoder_window_new();
}
static void
spice_session_dispose(GObject *gobject)
{
SpiceSession *session = SPICE_SESSION(gobject);
SpiceSessionPrivate *s = session->priv;
SPICE_DEBUG("session dispose");
spice_session_disconnect(session);
if (s->migration) {
spice_session_disconnect(s->migration);
g_object_unref(s->migration);
s->migration = NULL;
}
if (s->migration_left) {
g_list_free(s->migration_left);
s->migration_left = NULL;
}
if (s->after_main_init) {
g_source_remove(s->after_main_init);
s->after_main_init = 0;
}
g_clear_object(&s->audio_manager);
g_clear_object(&s->gtk_session);
g_clear_object(&s->usb_manager);
/* Chain up to the parent class */
if (G_OBJECT_CLASS(spice_session_parent_class)->dispose)
G_OBJECT_CLASS(spice_session_parent_class)->dispose(gobject);
}
G_GNUC_INTERNAL
void spice_session_palettes_clear(SpiceSession *session)
{
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
g_return_if_fail(s != NULL);
for (;;) {
display_cache_item *item = cache_get_lru(&s->palettes);
if (item == NULL)
break;
cache_del(&s->palettes, item);
}
}
G_GNUC_INTERNAL
void spice_session_images_clear(SpiceSession *session)
{
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
g_return_if_fail(s != NULL);
for (;;) {
display_cache_item *item = cache_get_lru(&s->images);
if (item == NULL)
break;
pixman_image_unref(item->ptr);
cache_del(&s->images, item);
}
}
static void
spice_session_finalize(GObject *gobject)
{
SpiceSession *session = SPICE_SESSION(gobject);
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
/* release stuff */
g_free(s->host);
g_free(s->port);
g_free(s->tls_port);
g_free(s->password);
g_free(s->ca_file);
g_free(s->ciphers);
g_free(s->cert_subject);
g_strfreev(s->smartcard_certificates);
g_free(s->smartcard_db);
g_strfreev(s->disable_effects);
spice_session_palettes_clear(session);
spice_session_images_clear(session);
glz_decoder_window_destroy(s->glz_window);
if (s->pubkey)
g_byte_array_unref(s->pubkey);
/* Chain up to the parent class */
if (G_OBJECT_CLASS(spice_session_parent_class)->finalize)
G_OBJECT_CLASS(spice_session_parent_class)->finalize(gobject);
}
static int spice_uri_create(SpiceSession *session, char *dest, int len)
{
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
int pos = 0;
if (s->host == NULL || (s->port == NULL && s->tls_port == NULL)) {
return 0;
}
pos += snprintf(dest + pos, len-pos, "spice://%s?", s->host);
if (s->port && strlen(s->port))
pos += snprintf(dest + pos, len - pos, "port=%s;", s->port);
if (s->tls_port && strlen(s->tls_port))
pos += snprintf(dest + pos, len - pos, "tls-port=%s;", s->tls_port);
return pos;
}
static int spice_uri_parse(SpiceSession *session, const char *original_uri)
{
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
char host[128], key[32], value[128];
char *port = NULL, *tls_port = NULL, *uri = NULL, *password = NULL;
char **target_key;
int punctuation = 0;
int len, pos = 0;
g_return_val_if_fail(original_uri != NULL, -1);
uri = g_uri_unescape_string(original_uri, NULL);
g_return_val_if_fail(uri != NULL, -1);
if (sscanf(uri, "spice://%127[-.0-9a-zA-Z]%n", host, &len) != 1)
goto fail;
pos += len;
if (uri[pos] == '/')
pos++;
for (;;) {
if (uri[pos] == '?' || uri[pos] == ';' || uri[pos] == '&') {
pos++;
punctuation++;
continue;
}
if (uri[pos] == 0) {
break;
}
if (uri[pos] == ':') {
if (punctuation++) {
g_warning("colon seen after a previous punctuation (?;&:)");
goto fail;
}
pos++;
/* port numbers are 16 bit, fits in five decimal figures. */
if (sscanf(uri + pos, "%5[0-9]%n", value, &len) != 1)
goto fail;
port = g_strdup(value);
pos += len;
continue;
} else {
if (sscanf(uri + pos, "%31[-a-zA-Z0-9]=%127[^;&]%n", key, value, &len) != 2)
goto fail;
}
pos += len;
target_key = NULL;
if (g_str_equal(key, "port")) {
target_key = &port;
} else if (g_str_equal(key, "tls-port")) {
target_key = &tls_port;
} else if (g_str_equal(key, "password")) {
target_key = &password;
g_warning("password may be visible in process listings");
} else {
g_warning("unknown key in spice URI parsing: %s", key);
goto fail;
}
if (target_key) {
if (*target_key) {
g_warning("double set of %s", key);
goto fail;
}
*target_key = g_strdup(value);
}
}
if (port == NULL && tls_port == NULL) {
g_warning("missing port or tls-port in spice URI");
goto fail;
}
/* parsed ok -> apply */
g_free(uri);
g_free(s->host);
g_free(s->port);
g_free(s->tls_port);
g_free(s->password);
s->host = g_strdup(host);
s->port = port;
s->tls_port = tls_port;
s->password = password;
return 0;
fail:
g_free(uri);
g_free(port);
g_free(tls_port);
g_free(password);
return -1;
}
static void spice_session_get_property(GObject *gobject,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
SpiceSession *session = SPICE_SESSION(gobject);
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
char buf[256];
int len;
switch (prop_id) {
case PROP_HOST:
g_value_set_string(value, s->host);
break;
case PROP_PORT:
g_value_set_string(value, s->port);
break;
case PROP_TLS_PORT:
g_value_set_string(value, s->tls_port);
break;
case PROP_PASSWORD:
g_value_set_string(value, s->password);
break;
case PROP_CA_FILE:
g_value_set_string(value, s->ca_file);
break;
case PROP_CIPHERS:
g_value_set_string(value, s->ciphers);
break;
case PROP_PROTOCOL:
g_value_set_int(value, s->protocol);
break;
case PROP_URI:
len = spice_uri_create(session, buf, sizeof(buf));
g_value_set_string(value, len ? buf : NULL);
break;
case PROP_CLIENT_SOCKETS:
g_value_set_boolean(value, s->client_provided_sockets);
break;
case PROP_PUBKEY:
g_value_set_boxed(value, s->pubkey);
break;
case PROP_CERT_SUBJECT:
g_value_set_string(value, s->cert_subject);
break;
case PROP_VERIFY:
g_value_set_flags(value, s->verify);
break;
case PROP_MIGRATION_STATE:
g_value_set_enum(value, s->migration_state);
break;
case PROP_SMARTCARD:
g_value_set_boolean(value, s->smartcard);
break;
case PROP_SMARTCARD_CERTIFICATES:
g_value_set_boxed(value, s->smartcard_certificates);
break;
case PROP_SMARTCARD_DB:
g_value_set_string(value, s->smartcard_db);
break;
case PROP_USBREDIR:
g_value_set_boolean(value, s->usbredir);
break;
case PROP_INHIBIT_KEYBOARD_GRAB:
g_value_set_boolean(value, s->inhibit_keyboard_grab);
break;
case PROP_DISABLE_EFFECTS:
g_value_set_boxed(value, s->disable_effects);
break;
case PROP_COLOR_DEPTH:
g_value_set_int(value, s->color_depth);
break;
case PROP_AUDIO:
g_value_set_boolean(value, s->audio);
break;
case PROP_READ_ONLY:
g_value_set_boolean(value, s->read_only);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
break;
}
}
static void spice_session_set_property(GObject *gobject,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
SpiceSession *session = SPICE_SESSION(gobject);
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
const char *str;
switch (prop_id) {
case PROP_HOST:
g_free(s->host);
s->host = g_value_dup_string(value);
break;
case PROP_PORT:
g_free(s->port);
s->port = g_value_dup_string(value);
break;
case PROP_TLS_PORT:
g_free(s->tls_port);
s->tls_port = g_value_dup_string(value);
break;
case PROP_PASSWORD:
g_free(s->password);
s->password = g_value_dup_string(value);
break;
case PROP_CA_FILE:
g_free(s->ca_file);
s->ca_file = g_value_dup_string(value);
break;
case PROP_CIPHERS:
g_free(s->ciphers);
s->ciphers = g_value_dup_string(value);
break;
case PROP_PROTOCOL:
s->protocol = g_value_get_int(value);
break;
case PROP_URI:
str = g_value_get_string(value);
if (str != NULL)
spice_uri_parse(session, str);
break;
case PROP_CLIENT_SOCKETS:
s->client_provided_sockets = g_value_get_boolean(value);
break;
case PROP_PUBKEY:
g_byte_array_unref(s->pubkey);
s->pubkey = g_value_get_boxed(value);
if (s->pubkey)
s->verify = SPICE_SESSION_VERIFY_PUBKEY;
break;
case PROP_CERT_SUBJECT:
g_free(s->cert_subject);
s->cert_subject = g_value_dup_string(value);
if (s->cert_subject)
s->verify = SPICE_SESSION_VERIFY_SUBJECT;
break;
case PROP_VERIFY:
s->verify = g_value_get_flags(value);
break;
case PROP_MIGRATION_STATE:
s->migration_state = g_value_get_enum(value);
break;
case PROP_SMARTCARD:
s->smartcard = g_value_get_boolean(value);
break;
case PROP_SMARTCARD_CERTIFICATES:
g_strfreev(s->smartcard_certificates);
s->smartcard_certificates = g_value_dup_boxed(value);
break;
case PROP_SMARTCARD_DB:
g_free(s->smartcard_db);
s->smartcard_db = g_value_dup_string(value);
break;
case PROP_USBREDIR:
s->usbredir = g_value_get_boolean(value);
break;
case PROP_INHIBIT_KEYBOARD_GRAB:
s->inhibit_keyboard_grab = g_value_get_boolean(value);
break;
case PROP_DISABLE_EFFECTS:
g_strfreev(s->disable_effects);
s->disable_effects = g_value_dup_boxed(value);
break;
case PROP_COLOR_DEPTH:
s->color_depth = g_value_get_int(value);
break;
case PROP_AUDIO:
s->audio = g_value_get_boolean(value);
break;
case PROP_READ_ONLY:
s->read_only = g_value_get_boolean(value);
g_object_notify(gobject, "read-only");
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
break;
}
}
static void spice_session_class_init(SpiceSessionClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
gobject_class->dispose = spice_session_dispose;
gobject_class->finalize = spice_session_finalize;
gobject_class->get_property = spice_session_get_property;
gobject_class->set_property = spice_session_set_property;
/**
* SpiceSession:host:
*
* URL of the SPICE host to connect to
*
**/
g_object_class_install_property
(gobject_class, PROP_HOST,
g_param_spec_string("host",
"Host",
"Remote host",
"localhost",
G_PARAM_READWRITE |
G_PARAM_CONSTRUCT |
G_PARAM_STATIC_STRINGS));
/**
* SpiceSession:port:
*
* Port to connect to for unencrypted sessions
*
**/
g_object_class_install_property
(gobject_class, PROP_PORT,
g_param_spec_string("port",
"Port",
"Remote port (plaintext)",
NULL,
G_PARAM_READWRITE |
G_PARAM_STATIC_STRINGS));
/**
* SpiceSession:tls-port:
*
* Port to connect to for TLS sessions
*
**/
g_object_class_install_property
(gobject_class, PROP_TLS_PORT,
g_param_spec_string("tls-port",
"TLS port",
"Remote port (encrypted)",
NULL,
G_PARAM_READWRITE |
G_PARAM_STATIC_STRINGS));
/**
* SpiceSession:password:
*
* TLS password to use
*
**/
g_object_class_install_property
(gobject_class, PROP_PASSWORD,
g_param_spec_string("password",
"Password",
"",
NULL,
G_PARAM_READWRITE |
G_PARAM_STATIC_STRINGS));
/**
* SpiceSession:ca-file:
*
* File holding the CA certificates for the host the client is
* connecting to
*
**/
g_object_class_install_property
(gobject_class, PROP_CA_FILE,
g_param_spec_string("ca-file",
"CA file",
"File holding the CA certificates",
NULL,
G_PARAM_READWRITE |
G_PARAM_STATIC_STRINGS));
/**
* SpiceSession:ciphers:
*
**/
g_object_class_install_property
(gobject_class, PROP_CIPHERS,
g_param_spec_string("ciphers",
"Ciphers",
"SSL cipher list",
NULL,
G_PARAM_READWRITE |
G_PARAM_STATIC_STRINGS));
/**
* SpiceSession:protocol:
*
* Version of the SPICE protocol to use
*
**/
g_object_class_install_property
(gobject_class, PROP_PROTOCOL,
g_param_spec_int("protocol",
"Protocol",
"Spice protocol major version",
1, 2, 2,
G_PARAM_READWRITE |
G_PARAM_CONSTRUCT |
G_PARAM_STATIC_STRINGS));
/**
* SpiceSession:uri:
*
* URI of the SPICE host to connect to. The URI is of the form
* spice://hostname?port=XXX or spice://hostname?tls_port=XXX
*
**/
g_object_class_install_property
(gobject_class, PROP_URI,
g_param_spec_string("uri",
"URI",
"Spice connection URI",
NULL,
G_PARAM_READWRITE |
G_PARAM_STATIC_STRINGS));
/**
* SpiceSession:client-sockets:
*
**/
g_object_class_install_property
(gobject_class, PROP_CLIENT_SOCKETS,
g_param_spec_boolean("client-sockets",
"Client sockets",
"Sockets are provided by the client",
FALSE,
G_PARAM_READWRITE |
G_PARAM_STATIC_STRINGS));
/**
* SpiceSession:pubkey:
*
**/
g_object_class_install_property
(gobject_class, PROP_PUBKEY,
g_param_spec_boxed("pubkey",
"Pub Key",
"Public key to check",
G_TYPE_BYTE_ARRAY,
G_PARAM_READWRITE |
G_PARAM_STATIC_STRINGS));
/**
* SpiceSession:cert-subject:
*
**/
g_object_class_install_property
(gobject_class, PROP_CERT_SUBJECT,
g_param_spec_string("cert-subject",
"Cert Subject",
"Certificate subject to check",
NULL,
G_PARAM_READWRITE |
G_PARAM_STATIC_STRINGS));
/**
* SpiceSession:verify:
*
* #SpiceSessionVerify bit field indicating which parts of the peer
* certificate should be checked
**/
g_object_class_install_property
(gobject_class, PROP_VERIFY,
g_param_spec_flags("verify",
"Verify",
"Certificate verification parameters",
SPICE_TYPE_SESSION_VERIFY,
SPICE_SESSION_VERIFY_HOSTNAME,
G_PARAM_READWRITE |
G_PARAM_CONSTRUCT |
G_PARAM_STATIC_STRINGS));
/**
* SpiceSession:migration-state:
*
* #SpiceSessionMigration bit field indicating if a migration is in
* progress
*
**/
g_object_class_install_property
(gobject_class, PROP_MIGRATION_STATE,
g_param_spec_enum("migration-state",
"Migration state",
"Migration state",
SPICE_TYPE_SESSION_MIGRATION,
SPICE_SESSION_MIGRATION_NONE,
G_PARAM_READABLE |
G_PARAM_STATIC_STRINGS));
/**
* SpiceSession:disable-effects:
*
* A comma-separated list of effects to disable. The settings will
* be applied on new display channels. The following effets can be
* disabled "wallpaper", "font-smooth", "animation", and "all",
* which will disable all the effects. If NULL, don't apply changes.
*
* Since: 0.7
**/
g_object_class_install_property
(gobject_class, PROP_DISABLE_EFFECTS,
g_param_spec_boxed ("disable-effects",
"Disable effects",
"Comma-separated effects to disable",
G_TYPE_STRV,
G_PARAM_READWRITE |
G_PARAM_STATIC_STRINGS));
/**
* SpiceSession:color-depth:
*
* Display color depth to set on new display channels. If 0, don't set.
*
* Since: 0.7
**/
g_object_class_install_property
(gobject_class, PROP_COLOR_DEPTH,
g_param_spec_int("color-depth",
"Color depth",
"Display channel color depth",
0, 32, 0,
G_PARAM_READWRITE |
G_PARAM_STATIC_STRINGS));
/**
* SpiceSession:enable-smartcard:
*
* If set to TRUE, the smartcard channel will be enabled and smartcard
* events will be forwarded to the guest
*
* Since: 0.7
**/
g_object_class_install_property
(gobject_class, PROP_SMARTCARD,
g_param_spec_boolean("enable-smartcard",
"Enable smartcard event forwarding",
"Forward smartcard events to the SPICE server",
FALSE,
G_PARAM_READWRITE |
G_PARAM_STATIC_STRINGS));
/**
* SpiceSession:enable-audio:
*
* If set to TRUE, the audio channels will be enabled for
* playback and recording.
*
* Since: 0.8
**/
g_object_class_install_property
(gobject_class, PROP_AUDIO,
g_param_spec_boolean("enable-audio",
"Enable audio channels",
"Enable audio channels",
TRUE,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT |
G_PARAM_STATIC_STRINGS));
/**
* SpiceSession:smartcard-certificates:
*
* This property is used when one wants to simulate a smartcard with no
* hardware smartcard reader. If it's set to a NULL-terminated string
* array containing the names of 3 valid certificates, these will be
* used to simulate a smartcard in the guest
* @see_also: spice_smartcard_manager_insert_card()
*
* Since: 0.7
**/
g_object_class_install_property
(gobject_class, PROP_SMARTCARD_CERTIFICATES,
g_param_spec_boxed("smartcard-certificates",
"Smartcard certificates",
"Smartcard certificates for software-based smartcards",
G_TYPE_STRV,
G_PARAM_READABLE |
G_PARAM_WRITABLE |
G_PARAM_STATIC_STRINGS));
/**
* SpiceSession:smartcard-db:
*
* Path to the NSS certificate database containing the certificates to
* use to simulate a software smartcard
*
* Since: 0.7
**/
g_object_class_install_property
(gobject_class, PROP_SMARTCARD_DB,
g_param_spec_string("smartcard-db",
"Smartcard certificate database",
"Path to the database for smartcard certificates",
NULL,
G_PARAM_READABLE |
G_PARAM_WRITABLE |
G_PARAM_STATIC_STRINGS));
/**
* SpiceSession:enable-usbredir:
*
* If set to TRUE, the usbredir channel will be enabled and USB devices
* can be redirected to the guest
*
* Since: 0.8
**/
g_object_class_install_property
(gobject_class, PROP_USBREDIR,
g_param_spec_boolean("enable-usbredir",
"Enable USB device redirection",
"Forward USB devices to the SPICE server",
TRUE,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT |
G_PARAM_STATIC_STRINGS));
/**
* SpiceSession::inhibit-keyboard-grab
*
* This boolean is set by the usbredir channel to indicate to #SpiceDisplay
* that the keyboard grab should be temporarily released, because it is
* going to invoke policykit. It will get reset when the usbredir channel
* is done with polickit.
*
* Since: 0.8
**/
g_object_class_install_property
(gobject_class, PROP_INHIBIT_KEYBOARD_GRAB,
g_param_spec_boolean("inhibit-keyboard-grab",
"Inhibit Keyboard Grab",
"Request that SpiceDisplays don't grab the keyboard",
FALSE,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
/**
* SpiceSession::channel-new:
* @session: the session that emitted the signal
* @channel: the new #SpiceChannel
*
* The #SpiceSession::channel-new signal is emitted each time a #SpiceChannel is created.
**/
signals[SPICE_SESSION_CHANNEL_NEW] =
g_signal_new("channel-new",
G_OBJECT_CLASS_TYPE(gobject_class),
G_SIGNAL_RUN_FIRST,
G_STRUCT_OFFSET(SpiceSessionClass, channel_new),
NULL, NULL,
g_cclosure_marshal_VOID__OBJECT,
G_TYPE_NONE,
1,
SPICE_TYPE_CHANNEL);
/**
* SpiceSession::channel-destroy:
* @session: the session that emitted the signal
* @channel: the destroyed #SpiceChannel
*
* The #SpiceSession::channel-destroy signal is emitted each time a #SpiceChannel is destroyed.
**/
signals[SPICE_SESSION_CHANNEL_DESTROY] =
g_signal_new("channel-destroy",
G_OBJECT_CLASS_TYPE(gobject_class),
G_SIGNAL_RUN_FIRST,
G_STRUCT_OFFSET(SpiceSessionClass, channel_destroy),
NULL, NULL,
g_cclosure_marshal_VOID__OBJECT,
G_TYPE_NONE,
1,
SPICE_TYPE_CHANNEL);
/**
* SpiceSession:read-only:
*
* Whether this connection is read-only mode.
*
* Since: 0.8
**/
g_object_class_install_property
(gobject_class, PROP_READ_ONLY,
g_param_spec_boolean("read-only", "Read-only",
"Whether this connection is read-only mode",
FALSE,
G_PARAM_READWRITE |
G_PARAM_CONSTRUCT |
G_PARAM_STATIC_STRINGS));
g_type_class_add_private(klass, sizeof(SpiceSessionPrivate));
}
/* ------------------------------------------------------------------ */
/* public functions */
/**
* spice_session_new:
*
* Creates a new Spice session.
*
* Returns: a new #SpiceSession
**/
SpiceSession *spice_session_new(void)
{
return SPICE_SESSION(g_object_new(SPICE_TYPE_SESSION, NULL));
}
G_GNUC_INTERNAL
SpiceSession *spice_session_new_from_session(SpiceSession *session)
{
SpiceSession *copy = SPICE_SESSION(g_object_new(SPICE_TYPE_SESSION,
"host", NULL,
"ca-file", NULL,
NULL));
SpiceSessionPrivate *c = copy->priv, *s = session->priv;
g_warn_if_fail (c->host == NULL);
g_warn_if_fail (c->tls_port == NULL);
g_warn_if_fail (c->password == NULL);
g_warn_if_fail (c->ca_file == NULL);
g_warn_if_fail (c->ciphers == NULL);
g_warn_if_fail (c->cert_subject == NULL);
g_warn_if_fail (c->pubkey == NULL);
g_object_get(session,
"host", &c->host,
"tls-port", &c->tls_port,
"password", &c->password,
"ca-file", &c->ca_file,
"ciphers", &c->ciphers,
"cert-subject", &c->cert_subject,
"pubkey", &c->pubkey,
"verify", &c->verify,
"smartcard-certificates", &c->smartcard_certificates,
"smartcard-db", &c->smartcard_db,
NULL);
c->client_provided_sockets = s->client_provided_sockets;
c->protocol = s->protocol;
c->connection_id = s->connection_id;
return copy;
}
/**
* spice_session_connect:
* @session:
*
* Open the session using the #SpiceSession:host and
* #SpiceSession:port.
*
* Returns: %FALSE if the connection failed.
**/
gboolean spice_session_connect(SpiceSession *session)
{
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
g_return_val_if_fail(s != NULL, FALSE);
spice_session_disconnect(session);
s->disconnecting = FALSE;
s->client_provided_sockets = FALSE;
g_warn_if_fail(s->cmain == NULL);
s->cmain = spice_channel_new(session, SPICE_CHANNEL_MAIN, 0);
glz_decoder_window_clear(s->glz_window);
return spice_channel_connect(s->cmain);
}
/**
* spice_session_open_fd:
* @session:
* @fd: a file descriptor
*
* Open the session using the provided @fd socket file
* descriptor. This is useful if you create the fd yourself, for
* example to setup a SSH tunnel.
*
* Returns:
**/
gboolean spice_session_open_fd(SpiceSession *session, int fd)
{
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
g_return_val_if_fail(s != NULL, FALSE);
g_return_val_if_fail(fd >= 0, FALSE);
spice_session_disconnect(session);
s->client_provided_sockets = TRUE;
g_warn_if_fail(s->cmain == NULL);
s->cmain = spice_channel_new(session, SPICE_CHANNEL_MAIN, 0);
return spice_channel_open_fd(s->cmain, fd);
}
G_GNUC_INTERNAL
gboolean spice_session_get_client_provided_socket(SpiceSession *session)
{
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
g_return_val_if_fail(s != NULL, FALSE);
return s->client_provided_sockets;
}
G_GNUC_INTERNAL
void spice_session_switching_disconnect(SpiceSession *session)
{
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
struct channel *item;
RingItem *ring, *next;
g_return_if_fail(s != NULL);
g_return_if_fail(s->cmain != NULL);
/* disconnect/destroy all but main channel */
for (ring = ring_get_head(&s->channels); ring != NULL; ring = next) {
next = ring_next(&s->channels, ring);
item = SPICE_CONTAINEROF(ring, struct channel, link);
if (item->channel != s->cmain)
spice_channel_destroy(item->channel); /* /!\ item and channel are destroy() after this call */
}
g_warn_if_fail(!ring_is_empty(&s->channels)); /* ring_get_length() == 1 */
}
G_GNUC_INTERNAL
void spice_session_set_migration(SpiceSession *session, SpiceSession *migration)
{
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
SpiceSessionPrivate *m = SPICE_SESSION_GET_PRIVATE(migration);
gchar *tmp;
g_return_if_fail(s != NULL);
spice_session_set_migration_state(session, SPICE_SESSION_MIGRATION_MIGRATING);
g_warn_if_fail(s->migration == NULL);
s->migration = g_object_ref(migration);
tmp = s->host;
s->host = m->host;
m->host = tmp;
tmp = s->port;
s->port = m->port;
m->port = tmp;
tmp = s->tls_port;
s->tls_port = m->tls_port;
m->tls_port = tmp;
g_warn_if_fail(ring_get_length(&s->channels) == ring_get_length(&m->channels));
SPICE_DEBUG("migration channels left:%d (in migration:%d)",
ring_get_length(&s->channels), ring_get_length(&m->channels));
s->migration_left = spice_session_get_channels(session);
}
G_GNUC_INTERNAL
SpiceChannel* spice_session_lookup_channel(SpiceSession *session, gint id, gint type)
{
RingItem *ring, *next;
SpiceSessionPrivate *s = session->priv;
struct channel *c;
g_return_val_if_fail(s != NULL, NULL);
for (ring = ring_get_head(&s->channels);
ring != NULL; ring = next) {
next = ring_next(&s->channels, ring);
c = SPICE_CONTAINEROF(ring, struct channel, link);
if (c == NULL || c->channel == NULL) {
g_warn_if_reached();
continue;
}
if (id == spice_channel_get_channel_id(c->channel) &&
type == spice_channel_get_channel_type(c->channel))
break;
}
g_return_val_if_fail(ring != NULL, NULL);
return c->channel;
}
G_GNUC_INTERNAL
void spice_session_abort_migration(SpiceSession *session)
{
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
RingItem *ring, *next;
struct channel *c;
g_return_if_fail(s != NULL);
g_return_if_fail(s->migration != NULL);
for (ring = ring_get_head(&s->channels);
ring != NULL; ring = next) {
next = ring_next(&s->channels, ring);
c = SPICE_CONTAINEROF(ring, struct channel, link);
if (g_list_find(s->migration_left, c->channel))
continue;
spice_channel_swap(c->channel,
spice_session_lookup_channel(s->migration,
spice_channel_get_channel_id(c->channel),
spice_channel_get_channel_type(c->channel)));
}
g_list_free(s->migration_left);
s->migration_left = NULL;
spice_session_disconnect(s->migration);
g_object_unref(s->migration);
s->migration = NULL;
spice_session_set_migration_state(session, SPICE_SESSION_MIGRATION_NONE);
}
G_GNUC_INTERNAL
void spice_session_channel_migrate(SpiceSession *session, SpiceChannel *channel)
{
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
SpiceChannel *c;
gint id, type;
g_return_if_fail(s != NULL);
g_return_if_fail(s->migration != NULL);
g_return_if_fail(SPICE_IS_CHANNEL(channel));
id = spice_channel_get_channel_id(channel);
type = spice_channel_get_channel_type(channel);
SPICE_DEBUG("migrating channel id:%d type:%d", id, type);
c = spice_session_lookup_channel(s->migration, id, type);
g_return_if_fail(c != NULL);
spice_channel_swap(channel, c);
s->migration_left = g_list_remove(s->migration_left, channel);
if (g_list_length(s->migration_left) == 0) {
SPICE_DEBUG("all channel migrated");
spice_session_disconnect(s->migration);
g_object_unref(s->migration);
s->migration = NULL;
spice_session_set_migration_state(session, SPICE_SESSION_MIGRATION_NONE);
}
}
/* main context */
static gboolean after_main_init(gpointer data)
{
SpiceSession *self = data;
SpiceSessionPrivate *s = self->priv;
GList *l;
for (l = s->migration_left; l != NULL; ) {
SpiceChannel *channel = l->data;
l = l->next;
spice_session_channel_migrate(self, channel);
channel->priv->state = SPICE_CHANNEL_STATE_READY;
spice_channel_up(channel);
}
s->after_main_init = 0;
return FALSE;
}
/* coroutine context */
G_GNUC_INTERNAL
gboolean spice_session_migrate_after_main_init(SpiceSession *self)
{
SpiceSessionPrivate *s = self->priv;
if (!s->migrate_wait_init)
return FALSE;
g_return_val_if_fail(g_list_length(s->migration_left) != 0, FALSE);
g_return_val_if_fail(s->after_main_init == 0, FALSE);
s->migrate_wait_init = FALSE;
s->after_main_init = g_idle_add(after_main_init, self);
return TRUE;
}
/* main context */
G_GNUC_INTERNAL
void spice_session_migrate_end(SpiceSession *self)
{
SpiceSessionPrivate *s = self->priv;
SpiceMsgOut *out;
GList *l;
g_return_if_fail(s->migration);
g_return_if_fail(s->migration->priv->cmain);
g_return_if_fail(g_list_length(s->migration_left) != 0);
/* disconnect and reset all channels */
for (l = s->migration_left; l != NULL; ) {
SpiceChannel *channel = l->data;
l = l->next;
/* reset for migration, disconnect */
spice_channel_reset(channel, TRUE);
if (SPICE_IS_MAIN_CHANNEL(channel)) {
/* migrate main to target, so we can start talking */
spice_session_channel_migrate(self, channel);
} else {
/* freeze other channels */
channel->priv->state = SPICE_CHANNEL_STATE_MIGRATING;
}
}
spice_session_palettes_clear(self);
spice_session_images_clear(self);
glz_decoder_window_clear(s->glz_window);
/* send MIGRATE_END to target */
out = spice_msg_out_new(s->cmain, SPICE_MSGC_MAIN_MIGRATE_END);
spice_msg_out_send(out);
/* now wait after main init for the rest of channels migration */
s->migrate_wait_init = TRUE;
}
/**
* spice_session_get_read_only:
* @session: a #SpiceSession
*
* Returns: wether the @session is in read-only mode.
**/
gboolean spice_session_get_read_only(SpiceSession *self)
{
g_return_val_if_fail(SPICE_IS_SESSION(self), FALSE);
return self->priv->read_only;
}
/**
* spice_session_disconnect:
* @session:
*
* Disconnect the @session, and destroy all channels.
**/
void spice_session_disconnect(SpiceSession *session)
{
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
struct channel *item;
RingItem *ring, *next;
g_return_if_fail(s != NULL);
SPICE_DEBUG("session: disconnecting %d", s->disconnecting);
if (s->disconnecting)
return;
s->disconnecting = TRUE;
s->cmain = NULL;
for (ring = ring_get_head(&s->channels); ring != NULL; ring = next) {
next = ring_next(&s->channels, ring);
item = SPICE_CONTAINEROF(ring, struct channel, link);
spice_channel_destroy(item->channel); /* /!\ item and channel are destroy() after this call */
}
s->connection_id = 0;
/* we leave disconnecting = TRUE, so that spice_channel_destroy()
is not called multiple times on channels that are in pending
destroy state. */
}
/**
* spice_session_get_channels:
* @session: a #SpiceSession
*
* Get the list of current channels associated with this @session.
*
* Returns: (element-type SpiceChannel) (transfer container): a #GList
* of unowned #SpiceChannel channels.
**/
GList *spice_session_get_channels(SpiceSession *session)
{
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
struct channel *item;
GList *list = NULL;
RingItem *ring;
g_return_val_if_fail(s != NULL, NULL);
for (ring = ring_get_head(&s->channels);
ring != NULL;
ring = ring_next(&s->channels, ring)) {
item = SPICE_CONTAINEROF(ring, struct channel, link);
list = g_list_append(list, item->channel);
}
return list;
}
/**
* spice_session_has_channel_type:
* @session: a #SpiceSession
*
* See if there is a @type channel in the channels associated with this
* @session.
*
* Returns: TRUE if a @type channel is available otherwise FALSE.
**/
gboolean spice_session_has_channel_type(SpiceSession *session, gint type)
{
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
struct channel *item;
RingItem *ring;
g_return_val_if_fail(s != NULL, FALSE);
for (ring = ring_get_head(&s->channels);
ring != NULL;
ring = ring_next(&s->channels, ring)) {
item = SPICE_CONTAINEROF(ring, struct channel, link);
if (spice_channel_get_channel_type(item->channel) == type) {
return TRUE;
}
}
return FALSE;
}
/* ------------------------------------------------------------------ */
/* private functions */
static GSocket *channel_connect_socket(SpiceChannel *channel,
GSocketAddress *sockaddr,
GError **error)
{
SpiceChannelPrivate *c = channel->priv;
GSocket *sock = g_socket_new(g_socket_address_get_family(sockaddr),
G_SOCKET_TYPE_STREAM,
G_SOCKET_PROTOCOL_DEFAULT,
error);
if (!sock)
return NULL;
g_socket_set_blocking(sock, FALSE);
if (!g_socket_connect(sock, sockaddr, NULL, error)) {
if (*error && (*error)->code == G_IO_ERROR_PENDING) {
g_clear_error(error);
SPICE_DEBUG("Socket pending");
g_coroutine_socket_wait(&c->coroutine, sock, G_IO_OUT | G_IO_ERR | G_IO_HUP);
if (!g_socket_check_connect_result(sock, error)) {
SPICE_DEBUG("Failed to connect %s", (*error)->message);
g_object_unref(sock);
return NULL;
}
} else {
SPICE_DEBUG("Socket error: %s", *error ? (*error)->message : "unknown");
g_object_unref(sock);
return NULL;
}
}
SPICE_DEBUG("Finally connected");
return sock;
}
/* coroutine context */
G_GNUC_INTERNAL
GSocket* spice_session_channel_open_host(SpiceSession *session, SpiceChannel *channel,
gboolean use_tls)
{
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
GSocketConnectable *addr;
GSocketAddressEnumerator *enumerator;
GSocketAddress *sockaddr;
GError *conn_error = NULL;
GSocket *sock = NULL;
int port;
if ((use_tls && !s->tls_port) || (!use_tls && !s->port))
return NULL;
port = atoi(use_tls ? s->tls_port : s->port);
SPICE_DEBUG("Resolving host %s %d", s->host, port);
addr = g_network_address_new(s->host, port);
enumerator = g_socket_connectable_enumerate (addr);
g_object_unref (addr);
/* Try each sockaddr until we succeed. Record the first
* connection error, but not any further ones (since they'll probably
* be basically the same as the first).
*/
while (!sock &&
(sockaddr = g_socket_address_enumerator_next(enumerator, NULL, &conn_error))) {
SPICE_DEBUG("Trying one socket");
g_clear_error(&conn_error);
sock = channel_connect_socket(channel, sockaddr, &conn_error);
if (conn_error != NULL)
SPICE_DEBUG("%s", conn_error->message);
g_object_unref(sockaddr);
}
g_object_unref(enumerator);
g_clear_error(&conn_error);
return sock;
}
G_GNUC_INTERNAL
void spice_session_channel_new(SpiceSession *session, SpiceChannel *channel)
{
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
struct channel *item;
g_return_if_fail(s != NULL);
g_return_if_fail(channel != NULL);
item = spice_new0(struct channel, 1);
item->channel = channel;
ring_add(&s->channels, &item->link);
if (SPICE_IS_MAIN_CHANNEL(channel)) {
gboolean all = spice_strv_contains(s->disable_effects, "all");
g_object_set(channel,
"disable-wallpaper", all || spice_strv_contains(s->disable_effects, "wallpaper"),
"disable-font-smooth", all || spice_strv_contains(s->disable_effects, "font-smooth"),
"disable-animation", all || spice_strv_contains(s->disable_effects, "animation"),
NULL);
if (s->color_depth != 0)
g_object_set(channel, "color-depth", s->color_depth, NULL);
}
g_signal_emit(session, signals[SPICE_SESSION_CHANNEL_NEW], 0, channel);
}
G_GNUC_INTERNAL
void spice_session_channel_destroy(SpiceSession *session, SpiceChannel *channel)
{
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
struct channel *item = NULL;
RingItem *ring, *next;
g_return_if_fail(s != NULL);
g_return_if_fail(channel != NULL);
if (s->migration_left)
s->migration_left = g_list_remove(s->migration_left, channel);
for (ring = ring_get_head(&s->channels); ring != NULL;
ring = next) {
next = ring_next(&s->channels, ring);
item = SPICE_CONTAINEROF(ring, struct channel, link);
if (item->channel == s->cmain) {
SPICE_DEBUG("the session lost the main channel");
s->cmain = NULL;
}
if (item->channel == channel) {
ring_remove(&item->link);
free(item);
g_signal_emit(session, signals[SPICE_SESSION_CHANNEL_DESTROY], 0, channel);
return;
}
}
g_warn_if_reached();
}
G_GNUC_INTERNAL
void spice_session_set_connection_id(SpiceSession *session, int id)
{
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
g_return_if_fail(s != NULL);
s->connection_id = id;
}
G_GNUC_INTERNAL
int spice_session_get_connection_id(SpiceSession *session)
{
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
g_return_val_if_fail(s != NULL, -1);
return s->connection_id;
}
#if !GLIB_CHECK_VERSION(2,27,2)
static guint64 g_get_monotonic_time(void)
{
GTimeVal tv;
/* TODO: support real monotonic clock? */
g_get_current_time(&tv);
return (((gint64) tv.tv_sec) * 1000000) + tv.tv_usec;
}
#endif
G_GNUC_INTERNAL
guint32 spice_session_get_mm_time(SpiceSession *session)
{
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
g_return_val_if_fail(s != NULL, 0);
/* FIXME: we may want to estimate the drift of clocks, and well,
do something better than this trivial approach */
return s->mm_time + (g_get_monotonic_time() - s->mm_time_at_clock) / 1000;
}
G_GNUC_INTERNAL
void spice_session_set_mm_time(SpiceSession *session, guint32 time)
{
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
g_return_if_fail(s != NULL);
s->mm_time = time;
s->mm_time_at_clock = g_get_monotonic_time();
SPICE_DEBUG("set mm time: %u", spice_session_get_mm_time(session));
}
G_GNUC_INTERNAL
void spice_session_set_port(SpiceSession *session, int port, gboolean tls)
{
const char *prop = tls ? "tls-port" : "port";
char *tmp;
g_return_if_fail(session != NULL);
/* old spicec client doesn't accept port == 0, see Migrate::start */
tmp = port > 0 ? g_strdup_printf("%d", port) : NULL;
g_object_set(session, prop, tmp, NULL);
g_free(tmp);
}
G_GNUC_INTERNAL
void spice_session_get_pubkey(SpiceSession *session, guint8 **pubkey, guint *size)
{
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
g_return_if_fail(s != NULL);
g_return_if_fail(pubkey != NULL);
g_return_if_fail(size != NULL);
*pubkey = s->pubkey ? s->pubkey->data : NULL;
*size = s->pubkey ? s->pubkey->len : 0;
}
G_GNUC_INTERNAL
guint spice_session_get_verify(SpiceSession *session)
{
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
g_return_val_if_fail(s != NULL, 0);
return s->verify;
}
G_GNUC_INTERNAL
void spice_session_set_migration_state(SpiceSession *session, SpiceSessionMigration state)
{
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
g_return_if_fail(s != NULL);
s->migration_state = state;
g_object_notify(G_OBJECT(session), "migration-state");
}
G_GNUC_INTERNAL
const gchar* spice_session_get_password(SpiceSession *session)
{
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
g_return_val_if_fail(s != NULL, NULL);
return s->password;
}
G_GNUC_INTERNAL
const gchar* spice_session_get_host(SpiceSession *session)
{
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
g_return_val_if_fail(s != NULL, NULL);
return s->host;
}
G_GNUC_INTERNAL
const gchar* spice_session_get_cert_subject(SpiceSession *session)
{
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
g_return_val_if_fail(s != NULL, NULL);
return s->cert_subject;
}
G_GNUC_INTERNAL
const gchar* spice_session_get_ciphers(SpiceSession *session)
{
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
g_return_val_if_fail(s != NULL, NULL);
return s->ciphers;
}
G_GNUC_INTERNAL
const gchar* spice_session_get_ca_file(SpiceSession *session)
{
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
g_return_val_if_fail(s != NULL, NULL);
return s->ca_file;
}
G_GNUC_INTERNAL
void spice_session_get_caches(SpiceSession *session,
display_cache **images,
display_cache **palettes,
SpiceGlzDecoderWindow **glz_window)
{
SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
g_return_if_fail(s != NULL);
if (images)
*images = &s->images;
if (palettes)
*palettes = &s->palettes;
if (glz_window)
*glz_window = s->glz_window;
}