diff options
Diffstat (limited to 'plugins/spice')
-rw-r--r-- | plugins/spice/CMakeLists.txt | 55 | ||||
-rw-r--r-- | plugins/spice/scalable/emblems/org.remmina.Remmina-spice-ssh-symbolic.svg | 97 | ||||
-rw-r--r-- | plugins/spice/scalable/emblems/org.remmina.Remmina-spice-symbolic.svg | 100 | ||||
-rw-r--r-- | plugins/spice/spice_plugin.c | 836 | ||||
-rw-r--r-- | plugins/spice/spice_plugin.h | 93 | ||||
-rw-r--r-- | plugins/spice/spice_plugin_file_transfer.c | 244 | ||||
-rw-r--r-- | plugins/spice/spice_plugin_usb.c | 100 |
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); +} |