summaryrefslogtreecommitdiffstats
path: root/plugins/spice
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/spice')
-rw-r--r--plugins/spice/CMakeLists.txt55
-rw-r--r--plugins/spice/scalable/emblems/org.remmina.Remmina-spice-ssh-symbolic.svg97
-rw-r--r--plugins/spice/scalable/emblems/org.remmina.Remmina-spice-symbolic.svg100
-rw-r--r--plugins/spice/spice_plugin.c836
-rw-r--r--plugins/spice/spice_plugin.h93
-rw-r--r--plugins/spice/spice_plugin_file_transfer.c244
-rw-r--r--plugins/spice/spice_plugin_usb.c100
7 files changed, 1525 insertions, 0 deletions
diff --git a/plugins/spice/CMakeLists.txt b/plugins/spice/CMakeLists.txt
new file mode 100644
index 0000000..875d53f
--- /dev/null
+++ b/plugins/spice/CMakeLists.txt
@@ -0,0 +1,55 @@
+# Remmina - The GTK+ Remote Desktop Client
+#
+# Copyright (C) 2016-2018 Denis Ollier
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the
+# OpenSSL library under certain conditions as described in each
+# individual source file, and distribute linked combinations
+# including the two.
+# You must obey the GNU General Public License in all respects
+# for all of the code used other than OpenSSL. * If you modify
+# file(s) with this exception, you may extend this exception to your
+# version of the file(s), but you are not obligated to do so. * If you
+# do not wish to do so, delete this exception statement from your
+# version. * If you delete this exception statement from all source
+# files in the program, then also delete it here.
+
+set(REMMINA_PLUGIN_SPICE_SRCS
+ spice_plugin.c
+ spice_plugin_file_transfer.c
+ spice_plugin_usb.c
+ )
+
+add_library(remmina-plugin-spice MODULE ${REMMINA_PLUGIN_SPICE_SRCS})
+set_target_properties(remmina-plugin-spice PROPERTIES PREFIX "")
+set_target_properties(remmina-plugin-spice PROPERTIES NO_SONAME 1)
+
+include_directories(${REMMINA_COMMON_INCLUDE_DIRS} ${SPICE_INCLUDE_DIRS})
+target_link_libraries(remmina-plugin-spice ${REMMINA_COMMON_LIBRARIES} ${SPICE_LIBRARIES})
+
+install(TARGETS remmina-plugin-spice DESTINATION ${REMMINA_PLUGINDIR})
+
+install(FILES
+ scalable/emblems/org.remmina.Remmina-spice-ssh-symbolic.svg
+ scalable/emblems/org.remmina.Remmina-spice-symbolic.svg
+ DESTINATION ${APPICONSCALE_EMBLEMS_DIR})
+
+if(WITH_ICON_CACHE)
+ gtk_update_icon_cache("${REMMINA_DATADIR}/icons/hicolor")
+endif()
diff --git a/plugins/spice/scalable/emblems/org.remmina.Remmina-spice-ssh-symbolic.svg b/plugins/spice/scalable/emblems/org.remmina.Remmina-spice-ssh-symbolic.svg
new file mode 100644
index 0000000..db98dab
--- /dev/null
+++ b/plugins/spice/scalable/emblems/org.remmina.Remmina-spice-ssh-symbolic.svg
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="100"
+ height="100"
+ viewBox="0 0 26.458334 26.458333"
+ version="1.1"
+ id="svg8"
+ inkscape:version="0.92.2 2405546, 2018-03-11"
+ sodipodi:docname="remmina-spice-ssh-symbolic.svg">
+ <defs
+ id="defs2" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="4.0000001"
+ inkscape:cx="-13.204075"
+ inkscape:cy="42.75632"
+ inkscape:document-units="px"
+ inkscape:current-layer="g837"
+ inkscape:document-rotation="0"
+ showgrid="false"
+ borderlayer="true"
+ inkscape:showpageshadow="true"
+ units="px"
+ inkscape:pagecheckerboard="false"
+ showguides="true"
+ inkscape:window-width="1920"
+ inkscape:window-height="1041"
+ inkscape:window-x="1600"
+ inkscape:window-y="18"
+ inkscape:window-maximized="0"
+ objecttolerance="10"
+ guidetolerance="10"
+ inkscape:snap-tangential="true"
+ inkscape:snap-perpendicular="true"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid10"
+ dotted="false"
+ originx="-223.14604"
+ originy="-88.618291" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata5">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ <cc:license
+ rdf:resource="" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-223.14604,-181.92338)">
+ <g
+ id="g837"
+ transform="matrix(0.83396843,0,0,0.83396843,39.920727,35.024416)">
+ <g
+ transform="matrix(1.1087066,0,0,1.1087066,45.9421,19.072628)"
+ id="g817">
+ <path
+ style="isolation:isolate;fill:#000000;stroke-width:0.92462623"
+ d="M 49.886719 1.0253906 C 35.114901 1.0253906 23.296875 13.070848 23.296875 27.615234 L 23.296875 32.388672 L 10.34375 32.388672 L 10.34375 98.974609 L 31.503906 98.974609 L 89.65625 98.974609 L 89.65625 32.388672 L 76.474609 32.388672 L 76.474609 27.615234 C 76.474609 12.843381 64.43107 1.0253906 49.886719 1.0253906 z M 49.658203 15.117188 C 56.702929 15.117188 62.158203 20.797031 62.158203 27.615234 L 62.158203 32.388672 L 37.228516 32.388672 L 37.160156 27.615234 C 37.160156 20.569565 42.840944 15.117187 49.658203 15.117188 z M 65.447266 49.119141 L 81.927734 84.054688 L 69.425781 84.054688 L 59.197266 62.371094 L 65.447266 49.119141 z M 50.216797 49.291016 L 66.697266 84.226562 L 53.324219 84.226562 L 43.53125 63.466797 L 50.216797 49.291016 z M 34.550781 49.304688 L 51.03125 84.240234 L 18.072266 84.240234 L 34.550781 49.304688 z "
+ transform="matrix(0.28615164,0,0,0.28615164,156.72391,141.67128)"
+ id="path79" />
+ </g>
+ <g
+ style="fill:#ffffff"
+ transform="matrix(0.65160764,0,0,0.79754876,80.032738,36.675708)"
+ id="g822" />
+ </g>
+ </g>
+</svg>
diff --git a/plugins/spice/scalable/emblems/org.remmina.Remmina-spice-symbolic.svg b/plugins/spice/scalable/emblems/org.remmina.Remmina-spice-symbolic.svg
new file mode 100644
index 0000000..7019006
--- /dev/null
+++ b/plugins/spice/scalable/emblems/org.remmina.Remmina-spice-symbolic.svg
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="100"
+ height="100"
+ viewBox="0 0 26.458334 26.458333"
+ version="1.1"
+ id="svg8"
+ inkscape:version="0.92.3 (2405546, 2018-03-11)"
+ sodipodi:docname="spice protocol.svg">
+ <defs
+ id="defs2" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="2"
+ inkscape:cx="25.172184"
+ inkscape:cy="61.742456"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ inkscape:document-rotation="0"
+ showgrid="false"
+ borderlayer="true"
+ inkscape:showpageshadow="true"
+ units="px"
+ inkscape:pagecheckerboard="false"
+ showguides="true"
+ inkscape:window-width="1366"
+ inkscape:window-height="715"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ objecttolerance="10"
+ guidetolerance="10"
+ inkscape:snap-tangential="true"
+ inkscape:snap-perpendicular="true"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid10"
+ dotted="false"
+ originx="-223.14604"
+ originy="-88.618291" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata5">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ <cc:license
+ rdf:resource="" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-223.14604,-181.92338)">
+ <g
+ id="g822"
+ transform="matrix(0.85100655,0,0,1.0416072,33.247302,-14.623309)">
+ <path
+ inkscape:connector-curvature="0"
+ id="polygon65"
+ d="m 246.21287,194.41092 -3.04322,5.27151 4.98005,8.62583 h 6.08697 z"
+ style="isolation:isolate;vector-effect:non-scaling-stroke;fill:#000000;stroke:none;stroke-width:0.79374999;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:3" />
+ <path
+ inkscape:connector-curvature="0"
+ id="polygon67"
+ d="m 238.79737,194.47917 -3.25561,5.63893 4.76766,8.25841 h 6.51175 z"
+ style="isolation:isolate;vector-effect:non-scaling-stroke;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.64583325;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none" />
+ <polygon
+ transform="matrix(0.26458333,0,0,0.26458333,187.32648,46.374434)"
+ points="165.707,559.786 196.032,612.311 135.381,612.311 "
+ stroke-miterlimit="3"
+ id="polygon820"
+ style="isolation:isolate;vector-effect:non-scaling-stroke;fill:#000000;stroke:none;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;paint-order:fill markers stroke" />
+ </g>
+ </g>
+</svg>
diff --git a/plugins/spice/spice_plugin.c b/plugins/spice/spice_plugin.c
new file mode 100644
index 0000000..fea8a3e
--- /dev/null
+++ b/plugins/spice/spice_plugin.c
@@ -0,0 +1,836 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-2018 Denis Ollier
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#include "spice_plugin.h"
+
+#define XSPICE_DEFAULT_PORT 5900
+
+enum {
+ REMMINA_PLUGIN_SPICE_FEATURE_PREF_VIEWONLY = 1,
+ REMMINA_PLUGIN_SPICE_FEATURE_DYNRESUPDATE,
+ REMMINA_PLUGIN_SPICE_FEATURE_PREF_DISABLECLIPBOARD,
+ REMMINA_PLUGIN_SPICE_FEATURE_TOOL_SENDCTRLALTDEL,
+ REMMINA_PLUGIN_SPICE_FEATURE_TOOL_USBREDIR,
+ REMMINA_PLUGIN_SPICE_FEATURE_SCALE
+};
+
+RemminaPluginService *remmina_plugin_service = NULL;
+
+static void remmina_plugin_spice_channel_new_cb(SpiceSession *, SpiceChannel *, RemminaProtocolWidget *);
+static void remmina_plugin_spice_main_channel_event_cb(SpiceChannel *, SpiceChannelEvent, RemminaProtocolWidget *);
+static void remmina_plugin_spice_agent_connected_event_cb(SpiceChannel *, RemminaProtocolWidget *);
+static void remmina_plugin_spice_display_ready_cb(GObject *, GParamSpec *, RemminaProtocolWidget *);
+static void remmina_plugin_spice_update_scale_mode(RemminaProtocolWidget *);
+static void remmina_plugin_spice_session_open_fd(RemminaProtocolWidget *);
+static void remmina_plugin_spice_channel_open_fd_cb(SpiceChannel *channel, gint tls G_GNUC_UNUSED, RemminaProtocolWidget *);
+
+void remmina_plugin_spice_select_usb_devices(RemminaProtocolWidget *);
+#ifdef SPICE_GTK_CHECK_VERSION
+# if SPICE_GTK_CHECK_VERSION(0, 31, 0)
+void remmina_plugin_spice_file_transfer_new_cb(SpiceMainChannel *, SpiceFileTransferTask *, RemminaProtocolWidget *);
+# endif /* SPICE_GTK_CHECK_VERSION(0, 31, 0) */
+#endif /* SPICE_GTK_CHECK_VERSION */
+
+gchar* str_replace(const gchar *string, const gchar *search, const gchar *replacement)
+{
+ TRACE_CALL(__func__);
+ gchar *str, **arr;
+
+ g_return_val_if_fail(string != NULL, NULL);
+ g_return_val_if_fail(search != NULL, NULL);
+
+ if (replacement == NULL)
+ replacement = "";
+
+ arr = g_strsplit(string, search, -1);
+ if (arr != NULL && arr[0] != NULL)
+ str = g_strjoinv(replacement, arr);
+ else
+ str = g_strdup(string);
+
+ g_strfreev(arr);
+ return str;
+}
+
+static void
+remmina_plugin_spice_session_open_fd(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+ gint fd = remmina_plugin_service->open_unix_sock(gpdata->unixPath);
+ REMMINA_PLUGIN_DEBUG("Opening spice session with FD: %d -> %s", fd, gpdata->unixPath);
+ spice_session_open_fd(gpdata->session, fd);
+}
+
+static void
+remmina_plugin_spice_channel_open_fd_cb(SpiceChannel *channel, gint tls, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+
+ gint id, type;
+ g_object_get(channel, "channel-id", &id, "channel-type", &type, NULL);
+ gint fd = remmina_plugin_service->open_unix_sock(gpdata->unixPath);
+ REMMINA_PLUGIN_DEBUG ("Opening channel %p %s %d with FD: %d -> %s", channel, g_type_name(G_OBJECT_TYPE(channel)), id, fd, gpdata->unixPath);
+ spice_channel_open_fd(channel, fd);
+}
+
+static void remmina_plugin_spice_init(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ RemminaPluginSpiceData *gpdata;
+ RemminaFile *remminafile;
+
+ gpdata = g_new0(RemminaPluginSpiceData, 1);
+ g_object_set_data_full(G_OBJECT(gp), "plugin-data", gpdata, g_free);
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ gpdata->session = spice_session_new();
+ g_signal_connect(gpdata->session,
+ "channel-new",
+ G_CALLBACK(remmina_plugin_spice_channel_new_cb),
+ gp);
+
+ g_object_set(gpdata->session,
+ "password", g_strdup(remmina_plugin_service->file_get_string(remminafile, "password")),
+ "read-only", remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE),
+ "enable-audio", remmina_plugin_service->file_get_int(remminafile, "enableaudio", FALSE),
+ "enable-smartcard", remmina_plugin_service->file_get_int(remminafile, "sharesmartcard", FALSE),
+ "shared-dir", remmina_plugin_service->file_get_string(remminafile, "sharefolder"),
+ NULL);
+
+ gpdata->gtk_session = spice_gtk_session_get(gpdata->session);
+ g_object_set(gpdata->gtk_session,
+ "auto-clipboard",
+ !remmina_plugin_service->file_get_int(remminafile, "disableclipboard", FALSE),
+ NULL);
+}
+
+static gboolean remmina_plugin_spice_open_connection(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ gint port;
+ const gchar *cacert;
+ gchar *host, *tunnel;
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ /* Setup SSH tunnel if needed */
+ tunnel = remmina_plugin_service->protocol_plugin_start_direct_tunnel(gp, XSPICE_DEFAULT_PORT, FALSE);
+
+ if (!tunnel) {
+ return FALSE;
+ }
+
+ /**-START- UNIX socket */
+ if(strstr(g_strdup(tunnel), "unix:///") != NULL) {
+ REMMINA_PLUGIN_DEBUG("Tunnel contain unix:// -> %s", tunnel);
+ gchar *val = str_replace(tunnel, "unix://", "");
+ REMMINA_PLUGIN_DEBUG("tunnel after cleaning = %s", val);
+ g_object_set(gpdata->session, "unix-path", val, NULL);
+ gpdata->isUnix = TRUE;
+ gint fd = remmina_plugin_service->open_unix_sock(val);
+ REMMINA_PLUGIN_DEBUG("Unix socket fd: %d", fd);
+ gpdata->unixPath = g_strdup(val);
+ if (fd > 0)
+ remmina_plugin_spice_session_open_fd(gp);
+ g_free(val);
+ } else {
+
+ remmina_plugin_service->get_server_port(tunnel,
+ XSPICE_DEFAULT_PORT,
+ &host,
+ &port);
+
+ g_object_set(gpdata->session, "host", host, NULL);
+ gpdata->isUnix = FALSE;
+ g_free(host);
+ g_free(tunnel);
+
+ /* Unencrypted connection */
+ if (!remmina_plugin_service->file_get_int(remminafile, "usetls", FALSE)) {
+ g_object_set(gpdata->session, "port", g_strdup_printf("%i", port), NULL);
+ }
+ /* TLS encrypted connection */
+ else{
+ g_object_set(gpdata->session, "tls_port", g_strdup_printf("%i", port), NULL);
+
+ /* Server CA certificate */
+ cacert = remmina_plugin_service->file_get_string(remminafile, "cacert");
+ if (cacert) {
+ g_object_set(gpdata->session, "ca-file", cacert, NULL);
+ }
+ }
+
+ spice_session_connect(gpdata->session);
+ }
+ /** -END- UNIX socket */
+
+ /*
+ * FIXME: Add a waiting loop until the g_signal "channel-event" occurs.
+ * If the event is SPICE_CHANNEL_OPENED, TRUE should be returned,
+ * otherwise FALSE should be returned.
+ */
+ return TRUE;
+}
+
+static gboolean remmina_plugin_spice_close_connection(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+
+ if (gpdata->main_channel) {
+ g_signal_handlers_disconnect_by_func(gpdata->main_channel,
+ G_CALLBACK(remmina_plugin_spice_main_channel_event_cb),
+ gp);
+ g_signal_handlers_disconnect_by_func(gpdata->main_channel,
+ G_CALLBACK(remmina_plugin_spice_agent_connected_event_cb),
+ gp);
+ }
+
+ if (gpdata->session) {
+ spice_session_disconnect(gpdata->session);
+ g_object_unref(gpdata->session);
+ gpdata->session = NULL;
+ remmina_plugin_service->protocol_plugin_signal_connection_closed(gp);
+ }
+
+#ifdef SPICE_GTK_CHECK_VERSION
+# if SPICE_GTK_CHECK_VERSION(0, 31, 0)
+ if (gpdata->file_transfers) {
+ g_hash_table_unref(gpdata->file_transfers);
+ }
+# endif /* SPICE_GTK_CHECK_VERSION(0, 31, 0) */
+#endif /* SPICE_GTK_CHECK_VERSION */
+
+ return FALSE;
+}
+
+static gboolean remmina_plugin_spice_disable_gst_overlay(SpiceChannel *channel, void* pipeline_ptr, RemminaProtocolWidget *gp)
+{
+ g_signal_stop_emission_by_name(channel, "gst-video-overlay");
+ return FALSE;
+}
+
+static void remmina_plugin_spice_channel_new_cb(SpiceSession *session, SpiceChannel *channel, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ gint id, type;
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ g_return_if_fail(gpdata != NULL);
+
+ if(gpdata->isUnix) {
+ g_signal_connect(channel, "open-fd", G_CALLBACK(remmina_plugin_spice_channel_open_fd_cb), gp);
+ }
+
+ g_object_get(channel, "channel-id", &id, "channel-type", &type, NULL);
+ REMMINA_PLUGIN_DEBUG ("New spice channel %p %s %d", channel, g_type_name(G_OBJECT_TYPE(channel)), id);
+
+ if (SPICE_IS_MAIN_CHANNEL(channel)) {
+ gpdata->main_channel = SPICE_MAIN_CHANNEL(channel);
+ g_signal_connect(channel,
+ "channel-event",
+ G_CALLBACK(remmina_plugin_spice_main_channel_event_cb),
+ gp);
+ g_signal_connect(channel,
+ "main-agent-update",
+ G_CALLBACK(remmina_plugin_spice_agent_connected_event_cb),
+ gp);
+#ifdef SPICE_GTK_CHECK_VERSION
+# if SPICE_GTK_CHECK_VERSION(0, 31, 0)
+ g_signal_connect(channel,
+ "new-file-transfer",
+ G_CALLBACK(remmina_plugin_spice_file_transfer_new_cb),
+ gp);
+# endif /* SPICE_GTK_CHECK_VERSION(0, 31, 0) */
+#endif /* SPICE_GTK_CHECK_VERSION */
+ }
+
+ if (SPICE_IS_DISPLAY_CHANNEL(channel)) {
+ gpdata->display_channel = SPICE_DISPLAY_CHANNEL(channel);
+ gpdata->display = spice_display_new(gpdata->session, id);
+ g_signal_connect(gpdata->display,
+ "notify::ready",
+ G_CALLBACK(remmina_plugin_spice_display_ready_cb),
+ gp);
+ remmina_plugin_spice_display_ready_cb(G_OBJECT(gpdata->display), NULL, gp);
+
+ if (remmina_plugin_service->file_get_int(remminafile, "disablegstvideooverlay", FALSE)) {
+ g_signal_connect(channel,
+ "gst-video-overlay",
+ G_CALLBACK(remmina_plugin_spice_disable_gst_overlay),
+ gp);
+ }
+
+ }
+
+ if (SPICE_IS_INPUTS_CHANNEL(channel)) {
+ REMMINA_PLUGIN_DEBUG("New inputs channel");
+ }
+ if (SPICE_IS_PLAYBACK_CHANNEL(channel)) {
+ REMMINA_PLUGIN_DEBUG("New audio channel");
+ if (remmina_plugin_service->file_get_int(remminafile, "enableaudio", FALSE)) {
+ gpdata->audio = spice_audio_get(gpdata->session, NULL);
+ }
+ }
+
+ if (SPICE_IS_USBREDIR_CHANNEL(channel)) {
+ REMMINA_PLUGIN_DEBUG("New usbredir channel");
+ }
+
+ if (SPICE_IS_WEBDAV_CHANNEL(channel)) {
+ REMMINA_PLUGIN_DEBUG("New webdav channel");
+ if (remmina_plugin_service->file_get_string(remminafile, "sharefolder")) {
+ spice_channel_connect(channel);
+ }
+ }
+}
+
+static gboolean remmina_plugin_spice_ask_auth(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ gint ret;
+ gboolean disablepasswordstoring;
+ gchar *s_password;
+ gboolean save;
+
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ disablepasswordstoring = remmina_plugin_service->file_get_int(remminafile, "disablepasswordstoring", FALSE);
+
+ ret = remmina_plugin_service->protocol_plugin_init_auth(gp,
+ (disablepasswordstoring ? 0 : REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD),
+ _("Enter SPICE password"),
+ NULL,
+ remmina_plugin_service->file_get_string(remminafile, "password"),
+ NULL,
+ NULL);
+ if (ret == GTK_RESPONSE_OK) {
+ s_password = remmina_plugin_service->protocol_plugin_init_get_password(gp);
+ save = remmina_plugin_service->protocol_plugin_init_get_savepassword(gp);
+ if (save) {
+ remmina_plugin_service->file_set_string(remminafile, "password", s_password);
+ } else {
+ remmina_plugin_service->file_set_string(remminafile, "password", NULL);
+ }
+ } else {
+ return FALSE;
+ }
+
+ g_object_set(gpdata->session, "password", s_password, NULL);
+ return TRUE;
+}
+
+static void remmina_plugin_spice_main_channel_event_cb(SpiceChannel *channel, SpiceChannelEvent event, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+ gchar *serverOption = g_strdup(remmina_plugin_service->file_get_string(remminafile, "server"));
+ gchar *message = NULL;
+ gchar *server = NULL;
+
+ if(gpdata->isUnix) {
+ gchar *val = str_replace(serverOption, "unix://", "");
+ message = g_strdup_printf("Unix socket server %s", val);
+ g_free(val), val = NULL;
+ } else {
+ gint port;
+ RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+ remmina_plugin_service->get_server_port(remmina_plugin_service->file_get_string(remminafile, "server"),
+ XSPICE_DEFAULT_PORT,
+ &server,
+ &port);
+ message = g_strdup_printf("TCP server %s:%d", server, port);
+ }
+
+ switch (event) {
+ case SPICE_CHANNEL_CLOSED:
+ remmina_plugin_service->protocol_plugin_set_error(gp, _("Disconnected from the SPICE %s."), message);
+ remmina_plugin_spice_close_connection(gp);
+ REMMINA_PLUGIN_AUDIT(_("Disconnected from %s via SPICE"), message);
+ break;
+ case SPICE_CHANNEL_OPENED:
+ REMMINA_PLUGIN_AUDIT(_("Connected to %s via SPICE"), message);
+ break;
+ case SPICE_CHANNEL_ERROR_AUTH:
+ if (remmina_plugin_spice_ask_auth(gp)) {
+ remmina_plugin_spice_open_connection(gp);
+ }else{
+ /* Connection is cancelled by the user by clicking cancel on auth panel, close it without showing errors */
+ // remmina_plugin_service->protocol_plugin_set_error(gp, _("Invalid password."));
+ remmina_plugin_spice_close_connection(gp);
+ }
+ break;
+ case SPICE_CHANNEL_ERROR_TLS:
+ remmina_plugin_service->protocol_plugin_set_error(gp, _("TLS connection error."));
+ remmina_plugin_spice_close_connection(gp);
+ break;
+ case SPICE_CHANNEL_ERROR_IO:
+ case SPICE_CHANNEL_ERROR_LINK:
+ case SPICE_CHANNEL_ERROR_CONNECT:
+ remmina_plugin_service->protocol_plugin_set_error(gp, _("Connection to the SPICE server dropped."));
+ remmina_plugin_spice_close_connection(gp);
+ break;
+ default:
+ break;
+ }
+ g_free(server), server = NULL;
+ g_free(message), message = NULL;
+ g_free(serverOption), message = NULL;
+}
+
+void remmina_plugin_spice_agent_connected_event_cb(SpiceChannel *channel, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ gboolean connected;
+
+ g_object_get(channel,
+ "agent-connected", &connected,
+ NULL);
+
+ if (connected) {
+ remmina_plugin_service->protocol_plugin_unlock_dynres(gp);
+ } else {
+ remmina_plugin_service->protocol_plugin_lock_dynres(gp);
+ }
+}
+
+static void remmina_plugin_spice_display_ready_cb(GObject *display, GParamSpec *param_spec, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ gboolean ready;
+
+ g_object_get(display, "ready", &ready, NULL);
+
+ if (ready) {
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ g_signal_handlers_disconnect_by_func(display,
+ G_CALLBACK(remmina_plugin_spice_display_ready_cb),
+ gp);
+
+ RemminaScaleMode scaleMode = remmina_plugin_service->remmina_protocol_widget_get_current_scale_mode(gp);
+ g_object_set(display,
+ "scaling", (scaleMode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED),
+ "resize-guest", (scaleMode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES),
+ NULL);
+
+#ifdef SPICE_GTK_CHECK_VERSION
+# if SPICE_GTK_CHECK_VERSION(0, 34, 0)
+ SpiceVideoCodecType videocodec = remmina_plugin_service->file_get_int(remminafile, "videocodec", 0);
+ if (videocodec) {
+# if SPICE_GTK_CHECK_VERSION(0, 38, 0)
+ GError *err = NULL;
+ guint i;
+
+ GArray *preferred_codecs = g_array_sized_new(FALSE, FALSE,
+ sizeof(gint),
+ (SPICE_VIDEO_CODEC_TYPE_ENUM_END - 1));
+
+ g_array_append_val(preferred_codecs, videocodec);
+ for (i = SPICE_VIDEO_CODEC_TYPE_MJPEG; i < SPICE_VIDEO_CODEC_TYPE_ENUM_END; ++i) {
+ if (i != videocodec) {
+ g_array_append_val(preferred_codecs, i);
+ }
+ }
+
+ if (!spice_display_channel_change_preferred_video_codec_types(SPICE_CHANNEL(gpdata->display_channel),
+ (gint *) preferred_codecs->data,
+ preferred_codecs->len,
+ &err)) {
+ REMMINA_PLUGIN_DEBUG("Could not set video-codec preference. %s", err->message);
+ g_error_free(err);
+ }
+
+ g_clear_pointer(&preferred_codecs, g_array_unref);
+
+# elif SPICE_GTK_CHECK_VERSION(0, 35, 0)
+ spice_display_channel_change_preferred_video_codec_type(SPICE_CHANNEL(gpdata->display_channel),
+ videocodec);
+# else
+ spice_display_change_preferred_video_codec_type(SPICE_CHANNEL(gpdata->display_channel),
+ videocodec);
+# endif
+ }
+# endif
+#endif
+
+#ifdef SPICE_GTK_CHECK_VERSION
+# if SPICE_GTK_CHECK_VERSION(0, 31, 0)
+ SpiceImageCompression imagecompression = remmina_plugin_service->file_get_int(remminafile, "imagecompression", 0);
+ if (imagecompression) {
+# if SPICE_GTK_CHECK_VERSION(0, 35, 0)
+ spice_display_channel_change_preferred_compression(SPICE_CHANNEL(gpdata->display_channel),
+ imagecompression);
+# else
+ spice_display_change_preferred_compression(SPICE_CHANNEL(gpdata->display_channel),
+ imagecompression);
+# endif
+ }
+# endif
+#endif
+
+ gtk_container_add(GTK_CONTAINER(gp), GTK_WIDGET(display));
+ gtk_widget_show(GTK_WIDGET(display));
+
+ remmina_plugin_service->protocol_plugin_register_hostkey(gp, GTK_WIDGET(display));
+ remmina_plugin_service->protocol_plugin_signal_connection_opened(gp);
+ }
+}
+
+/* Send a keystroke to the plugin window */
+static void remmina_plugin_spice_keystroke(RemminaProtocolWidget *gp, const guint keystrokes[], const gint keylen)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+
+ if (gpdata->display) {
+ spice_display_send_keys(gpdata->display,
+ keystrokes,
+ keylen,
+ SPICE_DISPLAY_KEY_EVENT_CLICK);
+ }
+}
+
+/* Send CTRL+ALT+DEL keys keystrokes to the plugin socket widget */
+static void remmina_plugin_spice_send_ctrlaltdel(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ guint keys[] = { GDK_KEY_Control_L, GDK_KEY_Alt_L, GDK_KEY_Delete };
+
+ remmina_plugin_spice_keystroke(gp, keys, G_N_ELEMENTS(keys));
+}
+
+static void remmina_plugin_spice_update_scale_mode(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ gint width, height;
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaScaleMode scaleMode = remmina_plugin_service->remmina_protocol_widget_get_current_scale_mode(gp);
+
+ g_object_set(gpdata->display,
+ "scaling", (scaleMode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED),
+ "resize-guest", (scaleMode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES),
+ NULL);
+
+ if (scaleMode != REMMINA_PROTOCOL_WIDGET_SCALE_MODE_NONE) {
+ /* In scaled mode, the SpiceDisplay will get its dimensions from its parent */
+ gtk_widget_set_size_request(GTK_WIDGET(gpdata->display), -1, -1 );
+ }else {
+ /* In non scaled mode, the plugins forces dimensions of the SpiceDisplay */
+ g_object_get(gpdata->display_channel,
+ "width", &width,
+ "height", &height,
+ NULL);
+ gtk_widget_set_size_request(GTK_WIDGET(gpdata->display), width, height);
+ }
+}
+
+static gboolean remmina_plugin_spice_query_feature(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature)
+{
+ TRACE_CALL(__func__);
+
+ return TRUE;
+}
+
+static void remmina_plugin_spice_call_feature(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature)
+{
+ TRACE_CALL(__func__);
+
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ switch (feature->id) {
+ case REMMINA_PLUGIN_SPICE_FEATURE_PREF_VIEWONLY:
+ g_object_set(gpdata->session,
+ "read-only",
+ remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE),
+ NULL);
+ break;
+ case REMMINA_PLUGIN_SPICE_FEATURE_PREF_DISABLECLIPBOARD:
+ g_object_set(gpdata->gtk_session,
+ "auto-clipboard",
+ !remmina_plugin_service->file_get_int(remminafile, "disableclipboard", FALSE),
+ NULL);
+ break;
+ case REMMINA_PLUGIN_SPICE_FEATURE_DYNRESUPDATE:
+ case REMMINA_PLUGIN_SPICE_FEATURE_SCALE:
+ remmina_plugin_spice_update_scale_mode(gp);
+ break;
+ case REMMINA_PLUGIN_SPICE_FEATURE_TOOL_SENDCTRLALTDEL:
+ remmina_plugin_spice_send_ctrlaltdel(gp);
+ break;
+ case REMMINA_PLUGIN_SPICE_FEATURE_TOOL_USBREDIR:
+ remmina_plugin_spice_select_usb_devices(gp);
+ break;
+ default:
+ break;
+ }
+}
+
+#ifdef SPICE_GTK_CHECK_VERSION
+# if SPICE_GTK_CHECK_VERSION(0, 34, 0)
+/* Array of key/value pairs for preferred video codec
+ * Key - SpiceVideoCodecType (spice/enums.h)
+ */
+static gpointer videocodec_list[] =
+{
+ "0", N_("Default"),
+ "1", "mjpeg",
+ "2", "vp8",
+ "3", "h264",
+ "4", "vp9",
+ "5", "h265",
+ NULL
+};
+# endif
+#endif
+
+#ifdef SPICE_GTK_CHECK_VERSION
+# if SPICE_GTK_CHECK_VERSION(0, 31, 0)
+/* Array of key/value pairs for preferred video codec
+ * Key - SpiceImageCompression (spice/enums.h)
+ */
+static gpointer imagecompression_list[] =
+{
+ "0", N_("Default"),
+ "1", N_("Off"),
+ "2", N_("Auto GLZ"),
+ "3", N_("Auto LZ"),
+ "4", "Quic",
+ "5", "GLZ",
+ "6", "LZ",
+ "7", "LZ4",
+ NULL
+};
+# endif
+#endif
+
+#ifdef SPICE_GTK_CHECK_VERSION
+# if SPICE_GTK_CHECK_VERSION(0, 34, 0)
+static gchar disablegstvideooverlay_tooltip[] =
+ N_("Disable video overlay if videos are not displayed properly.\n");
+# endif
+#endif
+
+/* Array of RemminaProtocolSetting for basic settings.
+ * Each item is composed by:
+ * a) RemminaProtocolSettingType for setting type
+ * b) Setting name
+ * c) Setting description
+ * d) Compact disposition
+ * e) Values for REMMINA_PROTOCOL_SETTING_TYPE_SELECT or REMMINA_PROTOCOL_SETTING_TYPE_COMBO
+ * f) Setting tooltip
+ * g) Validation data pointer, will be passed to the validation callback method.
+ * h) Validation callback method (Can be NULL. Every entry will be valid then.)
+ * use following prototype:
+ * gboolean mysetting_validator_method(gpointer key, gpointer value,
+ * gpointer validator_data);
+ * gpointer key is a gchar* containing the setting's name,
+ * gpointer value contains the value which should be validated,
+ * gpointer validator_data contains your passed data.
+ */
+static const RemminaProtocolSetting remmina_plugin_spice_basic_settings[] =
+{
+ { REMMINA_PROTOCOL_SETTING_TYPE_SERVER, "server", NULL, FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD, "password", N_("User password"), FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "usetls", N_("Use TLS encryption"), FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_FILE, "cacert", N_("Server CA certificate"), FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "sharefolder", N_("Share folder"), FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, FALSE, NULL, NULL, NULL, NULL }
+};
+
+/* Array of RemminaProtocolSetting for advanced settings.
+ * Each item is composed by:
+ * a) RemminaProtocolSettingType for setting type
+ * b) Setting name
+ * c) Setting description
+ * d) Compact disposition
+ * e) Values for REMMINA_PROTOCOL_SETTING_TYPE_SELECT or REMMINA_PROTOCOL_SETTING_TYPE_COMBO
+ * f) Setting Tooltip
+ */
+static const RemminaProtocolSetting remmina_plugin_spice_advanced_settings[] =
+{
+#ifdef SPICE_GTK_CHECK_VERSION
+# if SPICE_GTK_CHECK_VERSION(0, 35, 0)
+ { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "videocodec", N_("Preferred video codec"), FALSE, videocodec_list, NULL},
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disablegstvideooverlay", N_("Turn off GStreamer overlay"), FALSE, NULL, disablegstvideooverlay_tooltip},
+# endif
+# if SPICE_GTK_CHECK_VERSION(0, 31, 0)
+ { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "imagecompression", N_("Preferred image compression"), FALSE, imagecompression_list, NULL},
+# endif
+#endif
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disableclipboard", N_("No clipboard sync"), TRUE, NULL, NULL},
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disablepasswordstoring", N_("Forget passwords after use"), TRUE, NULL, NULL},
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "enableaudio", N_("Enable audio channel"), TRUE, NULL, NULL},
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "sharesmartcard", N_("Share smart card"), TRUE, NULL, NULL},
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "viewonly", N_("View only"), TRUE, NULL, NULL},
+ { REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, TRUE, NULL, NULL}
+};
+
+/* Array for available features.
+ * The last element of the array must be REMMINA_PROTOCOL_FEATURE_TYPE_END. */
+static const RemminaProtocolFeature remmina_plugin_spice_features[] =
+{
+ { REMMINA_PROTOCOL_FEATURE_TYPE_PREF, REMMINA_PLUGIN_SPICE_FEATURE_PREF_VIEWONLY, GINT_TO_POINTER(REMMINA_PROTOCOL_FEATURE_PREF_CHECK), "viewonly", N_("View only")},
+ { REMMINA_PROTOCOL_FEATURE_TYPE_PREF, REMMINA_PLUGIN_SPICE_FEATURE_PREF_DISABLECLIPBOARD, GINT_TO_POINTER(REMMINA_PROTOCOL_FEATURE_PREF_CHECK), "disableclipboard", N_("No clipboard sync")},
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, REMMINA_PLUGIN_SPICE_FEATURE_TOOL_SENDCTRLALTDEL, N_("Send Ctrl+Alt+Delete"), NULL, NULL},
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, REMMINA_PLUGIN_SPICE_FEATURE_TOOL_USBREDIR, N_("Select USB devices for redirection"), NULL, NULL},
+ { REMMINA_PROTOCOL_FEATURE_TYPE_DYNRESUPDATE, REMMINA_PLUGIN_SPICE_FEATURE_DYNRESUPDATE, NULL, NULL, NULL},
+ { REMMINA_PROTOCOL_FEATURE_TYPE_SCALE, REMMINA_PLUGIN_SPICE_FEATURE_SCALE, NULL, NULL, NULL},
+ { REMMINA_PROTOCOL_FEATURE_TYPE_END, 0, NULL, NULL, NULL}
+};
+
+
+static RemminaProtocolPlugin remmina_plugin_spice =
+{
+ REMMINA_PLUGIN_TYPE_PROTOCOL, // Type
+ "SPICE", // Name
+ N_("SPICE - Simple Protocol for Independent Computing Environments"), // Description
+ GETTEXT_PACKAGE, // Translation domain
+ VERSION, // Version number
+ "org.remmina.Remmina-spice-symbolic", // Icon for normal connection
+ "org.remmina.Remmina-spice-ssh-symbolic", // Icon for SSH connection
+ remmina_plugin_spice_basic_settings, // Array for basic settings
+ remmina_plugin_spice_advanced_settings, // Array for advanced settings
+ REMMINA_PROTOCOL_SSH_SETTING_TUNNEL, // SSH settings type
+ remmina_plugin_spice_features, // Array for available features
+ remmina_plugin_spice_init, // Plugin initialization
+ remmina_plugin_spice_open_connection, // Plugin open connection
+ remmina_plugin_spice_close_connection, // Plugin close connection
+ remmina_plugin_spice_query_feature, // Query for available features
+ remmina_plugin_spice_call_feature, // Call a feature
+ remmina_plugin_spice_keystroke, // Send a keystroke
+ NULL, // No screenshot support available
+ NULL, // RCW map event
+ NULL // RCW unmap event
+};
+
+void remmina_plugin_spice_remove_list_option(gpointer *option_list, const gchar *option_to_remove) {
+ gpointer *src, *dst;
+
+ TRACE_CALL(__func__);
+
+ dst = src = option_list;
+ while (*src) {
+ if (strcmp(*src, option_to_remove) != 0) {
+ if (dst != src) {
+ *dst = *src;
+ *(dst + 1) = *(src + 1);
+ }
+ dst += 2;
+ }
+ src += 2;
+ }
+ *dst = NULL;
+}
+
+#ifdef SPICE_GTK_CHECK_VERSION
+# if SPICE_GTK_CHECK_VERSION(0, 31, 0)
+gboolean remmina_plugin_spice_is_lz4_supported() {
+ gboolean result = FALSE;
+ GOptionContext *context;
+ GOptionGroup *spiceGroup;
+ gchar *spiceHelp;
+
+ TRACE_CALL(__func__);
+
+ spiceGroup = spice_get_option_group();
+ context = g_option_context_new("- SPICE client test application");
+ g_option_context_add_group(context, spiceGroup);
+
+ spiceHelp = g_option_context_get_help(context, FALSE, spiceGroup);
+ if (g_strcmp0(spiceHelp, "") != 0) {
+ gchar **spiceHelpLines, **line;
+ spiceHelpLines = g_strsplit(spiceHelp, "\n", -1);
+
+ for (line = spiceHelpLines; *line != NULL; ++line) {
+ if (g_strstr_len(*line, -1, "spice-preferred-compression")) {
+ if (g_strstr_len(*line, -1, ",lz4,")) {
+ result = TRUE;
+ }
+
+ break;
+ }
+ }
+
+ g_strfreev(spiceHelpLines);
+ }
+ g_option_context_free(context);
+ g_free(spiceHelp);
+
+ return result;
+}
+# endif
+#endif
+
+G_MODULE_EXPORT gboolean
+remmina_plugin_entry(RemminaPluginService *service)
+{
+ TRACE_CALL(__func__);
+ remmina_plugin_service = service;
+
+ bindtextdomain(GETTEXT_PACKAGE, REMMINA_RUNTIME_LOCALEDIR);
+ bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
+
+#ifdef SPICE_GTK_CHECK_VERSION
+# if SPICE_GTK_CHECK_VERSION(0, 31, 0)
+ if (!remmina_plugin_spice_is_lz4_supported()) {
+ char key_str[10];
+ sprintf(key_str, "%d", SPICE_IMAGE_COMPRESSION_LZ4);
+ remmina_plugin_spice_remove_list_option(imagecompression_list, key_str);
+ }
+# endif
+#endif
+
+ if (!service->register_plugin((RemminaPlugin*)&remmina_plugin_spice)) {
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
diff --git a/plugins/spice/spice_plugin.h b/plugins/spice/spice_plugin.h
new file mode 100644
index 0000000..d651689
--- /dev/null
+++ b/plugins/spice/spice_plugin.h
@@ -0,0 +1,93 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-2018 Denis Ollier
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#pragma once
+
+#include "common/remmina_plugin.h"
+#include <spice-client.h>
+#ifdef SPICE_GTK_CHECK_VERSION
+# if SPICE_GTK_CHECK_VERSION(0, 31, 0)
+# include <spice-client-gtk.h>
+# else
+# include <spice-widget.h>
+# include <usb-device-widget.h>
+# endif
+#else
+# include <spice-widget.h>
+# include <usb-device-widget.h>
+#endif
+
+#define GET_PLUGIN_DATA(gp) (RemminaPluginSpiceData *)g_object_get_data(G_OBJECT(gp), "plugin-data")
+
+extern RemminaPluginService *remmina_plugin_service;
+
+#define REMMINA_PLUGIN_INFO(fmt, ...) \
+ remmina_plugin_service->_remmina_info(__func__, fmt, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_MESSAGE(fmt, ...) \
+ remmina_plugin_service->_remmina_message(__func, fmt, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_DEBUG(fmt, ...) \
+ remmina_plugin_service->_remmina_debug(__func__, fmt, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_WARNING(fmt, ...) \
+ remmina_plugin_service->_remmina_warning(__func__, fmt, ##__VA_ARGS__)
+
+/* This will intentionally crash Remmina */
+#define REMMINA_PLUGIN_ERROR(fmt, ...) \
+ remmina_plugin_service->_remmina_error(__func__, fmt, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_CRITICAL(fmt, ...) \
+ remmina_plugin_service->_remmina_critical(__func__, fmt, ##__VA_ARGS__)
+#define REMMINA_PLUGIN_AUDIT(fmt, ...) \
+ remmina_plugin_service->_remmina_audit(__func__, fmt, ##__VA_ARGS__)
+
+typedef struct _RemminaPluginSpiceData {
+ SpiceAudio * audio;
+ SpiceDisplay * display;
+ SpiceDisplayChannel * display_channel;
+ SpiceGtkSession * gtk_session;
+ SpiceMainChannel * main_channel;
+ SpiceSession * session;
+ gchar * unixPath;
+ gboolean isUnix;
+
+#ifdef SPICE_GTK_CHECK_VERSION
+# if SPICE_GTK_CHECK_VERSION(0, 31, 0)
+ /* key: SpiceFileTransferTask, value: RemminaPluginSpiceXferWidgets */
+ GHashTable * file_transfers;
+ GtkWidget * file_transfer_dialog;
+# endif /* SPICE_GTK_CHECK_VERSION(0, 31, 0) */
+#endif /* SPICE_GTK_CHECK_VERSION */
+} RemminaPluginSpiceData;
diff --git a/plugins/spice/spice_plugin_file_transfer.c b/plugins/spice/spice_plugin_file_transfer.c
new file mode 100644
index 0000000..ada7153
--- /dev/null
+++ b/plugins/spice/spice_plugin_file_transfer.c
@@ -0,0 +1,244 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-2018 Denis Ollier
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#include "spice_plugin.h"
+
+#ifdef SPICE_GTK_CHECK_VERSION
+# if SPICE_GTK_CHECK_VERSION(0, 31, 0)
+
+static void remmina_plugin_spice_file_transfer_cancel_cb(GtkButton *, SpiceFileTransferTask *);
+static void remmina_plugin_spice_file_transfer_dialog_response_cb(GtkDialog *, gint, RemminaProtocolWidget *);
+static void remmina_plugin_spice_file_transfer_finished_cb(SpiceFileTransferTask *, GError *, RemminaProtocolWidget *);
+static void remmina_plugin_spice_file_transfer_progress_cb(GObject *, GParamSpec *, RemminaProtocolWidget *);
+
+typedef struct _RemminaPluginSpiceXferWidgets {
+ GtkWidget *vbox;
+ GtkWidget *hbox;
+ GtkWidget *progress;
+ GtkWidget *label;
+ GtkWidget *cancel;
+} RemminaPluginSpiceXferWidgets;
+
+static RemminaPluginSpiceXferWidgets * remmina_plugin_spice_xfer_widgets_new(SpiceFileTransferTask *);
+static void remmina_plugin_spice_xfer_widgets_free(RemminaPluginSpiceXferWidgets *widgets);
+
+void remmina_plugin_spice_file_transfer_new_cb(SpiceMainChannel *main_channel, SpiceFileTransferTask *task, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ GtkWidget *dialog_content;
+ RemminaPluginSpiceXferWidgets *widgets;
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+
+ g_signal_connect(task,
+ "finished",
+ G_CALLBACK(remmina_plugin_spice_file_transfer_finished_cb),
+ gp);
+
+ if (!gpdata->file_transfers) {
+ gpdata->file_transfers = g_hash_table_new_full(g_direct_hash,
+ g_direct_equal,
+ g_object_unref,
+ (GDestroyNotify)remmina_plugin_spice_xfer_widgets_free);
+ }
+
+ if (!gpdata->file_transfer_dialog) {
+ /*
+ * FIXME: Use the RemminaConnectionWindow as transient parent widget
+ * (and add the GTK_DIALOG_DESTROY_WITH_PARENT flag) if it becomes
+ * accessible from the Remmina plugin API.
+ */
+ gpdata->file_transfer_dialog = gtk_dialog_new_with_buttons(_("File Transfers"),
+ NULL, 0,
+ _("_Cancel"),
+ GTK_RESPONSE_CANCEL,
+ NULL);
+ dialog_content = gtk_dialog_get_content_area(GTK_DIALOG(gpdata->file_transfer_dialog));
+ gtk_widget_set_size_request(dialog_content, 400, -1);
+ gtk_window_set_resizable(GTK_WINDOW(gpdata->file_transfer_dialog), FALSE);
+ g_signal_connect(gpdata->file_transfer_dialog,
+ "response",
+ G_CALLBACK(remmina_plugin_spice_file_transfer_dialog_response_cb),
+ gp);
+ }
+
+ widgets = remmina_plugin_spice_xfer_widgets_new(task);
+ g_hash_table_insert(gpdata->file_transfers, g_object_ref(task), widgets);
+
+ gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(gpdata->file_transfer_dialog))),
+ widgets->vbox,
+ TRUE, TRUE, 6);
+
+ g_signal_connect(task,
+ "notify::progress",
+ G_CALLBACK(remmina_plugin_spice_file_transfer_progress_cb),
+ gp);
+
+ gtk_widget_show(gpdata->file_transfer_dialog);
+}
+
+static RemminaPluginSpiceXferWidgets * remmina_plugin_spice_xfer_widgets_new(SpiceFileTransferTask *task)
+{
+ TRACE_CALL(__func__);
+
+ gchar *filename;
+ RemminaPluginSpiceXferWidgets *widgets = g_new0(RemminaPluginSpiceXferWidgets, 1);
+
+ widgets->vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ widgets->hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
+
+ filename = spice_file_transfer_task_get_filename(task);
+ widgets->label = gtk_label_new(filename);
+ gtk_widget_set_halign(widgets->label, GTK_ALIGN_START);
+ gtk_widget_set_valign(widgets->label, GTK_ALIGN_BASELINE);
+
+ widgets->progress = gtk_progress_bar_new();
+ gtk_widget_set_hexpand(widgets->progress, TRUE);
+ gtk_widget_set_valign(widgets->progress, GTK_ALIGN_CENTER);
+
+ widgets->cancel = gtk_button_new_from_icon_name("gtk-cancel", GTK_ICON_SIZE_SMALL_TOOLBAR);
+ g_signal_connect(widgets->cancel,
+ "clicked",
+ G_CALLBACK(remmina_plugin_spice_file_transfer_cancel_cb),
+ task);
+ gtk_widget_set_hexpand(widgets->cancel, FALSE);
+ gtk_widget_set_valign(widgets->cancel, GTK_ALIGN_CENTER);
+
+ gtk_box_pack_start(GTK_BOX(widgets->hbox), widgets->progress,
+ TRUE, TRUE, 0);
+ gtk_box_pack_start(GTK_BOX(widgets->hbox), widgets->cancel,
+ FALSE, TRUE, 0);
+
+ gtk_box_pack_start(GTK_BOX(widgets->vbox), widgets->label,
+ TRUE, TRUE, 0);
+ gtk_box_pack_start(GTK_BOX(widgets->vbox), widgets->hbox,
+ TRUE, TRUE, 0);
+
+ gtk_widget_show_all(widgets->vbox);
+
+ g_free(filename);
+
+ return widgets;
+}
+
+static void remmina_plugin_spice_xfer_widgets_free(RemminaPluginSpiceXferWidgets *widgets)
+{
+ TRACE_CALL(__func__);
+
+ /* Child widgets will be destroyed automatically */
+ gtk_widget_destroy(widgets->vbox);
+ g_free(widgets);
+}
+
+static void remmina_plugin_spice_file_transfer_cancel_cb(GtkButton *button, SpiceFileTransferTask *task)
+{
+ TRACE_CALL(__func__);
+
+ spice_file_transfer_task_cancel(task);
+}
+
+
+static void remmina_plugin_spice_file_transfer_dialog_response_cb(GtkDialog *dialog, gint response, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ GHashTableIter iter;
+ gpointer key, value;
+ SpiceFileTransferTask *task;
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+
+ if (response == GTK_RESPONSE_CANCEL) {
+ g_hash_table_iter_init(&iter, gpdata->file_transfers);
+ while (g_hash_table_iter_next(&iter, &key, &value)) {
+ task = key;
+ spice_file_transfer_task_cancel(task);
+ }
+ }
+}
+
+static void remmina_plugin_spice_file_transfer_progress_cb(GObject *task, GParamSpec *param_spec, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ RemminaPluginSpiceXferWidgets *widgets;
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+
+ widgets = g_hash_table_lookup(gpdata->file_transfers, task);
+ if (widgets) {
+ gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(widgets->progress),
+ spice_file_transfer_task_get_progress(SPICE_FILE_TRANSFER_TASK(task)));
+ }
+}
+
+static void remmina_plugin_spice_file_transfer_finished_cb(SpiceFileTransferTask *task, GError *error, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ gchar *filename, *notification_message;
+ GNotification *notification;
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+
+ /*
+ * Send a desktop notification to inform about the outcome of
+ * the file transfer.
+ */
+ filename = spice_file_transfer_task_get_filename(task);
+
+ if (error) {
+ notification = g_notification_new(_("Transfer error"));
+ notification_message = g_strdup_printf(_("%s: %s"),
+ filename, error->message);
+ }else {
+ notification = g_notification_new(_("Transfer completed"));
+ notification_message = g_strdup_printf(_("The %s file has been transferred"),
+ filename);
+ }
+
+ g_notification_set_body(notification, notification_message);
+ g_application_send_notification(g_application_get_default(),
+ "remmina-plugin-spice-file-transfer-finished",
+ notification);
+
+ g_hash_table_remove(gpdata->file_transfers, task);
+
+ if (!g_hash_table_size(gpdata->file_transfers)) {
+ gtk_widget_hide(gpdata->file_transfer_dialog);
+ }
+
+ g_free(filename);
+ g_free(notification_message);
+ g_object_unref(notification);
+}
+# endif /* SPICE_GTK_CHECK_VERSION(0, 31, 0) */
+#endif /* SPICE_GTK_CHECK_VERSION */
diff --git a/plugins/spice/spice_plugin_usb.c b/plugins/spice/spice_plugin_usb.c
new file mode 100644
index 0000000..b03205b
--- /dev/null
+++ b/plugins/spice/spice_plugin_usb.c
@@ -0,0 +1,100 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-2018 Denis Ollier
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#include "spice_plugin.h"
+
+static void remmina_plugin_spice_usb_connect_failed_cb(GObject *, SpiceUsbDevice *, GError *, RemminaProtocolWidget *);
+
+void remmina_plugin_spice_select_usb_devices(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ GtkWidget *dialog, *usb_device_widget;
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+
+ /*
+ * FIXME: Use the RemminaConnectionWindow as transient parent widget
+ * (and add the GTK_DIALOG_DESTROY_WITH_PARENT flag) if it becomes
+ * accessible from the Remmina plugin API.
+ */
+ dialog = gtk_dialog_new_with_buttons(_("Select USB devices for redirection"),
+ NULL,
+ GTK_DIALOG_MODAL,
+ _("_Close"),
+ GTK_RESPONSE_ACCEPT,
+ NULL);
+ gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT);
+
+ usb_device_widget = spice_usb_device_widget_new(gpdata->session, NULL);
+ g_signal_connect(usb_device_widget,
+ "connect-failed",
+ G_CALLBACK(remmina_plugin_spice_usb_connect_failed_cb),
+ gp);
+
+ gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(dialog))),
+ usb_device_widget,
+ TRUE,
+ TRUE,
+ 0);
+ gtk_widget_show_all(dialog);
+ gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+}
+
+static void remmina_plugin_spice_usb_connect_failed_cb(GObject *object, SpiceUsbDevice *usb_device, GError *error, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ GtkWidget *dialog;
+
+ if (error->domain == G_IO_ERROR && error->code == G_IO_ERROR_CANCELLED) {
+ return;
+ }
+
+ /*
+ * FIXME: Use the RemminaConnectionWindow as transient parent widget
+ * (and add the GTK_DIALOG_DESTROY_WITH_PARENT flag) if it becomes
+ * accessible from the Remmina plugin API.
+ */
+ dialog = gtk_message_dialog_new(NULL,
+ GTK_DIALOG_MODAL,
+ GTK_MESSAGE_ERROR,
+ GTK_BUTTONS_CLOSE,
+ _("USB redirection error"));
+ gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog),
+ "%s",
+ error->message);
+ gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+}