diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 17:06:32 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 17:06:32 +0000 |
commit | 2dad5357405ad33cfa792f04b3ab62a5d188841e (patch) | |
tree | b8f8893942060fe3cfb04ac374cda96fdfc8f453 /plugins | |
parent | Initial commit. (diff) | |
download | remmina-2dad5357405ad33cfa792f04b3ab62a5d188841e.tar.xz remmina-2dad5357405ad33cfa792f04b3ab62a5d188841e.zip |
Adding upstream version 1.4.34+dfsg.upstream/1.4.34+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'plugins')
102 files changed, 27911 insertions, 0 deletions
diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt new file mode 100644 index 0000000..acb93e0 --- /dev/null +++ b/plugins/CMakeLists.txt @@ -0,0 +1,195 @@ +# Remmina - The GTK+ Remote Desktop Client +# +# Copyright (C) 2011 Marc-Andre Moreau +# Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo +# Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo +# +# 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. + +# Needed FreeRDP version to build freerdp plugin + + +include_directories(${CMAKE_SOURCE_DIR}/plugins) + +set(APPICONSCALE_EMBLEMS_DIR "${REMMINA_DATADIR}/icons/hicolor/scalable/emblems") + +set(REMMINA_COMMON_INCLUDE_DIRS ${GTK3_INCLUDE_DIRS}) +set(REMMINA_COMMON_LIBRARIES ${GTK_LIBRARIES} ${Intl_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT}) + +find_suggested_package(LIBSSH) +if(LIBSSH_FOUND) + add_definitions(-DHAVE_LIBSSH) + include_directories(${SSH_INCLUDE_DIRS}) + target_link_libraries(remmina ${SSH_LIBRARIES}) +endif() + +option(WITH_FREERDP3 "Compile the RDP plugin using symbols from FreeRDP3" OFF) +if (WITH_FREERDP3) + set(FREERDP_REQUIRED_VERSION 3) + set(FREERDP_REQUIRED_MAJOR 3) + set(FREERDP_REQUIRED_MINOR 0) + set(FREERDP_REQUIRED_REVISION 0) +else() + set(FREERDP_REQUIRED_VERSION 2) + set(FREERDP_REQUIRED_MAJOR 2) + set(FREERDP_REQUIRED_MINOR 0) + set(FREERDP_REQUIRED_REVISION 0) +endif() + +find_package(WinPR ${FREERDP_REQUIRED_VERSION} ) +find_package(FreeRDP ${FREERDP_REQUIRED_VERSION} ) +find_package(FreeRDP-Client ${FREERDP_REQUIRED_VERSION} ) + + +if (WinPR_FOUND AND FreeRDP_FOUND AND FreeRDP-Client_FOUND) + message("Enabling RDP plugin") + add_subdirectory(rdp) +else() + message(STATUS "Disabling RDP plugin.") +endif() + +# X2Go +option(WITH_X2GO "Build X2Go plugin" OFF) +if(WITH_X2GO) + message(STATUS "Enabling X2Go plugin.") + message(AUTHOR_WARNING "pyhoca-cli is needed at runtime, be sure to install it") + add_subdirectory(x2go) +else() + message(STATUS "Disabling X2Go plugin.") +endif() + +find_suggested_package(LIBVNCSERVER) +if(LIBVNCSERVER_FOUND) + add_subdirectory(vnc) +endif() + +option(WITH_GVNC "Build GVNC plugin" OFF) +if(WITH_GVNC) + message(STATUS "Enabling GVNC plugin.") + add_definitions(-DWITH_GVNC) + find_suggested_package(GTK-VNC) + if(GTK-VNC_FOUND) + add_subdirectory(gvnc) + endif() +else() + message(STATUS "Disabling GVNC plugin.") +endif() + +find_suggested_package(Spice) +if(SPICE_FOUND) + add_subdirectory(spice) +endif() + +option(WITH_WWW "Build WWW plugin" ON) +if(WITH_WWW) + message(STATUS "Enabling WWW plugin.") + add_definitions(-DWITH_WWW) + find_suggested_package(WEBKIT2GTK) + if(WEBKIT2GTK_FOUND) + add_subdirectory(www) + endif() +else() + message(STATUS "Disabling WWW plugin.") +endif() + +if(WITH_EXAMPLES) + message(STATUS "Enabling examples and test plugins.") + add_subdirectory(tool_hello_world) +endif() + +add_subdirectory(exec) + +option(WITH_KF5WALLET "Building KF5WALLET plugin" OFF) +if(WITH_KF5WALLET) + CheckHasModule(KF5Wallet) + if(HAS_MODULE_KF5Wallet) + message(STATUS "Enabling KDE Wallet plugin.") + add_definitions(-DWITH_KF5WALLET) + add_subdirectory(kwallet) + else() + message(FATAL_ERROR "libKF5wallet not found but requested") + endif() +endif() + +option(WITH_ST "Build Simple Terminal plugin" OFF) +if(WITH_ST) + message(STATUS "Enabling ST plugin.") + add_definitions(-DWITH_ST) + get_filename_component(_st_fullpath "st" REALPATH) + if(EXISTS "${_st_fullpath}" AND EXISTS "${_st_fullpath}/CMakeLists.txt") + add_subdirectory(st) + else() + message(WARNING "Can't find ${_st_fullpath}/CMakeLists.txt") + endif() +endif() + +option(WITH_XDMCP "Build XDMCP plugin" OFF) +if(WITH_XDMCP) + add_definitions(-DWITH_XDMCP) + message(STATUS "Enabling XDMCP plugin.") + get_filename_component(_xdmcp_fullpath "xdmcp" REALPATH) + if(EXISTS "${_xdmcp_fullpath}" AND EXISTS "${_xdmcp_fullpath}/CMakeLists.txt") + add_subdirectory(xdmcp) + else() + message(WARNING "Can't find ${_xdmcp_fullpath}/CMakeLists.txt") + endif() +endif() + +option(WITH_NX "Build NX plugin" OFF) +if(WITH_NX) + add_definitions(-DWITH_NX) + # After the plugin split only the NX plugin needs XKBfile.h + find_required_package(XKBFILE) + if(LIBSSH_FOUND AND XKBFILE_FOUND) + message(STATUS "Enabling NX plugin.") + get_filename_component(_nx_fullpath "nx" REALPATH) + if(EXISTS "${_nx_fullpath}" AND EXISTS "${_nx_fullpath}/CMakeLists.txt") + add_subdirectory(nx) + else() + message(WARNING "Can't find ${_nx_fullpath}/CMakeLists.txt") + endif() + endif() +endif() + +option(WITH_PYTHONLIBS "Build Python Wrapper Plugin" ON) +if(WITH_PYTHONLIBS) + message(STATUS "Enabling Python Wrapper Plugin.") + add_definitions(-DWITH_PYTHONLIBS) + find_suggested_package(PythonLibs) + if(PythonLibs_FOUND) + add_subdirectory(python_wrapper) + endif() +else() + message(STATUS "Disabling Python Wrapper plugin.") +endif() + +#find_suggested_package(TELEPATHY) +#if(TELEPATHY_FOUND) +#add_subdirectory(telepathy) +#endif() diff --git a/plugins/README.md b/plugins/README.md new file mode 100644 index 0000000..162a407 --- /dev/null +++ b/plugins/README.md @@ -0,0 +1,29 @@ +## Removed plugins + +We have removed the folders xdmcp, nx and st due to the fact that Gtk have deprecated the functions +using the XEmbed protocol. + +These folders have been split in a new repository as shown in the following example + +``` +# Split of the folder plugins/st in a new branch +git subtree split -P plugins/st -b plugins/st +# We add the remote of the new repository +git remote add plugins/st git@gitlab.com:Remmina/remmina-plugins.git +# We push to the new repository +git push plugins/st plugins/st +# We change to the location of the local copy of git@gitlab.com:Remmina/remmina-plugins.git +cd ../remmina-plugins/ +# The branch plugins/st does not have the folder plugins, the st source is in the root +# The following subtree command recreate the folder structure (../Remmina is the main repo) +git subtree add -P plugins/st ../Remmina plugins/st +git push +``` + +The next step is to `git rm -r plugins/st` and if we need again `st`, it's enough to add it as a submodule: + +``` +git submodule add git@gitlab.com:Remmina/remmina-plugins.git plugins +``` + +The submodule part has to be tested as it'll cause issues with the existing files diff --git a/plugins/common/remmina_plugin.h b/plugins/common/remmina_plugin.h new file mode 100644 index 0000000..f76df5e --- /dev/null +++ b/plugins/common/remmina_plugin.h @@ -0,0 +1,99 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2010 Vic Lee + * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * 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 "config.h" +#include <gtk/gtk.h> +#include <glib/gi18n-lib.h> +#include <glib/gstdio.h> +#include <remmina/plugin.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <signal.h> +#include <pthread.h> +#ifdef HAVE_NETDB_H +#include <netdb.h> +#endif +#ifdef HAVE_ARPA_INET_H +#include <arpa/inet.h> +#endif +#ifdef HAVE_NETINET_IN_H +#include <netinet/in.h> +#endif +#ifdef HAVE_SYS_SOCKET_H +#include <sys/socket.h> +#endif +#ifdef HAVE_FCNTL_H +#include <fcntl.h> +#endif +#ifdef HAVE_ERRNO_H +#include <errno.h> +#endif +#include "remmina/remmina_trace_calls.h" + +typedef void (*PThreadCleanupFunc)(void *); + + +#define IDLE_ADD gdk_threads_add_idle +#define TIMEOUT_ADD gdk_threads_add_timeout +#define CANCEL_ASYNC pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL); pthread_testcancel(); +#define CANCEL_DEFER pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL); + +#define THREADS_ENTER _Pragma("GCC error \"THREADS_ENTER has been deprecated in Remmina 1.2\"") +#define THREADS_LEAVE _Pragma("GCC error \"THREADS_LEAVE has been deprecated in Remmina 1.2\"") + +#define MAX_X_DISPLAY_NUMBER 99 +#define X_UNIX_SOCKET "/tmp/.X11-unix/X%d" + +#define INCLUDE_GET_AVAILABLE_XDISPLAY static gint \ + remmina_get_available_xdisplay(void) \ + { \ + gint i; \ + gint display = 0; \ + gchar fn[200]; \ + for (i = 1; i < MAX_X_DISPLAY_NUMBER; i++) \ + { \ + g_snprintf(fn, sizeof(fn), X_UNIX_SOCKET, i); \ + if (!g_file_test(fn, G_FILE_TEST_EXISTS)) \ + { \ + display = i; \ + break; \ + } \ + } \ + return display; \ + } diff --git a/plugins/exec/CMakeLists.txt b/plugins/exec/CMakeLists.txt new file mode 100644 index 0000000..4906f3b --- /dev/null +++ b/plugins/exec/CMakeLists.txt @@ -0,0 +1,55 @@ +# remmina-plugin-tool_hello_world - The GTK+ Remote Desktop Client +# +# Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo +# Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo +# +# 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_EXEC_SRCS + exec_plugin_config.h + exec_plugin.c +) + +add_library(remmina-plugin-exec MODULE ${REMMINA_PLUGIN_EXEC_SRCS}) +set_target_properties(remmina-plugin-exec PROPERTIES PREFIX "") +set_target_properties(remmina-plugin-exec PROPERTIES NO_SONAME 1) + +include_directories(${REMMINA_COMMON_INCLUDE_DIRS} ${GTK_INCLUDE_DIRS}) +target_link_libraries(remmina-plugin-exec ${REMMINA_COMMON_LIBRARIES}) + +install(TARGETS remmina-plugin-exec DESTINATION ${REMMINA_PLUGINDIR}) + +install(FILES + scalable/emblems/org.remmina.Remmina-tool-symbolic.svg + DESTINATION ${APPICONSCALE_EMBLEMS_DIR}) + +if(WITH_ICON_CACHE) + gtk_update_icon_cache("${REMMINA_DATADIR}/icons/hicolor") +endif() diff --git a/plugins/exec/exec_plugin.c b/plugins/exec/exec_plugin.c new file mode 100644 index 0000000..f22b535 --- /dev/null +++ b/plugins/exec/exec_plugin.c @@ -0,0 +1,340 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2017-2023 Antenore Gatta, Giovanni Panozzo + * + * Initially based on the plugin "Remmina Plugin EXEC", created and written by + * Fabio Castelli (Muflone) <muflone@vbsimple.net>. + * + * 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 "exec_plugin_config.h" + +#include "common/remmina_plugin.h" + +#include <gdk/gdkkeysyms.h> +#include <gtk/gtk.h> +#include <glib.h> +#include <stdlib.h> +#include <sys/wait.h> +#include <unistd.h> + +#define GET_PLUGIN_DATA(gp) (RemminaPluginExecData*)g_object_get_data(G_OBJECT(gp), "plugin-data") + +typedef struct _RemminaPluginExecData { + GtkWidget *log_view; + GtkTextBuffer *log_buffer; + GtkTextBuffer *err; + GtkWidget *sw; + GPid pid; +} RemminaPluginExecData; + +static RemminaPluginService *remmina_plugin_service = NULL; +#define REMMINA_PLUGIN_DEBUG(fmt, ...) remmina_plugin_service->_remmina_debug(__func__, fmt, ##__VA_ARGS__) + + + static void +cb_child_watch( GPid pid, gint status) +{ + /* Close pid */ + g_spawn_close_pid( pid ); +} + +static void cb_child_setup(gpointer data){ + int pid = getpid(); + setpgid(pid, 0); +} + + static gboolean +cb_out_watch (GIOChannel *channel, GIOCondition cond, RemminaProtocolWidget *gp) +{ + gchar *string; + gsize size; + + RemminaPluginExecData *gpdata = GET_PLUGIN_DATA(gp); + + if( cond == G_IO_HUP ) + { + g_io_channel_unref( channel ); + return FALSE; + } + + g_io_channel_read_line( channel, &string, &size, NULL, NULL ); + gtk_text_buffer_insert_at_cursor( gpdata->log_buffer, string, -1 ); + g_free( string ); + + return TRUE; +} + + static gboolean +cb_err_watch (GIOChannel *channel, GIOCondition cond, RemminaProtocolWidget *gp) +{ + gchar *string; + gsize size; + + RemminaPluginExecData *gpdata = GET_PLUGIN_DATA(gp); + + if( cond == G_IO_HUP ) + { + g_io_channel_unref( channel ); + return FALSE; + } + + g_io_channel_read_line( channel, &string, &size, NULL, NULL ); + gtk_text_buffer_insert_at_cursor( gpdata->err, string, -1 ); + g_free( string ); + + return TRUE; +} + +static void remmina_plugin_exec_init(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginExecData *gpdata; + + REMMINA_PLUGIN_DEBUG("[%s] Plugin init", PLUGIN_NAME); + + gpdata = g_new0(RemminaPluginExecData, 1); + g_object_set_data_full(G_OBJECT(gp), "plugin-data", gpdata, g_free); + + gpdata->pid = 0; + gpdata->log_view = gtk_text_view_new(); + gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(gpdata->log_view), GTK_WRAP_CHAR); + gtk_text_view_set_editable(GTK_TEXT_VIEW(gpdata->log_view), FALSE); + gtk_text_view_set_left_margin (GTK_TEXT_VIEW (gpdata->log_view), 20); + gtk_text_view_set_right_margin (GTK_TEXT_VIEW (gpdata->log_view), 20); + gpdata->log_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (gpdata->log_view)); + gpdata->sw = gtk_scrolled_window_new (NULL, NULL); + gtk_widget_set_size_request (gpdata->sw, 640, 480); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (gpdata->sw), + GTK_POLICY_AUTOMATIC, + GTK_POLICY_AUTOMATIC); + gtk_container_add(GTK_CONTAINER(gp), gpdata->sw); + gtk_container_add(GTK_CONTAINER(gpdata->sw), gpdata->log_view); + gtk_text_buffer_set_text (gpdata->log_buffer, "Remmina Exec Plugin Logger", -1); + + gtk_widget_show_all(gpdata->sw); +} + +static gboolean remmina_plugin_exec_run(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaFile* remminafile; + const gchar *cmd; + gchar *stdout_buffer; + gchar *stderr_buffer; + char **argv; + GError *error = NULL; + GPid child_pid; + gint child_stdout, child_stderr; + GtkDialog *dialog; + GIOChannel *out_ch, *err_ch; + + REMMINA_PLUGIN_DEBUG("[%s] Plugin run", PLUGIN_NAME); + RemminaPluginExecData *gpdata = GET_PLUGIN_DATA(gp); + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + + cmd = remmina_plugin_service->file_get_string(remminafile, "execcommand"); + if (!cmd) { + gtk_text_buffer_set_text (gpdata->log_buffer, + _("You did not set any command to be executed"), -1); + remmina_plugin_service->protocol_plugin_signal_connection_opened(gp); + return TRUE; + } + + g_shell_parse_argv(cmd, NULL, &argv, &error); + if (error) { + gtk_text_buffer_set_text (gpdata->log_buffer, error->message, -1); + remmina_plugin_service->protocol_plugin_signal_connection_opened(gp); + g_error_free(error); + return TRUE; + } + + if (remmina_plugin_service->file_get_int(remminafile, "runasync", FALSE)) { + REMMINA_PLUGIN_DEBUG("[%s] Run Async", PLUGIN_NAME); + g_spawn_async_with_pipes( NULL, + argv, + NULL, + G_SPAWN_DO_NOT_REAP_CHILD | + G_SPAWN_SEARCH_PATH_FROM_ENVP | + G_SPAWN_SEARCH_PATH, + cb_child_setup, + NULL, + &child_pid, + NULL, + &child_stdout, + &child_stderr, + &error); + if (error != NULL) { + gtk_text_buffer_set_text (gpdata->log_buffer, error->message, -1); + g_error_free(error); + remmina_plugin_service->protocol_plugin_signal_connection_opened(gp); + return TRUE; + } + g_child_watch_add(child_pid, (GChildWatchFunc)cb_child_watch, gp ); + + gpdata->pid = child_pid; + /* Create channels that will be used to read data from pipes. */ + out_ch = g_io_channel_unix_new(child_stdout); + err_ch = g_io_channel_unix_new(child_stderr); + /* Add watches to channels */ + g_io_add_watch(out_ch, G_IO_IN | G_IO_HUP, (GIOFunc)cb_out_watch, gp ); + g_io_add_watch(err_ch, G_IO_IN | G_IO_HUP, (GIOFunc)cb_err_watch, gp ); + + }else { + dialog = GTK_DIALOG(gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, + GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO, + _("Warning: Running a command synchronously may cause Remmina not to respond.\nDo you really want to continue?"))); + gint result = gtk_dialog_run (GTK_DIALOG (dialog)); + + switch (result) + { + case GTK_RESPONSE_YES: + break; + default: + gtk_widget_destroy(GTK_WIDGET(dialog)); + return FALSE; + break; + } + gtk_widget_destroy(GTK_WIDGET(dialog)); + REMMINA_PLUGIN_DEBUG("[%s] Run Sync", PLUGIN_NAME); + g_spawn_sync (NULL, // CWD or NULL + argv, + NULL, // ENVP or NULL + G_SPAWN_SEARCH_PATH | + G_SPAWN_SEARCH_PATH_FROM_ENVP, + NULL, + NULL, + &stdout_buffer, // STDOUT + &stderr_buffer, // STDERR + NULL, // Exit status + &error); + if (!error) { + REMMINA_PLUGIN_DEBUG("[%s] Command executed", PLUGIN_NAME); + gtk_text_buffer_set_text (gpdata->log_buffer, stdout_buffer, -1); + }else { + g_warning("Command %s exited with error: %s\n", cmd, error->message); + gtk_text_buffer_set_text (gpdata->log_buffer, error->message, -1); + g_error_free(error); + } + } + + g_strfreev(argv); + + remmina_plugin_service->protocol_plugin_signal_connection_opened(gp); + return TRUE; +} + +static gboolean remmina_plugin_exec_close(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + REMMINA_PLUGIN_DEBUG("[%s] Plugin close", PLUGIN_NAME); + RemminaPluginExecData *gpdata = GET_PLUGIN_DATA(gp); + //if async process was started, make sure it's dead if option is selected + RemminaFile* remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + if (remmina_plugin_service->file_get_int(remminafile, "kill_proc", FALSE)) { + if (gpdata->pid !=0 ){ + int pgid = getpgid(gpdata->pid); + if (pgid != 0){ + kill(-gpdata->pid, SIGHUP); + } + else{ + kill(gpdata->pid, SIGHUP); + } + + } + } + remmina_plugin_service->protocol_plugin_signal_connection_closed(gp); + return FALSE; +} + +/* 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_exec_basic_settings[] = +{ + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "execcommand", N_("Command"), FALSE, NULL, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "runasync", N_("Asynchronous execution"), FALSE, NULL, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "kill_proc", N_("Kill process on disconnect"), FALSE, NULL, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, FALSE, NULL, NULL, NULL, NULL } +}; + +/* Protocol plugin definition and features */ +static RemminaProtocolPlugin remmina_plugin = { + REMMINA_PLUGIN_TYPE_PROTOCOL, // Type + PLUGIN_NAME, // Name + PLUGIN_DESCRIPTION, // Description + GETTEXT_PACKAGE, // Translation domain + PLUGIN_VERSION, // Version number + PLUGIN_APPICON, // Icon for normal connection + PLUGIN_APPICON, // Icon for SSH connection + remmina_plugin_exec_basic_settings, // Array for basic settings + NULL, // Array for advanced settings + REMMINA_PROTOCOL_SSH_SETTING_NONE, // SSH settings type + NULL, // Array for available features + remmina_plugin_exec_init, // Plugin initialization + remmina_plugin_exec_run, // Plugin open connection + remmina_plugin_exec_close, // Plugin close connection + NULL, // Query for available features + NULL, // Call a feature + NULL, // Send a keystroke + NULL, // No screenshot support available + NULL, // RCW map event + NULL // RCW unmap event +}; + +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"); + + if (!service->register_plugin((RemminaPlugin*)&remmina_plugin)) { + return FALSE; + } + + return TRUE; +} diff --git a/plugins/exec/exec_plugin_config.h b/plugins/exec/exec_plugin_config.h new file mode 100644 index 0000000..d62a4fa --- /dev/null +++ b/plugins/exec/exec_plugin_config.h @@ -0,0 +1,64 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * Initially based on the plugin "Remmina Plugin EXEC", created and written by + * Fabio Castelli (Muflone) <muflone@vbsimple.net>. + * + * 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 + +#define PLUGIN_NAME "EXEC" +#define PLUGIN_DESCRIPTION N_("Execute a command") +#define PLUGIN_VERSION "1.0" +#define PLUGIN_APPICON "org.remmina.Remmina-tool-symbolic" + +#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__) diff --git a/plugins/exec/scalable/emblems/org.remmina.Remmina-tool-symbolic.svg b/plugins/exec/scalable/emblems/org.remmina.Remmina-tool-symbolic.svg new file mode 100644 index 0000000..f383629 --- /dev/null +++ b/plugins/exec/scalable/emblems/org.remmina.Remmina-tool-symbolic.svg @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + width="24" + height="24" + viewBox="0 0 24 24" + version="1.1" + id="svg8" + inkscape:version="1.2 (dc2aedaf03, 2022-05-15)" + sodipodi:docname="org.remmina.Remmina-tool-symbolic.svg" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> + <defs + id="defs2" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:zoom="11.871974" + inkscape:cx="34.28242" + inkscape:cy="27.670209" + 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="1920" + inkscape:window-height="995" + inkscape:window-x="1080" + inkscape:window-y="427" + 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:deskcolor="#d1d1d1"> + <inkscape:grid + type="xygrid" + id="grid10" + dotted="false" + originx="-171.36869" + originy="-105.18186" /> + </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" /> + <cc:license + rdf:resource="" /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-171.36866,-165.16877)"> + <path + d="m 187.77406,165.16877 v 0.20572 l -7.85028,10.64488 c 0.22721,0.0656 0.45083,0.14294 0.66991,0.23189 -0.77754,-0.31322 -1.62348,-0.4954 -2.51368,-0.4954 -1.77942,0 -3.48494,0.70612 -4.74227,1.95807 -0.35468,0.35318 -0.66823,0.74465 -0.93515,1.16728 l 3.12261,3.10514 -2.0827,2.82424 c 0.4982,0.95633 1.28315,1.73475 2.24581,2.22754 l 2.83728,-2.06819 3.12548,3.10799 c 0.42429,-0.26637 0.817,-0.57912 1.17098,-0.93253 1.25898,-1.25029 1.96741,-2.94651 1.96908,-4.71574 0,-0.8619 -0.17842,-1.6798 -0.47762,-2.43611 0.0814,0.19645 0.15331,0.39646 0.21549,0.59958 l 10.70216,-8.009 z m -12.05944,0.3332 -4.21776,4.19702 5.47604,5.44278 4.21801,-4.19416 z m 13.91206,13.83421 -4.21801,4.19703 5.47628,5.44563 4.21801,-4.19726 z m -17.14253,6.67356 -1.11549,1.63082 1.49123,1.48312 1.52064,-1.10949 c -0.75315,-0.49516 -1.39819,-1.13659 -1.89638,-1.88553 z" + id="path32" + inkscape:connector-curvature="0" + style="isolation:isolate;fill:#171717;stroke-width:0.2385" /> + </g> +</svg> diff --git a/plugins/gvnc/CMakeLists.txt b/plugins/gvnc/CMakeLists.txt new file mode 100644 index 0000000..ca5c1cd --- /dev/null +++ b/plugins/gvnc/CMakeLists.txt @@ -0,0 +1,54 @@ +# remmina-plugin-gvnc - The GTK+ Remote Desktop Client +# +# Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo +# +# 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_GVNC_SRCS + gvnc_plugin_config.h + gvnc_plugin.h + gvnc_plugin.c + ) + +add_library(remmina-plugin-gvnc MODULE ${REMMINA_PLUGIN_GVNC_SRCS}) +set_target_properties(remmina-plugin-gvnc PROPERTIES PREFIX "") +set_target_properties(remmina-plugin-gvnc PROPERTIES NO_SONAME 1) + +include_directories(${REMMINA_COMMON_INCLUDE_DIRS} ${GTK-VNC_INCLUDE_DIRS}) +target_link_libraries(remmina-plugin-gvnc ${REMMINA_COMMON_LIBRARIES} ${GTK-VNC_LIBRARIES}) + +install(TARGETS remmina-plugin-gvnc DESTINATION ${REMMINA_PLUGINDIR}) + +install(FILES + scalable/emblems/org.remmina.Remmina-gvnc-symbolic.svg + DESTINATION ${APPICONSCALE_EMBLEMS_DIR}) + +if(WITH_ICON_CACHE) + gtk_update_icon_cache("${REMMINA_DATADIR}/icons/hicolor") +endif() diff --git a/plugins/gvnc/gvnc_plugin.c b/plugins/gvnc/gvnc_plugin.c new file mode 100644 index 0000000..14cead9 --- /dev/null +++ b/plugins/gvnc/gvnc_plugin.c @@ -0,0 +1,941 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * 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 "gvnc_plugin_config.h" +#include "gvnc_plugin.h" + +#include <vncdisplay.h> +#include <vncutil.h> +#include <vncaudiopulse.h> + +#define GVNC_DEFAULT_PORT 5900 + +#ifndef VNC_CHECK_VERSION +# define VNC_CHECK_VERSION(a, b, c) 0 +#endif +#if VNC_CHECK_VERSION(1, 2, 0) +# define HAVE_VNC_REMOTE_RESIZE +# define HAVE_VNC_POWER_CONTROL +#endif + +enum { + GVNC_PLUGIN_FEATURE_PREF_VIEWONLY = 1, + GVNC_PLUGIN_FEATURE_DYNRESUPDATE, + GVNC_PLUGIN_FEATURE_PREF_DISABLECLIPBOARD, + GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTDEL, + GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTBACKSPACE, + GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF1, + GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF2, + GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF3, + GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF4, + GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF5, + GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF6, + GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF7, + GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF8, + GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF9, + GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF10, + GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF11, + GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF12, +#ifdef HAVE_VNC_POWER_CONTROL + GVNC_PLUGIN_FEATURE_TOOL_REBOOT, + GVNC_PLUGIN_FEATURE_TOOL_RESET, + GVNC_PLUGIN_FEATURE_TOOL_SHUTDOWN, +#endif + GVNC_PLUGIN_FEATURE_SCALE +}; + + +static RemminaPluginService *remmina_plugin_service = NULL; + +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; +} + +/* Send a keystroke to the plugin window */ +static void gvnc_plugin_keystroke(RemminaProtocolWidget *gp, const guint keystrokes[], const gint keylen) +{ + TRACE_CALL(__func__); + GVncPluginData *gpdata = GET_PLUGIN_DATA(gp); + + if (gpdata->vnc) { + vnc_display_send_keys(VNC_DISPLAY(gpdata->vnc), + keystrokes, + keylen); + } +} + +#ifdef HAVE_VNC_POWER_CONTROL +static void gvnc_plugin_power_ctrl(RemminaProtocolWidget *gp, VncConnectionPowerAction action) +{ + GVncPluginData *gpdata = GET_PLUGIN_DATA(gp); + VncConnection *conn = vnc_display_get_connection(VNC_DISPLAY(gpdata->vnc)); + + if (!vnc_connection_get_power_control(conn)) { + REMMINA_PLUGIN_DEBUG("Power control not available on this server"); + return; + } + vnc_connection_power_control(conn, action); +} +#endif + + +static void gvnc_plugin_mouse_grab(GtkWidget *vncdisplay, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + GVncPluginData *gpdata = GET_PLUGIN_DATA(gp); + VncGrabSequence *seq = vnc_display_get_grab_keys(VNC_DISPLAY(gpdata->vnc)); + gchar *seqstr = vnc_grab_sequence_as_string(seq); + + REMMINA_PLUGIN_DEBUG("Pointer grabbed: %s", seqstr); +} + +static void gvnc_plugin_mouse_ungrab(GtkWidget *vncdisplay, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + REMMINA_PLUGIN_DEBUG("Pointer ungrabbed"); +} + +static void gvnc_plugin_on_vnc_error(GtkWidget *vncdisplay G_GNUC_UNUSED, const gchar *msg, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + GVncPluginData *gpdata = GET_PLUGIN_DATA(gp); + + REMMINA_PLUGIN_DEBUG("Error: %s\n", msg); + /* "vnc-error" is always followed by "vnc-disconnected", + * so save the error for that signal */ + g_free(gpdata->error_msg); + gpdata->error_msg = g_strdup(msg); +} + +static gboolean gvnc_plugin_get_screenshot(RemminaProtocolWidget *gp, RemminaPluginScreenshotData *rpsd) __attribute__ ((unused)); +static gboolean gvnc_plugin_get_screenshot(RemminaProtocolWidget *gp, RemminaPluginScreenshotData *rpsd) +{ + GVncPluginData *gpdata = GET_PLUGIN_DATA(gp); + //gsize szmem; + const VncPixelFormat *currentFormat; + //GError *err = NULL; + + if (!gpdata) + return FALSE; + + /* Get current pixel format for server */ + currentFormat = vnc_connection_get_pixel_format(gpdata->conn); + + + GdkPixbuf *pix = vnc_display_get_pixbuf(VNC_DISPLAY(gpdata->vnc)); + + rpsd->width = gdk_pixbuf_get_width(pix); + rpsd->height = gdk_pixbuf_get_height(pix); + rpsd->bitsPerPixel = currentFormat->bits_per_pixel; + rpsd->bytesPerPixel = rpsd->bitsPerPixel / 8; + //szmem = gdk_pixbuf_get_byte_length(pix); + + //szmem = rpsd->width * rpsd->height * rpsd->bytesPerPixel; + + //REMMINA_PLUGIN_DEBUG("allocating %zu bytes for a full screenshot", szmem); + //REMMINA_PLUGIN_DEBUG("Calculated screenshot size: %zu", szcalc); + //rpsd->buffer = malloc(szmem); + + //memcpy(rpsd->buffer, pix, szmem); + //gdk_pixbuf_save_to_buffer(pix, &rpsd->buffer, &szmem, "jpeg", &err, "quality", "100", NULL); + + + /* Returning TRUE instruct also the caller to deallocate rpsd->buffer */ + return TRUE; +} + +void gvnc_plugin_paste_text(RemminaProtocolWidget *gp, const gchar *text) +{ + TRACE_CALL(__func__); + GVncPluginData *gpdata = GET_PLUGIN_DATA(gp); + gchar *out; + gsize a, b; + GError *error = NULL; + + if (!gpdata) return; + + out = g_convert_with_fallback(text, -1, "iso8859-1//TRANSLIT", "utf-8", NULL, &a, &b, &error); + if (out) { + REMMINA_PLUGIN_DEBUG("Pasting text"); + vnc_display_client_cut_text(VNC_DISPLAY(gpdata->vnc), out); + g_free(out); + } else { + REMMINA_PLUGIN_DEBUG("Error pasting text: %s", error->message); + g_error_free(error); + } +} + +static void gvnc_plugin_clipboard_cb(GtkClipboard *cb, GdkEvent *event, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + //GVncPluginData *gpdata = GET_PLUGIN_DATA(gp); + gchar *text; + + REMMINA_PLUGIN_DEBUG("owner-change event received"); + + if (cb && gtk_clipboard_get_owner(cb) == (GObject *)gp) + return; + + text = gtk_clipboard_wait_for_text(cb); + if (!text) + return; + + gvnc_plugin_paste_text(gp, text); + g_free(text); +} + + +/* text was actually requested */ +static void gvnc_plugin_clipboard_copy(GtkClipboard *clipboard G_GNUC_UNUSED, GtkSelectionData *data, guint info G_GNUC_UNUSED, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + GVncPluginData *gpdata = GET_PLUGIN_DATA(gp); + + if (!gpdata) return; + gtk_selection_data_set_text(data, gpdata->clipstr, -1); + REMMINA_PLUGIN_DEBUG("Text copied"); +} + +static void gvnc_plugin_cut_text(VncDisplay *vnc G_GNUC_UNUSED, const gchar *text, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + GVncPluginData *gpdata = GET_PLUGIN_DATA(gp); + + REMMINA_PLUGIN_DEBUG("Got clipboard request for “%s”", text); + + GtkClipboard *cb; + gsize a, b; + GtkTargetEntry targets[] = { + { g_strdup("UTF8_STRING"), 0, 0 }, + { g_strdup("COMPOUND_TEXT"), 0, 0 }, + { g_strdup("TEXT"), 0, 0 }, + { g_strdup("STRING"), 0, 0 }, + }; + + if (!text) + return; + g_free(gpdata->clipstr); + gpdata->clipstr = g_convert(text, -1, "utf-8", "iso8859-1", &a, &b, NULL); + + if (gpdata->clipstr) { + cb = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); + + REMMINA_PLUGIN_DEBUG("setting clipboard with owner to owner %p", gp); + gtk_clipboard_set_with_owner(cb, + targets, + G_N_ELEMENTS(targets), + (GtkClipboardGetFunc)gvnc_plugin_clipboard_copy, + NULL, + G_OBJECT(gp)); + } + + //g_signal_emit_by_name(session, "session-cut-text", text); +} + + +static void gvnc_plugin_desktop_resize(GtkWidget *vncdisplay G_GNUC_UNUSED, int width, int height, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + GVncPluginData *gpdata = GET_PLUGIN_DATA(gp); + + REMMINA_PLUGIN_DEBUG("Remote desktop size changed to %dx%d\n", width, height); + REMMINA_PLUGIN_DEBUG("Desktop size is set to %dx%d\n", gpdata->width, gpdata->height); + remmina_plugin_service->protocol_plugin_set_width(gp, width); + remmina_plugin_service->protocol_plugin_set_height(gp, height); + + remmina_plugin_service->protocol_plugin_desktop_resize(gp); +} + +static void gvnc_plugin_on_bell(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + REMMINA_PLUGIN_DEBUG("Bell message received"); + //GVncPluginData *gpdata = GET_PLUGIN_DATA(gp); + RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + + if (remmina_plugin_service->file_get_int(remminafile, "disableserverbell", FALSE)) + return; + GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(gp)); + + if (window) + gdk_window_beep(window); + REMMINA_PLUGIN_DEBUG("Beep emitted"); +} +static void gvnc_plugin_update_scale_mode(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + + GVncPluginData *gpdata = GET_PLUGIN_DATA(gp); + RemminaScaleMode scaleMode = remmina_plugin_service->remmina_protocol_widget_get_current_scale_mode(gp); + +#if 0 + g_object_set(gpdata->display, + "scaling", (scaleMode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED), + "resize-guest", (scaleMode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES), + NULL); +#endif + if (scaleMode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED || scaleMode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES) + vnc_display_set_scaling(VNC_DISPLAY(gpdata->vnc), TRUE); + else + vnc_display_set_scaling(VNC_DISPLAY(gpdata->vnc), FALSE); + + //width = remmina_plugin_service->protocol_plugin_get_width(gp); + //height = remmina_plugin_service->protocol_plugin_get_height(gp); + gpdata->width = vnc_display_get_width(VNC_DISPLAY(gpdata->vnc)); + gpdata->height = vnc_display_get_height(VNC_DISPLAY(gpdata->vnc)); + + if (scaleMode != REMMINA_PROTOCOL_WIDGET_SCALE_MODE_NONE) { + /* In scaled mode, the VncDisplay will get its dimensions from its parent */ + gtk_widget_set_size_request(GTK_WIDGET(gpdata->vnc), -1, -1); + } else { + /* In non scaled mode, the plugins forces dimensions of the VncDisplay */ +#if 0 + g_object_get(gpdata->display_channel, + "width", &width, + "height", &height, + NULL); + gtk_widget_set_size_request(GTK_WIDGET(gpdata->vnc), width, height); +#endif + gtk_widget_set_size_request(GTK_WIDGET(gpdata->vnc), gpdata->width, gpdata->height); + } + remmina_plugin_service->protocol_plugin_update_align(gp); +} + +static gboolean gvnc_plugin_query_feature(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature) +{ + TRACE_CALL(__func__); + + return TRUE; +} + +static void gvnc_plugin_call_feature(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature) +{ + TRACE_CALL(__func__); + + GVncPluginData *gpdata = GET_PLUGIN_DATA(gp); + RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + + guint keys[3]; + + switch (feature->id) { + case GVNC_PLUGIN_FEATURE_PREF_VIEWONLY: + gpdata->viewonly = remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE); + break; + case GVNC_PLUGIN_FEATURE_DYNRESUPDATE: + case GVNC_PLUGIN_FEATURE_SCALE: + gvnc_plugin_update_scale_mode(gp); + break; + case GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTDEL: + keys[0] = GDK_KEY_Control_L; + keys[1] = GDK_KEY_Alt_L; + keys[2] = GDK_KEY_Delete; + gvnc_plugin_keystroke(gp, keys, G_N_ELEMENTS(keys)); + break; + case GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTBACKSPACE: + keys[0] = GDK_KEY_Control_L; + keys[1] = GDK_KEY_Alt_L; + keys[2] = GDK_KEY_BackSpace; + gvnc_plugin_keystroke(gp, keys, G_N_ELEMENTS(keys)); + break; + case GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF1: + keys[0] = GDK_KEY_Control_L; + keys[1] = GDK_KEY_Alt_L; + keys[2] = GDK_KEY_F1; + gvnc_plugin_keystroke(gp, keys, G_N_ELEMENTS(keys)); + break; + case GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF2: + keys[0] = GDK_KEY_Control_L; + keys[1] = GDK_KEY_Alt_L; + keys[2] = GDK_KEY_F2; + gvnc_plugin_keystroke(gp, keys, G_N_ELEMENTS(keys)); + break; + case GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF3: + keys[0] = GDK_KEY_Control_L; + keys[1] = GDK_KEY_Alt_L; + keys[2] = GDK_KEY_F3; + gvnc_plugin_keystroke(gp, keys, G_N_ELEMENTS(keys)); + break; + case GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF4: + keys[0] = GDK_KEY_Control_L; + keys[1] = GDK_KEY_Alt_L; + keys[2] = GDK_KEY_F4; + gvnc_plugin_keystroke(gp, keys, G_N_ELEMENTS(keys)); + break; + case GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF5: + keys[0] = GDK_KEY_Control_L; + keys[1] = GDK_KEY_Alt_L; + keys[2] = GDK_KEY_F5; + gvnc_plugin_keystroke(gp, keys, G_N_ELEMENTS(keys)); + break; + case GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF6: + keys[0] = GDK_KEY_Control_L; + keys[1] = GDK_KEY_Alt_L; + keys[2] = GDK_KEY_F6; + gvnc_plugin_keystroke(gp, keys, G_N_ELEMENTS(keys)); + break; + case GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF7: + keys[0] = GDK_KEY_Control_L; + keys[1] = GDK_KEY_Alt_L; + keys[2] = GDK_KEY_F7; + gvnc_plugin_keystroke(gp, keys, G_N_ELEMENTS(keys)); + break; + case GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF9: + keys[0] = GDK_KEY_Control_L; + keys[1] = GDK_KEY_Alt_L; + keys[2] = GDK_KEY_F9; + gvnc_plugin_keystroke(gp, keys, G_N_ELEMENTS(keys)); + break; + case GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF10: + keys[0] = GDK_KEY_Control_L; + keys[1] = GDK_KEY_Alt_L; + keys[2] = GDK_KEY_F10; + gvnc_plugin_keystroke(gp, keys, G_N_ELEMENTS(keys)); + break; + case GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF11: + keys[0] = GDK_KEY_Control_L; + keys[1] = GDK_KEY_Alt_L; + keys[2] = GDK_KEY_F11; + gvnc_plugin_keystroke(gp, keys, G_N_ELEMENTS(keys)); + break; + case GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF12: + keys[0] = GDK_KEY_Control_L; + keys[1] = GDK_KEY_Alt_L; + keys[2] = GDK_KEY_F12; + gvnc_plugin_keystroke(gp, keys, G_N_ELEMENTS(keys)); + break; +#ifdef HAVE_VNC_POWER_CONTROL + case GVNC_PLUGIN_FEATURE_TOOL_REBOOT: + gvnc_plugin_power_ctrl(gp, VNC_CONNECTION_POWER_ACTION_REBOOT); + break; + case GVNC_PLUGIN_FEATURE_TOOL_RESET: + gvnc_plugin_power_ctrl(gp, VNC_CONNECTION_POWER_ACTION_RESET); + break; + case GVNC_PLUGIN_FEATURE_TOOL_SHUTDOWN: + gvnc_plugin_power_ctrl(gp, VNC_CONNECTION_POWER_ACTION_SHUTDOWN); + break; +#endif + default: + break; + } +} + +static void gvnc_plugin_auth_unsupported(VncDisplay *vnc G_GNUC_UNUSED, unsigned int authType, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + GVncPluginData *gpdata = GET_PLUGIN_DATA(gp); + + g_clear_pointer(&gpdata->error_msg, g_free); + gchar *msg = g_strdup_printf(_("Unsupported authentication type %u"), authType); + + remmina_plugin_service->protocol_plugin_set_error(gp, "%s", msg); + g_free(msg); +} + +static void gvnc_plugin_auth_failure(VncDisplay *vnc G_GNUC_UNUSED, const gchar *reason, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + GVncPluginData *gpdata = GET_PLUGIN_DATA(gp); + + g_clear_pointer(&gpdata->error_msg, g_free); + gchar *msg = g_strdup_printf(_("Authentication failure: %s"), reason); + + remmina_plugin_service->protocol_plugin_set_error(gp, "%s", msg); + g_free(msg); +} + +static gboolean gvnc_plugin_ask_auth(GtkWidget *vncdisplay, GValueArray *credList, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + + gint ret; + gboolean disablepasswordstoring; + gchar *s_username = NULL, *s_password = NULL; + gboolean wantPassword = FALSE, wantUsername = FALSE; + int i; + gboolean save; + + + GVncPluginData *gpdata = GET_PLUGIN_DATA(gp); + RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + + + GArray *credArray = g_array_sized_new(FALSE, TRUE, sizeof(GValue), credList->n_values); + g_array_set_clear_func(credArray, (GDestroyNotify) g_value_unset); + g_array_append_vals(credArray, credList->values, credList->n_values); + + REMMINA_PLUGIN_DEBUG("Got credential request for %d credential(s)\n", credArray->len); + + for (i = 0 ; i < credArray->len ; i++) { + GValue *cred = &g_array_index(credArray, GValue, i); + switch (g_value_get_enum(cred)) { + case VNC_DISPLAY_CREDENTIAL_USERNAME: + wantUsername = TRUE; + break; + case VNC_DISPLAY_CREDENTIAL_PASSWORD: + wantPassword = TRUE; + break; + case VNC_DISPLAY_CREDENTIAL_CLIENTNAME: + break; + default: + REMMINA_PLUGIN_DEBUG("Unsupported credential type %d", g_value_get_enum(cred)); + vnc_display_close(VNC_DISPLAY(gpdata->vnc)); + goto cleanup; + } + } + 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) + | (wantUsername ? REMMINA_MESSAGE_PANEL_FLAG_USERNAME : 0), + _("Enter VNC authentication credentials"), + (wantUsername ? remmina_plugin_service->file_get_string(remminafile, "username") : NULL), + (wantPassword ? remmina_plugin_service->file_get_string(remminafile, "password") : NULL), + NULL, + NULL); + if (ret == GTK_RESPONSE_OK) { + s_username = remmina_plugin_service->protocol_plugin_init_get_username(gp); + s_password = remmina_plugin_service->protocol_plugin_init_get_password(gp); + remmina_plugin_service->file_set_string(remminafile, "username", s_username); + + save = remmina_plugin_service->protocol_plugin_init_get_savepassword(gp); + if (save) { + // User has requested to save credentials. We put the password + // into remminafile->settings. It will be saved later, on successful connection, by + // rcw.c + remmina_plugin_service->file_set_string(remminafile, "password", s_password); + } else { + remmina_plugin_service->file_set_string(remminafile, "password", NULL); + } + + for (i = 0 ; i < credArray->len ; i++) { + GValue *cred = &g_array_index(credArray, GValue, i); + switch (g_value_get_enum(cred)) { + case VNC_DISPLAY_CREDENTIAL_USERNAME: + if (!s_username || + vnc_display_set_credential(VNC_DISPLAY(gpdata->vnc), + g_value_get_enum(cred), + s_username)) { + g_debug("Failed to set credential type %d", g_value_get_enum(cred)); + vnc_display_close(VNC_DISPLAY(gpdata->vnc)); + } + break; + case VNC_DISPLAY_CREDENTIAL_PASSWORD: + if (!s_password || + vnc_display_set_credential(VNC_DISPLAY(gpdata->vnc), + g_value_get_enum(cred), + s_password)) { + g_debug("Failed to set credential type %d", g_value_get_enum(cred)); + vnc_display_close(VNC_DISPLAY(gpdata->vnc)); + } + break; + case VNC_DISPLAY_CREDENTIAL_CLIENTNAME: + if (vnc_display_set_credential(VNC_DISPLAY(gpdata->vnc), + g_value_get_enum(cred), + "remmina")) { + g_debug("Failed to set credential type %d", g_value_get_enum(cred)); + vnc_display_close(VNC_DISPLAY(gpdata->vnc)); + } + break; + default: + g_debug("Unsupported credential type %d", g_value_get_enum(cred)); + vnc_display_close(VNC_DISPLAY(gpdata->vnc)); + } + } + + if (s_username) g_free(s_username); + if (s_password) g_free(s_password); + + return TRUE; + } else { + return FALSE; + } + +cleanup: + g_free(s_username); + g_free(s_password); + + //g_object_set(gpdata->session, "password", s_password, NULL); + return TRUE; +} + +static void gvnc_plugin_initialized(GtkWidget *vncdisplay, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + + GVncPluginData *gpdata = GET_PLUGIN_DATA(gp); + RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + + REMMINA_PLUGIN_DEBUG("Connection initialized"); + g_return_if_fail(gpdata != NULL); + REMMINA_PLUGIN_DEBUG("Presenting the window"); + + VncAudioFormat format = { + VNC_AUDIO_FORMAT_RAW_S32, + 2, + 44100, + }; + + REMMINA_PLUGIN_DEBUG("Gathering the VNC connection object"); + gpdata->conn = vnc_display_get_connection(VNC_DISPLAY(gpdata->vnc)); + g_return_if_fail(gpdata->conn != NULL); + + if (remmina_plugin_service->file_get_int(remminafile, "enableaudio", FALSE)) { + REMMINA_PLUGIN_DEBUG("Setting up VNC audio channel"); + if (vnc_connection_set_audio_format(gpdata->conn, &format)) + REMMINA_PLUGIN_DEBUG("VNC audio format set"); + else { + REMMINA_PLUGIN_DEBUG("VNC audio format returned an error"); + return; + } + + if (vnc_connection_set_audio(gpdata->conn, VNC_AUDIO(gpdata->pa))) + REMMINA_PLUGIN_DEBUG("VNC audio channel has been set"); + else { + REMMINA_PLUGIN_DEBUG("VNC audio channel cannot be set"); + return; + } + REMMINA_PLUGIN_DEBUG("Enabling audio"); + if (vnc_connection_audio_enable(gpdata->conn)) + REMMINA_PLUGIN_DEBUG("Audio enabled"); + else + REMMINA_PLUGIN_DEBUG("Audio cannot be enabled"); + } + gpdata->width = vnc_display_get_width(VNC_DISPLAY(gpdata->vnc)); + gpdata->width = vnc_display_get_height(VNC_DISPLAY(gpdata->vnc)); +} + +static void gvnc_plugin_disconnected(VncDisplay *vnc G_GNUC_UNUSED, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + //GVncPluginData *gpdata = GET_PLUGIN_DATA(gp); + + REMMINA_PLUGIN_DEBUG("[%s] Plugin disconnected", PLUGIN_NAME); +} +static gboolean gvnc_plugin_close_connection(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + GVncPluginData *gpdata = GET_PLUGIN_DATA(gp); + + REMMINA_PLUGIN_DEBUG("[%s] Plugin close connection", PLUGIN_NAME); + + if (gpdata) { + if (gpdata->error_msg) g_free(gpdata->error_msg); + if (gpdata->vnc) + vnc_display_close(VNC_DISPLAY(gpdata->vnc)); + //g_object_unref(gpdata->vnc); + } + + /* Remove instance->context from gp object data to avoid double free */ + g_object_steal_data(G_OBJECT(gp), "plugin-data"); + remmina_plugin_service->protocol_plugin_signal_connection_closed(gp); + return FALSE; +} + +static void gvnc_plugin_init(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + GVncPluginData *gpdata; + //VncGrabSequence *seq; + + GtkClipboard *cb; + + gpdata = g_new0(GVncPluginData, 1); + g_object_set_data_full(G_OBJECT(gp), "plugin-data", gpdata, g_free); + + gpdata->pa = NULL; + + REMMINA_PLUGIN_DEBUG("[%s] Plugin init", PLUGIN_NAME); + + RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + + if (remmina_plugin_service->file_get_int(remminafile, "gvncdebug", FALSE)) + vnc_util_set_debug(TRUE); + + gpdata->vnc = vnc_display_new(); + if (remmina_plugin_service->file_get_int(remminafile, "enableaudio", FALSE)) + gpdata->pa = vnc_audio_pulse_new(); + + + g_signal_connect(gpdata->vnc, "vnc-auth-credential", + G_CALLBACK(gvnc_plugin_ask_auth), gp); + g_signal_connect(gpdata->vnc, "vnc-auth-failure", + G_CALLBACK(gvnc_plugin_auth_failure), gp); + g_signal_connect(gpdata->vnc, "vnc-auth-unsupported", + G_CALLBACK(gvnc_plugin_auth_unsupported), gp); + g_signal_connect(gpdata->vnc, "vnc-disconnected", + G_CALLBACK(gvnc_plugin_disconnected), gp); + g_signal_connect(gpdata->vnc, "vnc-initialized", + G_CALLBACK(gvnc_plugin_initialized), gp); + g_signal_connect(gpdata->vnc, "vnc-desktop-resize", + G_CALLBACK(gvnc_plugin_desktop_resize), gp); + g_signal_connect(gpdata->vnc, "vnc-bell", + G_CALLBACK(gvnc_plugin_on_bell), gp); + g_signal_connect(gpdata->vnc, "vnc-error", + G_CALLBACK(gvnc_plugin_on_vnc_error), gp); + g_signal_connect(gpdata->vnc, "vnc-pointer-grab", + G_CALLBACK(gvnc_plugin_mouse_grab), gp); + g_signal_connect(gpdata->vnc, "vnc-pointer-ungrab", + G_CALLBACK(gvnc_plugin_mouse_ungrab), gp); + g_signal_connect(gpdata->vnc, "vnc-server-cut-text", + G_CALLBACK(gvnc_plugin_cut_text), gp); + //seq = vnc_grab_sequence_new_from_string ("Control_R"); + //vnc_display_set_grab_keys(VNC_DISPLAY(gpdata->vnc), seq); + //vnc_grab_sequence_free(seq); + + /* Setup the clipboard */ + cb = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); + gpdata->signal_clipboard = g_signal_connect(cb, + "owner-change", + G_CALLBACK(gvnc_plugin_clipboard_cb), + gp); +} + +static gboolean gvnc_plugin_open_connection(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + + gint port; + gchar *host = NULL, *tunnel = NULL; + GVncPluginData *gpdata = GET_PLUGIN_DATA(gp); + RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + + REMMINA_PLUGIN_DEBUG("[%s] Plugin open connection", PLUGIN_NAME); + + gpdata->box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_container_add(GTK_CONTAINER(gp), gpdata->box); + gtk_widget_set_hexpand(GTK_WIDGET(gpdata->vnc), TRUE); + gtk_widget_set_vexpand(GTK_WIDGET(gpdata->vnc), TRUE); + gtk_container_add(GTK_CONTAINER(gpdata->box), gpdata->vnc); + + remmina_plugin_service->protocol_plugin_register_hostkey(gp, gpdata->vnc); + + + const gchar *address = remmina_plugin_service->file_get_string(remminafile, "server"); + if(strstr(g_strdup(address), "unix:///") != NULL) { + REMMINA_PLUGIN_DEBUG("address contain unix:// -> %s", address); + gchar *val = str_replace (address, "unix://", ""); + REMMINA_PLUGIN_DEBUG("address after cleaning = %s", val); + gint fd = remmina_plugin_service->open_unix_sock(val); + REMMINA_PLUGIN_DEBUG("Unix socket fd: %d", fd); + gpdata->fd = fd; + g_free(val); + + } else { + /* Setup SSH tunnel if needed */ + tunnel = remmina_plugin_service->protocol_plugin_start_direct_tunnel(gp, GVNC_DEFAULT_PORT, FALSE); + if (!tunnel) + return FALSE; + remmina_plugin_service->get_server_port(tunnel, + GVNC_DEFAULT_PORT, + &host, + &port); + } + + + + gpdata->depth_profile = remmina_plugin_service->file_get_int(remminafile, "depth_profile", 24); + gpdata->viewonly = remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE); + vnc_display_set_depth(VNC_DISPLAY(gpdata->vnc), gpdata->depth_profile); + if (gpdata->fd > 0) + vnc_display_open_fd(VNC_DISPLAY(gpdata->vnc), gpdata->fd); + else + vnc_display_open_host(VNC_DISPLAY(gpdata->vnc), host, g_strdup_printf("%d", port)); + gpdata->lossy_encoding = remmina_plugin_service->file_get_int(remminafile, "lossy_encoding", FALSE); + vnc_display_set_lossy_encoding(VNC_DISPLAY(gpdata->vnc), gpdata->lossy_encoding); + vnc_display_set_shared_flag(VNC_DISPLAY(gpdata->vnc), gpdata->shared); + + if(host) g_free(host); + if(tunnel) g_free(tunnel); + + /* TRUE Conflict with remmina? */ + vnc_display_set_keyboard_grab(VNC_DISPLAY(gpdata->vnc), FALSE); + vnc_display_set_read_only(VNC_DISPLAY(gpdata->vnc), gpdata->viewonly); + /* TRUE Conflict with remmina? */ + vnc_display_set_pointer_grab(VNC_DISPLAY(gpdata->vnc), FALSE); + vnc_display_set_pointer_local(VNC_DISPLAY(gpdata->vnc), TRUE); + + vnc_display_set_force_size(VNC_DISPLAY(gpdata->vnc), FALSE); + vnc_display_set_scaling(VNC_DISPLAY(gpdata->vnc), TRUE); +#ifdef HAVE_VNC_REMOTE_RESIZE + vnc_display_set_allow_resize(VNC_DISPLAY(gpdata->vnc), TRUE); + vnc_display_set_zoom_level(VNC_DISPLAY(gpdata->vnc), opt_zoom); +#endif + remmina_plugin_service->protocol_plugin_signal_connection_opened(gp); + gtk_widget_show_all(gpdata->box); + return TRUE; +} + +/* Array of key/value pairs for color depths */ +static gpointer colordepth_list[] = +{ + "0", N_("Use server settings"), + "1", N_("True colour (24 bits)"), + "2", N_("High colour (16 bits)"), + "3", N_("Low colour (8 bits)"), + "4", N_("Ultra low colour (3 bits)"), + NULL +}; + +/* 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 gvnc_plugin_basic_settings[] = +{ + { REMMINA_PROTOCOL_SETTING_TYPE_SERVER, "server", NULL, FALSE, NULL, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD, "password", N_("VNC password"), FALSE, NULL, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "depth_profile", N_("Colour depth"), FALSE, colordepth_list, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "lossy_encoding", N_("Use JPEG Compression"), TRUE, NULL, N_("This might not work on all VNC servers"), NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "gvncdebug", N_("Enable GTK-VNC debug"), 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 gvnc_plugin_advanced_settings[] = +{ + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disableclipboard", N_("No clipboard sync"), TRUE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disablepasswordstoring", N_("Forget passwords after use"), FALSE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disableserverbell", N_("Ignore remote bell messages"), TRUE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "enableaudio", N_("Enable audio channel"), FALSE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "viewonly", N_("View only"), TRUE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "shared", N_("Shared connection"), TRUE, NULL, N_("If the server should try to share the desktop by leaving other clients connected") }, + { 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 gvnc_plugin_features[] = +{ + { REMMINA_PROTOCOL_FEATURE_TYPE_PREF, GVNC_PLUGIN_FEATURE_PREF_VIEWONLY, GINT_TO_POINTER(REMMINA_PROTOCOL_FEATURE_PREF_CHECK), "viewonly", N_("View only") }, + { REMMINA_PROTOCOL_FEATURE_TYPE_PREF, GVNC_PLUGIN_FEATURE_PREF_DISABLECLIPBOARD, GINT_TO_POINTER(REMMINA_PROTOCOL_FEATURE_PREF_CHECK), "disableclipboard", N_("No clipboard sync") }, + { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTDEL, N_("Send Ctrl+Alt+_Del"), NULL, NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTBACKSPACE, N_("Send Ctrl+Alt+_Backspace"), NULL, NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF1, N_("Send Ctrl+Alt+_F1"), NULL, NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF2, N_("Send Ctrl+Alt+_F2"), NULL, NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF3, N_("Send Ctrl+Alt+_F3"), NULL, NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF4, N_("Send Ctrl+Alt+_F4"), NULL, NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF5, N_("Send Ctrl+Alt+_F5"), NULL, NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF6, N_("Send Ctrl+Alt+_F6"), NULL, NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF7, N_("Send Ctrl+Alt+_F7"), NULL, NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF8, N_("Send Ctrl+Alt+_F8"), NULL, NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF9, N_("Send Ctrl+Alt+_F9"), NULL, NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF10, N_("Send Ctrl+Alt+_F10"), NULL, NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF11, N_("Send Ctrl+Alt+_F11"), NULL, NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF12, N_("Send Ctrl+Alt+_F12"), NULL, NULL }, +#ifdef HAVE_VNC_POWER_CONTROL + { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_REBOOT, N_("Reboot remote host"), NULL, NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_RESET, N_("Reset remote host (hard reboot)"), NULL, NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_SHUTDOWN, N_("Shutdown remote host"), NULL, NULL }, +#endif + { REMMINA_PROTOCOL_FEATURE_TYPE_DYNRESUPDATE, GVNC_PLUGIN_FEATURE_DYNRESUPDATE, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_SCALE, GVNC_PLUGIN_FEATURE_SCALE, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_END, 0, NULL, NULL, NULL } +}; + +/* Protocol plugin definition and features */ +static RemminaProtocolPlugin remmina_plugin = { + REMMINA_PLUGIN_TYPE_PROTOCOL, // Type + PLUGIN_NAME, // Name + PLUGIN_DESCRIPTION, // Description + GETTEXT_PACKAGE, // Translation domain + PLUGIN_VERSION, // Version number + PLUGIN_APPICON, // Icon for normal connection + PLUGIN_APPICON, // Icon for SSH connection + gvnc_plugin_basic_settings, // Array for basic settings + gvnc_plugin_advanced_settings, // Array for advanced settings + REMMINA_PROTOCOL_SSH_SETTING_TUNNEL, // SSH settings type + gvnc_plugin_features, // Array for available features + gvnc_plugin_init, // Plugin initialization + gvnc_plugin_open_connection, // Plugin open connection + gvnc_plugin_close_connection, // Plugin close connection + gvnc_plugin_query_feature, // Query for available features + gvnc_plugin_call_feature, // Call a feature + gvnc_plugin_keystroke, // Send a keystroke + NULL, // No screenshot support available + //gvnc_plugin_get_screenshot, // No screenshot support available + NULL, // RCW map event + NULL // RCW unmap event +}; + +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"); + + if (!service->register_plugin((RemminaPlugin *)&remmina_plugin)) + return FALSE; + + return TRUE; +} diff --git a/plugins/gvnc/gvnc_plugin.h b/plugins/gvnc/gvnc_plugin.h new file mode 100644 index 0000000..613d032 --- /dev/null +++ b/plugins/gvnc/gvnc_plugin.h @@ -0,0 +1,131 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * 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 "common/remmina_plugin.h" + +#include <gdk/gdkkeysyms.h> +#include <vncdisplay.h> +#include <vncutil.h> +#include <vncaudiopulse.h> + + +#ifndef GDK_Return +#define GDK_Return GDK_KEY_Return +#endif +#ifndef GDK_Escape +#define GDK_Escape GDK_KEY_Escape +#endif +#ifndef GDK_BackSpace +#define GDK_BackSpace GDK_KEY_BackSpace +#endif +#ifndef GDK_Delete +#define GDK_Delete GDK_KEY_Delete +#endif +#ifndef GDK_Control_L +#define GDK_Control_L GDK_KEY_Control_L +#endif +#ifndef GDK_Alt_L +#define GDK_Alt_L GDK_KEY_Alt_L +#endif +#ifndef GDK_F1 +#define GDK_F1 GDK_KEY_F1 +#endif +#ifndef GDK_F2 +#define GDK_F2 GDK_KEY_F2 +#endif +#ifndef GDK_F3 +#define GDK_F3 GDK_KEY_F3 +#endif +#ifndef GDK_F4 +#define GDK_F4 GDK_KEY_F4 +#endif +#ifndef GDK_F5 +#define GDK_F5 GDK_KEY_F5 +#endif +#ifndef GDK_F6 +#define GDK_F6 GDK_KEY_F6 +#endif +#ifndef GDK_F7 +#define GDK_F7 GDK_KEY_F7 +#endif +#ifndef GDK_F8 +#define GDK_F8 GDK_KEY_F8 +#endif +#ifndef GDK_F11 +#define GDK_F11 GDK_KEY_F11 +#endif + +#define GET_PLUGIN_DATA(gp) (GVncPluginData *)g_object_get_data(G_OBJECT(gp), "plugin-data") + +#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 _GVncPluginData { + GtkWidget * box; + GtkWidget * vnc; + VncConnection * conn; + VncAudioPulse * pa; + gchar * error_msg; + gchar * clipstr; + gulong signal_clipboard; + gint depth_profile; + gint shared; + gboolean lossy_encoding; + gboolean viewonly; + gint width; + gint height; + gint fd; + gchar * addr; +} GVncPluginData; + +G_BEGIN_DECLS +G_END_DECLS diff --git a/plugins/gvnc/gvnc_plugin_config.h b/plugins/gvnc/gvnc_plugin_config.h new file mode 100644 index 0000000..af55b3c --- /dev/null +++ b/plugins/gvnc/gvnc_plugin_config.h @@ -0,0 +1,43 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * 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. + * + */ + +#ifndef __PLUGIN_CONFIG_H +#define __PLUGIN_CONFIG_H + +#define PLUGIN_NAME "GVNC" +#define PLUGIN_DESCRIPTION N_("Remmina VNC plugin for GNOME and KVM") +#define PLUGIN_VERSION "0.1b" +#define PLUGIN_APPICON "org.remmina.Remmina-gvnc-symbolic" +#endif diff --git a/plugins/gvnc/scalable/emblems/org.remmina.Remmina-gvnc-ssh-symbolic.svg b/plugins/gvnc/scalable/emblems/org.remmina.Remmina-gvnc-ssh-symbolic.svg new file mode 100644 index 0000000..6c1da59 --- /dev/null +++ b/plugins/gvnc/scalable/emblems/org.remmina.Remmina-gvnc-ssh-symbolic.svg @@ -0,0 +1,90 @@ +<?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="99.772003" + height="99.772079" + viewBox="0 0 26.39801 26.398028" + version="1.1" + id="svg8" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="vnc over ssh.svg"> + <defs + id="defs2" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:zoom="1.0493442" + inkscape:cx="-11.459557" + inkscape:cy="109.76653" + 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="-162.7958" + originy="-83.119086" /> + </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(-162.79579,-187.48289)"> + <path + d="m 175.99485,195.20186 c -1.73487,0 -3.13161,1.42611 -3.13161,3.18029 v 1.51686 h -0.7829 c -0.43365,0 -0.7829,0.34925 -0.7829,0.7829 v 4.69715 c 0,0.43365 0.34925,0.7829 0.7829,0.7829 h 7.82875 c 0.43366,0 0.78291,-0.34925 0.78291,-0.7829 v -4.69715 c 0,-0.43365 -0.34925,-0.7829 -0.78291,-0.7829 h -0.7829 v -1.51686 c 0,-1.75365 -1.39673,-3.18029 -3.13134,-3.18029 z m 0,1.56581 c 0.8673,0 1.56554,0.69823 1.56554,1.56554 v 1.5658 h -3.13135 v -1.5658 c 0,-0.86731 0.69824,-1.56554 1.56581,-1.56554 z" + id="path34" + inkscape:connector-curvature="0" + style="isolation:isolate;fill:#171717;stroke-width:0.26458332" /> + <path + d="m 187.30817,213.88092 c 0,0 0.14367,-0.008 0.34237,-0.0442 0.38523,-0.0569 0.67627,-0.195 0.89138,-0.38312 0.0415,-0.0317 0.082,-0.0661 0.12144,-0.10319 0.037,-0.0394 0.0712,-0.0796 0.10319,-0.12144 0.18785,-0.21484 0.32623,-0.50615 0.38285,-0.89112 0.0368,-0.1987 0.0444,-0.34263 0.0444,-0.34263 v -22.62691 c 0,-1.88542 -1.88568,-1.88542 -1.88568,-1.88542 h -22.6269 c 0,0 -1.88543,0 -1.88543,1.88542 v 22.6269 c 0,0 0.008,0.14393 0.0442,0.34264 0.0585,0.38497 0.19447,0.67627 0.38285,0.89111 0.032,0.0418 0.0675,0.082 0.10319,0.12145 0.0397,0.037 0.0802,0.0714 0.12144,0.10318 0.21511,0.18812 0.50668,0.32624 0.89138,0.38312 0.19976,0.0363 0.34238,0.0442 0.34238,0.0442 z m -1.88569,-4.5347 h -18.85553 v -18.09221 h 18.85553 z" + id="path36" + inkscape:connector-curvature="0" + style="isolation:isolate;fill:#171717;stroke-width:0.26458332" /> + </g> +</svg> diff --git a/plugins/gvnc/scalable/emblems/org.remmina.Remmina-gvnc-symbolic.svg b/plugins/gvnc/scalable/emblems/org.remmina.Remmina-gvnc-symbolic.svg new file mode 100644 index 0000000..060a62a --- /dev/null +++ b/plugins/gvnc/scalable/emblems/org.remmina.Remmina-gvnc-symbolic.svg @@ -0,0 +1,115 @@ +<?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.458335 26.458332" + version="1.1" + id="svg8" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="vnc protocol.svg"> + <defs + id="defs2"> + <mask + id="_mask_t2Us8opi94eYjaS69EfyEcHSNVQ03DDZ" + x="-200%" + y="-200%" + width="400%" + height="400%"> + <rect + x="0" + y="0" + width="100%" + height="100%" + style="fill:#ffffff" + id="rect40" /> + <path + d="m 850.286,202.852 c 0,0 0.452,-0.025 1.077,-0.139 1.211,-0.179 2.128,-0.614 2.804,-1.205 0.13,-0.1 0.257,-0.208 0.382,-0.324 0.116,-0.125 0.224,-0.251 0.324,-0.382 0.592,-0.677 1.026,-1.593 1.205,-2.804 0.115,-0.625 0.139,-1.077 0.139,-1.077 V 125.75 c 0,-5.931 -5.931,-5.931 -5.931,-5.931 h -71.171 c 0,0 -5.931,0 -5.931,5.931 v 71.171 c 0,0 0.026,0.452 0.139,1.077 0.184,1.211 0.612,2.127 1.205,2.804 0.101,0.131 0.212,0.257 0.324,0.382 0.125,0.116 0.252,0.224 0.383,0.324 0.676,0.591 1.593,1.026 2.803,1.205 0.628,0.114 1.077,0.139 1.077,0.139 z m -5.931,-14.263 h -59.309 v -56.908 h 59.309 z" + id="path42" + inkscape:connector-curvature="0" + style="fill:#000000;stroke:none" /> + </mask> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:zoom="1.4839967" + inkscape:cx="-65.390539" + inkscape:cy="-17.020138" + inkscape:document-units="px" + inkscape:current-layer="g820" + 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="-146.65307" + originy="-135.56714" /> + </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(-146.65306,-134.97453)"> + <g + id="g820" + transform="matrix(1.0697634,0,0,1.0697634,-9.2735492,-11.925523)"> + <path + inkscape:connector-curvature="0" + id="path38" + d="m 147.83075,137.94029 c 0,0 -1.17769,2e-5 -1.17769,1.17769 v 14.13263 c 0,0 0.005,0.09 0.0274,0.21405 0.0365,0.24056 0.12144,0.42255 0.23928,0.55686 0.02,0.0261 0.0421,0.051 0.0644,0.0757 0.0248,0.023 0.0503,0.0445 0.0761,0.0644 0.13431,0.11739 0.31608,0.20363 0.55643,0.23928 0.12474,0.0227 0.21405,0.0278 0.21405,0.0278 h 4.62807 v -2.83218 H 149.0084 V 140.2961 h 11.77727 v 4.05511 h 2.35536 v -5.23323 c 0,-1.17767 -1.17768,-1.17769 -1.17768,-1.17769 z m 12.95495,10.06883 v 3.58743 h -4.66941 v 2.83218 h 5.84709 c 0,0 0.09,-0.005 0.21405,-0.0278 0.24056,-0.0356 0.4221,-0.1219 0.55643,-0.23928 0.0258,-0.0199 0.0514,-0.0414 0.0761,-0.0644 0.023,-0.0247 0.0445,-0.0497 0.0644,-0.0757 0.11738,-0.13431 0.20386,-0.3163 0.23927,-0.55686 0.023,-0.12408 0.0274,-0.21405 0.0274,-0.21405 v -5.24149 z" + style="isolation:isolate;fill:#171717;stroke-width:0.22274615" /> + <path + style="isolation:isolate;fill:#171717;stroke-width:0.22274615" + inkscape:connector-curvature="0" + id="path38-5" + d="m 168.41828,161.43286 c 0,0 0.0898,-0.005 0.21384,-0.0276 0.24056,-0.0356 0.42232,-0.12185 0.55664,-0.23923 0.0258,-0.0199 0.0513,-0.0414 0.0759,-0.0644 0.0229,-0.0248 0.0445,-0.0499 0.0644,-0.0759 0.11739,-0.13432 0.20381,-0.31608 0.23923,-0.55665 0.023,-0.12407 0.0276,-0.21405 0.0276,-0.21405 v -14.13259 c 0,-1.17766 -1.17767,-1.17766 -1.17767,-1.17766 h -14.13279 c 0,0 -1.17765,0 -1.17765,1.17766 v 14.13258 c 0,0 0.005,0.09 0.0276,0.21406 0.0365,0.24057 0.1214,0.42233 0.23924,0.55664 0.02,0.0261 0.0421,0.0512 0.0644,0.0759 0.0248,0.023 0.0501,0.0445 0.0759,0.0644 0.13432,0.11738 0.3163,0.20359 0.55664,0.23923 0.12474,0.0227 0.21384,0.0276 0.21384,0.0276 z m -1.17788,-2.83222 v -11.30036 h -11.77704 11.77704 z" + sodipodi:nodetypes="cccccccsccsccccccccccccc" /> + </g> + </g> +</svg> diff --git a/plugins/kwallet/CMakeLists.txt b/plugins/kwallet/CMakeLists.txt new file mode 100644 index 0000000..a448e31 --- /dev/null +++ b/plugins/kwallet/CMakeLists.txt @@ -0,0 +1,54 @@ +# remmina-plugin-www - The GTK+ Remote Desktop Client +# +# Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo +# +# 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. + + + +# find_suggested_package(KF5Wallet) has already been run on main CMakeLists.txt + +find_suggested_package(KF5Wallet) +if(KF5Wallet_FOUND) + message(STATUS "KF5Wallet library found.") + set(REMMINA_PLUGIN_KF5WALLET_SRCS + src/kwallet_plugin_main.c + src/kwallet_plugin.cpp + src/kwallet_plugin.h + ) + + add_library(remmina-plugin-kwallet MODULE ${REMMINA_PLUGIN_KF5WALLET_SRCS}) + set_target_properties(remmina-plugin-kwallet PROPERTIES PREFIX "") + set_target_properties(remmina-plugin-kwallet PROPERTIES NO_SONAME 1) + + include_directories(${GTK_INCLUDE_DIRS}) + target_link_libraries(remmina-plugin-kwallet KF5::Wallet ${GLib_LIBRARY}) + + install(TARGETS remmina-plugin-kwallet DESTINATION ${REMMINA_PLUGINDIR}) +endif() diff --git a/plugins/kwallet/src/kwallet_plugin.cpp b/plugins/kwallet/src/kwallet_plugin.cpp new file mode 100644 index 0000000..aeda596 --- /dev/null +++ b/plugins/kwallet/src/kwallet_plugin.cpp @@ -0,0 +1,89 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * 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 "kwallet_plugin.h" +#include <KWallet> + +static KWallet::Wallet* wallet; + +static char folderName[] = "Remmina"; + +int rp_kwallet_init(void) +{ + QString s = KWallet::Wallet::LocalWallet(); + wallet = KWallet::Wallet::openWallet(s, 0); + if (!wallet) { + return 0; + } + + if (!wallet->createFolder(folderName)) { + delete wallet; + return 0; + } + if (!wallet->setFolder(folderName)) { + delete wallet; + return 0; + } + + return 1; +} + +int rp_kwallet_is_service_available(void) +{ + return wallet != 0; +} + +void rp_kwallet_store_password(const char *key, const char *password) +{ + wallet->writePassword(key, password); +} + +char *rp_kwallet_get_password(const char *key) +{ + QString password; + if (wallet->readPassword(key, password) != 0) { + return NULL; + } + QByteArray pba = password.toUtf8(); + char *cpassword = (char *)malloc(pba.size()+1); + strcpy(cpassword, pba.data()); + return cpassword; +} + +void rp_kwallet_delete_password(const char *key) +{ + wallet->removeEntry(key); +} diff --git a/plugins/kwallet/src/kwallet_plugin.h b/plugins/kwallet/src/kwallet_plugin.h new file mode 100644 index 0000000..7499f8f --- /dev/null +++ b/plugins/kwallet/src/kwallet_plugin.h @@ -0,0 +1,52 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * 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 <gtk/gtk.h> +#include "remmina/plugin.h" + +#ifdef __cplusplus +extern "C" { +#endif + +int rp_kwallet_init(void); +void rp_kwallet_store_password(const char *key, const char *password); +char *rp_kwallet_get_password(const char *key); +void rp_kwallet_delete_password(const char *key); +int rp_kwallet_is_service_available(void); + + +#ifdef __cplusplus +} +#endif diff --git a/plugins/kwallet/src/kwallet_plugin_main.c b/plugins/kwallet/src/kwallet_plugin_main.c new file mode 100644 index 0000000..6a6be3b --- /dev/null +++ b/plugins/kwallet/src/kwallet_plugin_main.c @@ -0,0 +1,146 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * 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 "config.h" +#include <gtk/gtk.h> +#include <glib.h> +#include <glib/gi18n-lib.h> +#include <glib/gstdio.h> +#include <remmina/plugin.h> + +#include "kwallet_plugin.h" + +static RemminaPluginService *remmina_plugin_service = NULL; + +gboolean remmina_plugin_kwallet_is_service_available(RemminaPlugin *plugin) +{ + return rp_kwallet_is_service_available(); +} + + +static gchar *build_kwallet_key(RemminaFile *remminafile, const gchar *key) +{ + const gchar *path; + gchar *kwkey; + size_t kwkey_sz; + + path = remmina_plugin_service->file_get_path(remminafile); + + kwkey_sz = strlen(key) + 1 + strlen(path) + 1; + kwkey = g_malloc(kwkey_sz); + + strcpy(kwkey, key); + strcat(kwkey, ";"); + strcat(kwkey, path); + + return kwkey; +} + +void remmina_plugin_kwallet_store_password(RemminaPlugin *plugin, RemminaFile *remminafile, const gchar *key, const gchar *password) +{ + TRACE_CALL(__func__); + gchar *kwkey; + kwkey = build_kwallet_key(remminafile, key); + rp_kwallet_store_password(kwkey, password); + g_free(kwkey); +} + +gchar* +remmina_plugin_kwallet_get_password(RemminaPlugin *plugin, RemminaFile *remminafile, const gchar *key) +{ + TRACE_CALL(__func__); + gchar *kwkey, *password; + + kwkey = build_kwallet_key(remminafile, key); + password = rp_kwallet_get_password(kwkey); + g_free(kwkey); + + return password; +} + +void remmina_plugin_kwallet_delete_password(RemminaPlugin *plugin, RemminaFile *remminafile, const gchar *key) +{ + TRACE_CALL(__func__); + gchar *kwkey; + kwkey = build_kwallet_key(remminafile, key); + rp_kwallet_delete_password(kwkey); + g_free(kwkey); +} + +gboolean remmina_plugin_kwallet_init(RemminaPlugin *plugin) +{ + /* Activates only when KDE is running */ + const gchar *envvar; + + envvar = g_environ_getenv(g_get_environ(), "XDG_CURRENT_DESKTOP"); + if (!envvar || strcmp(envvar, "KDE") != 0) + return FALSE; + + return rp_kwallet_init(); + +} + +static RemminaSecretPlugin remmina_plugin_kwallet = +{ REMMINA_PLUGIN_TYPE_SECRET, + "kwallet", + N_("Secured password storage in KWallet"), + NULL, + VERSION, + 1000, + remmina_plugin_kwallet_init, + remmina_plugin_kwallet_is_service_available, + remmina_plugin_kwallet_store_password, + remmina_plugin_kwallet_get_password, + remmina_plugin_kwallet_delete_password, +}; + +G_MODULE_EXPORT gboolean +remmina_plugin_entry(RemminaPluginService *service) +{ + TRACE_CALL(__func__); + + /* This function should only register the secret plugin. No init action + * should be performed here. Initialization will be done later + * with remmina_plugin_xxx_init() . */ + + remmina_plugin_service = service; + if (!service->register_plugin((RemminaPlugin*)&remmina_plugin_kwallet)) { + return FALSE; + } + + return TRUE; + +} + diff --git a/plugins/python_wrapper/CMakeLists.txt b/plugins/python_wrapper/CMakeLists.txt new file mode 100644 index 0000000..d77eac5 --- /dev/null +++ b/plugins/python_wrapper/CMakeLists.txt @@ -0,0 +1,68 @@ +# 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_PYTHON_WRAPPER_SRCS + python_wrapper_common.c + python_wrapper_common.h + python_wrapper_entry.c + python_wrapper_entry.h + python_wrapper_file.c + python_wrapper_file.h + python_wrapper_plugin.c + python_wrapper_plugin.h + python_wrapper_pref.c + python_wrapper_pref.h + python_wrapper_protocol.c + python_wrapper_protocol.h + python_wrapper_protocol_widget.c + python_wrapper_protocol_widget.h + python_wrapper_remmina.c + python_wrapper_remmina.h + python_wrapper_remmina_file.c + python_wrapper_remmina_file.h + python_wrapper_secret.c + python_wrapper_secret.h + python_wrapper_tool.c + python_wrapper_tool.h + ) + +add_library(remmina-plugin-python_wrapper MODULE ${REMMINA_PLUGIN_PYTHON_WRAPPER_SRCS}) +set_target_properties(remmina-plugin-python_wrapper PROPERTIES PREFIX "") +set_target_properties(remmina-plugin-python_wrapper PROPERTIES NO_SONAME 1) + +include_directories(${REMMINA_COMMON_INCLUDE_DIRS} ${PYTHON_WRAPPER_INCLUDE_DIRS}) +target_link_libraries(remmina-plugin-python_wrapper ${REMMINA_COMMON_LIBRARIES} ${PYTHON_WRAPPER_LIBRARIES}) + +install(TARGETS remmina-plugin-python_wrapper DESTINATION ${REMMINA_PLUGINDIR}) + +target_include_directories(remmina-plugin-python_wrapper PRIVATE ${PYTHON_INCLUDE_DIRS}) +target_link_libraries(remmina-plugin-python_wrapper ${PYTHON_LIBRARIES}) diff --git a/plugins/python_wrapper/pygobject.h b/plugins/python_wrapper/pygobject.h new file mode 100644 index 0000000..145d1c6 --- /dev/null +++ b/plugins/python_wrapper/pygobject.h @@ -0,0 +1,669 @@ +/* -*- Mode: C; c-basic-offset: 4 -*- */ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * 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. + * + */ + +#ifndef _PYGOBJECT_H_ +#define _PYGOBJECT_H_ + +#include <Python.h> + +#include <glib.h> +#include <glib-object.h> + +G_BEGIN_DECLS + +/* PyGClosure is a _private_ structure */ +typedef void (* PyClosureExceptionHandler) (GValue *ret, guint n_param_values, const GValue *params); +typedef struct _PyGClosure PyGClosure; +typedef struct _PyGObjectData PyGObjectData; + +struct _PyGClosure { + GClosure closure; + PyObject *callback; + PyObject *extra_args; /* tuple of extra args to pass to callback */ + PyObject *swap_data; /* other object for gtk_signal_connect__object */ + PyClosureExceptionHandler exception_handler; +}; + +typedef enum { + PYGOBJECT_USING_TOGGLE_REF = 1 << 0, + PYGOBJECT_IS_FLOATING_REF = 1 << 1 +} PyGObjectFlags; + + /* closures is just an alias for what is found in the + * PyGObjectData */ +typedef struct { + PyObject_HEAD + GObject *obj; + PyObject *inst_dict; /* the instance dictionary -- must be last */ + PyObject *weakreflist; /* list of weak references */ + + /*< private >*/ + /* using union to preserve ABI compatibility (structure size + * must not change) */ + union { + GSList *closures; /* stale field; no longer updated DO-NOT-USE! */ + PyGObjectFlags flags; + } private_flags; + +} PyGObject; + +#define pygobject_get(v) (((PyGObject *)(v))->obj) +#define pygobject_check(v,base) (PyObject_TypeCheck(v,base)) + +typedef struct { + PyObject_HEAD + gpointer boxed; + GType gtype; + gboolean free_on_dealloc; +} PyGBoxed; + +#define pyg_boxed_get(v,t) ((t *)((PyGBoxed *)(v))->boxed) +#define pyg_boxed_check(v,typecode) (PyObject_TypeCheck(v, &PyGBoxed_Type) && ((PyGBoxed *)(v))->gtype == typecode) + +typedef struct { + PyObject_HEAD + gpointer pointer; + GType gtype; +} PyGPointer; + +#define pyg_pointer_get(v,t) ((t *)((PyGPointer *)(v))->pointer) +#define pyg_pointer_check(v,typecode) (PyObject_TypeCheck(v, &PyGPointer_Type) && ((PyGPointer *)(v))->gtype == typecode) + +typedef void (*PyGFatalExceptionFunc) (void); +typedef void (*PyGThreadBlockFunc) (void); + +typedef struct { + PyObject_HEAD + GParamSpec *pspec; +} PyGParamSpec; + +#define PyGParamSpec_Get(v) (((PyGParamSpec *)v)->pspec) +#define PyGParamSpec_Check(v) (PyObject_TypeCheck(v, &PyGParamSpec_Type)) + +typedef int (*PyGClassInitFunc) (gpointer gclass, PyTypeObject *pyclass); +typedef PyTypeObject * (*PyGTypeRegistrationFunction) (const gchar *name, + gpointer data); + +struct _PyGObject_Functions { + /* + * All field names in here are considered private, + * use the macros below instead, which provides stability + */ + void (* register_class)(PyObject *dict, const gchar *class_name, + GType gtype, PyTypeObject *type, PyObject *bases); + void (* register_wrapper)(PyObject *self); + PyTypeObject *(* lookup_class)(GType type); + PyObject *(* newgobj)(GObject *obj); + + GClosure *(* closure_new)(PyObject *callback, PyObject *extra_args, + PyObject *swap_data); + void (* object_watch_closure)(PyObject *self, GClosure *closure); + GDestroyNotify destroy_notify; + + GType (* type_from_object)(PyObject *obj); + PyObject *(* type_wrapper_new)(GType type); + + gint (* enum_get_value)(GType enum_type, PyObject *obj, gint *val); + gint (* flags_get_value)(GType flag_type, PyObject *obj, gint *val); + void (* register_gtype_custom)(GType gtype, + PyObject *(* from_func)(const GValue *value), + int (* to_func)(GValue *value, PyObject *obj)); + int (* value_from_pyobject)(GValue *value, PyObject *obj); + PyObject *(* value_as_pyobject)(const GValue *value, gboolean copy_boxed); + + void (* register_interface)(PyObject *dict, const gchar *class_name, + GType gtype, PyTypeObject *type); + + PyTypeObject *boxed_type; + void (* register_boxed)(PyObject *dict, const gchar *class_name, + GType boxed_type, PyTypeObject *type); + PyObject *(* boxed_new)(GType boxed_type, gpointer boxed, + gboolean copy_boxed, gboolean own_ref); + + PyTypeObject *pointer_type; + void (* register_pointer)(PyObject *dict, const gchar *class_name, + GType pointer_type, PyTypeObject *type); + PyObject *(* pointer_new)(GType boxed_type, gpointer pointer); + + void (* enum_add_constants)(PyObject *module, GType enum_type, + const gchar *strip_prefix); + void (* flags_add_constants)(PyObject *module, GType flags_type, + const gchar *strip_prefix); + + const gchar *(* constant_strip_prefix)(const gchar *name, + const gchar *strip_prefix); + + gboolean (* error_check)(GError **error); + + /* hooks to register handlers for getting GDK threads to cooperate + * with python threading */ + void (* set_thread_block_funcs) (PyGThreadBlockFunc block_threads_func, + PyGThreadBlockFunc unblock_threads_func); + PyGThreadBlockFunc block_threads; + PyGThreadBlockFunc unblock_threads; + PyTypeObject *paramspec_type; + PyObject *(* paramspec_new)(GParamSpec *spec); + GParamSpec *(*paramspec_get)(PyObject *tuple); + int (*pyobj_to_unichar_conv)(PyObject *pyobj, void* ptr); + gboolean (*parse_constructor_args)(GType obj_type, + char **arg_names, + char **prop_names, + GParameter *params, + guint *nparams, + PyObject **py_args); + PyObject *(* param_gvalue_as_pyobject) (const GValue* gvalue, + gboolean copy_boxed, + const GParamSpec* pspec); + int (* gvalue_from_param_pyobject) (GValue* value, + PyObject* py_obj, + const GParamSpec* pspec); + PyTypeObject *enum_type; + PyObject *(*enum_add)(PyObject *module, + const char *type_name_, + const char *strip_prefix, + GType gtype); + PyObject* (*enum_from_gtype)(GType gtype, int value); + + PyTypeObject *flags_type; + PyObject *(*flags_add)(PyObject *module, + const char *type_name_, + const char *strip_prefix, + GType gtype); + PyObject* (*flags_from_gtype)(GType gtype, int value); + + gboolean threads_enabled; + int (*enable_threads) (void); + + int (*gil_state_ensure) (void); + void (*gil_state_release) (int flag); + + void (*register_class_init) (GType gtype, PyGClassInitFunc class_init); + void (*register_interface_info) (GType gtype, const GInterfaceInfo *info); + void (*closure_set_exception_handler) (GClosure *closure, PyClosureExceptionHandler handler); + + void (*add_warning_redirection) (const char *domain, + PyObject *warning); + void (*disable_warning_redirections) (void); + void (*type_register_custom)(const gchar *type_name, + PyGTypeRegistrationFunction callback, + gpointer data); + gboolean (*gerror_exception_check) (GError **error); + PyObject* (*option_group_new) (GOptionGroup *group); + GType (* type_from_object_strict) (PyObject *obj, gboolean strict); +}; + +#ifndef _INSIDE_PYGOBJECT_ + +#if defined(NO_IMPORT) || defined(NO_IMPORT_PYGOBJECT) +extern struct _PyGObject_Functions *_PyGObject_API; +#else +struct _PyGObject_Functions *_PyGObject_API; +#endif + +#define pygobject_register_class (_PyGObject_API->register_class) +#define pygobject_register_wrapper (_PyGObject_API->register_wrapper) +#define pygobject_lookup_class (_PyGObject_API->lookup_class) +#define pygobject_new (_PyGObject_API->newgobj) +#define pyg_closure_new (_PyGObject_API->closure_new) +#define pygobject_watch_closure (_PyGObject_API->object_watch_closure) +#define pyg_closure_set_exception_handler (_PyGObject_API->closure_set_exception_handler) +#define pyg_destroy_notify (_PyGObject_API->destroy_notify) +#define pyg_type_from_object_strict (_PyGObject_API->type_from_object_strict) +#define pyg_type_from_object (_PyGObject_API->type_from_object) +#define pyg_type_wrapper_new (_PyGObject_API->type_wrapper_new) +#define pyg_enum_get_value (_PyGObject_API->enum_get_value) +#define pyg_flags_get_value (_PyGObject_API->flags_get_value) +#define pyg_register_gtype_custom (_PyGObject_API->register_gtype_custom) +#define pyg_value_from_pyobject (_PyGObject_API->value_from_pyobject) +#define pyg_value_as_pyobject (_PyGObject_API->value_as_pyobject) +#define pyg_register_interface (_PyGObject_API->register_interface) +#define PyGBoxed_Type (*_PyGObject_API->boxed_type) +#define pyg_register_boxed (_PyGObject_API->register_boxed) +#define pyg_boxed_new (_PyGObject_API->boxed_new) +#define PyGPointer_Type (*_PyGObject_API->pointer_type) +#define pyg_register_pointer (_PyGObject_API->register_pointer) +#define pyg_pointer_new (_PyGObject_API->pointer_new) +#define pyg_enum_add_constants (_PyGObject_API->enum_add_constants) +#define pyg_flags_add_constants (_PyGObject_API->flags_add_constants) +#define pyg_constant_strip_prefix (_PyGObject_API->constant_strip_prefix) +#define pyg_error_check (_PyGObject_API->error_check) +#define pyg_set_thread_block_funcs (_PyGObject_API->set_thread_block_funcs) +#define PyGParamSpec_Type (*_PyGObject_API->paramspec_type) +#define pyg_param_spec_new (_PyGObject_API->paramspec_new) +#define pyg_param_spec_from_object (_PyGObject_API->paramspec_get) +#define pyg_pyobj_to_unichar_conv (_PyGObject_API->pyobj_to_unichar_conv) +#define pyg_parse_constructor_args (_PyGObject_API->parse_constructor_args) +#define pyg_param_gvalue_as_pyobject (_PyGObject_API->value_as_pyobject) +#define pyg_param_gvalue_from_pyobject (_PyGObject_API->gvalue_from_param_pyobject) +#define PyGEnum_Type (*_PyGObject_API->enum_type) +#define pyg_enum_add (_PyGObject_API->enum_add) +#define pyg_enum_from_gtype (_PyGObject_API->enum_from_gtype) +#define PyGFlags_Type (*_PyGObject_API->flags_type) +#define pyg_flags_add (_PyGObject_API->flags_add) +#define pyg_flags_from_gtype (_PyGObject_API->flags_from_gtype) +#define pyg_enable_threads (_PyGObject_API->enable_threads) +#define pyg_gil_state_ensure (_PyGObject_API->gil_state_ensure) +#define pyg_gil_state_release (_PyGObject_API->gil_state_release) +#define pyg_register_class_init (_PyGObject_API->register_class_init) +#define pyg_register_interface_info (_PyGObject_API->register_interface_info) +#define pyg_add_warning_redirection (_PyGObject_API->add_warning_redirection) +#define pyg_disable_warning_redirections (_PyGObject_API->disable_warning_redirections) +#define pyg_type_register_custom_callback (_PyGObject_API->type_register_custom) +#define pyg_gerror_exception_check (_PyGObject_API->gerror_exception_check) +#define pyg_option_group_new (_PyGObject_API->option_group_new) + +#define pyg_block_threads() G_STMT_START { \ + if (_PyGObject_API->block_threads != NULL) \ + (* _PyGObject_API->block_threads)(); \ + } G_STMT_END +#define pyg_unblock_threads() G_STMT_START { \ + if (_PyGObject_API->unblock_threads != NULL) \ + (* _PyGObject_API->unblock_threads)(); \ + } G_STMT_END + +#define pyg_threads_enabled (_PyGObject_API->threads_enabled) + +#define pyg_begin_allow_threads \ + G_STMT_START { \ + PyThreadState *_save = NULL; \ + if (_PyGObject_API->threads_enabled) \ + _save = PyEval_SaveThread(); +#define pyg_end_allow_threads \ + if (_PyGObject_API->threads_enabled) \ + PyEval_RestoreThread(_save); \ + } G_STMT_END + + +/** + * pygobject_init: + * @req_major: minimum version major number, or -1 + * @req_minor: minimum version minor number, or -1 + * @req_micro: minimum version micro number, or -1 + * + * Imports and initializes the 'gobject' python module. Can + * optionally check for a required minimum version if @req_major, + * @req_minor, and @req_micro are all different from -1. + * + * Returns: a new reference to the gobject module on success, NULL in + * case of failure (and raises ImportError). + **/ +static inline PyObject * +pygobject_init(int req_major, int req_minor, int req_micro) +{ + PyObject *gobject, *cobject; + + gobject = PyImport_ImportModule("gi._gobject"); + if (!gobject) { + if (PyErr_Occurred()) + { + PyObject *type, *value, *traceback; + PyObject *py_orig_exc; + PyErr_Fetch(&type, &value, &traceback); + py_orig_exc = PyObject_Repr(value); + Py_XDECREF(type); + Py_XDECREF(value); + Py_XDECREF(traceback); + + +#if PY_VERSION_HEX < 0x03000000 + PyErr_Format(PyExc_ImportError, + "could not import gobject (error was: %s)", + PyString_AsString(py_orig_exc)); +#else + { + /* Can not use PyErr_Format because it doesn't have + * a format string for dealing with PyUnicode objects + * like PyUnicode_FromFormat has + */ + PyObject *errmsg = PyUnicode_FromFormat("could not import gobject (error was: %U)", + py_orig_exc); + + if (errmsg) { + PyErr_SetObject(PyExc_ImportError, + errmsg); + Py_DECREF(errmsg); + } + /* if errmsg is NULL then we might have OOM + * PyErr should already be set and trying to + * return our own error would be futile + */ + } +#endif + Py_DECREF(py_orig_exc); + } else { + PyErr_SetString(PyExc_ImportError, + "could not import gobject (no error given)"); + } + return NULL; + } + + cobject = PyObject_GetAttrString(gobject, "_PyGObject_API"); +#if PY_VERSION_HEX >= 0x03000000 + if (cobject && PyCapsule_CheckExact(cobject)) + _PyGObject_API = (struct _PyGObject_Functions *) PyCapsule_GetPointer(cobject, "gobject._PyGObject_API"); + +#else + if (cobject && PyCObject_Check(cobject)) + _PyGObject_API = (struct _PyGObject_Functions *) PyCObject_AsVoidPtr(cobject); +#endif + else { + PyErr_SetString(PyExc_ImportError, + "could not import gobject (could not find _PyGObject_API object)"); + Py_DECREF(gobject); + return NULL; + } + + if (req_major != -1) + { + int found_major, found_minor, found_micro; + PyObject *version; + + version = PyObject_GetAttrString(gobject, "pygobject_version"); + if (!version) { + PyErr_SetString(PyExc_ImportError, + "could not import gobject (version too old)"); + Py_DECREF(gobject); + return NULL; + } + if (!PyArg_ParseTuple(version, "iii", + &found_major, &found_minor, &found_micro)) { + PyErr_SetString(PyExc_ImportError, + "could not import gobject (version has invalid format)"); + Py_DECREF(version); + Py_DECREF(gobject); + return NULL; + } + Py_DECREF(version); + if (req_major != found_major || + req_minor > found_minor || + (req_minor == found_minor && req_micro > found_micro)) { + PyErr_Format(PyExc_ImportError, + "could not import gobject (version mismatch, %d.%d.%d is required, " + "found %d.%d.%d)", req_major, req_minor, req_micro, + found_major, found_minor, found_micro); + Py_DECREF(gobject); + return NULL; + } + } + return gobject; +} + +/** + * PYLIST_FROMGLIBLIST: + * @type: the type of the GLib list e.g. #GList or #GSList + * @prefix: the prefix of functions that manipulate a list of the type + * given by type. + * + * A macro that creates a type specific code block which converts a GLib + * list (#GSList or #GList) to a Python list. The first two args of the macro + * are used to specify the type and list function prefix so that the type + * specific macros can be generated. + * + * The rest of the args are for the standard args for the type specific + * macro(s) created from this macro. + */ + #define PYLIST_FROMGLIBLIST(type,prefix,py_list,list,item_convert_func,\ + list_free,list_item_free) \ +G_STMT_START \ +{ \ + gint i, len; \ + PyObject *item; \ + void (*glib_list_free)(type*) = list_free; \ + GFunc glib_list_item_free = (GFunc)list_item_free; \ + \ + len = prefix##_length(list); \ + py_list = PyList_New(len); \ + for (i = 0; i < len; i++) { \ + gpointer list_item = prefix##_nth_data(list, i); \ + \ + item = item_convert_func; \ + PyList_SetItem(py_list, i, item); \ + } \ + if (glib_list_item_free != NULL) \ + prefix##_foreach(list, glib_list_item_free, NULL); \ + if (glib_list_free != NULL) \ + glib_list_free(list); \ +} G_STMT_END + +/** + * PYLIST_FROMGLIST: + * @py_list: the name of the Python list + * + * @list: the #GList to be converted to a Python list + * + * @item_convert_func: the function that converts a list item to a Python + * object. The function must refer to the list item using "@list_item" and + * must return a #PyObject* object. An example conversion function is: + * [[ + * PyString_FromString(list_item) + * ]] + * A more elaborate function is: + * [[ + * pyg_boxed_new(GTK_TYPE_RECENT_INFO, list_item, TRUE, TRUE) + * ]] + * @list_free: the name of a function that takes a single arg (the list) and + * frees its memory. Can be NULL if the list should not be freed. An example + * is: + * [[ + * g_list_free + * ]] + * @list_item_free: the name of a #GFunc function that frees the memory used + * by the items in the list or %NULL if the list items do not have to be + * freed. A simple example is: + * [[ + * g_free + * ]] + * + * A macro that adds code that converts a #GList to a Python list. + * + */ +#define PYLIST_FROMGLIST(py_list,list,item_convert_func,list_free,\ + list_item_free) \ + PYLIST_FROMGLIBLIST(GList,g_list,py_list,list,item_convert_func,\ + list_free,list_item_free) + +/** + * PYLIST_FROMGSLIST: + * @py_list: the name of the Python list + * + * @list: the #GSList to be converted to a Python list + * + * @item_convert_func: the function that converts a list item to a Python + * object. The function must refer to the list item using "@list_item" and + * must return a #PyObject* object. An example conversion function is: + * [[ + * PyString_FromString(list_item) + * ]] + * A more elaborate function is: + * [[ + * pyg_boxed_new(GTK_TYPE_RECENT_INFO, list_item, TRUE, TRUE) + * ]] + * @list_free: the name of a function that takes a single arg (the list) and + * frees its memory. Can be %NULL if the list should not be freed. An example + * is: + * [[ + * g_list_free + * ]] + * @list_item_free: the name of a #GFunc function that frees the memory used + * by the items in the list or %NULL if the list items do not have to be + * freed. A simple example is: + * [[ + * g_free + * ]] + * + * A macro that adds code that converts a #GSList to a Python list. + * + */ +#define PYLIST_FROMGSLIST(py_list,list,item_convert_func,list_free,\ + list_item_free) \ + PYLIST_FROMGLIBLIST(GSList,g_slist,py_list,list,item_convert_func,\ + list_free,list_item_free) + +/** + * PYLIST_ASGLIBLIST + * @type: the type of the GLib list e.g. GList or GSList + * @prefix: the prefix of functions that manipulate a list of the type + * given by type e.g. g_list or g_slist + * + * A macro that creates a type specific code block to be used to convert a + * Python list to a GLib list (GList or GSList). The first two args of the + * macro are used to specify the type and list function prefix so that the + * type specific macros can be generated. + * + * The rest of the args are for the standard args for the type specific + * macro(s) created from this macro. + */ +#define PYLIST_ASGLIBLIST(type,prefix,py_list,list,check_func,\ + convert_func,child_free_func,errormsg,errorreturn) \ +G_STMT_START \ +{ \ + Py_ssize_t i, n_list; \ + GFunc glib_child_free_func = (GFunc)child_free_func; \ + \ + if (!(py_list = PySequence_Fast(py_list, ""))) { \ + errormsg; \ + return errorreturn; \ + } \ + n_list = PySequence_Fast_GET_SIZE(py_list); \ + for (i = 0; i < n_list; i++) { \ + PyObject *py_item = PySequence_Fast_GET_ITEM(py_list, i); \ + \ + if (!check_func) { \ + if (glib_child_free_func) \ + prefix##_foreach(list, glib_child_free_func, NULL); \ + prefix##_free(list); \ + Py_DECREF(py_list); \ + errormsg; \ + return errorreturn; \ + } \ + list = prefix##_prepend(list, convert_func); \ + }; \ + Py_DECREF(py_list); \ + list = prefix##_reverse(list); \ +} \ +G_STMT_END +/** + * PYLIST_ASGLIST + * @py_list: the Python list to be converted + * @list: the #GList list to be converted + * @check_func: the expression that takes a #PyObject* arg (must be named + * @py_item) and returns an int value indicating if the Python object matches + * the required list item type (0 - %False or 1 - %True). An example is: + * [[ + * (PyString_Check(py_item)||PyUnicode_Check(py_item)) + * ]] + * @convert_func: the function that takes a #PyObject* arg (must be named + * py_item) and returns a pointer to the converted list object. An example + * is: + * [[ + * pygobject_get(py_item) + * ]] + * @child_free_func: the name of a #GFunc function that frees a GLib list + * item or %NULL if the list item does not have to be freed. This function is + * used to help free the items in a partially created list if there is an + * error. An example is: + * [[ + * g_free + * ]] + * @errormsg: a function that sets up a Python error message. An example is: + * [[ + * PyErr_SetString(PyExc_TypeError, "strings must be a sequence of" "strings + * or unicode objects") + * ]] + * @errorreturn: the value to return if an error occurs, e.g.: + * [[ + * %NULL + * ]] + * + * A macro that creates code that converts a Python list to a #GList. The + * returned list must be freed using the appropriate list free function when + * it's no longer needed. If an error occurs the child_free_func is used to + * release the memory used by the list items and then the list memory is + * freed. + */ +#define PYLIST_ASGLIST(py_list,list,check_func,convert_func,child_free_func,\ + errormsg,errorreturn) \ + PYLIST_ASGLIBLIST(GList,g_list,py_list,list,check_func,convert_func,\ + child_free_func,errormsg,errorreturn) + +/** + * PYLIST_ASGSLIST + * @py_list: the Python list to be converted + * @list: the #GSList list to be converted + * @check_func: the expression that takes a #PyObject* arg (must be named + * @py_item) and returns an int value indicating if the Python object matches + * the required list item type (0 - %False or 1 - %True). An example is: + * [[ + * (PyString_Check(py_item)||PyUnicode_Check(py_item)) + * ]] + * @convert_func: the function that takes a #PyObject* arg (must be named + * py_item) and returns a pointer to the converted list object. An example + * is: + * [[ + * pygobject_get(py_item) + * ]] + * @child_free_func: the name of a #GFunc function that frees a GLib list + * item or %NULL if the list item does not have to be freed. This function is + * used to help free the items in a partially created list if there is an + * error. An example is: + * [[ + * g_free + * ]] + * @errormsg: a function that sets up a Python error message. An example is: + * [[ + * PyErr_SetString(PyExc_TypeError, "strings must be a sequence of" "strings + * or unicode objects") + * ]] + * @errorreturn: the value to return if an error occurs, e.g.: + * [[ + * %NULL + * ]] + * + * A macro that creates code that converts a Python list to a #GSList. The + * returned list must be freed using the appropriate list free function when + * it's no longer needed. If an error occurs the child_free_func is used to + * release the memory used by the list items and then the list memory is + * freed. + */ +#define PYLIST_ASGSLIST(py_list,list,check_func,convert_func,child_free_func,\ + errormsg,errorreturn) \ + PYLIST_ASGLIBLIST(GSList,g_slist,py_list,list,check_func,convert_func,\ + child_free_func,errormsg,errorreturn) + +#endif /* !_INSIDE_PYGOBJECT_ */ + +G_END_DECLS + +#endif /* !_PYGOBJECT_H_ */ diff --git a/plugins/python_wrapper/python_wrapper_common.c b/plugins/python_wrapper/python_wrapper_common.c new file mode 100644 index 0000000..e6a2d05 --- /dev/null +++ b/plugins/python_wrapper/python_wrapper_common.c @@ -0,0 +1,327 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo, Mathias Winterhalter (ToolsDevler) + * + * 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. + */ + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// I N C L U D E S +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "python_wrapper_common.h" + +#include <assert.h> +#include <stdio.h> + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#include "pygobject.h" +#pragma GCC diagnostic pop + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// D E C L A R A T I O N S +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * A cache to store the last result that has been returned by the Python code using CallPythonMethod + * (@see python_wrapper_common.h) + */ +static PyObject* __last_result; +static GPtrArray* plugin_map = NULL; + +static RemminaPluginService* remmina_plugin_service; + +const char* ATTR_NAME = "name"; +const char* ATTR_ICON_NAME = "icon_name"; +const char* ATTR_DESCRIPTION = "description"; +const char* ATTR_VERSION = "version"; +const char* ATTR_ICON_NAME_SSH = "icon_name_ssh"; +const char* ATTR_FEATURES = "features"; +const char* ATTR_BASIC_SETTINGS = "basic_settings"; +const char* ATTR_ADVANCED_SETTINGS = "advanced_settings"; +const char* ATTR_SSH_SETTING = "ssh_setting"; +const char* ATTR_EXPORT_HINTS = "export_hints"; +const char* ATTR_PREF_LABEL = "pref_label"; +const char* ATTR_INIT_ORDER = "init_order"; + +/** + * To prevent some memory related attacks or accidental allocation of an excessive amount of byes, this limit should + * always be used to check for a sane amount of bytes to allocate. + */ +static const int REASONABLE_LIMIT_FOR_MALLOC = 1024 * 1024; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// A P I +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +PyObject* python_wrapper_last_result(void) +{ + TRACE_CALL(__func__); + + return __last_result; +} + +PyObject* python_wrapper_last_result_set(PyObject* last_result) +{ + TRACE_CALL(__func__); + + return __last_result = last_result; +} + +gboolean python_wrapper_check_error(void) +{ + TRACE_CALL(__func__); + + if (PyErr_Occurred()) + { + PyErr_Print(); + return TRUE; + } + + return FALSE; +} + +void python_wrapper_log_method_call(PyObject* instance, const char* method) +{ + TRACE_CALL(__func__); + + assert(instance); + assert(method); + g_print("Python@%ld: %s.%s(...) -> %s\n", + PyObject_Hash(instance), + instance->ob_type->tp_name, + method, + PyUnicode_AsUTF8(PyObject_Str(python_wrapper_last_result()))); +} + +long python_wrapper_get_attribute_long(PyObject* instance, const char* attr_name, long def) +{ + TRACE_CALL(__func__); + + assert(instance); + assert(attr_name); + PyObject* attr = PyObject_GetAttrString(instance, attr_name); + if (attr && PyLong_Check(attr)) + { + return PyLong_AsLong(attr); + } + + return def; +} + +gboolean python_wrapper_check_attribute(PyObject* instance, const char* attr_name) +{ + TRACE_CALL(__func__); + + assert(instance); + assert(attr_name); + if (PyObject_HasAttrString(instance, attr_name)) + { + return TRUE; + } + + g_printerr("Python plugin instance is missing member: %s\n", attr_name); + return FALSE; +} + +void* python_wrapper_malloc(int bytes) +{ + TRACE_CALL(__func__); + + assert(bytes > 0); + assert(bytes <= REASONABLE_LIMIT_FOR_MALLOC); + + void* result = malloc(bytes); + + if (!result) + { + g_printerr("Unable to allocate %d bytes in memory!\n", bytes); + perror("malloc"); + } + + return result; +} + +char* python_wrapper_copy_string_from_python(PyObject* string, Py_ssize_t len) +{ + TRACE_CALL(__func__); + + char* result = NULL; + if (len <= 0 || string == NULL) + { + return NULL; + } + + const char* py_str = PyUnicode_AsUTF8(string); + if (py_str) + { + const int label_size = sizeof(char) * (len + 1); + result = (char*)python_wrapper_malloc(label_size); + result[len] = '\0'; + memcpy(result, py_str, len); + } + + return result; +} + +void python_wrapper_set_service(RemminaPluginService* service) +{ + remmina_plugin_service = service; +} + +RemminaPluginService* python_wrapper_get_service(void) +{ + return remmina_plugin_service; +} + +void python_wrapper_add_plugin(PyPlugin* plugin) +{ + TRACE_CALL(__func__); + + if (!plugin_map) + { + plugin_map = g_ptr_array_new(); + } + + PyPlugin* test = python_wrapper_get_plugin(plugin->generic_plugin->name); + if (test) + { + g_printerr("A plugin named '%s' has already been registered! Skipping...", plugin->generic_plugin->name); + } + else + { + g_ptr_array_add(plugin_map, plugin); + } +} + +RemminaTypeHint python_wrapper_to_generic(PyObject* field, gpointer* target) +{ + TRACE_CALL(__func__); + + if (PyUnicode_Check(field)) + { + Py_ssize_t len = PyUnicode_GetLength(field); + + if (len > 0) + { + *target = python_wrapper_copy_string_from_python(field, len); + } + else + { + *target = ""; + } + + return REMMINA_TYPEHINT_STRING; + } + else if (PyBool_Check(field)) + { + *target = python_wrapper_malloc(sizeof(long)); + long* long_target = (long*)target; + *long_target = PyLong_AsLong(field); + return REMMINA_TYPEHINT_BOOLEAN; + } + else if (PyLong_Check(field)) + { + *target = python_wrapper_malloc(sizeof(long)); + long* long_target = (long*)target; + *long_target = PyLong_AsLong(field); + return REMMINA_TYPEHINT_SIGNED; + } + else if (PyTuple_Check(field)) + { + Py_ssize_t len = PyTuple_Size(field); + if (len) + { + gpointer* dest = (gpointer*)python_wrapper_malloc(sizeof(gpointer) * (len + 1)); + memset(dest, 0, sizeof(gpointer) * (len + 1)); + + for (Py_ssize_t i = 0; i < len; ++i) + { + PyObject* item = PyTuple_GetItem(field, i); + python_wrapper_to_generic(item, dest + i); + } + + *target = dest; + } + return REMMINA_TYPEHINT_TUPLE; + } + + *target = NULL; + return REMMINA_TYPEHINT_UNDEFINED; +} + +PyPlugin* python_wrapper_get_plugin(const gchar* name) +{ + TRACE_CALL(__func__); + + assert(plugin_map); + assert(name); + + for (gint i = 0; i < plugin_map->len; ++i) + { + PyPlugin* plugin = (PyPlugin*)g_ptr_array_index(plugin_map, i); + if (plugin->generic_plugin && plugin->generic_plugin->name && g_str_equal(name, plugin->generic_plugin->name)) + { + return plugin; + } + } + + return NULL; +} + +PyPlugin* python_wrapper_get_plugin_by_protocol_widget(RemminaProtocolWidget* gp) +{ + TRACE_CALL(__func__); + + assert(plugin_map); + assert(gp); + + const gchar* name = python_wrapper_get_service()->protocol_widget_get_name(gp); + if (!name) { + return NULL; + } + + return python_wrapper_get_plugin(name); +} + +void init_pygobject() +{ + pygobject_init(-1, -1, -1); +} + +GtkWidget* new_pywidget(GObject* obj) +{ + return (GtkWidget*)pygobject_new(obj); +} + +GtkWidget* get_pywidget(PyObject* obj) +{ + return (GtkWidget*)pygobject_get(obj); +} diff --git a/plugins/python_wrapper/python_wrapper_common.h b/plugins/python_wrapper/python_wrapper_common.h new file mode 100644 index 0000000..8c7a167 --- /dev/null +++ b/plugins/python_wrapper/python_wrapper_common.h @@ -0,0 +1,295 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo, Mathias Winterhalter (ToolsDevler) + * + * 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. + */ + +/** + * @file python_wrapper_common.h + * + * @brief Contains functions and constants that are commonly used throughout the Python plugin implementation. + * + * @details These functions should not be used outside of the Python plugin implementation, since everything is intended + * to be used with the Python engine. + */ + +#pragma once + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// I N C L U D E S +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "common/remmina_plugin.h" + +#include <gtk/gtk.h> + +#include <Python.h> +#include <glib.h> +#include <Python.h> +#include <structmember.h> + +#include "remmina/plugin.h" +#include "config.h" + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// D E C L A R A T I O N S +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +G_BEGIN_DECLS + +// - Attribute names + +extern const char* ATTR_NAME; +extern const char* ATTR_ICON_NAME; +extern const char* ATTR_DESCRIPTION; +extern const char* ATTR_VERSION; +extern const char* ATTR_ICON_NAME_SSH; +extern const char* ATTR_FEATURES; +extern const char* ATTR_BASIC_SETTINGS; +extern const char* ATTR_ADVANCED_SETTINGS; +extern const char* ATTR_SSH_SETTING; +extern const char* ATTR_EXPORT_HINTS; +extern const char* ATTR_PREF_LABEL; +extern const char* ATTR_INIT_ORDER; + +// You can enable this for debuggin purposes or specify it in the build. +// #define WITH_PYTHON_TRACE_CALLS + +/** + * If WITH_PYTHON_TRACE_CALLS is defined, it logs the calls to the Python code and errors in case. + */ +#ifdef WITH_PYTHON_TRACE_CALLS +#define CallPythonMethod(instance, name, params, ...) \ + python_wrapper_last_result_set(PyObject_CallMethod(instance, name, params, ##__VA_ARGS__)); \ + python_wrapper_log_method_call(instance, name); \ + python_wrapper_check_error() +#else +/** + * If WITH_TRACE_CALL is not defined, it still logs errors but doesn't print the call anymore. + */ +#define CallPythonMethod(instance, name, params, ...) \ + PyObject_CallMethod(instance, name, params, ##__VA_ARGS__); \ + python_wrapper_check_error() +#endif // WITH_PYTHON_TRACE_CALLS + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// T Y P E S +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * @brief The Python abstraction of the protocol widget struct. + * + * @details This struct is responsible to provide the same accessibility to the protocol widget for Python as for + * native plugins. + */ +typedef struct +{ + PyObject_HEAD + RemminaProtocolWidget* gp; +} PyRemminaProtocolWidget; + +/** + * @brief Maps an instance of a Python plugin to a Remmina one. + * + * @details This is used to map a Python plugin instance to the Remmina plugin one. Also instance specific data as the + * protocol widget are stored in this struct. + */ +typedef struct +{ + RemminaProtocolPlugin* protocol_plugin; + RemminaFilePlugin* file_plugin; + RemminaSecretPlugin* secret_plugin; + RemminaToolPlugin* tool_plugin; + RemminaEntryPlugin* entry_plugin; + RemminaPrefPlugin* pref_plugin; + RemminaPlugin* generic_plugin; + PyRemminaProtocolWidget* gp; + PyObject* instance; +} PyPlugin; + +/** + * A struct used to communicate data between Python and C without strict data type. + */ +typedef struct +{ + PyObject_HEAD; + RemminaTypeHint type_hint; + gpointer raw; +} PyGeneric; + +/** + * Checks if self is set and returns NULL if not. + */ +#define SELF_CHECK() if (!self) { \ + g_printerr("[%s:%d]: self is null!\n", __FILE__, __LINE__); \ + PyErr_SetString(PyExc_RuntimeError, "Method is not called from an instance (self is null)!"); \ + return NULL; \ + } + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// A P I +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Creates a new instance of PyGeneric. + */ +PyGeneric* python_wrapper_generic_new(void); + +/** + * Registers the given plugin if no other plugin with the same name has been already registered. + */ +void python_wrapper_add_plugin(PyPlugin* plugin); + +/** + * Sets the pointer to the plugin service of Remmina. + */ +void python_wrapper_set_service(RemminaPluginService* service); + +/** + * Gets the pointer to the plugin service of Remmina. + */ +RemminaPluginService* python_wrapper_get_service(void); + +/** + * Extracts data from a PyObject instance to a generic pointer and returns a type hint if it could be determined. + */ +RemminaTypeHint python_wrapper_to_generic(PyObject* field, gpointer* target); + +/** + * Gets the result of the last python method call. + */ +PyObject* python_wrapper_last_result(void); + +/** + * @brief Sets the result of the last python method call. + * + * @return Returns the passed result (it's done to be compatible with the CallPythonMethod macro). + */ +PyObject* python_wrapper_last_result_set(PyObject* result); + +/** + * @brief Prints a log message to inform the user a python message has been called. + * + * @detail This method is called from the CALL_PYTHON macro if WITH_PYTHON_TRACE_CALLS is defined. + * + * @param instance The instance that contains the called method. + * @param method The name of the method called. + */ +void python_wrapper_log_method_call(PyObject* instance, const char* method); + +/** + * @brief Checks if an error has occurred and prints it. + * + * @return Returns TRUE if an error has occurred. + */ +gboolean python_wrapper_check_error(void); + +/** + * @brief Gets the attribute as long value. + * + * @param instance The instance of the object to get the attribute. + * @param constant_name The name of the attribute to get. + * @param def The value to return if the attribute doesn't exist or is not set. + * + * @return The value attribute as long. + */ +long python_wrapper_get_attribute_long(PyObject* instance, const char* attr_name, long def); + +/** + * @brief Checks if a given attribute exists. + * + * @param instance The object to check for the attribute. + * @param attr_name The name of the attribute to check. + * + * @return Returns TRUE if the attribute exists. + */ +gboolean python_wrapper_check_attribute(PyObject* instance, const char* attr_name); + +/** + * @brief Allocates memory and checks for errors before returning. + * + * @param bytes Amount of bytes to allocate. + * + * @return Address to the allocated memory. + */ +void* python_wrapper_malloc(int bytes); + +/** + * @biref Copies a string from a Python object to a new point in memory. + * + * @param string The python object, containing the string to copy. + * @param len The length of the string to copy. + * + * @return A char pointer to the new copy of the string. + */ +char* python_wrapper_copy_string_from_python(PyObject* string, Py_ssize_t len); + +/** + * @brief Tries to find the Python plugin matching to the given instance of RemminaPlugin. + * + * @param plugin_map An array of PyPlugin pointers to search. + * @param instance The RemminaPlugin instance to find the correct PyPlugin instance for. + * + * @return A pointer to a PyPlugin instance if successful. Otherwise NULL is returned. + */ +PyPlugin* python_wrapper_get_plugin(const gchar* name); + +/** + * @brief Tries to find the Python plugin matching to the given instance of RemminaPlugin. + * + * @param plugin_map An array of PyPlugin pointers to search. + * @param instance The RemminaPlugin instance to find the correct PyPlugin instance for. + * + * @return A pointer to a PyPlugin instance if successful. Otherwise NULL is returned. + */ +PyPlugin* python_wrapper_get_plugin_by_protocol_widget(RemminaProtocolWidget* gp); + +/** + * Creates a new GtkWidget + * @param obj + * @return + */ +GtkWidget* new_pywidget(GObject* obj); + +/** + * Extracts a GtkWidget from a PyObject instance. + * @param obj + * @return + */ +GtkWidget* get_pywidget(PyObject* obj); + +/** + * Initializes the pygobject library. This needs to be called before any Python plugin is being initialized. + */ +void init_pygobject(void); + +G_END_DECLS diff --git a/plugins/python_wrapper/python_wrapper_entry.c b/plugins/python_wrapper/python_wrapper_entry.c new file mode 100644 index 0000000..8865863 --- /dev/null +++ b/plugins/python_wrapper/python_wrapper_entry.c @@ -0,0 +1,100 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo, Mathias Winterhalter (ToolsDevler) + * + * 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. + */ + +/** + * @file python_wrapper_entry.c + * @brief Contains the wiring of a Python pluing based on RemminaPluginProtocol. + * @author Mathias Winterhalter + * @date 07.04.2021 + */ + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// I N C L U D E S +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "python_wrapper_entry.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// D E C L A R A T I O N S +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// A P I +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +void python_wrapper_entry_init(void) +{ + TRACE_CALL(__func__); +} + +void python_wrapper_entry_entry_func_wrapper(RemminaEntryPlugin* instance) +{ + TRACE_CALL(__func__); + + PyPlugin* plugin = python_wrapper_get_plugin(instance->name); + if (plugin) + { + CallPythonMethod(plugin->instance, "entry_func", NULL); + } +} + +RemminaPlugin* python_wrapper_create_entry_plugin(PyPlugin* plugin) +{ + TRACE_CALL(__func__); + + PyObject* instance = plugin->instance; + + if (!python_wrapper_check_attribute(instance, ATTR_NAME) + || !python_wrapper_check_attribute(instance, ATTR_VERSION) + || !python_wrapper_check_attribute(instance, ATTR_DESCRIPTION)) + { + g_printerr("Unable to create entry plugin. Aborting!\n"); + return NULL; + } + + RemminaEntryPlugin* remmina_plugin = (RemminaEntryPlugin*)python_wrapper_malloc(sizeof(RemminaEntryPlugin)); + + remmina_plugin->type = REMMINA_PLUGIN_TYPE_ENTRY; + remmina_plugin->domain = GETTEXT_PACKAGE; + remmina_plugin->name = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_NAME)); + remmina_plugin->version = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_VERSION)); + remmina_plugin->description = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_DESCRIPTION)); + remmina_plugin->entry_func = python_wrapper_entry_entry_func_wrapper; + + plugin->entry_plugin = remmina_plugin; + plugin->generic_plugin = (RemminaPlugin*)remmina_plugin; + + python_wrapper_add_plugin(plugin); + + return (RemminaPlugin*)remmina_plugin; +} diff --git a/plugins/python_wrapper/python_wrapper_entry.h b/plugins/python_wrapper/python_wrapper_entry.h new file mode 100644 index 0000000..396f7be --- /dev/null +++ b/plugins/python_wrapper/python_wrapper_entry.h @@ -0,0 +1,63 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo, Mathias Winterhalter (ToolsDevler) + * + * 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. + */ + +/** + * @file python_wrapper_entry.h + * + * @brief Contains the specialisation of RemminaPluginEntry plugins in Python. + */ + +#pragma once + +#include "python_wrapper_common.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// A P I +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +G_BEGIN_DECLS + +/** + * Initializes the Python plugin specialisation for entry plugins. + */ +void python_wrapper_entry_init(void); + +/** + * @brief Creates a new instance of the RemminaPluginEntry, initializes its members and references the wrapper + * functions. + * @param instance The instance of the Python plugin. + * @return Returns a new instance of the RemminaPlugin (must be freed!). + */ +RemminaPlugin* python_wrapper_create_entry_plugin(PyPlugin* instance); + +G_END_DECLS diff --git a/plugins/python_wrapper/python_wrapper_file.c b/plugins/python_wrapper/python_wrapper_file.c new file mode 100644 index 0000000..e69c9b0 --- /dev/null +++ b/plugins/python_wrapper/python_wrapper_file.c @@ -0,0 +1,160 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo, Mathias Winterhalter (ToolsDevler) + * + * 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. + */ + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// I N C L U D E S +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "python_wrapper_common.h" +#include "python_wrapper_file.h" +#include "python_wrapper_remmina_file.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// D E C L A R A T I O N S +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// A P I +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +void python_wrapper_file_init(void) +{ + TRACE_CALL(__func__); +} + +gboolean python_wrapper_file_import_test_func_wrapper(RemminaFilePlugin* instance, const gchar* from_file) +{ + TRACE_CALL(__func__); + + PyObject* result = NULL; + + PyPlugin* plugin = python_wrapper_get_plugin(instance->name); + + if (plugin) + { + result = CallPythonMethod(plugin->instance, "import_test_func", "s", from_file); + } + + return result == Py_None || result != Py_False; +} + +RemminaFile* python_wrapper_file_import_func_wrapper(RemminaFilePlugin* instance, const gchar* from_file) +{ + TRACE_CALL(__func__); + + PyObject* result = NULL; + + PyPlugin* plugin = python_wrapper_get_plugin(instance->name); + if (!plugin) + { + return NULL; + } + + result = CallPythonMethod(plugin->instance, "import_func", "s", from_file); + + if (result == Py_None || result == Py_False) + { + return NULL; + } + + return ((PyRemminaFile*)result)->file; +} + +gboolean python_wrapper_file_export_test_func_wrapper(RemminaFilePlugin* instance, RemminaFile* file) +{ + TRACE_CALL(__func__); + + PyObject* result = NULL; + + PyPlugin* plugin = python_wrapper_get_plugin(instance->name); + if (plugin) + { + result = CallPythonMethod(plugin->instance, + "export_test_func", + "O", + python_wrapper_remmina_file_to_python(file)); + } + + return result == Py_None || result != Py_False; +} + +gboolean +python_wrapper_file_export_func_wrapper(RemminaFilePlugin* instance, RemminaFile* file, const gchar* to_file) +{ + TRACE_CALL(__func__); + + PyObject* result = NULL; + + PyPlugin* plugin = python_wrapper_get_plugin(instance->name); + if (plugin) + { + result = CallPythonMethod(plugin->instance, "export_func", "s", to_file); + } + + return result == Py_None || result != Py_False; +} + +RemminaPlugin* python_wrapper_create_file_plugin(PyPlugin* plugin) +{ + TRACE_CALL(__func__); + + PyObject* instance = plugin->instance; + Py_IncRef(instance); + + if (!python_wrapper_check_attribute(instance, ATTR_NAME)) + { + g_printerr("Unable to create file plugin. Aborting!\n"); + return NULL; + } + + RemminaFilePlugin* remmina_plugin = (RemminaFilePlugin*)python_wrapper_malloc(sizeof(RemminaFilePlugin)); + + remmina_plugin->type = REMMINA_PLUGIN_TYPE_FILE; + remmina_plugin->domain = GETTEXT_PACKAGE; + remmina_plugin->name = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_NAME)); + remmina_plugin->version = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_VERSION)); + remmina_plugin->description = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_DESCRIPTION)); + remmina_plugin->export_hints = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_EXPORT_HINTS)); + + remmina_plugin->import_test_func = python_wrapper_file_import_test_func_wrapper; + remmina_plugin->import_func = python_wrapper_file_import_func_wrapper; + remmina_plugin->export_test_func = python_wrapper_file_export_test_func_wrapper; + remmina_plugin->export_func = python_wrapper_file_export_func_wrapper; + + plugin->file_plugin = remmina_plugin; + plugin->generic_plugin = (RemminaPlugin*)remmina_plugin; + + python_wrapper_add_plugin(plugin); + + return (RemminaPlugin*)remmina_plugin; +} diff --git a/plugins/python_wrapper/python_wrapper_file.h b/plugins/python_wrapper/python_wrapper_file.h new file mode 100644 index 0000000..035712e --- /dev/null +++ b/plugins/python_wrapper/python_wrapper_file.h @@ -0,0 +1,63 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo, Mathias Winterhalter (ToolsDevler) + * + * 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. + */ + +/** + * @file python_wrapper_file.h + * + * @brief Contains the specialisation of RemminaPluginFile plugins in Python. + */ + +#pragma once + +#include "python_wrapper_common.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// A P I +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +G_BEGIN_DECLS + +/** + * Initializes the Python plugin specialisation for file plugins. + */ +void python_wrapper_file_init(void); + +/** + * @brief Creates a new instance of the RemminaPluginFile, initializes its members and references the wrapper + * functions. + * @param instance The instance of the Python plugin. + * @return Returns a new instance of the RemminaPlugin (must be freed!). + */ +RemminaPlugin* python_wrapper_create_file_plugin(PyPlugin* instance); + +G_END_DECLS diff --git a/plugins/python_wrapper/python_wrapper_plugin.c b/plugins/python_wrapper/python_wrapper_plugin.c new file mode 100644 index 0000000..bd57ffc --- /dev/null +++ b/plugins/python_wrapper/python_wrapper_plugin.c @@ -0,0 +1,220 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo + * Copyright (C) 2023-2024 Hiroyuki Tanaka, Sunil Bhat + * + * 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. + */ + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// I N C L U D E S +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#include <gtk/gtk.h> +#ifdef GDK_WINDOWING_X11 +#include <gdk/gdkx.h> +#elif defined(GDK_WINDOWING_WAYLAND) +#include <gdk/gdkwayland.h> +#endif +#include "config.h" +#include "remmina/plugin.h" +#include "remmina/remmina_trace_calls.h" +#include "python_wrapper_common.h" +#include "python_wrapper_plugin.h" +#include "python_wrapper_remmina.h" +#include "python_wrapper_protocol_widget.h" + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// D E C L A R A T I O N S +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * An null terminated array of commands that are executed after the initialization of the Python engine. Every entry + * represents a line of Python code. + */ + +static const gchar* python_wrapper_supported_extensions[] = {"py", NULL}; + +static RemminaLanguageWrapperPlugin remmina_python_wrapper = +{ + REMMINA_PLUGIN_TYPE_LANGUAGE_WRAPPER, // Type + "Python Wrapper", // Name + "Enables Python plugins for Remmina", // Description + GETTEXT_PACKAGE, // Translation domain + "Python Wrapper for Remmina v0.1", // Version number + python_wrapper_supported_extensions, // Supported extentions + python_wrapper_init, // Plugin initialization + python_wrapper_load, // Plugin load Python file +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// U T I L S +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * @brief Extracts the filename without extension from a path. + * + * @param in The string to extract the filename from + * @param out The resulting filename without extension (must point to allocated memory). + * + * @return The length of the filename extracted. + */ +static int basename_no_ext(const char* in, char** out) +{ + TRACE_CALL(__func__); + + assert(in); + assert(out); + + const char* base = strrchr(in, '/'); + if (base) + { + base++; + } else { + return 0; + } + + const char* base_end = strrchr(base, '.'); + if (!base_end) + { + base_end = base + strlen(base); + } + + const int length = base_end - base; + const int memsize = sizeof(char*) * ((length) + 1); + + *out = (char*)python_wrapper_malloc(memsize); + + memset(*out, 0, memsize); + strncpy(*out, base, length); + (*out)[length] = '\0'; + + return length; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// A P I +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + +gboolean python_wrapper_init(RemminaLanguageWrapperPlugin* plugin) +{ + TRACE_CALL(__func__); + assert(plugin); + return TRUE; +} + +gboolean python_wrapper_load(RemminaLanguageWrapperPlugin* plugin, const char* name) +{ + TRACE_CALL(__func__); + + assert(plugin); + assert(name); + + + char* filename = NULL; + if (basename_no_ext(name, &filename) == 0) + { + g_printerr("[%s:%d]: Can not extract filename from '%s'!\n", __FILE__, __LINE__, name); + return FALSE; + } + + PyObject* plugin_name = PyUnicode_DecodeFSDefault(filename); + + if (!plugin_name) + { + free(filename); + g_printerr("[%s:%d]: Error converting plugin filename to PyUnicode!\n", __FILE__, __LINE__); + return FALSE; + } + + wchar_t* program_name = NULL; + Py_ssize_t len = PyUnicode_AsWideChar(plugin_name, program_name, 0); + if (len <= 0) + { + free(filename); + g_printerr("[%s:%d]: Failed allocating %lu bytes!\n", __FILE__, __LINE__, (sizeof(wchar_t) * len)); + return FALSE; + } + + program_name = (wchar_t*)python_wrapper_malloc(sizeof(wchar_t) * len); + if (!program_name) + { + free(filename); + g_printerr("[%s:%d]: Failed allocating %lu bytes!\n", __FILE__, __LINE__, (sizeof(wchar_t) * len)); + return FALSE; + } + + PyUnicode_AsWideChar(plugin_name, program_name, len); + + PySys_SetArgv(1, &program_name); + + if (PyImport_Import(plugin_name)) + { + free(filename); + return TRUE; + } + + g_print("[%s:%d]: Failed to load python plugin file: '%s'\n", __FILE__, __LINE__, name); + PyErr_Print(); + free(filename); + + return FALSE; +} + + +G_MODULE_EXPORT gboolean remmina_plugin_entry(RemminaPluginService *service) +{ + TRACE_CALL(__func__); + python_wrapper_set_service(service); + + python_wrapper_module_init(); + Py_InitializeEx(0); + + gchar* plugin_dir; + plugin_dir = g_build_path("/", g_get_user_config_dir(), "remmina", "plugins", NULL); + + gchar* python_command = g_strdup_printf("sys.path.append('%s')", plugin_dir); + + char* python_init_commands[] = { "import sys", python_command, "sys.path.append('" REMMINA_RUNTIME_PLUGINDIR "')", NULL }; // Sentinel}; + + for (char** ptr = python_init_commands; *ptr; ++ptr) + { + PyRun_SimpleString(*ptr); + } + + g_free(python_command); + g_free(plugin_dir); + + python_wrapper_protocol_widget_init(); + + service->register_plugin((RemminaPlugin*)&remmina_python_wrapper); + + return TRUE; +} diff --git a/plugins/python_wrapper/python_wrapper_plugin.h b/plugins/python_wrapper/python_wrapper_plugin.h new file mode 100644 index 0000000..5678088 --- /dev/null +++ b/plugins/python_wrapper/python_wrapper_plugin.h @@ -0,0 +1,80 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo + * + * 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. + */ + +/** + * @file python_wrapper.h + * + * @brief Declares the interface between the Python plugin implementation and Remmina covering the initialization of + * the implementation and the load function, that allows Remmina to load plugins into the application. + * + * @details When Remmina discovers Python scripts in the plugin root folder the plugin manager passes the path to the + * Python plugin loader. There it gets executed and the plugin classes get mapped to "real" Remmina plugin + * instances. + * + * For the communication between Remmina and Python the python module called 'remmina' is initialized and + * loaded into the environment of the plugin script (@see python_wrapper_module.c). + * + * @see http://www.remmina.org/wp for more information. + */ + +#pragma once + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// I N C L U D E +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "remmina/plugin.h" + +G_BEGIN_DECLS + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// A P I +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * @brief Initializes the Python plugin loaders. + * @details This does not load any plugins but initializes the implementation (e.g. globals and the Python engine). + */ +gboolean python_wrapper_init(RemminaLanguageWrapperPlugin* plugin); + +/** + * @brief Loads a plugin from the Remmina plugin folder with the given name. + * + * @param service The instance of the service providing an API between Remmina and its plugins. + * @param filename The filename of the plugin to load. + * + * @return TRUE on success, FALSE otherwise. + */ +gboolean python_wrapper_load(RemminaLanguageWrapperPlugin* plugin, const gchar* filename); + +G_END_DECLS diff --git a/plugins/python_wrapper/python_wrapper_pref.c b/plugins/python_wrapper/python_wrapper_pref.c new file mode 100644 index 0000000..84a76db --- /dev/null +++ b/plugins/python_wrapper/python_wrapper_pref.c @@ -0,0 +1,104 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo, Mathias Winterhalter (ToolsDevler) + * + * 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. + */ + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// I N C L U D E S +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "python_wrapper_common.h" +#include "python_wrapper_pref.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// D E C L A R A T I O N S +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// A P I +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +void python_wrapper_pref_init(void) +{ + TRACE_CALL(__func__); + +} + +/** + * @brief + */ +GtkWidget* python_wrapper_pref_get_pref_body_wrapper(RemminaPrefPlugin* instance) +{ + TRACE_CALL(__func__); + + PyPlugin* plugin = python_wrapper_get_plugin(instance->name); + + PyObject* result = CallPythonMethod(plugin->instance, "get_pref_body", NULL, NULL); + if (result == Py_None || result == NULL) + { + return NULL; + } + + return get_pywidget(result); +} + +RemminaPlugin* python_wrapper_create_pref_plugin(PyPlugin* plugin) +{ + TRACE_CALL(__func__); + + PyObject* instance = plugin->instance; + + if (!python_wrapper_check_attribute(instance, ATTR_NAME) + || !python_wrapper_check_attribute(instance, ATTR_VERSION) + || !python_wrapper_check_attribute(instance, ATTR_DESCRIPTION) + || !python_wrapper_check_attribute(instance, ATTR_PREF_LABEL)) + { + g_printerr("Unable to create pref plugin. Aborting!\n"); + return NULL; + } + + RemminaPrefPlugin* remmina_plugin = (RemminaPrefPlugin*)python_wrapper_malloc(sizeof(RemminaPrefPlugin)); + + remmina_plugin->type = REMMINA_PLUGIN_TYPE_PREF; + remmina_plugin->domain = GETTEXT_PACKAGE; + remmina_plugin->name = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_NAME)); + remmina_plugin->version = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_VERSION)); + remmina_plugin->description = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_DESCRIPTION)); + remmina_plugin->pref_label = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_PREF_LABEL)); + remmina_plugin->get_pref_body = python_wrapper_pref_get_pref_body_wrapper; + + plugin->pref_plugin = remmina_plugin; + plugin->generic_plugin = (RemminaPlugin*)remmina_plugin; + + python_wrapper_add_plugin(plugin); + + return (RemminaPlugin*)remmina_plugin; +} diff --git a/plugins/python_wrapper/python_wrapper_pref.h b/plugins/python_wrapper/python_wrapper_pref.h new file mode 100644 index 0000000..9dadb96 --- /dev/null +++ b/plugins/python_wrapper/python_wrapper_pref.h @@ -0,0 +1,63 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo, Mathias Winterhalter (ToolsDevler) + * + * 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. + */ + +/** + * @file python_wrapper_pref.h + * + * @brief Contains the specialisation of RemminaPluginFile plugins in Python. + */ + +#pragma once + +#include "python_wrapper_common.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// A P I +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +G_BEGIN_DECLS + +/** + * Initializes the Python plugin specialisation for preferences plugins. + */ +void python_wrapper_pref_init(void); + +/** + * @brief Creates a new instance of the RemminaPluginPref, initializes its members and references the wrapper + * functions. + * @param instance The instance of the Python plugin. + * @return Returns a new instance of the RemminaPlugin (must be freed!). + */ +RemminaPlugin* python_wrapper_create_pref_plugin(PyPlugin* instance); + +G_END_DECLS diff --git a/plugins/python_wrapper/python_wrapper_protocol.c b/plugins/python_wrapper/python_wrapper_protocol.c new file mode 100644 index 0000000..64225aa --- /dev/null +++ b/plugins/python_wrapper/python_wrapper_protocol.c @@ -0,0 +1,315 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo, Mathias Winterhalter (ToolsDevler) + * + * 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. + */ + +/** + * @file python_wrapper_common.c + * @brief + * @author Mathias Winterhalter + * @date 07.04.2021 + */ + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// I N C L U D E S +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "python_wrapper_common.h" +#include "python_wrapper_protocol.h" +#include "python_wrapper_remmina.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// D E C L A R A T I O N S +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// A P I +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +void python_wrapper_protocol_init(void) +{ + TRACE_CALL(__func__); +} + +void remmina_protocol_init_wrapper(RemminaProtocolWidget* gp) +{ + TRACE_CALL(__func__); + PyPlugin* py_plugin = python_wrapper_get_plugin_by_protocol_widget(gp); + py_plugin->gp->gp = gp; + CallPythonMethod(py_plugin->instance, "init", "O", py_plugin->gp); +} + +gboolean remmina_protocol_open_connection_wrapper(RemminaProtocolWidget* gp) +{ + TRACE_CALL(__func__); + PyPlugin* py_plugin = python_wrapper_get_plugin_by_protocol_widget(gp); + if (py_plugin) + { + PyObject* result = CallPythonMethod(py_plugin->instance, "open_connection", "O", py_plugin->gp); + return result == Py_True; + } + else + { + return gtk_false(); + } +} + +gboolean remmina_protocol_close_connection_wrapper(RemminaProtocolWidget* gp) +{ + TRACE_CALL(__func__); + PyPlugin* py_plugin = python_wrapper_get_plugin_by_protocol_widget(gp); + PyObject* result = CallPythonMethod(py_plugin->instance, "close_connection", "O", py_plugin->gp); + return result == Py_True; +} + +gboolean remmina_protocol_query_feature_wrapper(RemminaProtocolWidget* gp, + const RemminaProtocolFeature* feature) +{ + TRACE_CALL(__func__); + PyPlugin* py_plugin = python_wrapper_get_plugin_by_protocol_widget(gp); + PyRemminaProtocolFeature* pyFeature = python_wrapper_protocol_feature_new(); + pyFeature->type = (gint)feature->type; + pyFeature->id = feature->id; + pyFeature->opt1 = python_wrapper_generic_new(); + pyFeature->opt1->raw = feature->opt1; + pyFeature->opt2 = python_wrapper_generic_new(); + pyFeature->opt2->raw = feature->opt2; + pyFeature->opt3 = python_wrapper_generic_new(); + pyFeature->opt3->raw = feature->opt3; + + PyObject* result = CallPythonMethod(py_plugin->instance, "query_feature", "OO", py_plugin->gp, pyFeature); + Py_DecRef((PyObject*)pyFeature); + Py_DecRef((PyObject*)pyFeature->opt1); + Py_DecRef((PyObject*)pyFeature->opt2); + Py_DecRef((PyObject*)pyFeature->opt3); + return result == Py_True; +} + +void remmina_protocol_call_feature_wrapper(RemminaProtocolWidget* gp, const RemminaProtocolFeature* feature) +{ + TRACE_CALL(__func__); + PyPlugin* py_plugin = python_wrapper_get_plugin_by_protocol_widget(gp); + PyRemminaProtocolFeature* pyFeature = python_wrapper_protocol_feature_new(); + pyFeature->type = (gint)feature->type; + pyFeature->id = feature->id; + pyFeature->opt1 = python_wrapper_generic_new(); + pyFeature->opt1->raw = feature->opt1; + pyFeature->opt1->type_hint = feature->opt1_type_hint; + pyFeature->opt2 = python_wrapper_generic_new(); + pyFeature->opt2->raw = feature->opt2; + pyFeature->opt2->type_hint = feature->opt2_type_hint; + pyFeature->opt3 = python_wrapper_generic_new(); + pyFeature->opt3->raw = feature->opt3; + pyFeature->opt3->type_hint = feature->opt3_type_hint; + + CallPythonMethod(py_plugin->instance, "call_feature", "OO", py_plugin->gp, pyFeature); + Py_DecRef((PyObject*)pyFeature); + Py_DecRef((PyObject*)pyFeature->opt1); + Py_DecRef((PyObject*)pyFeature->opt2); + Py_DecRef((PyObject*)pyFeature->opt3); +} + +void remmina_protocol_send_keytrokes_wrapper(RemminaProtocolWidget* gp, + const guint keystrokes[], + const gint keylen) +{ + TRACE_CALL(__func__); + PyPlugin* py_plugin = python_wrapper_get_plugin_by_protocol_widget(gp); + PyObject* obj = PyList_New(keylen); + Py_IncRef(obj); + for (int i = 0; i < keylen; ++i) + { + PyList_SetItem(obj, i, PyLong_FromLong(keystrokes[i])); + } + CallPythonMethod(py_plugin->instance, "send_keystrokes", "OO", py_plugin->gp, obj); + Py_DecRef(obj); +} + +gboolean remmina_protocol_get_plugin_screenshot_wrapper(RemminaProtocolWidget* gp, + RemminaPluginScreenshotData* rpsd) +{ + TRACE_CALL(__func__); + + PyPlugin* py_plugin = python_wrapper_get_plugin_by_protocol_widget(gp); + PyRemminaPluginScreenshotData* data = python_wrapper_screenshot_data_new(); + Py_IncRef((PyObject*)data); + PyObject* result = CallPythonMethod(py_plugin->instance, "get_plugin_screenshot", "OO", py_plugin->gp, data); + if (result == Py_True) + { + if (!PyByteArray_Check((PyObject*)data->buffer)) + { + g_printerr("Unable to parse screenshot data. 'buffer' needs to be an byte array!"); + return 0; + } + Py_ssize_t buffer_len = PyByteArray_Size((PyObject*)data->buffer); + + // Is being freed by Remmina! + rpsd->buffer = (unsigned char*)python_wrapper_malloc(sizeof(unsigned char) * buffer_len); + if (!rpsd->buffer) + { + return 0; + } + memcpy(rpsd->buffer, PyByteArray_AsString((PyObject*)data->buffer), sizeof(unsigned char) * buffer_len); + rpsd->bytesPerPixel = data->bytesPerPixel; + rpsd->bitsPerPixel = data->bitsPerPixel; + rpsd->height = data->height; + rpsd->width = data->width; + } + Py_DecRef((PyObject*)data->buffer); + Py_DecRef((PyObject*)data); + return result == Py_True; +} + +gboolean remmina_protocol_map_event_wrapper(RemminaProtocolWidget* gp) +{ + PyPlugin* plugin = python_wrapper_get_plugin_by_protocol_widget(gp); + PyObject* result = CallPythonMethod(plugin->instance, "map_event", "O", plugin->gp); + return PyBool_Check(result) && result == Py_True; +} + +gboolean remmina_protocol_unmap_event_wrapper(RemminaProtocolWidget* gp) +{ + PyPlugin* plugin = python_wrapper_get_plugin_by_protocol_widget(gp); + PyObject* result = CallPythonMethod(plugin->instance, "unmap_event", "O", plugin->gp); + return PyBool_Check(result) && result == Py_True; +} + +RemminaPlugin* python_wrapper_create_protocol_plugin(PyPlugin* plugin) +{ + PyObject* instance = plugin->instance; + + if (!python_wrapper_check_attribute(instance, ATTR_ICON_NAME_SSH) + || !python_wrapper_check_attribute(instance, ATTR_ICON_NAME) + || !python_wrapper_check_attribute(instance, ATTR_FEATURES) + || !python_wrapper_check_attribute(instance, ATTR_BASIC_SETTINGS) + || !python_wrapper_check_attribute(instance, ATTR_ADVANCED_SETTINGS) + || !python_wrapper_check_attribute(instance, ATTR_SSH_SETTING)) + { + g_printerr("Unable to create protocol plugin. Aborting!\n"); + return NULL; + } + + RemminaProtocolPlugin* remmina_plugin = (RemminaProtocolPlugin*)python_wrapper_malloc(sizeof(RemminaProtocolPlugin)); + + remmina_plugin->type = REMMINA_PLUGIN_TYPE_PROTOCOL; + remmina_plugin->domain = GETTEXT_PACKAGE; + remmina_plugin->basic_settings = NULL; + remmina_plugin->advanced_settings = NULL; + remmina_plugin->features = NULL; + + remmina_plugin->name = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_NAME)); + remmina_plugin->description = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_DESCRIPTION)); + remmina_plugin->version = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_VERSION)); + remmina_plugin->icon_name = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_ICON_NAME)); + remmina_plugin->icon_name_ssh = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_ICON_NAME_SSH)); + + PyObject* list = PyObject_GetAttrString(instance, "basic_settings"); + Py_ssize_t len = PyList_Size(list); + if (len) + { + RemminaProtocolSetting* basic_settings = (RemminaProtocolSetting*)python_wrapper_malloc( + sizeof(RemminaProtocolSetting) * (len + 1)); + memset(basic_settings, 0, sizeof(RemminaProtocolSetting) * (len + 1)); + + for (Py_ssize_t i = 0; i < len; ++i) + { + RemminaProtocolSetting* dest = basic_settings + i; + python_wrapper_to_protocol_setting(dest, PyList_GetItem(list, i)); + } + RemminaProtocolSetting* dest = basic_settings + len; + dest->type = REMMINA_PROTOCOL_SETTING_TYPE_END; + remmina_plugin->basic_settings = basic_settings; + } + + list = PyObject_GetAttrString(instance, "advanced_settings"); + len = PyList_Size(list); + if (len) + { + RemminaProtocolSetting* advanced_settings = (RemminaProtocolSetting*)python_wrapper_malloc( + sizeof(RemminaProtocolSetting) * (len + 1)); + memset(advanced_settings, 0, sizeof(RemminaProtocolSetting) * (len + 1)); + + for (Py_ssize_t i = 0; i < len; ++i) + { + RemminaProtocolSetting* dest = advanced_settings + i; + python_wrapper_to_protocol_setting(dest, PyList_GetItem(list, i)); + } + + RemminaProtocolSetting* dest = advanced_settings + len; + dest->type = REMMINA_PROTOCOL_SETTING_TYPE_END; + + remmina_plugin->advanced_settings = advanced_settings; + } + + list = PyObject_GetAttrString(instance, "features"); + len = PyList_Size(list); + if (len) + { + RemminaProtocolFeature* features = (RemminaProtocolFeature*)python_wrapper_malloc( + sizeof(RemminaProtocolFeature) * (len + 1)); + memset(features, 0, sizeof(RemminaProtocolFeature) * (len + 1)); + + for (Py_ssize_t i = 0; i < len; ++i) + { + RemminaProtocolFeature* dest = features + i; + python_wrapper_to_protocol_feature(dest, PyList_GetItem(list, i)); + } + + RemminaProtocolFeature* dest = features + len; + dest->type = REMMINA_PROTOCOL_FEATURE_TYPE_END; + + remmina_plugin->features = features; + } + + remmina_plugin->ssh_setting = (RemminaProtocolSSHSetting)python_wrapper_get_attribute_long(instance, + ATTR_SSH_SETTING, + REMMINA_PROTOCOL_SSH_SETTING_NONE); + + remmina_plugin->init = remmina_protocol_init_wrapper; // Plugin initialization + remmina_plugin->open_connection = remmina_protocol_open_connection_wrapper; // Plugin open connection + remmina_plugin->close_connection = remmina_protocol_close_connection_wrapper; // Plugin close connection + remmina_plugin->query_feature = remmina_protocol_query_feature_wrapper; // Query for available features + remmina_plugin->call_feature = remmina_protocol_call_feature_wrapper; // Call a feature + remmina_plugin->send_keystrokes = + remmina_protocol_send_keytrokes_wrapper; // Send a keystroke + remmina_plugin->get_plugin_screenshot = + remmina_protocol_get_plugin_screenshot_wrapper; // Screenshot support unavailable + + remmina_plugin->map_event = remmina_protocol_map_event_wrapper; + remmina_plugin->unmap_event = remmina_protocol_unmap_event_wrapper; + + plugin->protocol_plugin = remmina_plugin; + plugin->generic_plugin = (RemminaPlugin*)remmina_plugin; + + python_wrapper_add_plugin(plugin); + + return (RemminaPlugin*)remmina_plugin; +}
\ No newline at end of file diff --git a/plugins/python_wrapper/python_wrapper_protocol.h b/plugins/python_wrapper/python_wrapper_protocol.h new file mode 100644 index 0000000..7c4c9da --- /dev/null +++ b/plugins/python_wrapper/python_wrapper_protocol.h @@ -0,0 +1,109 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo, Mathias Winterhalter (ToolsDevler) + * + * 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. + */ + +/** + * @file python_wrapper_protocol.h + * + * @brief Contains the specialisation of RemminaPluginFile plugins in Python. + */ + +#pragma once + +#include "python_wrapper_common.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// I N C L U D E S +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "remmina/plugin.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// A P I +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +G_BEGIN_DECLS + +/** + * Wrapper for a Python object that contains a pointer to an instance of RemminaProtocolFeature. + */ +typedef struct +{ + PyObject_HEAD + RemminaProtocolFeatureType type; + gint id; + PyGeneric* opt1; + PyGeneric* opt2; + PyGeneric* opt3; +} PyRemminaProtocolFeature; + +/** + * + */ +typedef struct +{ + PyObject_HEAD + PyByteArrayObject* buffer; + int bitsPerPixel; + int bytesPerPixel; + int width; + int height; +} PyRemminaPluginScreenshotData; + +/** + * Initializes the Python plugin specialisation for protocol plugins. + */ +void python_wrapper_protocol_init(void); + +/** + * @brief Creates a new instance of the RemminaPluginProtocol, initializes its members and references the wrapper + * functions. + * + * @param instance The instance of the Python plugin. + * + * @return Returns a new instance of the RemminaPlugin (must be freed!). + */ +RemminaPlugin* python_wrapper_create_protocol_plugin(PyPlugin* plugin); + +/** + * + * @return + */ +PyRemminaProtocolFeature* python_wrapper_protocol_feature_new(void); + +/** + * + * @return + */ +PyRemminaPluginScreenshotData* python_wrapper_screenshot_data_new(void); + +G_END_DECLS diff --git a/plugins/python_wrapper/python_wrapper_protocol_widget.c b/plugins/python_wrapper/python_wrapper_protocol_widget.c new file mode 100644 index 0000000..3f8ee7a --- /dev/null +++ b/plugins/python_wrapper/python_wrapper_protocol_widget.c @@ -0,0 +1,845 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo + * + * 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. + */ + +/** + * @file python_wrapper_protocol_widget.c + * @brief Implementation of the Protocol Widget API. + * @author Mathias Winterhalter + * @date 19.11.2020 + * + * The RemminaPluginService provides an API for plugins to interact with Remmina. The + * module called 'remmina' forwards this interface to make it accessible for Python + * scripts. + * + * This is an example of a minimal protocol plugin: + * + * @code + * import remmina + * + * class MyProtocol: + * def __init__(self): + * self.name = "MyProtocol" + * self.description = "Example protocol plugin to explain how Python plugins work." + * self.version = "0.1" + * self.icon_name = "" + * self.icon_name_ssh = "" + * + * def init(self, handle): + * print("This is getting logged to the standard output of Remmina.") + * remmina.log_print("For debugging purposes it would be better to log the output to the %s window %s!" % ("debug", ":)")) + * self.init_your_stuff(handle) + * + * def open_connection(self, handle): + * if not self.connect(): + * remmina.log_print("Error! Can not connect...") + * return False + * + * remmina.remmina_signal_connected(handle) + * remmina.log_print("Connection established!") + * return True + * + * + * def close_connection(self, handle): + * self.disconnect() + * return True + * + * plugin = MyProtocol() + * remmina.register_plugin(plugin) + * @endcode + * + * + * + * @see http://www.remmina.org/wp for more information. + */ + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// I N L U C E S +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "python_wrapper_common.h" +#include "remmina/plugin.h" +#include "remmina/types.h" +#include "python_wrapper_remmina_file.h" +#include "python_wrapper_protocol_widget.h" +#include "python_wrapper_protocol.h" + +// -- Python Type -> RemminaWidget + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// D E C L A R A T I O N S +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +static PyObject* protocol_widget_get_viewport(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_get_width(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_set_width(PyRemminaProtocolWidget* self, PyObject* var_width); +static PyObject* protocol_widget_get_height(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_set_height(PyRemminaProtocolWidget* self, PyObject* var_height); +static PyObject* protocol_widget_get_current_scale_mode(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_get_expand(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_set_expand(PyRemminaProtocolWidget* self, PyObject* var_expand); +static PyObject* protocol_widget_has_error(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_set_error(PyRemminaProtocolWidget* self, PyObject* var_msg); +static PyObject* protocol_widget_is_closed(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_get_file(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_emit_signal(PyRemminaProtocolWidget* self, PyObject* var_signal); +static PyObject* protocol_widget_register_hostkey(PyRemminaProtocolWidget* self, PyObject* var_widget); +static PyObject* protocol_widget_start_direct_tunnel(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_start_reverse_tunnel(PyRemminaProtocolWidget* self, PyObject* var_local_port); +static PyObject* protocol_widget_start_xport_tunnel(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_set_display(PyRemminaProtocolWidget* self, PyObject* var_display); +static PyObject* protocol_widget_signal_connection_closed(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_signal_connection_opened(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_update_align(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_unlock_dynres(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_desktop_resize(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_panel_new_certificate(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_panel_changed_certificate(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_get_username(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_get_password(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_get_domain(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_get_savepassword(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_panel_authx509(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_get_cacert(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_get_cacrl(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_get_clientcert(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_get_clientkey(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_save_cred(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_panel_show_listen(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_panel_show_retry(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_panel_show(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_panel_hide(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_ssh_exec(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_chat_open(PyRemminaProtocolWidget* self, PyObject* var_name); +static PyObject* protocol_widget_chat_close(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_chat_receive(PyRemminaProtocolWidget* self, PyObject* args); +static PyObject* protocol_widget_send_keys_signals(PyRemminaProtocolWidget* self, PyObject* args); + +static struct PyMethodDef python_protocol_widget_type_methods[] = + {{ "get_viewport", (PyCFunction)protocol_widget_get_viewport, METH_NOARGS, "" }, + { "get_width", (PyCFunction)protocol_widget_get_width, METH_NOARGS, "" }, + { "set_width", (PyCFunction)protocol_widget_set_width, METH_VARARGS, "" }, + { "get_height", (PyCFunction)protocol_widget_get_height, METH_VARARGS, "" }, + { "set_height", (PyCFunction)protocol_widget_set_height, METH_VARARGS, "" }, + { "get_current_scale_mode", (PyCFunction)protocol_widget_get_current_scale_mode, METH_VARARGS, "" }, + { "get_expand", (PyCFunction)protocol_widget_get_expand, METH_VARARGS, "" }, + { "set_expand", (PyCFunction)protocol_widget_set_expand, METH_VARARGS, "" }, + { "has_error", (PyCFunction)protocol_widget_has_error, METH_VARARGS, "" }, + { "set_error", (PyCFunction)protocol_widget_set_error, METH_VARARGS, "" }, + { "is_closed", (PyCFunction)protocol_widget_is_closed, METH_VARARGS, "" }, + { "get_file", (PyCFunction)protocol_widget_get_file, METH_NOARGS, "" }, + { "emit_signal", (PyCFunction)protocol_widget_emit_signal, METH_VARARGS, "" }, + { "register_hostkey", (PyCFunction)protocol_widget_register_hostkey, METH_VARARGS, "" }, + { "start_direct_tunnel", (PyCFunction)protocol_widget_start_direct_tunnel, METH_VARARGS | METH_KEYWORDS, "" }, + { "start_reverse_tunnel", (PyCFunction)protocol_widget_start_reverse_tunnel, METH_VARARGS, "" }, + { "start_xport_tunnel", (PyCFunction)protocol_widget_start_xport_tunnel, METH_VARARGS, "" }, + { "set_display", (PyCFunction)protocol_widget_set_display, METH_VARARGS, "" }, + { "signal_connection_closed", (PyCFunction)protocol_widget_signal_connection_closed, METH_VARARGS, "" }, + { "signal_connection_opened", (PyCFunction)protocol_widget_signal_connection_opened, METH_VARARGS, "" }, + { "update_align", (PyCFunction)protocol_widget_update_align, METH_VARARGS, "" }, + { "unlock_dynres", (PyCFunction)protocol_widget_unlock_dynres, METH_VARARGS, "" }, + { "desktop_resize", (PyCFunction)protocol_widget_desktop_resize, METH_VARARGS, "" }, + { "panel_new_certificate", (PyCFunction)protocol_widget_panel_new_certificate, METH_VARARGS | METH_KEYWORDS, "" }, + { "panel_changed_certificate", (PyCFunction)protocol_widget_panel_changed_certificate, + METH_VARARGS | METH_KEYWORDS, "" }, + { "get_username", (PyCFunction)protocol_widget_get_username, METH_VARARGS, "" }, + { "get_password", (PyCFunction)protocol_widget_get_password, METH_VARARGS, "" }, + { "get_domain", (PyCFunction)protocol_widget_get_domain, METH_VARARGS, "" }, + { "get_savepassword", (PyCFunction)protocol_widget_get_savepassword, METH_VARARGS, "" }, + { "panel_authx509", (PyCFunction)protocol_widget_panel_authx509, METH_VARARGS, "" }, + { "get_cacert", (PyCFunction)protocol_widget_get_cacert, METH_VARARGS, "" }, + { "get_cacrl", (PyCFunction)protocol_widget_get_cacrl, METH_VARARGS, "" }, + { "get_clientcert", (PyCFunction)protocol_widget_get_clientcert, METH_VARARGS, "" }, + { "get_clientkey", (PyCFunction)protocol_widget_get_clientkey, METH_VARARGS, "" }, + { "save_cred", (PyCFunction)protocol_widget_save_cred, METH_VARARGS, "" }, + { "panel_show_listen", (PyCFunction)protocol_widget_panel_show_listen, METH_VARARGS, "" }, + { "panel_show_retry", (PyCFunction)protocol_widget_panel_show_retry, METH_VARARGS, "" }, + { "panel_show", (PyCFunction)protocol_widget_panel_show, METH_VARARGS, "" }, + { "panel_hide", (PyCFunction)protocol_widget_panel_hide, METH_VARARGS, "" }, + { "ssh_exec", (PyCFunction)protocol_widget_ssh_exec, METH_VARARGS | METH_KEYWORDS, "" }, + { "chat_open", (PyCFunction)protocol_widget_chat_open, METH_VARARGS, "" }, + { "chat_close", (PyCFunction)protocol_widget_chat_close, METH_VARARGS, "" }, + { "chat_receive", (PyCFunction)protocol_widget_chat_receive, METH_VARARGS, "" }, + { "send_keys_signals", (PyCFunction)protocol_widget_send_keys_signals, METH_VARARGS | METH_KEYWORDS, "" }, + { NULL }}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// A P I +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +static PyObject* python_protocol_feature_new(PyTypeObject* type, PyObject* kws, PyObject* args) +{ + TRACE_CALL(__func__); + PyRemminaProtocolWidget* self; + self = (PyRemminaProtocolWidget*)type->tp_alloc(type, 0); + if (!self) + { + return NULL; + } + + return (PyObject*)self; +} + +static int python_protocol_feature_init(PyObject* self, PyObject* args, PyObject* kwds) +{ + return 0; +} + +static PyTypeObject python_protocol_widget_type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "remmina.RemminaProtocolWidget", + .tp_doc = "RemminaProtocolWidget", + .tp_basicsize = sizeof(PyRemminaProtocolWidget), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_new = python_protocol_feature_new, + .tp_init = python_protocol_feature_init, + .tp_methods = python_protocol_widget_type_methods +}; + +PyRemminaProtocolWidget* python_wrapper_protocol_widget_create(void) +{ + TRACE_CALL(__func__); + + PyRemminaProtocolWidget* result = PyObject_NEW(PyRemminaProtocolWidget, &python_protocol_widget_type); + assert(result); + + PyErr_Print(); + Py_INCREF(result); + result->gp = NULL; + return result; +} + +void python_wrapper_protocol_widget_init(void) +{ + init_pygobject(); +} + +void python_wrapper_protocol_widget_type_ready(void) +{ + TRACE_CALL(__func__); + if (PyType_Ready(&python_protocol_widget_type) < 0) + { + g_printerr("Error initializing remmina.RemminaWidget!\n"); + PyErr_Print(); + } +} + +static PyObject* protocol_widget_get_viewport(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + return (PyObject*)new_pywidget(G_OBJECT(python_wrapper_get_service()->protocol_widget_gtkviewport(self->gp))); +} + +static PyObject* protocol_widget_get_width(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + return Py_BuildValue("i", python_wrapper_get_service()->protocol_widget_get_width(self->gp)); +} + +static PyObject* protocol_widget_set_width(PyRemminaProtocolWidget* self, PyObject* var_width) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + if (!var_width) + { + g_printerr("[%s:%d@%s]: Argument is null!\n", __FILE__, __LINE__, __func__); + return NULL; + } + + if (PyLong_Check(var_width)) + { + g_printerr("[%s:%d@%s]: Argument is not of type Long!\n", __FILE__, __LINE__, __func__); + return NULL; + } + + gint width = (gint)PyLong_AsLong(var_width); + python_wrapper_get_service()->protocol_widget_set_height(self->gp, width); + + return Py_None; +} + +static PyObject* protocol_widget_get_height(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + return Py_BuildValue("i", python_wrapper_get_service()->protocol_widget_get_height(self->gp)); +} + +static PyObject* protocol_widget_set_height(PyRemminaProtocolWidget* self, PyObject* var_height) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + if (!var_height) + { + g_printerr("[%s:%d@%s]: Argument is null!\n", __FILE__, __LINE__, __func__); + return NULL; + } + + if (PyLong_Check(var_height)) + { + g_printerr("[%s:%d@%s]: Argument is not of type Long!\n", __FILE__, __LINE__, __func__); + return NULL; + } + + gint height = (gint)PyLong_AsLong(var_height); + python_wrapper_get_service()->protocol_widget_set_height(self->gp, height); + + return Py_None; +} + +static PyObject* protocol_widget_get_current_scale_mode(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + return Py_BuildValue("i", python_wrapper_get_service()->protocol_widget_get_current_scale_mode(self->gp)); +} + +static PyObject* protocol_widget_get_expand(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + return Py_BuildValue("p", python_wrapper_get_service()->protocol_widget_get_expand(self->gp)); +} + +static PyObject* protocol_widget_set_expand(PyRemminaProtocolWidget* self, PyObject* var_expand) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + if (!var_expand) + { + g_printerr("[%s:%d@%s]: Argument is null!\n", __FILE__, __LINE__, __func__); + return NULL; + } + + if (PyBool_Check(var_expand)) + { + g_printerr("[%s:%d@%s]: Argument is not of type Boolean!\n", __FILE__, __LINE__, __func__); + return NULL; + } + + python_wrapper_get_service()->protocol_widget_set_expand(self->gp, PyObject_IsTrue(var_expand)); + + return Py_None; +} + +static PyObject* protocol_widget_has_error(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + return Py_BuildValue("p", python_wrapper_get_service()->protocol_widget_has_error(self->gp)); +} + +static PyObject* protocol_widget_set_error(PyRemminaProtocolWidget* self, PyObject* var_msg) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + if (!var_msg) + { + g_printerr("[%s:%d@%s]: Argument is null!\n", __FILE__, __LINE__, __func__); + return NULL; + } + + if (PyUnicode_Check(var_msg)) + { + g_printerr("[%s:%d@%s]: Argument is not of type String!\n", __FILE__, __LINE__, __func__); + return NULL; + } + + const gchar* msg = PyUnicode_AsUTF8(var_msg); + python_wrapper_get_service()->protocol_widget_set_error(self->gp, msg); + + return Py_None; +} + +static PyObject* protocol_widget_is_closed(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + return Py_BuildValue("p", python_wrapper_get_service()->protocol_widget_is_closed(self->gp)); +} + +static PyObject* protocol_widget_get_file(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + RemminaFile* file = python_wrapper_get_service()->protocol_widget_get_file(self->gp); + return (PyObject*)python_wrapper_remmina_file_to_python(file); +} + +static PyObject* protocol_widget_emit_signal(PyRemminaProtocolWidget* self, PyObject* var_signal) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + if (!var_signal) + { + g_printerr("[%s:%d@%s]: Argument is null!\n", __FILE__, __LINE__, __func__); + return NULL; + } + + if (PyUnicode_Check(var_signal)) + { + g_printerr("[%s:%d@%s]: Argument is not of type String!\n", __FILE__, __LINE__, __func__); + return NULL; + } + + python_wrapper_get_service()->protocol_widget_set_error(self->gp, PyUnicode_AsUTF8(var_signal)); + + return Py_None; +} + +static PyObject* protocol_widget_register_hostkey(PyRemminaProtocolWidget* self, PyObject* var_widget) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + if (!var_widget) + { + g_printerr("[%s:%d@%s]: Argument is null!\n", __FILE__, __LINE__, __func__); + return NULL; + } + + python_wrapper_get_service()->protocol_widget_register_hostkey(self->gp, get_pywidget(var_widget)); + + return Py_None; +} + +static PyObject* protocol_widget_start_direct_tunnel(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + gint default_port; + gboolean port_plus; + + if (!args) + { + g_printerr("[%s:%d@%s]: Argument is null!\n", __FILE__, __LINE__, __func__); + } + + if (PyArg_ParseTuple(args, "ii", &default_port, &port_plus)) + { + return Py_BuildValue("s", python_wrapper_get_service()->protocol_widget_start_direct_tunnel(self->gp, default_port, port_plus)); + } + else + { + PyErr_Print(); + return NULL; + } + return Py_None; +} + +static PyObject* protocol_widget_start_reverse_tunnel(PyRemminaProtocolWidget* self, PyObject* var_local_port) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + if (!PyLong_Check(var_local_port)) + { + g_printerr("[%s:%d@%s]: Argument is null!\n", __FILE__, __LINE__, __func__); + return NULL; + } + + if (!PyLong_Check(var_local_port)) + { + g_printerr("[%s:%d@%s]: Argument is not of type Long!\n", __FILE__, __LINE__, __func__); + return NULL; + } + + return Py_BuildValue("p", python_wrapper_get_service()->protocol_widget_start_reverse_tunnel(self->gp, (gint)PyLong_AsLong(var_local_port))); +} + +static gboolean xport_tunnel_init(RemminaProtocolWidget* gp, gint remotedisplay, const gchar* server, gint port) +{ + TRACE_CALL(__func__); + PyPlugin* plugin = python_wrapper_get_plugin_by_protocol_widget(gp); + PyObject* result = PyObject_CallMethod(plugin->instance, "xport_tunnel_init", "Oisi", gp, remotedisplay, server, port); + return PyObject_IsTrue(result); +} + +static PyObject* protocol_widget_start_xport_tunnel(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + return Py_BuildValue("p", python_wrapper_get_service()->protocol_widget_start_xport_tunnel(self->gp, xport_tunnel_init)); +} + +static PyObject* protocol_widget_set_display(PyRemminaProtocolWidget* self, PyObject* var_display) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + if (!var_display) + { + g_printerr("[%s:%d@%s]: Argument is null!\n", __FILE__, __LINE__, __func__); + return NULL; + } + + if (!PyLong_Check(var_display)) + { + g_printerr("[%s:%d@%s]: Argument is not of type Long!\n", __FILE__, __LINE__, __func__); + return NULL; + } + + python_wrapper_get_service()->protocol_widget_set_display(self->gp, (gint)PyLong_AsLong(var_display)); + + return Py_None; +} + +static PyObject* protocol_widget_signal_connection_closed(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + python_wrapper_get_service()->protocol_widget_signal_connection_closed(self->gp); + return Py_None; +} + +static PyObject* protocol_widget_signal_connection_opened(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + python_wrapper_get_service()->protocol_widget_signal_connection_opened(self->gp); + return Py_None; +} + +static PyObject* protocol_widget_update_align(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + python_wrapper_get_service()->protocol_widget_update_align(self->gp); + return Py_None; +} + +static PyObject* protocol_widget_unlock_dynres(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + python_wrapper_get_service()->protocol_widget_unlock_dynres(self->gp); + return Py_None; +} + +static PyObject* protocol_widget_desktop_resize(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + python_wrapper_get_service()->protocol_widget_desktop_resize(self->gp); + return Py_None; +} + +static PyObject* protocol_widget_panel_new_certificate(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + gchar* subject, * issuer, * fingerprint; + + if (PyArg_ParseTuple(args, "sss", &subject, &issuer, &fingerprint)) + { + python_wrapper_get_service()->protocol_widget_panel_new_certificate(self->gp, subject, issuer, fingerprint); + } + else + { + PyErr_Print(); + return NULL; + } + return Py_None; +} + +static PyObject* protocol_widget_panel_changed_certificate(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + gchar* subject, * issuer, * new_fingerprint, * old_fingerprint; + + if (PyArg_ParseTuple(args, "sss", &subject, &issuer, &new_fingerprint, &old_fingerprint)) + { + python_wrapper_get_service()->protocol_widget_panel_changed_certificate(self->gp, subject, issuer, new_fingerprint, old_fingerprint); + } + else + { + PyErr_Print(); + return NULL; + } + return Py_None; +} + +static PyObject* protocol_widget_get_username(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + return Py_BuildValue("s", python_wrapper_get_service()->protocol_widget_get_username(self->gp)); +} + +static PyObject* protocol_widget_get_password(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + return Py_BuildValue("s", python_wrapper_get_service()->protocol_widget_get_password(self->gp)); +} + +static PyObject* protocol_widget_get_domain(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + return Py_BuildValue("s", python_wrapper_get_service()->protocol_widget_get_domain(self->gp)); +} + +static PyObject* protocol_widget_get_savepassword(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + return Py_BuildValue("p", python_wrapper_get_service()->protocol_widget_get_savepassword(self->gp)); +} + +static PyObject* protocol_widget_panel_authx509(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + return Py_BuildValue("i", python_wrapper_get_service()->protocol_widget_panel_authx509(self->gp)); +} + +static PyObject* protocol_widget_get_cacert(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + return Py_BuildValue("s", python_wrapper_get_service()->protocol_widget_get_cacert(self->gp)); +} + +static PyObject* protocol_widget_get_cacrl(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + return Py_BuildValue("s", python_wrapper_get_service()->protocol_widget_get_cacrl(self->gp)); +} + +static PyObject* protocol_widget_get_clientcert(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + return Py_BuildValue("s", python_wrapper_get_service()->protocol_widget_get_clientcert(self->gp)); +} + +static PyObject* protocol_widget_get_clientkey(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + return Py_BuildValue("s", python_wrapper_get_service()->protocol_widget_get_clientkey(self->gp)); +} + +static PyObject* protocol_widget_save_cred(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + python_wrapper_get_service()->protocol_widget_save_cred(self->gp); + return Py_None; +} + +static PyObject* protocol_widget_panel_show_listen(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + gint port = 0; + + if (PyArg_ParseTuple(args, "i", &port)) + { + python_wrapper_get_service()->protocol_widget_panel_show_listen(self->gp, port); + } + else + { + PyErr_Print(); + return NULL; + } + return Py_None; +} + +static PyObject* protocol_widget_panel_show_retry(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + python_wrapper_get_service()->protocol_widget_panel_show_retry(self->gp); + return Py_None; +} + +static PyObject* protocol_widget_panel_show(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + python_wrapper_get_service()->protocol_widget_panel_show(self->gp); + return Py_None; +} + +static PyObject* protocol_widget_panel_hide(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + python_wrapper_get_service()->protocol_widget_panel_hide(self->gp); + return Py_None; +} + +static PyObject* protocol_widget_ssh_exec(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + gboolean wait; + gchar* cmd; + + if (PyArg_ParseTuple(args, "ps", &wait, &cmd)) + { + python_wrapper_get_service()->protocol_widget_ssh_exec(self->gp, wait, cmd); + } + else + { + PyErr_Print(); + return NULL; + } + return Py_None; +} + +static void _on_send_callback_wrapper(RemminaProtocolWidget* gp, const gchar* text) +{ + PyPlugin* plugin = python_wrapper_get_plugin_by_protocol_widget(gp); + PyObject_CallMethod(plugin->instance, "on_send", "Os", gp, text); +} + +static void _on_destroy_callback_wrapper(RemminaProtocolWidget* gp) +{ + PyPlugin* plugin = python_wrapper_get_plugin_by_protocol_widget(gp); + PyObject_CallMethod(plugin->instance, "on_destroy", "O", gp); +} + +static PyObject* protocol_widget_chat_open(PyRemminaProtocolWidget* self, PyObject* var_name) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + if (!PyUnicode_Check(var_name)) + { + g_printerr("[%s:%d@%s]: Argument is not of type String!\n", __FILE__, __LINE__, __func__); + } + + python_wrapper_get_service()->protocol_widget_chat_open(self->gp, + PyUnicode_AsUTF8(var_name), + _on_send_callback_wrapper, + _on_destroy_callback_wrapper); + + return Py_None; +} + +static PyObject* protocol_widget_chat_close(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + + python_wrapper_get_service()->protocol_widget_panel_hide(self->gp); + return Py_None; +} + +static PyObject* protocol_widget_chat_receive(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + gchar* text; + + if (PyArg_ParseTuple(args, "s", &text)) + { + python_wrapper_get_service()->protocol_widget_chat_receive(self->gp, text); + } + else + { + PyErr_Print(); + return NULL; + } + + return Py_None; +} + +static PyObject* protocol_widget_send_keys_signals(PyRemminaProtocolWidget* self, PyObject* args) +{ + TRACE_CALL(__func__); + SELF_CHECK(); + guint* keyvals; + int length; + GdkEventType event_type; + PyObject* widget; + + if (PyArg_ParseTuple(args, "Osii", &widget, &keyvals, &length, &event_type) && widget && keyvals) + { + if (event_type < GDK_NOTHING || event_type >= GDK_EVENT_LAST) + { + g_printerr("[%s:%d@%s]: %d is not a known value for GdkEventType!\n", __FILE__, __LINE__, __func__, event_type); + return NULL; + } + else + { + python_wrapper_get_service()->protocol_widget_send_keys_signals((GtkWidget*)widget, keyvals, length, event_type); + } + } + else + { + PyErr_Print(); + return NULL; + } + + return Py_None; +} diff --git a/plugins/python_wrapper/python_wrapper_protocol_widget.h b/plugins/python_wrapper/python_wrapper_protocol_widget.h new file mode 100644 index 0000000..40e54ac --- /dev/null +++ b/plugins/python_wrapper/python_wrapper_protocol_widget.h @@ -0,0 +1,65 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * 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. + */ + +/** + * @file python_wrapper_protocol_widget.h + * + * @brief Contains the implementation of the widget handling used from the protocol plugin. + */ + +#pragma once + +#include "python_wrapper_common.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// A P I +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +G_BEGIN_DECLS + +/** + * Initializes the widget backend of the protocol plugin implementation. + */ +void python_wrapper_protocol_widget_init(void); + +/** + * Initializes Python types used for protocol widgets. + */ +void python_wrapper_protocol_widget_type_ready(void); + +/** + * Creates a new instance of PyRemminaProtocolWidget and initializes its fields. + */ +PyRemminaProtocolWidget* python_wrapper_protocol_widget_create(); + +G_END_DECLS diff --git a/plugins/python_wrapper/python_wrapper_remmina.c b/plugins/python_wrapper/python_wrapper_remmina.c new file mode 100644 index 0000000..7a4a771 --- /dev/null +++ b/plugins/python_wrapper/python_wrapper_remmina.c @@ -0,0 +1,1241 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * 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. + */ + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// I N C L U D E S +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "python_wrapper_common.h" +#include "remmina/plugin.h" +#include "remmina/types.h" + +#include "python_wrapper_remmina.h" +#include "python_wrapper_protocol_widget.h" + +#include "python_wrapper_entry.h" +#include "python_wrapper_file.h" +#include "python_wrapper_protocol.h" +#include "python_wrapper_tool.h" +#include "python_wrapper_secret.h" +#include "python_wrapper_pref.h" + +#include "python_wrapper_remmina_file.h" + +#include <string.h> + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// D E C L A R A T I O N S +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + +/** + * Util function to check if a specific member is define in a Python object. + */ +gboolean python_wrapper_check_mandatory_member(PyObject* instance, const gchar* member); + +static PyObject* python_wrapper_debug_wrapper(PyObject* self, PyObject* msg); +static PyObject* remmina_register_plugin_wrapper(PyObject* self, PyObject* plugin); +static PyObject* remmina_file_get_datadir_wrapper(PyObject* self, PyObject* plugin); +static PyObject* remmina_file_new_wrapper(PyObject* self, PyObject* args, PyObject* kwargs); +static PyObject* remmina_unlock_new_wrapper(PyObject* self, PyObject* args, PyObject* kwargs); +static PyObject* remmina_pref_set_value_wrapper(PyObject* self, PyObject* args, PyObject* kwargs); +static PyObject* remmina_pref_get_value_wrapper(PyObject* self, PyObject* args, PyObject* kwargs); +static PyObject* remmina_pref_get_scale_quality_wrapper(PyObject* self, PyObject* plugin); +static PyObject* remmina_pref_get_sshtunnel_port_wrapper(PyObject* self, PyObject* plugin); +static PyObject* remmina_pref_get_ssh_loglevel_wrapper(PyObject* self, PyObject* plugin); +static PyObject* remmina_pref_get_ssh_parseconfig_wrapper(PyObject* self, PyObject* plugin); +static PyObject* remmina_pref_keymap_get_keyval_wrapper(PyObject* self, PyObject* args, PyObject* kwargs); +static PyObject* python_wrapper_log_print_wrapper(PyObject* self, PyObject* arg); +static PyObject* remmina_widget_pool_register_wrapper(PyObject* self, PyObject* args, PyObject* kwargs); +static PyObject* rcw_open_from_file_full_wrapper(PyObject* self, PyObject* args, PyObject* kwargs); +static PyObject* remmina_public_get_server_port_wrapper(PyObject* self, PyObject* args, PyObject* kwargs); +static PyObject* remmina_masterthread_exec_is_main_thread_wrapper(PyObject* self, PyObject* plugin); +static PyObject* remmina_gtksocket_available_wrapper(PyObject* self, PyObject* plugin); +static PyObject* +remmina_protocol_widget_get_profile_remote_height_wrapper(PyObject* self, PyObject* args, PyObject* kwargs); +static PyObject* +remmina_protocol_widget_get_profile_remote_width_wrapper(PyObject* self, PyObject* args, PyObject* kwargs); +static PyObject* python_wrapper_show_dialog_wrapper(PyObject* self, PyObject* args, PyObject* kwargs); +static PyObject* python_wrapper_get_mainwindow_wrapper(PyObject* self, PyObject* args); +static PyObject* remmina_protocol_plugin_signal_connection_opened_wrapper(PyObject* self, PyObject* args); +static PyObject* remmina_protocol_plugin_signal_connection_closed_wrapper(PyObject* self, PyObject* args); +static PyObject* remmina_protocol_plugin_init_auth_wrapper(PyObject* self, PyObject* args, PyObject* kwargs); + +/** + * Declares functions for the Remmina module. These functions can be called from Python and are wired to one of the + * functions here in this file. + */ +static PyMethodDef remmina_python_module_type_methods[] = { + /** + * The first function that need to be called from the plugin code, since it registers the Python class acting as + * a Remmina plugin. Without this call the plugin will not be recognized. + */ + { "register_plugin", remmina_register_plugin_wrapper, METH_O, NULL }, + + /** + * Prints a string into the Remmina log infrastructure. + */ + { "log_print", python_wrapper_log_print_wrapper, METH_VARARGS, NULL }, + + /** + * Prints a debug message if enabled. + */ + { "debug", python_wrapper_debug_wrapper, METH_VARARGS, NULL }, + + /** + * Shows a GTK+ dialog. + */ + { "show_dialog", (PyCFunction)python_wrapper_show_dialog_wrapper, METH_VARARGS | METH_KEYWORDS, + NULL }, + + /** + * Returns the GTK+ object of the main window of Remmina. + */ + { "get_main_window", python_wrapper_get_mainwindow_wrapper, METH_NOARGS, NULL }, + + /** + * Calls remmina_file_get_datadir and returns its result. + */ + { "get_datadir", remmina_file_get_datadir_wrapper, METH_VARARGS, NULL }, + + /** + * Calls remmina_file_new and returns its result. + */ + { "file_new", (PyCFunction)remmina_file_new_wrapper, METH_VARARGS | METH_KEYWORDS, NULL }, + + /** + * Calls remmina_unlock_new and returns its result. + */ + { "unlock_new", (PyCFunction)remmina_unlock_new_wrapper, METH_VARARGS | METH_KEYWORDS, NULL }, + + /** + * Calls remmina_pref_set_value and returns its result. + */ + { "pref_set_value", (PyCFunction)remmina_pref_set_value_wrapper, METH_VARARGS | METH_KEYWORDS, NULL }, + + /** + * Calls remmina_pref_get_value and returns its result. + */ + { "pref_get_value", (PyCFunction)remmina_pref_get_value_wrapper, METH_VARARGS | METH_KEYWORDS, NULL }, + + /** + * Calls remmina_pref_get_scale_quality and returns its result. + */ + { "pref_get_scale_quality", remmina_pref_get_scale_quality_wrapper, METH_VARARGS, NULL }, + + /** + * Calls remmina_pref_get_sshtunnel_port and returns its result. + */ + { "pref_get_sshtunnel_port", remmina_pref_get_sshtunnel_port_wrapper, METH_VARARGS, NULL }, + + /** + * Calls remmina_pref_get_ssh_loglevel and returns its result. + */ + { "pref_get_ssh_loglevel", remmina_pref_get_ssh_loglevel_wrapper, METH_VARARGS, NULL }, + + /** + * Calls remmina_pref_get_ssh_parseconfig and returns its result. + */ + { "pref_get_ssh_parseconfig", remmina_pref_get_ssh_parseconfig_wrapper, METH_VARARGS, NULL }, + + /** + * Calls remmina_pref_keymap_get_keyval and returns its result. + */ + { "pref_keymap_get_keyval", (PyCFunction)remmina_pref_keymap_get_keyval_wrapper, METH_VARARGS | METH_KEYWORDS, + NULL }, + + /** + * Calls remmina_widget_pool_register and returns its result. + */ + { "widget_pool_register", (PyCFunction)remmina_widget_pool_register_wrapper, METH_VARARGS | METH_KEYWORDS, NULL }, + + /** + * Calls rcw_open_from_file_full and returns its result. + */ + { "rcw_open_from_file_full", (PyCFunction)rcw_open_from_file_full_wrapper, METH_VARARGS | METH_KEYWORDS, NULL }, + + /** + * Calls remmina_public_get_server_port and returns its result. + */ + { "public_get_server_port", (PyCFunction)remmina_public_get_server_port_wrapper, METH_VARARGS | METH_KEYWORDS, + NULL }, + + /** + * Calls remmina_masterthread_exec_is_main_thread and returns its result. + */ + { "masterthread_exec_is_main_thread", remmina_masterthread_exec_is_main_thread_wrapper, METH_VARARGS, NULL }, + + /** + * Calls remmina_gtksocket_available and returns its result. + */ + { "gtksocket_available", remmina_gtksocket_available_wrapper, METH_VARARGS, NULL }, + + /** + * Calls remmina_protocol_widget_get_profile_remote_width and returns its result. + */ + { "protocol_widget_get_profile_remote_width", + (PyCFunction)remmina_protocol_widget_get_profile_remote_width_wrapper, METH_VARARGS | METH_KEYWORDS, NULL }, + + /** + * Calls remmina_protocol_widget_get_profile_remote_height and returns its result. + */ + { "protocol_widget_get_profile_remote_height", + (PyCFunction)remmina_protocol_widget_get_profile_remote_height_wrapper, METH_VARARGS | METH_KEYWORDS, NULL }, + { "protocol_plugin_signal_connection_opened", (PyCFunction)remmina_protocol_plugin_signal_connection_opened_wrapper, + METH_VARARGS, NULL }, + + { "protocol_plugin_signal_connection_closed", (PyCFunction)remmina_protocol_plugin_signal_connection_closed_wrapper, + METH_VARARGS, NULL }, + + { "protocol_plugin_init_auth", (PyCFunction)remmina_protocol_plugin_init_auth_wrapper, METH_VARARGS | METH_KEYWORDS, + NULL }, + + /* Sentinel */ + { NULL } +}; + +/** + * Adapter struct to handle Remmina protocol settings. + */ +typedef struct +{ + PyObject_HEAD + RemminaProtocolSettingType settingType; + gchar* name; + gchar* label; + gboolean compact; + PyObject* opt1; + PyObject* opt2; +} PyRemminaProtocolSetting; + +/** + * The definition of the Python module 'remmina'. + */ +static PyModuleDef remmina_python_module_type = { + PyModuleDef_HEAD_INIT, + .m_name = "remmina", + .m_doc = "Remmina API.", + .m_size = -1, + .m_methods = remmina_python_module_type_methods +}; + +// -- Python Object -> Setting + +/** + * Initializes the memory and the fields of the remmina.Setting Python type. + * @details This function is callback for the Python engine. + */ +static PyObject* python_protocol_setting_new(PyTypeObject* type, PyObject* args, PyObject* kwargs) +{ + TRACE_CALL(__func__); + + PyRemminaProtocolSetting* self = (PyRemminaProtocolSetting*)type->tp_alloc(type, 0); + + if (!self) + { + return NULL; + } + + self->name = ""; + self->label = ""; + self->compact = FALSE; + self->opt1 = NULL; + self->opt2 = NULL; + self->settingType = 0; + + return (PyObject*)self; +} + +/** + * Constructor of the remmina.Setting Python type. + * @details This function is callback for the Python engine. + */ +static int python_protocol_setting_init(PyRemminaProtocolSetting* self, PyObject* args, PyObject* kwargs) +{ + TRACE_CALL(__func__); + + static gchar* kwlist[] = { "type", "name", "label", "compact", "opt1", "opt2", NULL }; + PyObject* name; + PyObject* label; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|lOOpOO", kwlist, + &self->settingType, &name, &label, &self->compact, &self->opt1, &self->opt2)) + { + return -1; + } + + Py_ssize_t len = PyUnicode_GetLength(label); + if (len == 0) + { + self->label = ""; + } + else + { + self->label = python_wrapper_copy_string_from_python(label, len); + if (!self->label) + { + g_printerr("Unable to extract label during initialization of Python settings module!\n"); + python_wrapper_check_error(); + } + } + + len = PyUnicode_GetLength(name); + if (len == 0) + { + self->name = ""; + } + else + { + self->name = python_wrapper_copy_string_from_python(label, len); + if (!self->name) + { + g_printerr("Unable to extract name during initialization of Python settings module!\n"); + python_wrapper_check_error(); + } + } + + return 0; +} + +static PyMemberDef python_protocol_setting_type_members[] = { + { "settingType", offsetof(PyRemminaProtocolSetting, settingType), T_INT, 0, NULL }, + { "name", offsetof(PyRemminaProtocolSetting, name), T_STRING, 0, NULL }, + { "label", offsetof(PyRemminaProtocolSetting, label), T_STRING, 0, NULL }, + { "compact", offsetof(PyRemminaProtocolSetting, compact), T_BOOL, 0, NULL }, + { "opt1", offsetof(PyRemminaProtocolSetting, opt1), T_OBJECT, 0, NULL }, + { "opt2", offsetof(PyRemminaProtocolSetting, opt2), T_OBJECT, 0, NULL }, + { NULL } +}; + +static PyTypeObject python_protocol_setting_type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "remmina.Setting", + .tp_doc = "Remmina Setting information", + .tp_basicsize = sizeof(PyRemminaProtocolSetting), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_new = python_protocol_setting_new, + .tp_init = (initproc)python_protocol_setting_init, + .tp_members = python_protocol_setting_type_members +}; + +// -- Python Type -> Feature + + +static PyMemberDef python_protocol_feature_members[] = { + { "type", T_INT, offsetof(PyRemminaProtocolFeature, type), 0, NULL }, + { "id", T_INT, offsetof(PyRemminaProtocolFeature, id), 0, NULL }, + { "opt1", T_OBJECT, offsetof(PyRemminaProtocolFeature, opt1), 0, NULL }, + { "opt2", T_OBJECT, offsetof(PyRemminaProtocolFeature, opt2), 0, NULL }, + { "opt3", T_OBJECT, offsetof(PyRemminaProtocolFeature, opt3), 0, NULL }, + { NULL } +}; + +PyObject* python_protocol_feature_new(PyTypeObject* type, PyObject* kws, PyObject* args) +{ + TRACE_CALL(__func__); + + PyRemminaProtocolFeature* self; + self = (PyRemminaProtocolFeature*)type->tp_alloc(type, 0); + if (!self) + return NULL; + + self->id = 0; + self->type = 0; + self->opt1 = (PyGeneric*)Py_None; + self->opt1->raw = NULL; + self->opt1->type_hint = REMMINA_TYPEHINT_UNDEFINED; + self->opt2 = (PyGeneric*)Py_None; + self->opt2->raw = NULL; + self->opt2->type_hint = REMMINA_TYPEHINT_UNDEFINED; + self->opt3 = (PyGeneric*)Py_None; + self->opt3->raw = NULL; + self->opt3->type_hint = REMMINA_TYPEHINT_UNDEFINED; + + return (PyObject*)self; +} + +static int python_protocol_feature_init(PyRemminaProtocolFeature* self, PyObject* args, PyObject* kwargs) +{ + TRACE_CALL(__func__); + + static char* kwlist[] = { "type", "id", "opt1", "opt2", "opt3", NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|llOOO", kwlist, &self->type, &self->id, &self->opt1, &self + ->opt2, &self->opt3)) + return -1; + + return 0; +} + +static PyTypeObject python_protocol_feature_type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "remmina.ProtocolFeature", + .tp_doc = "Remmina Setting information", + .tp_basicsize = sizeof(PyRemminaProtocolFeature), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_new = python_protocol_feature_new, + .tp_init = (initproc)python_protocol_feature_init, + .tp_members = python_protocol_feature_members +}; + +PyRemminaProtocolFeature* python_wrapper_protocol_feature_new(void) +{ + PyRemminaProtocolFeature* feature = (PyRemminaProtocolFeature*)PyObject_New(PyRemminaProtocolFeature, &python_protocol_feature_type); + feature->id = 0; + feature->opt1 = python_wrapper_generic_new(); + feature->opt1->raw = NULL; + feature->opt1->type_hint = REMMINA_TYPEHINT_UNDEFINED; + feature->opt2 = python_wrapper_generic_new(); + feature->opt2->raw = NULL; + feature->opt2->type_hint = REMMINA_TYPEHINT_UNDEFINED; + feature->opt3 = python_wrapper_generic_new(); + feature->opt3->raw = NULL; + feature->opt3->type_hint = REMMINA_TYPEHINT_UNDEFINED; + feature->type = 0; + Py_IncRef((PyObject*)feature); + return feature; +} + + +// -- Python Type -> Screenshot Data + +static PyMemberDef python_screenshot_data_members[] = { + { "buffer", T_OBJECT, offsetof(PyRemminaPluginScreenshotData, buffer), 0, NULL }, + { "width", T_INT, offsetof(PyRemminaPluginScreenshotData, width), 0, NULL }, + { "height", T_INT, offsetof(PyRemminaPluginScreenshotData, height), 0, NULL }, + { "bitsPerPixel", T_INT, offsetof(PyRemminaPluginScreenshotData, bitsPerPixel), 0, NULL }, + { "bytesPerPixel", T_INT, offsetof(PyRemminaPluginScreenshotData, bytesPerPixel), 0, NULL }, + { NULL } +}; + +PyObject* python_screenshot_data_new(PyTypeObject* type, PyObject* kws, PyObject* args) +{ + TRACE_CALL(__func__); + + PyRemminaPluginScreenshotData* self; + self = (PyRemminaPluginScreenshotData*)type->tp_alloc(type, 0); + if (!self) + return NULL; + + self->buffer = (PyByteArrayObject*)PyObject_New(PyByteArrayObject, &PyByteArray_Type); + self->height = 0; + self->width = 0; + self->bitsPerPixel = 0; + self->bytesPerPixel = 0; + + return (PyObject*)self; +} + +static int python_screenshot_data_init(PyRemminaPluginScreenshotData* self, PyObject* args, PyObject* kwargs) +{ + TRACE_CALL(__func__); + + g_printerr("Not to be initialized within Python!"); + return -1; +} + +static PyTypeObject python_screenshot_data_type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "remmina.RemminaScreenshotData", + .tp_doc = "Remmina Screenshot Data", + .tp_basicsize = sizeof(PyRemminaPluginScreenshotData), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_new = python_screenshot_data_new, + .tp_init = (initproc)python_screenshot_data_init, + .tp_members = python_screenshot_data_members +}; +PyRemminaPluginScreenshotData* python_wrapper_screenshot_data_new(void) +{ + PyRemminaPluginScreenshotData* data = (PyRemminaPluginScreenshotData*)PyObject_New(PyRemminaPluginScreenshotData, &python_screenshot_data_type); + data->buffer = PyObject_New(PyByteArrayObject, &PyByteArray_Type); + Py_IncRef((PyObject*)data->buffer); + data->height = 0; + data->width = 0; + data->bitsPerPixel = 0; + data->bytesPerPixel = 0; + return data; +} + +static PyObject* python_wrapper_generic_to_int(PyGeneric* self, PyObject* args); +static PyObject* python_wrapper_generic_to_bool(PyGeneric* self, PyObject* args); +static PyObject* python_wrapper_generic_to_string(PyGeneric* self, PyObject* args); + +static void python_wrapper_generic_dealloc(PyObject* self) +{ + PyObject_Del(self); +} + +static PyMethodDef python_wrapper_generic_methods[] = { + { "to_int", (PyCFunction)python_wrapper_generic_to_int, METH_NOARGS, "" }, + { "to_bool", (PyCFunction)python_wrapper_generic_to_bool, METH_NOARGS, "" }, + { "to_string", (PyCFunction)python_wrapper_generic_to_string, METH_NOARGS, "" }, + { NULL } +}; + +static PyMemberDef python_wrapper_generic_members[] = { + { "raw", T_OBJECT, offsetof(PyGeneric, raw), 0, "" }, + { NULL } +}; + +PyObject* python_wrapper_generic_type_new(PyTypeObject* type, PyObject* kws, PyObject* args) +{ + TRACE_CALL(__func__); + + PyGeneric* self; + self = (PyGeneric*)type->tp_alloc(type, 0); + if (!self) + return NULL; + + self->raw = Py_None; + + return (PyObject*)self; +} + +static int python_wrapper_generic_init(PyGeneric* self, PyObject* args, PyObject* kwargs) +{ + TRACE_CALL(__func__); + + static char* kwlist[] = { "raw", NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|O", kwlist, &self->raw)) + return -1; + + return 0; +} + +static PyTypeObject python_generic_type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "remmina.Generic", + .tp_doc = "", + .tp_basicsize = sizeof(PyGeneric), + .tp_itemsize = 0, + .tp_dealloc = python_wrapper_generic_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_new = python_wrapper_generic_type_new, + .tp_init = (initproc)python_wrapper_generic_init, + .tp_members = python_wrapper_generic_members, + .tp_methods = python_wrapper_generic_methods, +}; + +PyGeneric* python_wrapper_generic_new(void) +{ + PyGeneric* generic = (PyGeneric*)PyObject_New(PyGeneric, &python_generic_type); + generic->raw = PyLong_FromLongLong(0LL); + Py_IncRef((PyObject*)generic); + return generic; +} + +static PyObject* python_wrapper_generic_to_int(PyGeneric* self, PyObject* args) +{ + SELF_CHECK(); + + if (self->raw == NULL) + { + return Py_None; + } + else if (self->type_hint == REMMINA_TYPEHINT_SIGNED) + { + return PyLong_FromLongLong((long long)self->raw); + } + else if (self->type_hint == REMMINA_TYPEHINT_UNSIGNED) + { + return PyLong_FromUnsignedLongLong((unsigned long long)self->raw); + } + + return Py_None; +} +static PyObject* python_wrapper_generic_to_bool(PyGeneric* self, PyObject* args) +{ + SELF_CHECK(); + + if (self->raw == NULL) + { + return Py_None; + } + else if (self->type_hint == REMMINA_TYPEHINT_BOOLEAN) + { + return PyBool_FromLong((long)self->raw); + } + + return Py_None; +} +static PyObject* python_wrapper_generic_to_string(PyGeneric* self, PyObject* args) +{ + SELF_CHECK(); + + if (self->raw == NULL) + { + return Py_None; + } + else if (self->type_hint == REMMINA_TYPEHINT_STRING) + { + return PyUnicode_FromString((const char*)self->raw); + } + + return Py_None; +} + +/** + * Is called from the Python engine when it initializes the 'remmina' module. + * @details This function is only called by the Python engine! + */ +PyMODINIT_FUNC python_wrapper_module_initialize(void) +{ + TRACE_CALL(__func__); + + if (PyType_Ready(&python_screenshot_data_type) < 0) + { + PyErr_Print(); + return NULL; + } + + if (PyType_Ready(&python_generic_type) < 0) + { + PyErr_Print(); + return NULL; + } + + if (PyType_Ready(&python_protocol_setting_type) < 0) + { + PyErr_Print(); + return NULL; + } + + if (PyType_Ready(&python_protocol_feature_type) < 0) + { + PyErr_Print(); + return NULL; + } + + python_wrapper_protocol_widget_type_ready(); + python_wrapper_remmina_init_types(); + + PyObject* module = PyModule_Create(&remmina_python_module_type); + if (!module) + { + PyErr_Print(); + return NULL; + } + + PyModule_AddIntConstant(module, "BUTTONS_CLOSE", (long)GTK_BUTTONS_CLOSE); + PyModule_AddIntConstant(module, "BUTTONS_NONE", (long)GTK_BUTTONS_NONE); + PyModule_AddIntConstant(module, "BUTTONS_OK", (long)GTK_BUTTONS_OK); + PyModule_AddIntConstant(module, "BUTTONS_CLOSE", (long)GTK_BUTTONS_CLOSE); + PyModule_AddIntConstant(module, "BUTTONS_CANCEL", (long)GTK_BUTTONS_CANCEL); + PyModule_AddIntConstant(module, "BUTTONS_YES_NO", (long)GTK_BUTTONS_YES_NO); + PyModule_AddIntConstant(module, "BUTTONS_OK_CANCEL", (long)GTK_BUTTONS_OK_CANCEL); + + PyModule_AddIntConstant(module, "MESSAGE_INFO", (long)GTK_MESSAGE_INFO); + PyModule_AddIntConstant(module, "MESSAGE_WARNING", (long)GTK_MESSAGE_WARNING); + PyModule_AddIntConstant(module, "MESSAGE_QUESTION", (long)GTK_MESSAGE_QUESTION); + PyModule_AddIntConstant(module, "MESSAGE_ERROR", (long)GTK_MESSAGE_ERROR); + PyModule_AddIntConstant(module, "MESSAGE_OTHER", (long)GTK_MESSAGE_OTHER); + + PyModule_AddIntConstant(module, "PROTOCOL_SETTING_TYPE_SERVER", (long)REMMINA_PROTOCOL_SETTING_TYPE_SERVER); + PyModule_AddIntConstant(module, "PROTOCOL_SETTING_TYPE_PASSWORD", (long)REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD); + PyModule_AddIntConstant(module, "PROTOCOL_SETTING_TYPE_RESOLUTION", (long)REMMINA_PROTOCOL_SETTING_TYPE_RESOLUTION); + PyModule_AddIntConstant(module, "PROTOCOL_SETTING_TYPE_ASSISTANCE", (long)REMMINA_PROTOCOL_SETTING_TYPE_ASSISTANCE); + PyModule_AddIntConstant(module, "PROTOCOL_SETTING_TYPE_KEYMAP", (long)REMMINA_PROTOCOL_SETTING_TYPE_KEYMAP); + PyModule_AddIntConstant(module, "PROTOCOL_SETTING_TYPE_TEXT", (long)REMMINA_PROTOCOL_SETTING_TYPE_TEXT); + PyModule_AddIntConstant(module, "PROTOCOL_SETTING_TYPE_SELECT", (long)REMMINA_PROTOCOL_SETTING_TYPE_SELECT); + PyModule_AddIntConstant(module, "PROTOCOL_SETTING_TYPE_COMBO", (long)REMMINA_PROTOCOL_SETTING_TYPE_COMBO); + PyModule_AddIntConstant(module, "PROTOCOL_SETTING_TYPE_CHECK", (long)REMMINA_PROTOCOL_SETTING_TYPE_CHECK); + PyModule_AddIntConstant(module, "PROTOCOL_SETTING_TYPE_FILE", (long)REMMINA_PROTOCOL_SETTING_TYPE_FILE); + PyModule_AddIntConstant(module, "PROTOCOL_SETTING_TYPE_FOLDER", (long)REMMINA_PROTOCOL_SETTING_TYPE_FOLDER); + PyModule_AddIntConstant(module, "PROTOCOL_FEATURE_TYPE_MULTIMON", (long)REMMINA_PROTOCOL_FEATURE_TYPE_MULTIMON); + + PyModule_AddIntConstant(module, "PROTOCOL_FEATURE_TYPE_PREF", (long)REMMINA_PROTOCOL_FEATURE_TYPE_PREF); + PyModule_AddIntConstant(module, "PROTOCOL_FEATURE_TYPE_TOOL", (long)REMMINA_PROTOCOL_FEATURE_TYPE_TOOL); + PyModule_AddIntConstant(module, "PROTOCOL_FEATURE_TYPE_UNFOCUS", (long)REMMINA_PROTOCOL_FEATURE_TYPE_UNFOCUS); + PyModule_AddIntConstant(module, "PROTOCOL_FEATURE_TYPE_SCALE", (long)REMMINA_PROTOCOL_FEATURE_TYPE_SCALE); + PyModule_AddIntConstant(module, "PROTOCOL_FEATURE_TYPE_DYNRESUPDATE", (long)REMMINA_PROTOCOL_FEATURE_TYPE_DYNRESUPDATE); + PyModule_AddIntConstant(module, "PROTOCOL_FEATURE_TYPE_GTKSOCKET", (long)REMMINA_PROTOCOL_FEATURE_TYPE_GTKSOCKET); + + PyModule_AddIntConstant(module, "PROTOCOL_SSH_SETTING_NONE", (long)REMMINA_PROTOCOL_SSH_SETTING_NONE); + PyModule_AddIntConstant(module, "PROTOCOL_SSH_SETTING_TUNNEL", (long)REMMINA_PROTOCOL_SSH_SETTING_TUNNEL); + PyModule_AddIntConstant(module, "PROTOCOL_SSH_SETTING_SSH", (long)REMMINA_PROTOCOL_SSH_SETTING_SSH); + PyModule_AddIntConstant(module, "PROTOCOL_SSH_SETTING_REVERSE_TUNNEL", (long)REMMINA_PROTOCOL_SSH_SETTING_REVERSE_TUNNEL); + PyModule_AddIntConstant(module, "PROTOCOL_SSH_SETTING_SFTP", (long)REMMINA_PROTOCOL_SSH_SETTING_SFTP); + + PyModule_AddIntConstant(module, "PROTOCOL_FEATURE_PREF_RADIO", (long)REMMINA_PROTOCOL_FEATURE_PREF_RADIO); + PyModule_AddIntConstant(module, "PROTOCOL_FEATURE_PREF_CHECK", (long)REMMINA_PROTOCOL_FEATURE_PREF_CHECK); + + PyModule_AddIntConstant(module, "MESSAGE_PANEL_FLAG_USERNAME", REMMINA_MESSAGE_PANEL_FLAG_USERNAME); + PyModule_AddIntConstant(module, "MESSAGE_PANEL_FLAG_USERNAME_READONLY", REMMINA_MESSAGE_PANEL_FLAG_USERNAME_READONLY); + PyModule_AddIntConstant(module, "MESSAGE_PANEL_FLAG_DOMAIN", REMMINA_MESSAGE_PANEL_FLAG_DOMAIN); + PyModule_AddIntConstant(module, "MESSAGE_PANEL_FLAG_SAVEPASSWORD", REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD); + + if (PyModule_AddObject(module, "Setting", (PyObject*)&python_protocol_setting_type) < 0) + { + Py_DECREF(&python_protocol_setting_type); + Py_DECREF(module); + PyErr_Print(); + return NULL; + } + + Py_INCREF(&python_protocol_feature_type); + if (PyModule_AddObject(module, "Feature", (PyObject*)&python_protocol_feature_type) < 0) + { + Py_DECREF(&python_protocol_setting_type); + Py_DECREF(&python_protocol_feature_type); + Py_DECREF(module); + PyErr_Print(); + return NULL; + } + + return module; +} + +/** + * Initializes all globals and registers the 'remmina' module in the Python engine. + * @details This + */ +void python_wrapper_module_init(void) +{ + TRACE_CALL(__func__); + + if (PyImport_AppendInittab("remmina", python_wrapper_module_initialize)) + { + PyErr_Print(); + exit(1); + } + + python_wrapper_entry_init(); + python_wrapper_protocol_init(); + python_wrapper_tool_init(); + python_wrapper_pref_init(); + python_wrapper_secret_init(); + python_wrapper_file_init(); +} + +gboolean python_wrapper_check_mandatory_member(PyObject* instance, const gchar* member) +{ + TRACE_CALL(__func__); + + if (PyObject_HasAttrString(instance, member)) + { + return TRUE; + } + + g_printerr("Missing mandatory member '%s' in Python plugin instance!\n", member); + return FALSE; +} + +static PyObject* remmina_register_plugin_wrapper(PyObject* self, PyObject* plugin_instance) +{ + TRACE_CALL(__func__); + + if (plugin_instance) + { + if (!python_wrapper_check_mandatory_member(plugin_instance, "name") + || !python_wrapper_check_mandatory_member(plugin_instance, "version")) + { + return NULL; + } + + /* Protocol plugin definition and features */ + const gchar* pluginType = PyUnicode_AsUTF8(PyObject_GetAttrString(plugin_instance, "type")); + + RemminaPlugin* remmina_plugin = NULL; + + PyPlugin* plugin = (PyPlugin*)python_wrapper_malloc(sizeof(PyPlugin)); + plugin->instance = plugin_instance; + Py_INCREF(plugin_instance); + plugin->protocol_plugin = NULL; + plugin->entry_plugin = NULL; + plugin->file_plugin = NULL; + plugin->pref_plugin = NULL; + plugin->secret_plugin = NULL; + plugin->tool_plugin = NULL; + g_print("New Python plugin registered: %ld\n", PyObject_Hash(plugin_instance)); + + if (g_str_equal(pluginType, "protocol")) + { + remmina_plugin = python_wrapper_create_protocol_plugin(plugin); + } + else if (g_str_equal(pluginType, "entry")) + { + remmina_plugin = python_wrapper_create_entry_plugin(plugin); + } + else if (g_str_equal(pluginType, "file")) + { + remmina_plugin = python_wrapper_create_file_plugin(plugin); + } + else if (g_str_equal(pluginType, "tool")) + { + remmina_plugin = python_wrapper_create_tool_plugin(plugin); + } + else if (g_str_equal(pluginType, "pref")) + { + remmina_plugin = python_wrapper_create_pref_plugin(plugin); + } + else if (g_str_equal(pluginType, "secret")) + { + remmina_plugin = python_wrapper_create_secret_plugin(plugin); + } + else + { + g_printerr("Unknown plugin type: %s\n", pluginType); + } + + if (remmina_plugin) + { + if (remmina_plugin->type == REMMINA_PLUGIN_TYPE_PROTOCOL) + { + plugin->gp = python_wrapper_protocol_widget_create(); + } + + if (python_wrapper_get_service()->register_plugin((RemminaPlugin*)remmina_plugin)) { + g_print("%s: Successfully reigstered!\n", remmina_plugin->name); + } else { + g_print("%s: Failed to reigster!\n", remmina_plugin->name); + } + } + } + + PyErr_Clear(); + return Py_None; +} + +static PyObject* remmina_file_get_datadir_wrapper(PyObject* self, PyObject* plugin) +{ + TRACE_CALL(__func__); + + PyObject* result = Py_None; + const gchar* datadir = python_wrapper_get_service()->file_get_user_datadir(); + + if (datadir) + { + result = PyUnicode_FromFormat("%s", datadir); + } + + python_wrapper_check_error(); + return result; +} + +static PyObject* remmina_file_new_wrapper(PyObject* self, PyObject* args, PyObject* kwargs) +{ + TRACE_CALL(__func__); + + RemminaFile* file = python_wrapper_get_service()->file_new(); + if (file) + { + return (PyObject*)python_wrapper_remmina_file_to_python(file); + } + + python_wrapper_check_error(); + return Py_None; +} + +static PyObject* remmina_unlock_new_wrapper(PyObject* self, PyObject* args, PyObject* kwargs) +{ + TRACE_CALL(__func__); + + static char* kwlist[] = { "window", NULL}; + GtkWindow* window = NULL; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|0", kwlist, &window)) + { + return Py_None; + } + + return PyBool_FromLong(python_wrapper_get_service()->plugin_unlock_new(window)); +} + +static PyObject* remmina_pref_set_value_wrapper(PyObject* self, PyObject* args, PyObject* kwargs) +{ + TRACE_CALL(__func__); + + static char* kwlist[] = { "key", "value", NULL }; + gchar* key, * value; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ss", kwlist, &key, &value)) + { + return Py_None; + } + + if (key) + { + python_wrapper_get_service()->pref_set_value(key, value); + } + + python_wrapper_check_error(); + return Py_None; +} + +static PyObject* remmina_pref_get_value_wrapper(PyObject* self, PyObject* args, PyObject* kwargs) +{ + TRACE_CALL(__func__); + + static char* kwlist[] = { "key", NULL }; + gchar* key; + PyObject* result = Py_None; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s", kwlist, &key)) + { + return Py_None; + } + + if (key) + { + const gchar* value = python_wrapper_get_service()->pref_get_value(key); + if (value) + { + result = PyUnicode_FromFormat("%s", value); + } + } + + python_wrapper_check_error(); + return result; +} + +static PyObject* remmina_pref_get_scale_quality_wrapper(PyObject* self, PyObject* plugin) +{ + TRACE_CALL(__func__); + + PyObject* result = PyLong_FromLong(python_wrapper_get_service()->pref_get_scale_quality()); + python_wrapper_check_error(); + return result; +} + +static PyObject* remmina_pref_get_sshtunnel_port_wrapper(PyObject* self, PyObject* plugin) +{ + TRACE_CALL(__func__); + + PyObject* result = PyLong_FromLong(python_wrapper_get_service()->pref_get_sshtunnel_port()); + python_wrapper_check_error(); + return result; +} + +static PyObject* remmina_pref_get_ssh_loglevel_wrapper(PyObject* self, PyObject* plugin) +{ + TRACE_CALL(__func__); + + PyObject* result = PyLong_FromLong(python_wrapper_get_service()->pref_get_ssh_loglevel()); + python_wrapper_check_error(); + return result; +} + +static PyObject* remmina_pref_get_ssh_parseconfig_wrapper(PyObject* self, PyObject* plugin) +{ + TRACE_CALL(__func__); + + PyObject* result = PyLong_FromLong(python_wrapper_get_service()->pref_get_ssh_parseconfig()); + python_wrapper_check_error(); + return result; +} + +static PyObject* remmina_pref_keymap_get_keyval_wrapper(PyObject* self, PyObject* args, PyObject* kwargs) +{ + TRACE_CALL(__func__); + + static char* kwlist[] = { "keymap", "keyval", NULL }; + gchar* keymap; + guint keyval; + PyObject* result = Py_None; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "sl", kwlist, &keymap, &keyval)) + { + return PyLong_FromLong(-1); + } + + if (keymap) + { + const guint value = python_wrapper_get_service()->pref_keymap_get_keyval(keymap, keyval); + result = PyLong_FromUnsignedLong(value); + } + + python_wrapper_check_error(); + return result; +} + +static PyObject* python_wrapper_log_print_wrapper(PyObject* self, PyObject* args) +{ + TRACE_CALL(__func__); + + gchar* text; + if (!PyArg_ParseTuple(args, "s", &text) || !text) + { + return Py_None; + } + + python_wrapper_get_service()->log_print(text); + return Py_None; +} + +static PyObject* python_wrapper_debug_wrapper(PyObject* self, PyObject* args) +{ + TRACE_CALL(__func__); + + gchar* text; + if (!PyArg_ParseTuple(args, "s", &text) || !text) + { + return Py_None; + } + + python_wrapper_get_service()->_remmina_debug("python", "%s", text); + return Py_None; +} + +static PyObject* remmina_widget_pool_register_wrapper(PyObject* self, PyObject* args, PyObject* kwargs) +{ + TRACE_CALL(__func__); + + static char* kwlist[] = { "widget", NULL }; + PyObject* widget; + + if (PyArg_ParseTupleAndKeywords(args, kwargs, "O", kwlist, &widget) && widget) + { + python_wrapper_get_service()->widget_pool_register(get_pywidget(widget)); + } + + return Py_None; +} + +static PyObject* rcw_open_from_file_full_wrapper(PyObject* self, PyObject* args, PyObject* kwargs) +{ + TRACE_CALL(__func__); + + static char* kwlist[] = { "remminafile", "data", "handler", NULL }; + PyObject* pyremminafile; + PyObject* data; + + if (PyArg_ParseTupleAndKeywords(args, kwargs, "OOO", kwlist, &pyremminafile, &data) && pyremminafile && data) + { + python_wrapper_get_service()->rcw_open_from_file_full((RemminaFile*)pyremminafile, NULL, (void*)data, NULL); + } + + return Py_None; +} + +static PyObject* remmina_public_get_server_port_wrapper(PyObject* self, PyObject* args, PyObject* kwargs) +{ + TRACE_CALL(__func__); + + static char* kwlist[] = { "server", "defaultport", NULL }; + gchar* server; + gint defaultport; + + if (PyArg_ParseTupleAndKeywords(args, kwargs, "sl", kwlist, &server, &defaultport) && server) + { + gchar* host; + gint port; + python_wrapper_get_service()->get_server_port(server, defaultport, &host, &port); + + PyObject* result = PyList_New(2); + PyList_SetItem(result, 0, PyUnicode_FromString(host)); + PyList_SetItem(result, 1, PyLong_FromLong(port)); + + return PyList_AsTuple(result); + } + + return Py_None; +} + +static PyObject* remmina_masterthread_exec_is_main_thread_wrapper(PyObject* self, PyObject* plugin) +{ + TRACE_CALL(__func__); + + return PyBool_FromLong(python_wrapper_get_service()->is_main_thread()); +} + +static PyObject* remmina_gtksocket_available_wrapper(PyObject* self, PyObject* plugin) +{ + TRACE_CALL(__func__); + + return PyBool_FromLong(python_wrapper_get_service()->gtksocket_available()); +} + +static PyObject* +remmina_protocol_widget_get_profile_remote_height_wrapper(PyObject* self, PyObject* args, PyObject* kwargs) +{ + TRACE_CALL(__func__); + + static char* kwlist[] = { "widget", NULL }; + PyPlugin* plugin; + + if (PyArg_ParseTupleAndKeywords(args, kwargs, "O", kwlist, &plugin) && plugin && plugin->gp) + { + python_wrapper_get_service()->get_profile_remote_height(plugin->gp->gp); + } + + return Py_None; +} + +static PyObject* +remmina_protocol_widget_get_profile_remote_width_wrapper(PyObject* self, PyObject* args, PyObject* kwargs) +{ + TRACE_CALL(__func__); + + static char* kwlist[] = { "widget", NULL }; + PyPlugin* plugin; + + if (PyArg_ParseTupleAndKeywords(args, kwargs, "O", kwlist, &plugin) && plugin && plugin->gp) + { + python_wrapper_get_service()->get_profile_remote_width(plugin->gp->gp); + } + + return Py_None; +} + +void python_wrapper_to_protocol_setting(RemminaProtocolSetting* dest, PyObject* setting) +{ + TRACE_CALL(__func__); + + PyRemminaProtocolSetting* src = (PyRemminaProtocolSetting*)setting; + Py_INCREF(setting); + dest->name = src->name; + dest->label = src->label; + dest->compact = src->compact; + dest->type = src->settingType; + dest->validator = NULL; + dest->validator_data = NULL; + python_wrapper_to_generic(src->opt1, &dest->opt1); + python_wrapper_to_generic(src->opt2, &dest->opt2); +} + +void python_wrapper_to_protocol_feature(RemminaProtocolFeature* dest, PyObject* feature) +{ + TRACE_CALL(__func__); + + PyRemminaProtocolFeature* src = (PyRemminaProtocolFeature*)feature; + Py_INCREF(feature); + dest->id = src->id; + dest->type = src->type; + dest->opt1 = src->opt1->raw; + dest->opt1_type_hint = src->opt1->type_hint; + dest->opt2 = src->opt2->raw; + dest->opt2_type_hint = src->opt2->type_hint; + dest->opt3 = src->opt3->raw; + dest->opt3_type_hint = src->opt3->type_hint; +} + +PyObject* python_wrapper_show_dialog_wrapper(PyObject* self, PyObject* args, PyObject* kwargs) +{ + TRACE_CALL(__func__); + + static char* kwlist[] = { "type", "buttons", "message", NULL }; + GtkMessageType msgType; + GtkButtonsType btnType; + gchar* message; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "lls", kwlist, &msgType, &btnType, &message)) + { + return PyLong_FromLong(-1); + } + + python_wrapper_get_service()->show_dialog(msgType, btnType, message); + + return Py_None; +} + +PyObject* python_wrapper_get_mainwindow_wrapper(PyObject* self, PyObject* args) +{ + TRACE_CALL(__func__); + + GtkWindow* result = python_wrapper_get_service()->get_window(); + + if (!result) + { + return Py_None; + } + + return (PyObject*)new_pywidget((GObject*)result); +} + +static PyObject* remmina_protocol_plugin_signal_connection_closed_wrapper(PyObject* self, PyObject* args) +{ + TRACE_CALL(__func__); + + PyObject* pygp = NULL; + if (!PyArg_ParseTuple(args, "O", &pygp) || !pygp) + { + g_printerr("Please provide the Remmina protocol widget instance!"); + return Py_None; + } + + python_wrapper_get_service()->protocol_plugin_signal_connection_closed(((PyRemminaProtocolWidget*)pygp)->gp); + return Py_None; +} + +static PyObject* remmina_protocol_plugin_init_auth_wrapper(PyObject* module, PyObject* args, PyObject* kwds) +{ + TRACE_CALL(__func__); + + static gchar* keyword_list[] = { "widget", "flags", "title", "default_username", "default_password", + "default_domain", "password_prompt" }; + + PyRemminaProtocolWidget* self; + gint pflags = 0; + gchar* title, * default_username, * default_password, * default_domain, * password_prompt; + + if (PyArg_ParseTupleAndKeywords(args, kwds, "Oisssss", keyword_list, &self, &pflags, &title, &default_username, + &default_password, &default_domain, &password_prompt)) + { + if (pflags != 0 && !(pflags & REMMINA_MESSAGE_PANEL_FLAG_USERNAME) + && !(pflags & REMMINA_MESSAGE_PANEL_FLAG_USERNAME_READONLY) + && !(pflags & REMMINA_MESSAGE_PANEL_FLAG_DOMAIN) + && !(pflags & REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD)) + { + g_printerr("panel_auth(pflags, title, default_username, default_password, default_domain, password_prompt): " + "%d is not a known value for RemminaMessagePanelFlags!\n", pflags); + } + else + { + return Py_BuildValue("i", python_wrapper_get_service()->protocol_widget_panel_auth(self + ->gp, pflags, title, default_username, default_password, default_domain, password_prompt)); + } + } + else + { + g_printerr("panel_auth(pflags, title, default_username, default_password, default_domain, password_prompt): Error parsing arguments!\n"); + PyErr_Print(); + } + return Py_None; +} + +static PyObject* remmina_protocol_plugin_signal_connection_opened_wrapper(PyObject* self, PyObject* args) +{ + PyObject* pygp = NULL; + if (!PyArg_ParseTuple(args, "O", &pygp) || !pygp) + { + g_printerr("Please provide the Remmina protocol widget instance!"); + return Py_None; + } + + python_wrapper_get_service()->protocol_plugin_signal_connection_opened(((PyRemminaProtocolWidget*)pygp)->gp); + return Py_None; +} diff --git a/plugins/python_wrapper/python_wrapper_remmina.h b/plugins/python_wrapper/python_wrapper_remmina.h new file mode 100644 index 0000000..4b3df13 --- /dev/null +++ b/plugins/python_wrapper/python_wrapper_remmina.h @@ -0,0 +1,93 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo + * + * 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. + */ + +/** + * @file python_wrapper_remmina.h + * + * @brief Contains the implementation of the Python module 'remmina', provided to interface with the application from + * the Python plugin source. + * + * @detail In contrast to the wrapper functions that exist in the plugin specialisation files (e.g. + * python_wrapper_protocol.c or python_wrapper_entry.c), this file contains the API for the + * communication in the direction, from Python to Remmina. This means, if in the Python plugin a function + * is called, that is defined in Remmina, C code, at least in this file, is executed. + */ + +#pragma once + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// I N C L U D E S +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "python_wrapper_common.h" +#include "python_wrapper_protocol_widget.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// A P I +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +G_BEGIN_DECLS + +/** + * Initializes the 'remmina' module in the Python engine. + */ +void python_wrapper_module_init(void); + +/** + * @brief Returns a pointer to the Python instance, mapped to the RemminaProtocolWidget or null if not found. + * + * @param gp The widget that is owned by the plugin that should be found. + * + * @details Remmina expects this callback function to be part of one plugin, which is the reason no instance information + * is explicitly passed. To bridge that, this function can be used to retrieve the very plugin instance owning + * the given RemminaProtocolWidget. + */ +PyPlugin* python_wrapper_module_get_plugin(RemminaProtocolWidget* gp); + +/** + * @brief Converts the PyObject to RemminaProtocolSetting. + * + * @param dest A target for the converted value. + * @param setting The source value to convert. + */ +void python_wrapper_to_protocol_setting(RemminaProtocolSetting* dest, PyObject* setting); + +/** + * @brief Converts the PyObject to RemminaProtocolFeature. + * + * @param dest A target for the converted value. + * @param setting The source value to convert. + */ +void python_wrapper_to_protocol_feature(RemminaProtocolFeature* dest, PyObject* feature); + +G_END_DECLS diff --git a/plugins/python_wrapper/python_wrapper_remmina_file.c b/plugins/python_wrapper/python_wrapper_remmina_file.c new file mode 100644 index 0000000..001c940 --- /dev/null +++ b/plugins/python_wrapper/python_wrapper_remmina_file.c @@ -0,0 +1,205 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * 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. + */ + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// I N C L U D E S +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + +#include "remmina/remmina_trace_calls.h" +#include "python_wrapper_common.h" +#include "python_wrapper_remmina_file.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// D E C L A R A T I O N S +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +static PyObject* file_get_path(PyRemminaFile* self, PyObject* args); +static PyObject* file_set_setting(PyRemminaFile* self, PyObject* args, PyObject* kwds); +static PyObject* file_get_setting(PyRemminaFile* self, PyObject* args, PyObject* kwds); +static PyObject* file_get_secret(PyRemminaFile* self, PyObject* setting); +static PyObject* file_unsave_passwords(PyRemminaFile* self, PyObject* args); + +static void file_dealloc(PyObject* self) +{ + PyObject_Del(self); +} + +static PyMethodDef python_remmina_file_type_methods[] = { + { "get_path", (PyCFunction)file_get_path, METH_NOARGS, "" }, + { "set_setting", (PyCFunction)file_set_setting, METH_VARARGS | METH_KEYWORDS, "Set file setting" }, + { "get_setting", (PyCFunction)file_get_setting, METH_VARARGS | METH_KEYWORDS, "Get file setting" }, + { "get_secret", (PyCFunction)file_get_secret, METH_VARARGS, "Get secret file setting" }, + { "unsave_passwords", (PyCFunction)file_unsave_passwords, METH_NOARGS, "" }, + { NULL } +}; + +/** + * @brief The definition of the Python module 'remmina'. + */ +static PyTypeObject python_remmina_file_type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "remmina.RemminaFile", + .tp_doc = "", + .tp_basicsize = sizeof(PyRemminaFile), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = python_remmina_file_type_methods, + .tp_dealloc = file_dealloc +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// A P I +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +void python_wrapper_remmina_init_types(void) +{ + PyType_Ready(&python_remmina_file_type); +} + +PyRemminaFile* python_wrapper_remmina_file_to_python(RemminaFile* file) +{ + TRACE_CALL(__func__); + + PyRemminaFile* result = PyObject_New(PyRemminaFile, &python_remmina_file_type); + result->file = file; + Py_INCREF(result); + return result; +} + +static PyObject* file_get_path(PyRemminaFile* self, PyObject* args) +{ + TRACE_CALL(__func__); + + return Py_BuildValue("s", python_wrapper_get_service()->file_get_path(self->file)); +} + +static PyObject* file_set_setting(PyRemminaFile* self, PyObject* args, PyObject* kwds) +{ + TRACE_CALL(__func__); + + static char* keyword_list[] = { "key", "value", NULL }; + gchar* key; + PyObject* value; + + if (PyArg_ParseTupleAndKeywords(args, kwds, "s|O", keyword_list, &key, &value)) + { + if (PyUnicode_Check(value)) + { + python_wrapper_get_service()->file_set_string(self->file, key, PyUnicode_AsUTF8(value)); + } + else if (PyLong_Check(value)) + { + python_wrapper_get_service()->file_set_int(self->file, key, PyLong_AsLong(value)); + } + else + { + g_printerr("%s: Not a string or int value\n", PyUnicode_AsUTF8(PyObject_Str(value))); + } + return Py_None; + } + else + { + g_printerr("file.set_setting(key, value): Error parsing arguments!\n"); + PyErr_Print(); + return NULL; + } +} + +static PyObject* file_get_setting(PyRemminaFile* self, PyObject* args, PyObject* kwds) +{ + TRACE_CALL(__func__); + + static gchar* keyword_list[] = { "key", "default" }; + gchar* key; + PyObject* def; + + if (PyArg_ParseTupleAndKeywords(args, kwds, "sO", keyword_list, &key, &def)) + { + if (PyUnicode_Check(def)) + { + return Py_BuildValue("s", python_wrapper_get_service()->file_get_string(self->file, key)); + } + else if (PyBool_Check(def)) + { + return python_wrapper_get_service()->file_get_int(self->file, key, (gint)PyLong_AsLong(def)) ? Py_True : Py_False; + } + else if (PyLong_Check(def)) + { + return Py_BuildValue("i", python_wrapper_get_service()->file_get_int(self->file, key, (gint)PyLong_AsLong(def))); + } + else + { + g_printerr("%s: Not a string or int value\n", PyUnicode_AsUTF8(PyObject_Str(def))); + } + return def; + } + else + { + g_printerr("file.get_setting(key, default): Error parsing arguments!\n"); + PyErr_Print(); + return Py_None; + } +} + +static PyObject* file_get_secret(PyRemminaFile* self, PyObject* key) +{ + TRACE_CALL(__func__); + + if (key && PyUnicode_Check(key)) + { + return Py_BuildValue("s", python_wrapper_get_service()->file_get_secret(self->file, PyUnicode_AsUTF8(key))); + } + else + { + g_printerr("file.get_secret(key): Error parsing arguments!\n"); + PyErr_Print(); + return NULL; + } +} + +static PyObject* file_unsave_passwords(PyRemminaFile* self, PyObject* args) +{ + TRACE_CALL(__func__); + + if (self) + { + python_wrapper_get_service()->file_unsave_passwords(self->file); + return Py_None; + } + else + { + g_printerr("unsave_passwords(): self is null!\n"); + return NULL; + } +} diff --git a/plugins/python_wrapper/python_wrapper_remmina_file.h b/plugins/python_wrapper/python_wrapper_remmina_file.h new file mode 100644 index 0000000..2899ae0 --- /dev/null +++ b/plugins/python_wrapper/python_wrapper_remmina_file.h @@ -0,0 +1,63 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * 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. + */ + + +/** + * @file python_wrapper_protocol.h + * + * @brief Contains the implementation of the Python type remmina.RemminaFile. + */ + +#pragma once + +#include "python_wrapper_common.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// A P I +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Wrapper for a Python object that contains a pointer to an instance of RemminaFile. + */ +typedef struct +{ + PyObject_HEAD + RemminaFile* file; +} PyRemminaFile; + +void python_wrapper_remmina_init_types(void); + +/** + * Converts the instance of RemminaFile to a Python object that can be passed to the Python engine. + */ +PyRemminaFile* python_wrapper_remmina_file_to_python(RemminaFile* file); diff --git a/plugins/python_wrapper/python_wrapper_secret.c b/plugins/python_wrapper/python_wrapper_secret.c new file mode 100644 index 0000000..1f34176 --- /dev/null +++ b/plugins/python_wrapper/python_wrapper_secret.c @@ -0,0 +1,138 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2014-2021 Antenore Gatta, Giovanni Panozzo, Mathias Winterhalter (ToolsDevler) + * + * 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. + */ + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// I N L U C E S +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "python_wrapper_common.h" +#include "python_wrapper_secret.h" +#include "python_wrapper_remmina_file.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// A P I +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +void python_wrapper_secret_init(void) +{ + TRACE_CALL(__func__); +} + +gboolean python_wrapper_secret_init_wrapper(RemminaSecretPlugin* instance) +{ + TRACE_CALL(__func__); + + PyPlugin* plugin = python_wrapper_get_plugin(instance->name); + PyObject* result = CallPythonMethod(plugin->instance, "init", NULL); + return result == Py_None || result != Py_False; +} + +gboolean python_wrapper_secret_is_service_available_wrapper(RemminaSecretPlugin* instance) +{ + TRACE_CALL(__func__); + + PyPlugin* plugin = python_wrapper_get_plugin(instance->name); + PyObject* result = CallPythonMethod(plugin->instance, "is_service_available", NULL); + return result == Py_None || result != Py_False; +} + +void +python_wrapper_secret_store_password_wrapper(RemminaSecretPlugin* instance, RemminaFile* file, const gchar* key, const gchar* password) +{ + TRACE_CALL(__func__); + + PyPlugin* plugin = python_wrapper_get_plugin(instance->name); + CallPythonMethod(plugin + ->instance, "store_password", "Oss", (PyObject*)python_wrapper_remmina_file_to_python(file), key, password); +} + +gchar* +python_wrapper_secret_get_password_wrapper(RemminaSecretPlugin* instance, RemminaFile* file, const gchar* key) +{ + TRACE_CALL(__func__); + + PyPlugin* plugin = python_wrapper_get_plugin(instance->name); + PyObject* result = CallPythonMethod(plugin + ->instance, "get_password", "Os", (PyObject*)python_wrapper_remmina_file_to_python(file), key); + Py_ssize_t len = PyUnicode_GetLength(result); + if (len == 0) + { + return NULL; + } + + return python_wrapper_copy_string_from_python(result, len); +} + +void +python_wrapper_secret_delete_password_wrapper(RemminaSecretPlugin* instance, RemminaFile* file, const gchar* key) +{ + TRACE_CALL(__func__); + + PyPlugin* plugin = python_wrapper_get_plugin(instance->name); + CallPythonMethod(plugin + ->instance, "delete_password", "Os", (PyObject*)python_wrapper_remmina_file_to_python(file), key); +} + +RemminaPlugin* python_wrapper_create_secret_plugin(PyPlugin* plugin) +{ + TRACE_CALL(__func__); + + PyObject* instance = plugin->instance; + + if (!python_wrapper_check_attribute(instance, ATTR_NAME)) + { + return NULL; + } + + RemminaSecretPlugin* remmina_plugin = (RemminaSecretPlugin*)python_wrapper_malloc(sizeof(RemminaSecretPlugin)); + + remmina_plugin->type = REMMINA_PLUGIN_TYPE_SECRET; + remmina_plugin->domain = GETTEXT_PACKAGE; + remmina_plugin->name = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_NAME)); + remmina_plugin->version = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_VERSION)); + remmina_plugin->description = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_DESCRIPTION)); + remmina_plugin->init_order = PyLong_AsLong(PyObject_GetAttrString(instance, ATTR_INIT_ORDER)); + + remmina_plugin->init = python_wrapper_secret_init_wrapper; + remmina_plugin->is_service_available = python_wrapper_secret_is_service_available_wrapper; + remmina_plugin->store_password = python_wrapper_secret_store_password_wrapper; + remmina_plugin->get_password = python_wrapper_secret_get_password_wrapper; + remmina_plugin->delete_password = python_wrapper_secret_delete_password_wrapper; + + plugin->secret_plugin = remmina_plugin; + plugin->generic_plugin = (RemminaPlugin*)remmina_plugin; + + python_wrapper_add_plugin(plugin); + + return (RemminaPlugin*)remmina_plugin; +}
\ No newline at end of file diff --git a/plugins/python_wrapper/python_wrapper_secret.h b/plugins/python_wrapper/python_wrapper_secret.h new file mode 100644 index 0000000..b40e058 --- /dev/null +++ b/plugins/python_wrapper/python_wrapper_secret.h @@ -0,0 +1,68 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2014-2021 Antenore Gatta, Giovanni Panozzo, Mathias Winterhalter (ToolsDevler) + * + * 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. + */ + +/** + * @file python_wrapper_entry.h + * + * @brief Contains the specialisation of RemminaPluginEntry plugins in Python. + */ + +#pragma once + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// I N L U C E S +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "python_wrapper_common.h" +#include "remmina/plugin.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// A P I +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +G_BEGIN_DECLS + +/** + * Initializes the Python plugin specialisation for secret plugins. + */ +void python_wrapper_secret_init(void); + +/** + * @brief Creates a new instance of the RemminaPluginSecret, initializes its members and references the wrapper + * functions. + * @param instance The instance of the Python plugin. + * @return Returns a new instance of the RemminaPlugin (must be freed!). + */ +RemminaPlugin* python_wrapper_create_secret_plugin(PyPlugin* instance); + +G_END_DECLS diff --git a/plugins/python_wrapper/python_wrapper_tool.c b/plugins/python_wrapper/python_wrapper_tool.c new file mode 100644 index 0000000..db721b6 --- /dev/null +++ b/plugins/python_wrapper/python_wrapper_tool.c @@ -0,0 +1,84 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2014-2021 Antenore Gatta, Giovanni Panozzo, Mathias Winterhalter (ToolsDevler) + * + * 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. + */ + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// I N L U C E S +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "python_wrapper_common.h" +#include "python_wrapper_tool.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// A P I +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +void python_wrapper_tool_init(void) +{ + TRACE_CALL(__func__); +} + +void python_wrapper_tool_exec_func_wrapper(GtkMenuItem* self, RemminaToolPlugin* instance) +{ + TRACE_CALL(__func__); + + PyPlugin* plugin = python_wrapper_get_plugin(instance->name); + CallPythonMethod(plugin->instance, "exec_func", NULL); +} + +RemminaPlugin* python_wrapper_create_tool_plugin(PyPlugin* plugin) +{ + TRACE_CALL(__func__); + + PyObject* instance = plugin->instance; + + if (!python_wrapper_check_attribute(instance, ATTR_NAME)) + { + return NULL; + } + + RemminaToolPlugin* remmina_plugin = (RemminaToolPlugin*)python_wrapper_malloc(sizeof(RemminaToolPlugin)); + + remmina_plugin->type = REMMINA_PLUGIN_TYPE_TOOL; + remmina_plugin->domain = GETTEXT_PACKAGE; + remmina_plugin->name = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_NAME)); + remmina_plugin->version = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_VERSION)); + remmina_plugin->description = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_DESCRIPTION)); + remmina_plugin->exec_func = python_wrapper_tool_exec_func_wrapper; + + plugin->tool_plugin = remmina_plugin; + plugin->generic_plugin = (RemminaPlugin*)remmina_plugin; + + python_wrapper_add_plugin(plugin); + + return (RemminaPlugin*)remmina_plugin; +} diff --git a/plugins/python_wrapper/python_wrapper_tool.h b/plugins/python_wrapper/python_wrapper_tool.h new file mode 100644 index 0000000..595e7b3 --- /dev/null +++ b/plugins/python_wrapper/python_wrapper_tool.h @@ -0,0 +1,69 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2014-2021 Antenore Gatta, Giovanni Panozzo, Mathias Winterhalter (ToolsDevler) + * + * 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. + */ + +/** + * @file python_wrapper_entry.h + * + * @brief Contains the specialisation of RemminaPluginEntry plugins in Python. + */ + +#pragma once + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// I N L U C E S +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "remmina/plugin.h" +#include "python_wrapper_common.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// A P I +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +G_BEGIN_DECLS + +/** + * Initializes the Python plugin specialisation for tool plugins. + */ +void python_wrapper_tool_init(void); + +/** + * @brief Creates a new instance of the RemminaPluginTool, initializes its members and references the wrapper + * functions. + * @param instance The instance of the Python plugin. + * @return Returns a new instance of the RemminaPlugin (must be freed!). + */ +RemminaPlugin* python_wrapper_create_tool_plugin(PyPlugin* instance); + +G_END_DECLS + diff --git a/plugins/rdp/CMakeLists.txt b/plugins/rdp/CMakeLists.txt new file mode 100644 index 0000000..da4798b --- /dev/null +++ b/plugins/rdp/CMakeLists.txt @@ -0,0 +1,88 @@ +# remmina-plugin-rdp - The GTK+ Remote Desktop Client +# +# Copyright (C) 2011 Marc-Andre Moreau +# Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo +# Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo +# +# 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(CMAKE_THREAD_PREFER_PTHREAD TRUE) +find_package(Threads REQUIRED) + +find_suggested_package(Cups) + +set(REMMINA_PLUGIN_RDP_SRCS + rdp_plugin.c + rdp_plugin.h + rdp_event.c + rdp_event.h + rdp_file.c + rdp_file.h + rdp_settings.c + rdp_settings.h + rdp_graphics.c + rdp_graphics.h + rdp_cliprdr.c + rdp_cliprdr.h + rdp_monitor.c + rdp_monitor.h + rdp_channels.c + rdp_channels.h + ) + +add_definitions(-DFREERDP_REQUIRED_MAJOR=${FREERDP_REQUIRED_MAJOR}) +add_definitions(-DFREERDP_REQUIRED_MINOR=${FREERDP_REQUIRED_MINOR}) +add_definitions(-DFREERDP_REQUIRED_REVISION=${FREERDP_REQUIRED_REVISION}) + +add_library(remmina-plugin-rdp MODULE ${REMMINA_PLUGIN_RDP_SRCS}) +set_target_properties(remmina-plugin-rdp PROPERTIES PREFIX "") +set_target_properties(remmina-plugin-rdp PROPERTIES NO_SONAME 1) + +include_directories(${REMMINA_COMMON_INCLUDE_DIRS} ${FreeRDP-Client_INCLUDE_DIR} ${FreeRDP_INCLUDE_DIR} ${WinPR_INCLUDE_DIR}) +target_link_libraries(remmina-plugin-rdp + ${REMMINA_COMMON_LIBRARIES} freerdp-client) + +if(CUPS_FOUND) + add_definitions(-DHAVE_CUPS) + include_directories(${CUPS_INCLUDE_DIR}) + target_link_libraries(remmina-plugin-rdp + ${REMMINA_COMMON_LIBRARIES} freerdp-client ${CUPS_LIBRARIES}) +endif() + +install(TARGETS remmina-plugin-rdp DESTINATION ${REMMINA_PLUGINDIR}) + +install(FILES + scalable/emblems/org.remmina.Remmina-rdp-ssh-symbolic.svg + scalable/emblems/org.remmina.Remmina-rdp-symbolic.svg + DESTINATION ${APPICONSCALE_EMBLEMS_DIR}) + +if(WITH_ICON_CACHE) + gtk_update_icon_cache("${REMMINA_DATADIR}/icons/hicolor") +endif() diff --git a/plugins/rdp/rdp_channels.c b/plugins/rdp/rdp_channels.c new file mode 100644 index 0000000..fabb25f --- /dev/null +++ b/plugins/rdp/rdp_channels.c @@ -0,0 +1,96 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2012-2012 Jean-Louis Dupond + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * 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 "rdp_plugin.h" +#include "rdp_cliprdr.h" +#include "rdp_channels.h" +#include "rdp_event.h" + +#include <freerdp/freerdp.h> +#include <freerdp/channels/channels.h> +#include <freerdp/client/cliprdr.h> +#include <freerdp/gdi/gfx.h> + +void remmina_rdp_OnChannelConnectedEventHandler(void *context, CONST_ARG ChannelConnectedEventArgs *e) +{ + TRACE_CALL(__func__); + + rfContext* rfi = (rfContext*)context; + + if (g_strcmp0(e->name, RDPEI_DVC_CHANNEL_NAME) == 0) { + g_print("Unimplemented: channel %s connected but we can’t use it\n", e->name); + // xfc->rdpei = (RdpeiClientContext*) e->pInterface; + }else if (g_strcmp0(e->name, TSMF_DVC_CHANNEL_NAME) == 0) { + g_print("Unimplemented: channel %s connected but we can’t use it\n", e->name); + // xf_tsmf_init(xfc, (TsmfClientContext*) e->pInterface); + }else if (g_strcmp0(e->name, RDPGFX_DVC_CHANNEL_NAME) == 0) { + if (freerdp_settings_get_bool(rfi->clientContext.context.settings, FreeRDP_SoftwareGdi)) { + rfi->rdpgfxchan = TRUE; + gdi_graphics_pipeline_init(rfi->clientContext.context.gdi, (RdpgfxClientContext*) e->pInterface); + } + else + g_print("Unimplemented: channel %s connected but libfreerdp is in HardwareGdi mode\n", e->name); + }else if (g_strcmp0(e->name, RAIL_SVC_CHANNEL_NAME) == 0) { + g_print("Unimplemented: channel %s connected but we can’t use it\n", e->name); + // xf_rail_init(xfc, (RailClientContext*) e->pInterface); + }else if (g_strcmp0(e->name, CLIPRDR_SVC_CHANNEL_NAME) == 0) { + remmina_rdp_cliprdr_init( rfi, (CliprdrClientContext*)e->pInterface); + }else if (g_strcmp0(e->name, ENCOMSP_SVC_CHANNEL_NAME) == 0) { + g_print("Unimplemented: channel %s connected but we can’t use it\n", e->name); + // xf_encomsp_init(xfc, (EncomspClientContext*) e->pInterface); + }else if (g_strcmp0(e->name, DISP_DVC_CHANNEL_NAME) == 0) { + // "disp" channel connected, save its context pointer + rfi->dispcontext = (DispClientContext*)e->pInterface; + // Notify rcw to unlock dynres capability + remmina_plugin_service->protocol_plugin_unlock_dynres(rfi->protocol_widget); + // Send monitor layout message here to ask for resize of remote desktop now + if (rfi->scale == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES) { + remmina_rdp_event_send_delayed_monitor_layout(rfi->protocol_widget); + } + } + REMMINA_PLUGIN_DEBUG("Channel %s has been opened", e->name); +} + +void remmina_rdp_OnChannelDisconnectedEventHandler(void *context, CONST_ARG ChannelDisconnectedEventArgs *e) { + TRACE_CALL(__func__); + rfContext* rfi = (rfContext*)context; + + if (strcmp(e->name, RDPGFX_DVC_CHANNEL_NAME) == 0) { + if (freerdp_settings_get_bool(rfi->clientContext.context.settings, FreeRDP_SoftwareGdi)) + gdi_graphics_pipeline_uninit(rfi->clientContext.context.gdi, (RdpgfxClientContext*) e->pInterface); + } + REMMINA_PLUGIN_DEBUG("Channel %s has been closed", e->name); + +} diff --git a/plugins/rdp/rdp_channels.h b/plugins/rdp/rdp_channels.h new file mode 100644 index 0000000..f614610 --- /dev/null +++ b/plugins/rdp/rdp_channels.h @@ -0,0 +1,62 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2012-2012 Jean-Louis Dupond + * Copyright (C) 2017-2023 Antenore Gatta, Giovanni Panozzo + * + * 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 <glib.h> +#include <freerdp/version.h> +#include <freerdp/freerdp.h> +#include <freerdp/client/channels.h> +#include <freerdp/client/rdpei.h> +#include <freerdp/client/tsmf.h> +#include <freerdp/client/rail.h> +#include <freerdp/client/cliprdr.h> +#include <freerdp/client/rdpgfx.h> +#include <freerdp/client/encomsp.h> + +G_BEGIN_DECLS + +#if FREERDP_VERSION_MAJOR >= 3 +#define CONST_ARG const +#else +#define CONST_ARG +#endif + +void remmina_rdp_OnChannelConnectedEventHandler(void *context, CONST_ARG ChannelConnectedEventArgs *e); +void remmina_rdp_OnChannelDisconnectedEventHandler(void *context, CONST_ARG ChannelDisconnectedEventArgs *e); + + +G_END_DECLS diff --git a/plugins/rdp/rdp_cliprdr.c b/plugins/rdp/rdp_cliprdr.c new file mode 100644 index 0000000..d42aa35 --- /dev/null +++ b/plugins/rdp/rdp_cliprdr.c @@ -0,0 +1,1034 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2012-2012 Jean-Louis Dupond + * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * 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 "rdp_plugin.h" +#include "rdp_cliprdr.h" +#include "rdp_event.h" + +#include <freerdp/freerdp.h> +#include <freerdp/channels/channels.h> +#include <freerdp/client/cliprdr.h> +#include <sys/time.h> + +#define CLIPBOARD_TRANSFER_WAIT_TIME 6 + +#define CB_FORMAT_HTML 0xD010 +#define CB_FORMAT_PNG 0xD011 +#define CB_FORMAT_JPEG 0xD012 +#define CB_FORMAT_GIF 0xD013 +#define CB_FORMAT_TEXTURILIST 0xD014 + +UINT32 remmina_rdp_cliprdr_get_format_from_gdkatom(GdkAtom atom) +{ + TRACE_CALL(__func__); + UINT32 rc; + gchar *name = gdk_atom_name(atom); + rc = 0; + if (g_strcmp0("UTF8_STRING", name) == 0 || g_strcmp0("text/plain;charset=utf-8", name) == 0) + rc = CF_UNICODETEXT; + if (g_strcmp0("TEXT", name) == 0 || g_strcmp0("text/plain", name) == 0) + rc = CF_TEXT; + if (g_strcmp0("text/html", name) == 0) + rc = CB_FORMAT_HTML; + if (g_strcmp0("image/png", name) == 0) + rc = CB_FORMAT_PNG; + if (g_strcmp0("image/jpeg", name) == 0) + rc = CB_FORMAT_JPEG; + if (g_strcmp0("image/bmp", name) == 0) + rc = CF_DIB; + if (g_strcmp0("text/uri-list", name) == 0) + rc = CB_FORMAT_TEXTURILIST; + g_free(name); + return rc; +} + +/* Never used? */ +void remmina_rdp_cliprdr_get_target_types(UINT32 **formats, UINT16 *size, GdkAtom *types, int count) +{ + TRACE_CALL(__func__); + int i; + *size = 1; + *formats = (UINT32 *)malloc(sizeof(UINT32) * (count + 1)); + + *formats[0] = 0; + for (i = 0; i < count; i++) { + UINT32 format = remmina_rdp_cliprdr_get_format_from_gdkatom(types[i]); + if (format != 0) { + (*formats)[*size] = format; + (*size)++; + } + } + + *formats = realloc(*formats, sizeof(UINT32) * (*size)); +} + +static UINT8 *lf2crlf(UINT8 *data, int *size) +{ + TRACE_CALL(__func__); + UINT8 c; + UINT8 *outbuf; + UINT8 *out; + UINT8 *in_end; + UINT8 *in; + int out_size; + + out_size = (*size) * 2 + 1; + outbuf = (UINT8 *)malloc(out_size); + out = outbuf; + in = data; + in_end = data + (*size); + + while (in < in_end) { + c = *in++; + if (c == '\n') { + *out++ = '\r'; + *out++ = '\n'; + } else { + *out++ = c; + } + } + + *out++ = 0; + *size = out - outbuf; + + return outbuf; +} + +static void crlf2lf(UINT8 *data, size_t *size) +{ + TRACE_CALL(__func__); + UINT8 c; + UINT8 *out; + UINT8 *in; + UINT8 *in_end; + + out = data; + in = data; + in_end = data + (*size); + + while (in < in_end) { + c = *in++; + if (c != '\r') + *out++ = c; + } + + *size = out - data; +} + +/* Never used? */ +int remmina_rdp_cliprdr_server_file_contents_request(CliprdrClientContext *context, CLIPRDR_FILE_CONTENTS_REQUEST *fileContentsRequest) +{ + TRACE_CALL(__func__); + return -1; +} + +/* Never used? */ +int remmina_rdp_cliprdr_server_file_contents_response(CliprdrClientContext *context, CLIPRDR_FILE_CONTENTS_RESPONSE *fileContentsResponse) +{ + TRACE_CALL(__func__); + return 1; +} + +void remmina_rdp_cliprdr_send_client_format_list(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginRdpUiObject *ui; + rfContext *rfi = GET_PLUGIN_DATA(gp); + rfClipboard *clipboard; + CLIPRDR_FORMAT_LIST *pFormatList; + RemminaPluginRdpEvent rdp_event = { 0 }; + + if (!rfi || !rfi->connected || rfi->is_reconnecting) + return; + + clipboard = &(rfi->clipboard); + + ui = g_new0(RemminaPluginRdpUiObject, 1); + ui->type = REMMINA_RDP_UI_CLIPBOARD; + ui->clipboard.clipboard = clipboard; + ui->clipboard.type = REMMINA_RDP_UI_CLIPBOARD_FORMATLIST; + pFormatList = remmina_rdp_event_queue_ui_sync_retptr(gp, ui); + + rdp_event.type = REMMINA_RDP_EVENT_TYPE_CLIPBOARD_SEND_CLIENT_FORMAT_LIST; + rdp_event.clipboard_formatlist.pFormatList = pFormatList; + remmina_rdp_event_event_push(gp, &rdp_event); +} + +static void remmina_rdp_cliprdr_send_client_capabilities(rfClipboard *clipboard) +{ + TRACE_CALL(__func__); + CLIPRDR_CAPABILITIES capabilities; + CLIPRDR_GENERAL_CAPABILITY_SET generalCapabilitySet; + + capabilities.cCapabilitiesSets = 1; + capabilities.capabilitySets = (CLIPRDR_CAPABILITY_SET *)&(generalCapabilitySet); + + generalCapabilitySet.capabilitySetType = CB_CAPSTYPE_GENERAL; + generalCapabilitySet.capabilitySetLength = 12; + + generalCapabilitySet.version = CB_CAPS_VERSION_2; + generalCapabilitySet.generalFlags = CB_USE_LONG_FORMAT_NAMES; + + clipboard->context->ClientCapabilities(clipboard->context, &capabilities); +} + +static UINT remmina_rdp_cliprdr_monitor_ready(CliprdrClientContext *context, const CLIPRDR_MONITOR_READY *monitorReady) +{ + TRACE_CALL(__func__); + rfClipboard *clipboard = (rfClipboard *)context->custom; + RemminaProtocolWidget *gp; + + remmina_rdp_cliprdr_send_client_capabilities(clipboard); + gp = clipboard->rfi->protocol_widget; + remmina_rdp_cliprdr_send_client_format_list(gp); + + return CHANNEL_RC_OK; +} + +static UINT remmina_rdp_cliprdr_server_capabilities(CliprdrClientContext *context, const CLIPRDR_CAPABILITIES *capabilities) +{ + TRACE_CALL(__func__); + return CHANNEL_RC_OK; +} + + +void remmina_rdp_cliprdr_cached_clipboard_free(rfClipboard *clipboard) +{ + TRACE_CALL(__func__); + guint fmt; + + pthread_mutex_lock(&clipboard->srv_data_mutex); + if (clipboard->srv_data != NULL) { + fmt = clipboard->format; + if (fmt == CB_FORMAT_PNG || fmt == CF_DIB || fmt == CF_DIBV5 || fmt == CB_FORMAT_JPEG) { + g_object_unref(clipboard->srv_data); + } else { + free(clipboard->srv_data); + } + clipboard->srv_data = NULL; + } + pthread_mutex_unlock(&clipboard->srv_data_mutex); +} + + +static UINT remmina_rdp_cliprdr_server_format_list(CliprdrClientContext *context, const CLIPRDR_FORMAT_LIST *formatList) +{ + TRACE_CALL(__func__); + + /* Called when a user do a "Copy" on the server: we collect all formats + * the server send us and then setup the local clipboard with the appropriate + * targets to request server data */ + + RemminaPluginRdpUiObject *ui; + RemminaProtocolWidget *gp; + rfClipboard *clipboard; + CLIPRDR_FORMAT *format; + CLIPRDR_FORMAT_LIST_RESPONSE formatListResponse; + UINT rc; + + int has_dib_level = 0; + + int i; + + clipboard = (rfClipboard *)context->custom; + gp = clipboard->rfi->protocol_widget; + + REMMINA_PLUGIN_DEBUG("gp=%p: Received a new ServerFormatList from server clipboard. Remmina version = %s", + gp, VERSION); + + GtkTargetList *list = gtk_target_list_new(NULL, 0); + + if (clipboard->srv_clip_data_wait == SCDW_BUSY_WAIT) { + REMMINA_PLUGIN_DEBUG("gp=%p: we already have a FormatDataRequest in progress to the server, aborting", gp); + remmina_rdp_clipboard_abort_client_format_data_request(clipboard->rfi); + } + + remmina_rdp_cliprdr_cached_clipboard_free(clipboard); + clipboard->server_html_format_id = 0; + + REMMINA_PLUGIN_DEBUG("gp=%p: format list from the server:", gp); + for (i = 0; i < formatList->numFormats; i++) { + format = &formatList->formats[i]; + const char *serverFormatName = format->formatName; + gchar *gtkFormatName = NULL; + if (format->formatId == CF_UNICODETEXT) { + serverFormatName = "CF_UNICODETEXT"; + gtkFormatName = "text/plain;charset=utf-8"; + GdkAtom atom = gdk_atom_intern(gtkFormatName, TRUE); + gtk_target_list_add(list, atom, 0, CF_UNICODETEXT); + /* Add also the older UTF8_STRING format for older applications */ + atom = gdk_atom_intern("UTF8_STRING", TRUE); + gtk_target_list_add(list, atom, 0, CF_UNICODETEXT); + } else if (format->formatId == CF_TEXT) { + serverFormatName = "CF_TEXT"; + gtkFormatName = "text/plain"; + GdkAtom atom = gdk_atom_intern(gtkFormatName, TRUE); + gtk_target_list_add(list, atom, 0, CF_TEXT); + /* Add also the older TEXT format for older applications */ + atom = gdk_atom_intern("TEXT", TRUE); + gtk_target_list_add(list, atom, 0, CF_TEXT); + } else if (format->formatId == CF_DIB) { + serverFormatName = "CF_DIB"; + if (has_dib_level < 1) + has_dib_level = 1; + } else if (format->formatId == CF_DIBV5) { + serverFormatName = "CF_DIBV5"; + if (has_dib_level < 5) + has_dib_level = 5; + } else if (format->formatId == CB_FORMAT_JPEG) { + serverFormatName = "CB_FORMAT_JPEG"; + gtkFormatName = "image/jpeg"; + GdkAtom atom = gdk_atom_intern(gtkFormatName, TRUE); + gtk_target_list_add(list, atom, 0, CB_FORMAT_JPEG); + } else if (format->formatId == CB_FORMAT_PNG) { + serverFormatName = "CB_FORMAT_PNG"; + gtkFormatName = "image/png"; + GdkAtom atom = gdk_atom_intern(gtkFormatName, TRUE); + gtk_target_list_add(list, atom, 0, CB_FORMAT_PNG); + } else if (format->formatId == CB_FORMAT_HTML) { + serverFormatName = "CB_FORMAT_HTML"; + gtkFormatName = "text/html"; + GdkAtom atom = gdk_atom_intern(gtkFormatName, TRUE); + gtk_target_list_add(list, atom, 0, CB_FORMAT_HTML); + } else if (format->formatId == CB_FORMAT_TEXTURILIST) { + serverFormatName = "CB_FORMAT_TEXTURILIST"; + gtkFormatName = "text/uri-list"; + GdkAtom atom = gdk_atom_intern(gtkFormatName, TRUE); + gtk_target_list_add(list, atom, 0, CB_FORMAT_TEXTURILIST); + } else if (format->formatId == CF_LOCALE) { + serverFormatName = "CF_LOCALE"; + } else if (format->formatId == CF_METAFILEPICT) { + serverFormatName = "CF_METAFILEPICT"; + } else if (serverFormatName != NULL && strcmp(serverFormatName, "HTML Format") == 0) { + gtkFormatName = "text/html"; + GdkAtom atom = gdk_atom_intern(gtkFormatName, TRUE); + gtk_target_list_add(list, atom, 0, format->formatId); + clipboard->server_html_format_id = format->formatId; + } + REMMINA_PLUGIN_DEBUG("the server has clipboard format %d: %s -> GTK %s", format->formatId, serverFormatName, gtkFormatName); + } + + /* Keep only one DIB format, if present */ + if (has_dib_level) { + GdkAtom atom = gdk_atom_intern("image/bmp", TRUE); + if (has_dib_level == 5) + gtk_target_list_add(list, atom, 0, CF_DIBV5); + else + gtk_target_list_add(list, atom, 0, CF_DIB); + } + + + REMMINA_PLUGIN_DEBUG("gp=%p: sending ClientFormatListResponse to server", gp); +#if FREERDP_VERSION_MAJOR >= 3 + formatListResponse.common.msgType = CB_FORMAT_LIST_RESPONSE; + formatListResponse.common.msgFlags = CB_RESPONSE_OK; // Can be CB_RESPONSE_FAIL in case of error + formatListResponse.common.dataLen = 0; +#else + formatListResponse.msgType = CB_FORMAT_LIST_RESPONSE; + formatListResponse.msgFlags = CB_RESPONSE_OK; // Can be CB_RESPONSE_FAIL in case of error + formatListResponse.dataLen = 0; +#endif + rc = clipboard->context->ClientFormatListResponse(clipboard->context, &formatListResponse); + /* Schedule GTK event to tell GTK to change the local clipboard calling gtk_clipboard_set_with_owner + * via REMMINA_RDP_UI_CLIPBOARD_SET_DATA + * GTK will immediately fire an "owner-change" event, that we should ignore. + * And if we are putting text on the clipboard, mutter or other clipboard + * managers may try immediately request a clipboard transfer. */ + + /* Ensure we have at least one target into list, or Gtk will not change owner + when setting clipboard owner later. + We put it directly in the clipboard cache (clipboard->srv_data), + so remmina will never ask it to the server via ClientFormatDataRequest */ + gint n_targets; + GtkTargetEntry *target_table = gtk_target_table_new_from_list(list, &n_targets); + if (target_table) + gtk_target_table_free(target_table, n_targets); + if (n_targets == 0) { + REMMINA_PLUGIN_DEBUG("gp=%p adding a dummy text target (empty text) for local clipboard, because we have no interesting targets from the server. Putting it in the local clipboard cache."); + GdkAtom atom = gdk_atom_intern("text/plain;charset=utf-8", TRUE); + gtk_target_list_add(list, atom, 0, CF_UNICODETEXT); + pthread_mutex_lock(&clipboard->srv_data_mutex); + clipboard->srv_data = malloc(1); + ((char *)(clipboard->srv_data))[0] = 0; + pthread_mutex_unlock(&clipboard->srv_data_mutex); + clipboard->format = CF_UNICODETEXT; + } + + + ui = g_new0(RemminaPluginRdpUiObject, 1); + ui->type = REMMINA_RDP_UI_CLIPBOARD; + ui->clipboard.clipboard = clipboard; + ui->clipboard.type = REMMINA_RDP_UI_CLIPBOARD_SET_DATA; + ui->clipboard.targetlist = list; + remmina_rdp_event_queue_ui_async(gp, ui); + + REMMINA_PLUGIN_DEBUG("gp=%p: processing of ServerFormatList ended, returning rc=%u to libfreerdp", gp, rc); + return rc; +} + +static UINT remmina_rdp_cliprdr_server_format_list_response(CliprdrClientContext *context, const CLIPRDR_FORMAT_LIST_RESPONSE *formatListResponse) +{ + TRACE_CALL(__func__); + return CHANNEL_RC_OK; +} + +static UINT remmina_rdp_cliprdr_server_format_data_request(CliprdrClientContext *context, const CLIPRDR_FORMAT_DATA_REQUEST *formatDataRequest) +{ + TRACE_CALL(__func__); + + RemminaPluginRdpUiObject *ui; + RemminaProtocolWidget *gp; + rfClipboard *clipboard; + + clipboard = (rfClipboard *)context->custom; + gp = clipboard->rfi->protocol_widget; + + ui = g_new0(RemminaPluginRdpUiObject, 1); + ui->type = REMMINA_RDP_UI_CLIPBOARD; + ui->clipboard.clipboard = clipboard; + ui->clipboard.type = REMMINA_RDP_UI_CLIPBOARD_GET_DATA; + ui->clipboard.format = formatDataRequest->requestedFormatId; + remmina_rdp_event_queue_ui_sync_retint(gp, ui); + + return CHANNEL_RC_OK; +} + +int timeval_diff(struct timeval *start, struct timeval *end) +{ + /* Returns the time elapsed from start to end, in ms */ + int64_t ms_end = (end->tv_sec * 1000) + (end->tv_usec / 1000); + int64_t ms_start = (start->tv_sec * 1000) + (start->tv_usec / 1000); + + return (int)(ms_end - ms_start); +} + + +static UINT remmina_rdp_cliprdr_server_format_data_response(CliprdrClientContext *context, const CLIPRDR_FORMAT_DATA_RESPONSE *formatDataResponse) +{ + TRACE_CALL(__func__); + + const UINT8 *data; + size_t size; + rfContext *rfi; + RemminaProtocolWidget *gp; + rfClipboard *clipboard; + gpointer output = NULL; + struct timeval now; + int mstrans; + + clipboard = (rfClipboard *)context->custom; + gp = clipboard->rfi->protocol_widget; + rfi = GET_PLUGIN_DATA(gp); + + data = formatDataResponse->requestedFormatData; +#if FREERDP_VERSION_MAJOR >= 3 + size = formatDataResponse->common.dataLen; +#else + size = formatDataResponse->dataLen; +#endif + + REMMINA_PLUGIN_DEBUG("gp=%p server FormatDataResponse received: clipboard data arrived form server.", gp); + gettimeofday(&now, NULL); + remmina_rdp_cliprdr_cached_clipboard_free(clipboard); + + /* Calculate stats */ + mstrans = timeval_diff(&(clipboard->clientformatdatarequest_tv), &now); + REMMINA_PLUGIN_DEBUG("gp=%p %zu bytes transferred from server in %d ms. Speed is %d bytes/sec", + gp, (size_t)size, mstrans, mstrans != 0 ? (int)((int64_t)size * 1000 / mstrans) : 0); + + if (size > 0) { + switch (rfi->clipboard.format) { + case CF_UNICODETEXT: + { + output = + g_utf16_to_utf8((const WCHAR *)data, size / sizeof(WCHAR), NULL, NULL, NULL); + if (output) { + size = strlen(output) + 1; + crlf2lf(output, &size); + } + break; + } + + case CF_TEXT: + case CB_FORMAT_HTML: + { + output = (gpointer)calloc(1, size + 1); + if (output) { + memcpy(output, data, size); + crlf2lf(output, &size); + } + break; + } + + case CF_DIBV5: + case CF_DIB: + { + wStream *s; + UINT32 offset; + GError *perr; + BITMAPINFOHEADER *pbi; + BITMAPV5HEADER *pbi5; + + pbi = (BITMAPINFOHEADER *)data; + + // offset calculation inspired by http://downloads.poolelan.com/MSDN/MSDNLibrary6/Disk1/Samples/VC/OS/WindowsXP/GetImage/BitmapUtil.cpp + offset = 14 + pbi->biSize; + if (pbi->biClrUsed != 0) + offset += sizeof(RGBQUAD) * pbi->biClrUsed; + else if (pbi->biBitCount <= 8) + offset += sizeof(RGBQUAD) * (1 << pbi->biBitCount); + if (pbi->biSize == sizeof(BITMAPINFOHEADER)) { + if (pbi->biCompression == 3) // BI_BITFIELDS is 3 + offset += 12; + } else if (pbi->biSize >= sizeof(BITMAPV5HEADER)) { + pbi5 = (BITMAPV5HEADER *)pbi; + if (pbi5->bV5ProfileData <= offset) + offset += pbi5->bV5ProfileSize; + } + s = Stream_New(NULL, 14 + size); + Stream_Write_UINT8(s, 'B'); + Stream_Write_UINT8(s, 'M'); + Stream_Write_UINT32(s, 14 + size); + Stream_Write_UINT32(s, 0); + Stream_Write_UINT32(s, offset); + Stream_Write(s, data, size); + + data = Stream_Buffer(s); + size = Stream_Length(s); + + GdkPixbufLoader *loader = gdk_pixbuf_loader_new(); + perr = NULL; + if (!gdk_pixbuf_loader_write(loader, data, size, &perr)) { + Stream_Free(s, TRUE); + g_warning("[RDP] rdp_cliprdr: gdk_pixbuf_loader_write() returned error %s\n", perr->message); + } else { + if (!gdk_pixbuf_loader_close(loader, &perr)) { + g_warning("[RDP] rdp_cliprdr: gdk_pixbuf_loader_close() returned error %s\n", perr->message); + perr = NULL; + } + Stream_Free(s, TRUE); + output = g_object_ref(gdk_pixbuf_loader_get_pixbuf(loader)); + } + g_object_unref(loader); + break; + } + + case CB_FORMAT_PNG: + case CB_FORMAT_JPEG: + { + GdkPixbufLoader *loader = gdk_pixbuf_loader_new(); + gdk_pixbuf_loader_write(loader, data, size, NULL); + output = g_object_ref(gdk_pixbuf_loader_get_pixbuf(loader)); + gdk_pixbuf_loader_close(loader, NULL); + g_object_unref(loader); + break; + } + default: + { + if (rfi->clipboard.format == clipboard->server_html_format_id) { + /* Converting from Microsoft HTML Clipboard Format to pure text/html + * https://docs.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format */ + int p = 0, lstart = 0; + size_t osize; + char c; + /* Find the 1st starting with '<' */ + while(p < size && (c = data[p]) != 0) { + if (c == '<' && p == lstart) break; + if (c == '\n') lstart = p + 1; + p++; + } + if (p < size) { + osize = size - lstart; + output = (gpointer)calloc(1, osize + 1); + if (output) { + memcpy(output, data + lstart, osize); + ((char *)output)[osize] = 0; + } + } + } + } + } + } + + pthread_mutex_lock(&clipboard->srv_data_mutex); + clipboard->srv_data = output; + pthread_mutex_unlock(&clipboard->srv_data_mutex); + + if (output != NULL) + REMMINA_PLUGIN_DEBUG("gp=%p: clipboard local cache data has been loaded", gp); + else + REMMINA_PLUGIN_DEBUG("gp=%p: data from server is not valid (size=%zu format=%d), cannot load into cache", gp, size, rfi->clipboard.format); + + + REMMINA_PLUGIN_DEBUG("gp=%p: signalling main GTK thread that we have some clipboard data.", gp); + + pthread_mutex_lock(&clipboard->transfer_clip_mutex); + pthread_cond_signal(&clipboard->transfer_clip_cond); + if (clipboard->srv_clip_data_wait == SCDW_BUSY_WAIT) { + REMMINA_PLUGIN_DEBUG("gp=%p: clipboard transfer from server completed.", gp); + } else { + // Clipboard data arrived from server when we are not busy waiting on main loop + // Unfortunately, we must discard it + REMMINA_PLUGIN_DEBUG("gp=%p: clipboard transfer from server completed, but no local application is requesting it. Data is on local cache now, try to paste later.", gp); + } + clipboard->srv_clip_data_wait = SCDW_NONE; + pthread_mutex_unlock(&clipboard->transfer_clip_mutex); + + return CHANNEL_RC_OK; +} + + +void remmina_rdp_cliprdr_request_data(GtkClipboard *gtkClipboard, GtkSelectionData *selection_data, guint info, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + + /* Called by GTK when someone presses "Paste" on the client side. + * We ask to the server the data we need */ + + CLIPRDR_FORMAT_DATA_REQUEST *pFormatDataRequest; + rfClipboard *clipboard; + rfContext *rfi = GET_PLUGIN_DATA(gp); + RemminaPluginRdpEvent rdp_event = { 0 }; + struct timespec to; + struct timeval tv; + int rc; + time_t tlimit, tlimit1s, tnow, tstart; + + REMMINA_PLUGIN_DEBUG("gp=%p: A local application has requested remote clipboard data for remote format id %d", gp, info); + + clipboard = &(rfi->clipboard); + if (clipboard->srv_clip_data_wait != SCDW_NONE) { + g_message("[RDP] Cannot paste now, I’m already transferring clipboard data from server. Try again later\n"); + return; + } + + if (clipboard->format != info || clipboard->srv_data == NULL) { + /* We do not have a local cached clipoard, so we have to start a remote request */ + remmina_rdp_cliprdr_cached_clipboard_free(clipboard); + + clipboard->format = info; + + pthread_mutex_lock(&clipboard->transfer_clip_mutex); + + pFormatDataRequest = (CLIPRDR_FORMAT_DATA_REQUEST *)malloc(sizeof(CLIPRDR_FORMAT_DATA_REQUEST)); + ZeroMemory(pFormatDataRequest, sizeof(CLIPRDR_FORMAT_DATA_REQUEST)); + pFormatDataRequest->requestedFormatId = clipboard->format; + + clipboard->srv_clip_data_wait = SCDW_BUSY_WAIT; // Annotate that we are waiting for ServerFormatDataResponse + + REMMINA_PLUGIN_DEBUG("gp=%p Requesting clipboard data with format %d from the server via ServerFormatDataRequest", gp, clipboard->format); + rdp_event.type = REMMINA_RDP_EVENT_TYPE_CLIPBOARD_SEND_CLIENT_FORMAT_DATA_REQUEST; + rdp_event.clipboard_formatdatarequest.pFormatDataRequest = pFormatDataRequest; + remmina_rdp_event_event_push(gp, &rdp_event); + + /* Busy wait clipboard data for CLIPBOARD_TRANSFER_WAIT_TIME seconds. + * In the meanwhile allow GTK event loop to proceed */ + + tstart = time(NULL); + tlimit = tstart + CLIPBOARD_TRANSFER_WAIT_TIME; + rc = 100000; + tlimit1s = tstart + 1; + while ((tnow = time(NULL)) < tlimit && rc != 0 && clipboard->srv_clip_data_wait == SCDW_BUSY_WAIT) { + + if (tnow >= tlimit1s) { + REMMINA_PLUGIN_DEBUG("gp=%p, clipboard data is still not here after %u seconds", gp, (unsigned)(tnow - tstart)); + tlimit1s = time(NULL) + 1; + } + + gettimeofday(&tv, NULL); + to.tv_sec = tv.tv_sec; + to.tv_nsec = tv.tv_usec * 1000 + 5000000; // wait for data for 5ms + if (to.tv_nsec >= 1000000000) { + to.tv_nsec -= 1000000000; + to.tv_sec++; + } + rc = pthread_cond_timedwait(&clipboard->transfer_clip_cond, &clipboard->transfer_clip_mutex, &to); + if (rc == 0) + break; + + gtk_main_iteration_do(FALSE); + } + + if (rc != 0) { + /* Timeout, just log it and hope that data will arrive later */ + if (clipboard->srv_clip_data_wait == SCDW_ABORTING) { + g_warning("[RDP] gp=%p Clipboard data wait aborted.",gp); + } else { + if (rc == ETIMEDOUT) + g_warning("[RDP] gp=%p Clipboard data from the server is not available in %d seconds. No data will be available to user.", + gp, CLIPBOARD_TRANSFER_WAIT_TIME); + else + g_warning("[RDP] gp=%p internal error: pthread_cond_timedwait() returned %d\n", gp, rc); + } + } + pthread_mutex_unlock(&clipboard->transfer_clip_mutex); + + } + + pthread_mutex_lock(&clipboard->srv_data_mutex); + if (clipboard->srv_data != NULL) { + REMMINA_PLUGIN_DEBUG("gp=%p pasting data to local application", gp); + /* We have data in cache, just paste it */ + if (info == CB_FORMAT_PNG || info == CF_DIB || info == CF_DIBV5 || info == CB_FORMAT_JPEG) { + gtk_selection_data_set_pixbuf(selection_data, clipboard->srv_data); + } else if (info == CB_FORMAT_HTML || info == clipboard->server_html_format_id) { + REMMINA_PLUGIN_DEBUG("gp=%p returning %zu bytes of HTML in clipboard to requesting application", gp, strlen(clipboard->srv_data)); + GdkAtom atom = gdk_atom_intern("text/html", TRUE); + gtk_selection_data_set(selection_data, atom, 8, clipboard->srv_data, strlen(clipboard->srv_data)); + } else { + REMMINA_PLUGIN_DEBUG("gp=%p returning %zu bytes of text in clipboard to requesting application", gp, strlen(clipboard->srv_data)); + gtk_selection_data_set_text(selection_data, clipboard->srv_data, -1); + } + clipboard->srv_clip_data_wait = SCDW_NONE; + } else { + REMMINA_PLUGIN_DEBUG("gp=%p cannot paste data to local application because ->srv_data is NULL", gp); + } + pthread_mutex_unlock(&clipboard->srv_data_mutex); + +} + +void remmina_rdp_cliprdr_empty_clipboard(GtkClipboard *gtkClipboard, rfClipboard *clipboard) +{ + TRACE_CALL(__func__); + /* No need to do anything here */ +} + +CLIPRDR_FORMAT_LIST *remmina_rdp_cliprdr_get_client_format_list(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + + GtkClipboard *gtkClipboard; + rfContext *rfi = GET_PLUGIN_DATA(gp); + GdkAtom *targets; + gboolean result = 0; + gint loccount, srvcount; + gint formatId, i; + CLIPRDR_FORMAT *formats; + struct retp_t { + CLIPRDR_FORMAT_LIST pFormatList; + CLIPRDR_FORMAT formats[]; + } *retp; + + formats = NULL; + retp = NULL; + loccount = 0; + + gtkClipboard = gtk_widget_get_clipboard(rfi->drawing_area, GDK_SELECTION_CLIPBOARD); + if (gtkClipboard) + result = gtk_clipboard_wait_for_targets(gtkClipboard, &targets, &loccount); + REMMINA_PLUGIN_DEBUG("gp=%p sending to server the following local clipboard content formats", gp); + if (result && loccount > 0) { + formats = (CLIPRDR_FORMAT *)malloc(loccount * sizeof(CLIPRDR_FORMAT)); + srvcount = 0; + for (i = 0; i < loccount; i++) { + formatId = remmina_rdp_cliprdr_get_format_from_gdkatom(targets[i]); + if (formatId != 0) { + gchar *name = gdk_atom_name(targets[i]); + REMMINA_PLUGIN_DEBUG(" local clipboard format %s will be sent to remote as %d", name, formatId); + g_free(name); + formats[srvcount].formatId = formatId; + formats[srvcount].formatName = NULL; + srvcount++; + } + } + if (srvcount > 0) { + retp = (struct retp_t *)malloc(sizeof(struct retp_t) + sizeof(CLIPRDR_FORMAT) * srvcount); + retp->pFormatList.formats = retp->formats; + retp->pFormatList.numFormats = srvcount; + memcpy(retp->formats, formats, sizeof(CLIPRDR_FORMAT) * srvcount); + } else { + retp = (struct retp_t *)malloc(sizeof(struct retp_t)); + retp->pFormatList.formats = NULL; + retp->pFormatList.numFormats = 0; + } + free(formats); + } else { + retp = (struct retp_t *)malloc(sizeof(struct retp_t) + sizeof(CLIPRDR_FORMAT)); + retp->pFormatList.formats = NULL; + retp->pFormatList.numFormats = 0; + } + + if (result) + g_free(targets); + +#if FREERDP_VERSION_MAJOR >= 3 + retp->pFormatList.common.msgType = CB_FORMAT_LIST; + retp->pFormatList.common.msgFlags = 0; +#else + retp->pFormatList.msgType = CB_FORMAT_LIST; + retp->pFormatList.msgFlags = 0; +#endif + + return (CLIPRDR_FORMAT_LIST *)retp; +} + +static void remmina_rdp_cliprdr_mt_get_format_list(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui) +{ + TRACE_CALL(__func__); + ui->retptr = (void *)remmina_rdp_cliprdr_get_client_format_list(gp); +} + +void remmina_rdp_cliprdr_get_clipboard_data(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui) +{ + TRACE_CALL(__func__); + GtkClipboard *gtkClipboard; + UINT8 *inbuf = NULL; + UINT8 *outbuf = NULL; +#if FREERDP_VERSION_MAJOR >= 3 + WCHAR *outbuf_wchar = NULL; +#endif + GdkPixbuf *image = NULL; + size_t size = 0; + rfContext *rfi = GET_PLUGIN_DATA(gp); + RemminaPluginRdpEvent rdp_event = { 0 }; + + gtkClipboard = gtk_widget_get_clipboard(rfi->drawing_area, GDK_SELECTION_CLIPBOARD); + if (gtkClipboard) { + switch (ui->clipboard.format) { + case CF_TEXT: + case CF_UNICODETEXT: + case CB_FORMAT_HTML: + { + inbuf = (UINT8 *)gtk_clipboard_wait_for_text(gtkClipboard); + break; + } + + case CB_FORMAT_PNG: + case CB_FORMAT_JPEG: + case CF_DIB: + case CF_DIBV5: + { + image = gtk_clipboard_wait_for_image(gtkClipboard); + break; + } + } + } + + /* No data received, send nothing */ + if (inbuf != NULL || image != NULL) { + switch (ui->clipboard.format) { + case CF_TEXT: + case CB_FORMAT_HTML: + { + size = strlen((char *)inbuf); + outbuf = lf2crlf(inbuf, (int *) &size); + break; + } + case CF_UNICODETEXT: + { + size = strlen((const char *)inbuf); + inbuf = lf2crlf(inbuf, (int *) &size); +#if FREERDP_VERSION_MAJOR >= 3 + size_t len = 0; + outbuf_wchar = ConvertUtf8NToWCharAlloc((const char *)inbuf, (size_t)size, &len); + size = (len + 1) * sizeof(WCHAR); +#else + const int rc = (ConvertToUnicode(CP_UTF8, 0, (CHAR *)inbuf, -1, (WCHAR **)&outbuf, 0)) * sizeof(WCHAR); + size = 0; + if (rc >= 0) { + size = (size_t)rc; + } +#endif + g_free(inbuf); + break; + } + case CB_FORMAT_PNG: + { + gchar *data; + gsize buffersize; + gdk_pixbuf_save_to_buffer(image, &data, &buffersize, "png", NULL, NULL); + outbuf = (UINT8 *)malloc(buffersize); + memcpy(outbuf, data, buffersize); + size = buffersize; + g_object_unref(image); + break; + } + case CB_FORMAT_JPEG: + { + gchar *data; + gsize buffersize; + gdk_pixbuf_save_to_buffer(image, &data, &buffersize, "jpeg", NULL, NULL); + outbuf = (UINT8 *)malloc(buffersize); + memcpy(outbuf, data, buffersize); + size = buffersize; + g_object_unref(image); + break; + } + case CF_DIB: + case CF_DIBV5: + { + gchar *data; + gsize buffersize; + gdk_pixbuf_save_to_buffer(image, &data, &buffersize, "bmp", NULL, NULL); + size = buffersize - 14; + outbuf = (UINT8 *)malloc(size); + memcpy(outbuf, data + 14, size); + g_object_unref(image); + break; + } + } + } + + rdp_event.type = REMMINA_RDP_EVENT_TYPE_CLIPBOARD_SEND_CLIENT_FORMAT_DATA_RESPONSE; + rdp_event.clipboard_formatdataresponse.size = (int)MIN(size, INT32_MAX); + +#if FREERDP_VERSION_MAJOR >= 3 + // For unicode, use the wchar buffer + if (outbuf == NULL && outbuf_wchar != NULL) { + rdp_event.clipboard_formatdataresponse.data = (unsigned char *)outbuf_wchar; + } + else { + rdp_event.clipboard_formatdataresponse.data = (unsigned char *)outbuf; + } +#else + rdp_event.clipboard_formatdataresponse.data = (unsigned char *)outbuf; +#endif + + remmina_rdp_event_event_push(gp, &rdp_event); +} + +void remmina_rdp_cliprdr_set_clipboard_data(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui) +{ + TRACE_CALL(__func__); + GtkClipboard *gtkClipboard; + gint n_targets; + rfContext *rfi = GET_PLUGIN_DATA(gp); + + gtkClipboard = gtk_widget_get_clipboard(rfi->drawing_area, GDK_SELECTION_CLIPBOARD); + if (gtkClipboard) { + GtkTargetEntry *targets = gtk_target_table_new_from_list(ui->clipboard.targetlist, &n_targets); + if (!targets) { + /* If no targets exists, this is an internal error, because + * remmina_rdp_cliprdr_server_format_list() must have produced + * at least one target before calling REMMINA_RDP_UI_CLIPBOARD_SET_DATA */ + g_warning("[RDP] internal error: no targets to insert into the local clipboard"); + } + + REMMINA_PLUGIN_DEBUG("setting clipboard with owner to me: %p", gp); + gtk_clipboard_set_with_owner(gtkClipboard, targets, n_targets, + (GtkClipboardGetFunc)remmina_rdp_cliprdr_request_data, + (GtkClipboardClearFunc)remmina_rdp_cliprdr_empty_clipboard, G_OBJECT(gp)); + gtk_target_table_free(targets, n_targets); + } +} + +void remmina_rdp_cliprdr_detach_owner(RemminaProtocolWidget *gp) +{ + /* When closing a rdp connection, we should check if gp is a clipboard owner. + * If it’s an owner, detach it from the clipboard */ + TRACE_CALL(__func__); + rfContext *rfi = GET_PLUGIN_DATA(gp); + GtkClipboard *gtkClipboard; + + if (!rfi || !rfi->drawing_area) return; + + gtkClipboard = gtk_widget_get_clipboard(rfi->drawing_area, GDK_SELECTION_CLIPBOARD); + if (gtkClipboard && gtk_clipboard_get_owner(gtkClipboard) == (GObject *)gp) + gtk_clipboard_clear(gtkClipboard); + +} + +void remmina_rdp_event_process_clipboard(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui) +{ + TRACE_CALL(__func__); + + switch (ui->clipboard.type) { + case REMMINA_RDP_UI_CLIPBOARD_FORMATLIST: + remmina_rdp_cliprdr_mt_get_format_list(gp, ui); + break; + + case REMMINA_RDP_UI_CLIPBOARD_GET_DATA: + remmina_rdp_cliprdr_get_clipboard_data(gp, ui); + break; + + case REMMINA_RDP_UI_CLIPBOARD_SET_DATA: + remmina_rdp_cliprdr_set_clipboard_data(gp, ui); + break; + + } +} + +void remmina_rdp_clipboard_init(rfContext *rfi) +{ + TRACE_CALL(__func__); + // Future: initialize rfi->clipboard +} +void remmina_rdp_clipboard_free(rfContext *rfi) +{ + TRACE_CALL(__func__); + + remmina_rdp_cliprdr_cached_clipboard_free(&(rfi->clipboard)); + +} + +void remmina_rdp_clipboard_abort_client_format_data_request(rfContext *rfi) +{ + TRACE_CALL(__func__); + if (rfi && rfi->clipboard.srv_clip_data_wait == SCDW_BUSY_WAIT) { + REMMINA_PLUGIN_DEBUG("requesting clipboard data transfer from server to be ignored and busywait loop to exit"); + /* Allow clipboard transfer from server to terminate */ + rfi->clipboard.srv_clip_data_wait = SCDW_ABORTING; + usleep(100000); + } +} + +void remmina_rdp_cliprdr_init(rfContext *rfi, CliprdrClientContext *cliprdr) +{ + TRACE_CALL(__func__); + + rfClipboard *clipboard; + clipboard = &(rfi->clipboard); + + rfi->clipboard.rfi = rfi; + cliprdr->custom = (void *)clipboard; + + clipboard->context = cliprdr; + pthread_mutex_init(&clipboard->transfer_clip_mutex, NULL); + pthread_cond_init(&clipboard->transfer_clip_cond, NULL); + clipboard->srv_clip_data_wait = SCDW_NONE; + + pthread_mutex_init(&clipboard->srv_data_mutex, NULL); + + cliprdr->MonitorReady = remmina_rdp_cliprdr_monitor_ready; + cliprdr->ServerCapabilities = remmina_rdp_cliprdr_server_capabilities; + cliprdr->ServerFormatList = remmina_rdp_cliprdr_server_format_list; + cliprdr->ServerFormatListResponse = remmina_rdp_cliprdr_server_format_list_response; + cliprdr->ServerFormatDataRequest = remmina_rdp_cliprdr_server_format_data_request; + cliprdr->ServerFormatDataResponse = remmina_rdp_cliprdr_server_format_data_response; + +// cliprdr->ServerFileContentsRequest = remmina_rdp_cliprdr_server_file_contents_request; +// cliprdr->ServerFileContentsResponse = remmina_rdp_cliprdr_server_file_contents_response; +} diff --git a/plugins/rdp/rdp_cliprdr.h b/plugins/rdp/rdp_cliprdr.h new file mode 100644 index 0000000..56b58a8 --- /dev/null +++ b/plugins/rdp/rdp_cliprdr.h @@ -0,0 +1,51 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2010-2011 Vic Lee + * Copyright (C) 2012-2012 Jean-Louis Dupond + * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * 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 <freerdp/freerdp.h> +#include "rdp_plugin.h" + +void remmina_rdp_clipboard_init(rfContext *rfi); +void remmina_rdp_clipboard_free(rfContext *rfi); +void remmina_rdp_cliprdr_init(rfContext *rfi, CliprdrClientContext *cliprdr); +void remmina_rdp_channel_cliprdr_process(RemminaProtocolWidget *gp, wMessage *event); +void remmina_rdp_event_process_clipboard(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui); +CLIPRDR_FORMAT_LIST *remmina_rdp_cliprdr_get_client_format_list(RemminaProtocolWidget *gp); +void remmina_rdp_cliprdr_detach_owner(RemminaProtocolWidget *gp); +void remmina_rdp_clipboard_abort_client_format_data_request(rfContext *rfi); diff --git a/plugins/rdp/rdp_event.c b/plugins/rdp/rdp_event.c new file mode 100644 index 0000000..19b349c --- /dev/null +++ b/plugins/rdp/rdp_event.c @@ -0,0 +1,1494 @@ +/* + * Remmina - The GTK Remote Desktop Client + * Copyright (C) 2010 Jay Sorg + * Copyright (C) 2010-2011 Vic Lee + * Copyright (C) 2011 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo + * Copyright (C) 2016-2022 Antenore Gatta, Giovanni Panozzo + * Copyright (C) 2022-2023 Antenore Gatta, Giovanni Panozzo, Hiroyuki Tanaka + * + * 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 "rdp_cliprdr.h" +#include "rdp_event.h" +#include "rdp_monitor.h" +#include "rdp_settings.h" +#include <gdk/gdkkeysyms.h> +#ifdef GDK_WINDOWING_X11 +#include <cairo/cairo-xlib.h> +#else +#include <cairo/cairo.h> +#endif +#include <freerdp/locale/keyboard.h> + +gboolean remmina_rdp_event_on_map(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + rfContext *rfi = GET_PLUGIN_DATA(gp); + rdpGdi *gdi; + + if (rfi == NULL) + return false; + + RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + int do_suppress = !remmina_plugin_service->file_get_int(remminafile, "no-suppress", FALSE); + + if (do_suppress) { + gdi = ((rdpContext *)rfi)->gdi; + + REMMINA_PLUGIN_DEBUG("Map event received, disabling TS_SUPPRESS_OUTPUT_PDU "); + gdi_send_suppress_output(gdi, FALSE); + } + + return FALSE; +} + +gboolean remmina_rdp_event_on_unmap(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + rfContext *rfi = GET_PLUGIN_DATA(gp); + rdpGdi *gdi; + + if (rfi == NULL) + return false; + + GtkWidget *toplevel = gtk_widget_get_toplevel(GTK_WIDGET(gp)); + GdkWindow *window = gtk_widget_get_window(toplevel); + + if (gdk_window_get_fullscreen_mode(window) == GDK_FULLSCREEN_ON_ALL_MONITORS) { + REMMINA_PLUGIN_DEBUG("Unmap event received, but cannot enable TS_SUPPRESS_OUTPUT_PDU when in fullscreen"); + return FALSE; + } + + RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + int do_suppress = !remmina_plugin_service->file_get_int(remminafile, "no-suppress", FALSE); + + if (do_suppress) { + gdi = ((rdpContext *)rfi)->gdi; + + REMMINA_PLUGIN_DEBUG("Unmap event received, enabling TS_SUPPRESS_OUTPUT_PDU "); + gdi_send_suppress_output(gdi, TRUE); + } + + return FALSE; +} + +static gboolean remmina_rdp_event_on_focus_in(GtkWidget *widget, GdkEventKey *event, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + + rfContext *rfi = GET_PLUGIN_DATA(gp); + rdpInput *input; + GdkModifierType state; + +#if GTK_CHECK_VERSION(3, 20, 0) + GdkSeat *seat; +#else + GdkDeviceManager *manager; +#endif + GdkDevice *keyboard = NULL; + + const gchar *wname = gtk_widget_get_name(gtk_widget_get_toplevel(widget)); + REMMINA_PLUGIN_DEBUG("Top level name is: %s", wname); + + if (!rfi || !rfi->connected || rfi->is_reconnecting) + return FALSE; + + input = rfi->clientContext.context.input; + UINT32 toggle_keys_state = 0; + +#if GTK_CHECK_VERSION(3, 20, 0) + seat = gdk_display_get_default_seat(gdk_display_get_default()); + keyboard = gdk_seat_get_pointer(seat); +#else + manager = gdk_display_get_device_manager(gdk_display_get_default()); + keyboard = gdk_device_manager_get_client_pointer(manager); +#endif + gdk_window_get_device_position(gdk_get_default_root_window(), keyboard, NULL, NULL, &state); + + if (state & GDK_LOCK_MASK) + toggle_keys_state |= KBD_SYNC_CAPS_LOCK; + if (state & GDK_MOD2_MASK) + toggle_keys_state |= KBD_SYNC_NUM_LOCK; + if (state & GDK_MOD5_MASK) + toggle_keys_state |= KBD_SYNC_SCROLL_LOCK; + + input->SynchronizeEvent(input, toggle_keys_state); + input->KeyboardEvent(input, KBD_FLAGS_RELEASE, 0x0F); + + return FALSE; +} + +void remmina_rdp_event_event_push(RemminaProtocolWidget *gp, const RemminaPluginRdpEvent *e) +{ + TRACE_CALL(__func__); + rfContext *rfi = GET_PLUGIN_DATA(gp); + RemminaPluginRdpEvent *event; + + /* Called by the main GTK thread to send an event to the libfreerdp thread */ + + if (!rfi || !rfi->connected || rfi->is_reconnecting) + return; + + if (rfi->event_queue) { +#if GLIB_CHECK_VERSION(2,67,3) + event = g_memdup2(e, sizeof(RemminaPluginRdpEvent)); +#else + event = g_memdup(e, sizeof(RemminaPluginRdpEvent)); +#endif + g_async_queue_push(rfi->event_queue, event); + + if (write(rfi->event_pipe[1], "\0", 1)) { + } + } +} + +static void remmina_rdp_event_release_all_keys(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + rfContext *rfi = GET_PLUGIN_DATA(gp); + RemminaPluginRdpEvent rdp_event = { 0 }; + int i; + + /* Send all release key events for previously pressed keys */ + for (i = 0; i < rfi->pressed_keys->len; i++) { + rdp_event = g_array_index(rfi->pressed_keys, RemminaPluginRdpEvent, i); + if ((rdp_event.type == REMMINA_RDP_EVENT_TYPE_SCANCODE || + rdp_event.type == REMMINA_RDP_EVENT_TYPE_SCANCODE_UNICODE) && + rdp_event.key_event.up == false) { + rdp_event.key_event.up = true; + remmina_rdp_event_event_push(gp, &rdp_event); + } + } + + g_array_set_size(rfi->pressed_keys, 0); +} + +static void remmina_rdp_event_release_key(RemminaProtocolWidget *gp, RemminaPluginRdpEvent rdp_event) +{ + TRACE_CALL(__func__); + gint i; + rfContext *rfi = GET_PLUGIN_DATA(gp); + RemminaPluginRdpEvent rdp_event_2 = { 0 }; + + rdp_event_2.type = REMMINA_RDP_EVENT_TYPE_SCANCODE; + + if ((rdp_event.type == REMMINA_RDP_EVENT_TYPE_SCANCODE || + rdp_event.type == REMMINA_RDP_EVENT_TYPE_SCANCODE_UNICODE) && + rdp_event.key_event.up) { + /* Unregister the keycode only */ + for (i = 0; i < rfi->pressed_keys->len; i++) { + rdp_event_2 = g_array_index(rfi->pressed_keys, RemminaPluginRdpEvent, i); + + if (rdp_event_2.key_event.key_code == rdp_event.key_event.key_code && + rdp_event_2.key_event.unicode_code == rdp_event.key_event.unicode_code && + rdp_event_2.key_event.extended == rdp_event.key_event.extended && + rdp_event_2.key_event.extended1 == rdp_event.key_event.extended1) { + g_array_remove_index_fast(rfi->pressed_keys, i); + break; + } + } + } +} + +static void keypress_list_add(RemminaProtocolWidget *gp, RemminaPluginRdpEvent rdp_event) +{ + TRACE_CALL(__func__); + rfContext *rfi = GET_PLUGIN_DATA(gp); + + if (!rdp_event.key_event.key_code) + return; + + if (rdp_event.key_event.up) + remmina_rdp_event_release_key(gp, rdp_event); + else + g_array_append_val(rfi->pressed_keys, rdp_event); +} + + +static void remmina_rdp_event_scale_area(RemminaProtocolWidget *gp, gint *x, gint *y, gint *w, gint *h) +{ + TRACE_CALL(__func__); + gint width, height; + gint sx, sy, sw, sh; + rfContext *rfi = GET_PLUGIN_DATA(gp); + + if (!rfi || !rfi->connected || rfi->is_reconnecting || !rfi->surface) + return; + + width = remmina_plugin_service->protocol_plugin_get_width(gp); + height = remmina_plugin_service->protocol_plugin_get_height(gp); + + if ((width == 0) || (height == 0)) + return; + + if ((rfi->scale_width == width) && (rfi->scale_height == height)) { + /* Same size, just copy the pixels */ + *x = MIN(MAX(0, *x), width - 1); + *y = MIN(MAX(0, *y), height - 1); + *w = MIN(width - *x, *w); + *h = MIN(height - *y, *h); + return; + } + + /* We have to extend the scaled region one scaled pixel, to avoid gaps */ + + sx = MIN(MAX(0, (*x) * rfi->scale_width / width + - rfi->scale_width / width - 2), rfi->scale_width - 1); + + sy = MIN(MAX(0, (*y) * rfi->scale_height / height + - rfi->scale_height / height - 2), rfi->scale_height - 1); + + sw = MIN(rfi->scale_width - sx, (*w) * rfi->scale_width / width + + rfi->scale_width / width + 4); + + sh = MIN(rfi->scale_height - sy, (*h) * rfi->scale_height / height + + rfi->scale_height / height + 4); + + *x = sx; + *y = sy; + *w = sw; + *h = sh; +} + +void remmina_rdp_event_update_regions(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui) +{ + TRACE_CALL(__func__); + rfContext *rfi = GET_PLUGIN_DATA(gp); + gint x, y, w, h, i; + + for (i = 0; i < ui->reg.ninvalid; i++) { + x = ui->reg.ureg[i].x; + y = ui->reg.ureg[i].y; + w = ui->reg.ureg[i].w; + h = ui->reg.ureg[i].h; + + if (rfi->scale == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED) + remmina_rdp_event_scale_area(gp, &x, &y, &w, &h); + + gtk_widget_queue_draw_area(rfi->drawing_area, x, y, w, h); + } + g_free(ui->reg.ureg); +} + +void remmina_rdp_event_update_rect(RemminaProtocolWidget *gp, gint x, gint y, gint w, gint h) +{ + TRACE_CALL(__func__); + rfContext *rfi = GET_PLUGIN_DATA(gp); + + if (rfi->scale == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED) + remmina_rdp_event_scale_area(gp, &x, &y, &w, &h); + + gtk_widget_queue_draw_area(rfi->drawing_area, x, y, w, h); +} + +static void remmina_rdp_event_update_scale_factor(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + GtkAllocation a; + gint rdwidth, rdheight; + gint gpwidth, gpheight; + rfContext *rfi = GET_PLUGIN_DATA(gp); + + gtk_widget_get_allocation(GTK_WIDGET(gp), &a); + gpwidth = a.width; + gpheight = a.height; + + if (rfi->scale == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED) { + if ((gpwidth > 1) && (gpheight > 1)) { + rdwidth = remmina_plugin_service->protocol_plugin_get_width(gp); + rdheight = remmina_plugin_service->protocol_plugin_get_height(gp); + + rfi->scale_width = gpwidth; + rfi->scale_height = gpheight; + + rfi->scale_x = (gdouble)rfi->scale_width / (gdouble)rdwidth; + rfi->scale_y = (gdouble)rfi->scale_height / (gdouble)rdheight; + } + } else { + rfi->scale_width = 0; + rfi->scale_height = 0; + rfi->scale_x = 0; + rfi->scale_y = 0; + } +} + +static gboolean remmina_rdp_event_on_draw(GtkWidget *widget, cairo_t *context, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + rfContext *rfi = GET_PLUGIN_DATA(gp); + guint width, height; + gchar *msg; + cairo_text_extents_t extents; + + if (!rfi || !rfi->connected) + return FALSE; + + + if (rfi->is_reconnecting) { + /* FreeRDP is reconnecting, just show a message to the user */ + + width = gtk_widget_get_allocated_width(widget); + height = gtk_widget_get_allocated_height(widget); + + /* Draw text */ + msg = g_strdup_printf(_("Reconnection attempt %d of %d…"), + rfi->reconnect_nattempt, rfi->reconnect_maxattempts); + + cairo_select_font_face(context, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD); + cairo_set_font_size(context, 24); + cairo_set_source_rgb(context, 0.9, 0.9, 0.9); + cairo_text_extents(context, msg, &extents); + cairo_move_to(context, (width - (extents.width + extents.x_bearing)) / 2, (height - (extents.height + extents.y_bearing)) / 2); + cairo_show_text(context, msg); + g_free(msg); + } else { + /* Standard drawing: We copy the surface from RDP */ + + if (!rfi->surface) + return FALSE; + + if (rfi->scale == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED) + cairo_scale(context, rfi->scale_x, rfi->scale_y); + + cairo_surface_flush(rfi->surface); + cairo_set_source_surface(context, rfi->surface, 0, 0); + cairo_surface_mark_dirty(rfi->surface); + + cairo_set_operator(context, CAIRO_OPERATOR_SOURCE); // Ignore alpha channel from FreeRDP + cairo_paint(context); + } + + return TRUE; +} + +static gboolean remmina_rdp_event_delayed_monitor_layout(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + rfContext *rfi = GET_PLUGIN_DATA(gp); + + RemminaPluginRdpEvent rdp_event = { 0 }; + GtkAllocation a; + gint desktopOrientation, desktopScaleFactor, deviceScaleFactor; + + RemminaFile *remminafile; + + if (!rfi || !rfi->connected || rfi->is_reconnecting) + return FALSE; + + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + + if (rfi->scale != REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES) + return FALSE; + + rfi->delayed_monitor_layout_handler = 0; + gint gpwidth, gpheight, prevwidth, prevheight; + + gchar *monitorids = NULL; + guint32 maxwidth = 0; + guint32 maxheight = 0; + + remmina_rdp_monitor_get(rfi, &monitorids, &maxwidth, &maxheight); + + REMMINA_PLUGIN_DEBUG("Sending preconfigured monitor layout"); + if (rfi->dispcontext && rfi->dispcontext->SendMonitorLayout) { + remmina_rdp_settings_get_orientation_scale_prefs(&desktopOrientation, &desktopScaleFactor, &deviceScaleFactor); + gtk_widget_get_allocation(GTK_WIDGET(gp), &a); + gpwidth = a.width; + gpheight = a.height; + prevwidth = remmina_plugin_service->protocol_plugin_get_width(gp); + prevheight = remmina_plugin_service->protocol_plugin_get_height(gp); + + if ((gpwidth != prevwidth || gpheight != prevheight) && gpwidth >= 200 && gpheight >= 200) { + if (rfi->rdpgfxchan) { + /* Workaround for FreeRDP issue #5417 */ + if (gpwidth < AVC_MIN_DESKTOP_WIDTH) + gpwidth = AVC_MIN_DESKTOP_WIDTH; + if (gpheight < AVC_MIN_DESKTOP_HEIGHT) + gpheight = AVC_MIN_DESKTOP_HEIGHT; + } + rdp_event.type = REMMINA_RDP_EVENT_TYPE_SEND_MONITOR_LAYOUT; + if (remmina_plugin_service->file_get_int(remminafile, "multimon", FALSE)) { + const rdpMonitor *base = freerdp_settings_get_pointer(rfi->clientContext.context.settings, FreeRDP_MonitorDefArray); + for (gint i = 0; i < freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_MonitorCount); ++i) { + const rdpMonitor *current = &base[i]; + REMMINA_PLUGIN_DEBUG("Sending display layout n° %d", i); + rdp_event.monitor_layout.Flags = current->is_primary; + REMMINA_PLUGIN_DEBUG("EVNT MON LAYOUT - Flags: %i", rdp_event.monitor_layout.Flags); + rdp_event.monitor_layout.Left = current->x; + REMMINA_PLUGIN_DEBUG("EVNT MON LAYOUT - Left: %i", rdp_event.monitor_layout.Left); + rdp_event.monitor_layout.Top = current->y; + REMMINA_PLUGIN_DEBUG("EVNT MON LAYOUT - Top: %i", rdp_event.monitor_layout.Top); + rdp_event.monitor_layout.width = current->width; + REMMINA_PLUGIN_DEBUG("EVNT MON LAYOUT - width: %i", rdp_event.monitor_layout.width); + rdp_event.monitor_layout.height = current->height; + REMMINA_PLUGIN_DEBUG("EVNT MON LAYOUT - height: %i", rdp_event.monitor_layout.height); + rdp_event.monitor_layout.physicalWidth = current->attributes.physicalWidth; + REMMINA_PLUGIN_DEBUG("EVNT MON LAYOUT - physicalWidth: %i", rdp_event.monitor_layout.physicalWidth); + rdp_event.monitor_layout.physicalHeight = current->attributes.physicalHeight; + REMMINA_PLUGIN_DEBUG("EVNT MON LAYOUT - PhysicalHeight: %i", rdp_event.monitor_layout.physicalHeight); + if (current->attributes.orientation) + rdp_event.monitor_layout.desktopOrientation = current->attributes.orientation; + else + rdp_event.monitor_layout.desktopOrientation = rdp_event.monitor_layout.desktopOrientation; + REMMINA_PLUGIN_DEBUG("EVNT MON LAYOUT - desktopOrientation: %i", rdp_event.monitor_layout.desktopOrientation); + rdp_event.monitor_layout.desktopScaleFactor = rdp_event.monitor_layout.desktopScaleFactor; + REMMINA_PLUGIN_DEBUG("EVNT MON LAYOUT - ScaleFactorflag: %i", rdp_event.monitor_layout.desktopScaleFactor); + rdp_event.monitor_layout.deviceScaleFactor = rdp_event.monitor_layout.deviceScaleFactor; + } + remmina_rdp_event_event_push(gp, &rdp_event); + } else { + rdp_event.monitor_layout.width = gpwidth; + rdp_event.monitor_layout.height = gpheight; + rdp_event.monitor_layout.desktopOrientation = desktopOrientation; + rdp_event.monitor_layout.desktopScaleFactor = desktopScaleFactor; + rdp_event.monitor_layout.deviceScaleFactor = deviceScaleFactor; + remmina_rdp_event_event_push(gp, &rdp_event); + } + } + } + + g_free(monitorids); + + return FALSE; +} + +void remmina_rdp_event_send_delayed_monitor_layout(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + rfContext *rfi = GET_PLUGIN_DATA(gp); + + if (!rfi || !rfi->connected || rfi->is_reconnecting) + return; + if (rfi->delayed_monitor_layout_handler) { + g_source_remove(rfi->delayed_monitor_layout_handler); + rfi->delayed_monitor_layout_handler = 0; + } + if (rfi->scale == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES) + rfi->delayed_monitor_layout_handler = g_timeout_add(500, (GSourceFunc)remmina_rdp_event_delayed_monitor_layout, gp); +} + +static gboolean remmina_rdp_event_on_configure(GtkWidget *widget, GdkEventConfigure *event, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + /* Called when gp changes its size or position */ + + rfContext *rfi = GET_PLUGIN_DATA(gp); + + if (!rfi || !rfi->connected || rfi->is_reconnecting) + return FALSE; + + remmina_rdp_event_update_scale_factor(gp); + + /* If the scaler is not active, schedule a delayed remote resolution change */ + remmina_rdp_event_send_delayed_monitor_layout(gp); + + + return FALSE; +} + +static void remmina_rdp_event_translate_pos(RemminaProtocolWidget *gp, int ix, int iy, UINT16 *ox, UINT16 *oy) +{ + TRACE_CALL(__func__); + rfContext *rfi = GET_PLUGIN_DATA(gp); + + /* + * Translate a position from local window coordinates (ix,iy) to + * RDP coordinates and put result on (*ox,*uy) + * */ + + if (!rfi || !rfi->connected || rfi->is_reconnecting) + return; + + if ((rfi->scale == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED) && (rfi->scale_width >= 1) && (rfi->scale_height >= 1)) { + *ox = (UINT16)(ix * remmina_plugin_service->protocol_plugin_get_width(gp) / rfi->scale_width); + *oy = (UINT16)(iy * remmina_plugin_service->protocol_plugin_get_height(gp) / rfi->scale_height); + } else { + *ox = (UINT16)ix; + *oy = (UINT16)iy; + } +} + +static void remmina_rdp_event_reverse_translate_pos_reverse(RemminaProtocolWidget *gp, int ix, int iy, int *ox, int *oy) +{ + TRACE_CALL(__func__); + rfContext *rfi = GET_PLUGIN_DATA(gp); + + /* + * Translate a position from RDP coordinates (ix,iy) to + * local window coordinates and put result on (*ox,*uy) + * */ + + if (!rfi || !rfi->connected || rfi->is_reconnecting) + return; + + if ((rfi->scale == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED) && (rfi->scale_width >= 1) && (rfi->scale_height >= 1)) { + *ox = (ix * rfi->scale_width) / remmina_plugin_service->protocol_plugin_get_width(gp); + *oy = (iy * rfi->scale_height) / remmina_plugin_service->protocol_plugin_get_height(gp); + } else { + *ox = ix; + *oy = iy; + } +} + +void remmina_rdp_mouse_jitter(RemminaProtocolWidget *gp){ + TRACE_CALL(__func__); + RemminaPluginRdpEvent rdp_event = { 0 }; + RemminaFile *remminafile; + rfContext *rfi = GET_PLUGIN_DATA(gp); + + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + if (remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE)) + return; + + rdp_event.type = REMMINA_RDP_EVENT_TYPE_MOUSE; + rdp_event.mouse_event.flags = PTR_FLAGS_MOVE; + rdp_event.mouse_event.extended = FALSE; + rdp_event.mouse_event.x = rfi->last_x; + rdp_event.mouse_event.y = rfi->last_y; + remmina_rdp_event_event_push(gp, &rdp_event); +} + +static gboolean remmina_rdp_event_on_motion(GtkWidget *widget, GdkEventMotion *event, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginRdpEvent rdp_event = { 0 }; + RemminaFile *remminafile; + rfContext *rfi = GET_PLUGIN_DATA(gp); + + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + if (remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE)) + return FALSE; + + rdp_event.type = REMMINA_RDP_EVENT_TYPE_MOUSE; + rdp_event.mouse_event.flags = PTR_FLAGS_MOVE; + rdp_event.mouse_event.extended = FALSE; + + remmina_rdp_event_translate_pos(gp, event->x, event->y, &rdp_event.mouse_event.x, &rdp_event.mouse_event.y); + if (rfi != NULL){ + rfi->last_x = rdp_event.mouse_event.x; + rfi->last_y = rdp_event.mouse_event.y; + } + + remmina_rdp_event_event_push(gp, &rdp_event); + + return TRUE; +} + +static gboolean remmina_rdp_event_on_button(GtkWidget *widget, GdkEventButton *event, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + gint flag; + gboolean extended = FALSE; + RemminaPluginRdpEvent rdp_event = { 0 }; + gint primary, secondary; + + RemminaFile *remminafile; + + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + if (remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE)) + return FALSE; + + /* We bypass 2button-press and 3button-press events */ + if ((event->type != GDK_BUTTON_PRESS) && (event->type != GDK_BUTTON_RELEASE)) + return TRUE; + + flag = 0; + + if (remmina_plugin_service->file_get_int(remminafile, "left-handed", FALSE)) { + primary = PTR_FLAGS_BUTTON2; + secondary = PTR_FLAGS_BUTTON1; + } else { + primary = PTR_FLAGS_BUTTON1; + secondary = PTR_FLAGS_BUTTON2; + } + + switch (event->button) { + case 1: + flag |= primary; + break; + case 2: + flag |= PTR_FLAGS_BUTTON3; + break; + case 3: + flag |= secondary; + break; + case 8: /* back */ + case 97: /* Xming */ + extended = TRUE; + flag |= PTR_XFLAGS_BUTTON1; + break; + case 9: /* forward */ + case 112: /* Xming */ + extended = TRUE; + flag |= PTR_XFLAGS_BUTTON2; + break; + default: + return FALSE; + } + + if (event->type == GDK_BUTTON_PRESS) { + if (extended) + flag |= PTR_XFLAGS_DOWN; + else + flag |= PTR_FLAGS_DOWN; + } + + rdp_event.type = REMMINA_RDP_EVENT_TYPE_MOUSE; + remmina_rdp_event_translate_pos(gp, event->x, event->y, &rdp_event.mouse_event.x, &rdp_event.mouse_event.y); + + if (flag != 0) { + rdp_event.mouse_event.flags = flag; + rdp_event.mouse_event.extended = extended; + remmina_rdp_event_event_push(gp, &rdp_event); + } + + return TRUE; +} + +static gboolean remmina_rdp_event_on_scroll(GtkWidget *widget, GdkEventScroll *event, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + gint flag; + RemminaPluginRdpEvent rdp_event = { 0 }; + float windows_delta; + RemminaFile *remminafile; + + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + if (remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE)) + return FALSE; + + flag = 0; + rdp_event.type = REMMINA_RDP_EVENT_TYPE_MOUSE; + + /* See [MS-RDPBCGR] TS_POINTER_EVENT and WM_MOUSEWHEEL message */ + + switch (event->direction) { + case GDK_SCROLL_UP: + flag = PTR_FLAGS_WHEEL | 0x0078; // 120 is one scroll unit defined in WM_MOUSEWHEEL + break; + + case GDK_SCROLL_DOWN: + flag = PTR_FLAGS_WHEEL | 0x0188; // -120 (one scroll unit) in 9 bits two's complement + break; + +#if GTK_CHECK_VERSION(3, 4, 0) + case GDK_SCROLL_SMOOTH: + + if (event->delta_y == 0.0) + return FALSE; + + windows_delta = event->delta_y * -120; + + if (windows_delta > 255) + windows_delta = 255; + if (windows_delta < -256) + windows_delta = -256; + + flag = PTR_FLAGS_WHEEL | ((short)windows_delta & WheelRotationMask); + + break; +#endif + + default: + return FALSE; + } + + rdp_event.mouse_event.flags = flag; + rdp_event.mouse_event.extended = FALSE; + remmina_rdp_event_translate_pos(gp, event->x, event->y, &rdp_event.mouse_event.x, &rdp_event.mouse_event.y); + remmina_rdp_event_event_push(gp, &rdp_event); + + return TRUE; +} + +static void remmina_rdp_event_init_keymap(rfContext *rfi, const gchar *strmap) +{ + long int v1, v2; + const char *s; + char *endptr; + RemminaPluginRdpKeymapEntry ke; + + if (strmap == NULL || strmap[0] == 0) { + rfi->keymap = NULL; + return; + } + s = strmap; + rfi->keymap = g_array_new(FALSE, TRUE, sizeof(RemminaPluginRdpKeymapEntry)); + while (1) { + v1 = strtol(s, &endptr, 10); + if (endptr == s) break; + s = endptr; + if (*s != ':') break; + s++; + v2 = strtol(s, &endptr, 10); + if (endptr == s) break; + s = endptr; + ke.orig_keycode = v1 & 0x7fffffff; + ke.translated_keycode = v2 & 0x7fffffff; + g_array_append_val(rfi->keymap, ke); + if (*s != ',') break; + s++; + } + if (rfi->keymap->len == 0) { + g_array_unref(rfi->keymap); + rfi->keymap = NULL; + } +} + +static gboolean remmina_rdp_event_on_key(GtkWidget *widget, GdkEventKey *event, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + guint32 unicode_keyval; + guint16 hardware_keycode; + rfContext *rfi = GET_PLUGIN_DATA(gp); + RemminaPluginRdpEvent rdp_event; + RemminaPluginRdpKeymapEntry *kep; + RemminaFile *remminafile; + DWORD scancode = 0; + int ik; + + if (!rfi || !rfi->connected || rfi->is_reconnecting) + return FALSE; + + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + if (remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE)) + return FALSE; + +#ifdef ENABLE_GTK_INSPECTOR_KEY + /* GTK inspector key is propagated up. Disabled by default. + * enable it by defining ENABLE_GTK_INSPECTOR_KEY */ + if ((event->state & GDK_CONTROL_MASK) != 0 && (event->keyval == GDK_KEY_I || event->keyval == GDK_KEY_D)) + return FALSE; + +#endif + + rdp_event.type = REMMINA_RDP_EVENT_TYPE_SCANCODE; + rdp_event.key_event.up = (event->type == GDK_KEY_PRESS ? false : true); + rdp_event.key_event.extended = false; + rdp_event.key_event.extended1 = false; + + switch (event->keyval) { + case GDK_KEY_Pause: + /* + * See https://msdn.microsoft.com/en-us/library/cc240584.aspx + * 2.2.8.1.1.3.1.1.1 Keyboard Event (TS_KEYBOARD_EVENT) + * for pause key management + */ + rdp_event.key_event.key_code = 0x1D; + rdp_event.key_event.up = false; + rdp_event.key_event.extended1 = TRUE; + remmina_rdp_event_event_push(gp, &rdp_event); + rdp_event.key_event.key_code = 0x45; + rdp_event.key_event.up = false; + rdp_event.key_event.extended1 = FALSE; + remmina_rdp_event_event_push(gp, &rdp_event); + rdp_event.key_event.key_code = 0x1D; + rdp_event.key_event.up = true; + rdp_event.key_event.extended1 = TRUE; + remmina_rdp_event_event_push(gp, &rdp_event); + rdp_event.key_event.key_code = 0x45; + rdp_event.key_event.up = true; + rdp_event.key_event.extended1 = FALSE; + remmina_rdp_event_event_push(gp, &rdp_event); + break; + + default: + if (!rfi->use_client_keymap) { + hardware_keycode = event->hardware_keycode; + if (rfi->keymap) { + for (ik = 0; ik < rfi->keymap->len; ik++) { + kep = &g_array_index(rfi->keymap, RemminaPluginRdpKeymapEntry, ik); + if (hardware_keycode == kep->orig_keycode) { + hardware_keycode = kep->translated_keycode; + break; + } + } + } + scancode = freerdp_keyboard_get_rdp_scancode_from_x11_keycode(hardware_keycode); + if (scancode) { + rdp_event.key_event.key_code = scancode & 0xFF; + rdp_event.key_event.extended = scancode & 0x100; + rdp_event.key_event.extended1 = FALSE; + remmina_rdp_event_event_push(gp, &rdp_event); + keypress_list_add(gp, rdp_event); + } + } else { + unicode_keyval = gdk_keyval_to_unicode(event->keyval); + /* Decide when whe should send a keycode or a Unicode character. + * - All non char keys (Shift, Alt, Super) should be sent as keycode + * - Space should be sent as keycode (see issue #1364) + * - All special keys (F1-F10, numeric pad, Home/End/Arrows/PgUp/PgDn/Insert/Delete) keycode + * - All key pressed while Ctrl or Alt or Super is down are not decoded by gdk_keyval_to_unicode(), so send it as keycode + * - All keycodes not translatable to unicode chars, as keycode + * - The rest as Unicode char + */ + if (event->keyval >= 0xfe00 || // Arrows, Shift, Alt, Fn, num keypad… + event->hardware_keycode == 0x41 || // Spacebar + unicode_keyval == 0 || // Impossible to translate + (event->state & (GDK_MOD1_MASK | GDK_CONTROL_MASK | GDK_SUPER_MASK)) != 0 // A modifier not recognized by gdk_keyval_to_unicode() + ) { + scancode = freerdp_keyboard_get_rdp_scancode_from_x11_keycode(event->hardware_keycode); + rdp_event.key_event.key_code = scancode & 0xFF; + rdp_event.key_event.extended = scancode & 0x100; + rdp_event.key_event.extended1 = FALSE; + if (rdp_event.key_event.key_code) { + remmina_rdp_event_event_push(gp, &rdp_event); + keypress_list_add(gp, rdp_event); + } + } else { + rdp_event.type = REMMINA_RDP_EVENT_TYPE_SCANCODE_UNICODE; + rdp_event.key_event.unicode_code = unicode_keyval; + rdp_event.key_event.extended = false; + rdp_event.key_event.extended1 = FALSE; + remmina_rdp_event_event_push(gp, &rdp_event); + keypress_list_add(gp, rdp_event); + } + } + break; + } + + return TRUE; +} + +gboolean remmina_rdp_event_on_clipboard(GtkClipboard *gtkClipboard, GdkEvent *event, RemminaProtocolWidget *gp) +{ + /* Signal handler for GTK clipboard owner-change */ + TRACE_CALL(__func__); + RemminaPluginRdpEvent rdp_event = { 0 }; + CLIPRDR_FORMAT_LIST *pFormatList; + GObject *new_owner; + + /* Usually "owner-change" is fired when a user presses "COPY" on the client + * OR when this plugin calls gtk_clipboard_set_with_owner() + * after receiving a RDP server format list in remmina_rdp_cliprdr_server_format_list() + * In the latter case, we must ignore owner change */ + + REMMINA_PLUGIN_DEBUG("gp=%p: owner-change event received", gp); + + rfContext *rfi = GET_PLUGIN_DATA(gp); + + if (rfi) + remmina_rdp_clipboard_abort_client_format_data_request(rfi); + + new_owner = gtk_clipboard_get_owner(gtkClipboard); + if (new_owner != (GObject *)gp) { + /* To do: avoid this when the new owner is another remmina protocol widget of + * the same remmina application */ + REMMINA_PLUGIN_DEBUG("gp=%p owner-change: new owner is different than me: new=%p me=%p", + gp, new_owner, gp); + + REMMINA_PLUGIN_DEBUG("gp=%p owner-change: new owner is not me: Sending local clipboard format list to server.", + gp, new_owner, gp); + pFormatList = remmina_rdp_cliprdr_get_client_format_list(gp); + rdp_event.type = REMMINA_RDP_EVENT_TYPE_CLIPBOARD_SEND_CLIENT_FORMAT_LIST; + rdp_event.clipboard_formatlist.pFormatList = pFormatList; + remmina_rdp_event_event_push(gp, &rdp_event); + } else { + REMMINA_PLUGIN_DEBUG(" ... but I'm the owner!"); + } + return TRUE; +} + +void remmina_rdp_event_init(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + gchar *s; + gint flags; + rfContext *rfi = GET_PLUGIN_DATA(gp); + GtkClipboard *clipboard; + RemminaFile *remminafile; + + gboolean disable_smooth_scrolling = FALSE; + + if (!rfi) return; + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + + /* we get first the global preferences */ + s = remmina_plugin_service->pref_get_value("rdp_disable_smooth_scrolling"); + disable_smooth_scrolling = (s && s[0] == '1' ? TRUE : FALSE); + g_free(s), s = NULL; + /* Otherwise we use the connection profile specific setting */ + disable_smooth_scrolling = remmina_plugin_service->file_get_int(remminafile, "disable-smooth-scrolling", disable_smooth_scrolling); + + REMMINA_PLUGIN_DEBUG("Disable smooth scrolling is set to %d", disable_smooth_scrolling); + + rfi->drawing_area = gtk_drawing_area_new(); + gtk_widget_show(rfi->drawing_area); + gtk_container_add(GTK_CONTAINER(gp), rfi->drawing_area); + + gtk_widget_add_events(rfi->drawing_area, GDK_POINTER_MOTION_MASK + | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + | GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK + | GDK_SCROLL_MASK | GDK_FOCUS_CHANGE_MASK); + + if (!disable_smooth_scrolling) { + REMMINA_PLUGIN_DEBUG("Adding GDK_SMOOTH_SCROLL_MASK"); + gtk_widget_add_events(rfi->drawing_area, GDK_SMOOTH_SCROLL_MASK); + } + + gtk_widget_set_can_focus(rfi->drawing_area, TRUE); + + remmina_plugin_service->protocol_plugin_register_hostkey(gp, rfi->drawing_area); + + s = remmina_plugin_service->pref_get_value("rdp_use_client_keymap"); + rfi->use_client_keymap = (s && s[0] == '1' ? TRUE : FALSE); + g_free(s), s = NULL; + + /* Read special keymap from profile file, if exists */ + remmina_rdp_event_init_keymap(rfi, remmina_plugin_service->pref_get_value("rdp_map_keycode")); + + if (rfi->use_client_keymap && rfi->keymap) + fprintf(stderr, "RDP profile error: you cannot define both rdp_map_hardware_keycode and have 'Use client keyboard mapping' enabled\n"); + + g_signal_connect(G_OBJECT(rfi->drawing_area), "draw", + G_CALLBACK(remmina_rdp_event_on_draw), gp); + g_signal_connect(G_OBJECT(rfi->drawing_area), "configure-event", + G_CALLBACK(remmina_rdp_event_on_configure), gp); + g_signal_connect(G_OBJECT(rfi->drawing_area), "motion-notify-event", + G_CALLBACK(remmina_rdp_event_on_motion), gp); + g_signal_connect(G_OBJECT(rfi->drawing_area), "button-press-event", + G_CALLBACK(remmina_rdp_event_on_button), gp); + g_signal_connect(G_OBJECT(rfi->drawing_area), "button-release-event", + G_CALLBACK(remmina_rdp_event_on_button), gp); + g_signal_connect(G_OBJECT(rfi->drawing_area), "scroll-event", + G_CALLBACK(remmina_rdp_event_on_scroll), gp); + g_signal_connect(G_OBJECT(rfi->drawing_area), "key-press-event", + G_CALLBACK(remmina_rdp_event_on_key), gp); + g_signal_connect(G_OBJECT(rfi->drawing_area), "key-release-event", + G_CALLBACK(remmina_rdp_event_on_key), gp); + g_signal_connect(G_OBJECT(rfi->drawing_area), "focus-in-event", + G_CALLBACK(remmina_rdp_event_on_focus_in), gp); + /** Fixme: This comment + * needed for TS_SUPPRESS_OUTPUT_PDU + * But it works only when we stay in the same window mode, if we switch to + * fullscreen, for instance, the object refernce is lost, so we loose these + * events. + */ + //g_signal_connect(G_OBJECT(gtk_widget_get_toplevel(rfi->drawing_area)), "map-event", + // G_CALLBACK(remmina_rdp_event_on_map), gp); + //g_signal_connect(G_OBJECT(gtk_widget_get_toplevel(rfi->drawing_area)), "unmap-event", + // G_CALLBACK(remmina_rdp_event_on_unmap), gp); + + if (!remmina_plugin_service->file_get_int(remminafile, "disableclipboard", FALSE)) { + clipboard = gtk_widget_get_clipboard(rfi->drawing_area, GDK_SELECTION_CLIPBOARD); + rfi->clipboard.clipboard_handler = g_signal_connect(clipboard, "owner-change", G_CALLBACK(remmina_rdp_event_on_clipboard), gp); + } + + rfi->pressed_keys = g_array_new(FALSE, TRUE, sizeof(RemminaPluginRdpEvent)); + rfi->event_queue = g_async_queue_new_full(g_free); + rfi->ui_queue = g_async_queue_new(); + pthread_mutex_init(&rfi->ui_queue_mutex, NULL); + + if (pipe(rfi->event_pipe)) { + g_print("Error creating pipes.\n"); + rfi->event_pipe[0] = -1; + rfi->event_pipe[1] = -1; + rfi->event_handle = NULL; + } else { + flags = fcntl(rfi->event_pipe[0], F_GETFL, 0); + fcntl(rfi->event_pipe[0], F_SETFL, flags | O_NONBLOCK); + rfi->event_handle = CreateFileDescriptorEvent(NULL, FALSE, FALSE, rfi->event_pipe[0], WINPR_FD_READ); + if (!rfi->event_handle) + g_print("CreateFileDescriptorEvent() failed\n"); + } + + rfi->object_table = g_hash_table_new_full(NULL, NULL, NULL, g_free); + + rfi->display = gdk_display_get_default(); + +#if GTK_CHECK_VERSION(3, 22, 0) + GdkVisual *visual = gdk_screen_get_system_visual( + gdk_display_get_default_screen(rfi->display)); + rfi->bpp = gdk_visual_get_depth(visual); +#else + rfi->bpp = gdk_visual_get_best_depth(); +#endif +} + +void remmina_rdp_event_free_event(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *obj) +{ + TRACE_CALL(__func__); + + switch (obj->type) { + case REMMINA_RDP_UI_NOCODEC: + free(obj->nocodec.bitmap); + break; + + default: + break; + } + + g_free(obj); +} + +void remmina_rdp_event_uninit(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + rfContext *rfi = GET_PLUGIN_DATA(gp); + RemminaPluginRdpUiObject *ui; + + if (!rfi) return; + + /* unregister the clipboard monitor */ + if (rfi->clipboard.clipboard_handler) { + g_signal_handler_disconnect(G_OBJECT(gtk_widget_get_clipboard(rfi->drawing_area, GDK_SELECTION_CLIPBOARD)), rfi->clipboard.clipboard_handler); + rfi->clipboard.clipboard_handler = 0; + } + if (rfi->delayed_monitor_layout_handler) { + g_source_remove(rfi->delayed_monitor_layout_handler); + rfi->delayed_monitor_layout_handler = 0; + } + if (rfi->ui_handler) { + g_source_remove(rfi->ui_handler); + rfi->ui_handler = 0; + } + while ((ui = (RemminaPluginRdpUiObject *)g_async_queue_try_pop(rfi->ui_queue)) != NULL) + remmina_rdp_event_free_event(gp, ui); + if (rfi->surface) { + cairo_surface_mark_dirty(rfi->surface); + cairo_surface_destroy(rfi->surface); + rfi->surface = NULL; + } + + g_hash_table_destroy(rfi->object_table); + + g_array_free(rfi->pressed_keys, TRUE); + if (rfi->keymap) { + g_array_free(rfi->keymap, TRUE); + rfi->keymap = NULL; + } + g_async_queue_unref(rfi->event_queue); + rfi->event_queue = NULL; + g_async_queue_unref(rfi->ui_queue); + rfi->ui_queue = NULL; + pthread_mutex_destroy(&rfi->ui_queue_mutex); + + if (rfi->event_handle) { + CloseHandle(rfi->event_handle); + rfi->event_handle = NULL; + } + + close(rfi->event_pipe[0]); + close(rfi->event_pipe[1]); +} + +static void remmina_rdp_event_create_cairo_surface(rfContext *rfi) +{ + int stride; + rdpGdi *gdi; + + if (!rfi) + return; + + gdi = ((rdpContext *)rfi)->gdi; + + if (!gdi) + return; + + if (rfi->surface) { + cairo_surface_mark_dirty(rfi->surface); + cairo_surface_destroy(rfi->surface); + rfi->surface = NULL; + } + stride = cairo_format_stride_for_width(rfi->cairo_format, gdi->width); + rfi->surface = cairo_image_surface_create_for_data((unsigned char *)gdi->primary_buffer, rfi->cairo_format, gdi->width, gdi->height, stride); + cairo_surface_flush(rfi->surface); +} + +void remmina_rdp_event_update_scale(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + gint width, height; + rdpGdi *gdi; + rfContext *rfi = GET_PLUGIN_DATA(gp); + + width = remmina_plugin_service->protocol_plugin_get_width(gp); + height = remmina_plugin_service->protocol_plugin_get_height(gp); + + gdi = ((rdpContext *)rfi)->gdi; + + rfi->scale = remmina_plugin_service->remmina_protocol_widget_get_current_scale_mode(gp); + + /* See if we also must rellocate rfi->surface with different width and height, + * this usually happens after a DesktopResize RDP event*/ + + if (rfi->surface && (cairo_image_surface_get_width(rfi->surface) != gdi->width || + cairo_image_surface_get_height(rfi->surface) != gdi->height)) { + /* Destroys and recreate rfi->surface with new width and height */ + cairo_surface_mark_dirty(rfi->surface); + cairo_surface_destroy(rfi->surface); + rfi->surface = NULL; + remmina_rdp_event_create_cairo_surface(rfi); + } else if (rfi->surface == NULL) { + remmina_rdp_event_create_cairo_surface(rfi); + } + + /* Send gdi->width and gdi->height obtained from remote server to gp plugin, + * so they will be saved when closing connection */ + if (width != gdi->width) + remmina_plugin_service->protocol_plugin_set_width(gp, gdi->width); + if (height != gdi->height) + remmina_plugin_service->protocol_plugin_set_height(gp, gdi->height); + + remmina_rdp_event_update_scale_factor(gp); + + if (rfi->scale == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED || rfi->scale == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES) + /* In scaled mode and autores mode, drawing_area will get its dimensions from its parent */ + gtk_widget_set_size_request(rfi->drawing_area, -1, -1); + else + /* In non scaled mode, the plugins forces dimensions of drawing area */ + gtk_widget_set_size_request(rfi->drawing_area, width, height); + remmina_plugin_service->protocol_plugin_update_align(gp); +} + +static void remmina_rdp_event_connected(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui) +{ + TRACE_CALL(__func__); + rfContext *rfi = GET_PLUGIN_DATA(gp); + rdpGdi *gdi; + + gdi = ((rdpContext *)rfi)->gdi; + + gtk_widget_realize(rfi->drawing_area); + + remmina_rdp_event_create_cairo_surface(rfi); + gtk_widget_queue_draw_area(rfi->drawing_area, 0, 0, gdi->width, gdi->height); + + remmina_rdp_event_update_scale(gp); + + remmina_plugin_service->protocol_plugin_signal_connection_opened(gp); + const gchar *host = freerdp_settings_get_string (rfi->clientContext.context.settings, FreeRDP_ServerHostname); + // TRANSLATORS: the placeholder may be either an IP/FQDN or a server hostname + REMMINA_PLUGIN_AUDIT(_("Connected to %s via RDP"), host); +} + +static void remmina_rdp_event_reconnect_progress(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui) +{ + TRACE_CALL(__func__); + rfContext *rfi = GET_PLUGIN_DATA(gp); + + gdk_window_invalidate_rect(gtk_widget_get_window(rfi->drawing_area), NULL, TRUE); +} + +static BOOL remmina_rdp_event_create_cursor(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui) +{ + TRACE_CALL(__func__); + GdkPixbuf *pixbuf; + rfContext *rfi = GET_PLUGIN_DATA(gp); + rdpPointer *pointer = (rdpPointer *)ui->cursor.pointer; + cairo_surface_t *surface; + UINT8 *data = malloc(pointer->width * pointer->height * 4); + + if (!freerdp_image_copy_from_pointer_data( + (BYTE *)data, PIXEL_FORMAT_BGRA32, + pointer->width * 4, 0, 0, pointer->width, pointer->height, + pointer->xorMaskData, pointer->lengthXorMask, + pointer->andMaskData, pointer->lengthAndMask, + pointer->xorBpp, &(ui->cursor.context->gdi->palette))) { + free(data); + return FALSE; + } + + surface = cairo_image_surface_create_for_data(data, CAIRO_FORMAT_ARGB32, pointer->width, pointer->height, cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, pointer->width)); + cairo_surface_flush(surface); + pixbuf = gdk_pixbuf_get_from_surface(surface, 0, 0, pointer->width, pointer->height); + cairo_surface_mark_dirty(surface); + cairo_surface_destroy(surface); + free(data); + ((rfPointer *)ui->cursor.pointer)->cursor = gdk_cursor_new_from_pixbuf(rfi->display, pixbuf, pointer->xPos, pointer->yPos); + g_object_unref(pixbuf); + + return TRUE; +} + +static void remmina_rdp_event_free_cursor(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui) +{ + TRACE_CALL(__func__); + g_object_unref(ui->cursor.pointer->cursor); + ui->cursor.pointer->cursor = NULL; +} + +static BOOL remmina_rdp_event_set_pointer_position(RemminaProtocolWidget *gp, gint x, gint y) +{ + TRACE_CALL(__func__); + GdkWindow *w, *nw; + gint nx, ny, wx, wy; + +#if GTK_CHECK_VERSION(3, 20, 0) + GdkSeat *seat; +#else + GdkDeviceManager *manager; +#endif + GdkDevice *dev; + rfContext *rfi = GET_PLUGIN_DATA(gp); + + if (rfi == NULL) + return FALSE; + + w = gtk_widget_get_window(rfi->drawing_area); +#if GTK_CHECK_VERSION(3, 20, 0) + seat = gdk_display_get_default_seat(gdk_display_get_default()); + dev = gdk_seat_get_pointer(seat); +#else + manager = gdk_display_get_device_manager(gdk_display_get_default()); + dev = gdk_device_manager_get_client_pointer(manager); +#endif + + nw = gdk_device_get_window_at_position(dev, NULL, NULL); + + if (nw == w) { + nx = 0; + ny = 0; + remmina_rdp_event_reverse_translate_pos_reverse(gp, x, y, &nx, &ny); + gdk_window_get_root_coords(w, nx, ny, &wx, &wy); + gdk_device_warp(dev, gdk_window_get_screen(w), wx, wy); + } + return TRUE; +} + +static void remmina_rdp_event_cursor(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui) +{ + TRACE_CALL(__func__); + rfContext *rfi = GET_PLUGIN_DATA(gp); + + switch (ui->cursor.type) { + case REMMINA_RDP_POINTER_NEW: + ui->retval = remmina_rdp_event_create_cursor(gp, ui) ? 1 : 0; + break; + + case REMMINA_RDP_POINTER_FREE: + remmina_rdp_event_free_cursor(gp, ui); + break; + + case REMMINA_RDP_POINTER_SET: + gdk_window_set_cursor(gtk_widget_get_window(rfi->drawing_area), ui->cursor.pointer->cursor); + ui->retval = 1; + break; + + case REMMINA_RDP_POINTER_SETPOS: + ui->retval = remmina_rdp_event_set_pointer_position(gp, ui->pos.x, ui->pos.y) ? 1 : 0; + break; + + case REMMINA_RDP_POINTER_NULL: + gdk_window_set_cursor(gtk_widget_get_window(rfi->drawing_area), + gdk_cursor_new_for_display(gdk_display_get_default(), + GDK_BLANK_CURSOR)); + ui->retval = 1; + break; + + case REMMINA_RDP_POINTER_DEFAULT: + gdk_window_set_cursor(gtk_widget_get_window(rfi->drawing_area), NULL); + ui->retval = 1; + break; + } +} + +static void remmina_rdp_ui_event_update_scale(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui) +{ + TRACE_CALL(__func__); + remmina_rdp_event_update_scale(gp); +} + +void remmina_rdp_event_unfocus(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + rfContext *rfi = GET_PLUGIN_DATA(gp); + + if (!rfi || !rfi->connected || rfi->is_reconnecting) + return; + remmina_rdp_event_release_all_keys(gp); +} + +static void remmina_rdp_ui_event_destroy_cairo_surface(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui) +{ + TRACE_CALL(__func__); + rfContext *rfi = GET_PLUGIN_DATA(gp); + + cairo_surface_mark_dirty(rfi->surface); + cairo_surface_destroy(rfi->surface); + rfi->surface = NULL; +} + +static void remmina_rdp_event_process_event(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui) +{ + TRACE_CALL(__func__); + switch (ui->event.type) { + case REMMINA_RDP_UI_EVENT_UPDATE_SCALE: + remmina_rdp_ui_event_update_scale(gp, ui); + break; + case REMMINA_RDP_UI_EVENT_DESTROY_CAIRO_SURFACE: + remmina_rdp_ui_event_destroy_cairo_surface(gp, ui); + break; + } +} + +static void remmina_rdp_event_process_ui_event(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui) +{ + TRACE_CALL(__func__); + switch (ui->type) { + case REMMINA_RDP_UI_UPDATE_REGIONS: + remmina_rdp_event_update_regions(gp, ui); + break; + + case REMMINA_RDP_UI_CONNECTED: + remmina_rdp_event_connected(gp, ui); + break; + + case REMMINA_RDP_UI_RECONNECT_PROGRESS: + remmina_rdp_event_reconnect_progress(gp, ui); + break; + + case REMMINA_RDP_UI_CURSOR: + remmina_rdp_event_cursor(gp, ui); + break; + + case REMMINA_RDP_UI_CLIPBOARD: + remmina_rdp_event_process_clipboard(gp, ui); + break; + + case REMMINA_RDP_UI_EVENT: + remmina_rdp_event_process_event(gp, ui); + break; + + default: + break; + } +} + +static gboolean remmina_rdp_event_process_ui_queue(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + + rfContext *rfi = GET_PLUGIN_DATA(gp); + RemminaPluginRdpUiObject *ui; + + pthread_mutex_lock(&rfi->ui_queue_mutex); + ui = (RemminaPluginRdpUiObject *)g_async_queue_try_pop(rfi->ui_queue); + if (ui) { + pthread_mutex_lock(&ui->sync_wait_mutex); + if (!rfi->thread_cancelled) + remmina_rdp_event_process_ui_event(gp, ui); + // Should we signal the caller thread to unlock ? + if (ui->sync) { + ui->complete = TRUE; + pthread_cond_signal(&ui->sync_wait_cond); + pthread_mutex_unlock(&ui->sync_wait_mutex); + } else { + remmina_rdp_event_free_event(gp, ui); + } + + pthread_mutex_unlock(&rfi->ui_queue_mutex); + return TRUE; + } else { + rfi->ui_handler = 0; + pthread_mutex_unlock(&rfi->ui_queue_mutex); + return FALSE; + } +} + +static void remmina_rdp_event_queue_ui(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui) +{ + TRACE_CALL(__func__); + rfContext *rfi = GET_PLUGIN_DATA(gp); + gboolean ui_sync_save; + int oldcanceltype; + + if (!rfi || rfi->thread_cancelled) + return; + + if (remmina_plugin_service->is_main_thread()) { + remmina_rdp_event_process_ui_event(gp, ui); + return; + } + + pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &oldcanceltype); + + pthread_mutex_lock(&rfi->ui_queue_mutex); + + ui_sync_save = ui->sync; + ui->complete = FALSE; + + if (ui_sync_save) { + pthread_mutex_init(&ui->sync_wait_mutex, NULL); + pthread_cond_init(&ui->sync_wait_cond, NULL); + } + + ui->complete = FALSE; + + g_async_queue_push(rfi->ui_queue, ui); + + if (!rfi->ui_handler) + rfi->ui_handler = IDLE_ADD((GSourceFunc)remmina_rdp_event_process_ui_queue, gp); + + if (ui_sync_save) { + /* Wait for main thread function completion before returning */ + pthread_mutex_lock(&ui->sync_wait_mutex); + pthread_mutex_unlock(&rfi->ui_queue_mutex); + while (!ui->complete) + pthread_cond_wait(&ui->sync_wait_cond, &ui->sync_wait_mutex); + pthread_cond_destroy(&ui->sync_wait_cond); + pthread_mutex_destroy(&ui->sync_wait_mutex); + } else { + pthread_mutex_unlock(&rfi->ui_queue_mutex); + } + pthread_setcanceltype(oldcanceltype, NULL); +} + +void remmina_rdp_event_queue_ui_async(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui) +{ + ui->sync = FALSE; + remmina_rdp_event_queue_ui(gp, ui); +} + +int remmina_rdp_event_queue_ui_sync_retint(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui) +{ + TRACE_CALL(__func__); + int retval; + + ui->sync = TRUE; + remmina_rdp_event_queue_ui(gp, ui); + retval = ui->retval; + remmina_rdp_event_free_event(gp, ui); + return retval; +} + +void *remmina_rdp_event_queue_ui_sync_retptr(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui) +{ + TRACE_CALL(__func__); + void *rp; + + ui->sync = TRUE; + remmina_rdp_event_queue_ui(gp, ui); + rp = ui->retptr; + remmina_rdp_event_free_event(gp, ui); + return rp; +} diff --git a/plugins/rdp/rdp_event.h b/plugins/rdp/rdp_event.h new file mode 100644 index 0000000..8a7f08a --- /dev/null +++ b/plugins/rdp/rdp_event.h @@ -0,0 +1,58 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2010-2011 Vic Lee + * Copyright (C) 2017-2023 Antenore Gatta, Giovanni Panozzo + * + * 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 <glib.h> +#include "common/remmina_plugin.h" +#include "rdp_plugin.h" + +G_BEGIN_DECLS + + +void remmina_rdp_event_init(RemminaProtocolWidget *gp); +void remmina_rdp_event_uninit(RemminaProtocolWidget *gp); +void remmina_rdp_event_update_scale(RemminaProtocolWidget *gp); +void remmina_rdp_event_unfocus(RemminaProtocolWidget *gp); +void remmina_rdp_event_send_delayed_monitor_layout(RemminaProtocolWidget *gp); +void remmina_rdp_event_update_rect(RemminaProtocolWidget *gp, gint x, gint y, gint w, gint h); +void remmina_rdp_event_queue_ui_async(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui); +int remmina_rdp_event_queue_ui_sync_retint(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui); +void *remmina_rdp_event_queue_ui_sync_retptr(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui); +gboolean remmina_rdp_event_on_map(RemminaProtocolWidget *gp); +gboolean remmina_rdp_event_on_unmap(RemminaProtocolWidget *gp); +void remmina_rdp_mouse_jitter(RemminaProtocolWidget *gp); + +G_END_DECLS diff --git a/plugins/rdp/rdp_file.c b/plugins/rdp/rdp_file.c new file mode 100644 index 0000000..fc161e8 --- /dev/null +++ b/plugins/rdp/rdp_file.c @@ -0,0 +1,299 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2010-2011 Vic Lee + * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * 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 "remmina/plugin.h" +#include "rdp_plugin.h" +#include "rdp_file.h" + +gboolean remmina_rdp_file_import_test(RemminaFilePlugin *plugin, const gchar *from_file) +{ + TRACE_CALL(__func__); + gchar *ext; + + ext = strrchr(from_file, '.'); + + if (!ext) + return FALSE; + + ext++; + + if (g_strcmp0(ext, "RDP") == 0) + return TRUE; + + if (g_strcmp0(ext, "rdp") == 0) + return TRUE; + + return FALSE; +} + +static void remmina_rdp_file_import_field(RemminaFile *remminafile, const gchar *key, const gchar *value) +{ + TRACE_CALL(__func__); + if (g_strcmp0(key, "desktopwidth") == 0) { + remmina_plugin_service->file_set_string(remminafile, "resolution_width", value); + } else if (g_strcmp0(key, "desktopheight") == 0) { + remmina_plugin_service->file_set_string(remminafile, "resolution_height", value); + } else if (g_strcmp0(key, "session bpp") == 0) { + remmina_plugin_service->file_set_string(remminafile, "colordepth", value); + } else if (g_strcmp0(key, "keyboardhook") == 0) { + remmina_plugin_service->file_set_int(remminafile, "keyboard_grab", (atoi(value) == 1)); + } else if (g_strcmp0(key, "full address") == 0) { + remmina_plugin_service->file_set_string(remminafile, "server", value); + } else if (g_strcmp0(key, "audiomode") == 0) { + switch (atoi(value)) { + case 0: + remmina_plugin_service->file_set_string(remminafile, "sound", "local"); + break; + case 1: + remmina_plugin_service->file_set_string(remminafile, "sound", "remote"); + break; + } + } else if (g_strcmp0(key, "redirectprinters") == 0) { + remmina_plugin_service->file_set_int(remminafile, "shareprinter", (atoi(value) == 1)); + } else if (g_strcmp0(key, "redirectsmartcard") == 0) { + remmina_plugin_service->file_set_int(remminafile, "sharesmartcard", (atoi(value) == 1)); + } else if (g_strcmp0(key, "redirectclipboard") == 0) { + remmina_plugin_service->file_set_int(remminafile, "disableclipboard", (atoi(value) != 1)); + } else if (g_strcmp0(key, "alternate shell") == 0) { + remmina_plugin_service->file_set_string(remminafile, "exec", value); + } else if (g_strcmp0(key, "shell working directory") == 0) { + remmina_plugin_service->file_set_string(remminafile, "execpath", value); + } else if (g_strcmp0(key, "loadbalanceinfo") == 0) { + remmina_plugin_service->file_set_string(remminafile, "loadbalanceinfo", value); + } else if (g_strcmp0(key, "gatewayhostname") == 0) { + remmina_plugin_service->file_set_string(remminafile, "gateway_server", value); + } else if (g_strcmp0(key, "gatewayusagemethod") == 0) { + remmina_plugin_service->file_set_int(remminafile, "gateway_usage", (atoi(value) == TSC_PROXY_MODE_DETECT)); + } else if (g_strcmp0(key, "gatewayaccesstoken") == 0) { + remmina_plugin_service->file_set_string(remminafile, "gatewayaccesstoken", value); + } else if (g_strcmp0(key, "authentication level") == 0) { + remmina_plugin_service->file_set_int(remminafile, "authentication level", atoi(value)); + } + /* tsclient fields, import only */ + else if (g_strcmp0(key, "client hostname") == 0) { + remmina_plugin_service->file_set_string(remminafile, "clientname", value); + } else if (g_strcmp0(key, "domain") == 0) { + remmina_plugin_service->file_set_string(remminafile, "domain", value); + } else if (g_strcmp0(key, "username") == 0) { + remmina_plugin_service->file_set_string(remminafile, "username", value); + } else if (g_strcmp0(key, "password") == 0) { + remmina_plugin_service->file_set_string(remminafile, "password", value); + } +} + +static RemminaFile *remmina_rdp_file_import_channel(GIOChannel *channel) +{ + TRACE_CALL(__func__); + gchar *p; + const gchar *enc; + gchar *line = NULL; + GError *error = NULL; + gsize bytes_read = 0; + RemminaFile *remminafile; + guchar magic[2] = { 0 }; + + if (g_io_channel_set_encoding(channel, NULL, &error) != G_IO_STATUS_NORMAL) { + g_print("g_io_channel_set_encoding: %s\n", error->message); + return NULL; + } + + /* Try to detect the UTF-16 encoding */ + if (g_io_channel_read_chars(channel, (gchar *)magic, 2, &bytes_read, &error) != G_IO_STATUS_NORMAL) { + g_print("g_io_channel_read_chars: %s\n", error->message); + return NULL; + } + + if (magic[0] == 0xFF && magic[1] == 0xFE) { + enc = "UTF-16LE"; + } else if (magic[0] == 0xFE && magic[1] == 0xFF) { + enc = "UTF-16BE"; + } else { + enc = "UTF-8"; + if (g_io_channel_seek_position(channel, 0, G_SEEK_SET, &error) != G_IO_STATUS_NORMAL) { + g_print("g_io_channel_seek: failed\n"); + return NULL; + } + } + + if (g_io_channel_set_encoding(channel, enc, &error) != G_IO_STATUS_NORMAL) { + g_print("g_io_channel_set_encoding: %s\n", error->message); + return NULL; + } + + remminafile = remmina_plugin_service->file_new(); + + while (g_io_channel_read_line(channel, &line, NULL, &bytes_read, &error) == G_IO_STATUS_NORMAL) { + if (line == NULL) + break; + + line[bytes_read] = '\0'; + p = strchr(line, ':'); + + if (p) { + *p++ = '\0'; + p = strchr(p, ':'); + + if (p) { + p++; + remmina_rdp_file_import_field(remminafile, line, p); + } + } + + g_free(line); + } + + remmina_plugin_service->file_set_string(remminafile, "name", + remmina_plugin_service->file_get_string(remminafile, "server")); + remmina_plugin_service->file_set_string(remminafile, "protocol", "RDP"); + + return remminafile; +} + +RemminaFile *remmina_rdp_file_import(RemminaFilePlugin *plugin,const gchar *from_file) +{ + TRACE_CALL(__func__); + GIOChannel *channel; + GError *error = NULL; + RemminaFile *remminafile; + + channel = g_io_channel_new_file(from_file, "r", &error); + + if (channel == NULL) { + g_print("Failed to import %s: %s\n", from_file, error->message); + return NULL; + } + + remminafile = remmina_rdp_file_import_channel(channel); + g_io_channel_shutdown(channel, TRUE, &error); + + return remminafile; +} + +gboolean remmina_rdp_file_export_test(RemminaFilePlugin *plugin, RemminaFile *remminafile) +{ + TRACE_CALL(__func__); + if (g_strcmp0(remmina_plugin_service->file_get_string(remminafile, "protocol"), "RDP") == 0) + return TRUE; + + return FALSE; +} + +gboolean remmina_rdp_file_export_channel(RemminaFile *remminafile, FILE *fp) +{ + TRACE_CALL(__func__); + const gchar *cs; + int w, h; + + fprintf(fp, "screen mode id:i:2\r\n"); + w = remmina_plugin_service->file_get_int(remminafile, "resolution_width", -1); + h = remmina_plugin_service->file_get_int(remminafile, "resolution_height", -1); + if (w > 0 && h > 0) { + fprintf(fp, "desktopwidth:i:%d\r\n", w); + fprintf(fp, "desktopheight:i:%d\r\n", h); + } + + fprintf(fp, "session bpp:i:%i\r\n", remmina_plugin_service->file_get_int(remminafile, "colordepth", 8)); + //fprintf(fp, "winposstr:s:0,1,123,34,931,661\r\n"); + fprintf(fp, "compression:i:1\r\n"); + fprintf(fp, "keyboardhook:i:2\r\n"); + fprintf(fp, "displayconnectionbar:i:1\r\n"); + fprintf(fp, "disable wallpaper:i:1\r\n"); + fprintf(fp, "disable full window drag:i:1\r\n"); + fprintf(fp, "allow desktop composition:i:0\r\n"); + fprintf(fp, "allow font smoothing:i:0\r\n"); + fprintf(fp, "disable menu anims:i:1\r\n"); + fprintf(fp, "disable themes:i:0\r\n"); + fprintf(fp, "disable cursor setting:i:0\r\n"); + fprintf(fp, "bitmapcachepersistenable:i:1\r\n"); + cs = remmina_plugin_service->file_get_string(remminafile, "server"); + fprintf(fp, "full address:s:%s\r\n", cs ? cs : ""); + if (g_strcmp0(remmina_plugin_service->file_get_string(remminafile, "sound"), "local") == 0) + fprintf(fp, "audiomode:i:0\r\n"); + else if (g_strcmp0(remmina_plugin_service->file_get_string(remminafile, "sound"), "remote") == 0) + fprintf(fp, "audiomode:i:1\r\n"); + else + fprintf(fp, "audiomode:i:2\r\n"); + if (g_strcmp0(remmina_plugin_service->file_get_string(remminafile, "microphone"), "") == 0) + fprintf(fp, "audiocapturemode:i:0\r\n"); + else if (g_strcmp0(remmina_plugin_service->file_get_string(remminafile, "microphone"), "0") == 0) + fprintf(fp, "audiocapturemode:i:1\r\n"); + else + fprintf(fp, "audiocapturemode:i:1\r\n"); + fprintf(fp, "redirectprinters:i:%i\r\n", remmina_plugin_service->file_get_int(remminafile, "shareprinter", FALSE) ? 1 : 0); + fprintf(fp, "redirectsmartcard:i:%i\r\n", remmina_plugin_service->file_get_int(remminafile, "sharesmartcard", FALSE) ? 1 : 0); + fprintf(fp, "redirectcomports:i:0\r\n"); + fprintf(fp, "redirectsmartcards:i:0\r\n"); + fprintf(fp, "redirectclipboard:i:1\r\n"); + fprintf(fp, "redirectposdevices:i:0\r\n"); + fprintf(fp, "autoreconnection enabled:i:1\r\n"); + fprintf(fp, "authentication level:i:0\r\n"); + fprintf(fp, "prompt for credentials:i:1\r\n"); + fprintf(fp, "negotiate security layer:i:1\r\n"); + fprintf(fp, "remoteapplicationmode:i:0\r\n"); + cs = remmina_plugin_service->file_get_string(remminafile, "exec"); + fprintf(fp, "alternate shell:s:%s\r\n", cs ? cs : ""); + cs = remmina_plugin_service->file_get_string(remminafile, "execpath"); + fprintf(fp, "shell working directory:s:%s\r\n", cs ? cs : ""); + cs = remmina_plugin_service->file_get_string(remminafile, "gateway_server"); + fprintf(fp, "gatewayhostname:s:%s\r\n", cs ? cs : ""); + fprintf(fp, "gatewayusagemethod:i:4\r\n"); + fprintf(fp, "gatewaycredentialssource:i:4\r\n"); + fprintf(fp, "gatewayprofileusagemethod:i:0\r\n"); + fprintf(fp, "precommand:s:\r\n"); + fprintf(fp, "promptcredentialonce:i:1\r\n"); + fprintf(fp, "drivestoredirect:s:\r\n"); + + return TRUE; +} + +gboolean remmina_rdp_file_export(RemminaFilePlugin *plugin, RemminaFile *remminafile, const gchar *to_file) +{ + TRACE_CALL(__func__); + FILE *fp; + gboolean ret; + + fp = g_fopen(to_file, "w+"); + + if (fp == NULL) { + g_print("Failed to export %s\n", to_file); + return FALSE; + } + + ret = remmina_rdp_file_export_channel(remminafile, fp); + fclose(fp); + + return ret; +} diff --git a/plugins/rdp/rdp_file.h b/plugins/rdp/rdp_file.h new file mode 100644 index 0000000..0196293 --- /dev/null +++ b/plugins/rdp/rdp_file.h @@ -0,0 +1,48 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2010-2011 Vic Lee + * Copyright (C) 2017-2023 Antenore Gatta, Giovanni Panozzo + * + * 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 <glib.h> +#include "common/remmina_plugin.h" + +G_BEGIN_DECLS + +gboolean remmina_rdp_file_import_test(RemminaFilePlugin *plugin, const gchar *from_file); +RemminaFile *remmina_rdp_file_import(RemminaFilePlugin *plugin, const gchar *from_file); +gboolean remmina_rdp_file_export_test(RemminaFilePlugin *plugin, RemminaFile *remminafile); +gboolean remmina_rdp_file_export(RemminaFilePlugin *plugin, RemminaFile *remminafile, const gchar *to_file); + +G_END_DECLS diff --git a/plugins/rdp/rdp_graphics.c b/plugins/rdp/rdp_graphics.c new file mode 100644 index 0000000..b97f30e --- /dev/null +++ b/plugins/rdp/rdp_graphics.c @@ -0,0 +1,163 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2010 Jay Sorg + * Copyright (C) 2010-2011 Vic Lee + * Copyright (C) 2011 Marc-Andre Moreau <marcandre.moreau@gmail.com> + * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * 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 "rdp_plugin.h" +#include "rdp_event.h" +#include "rdp_graphics.h" + +#include <freerdp/codec/color.h> +#include <freerdp/codec/bitmap.h> +#include <winpr/memory.h> + +//#define RF_BITMAP +//#define RF_GLYPH + +/* Pointer Class */ +#if FREERDP_VERSION_MAJOR < 3 +#define CONST_ARG const +#else +#define CONST_ARG +#endif + +static BOOL rf_Pointer_New(rdpContext* context, rdpPointer* pointer) +{ + TRACE_CALL(__func__); + RemminaPluginRdpUiObject* ui; + rfContext* rfi = (rfContext*)context; + + if (pointer->xorMaskData != 0) { + ui = g_new0(RemminaPluginRdpUiObject, 1); + ui->type = REMMINA_RDP_UI_CURSOR; + ui->cursor.context = context; + ui->cursor.pointer = (rfPointer*)pointer; + ui->cursor.type = REMMINA_RDP_POINTER_NEW; + return remmina_rdp_event_queue_ui_sync_retint(rfi->protocol_widget, ui) ? TRUE : FALSE; + } + return FALSE; +} + +static void rf_Pointer_Free(rdpContext* context, rdpPointer* pointer) +{ + TRACE_CALL(__func__); + RemminaPluginRdpUiObject* ui; + rfContext* rfi = (rfContext*)context; + + if (G_IS_OBJECT(((rfPointer*)pointer)->cursor)) + { + ui = g_new0(RemminaPluginRdpUiObject, 1); + ui->type = REMMINA_RDP_UI_CURSOR; + ui->cursor.context = context; + ui->cursor.pointer = (rfPointer*)pointer; + ui->cursor.type = REMMINA_RDP_POINTER_FREE; + remmina_rdp_event_queue_ui_sync_retint(rfi->protocol_widget, ui); + } +} + +static BOOL rf_Pointer_Set(rdpContext* context, CONST_ARG rdpPointer* pointer) +{ + TRACE_CALL(__func__); + RemminaPluginRdpUiObject* ui; + rfContext* rfi = (rfContext*)context; + + ui = g_new0(RemminaPluginRdpUiObject, 1); + ui->type = REMMINA_RDP_UI_CURSOR; + ui->cursor.pointer = (rfPointer*)pointer; + ui->cursor.type = REMMINA_RDP_POINTER_SET; + + return remmina_rdp_event_queue_ui_sync_retint(rfi->protocol_widget, ui) ? TRUE : FALSE; + +} + +static BOOL rf_Pointer_SetNull(rdpContext* context) +{ + TRACE_CALL(__func__); + RemminaPluginRdpUiObject* ui; + rfContext* rfi = (rfContext*)context; + + ui = g_new0(RemminaPluginRdpUiObject, 1); + ui->type = REMMINA_RDP_UI_CURSOR; + ui->cursor.type = REMMINA_RDP_POINTER_NULL; + + return remmina_rdp_event_queue_ui_sync_retint(rfi->protocol_widget, ui) ? TRUE : FALSE; +} + +static BOOL rf_Pointer_SetDefault(rdpContext* context) +{ + TRACE_CALL(__func__); + RemminaPluginRdpUiObject* ui; + rfContext* rfi = (rfContext*)context; + + ui = g_new0(RemminaPluginRdpUiObject, 1); + ui->type = REMMINA_RDP_UI_CURSOR; + ui->cursor.type = REMMINA_RDP_POINTER_DEFAULT; + + return remmina_rdp_event_queue_ui_sync_retint(rfi->protocol_widget, ui) ? TRUE : FALSE; +} + +static BOOL rf_Pointer_SetPosition(rdpContext* context, UINT32 x, UINT32 y) +{ + TRACE_CALL(__func__); + RemminaPluginRdpUiObject* ui; + rfContext* rfi = (rfContext*)context; + ui = g_new0(RemminaPluginRdpUiObject, 1); + ui->type = REMMINA_RDP_UI_CURSOR; + ui->cursor.type = REMMINA_RDP_POINTER_SETPOS; + ui->pos.x = x; + ui->pos.y = y; + + return remmina_rdp_event_queue_ui_sync_retint(rfi->protocol_widget, ui) ? TRUE : FALSE; +} + +/* Graphics Module */ + +void rf_register_graphics(rdpGraphics* graphics) +{ + TRACE_CALL(__func__); + rdpPointer pointer={0}; + + pointer.size = sizeof(rfPointer); + + pointer.New = rf_Pointer_New; + pointer.Free = rf_Pointer_Free; + pointer.Set = rf_Pointer_Set; + pointer.SetNull = rf_Pointer_SetNull; + pointer.SetDefault = rf_Pointer_SetDefault; + pointer.SetPosition = rf_Pointer_SetPosition; + + graphics_register_pointer(graphics, &pointer); +} diff --git a/plugins/rdp/rdp_graphics.h b/plugins/rdp/rdp_graphics.h new file mode 100644 index 0000000..1c290f9 --- /dev/null +++ b/plugins/rdp/rdp_graphics.h @@ -0,0 +1,40 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2010-2011 Vic Lee + * Copyright (C) 2017-2023 Antenore Gatta, Giovanni Panozzo + * + * 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 "rdp_plugin.h" + +void rf_register_graphics(rdpGraphics *graphics); diff --git a/plugins/rdp/rdp_monitor.c b/plugins/rdp/rdp_monitor.c new file mode 100644 index 0000000..4aaf413 --- /dev/null +++ b/plugins/rdp/rdp_monitor.c @@ -0,0 +1,231 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2016-2020 Antenore Gatta, Giovanni Panozzo + * + * 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 "rdp_plugin.h" +#include "rdp_monitor.h" + +/** @ToDo Utility functions should be moved somewhere else */ +gint remmina_rdp_utils_strpos(const gchar *haystack, const gchar *needle) +{ + TRACE_CALL(__func__); + const gchar *sub; + + if (!*needle) + return -1; + + sub = strstr(haystack, needle); + if (!sub) + return -1; + + return sub - haystack; +} + +/* https://github.com/adlocode/xfwm4/blob/1d21be9ffc0fa1cea91905a07d1446c5227745f4/common/xfwm-common.c */ + + +/** + * Set the MonitorIDs, the maxwidth and maxheight + * + * - number of monitors + * - Geometry of each + * - Choosen by the user + * - Primary monitor + * - Otherwise use current monitor as the origin + * + * The origin must be 0,0 + */ +void remmina_rdp_monitor_get (rfContext *rfi, gchar **monitorids, guint32 *maxwidth, guint32 *maxheight) +{ + TRACE_CALL(__func__); + + GdkDisplay *display; + GdkMonitor *monitor; + gboolean has_custom_monitors = FALSE; + + gboolean primary_found = FALSE; + + gint n_monitors; + gint scale; + gint index = 0; + gint count = 0; + + static gchar buffer[256]; + gint buffer_offset = 0; + + GdkRectangle geometry = { 0, 0, 0, 0 }; + GdkRectangle tempgeom = { 0, 0, 0, 0 }; + GdkRectangle destgeom = { 0, 0, 0, 0 }; + rdpSettings* settings; + if (!rfi || !rfi->clientContext.context.settings) + return; + + settings = rfi->clientContext.context.settings; + + *maxwidth = freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth); + *maxheight = freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight); + + display = gdk_display_get_default (); + n_monitors = gdk_display_get_n_monitors(display); + + /* Get monitor at windows curently in use */ + //w = gtk_widget_get_window(rfi->drawing_area); + + //current_monitor = gdk_display_get_monitor_at_window (display, w); + + /* we got monitorids as options */ + if (*monitorids) + has_custom_monitors = TRUE; + + rdpMonitor* base = (rdpMonitor *)freerdp_settings_get_pointer(settings, FreeRDP_MonitorDefArray); + for (gint i = 0; i < n_monitors; ++i) { + rdpMonitor* current; + if (has_custom_monitors) { + REMMINA_PLUGIN_DEBUG("We have custom monitors"); + gchar itoc[11]; + snprintf(itoc, sizeof(itoc), "%d", i); + if (remmina_rdp_utils_strpos(*monitorids, itoc) < 0 ) { + REMMINA_PLUGIN_DEBUG("Monitor n %d it's out of the provided list", i); + index += 1; + continue; + } + } + + monitor = gdk_display_get_monitor(display, i); + if (monitor == NULL) { + REMMINA_PLUGIN_DEBUG("Monitor n %d does not exist or is not active", i); + index +=1; + continue; + } + + monitor = gdk_display_get_monitor(display, index); + current = &base[index]; + REMMINA_PLUGIN_DEBUG("Monitor n %d", index); + /* If the desktop env in use doesn't have the working area concept + * gdk_monitor_get_workarea will return the monitor geometry*/ + //gdk_monitor_get_workarea (monitor, &geometry); + gdk_monitor_get_geometry (monitor, &geometry); + current->x = geometry.x; + REMMINA_PLUGIN_DEBUG("Monitor n %d x: %d", index, geometry.x); + current->y = geometry.y; + REMMINA_PLUGIN_DEBUG("Monitor n %d y: %d", index, geometry.y); + /* geometry contain the application geometry, to obtain the real one + * we must multiply by the scale factor */ + scale = gdk_monitor_get_scale_factor (monitor); + REMMINA_PLUGIN_DEBUG("Monitor n %d scale: %d", index, scale); + geometry.x *= scale; + geometry.y *= scale; + geometry.width *= scale; + geometry.height *= scale; + REMMINA_PLUGIN_DEBUG("Monitor n %d width: %d", index, geometry.width); + REMMINA_PLUGIN_DEBUG("Monitor n %d height: %d", index, geometry.height); + current->width = geometry.width; + current->height = geometry.height; + current->attributes.physicalHeight = gdk_monitor_get_height_mm (monitor); + REMMINA_PLUGIN_DEBUG("Monitor n %d physical height: %d", i, current->attributes.physicalHeight); + current->attributes.physicalWidth = gdk_monitor_get_width_mm (monitor); + REMMINA_PLUGIN_DEBUG("Monitor n %d physical width: %d", i, current->attributes.physicalWidth); + current->orig_screen = index; + if (!primary_found) { + freerdp_settings_set_uint32(settings, FreeRDP_MonitorLocalShiftX, current->x); + freerdp_settings_set_uint32(settings, FreeRDP_MonitorLocalShiftY, current->y); + } + if (gdk_monitor_is_primary(monitor)) { + REMMINA_PLUGIN_DEBUG ("Primary monitor found with id: %d", index); + current->is_primary = TRUE; + primary_found = TRUE; + if (current->x != 0 || current->y != 0) + { + REMMINA_PLUGIN_DEBUG ("Primary monitor not at 0,0 coordinates: %d", index); + freerdp_settings_set_uint32(settings, FreeRDP_MonitorLocalShiftX, current->x); + freerdp_settings_set_uint32(settings, FreeRDP_MonitorLocalShiftY, current->y); + } + } else { + if (!primary_found && current->x == 0 && + current->y == 0) + { + REMMINA_PLUGIN_DEBUG ("Monitor %d has 0,0 coordinates", index); + current->is_primary = TRUE; + freerdp_settings_set_uint32(settings, FreeRDP_MonitorLocalShiftX, current->x); + freerdp_settings_set_uint32(settings, FreeRDP_MonitorLocalShiftY, current->y); + primary_found = TRUE; + REMMINA_PLUGIN_DEBUG ("Primary monitor set to id: %d", index); + } + } + REMMINA_PLUGIN_DEBUG ("Local X Shift: %d", freerdp_settings_get_uint32(settings, FreeRDP_MonitorLocalShiftX)); + REMMINA_PLUGIN_DEBUG ("Local Y Shift: %d", freerdp_settings_get_uint32(settings, FreeRDP_MonitorLocalShiftY)); + //current->x = + //current->x - freerdp_settings_get_uint32(settings, FreeRDP_MonitorLocalShiftX); + //REMMINA_PLUGIN_DEBUG("Monitor n %d calculated x: %d", index, current->x); + //current->y = + //current->y - freerdp_settings_get_uint32(settings, FreeRDP_MonitorLocalShiftY); + //REMMINA_PLUGIN_DEBUG("Monitor n %d calculated y: %d", index, current->y); + + if (buffer_offset == 0) + buffer_offset = g_sprintf(buffer + buffer_offset, "%d", i); + else + buffer_offset = g_sprintf(buffer + buffer_offset, ",%d", i); + REMMINA_PLUGIN_DEBUG("Monitor IDs buffer: %s", buffer); + gdk_rectangle_union(&tempgeom, &geometry, &destgeom); + memcpy(&tempgeom, &destgeom, sizeof tempgeom); + count++; + index++; + + } + freerdp_settings_set_uint32(settings, FreeRDP_MonitorCount, index); + /* Subtract monitor shift from monitor variables for server-side use. + * We maintain monitor shift value as Window requires the primary monitor to have a + * coordinate of 0,0 In some X configurations, no monitor may have a coordinate of 0,0. This + * can also be happen if the user requests specific monitors from the command-line as well. + * So, we make sure to translate our primary monitor's upper-left corner to 0,0 on the + * server. + */ + for (gint i = 0; i < freerdp_settings_get_uint32(settings, FreeRDP_MonitorCount); i++) + { + rdpMonitor* current = &base[i]; + current->x = + current->x - freerdp_settings_get_uint32(settings, FreeRDP_MonitorLocalShiftX); + REMMINA_PLUGIN_DEBUG("Monitor n %d calculated x: %d", i, current->x); + current->y = + current->y - freerdp_settings_get_uint32(settings, FreeRDP_MonitorLocalShiftY); + REMMINA_PLUGIN_DEBUG("Monitor n %d calculated y: %d", i, current->y); + } + + REMMINA_PLUGIN_DEBUG("%d monitors on %d have been configured", freerdp_settings_get_uint32(settings, FreeRDP_MonitorCount), count); + *maxwidth = destgeom.width; + *maxheight = destgeom.height; + REMMINA_PLUGIN_DEBUG("maxw and maxh: %ux%u", *maxwidth, *maxheight); + if (n_monitors > 1) + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_SupportMonitorLayoutPdu, TRUE); + *monitorids = g_strdup(buffer); +} diff --git a/plugins/rdp/rdp_monitor.h b/plugins/rdp/rdp_monitor.h new file mode 100644 index 0000000..1c8f08d --- /dev/null +++ b/plugins/rdp/rdp_monitor.h @@ -0,0 +1,45 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2016-2020 Antenore Gatta, Giovanni Panozzo + * + * 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 <freerdp/freerdp.h> +#include "rdp_plugin.h" + +G_BEGIN_DECLS + +void remmina_rdp_monitor_get (rfContext *rfi, gchar **monitorids, guint32 *maxwidth, guint32 *maxheight); + +G_END_DECLS diff --git a/plugins/rdp/rdp_plugin.c b/plugins/rdp/rdp_plugin.c new file mode 100644 index 0000000..68ea505 --- /dev/null +++ b/plugins/rdp/rdp_plugin.c @@ -0,0 +1,3363 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2010-2011 Vic Lee + * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo + * Copyright (C) 2016-2022 Antenore Gatta, Giovanni Panozzo + * Copyright (C) 2022-2023 Antenore Gatta, Giovanni Panozzo, Hiroyuki Tanaka + * + * 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. + * + */ + + +#define _GNU_SOURCE + +#include "remmina/plugin.h" +#include "rdp_plugin.h" +#include "rdp_event.h" +#include "rdp_graphics.h" +#include "rdp_file.h" +#include "rdp_settings.h" +#include "rdp_cliprdr.h" +#include "rdp_monitor.h" +#include "rdp_channels.h" + +#include <errno.h> +#include <pthread.h> +#include <time.h> +#include <sys/time.h> +#ifdef GDK_WINDOWING_X11 +#include <cairo/cairo-xlib.h> +#else +#include <cairo/cairo.h> +#endif +#include <ctype.h> +#include <freerdp/addin.h> +#include <freerdp/assistance.h> +#if FREERDP_VERSION_MAJOR >= 3 +#include <freerdp/channels/rdp2tcp.h> +#else +#define RDP2TCP_DVC_CHANNEL_NAME "rdp2tcp" +#endif +#include <freerdp/client/channels.h> +#include <freerdp/client/cliprdr.h> +#include <freerdp/client/cmdline.h> +#include <freerdp/constants.h> +#include <freerdp/error.h> +#include <freerdp/event.h> +#include <freerdp/freerdp.h> +#include <freerdp/settings.h> +#include <winpr/cmdline.h> +#include <winpr/memory.h> + +#ifdef HAVE_CUPS +#include <cups/cups.h> +#endif + +#include <unistd.h> +#include <string.h> + +#ifdef GDK_WINDOWING_X11 +#include <X11/Xlib.h> +#include <X11/XKBlib.h> +#include <gdk/gdkx.h> +#elif defined(GDK_WINDOWING_WAYLAND) +#include <gdk/gdkwayland.h> +#endif + +#if defined(__FreeBSD__) +#include <pthread_np.h> +#endif + +#include <freerdp/locale/keyboard.h> + +#define REMMINA_RDP_FEATURE_TOOL_REFRESH 1 +#define REMMINA_RDP_FEATURE_SCALE 2 +#define REMMINA_RDP_FEATURE_UNFOCUS 3 +#define REMMINA_RDP_FEATURE_TOOL_SENDCTRLALTDEL 4 +#define REMMINA_RDP_FEATURE_DYNRESUPDATE 5 +#define REMMINA_RDP_FEATURE_MULTIMON 6 +#define REMMINA_RDP_FEATURE_VIEWONLY 7 + +#define REMMINA_CONNECTION_TYPE_NONE 0 + +#if FREERDP_VERSION_MAJOR >= 3 + #define CLPARAM const char +#else + #define CLPARAM char +#endif + +#if FREERDP_VERSION_MAJOR < 3 +static HANDLE freerdp_abort_event(rdpContext* context) { + WINPR_ASSERT(context); + return context->abortEvent; +} + +static BOOL freerdp_settings_set_pointer_len(rdpSettings* settings, size_t id, const void* data, size_t len) +{ + switch(id) { + case FreeRDP_LoadBalanceInfo: + free(settings->LoadBalanceInfo); + settings->LoadBalanceInfo = _strdup(data); + settings->LoadBalanceInfoLength = len; + return TRUE; + default: + return FALSE; + } +} + +static void freerdp_abort_connect_context(rdpContext* context) { + WINPR_ASSERT(context); + freerdp_abort_connect(context->instance); +} +#endif + +RemminaPluginService *remmina_plugin_service = NULL; + +static BOOL gfx_h264_available = FALSE; +// keep track of last interaction time for keep alive +static time_t last_time; + +/* Compatibility: these functions have been introduced with https://github.com/FreeRDP/FreeRDP/commit/8c5d96784d + * and are missing on older FreeRDP, so we add them here. + * They should be removed from here after all distributed versions of FreeRDP (libwinpr) will have + * CommandLineParseCommaSeparatedValuesEx() onboard. + * + * (C) Copyright goes to the FreeRDP authors. + */ +static CLPARAM **remmina_rdp_CommandLineParseCommaSeparatedValuesEx(const char *name, const char *list, size_t *count) +{ + TRACE_CALL(__func__); +#if FREERDP_CHECK_VERSION(2, 0, 0) + return (CLPARAM **)CommandLineParseCommaSeparatedValuesEx(name, list, count); +#else + char **p; + char *str; + size_t nArgs; + size_t index; + size_t nCommas; + size_t prefix, len; + + nCommas = 0; + + if (count == NULL) + return NULL; + + *count = 0; + + if (!list) { + if (name) { + size_t len = strlen(name); + p = (char **)calloc(2UL + len, sizeof(char *)); + + if (p) { + char *dst = (char *)&p[1]; + p[0] = dst; + sprintf_s(dst, len + 1, "%s", name); + *count = 1; + return p; + } + } + + return NULL; + } + + { + const char *it = list; + + while ((it = strchr(it, ',')) != NULL) { + it++; + nCommas++; + } + } + + nArgs = nCommas + 1; + + if (name) + nArgs++; + + prefix = (nArgs + 1UL) * sizeof(char *); + len = strlen(list); + p = (char **)calloc(len + prefix + 1, sizeof(char *)); + + if (!p) + return NULL; + + str = &((char *)p)[prefix]; + memcpy(str, list, len); + + if (name) + p[0] = (char *)name; + + for (index = name ? 1 : 0; index < nArgs; index++) { + char *comma = strchr(str, ','); + p[index] = str; + + if (comma) { + str = comma + 1; + *comma = '\0'; + } + } + + *count = nArgs; + return p; +#endif +} + +static CLPARAM **remmina_rdp_CommandLineParseCommaSeparatedValues(const char *list, size_t *count) +{ + TRACE_CALL(__func__); + return remmina_rdp_CommandLineParseCommaSeparatedValuesEx(NULL, list, count); +} + +/* + * End of CommandLineParseCommaSeparatedValuesEx() compatibility and copyright + */ +static BOOL rf_process_event_queue(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + UINT16 flags; + rdpInput *input; + rfContext *rfi = GET_PLUGIN_DATA(gp); + RemminaPluginRdpEvent *event; + DISPLAY_CONTROL_MONITOR_LAYOUT *dcml; + CLIPRDR_FORMAT_DATA_RESPONSE response = { 0 }; + RemminaFile *remminafile; + + if (rfi->event_queue == NULL) + return true; + + input = rfi->clientContext.context.input; + + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + + while ((event = (RemminaPluginRdpEvent *)g_async_queue_try_pop(rfi->event_queue)) != NULL) { + time(&last_time); //update last user interaction time + switch (event->type) { + case REMMINA_RDP_EVENT_TYPE_SCANCODE: + + if (event->key_event.extended1){ + flags = KBD_FLAGS_EXTENDED1; + } + else{ + flags = event->key_event.extended ? KBD_FLAGS_EXTENDED : 0; + } + flags |= event->key_event.up ? KBD_FLAGS_RELEASE : KBD_FLAGS_DOWN; + input->KeyboardEvent(input, flags, event->key_event.key_code); + break; + + case REMMINA_RDP_EVENT_TYPE_SCANCODE_UNICODE: + /* + * TS_UNICODE_KEYBOARD_EVENT RDP message, see https://msdn.microsoft.com/en-us/library/cc240585.aspx + */ + flags = event->key_event.up ? KBD_FLAGS_RELEASE : KBD_FLAGS_DOWN; + input->UnicodeKeyboardEvent(input, flags, event->key_event.unicode_code); + break; + + case REMMINA_RDP_EVENT_TYPE_MOUSE: + if (event->mouse_event.extended) + input->ExtendedMouseEvent(input, event->mouse_event.flags, + event->mouse_event.x, event->mouse_event.y); + else + input->MouseEvent(input, event->mouse_event.flags, + event->mouse_event.x, event->mouse_event.y); + break; + + case REMMINA_RDP_EVENT_TYPE_CLIPBOARD_SEND_CLIENT_FORMAT_LIST: + rfi->clipboard.context->ClientFormatList(rfi->clipboard.context, event->clipboard_formatlist.pFormatList); + free(event->clipboard_formatlist.pFormatList); + break; + + case REMMINA_RDP_EVENT_TYPE_CLIPBOARD_SEND_CLIENT_FORMAT_DATA_RESPONSE: + { + UINT32 msgFlags = (event->clipboard_formatdataresponse.data) ? CB_RESPONSE_OK : CB_RESPONSE_FAIL; +#if FREERDP_VERSION_MAJOR >= 3 + response.common.msgFlags = msgFlags; + response.common.dataLen = event->clipboard_formatdataresponse.size; +#else + response.msgFlags = msgFlags; + response.dataLen = event->clipboard_formatdataresponse.size; +#endif + response.requestedFormatData = event->clipboard_formatdataresponse.data; + rfi->clipboard.context->ClientFormatDataResponse(rfi->clipboard.context, &response); + } + break; + + case REMMINA_RDP_EVENT_TYPE_CLIPBOARD_SEND_CLIENT_FORMAT_DATA_REQUEST: + REMMINA_PLUGIN_DEBUG("Sending client FormatDataRequest to server"); + gettimeofday(&(rfi->clipboard.clientformatdatarequest_tv), NULL); + rfi->clipboard.context->ClientFormatDataRequest(rfi->clipboard.context, event->clipboard_formatdatarequest.pFormatDataRequest); + free(event->clipboard_formatdatarequest.pFormatDataRequest); + break; + + case REMMINA_RDP_EVENT_TYPE_SEND_MONITOR_LAYOUT: + if (remmina_plugin_service->file_get_int(remminafile, "multimon", FALSE)) { + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_UseMultimon, TRUE); + /* TODO Add an option for this */ + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_ForceMultimon, TRUE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_Fullscreen, TRUE); + /* got some crashes with g_malloc0, to be investigated */ + dcml = calloc(freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_MonitorCount), sizeof(DISPLAY_CONTROL_MONITOR_LAYOUT)); + REMMINA_PLUGIN_DEBUG("REMMINA_RDP_EVENT_TYPE_SEND_MONITOR_LAYOUT:"); + if (!dcml) + break; + + const rdpMonitor *base = freerdp_settings_get_pointer(rfi->clientContext.context.settings, FreeRDP_MonitorDefArray); + for (gint i = 0; i < freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_MonitorCount); ++i) { + const rdpMonitor *current = &base[i]; + REMMINA_PLUGIN_DEBUG("Sending display layout for monitor n° %d", i); + dcml[i].Flags = (current->is_primary ? DISPLAY_CONTROL_MONITOR_PRIMARY : 0); + REMMINA_PLUGIN_DEBUG("Monitor %d is primary: %d", i, dcml[i].Flags); + dcml[i].Left = current->x; + REMMINA_PLUGIN_DEBUG("Monitor %d x: %d", i, dcml[i].Left); + dcml[i].Top = current->y; + REMMINA_PLUGIN_DEBUG("Monitor %d y: %d", i, dcml[i].Top); + dcml[i].Width = current->width; + REMMINA_PLUGIN_DEBUG("Monitor %d width: %d", i, dcml[i].Width); + dcml[i].Height = current->height; + REMMINA_PLUGIN_DEBUG("Monitor %d height: %d", i, dcml[i].Height); + dcml[i].PhysicalWidth = current->attributes.physicalWidth; + REMMINA_PLUGIN_DEBUG("Monitor %d physical width: %d", i, dcml[i].PhysicalWidth); + dcml[i].PhysicalHeight = current->attributes.physicalHeight; + REMMINA_PLUGIN_DEBUG("Monitor %d physical height: %d", i, dcml[i].PhysicalHeight); + if (current->attributes.orientation) + dcml[i].Orientation = current->attributes.orientation; + else + dcml[i].Orientation = event->monitor_layout.desktopOrientation; + REMMINA_PLUGIN_DEBUG("Monitor %d orientation: %d", i, dcml[i].Orientation); + dcml[i].DesktopScaleFactor = event->monitor_layout.desktopScaleFactor; + dcml[i].DeviceScaleFactor = event->monitor_layout.deviceScaleFactor; + } + rfi->dispcontext->SendMonitorLayout(rfi->dispcontext, freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_MonitorCount), dcml); + g_free(dcml); + } else { + dcml = g_malloc0(sizeof(DISPLAY_CONTROL_MONITOR_LAYOUT)); + if (dcml) { + dcml->Flags = DISPLAY_CONTROL_MONITOR_PRIMARY; + dcml->Width = event->monitor_layout.width; + dcml->Height = event->monitor_layout.height; + dcml->Orientation = event->monitor_layout.desktopOrientation; + dcml->DesktopScaleFactor = event->monitor_layout.desktopScaleFactor; + dcml->DeviceScaleFactor = event->monitor_layout.deviceScaleFactor; + rfi->dispcontext->SendMonitorLayout(rfi->dispcontext, 1, dcml); + g_free(dcml); \ + } + } + break; + case REMMINA_RDP_EVENT_DISCONNECT: + /* Disconnect requested via GUI (i.e: tab destroy/close) */ + freerdp_abort_connect_context(&rfi->clientContext.context); + break; + } + + g_free(event); + } + + return true; +} + +static gboolean remmina_rdp_tunnel_init(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + + /* Opens the optional SSH tunnel if needed. + * Used also when reopening the same tunnel for a FreeRDP reconnect. + * Returns TRUE if all OK, and setups correct rfi->Settings values + * with connection and certificate parameters */ + + gchar *hostport; + gchar *s; + gchar *host; + gchar *cert_host; + gint cert_port; + gint port; + + rfContext *rfi = GET_PLUGIN_DATA(gp); + + REMMINA_PLUGIN_DEBUG("Tunnel init"); + hostport = remmina_plugin_service->protocol_plugin_start_direct_tunnel(gp, 3389, FALSE); + if (hostport == NULL) + return FALSE; + + remmina_plugin_service->get_server_port(hostport, 3389, &host, &port); + + if (host[0] == 0) + return FALSE; + + REMMINA_PLUGIN_DEBUG("protocol_plugin_start_direct_tunnel() returned %s", hostport); + + cert_host = host; + cert_port = port; + + if (!rfi->is_reconnecting) { + /* settings->CertificateName and settings->ServerHostname is created + * only on 1st connect, not on reconnections */ + + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname, host); + + if (cert_port == 3389) { + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_CertificateName, cert_host); + } else { + s = g_strdup_printf("%s:%d", cert_host, cert_port); + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_CertificateName, s); + g_free(s); + } + } + + REMMINA_PLUGIN_DEBUG("Tunnel has been optionally initialized. Now connecting to %s:%d", host, port); + + if (cert_host != host) g_free(cert_host); + g_free(host); + g_free(hostport); + + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_ServerPort, port); + + return TRUE; +} + +static BOOL rf_auto_reconnect(rfContext *rfi) +{ + TRACE_CALL(__func__); + rdpSettings *settings = rfi->clientContext.context.settings; + RemminaPluginRdpUiObject *ui; + time_t treconn; + gchar *cval; + gint maxattempts; + + RemminaProtocolWidget *gp = rfi->protocol_widget; + RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + + rfi->is_reconnecting = TRUE; + rfi->stop_reconnecting_requested = FALSE; + + /* Get the value set in FreeRDP_AutoReconnectMaxRetries (20) */ + maxattempts = freerdp_settings_get_uint32(settings, FreeRDP_AutoReconnectMaxRetries); + REMMINA_PLUGIN_DEBUG("maxattempts from default: %d", maxattempts); + /* Get the value from the global preferences if any */ + if ((cval = remmina_plugin_service->pref_get_value("rdp_reconnect_attempts")) != NULL) + maxattempts = atoi(cval); + REMMINA_PLUGIN_DEBUG("maxattempts from general preferences: %d", maxattempts); + /* Get the value from the profile if any, otherwise uses the value of maxattempts */ + maxattempts = remmina_plugin_service->file_get_int(remminafile, "rdp_reconnect_attempts", maxattempts); + REMMINA_PLUGIN_DEBUG("maxattempts from general plugin: %d", maxattempts); + /* If maxattemps is <= 0, we get the value from FreeRDP_AutoReconnectMaxRetries (20) */ + if (maxattempts <= 0) + maxattempts = freerdp_settings_get_uint32(settings, FreeRDP_AutoReconnectMaxRetries); + freerdp_settings_set_uint32(settings, FreeRDP_AutoReconnectMaxRetries, maxattempts); + REMMINA_PLUGIN_DEBUG("maxattempts set to: %d", maxattempts); + + rfi->reconnect_maxattempts = maxattempts; + rfi->reconnect_nattempt = 0; + + /* Only auto reconnect on network disconnects. */ + switch (freerdp_error_info(rfi->clientContext.context.instance)) { + case ERRINFO_GRAPHICS_SUBSYSTEM_FAILED: + /* Disconnected by server hitting a bug or resource limit */ + break; + case ERRINFO_SUCCESS: + /* A network disconnect was detected */ + break; + default: + rfi->is_reconnecting = FALSE; + return FALSE; + } + + if (!freerdp_settings_get_bool(settings, FreeRDP_AutoReconnectionEnabled)) { + /* No auto-reconnect - just quit */ + rfi->is_reconnecting = FALSE; + return FALSE; + } + + /* A network disconnect was detected and we should try to reconnect */ + REMMINA_PLUGIN_DEBUG("[%s] network disconnection detected, initiating reconnection attempt", + freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname)); + + ui = g_new0(RemminaPluginRdpUiObject, 1); + ui->type = REMMINA_RDP_UI_RECONNECT_PROGRESS; + remmina_rdp_event_queue_ui_async(rfi->protocol_widget, ui); + + /* Sleep half a second to allow: + * - processing of the UI event we just pushed on the queue + * - better network conditions + * Remember: We hare on a thread, so the main gui won’t lock */ + usleep(500000); + + /* Perform an auto-reconnect. */ + while (TRUE) { + /* Quit retrying if max retries has been exceeded */ + if (rfi->reconnect_nattempt++ >= rfi->reconnect_maxattempts) { + REMMINA_PLUGIN_DEBUG("[%s] maximum number of reconnection attempts exceeded.", + freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname)); + break; + } + + if (rfi->stop_reconnecting_requested) { + REMMINA_PLUGIN_DEBUG("[%s] reconnect request loop interrupted by user.", + freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname)); + break; + } + + /* Attempt the next reconnect */ + REMMINA_PLUGIN_DEBUG("[%s] reconnection, attempt #%d of %d", + freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname), rfi->reconnect_nattempt, rfi->reconnect_maxattempts); + + ui = g_new0(RemminaPluginRdpUiObject, 1); + ui->type = REMMINA_RDP_UI_RECONNECT_PROGRESS; + remmina_rdp_event_queue_ui_async(rfi->protocol_widget, ui); + + treconn = time(NULL); + + /* Reconnect the SSH tunnel, if needed */ + if (!remmina_rdp_tunnel_init(rfi->protocol_widget)) { + REMMINA_PLUGIN_DEBUG("[%s] unable to recreate tunnel with remmina_rdp_tunnel_init.", + freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname)); + } else { + if (freerdp_reconnect(rfi->clientContext.context.instance)) { + /* Reconnection is successful */ + REMMINA_PLUGIN_DEBUG("[%s] reconnected.", freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname)); + rfi->is_reconnecting = FALSE; + return TRUE; + } + } + + /* Wait until 5 secs have elapsed from last reconnect attempt, while checking for rfi->stop_reconnecting_requested */ + while (time(NULL) - treconn < 5) { + if (rfi->stop_reconnecting_requested) + break; + usleep(200000); // 200ms sleep + } + } + + rfi->is_reconnecting = FALSE; + return FALSE; +} + +static BOOL rf_begin_paint(rdpContext *context) +{ + TRACE_CALL(__func__); + rdpGdi *gdi; + + if (!context) + return FALSE; + + gdi = context->gdi; + if (!gdi || !gdi->primary || !gdi->primary->hdc || !gdi->primary->hdc->hwnd) + return FALSE; + + return TRUE; +} + +static BOOL rf_end_paint(rdpContext *context) +{ + TRACE_CALL(__func__); + rdpGdi *gdi; + rfContext *rfi; + RemminaPluginRdpUiObject *ui; + int i, ninvalid; + region *reg; + HGDI_RGN cinvalid; + + gdi = context->gdi; + rfi = (rfContext *)context; + + if (gdi == NULL || gdi->primary == NULL || gdi->primary->hdc == NULL || gdi->primary->hdc->hwnd == NULL) + return TRUE; + + if (gdi->primary->hdc->hwnd->invalid->null) + return TRUE; + + if (gdi->primary->hdc->hwnd->ninvalid < 1) + return TRUE; + + ninvalid = gdi->primary->hdc->hwnd->ninvalid; + cinvalid = gdi->primary->hdc->hwnd->cinvalid; + reg = (region *)g_malloc(sizeof(region) * ninvalid); + for (i = 0; i < ninvalid; i++) { + reg[i].x = cinvalid[i].x; + reg[i].y = cinvalid[i].y; + reg[i].w = cinvalid[i].w; + reg[i].h = cinvalid[i].h; + } + + ui = g_new0(RemminaPluginRdpUiObject, 1); + ui->type = REMMINA_RDP_UI_UPDATE_REGIONS; + ui->reg.ninvalid = ninvalid; + ui->reg.ureg = reg; + + remmina_rdp_event_queue_ui_async(rfi->protocol_widget, ui); + + + gdi->primary->hdc->hwnd->invalid->null = TRUE; + gdi->primary->hdc->hwnd->ninvalid = 0; + + + return TRUE; +} + +static BOOL rf_desktop_resize(rdpContext *context) +{ + TRACE_CALL(__func__); + rfContext *rfi; + RemminaProtocolWidget *gp; + RemminaPluginRdpUiObject *ui; + UINT32 w, h; + + rfi = (rfContext *)context; + gp = rfi->protocol_widget; + + w = freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopWidth); + h = freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopHeight); + remmina_plugin_service->protocol_plugin_set_width(gp, w); + remmina_plugin_service->protocol_plugin_set_height(gp, h); + + ui = g_new0(RemminaPluginRdpUiObject, 1); + ui->type = REMMINA_RDP_UI_EVENT; + ui->event.type = REMMINA_RDP_UI_EVENT_DESTROY_CAIRO_SURFACE; + remmina_rdp_event_queue_ui_sync_retint(gp, ui); + + /* Tell libfreerdp to change its internal GDI bitmap width and heigt, + * this will also destroy gdi->primary_buffer, making our rfi->surface invalid */ + gdi_resize(((rdpContext *)rfi)->gdi, w, h); + + /* Call to remmina_rdp_event_update_scale(gp) on the main UI thread, + * this will recreate rfi->surface from gdi->primary_buffer */ + + ui = g_new0(RemminaPluginRdpUiObject, 1); + ui->type = REMMINA_RDP_UI_EVENT; + ui->event.type = REMMINA_RDP_UI_EVENT_UPDATE_SCALE; + remmina_rdp_event_queue_ui_sync_retint(gp, ui); + + remmina_plugin_service->protocol_plugin_desktop_resize(gp); + + return TRUE; +} + +static BOOL rf_play_sound(rdpContext *context, const PLAY_SOUND_UPDATE *play_sound) +{ + TRACE_CALL(__func__); + rfContext *rfi; + RemminaProtocolWidget *gp; + GdkDisplay *disp; + + rfi = (rfContext *)context; + gp = rfi->protocol_widget; + + disp = gtk_widget_get_display(GTK_WIDGET(gp)); + gdk_display_beep(disp); + + return TRUE; +} + +static BOOL rf_keyboard_set_indicators(rdpContext *context, UINT16 led_flags) +{ + TRACE_CALL(__func__); + rfContext *rfi; + RemminaProtocolWidget *gp; + GdkDisplay *disp; + + rfi = (rfContext *)context; + gp = rfi->protocol_widget; + disp = gtk_widget_get_display(GTK_WIDGET(gp)); + +#ifdef GDK_WINDOWING_X11 + if (GDK_IS_X11_DISPLAY(disp)) { + /* TODO: We are not on the main thread. Will X.Org complain? */ + Display *x11_display; + x11_display = gdk_x11_display_get_xdisplay(disp); + XkbLockModifiers(x11_display, XkbUseCoreKbd, + LockMask | Mod2Mask, + ((led_flags & KBD_SYNC_CAPS_LOCK) ? LockMask : 0) | + ((led_flags & KBD_SYNC_NUM_LOCK) ? Mod2Mask : 0) + ); + + /* TODO: Add support to KANA_LOCK and SCROLL_LOCK */ + } +#endif + + return TRUE; +} + +static BOOL rf_keyboard_set_ime_status(rdpContext *context, UINT16 imeId, UINT32 imeState, + UINT32 imeConvMode) +{ + TRACE_CALL(__func__); + if (!context) + return FALSE; + + /* Unimplemented, we ignore it */ + + return TRUE; +} + +static BOOL remmina_rdp_pre_connect(freerdp *instance) +{ + TRACE_CALL(__func__); + rdpChannels *channels; + rdpSettings *settings; + rdpContext *context = instance->context; + + settings = context->settings; + channels = context->channels; + freerdp_settings_set_uint32(settings, FreeRDP_OsMajorType, OSMAJORTYPE_UNIX); + freerdp_settings_set_uint32(settings, FreeRDP_OsMinorType, OSMINORTYPE_UNSPECIFIED); + freerdp_settings_set_bool(settings, FreeRDP_BitmapCacheEnabled, TRUE); + freerdp_settings_set_uint32(settings, FreeRDP_OffscreenSupportLevel, 1); + + PubSub_SubscribeChannelConnected(instance->context->pubSub, + remmina_rdp_OnChannelConnectedEventHandler); + PubSub_SubscribeChannelDisconnected(instance->context->pubSub, + remmina_rdp_OnChannelDisconnectedEventHandler); + + if (!freerdp_client_load_addins(channels, settings)) + return FALSE; + + return true; +} + +static BOOL remmina_rdp_post_connect(freerdp *instance) +{ + TRACE_CALL(__func__); + rfContext *rfi; + RemminaProtocolWidget *gp; + RemminaPluginRdpUiObject *ui; + UINT32 freerdp_local_color_format; + + rfi = (rfContext *)instance->context; + gp = rfi->protocol_widget; + rfi->postconnect_error = REMMINA_POSTCONNECT_ERROR_OK; + + rfi->attempt_interactive_authentication = FALSE; // We authenticated! + + rfi->srcBpp = freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth); + + if (freerdp_settings_get_bool(rfi->clientContext.context.settings, FreeRDP_RemoteFxCodec) == FALSE) + rfi->sw_gdi = TRUE; + + rf_register_graphics(instance->context->graphics); + + REMMINA_PLUGIN_DEBUG("bpp: %d", rfi->bpp); + switch (rfi->bpp) { + case 24: + REMMINA_PLUGIN_DEBUG("CAIRO_FORMAT_RGB24"); + freerdp_local_color_format = PIXEL_FORMAT_BGRX32; + rfi->cairo_format = CAIRO_FORMAT_RGB24; + break; + case 32: + /** Do not use alpha as it's not used with the desktop + * CAIRO_FORMAT_ARGB32 + * See https://gitlab.com/Remmina/Remmina/-/issues/2456 + */ + REMMINA_PLUGIN_DEBUG("CAIRO_FORMAT_RGB24"); + freerdp_local_color_format = PIXEL_FORMAT_BGRA32; + rfi->cairo_format = CAIRO_FORMAT_RGB24; + break; + default: + REMMINA_PLUGIN_DEBUG("CAIRO_FORMAT_RGB16_565"); + freerdp_local_color_format = PIXEL_FORMAT_RGB16; + rfi->cairo_format = CAIRO_FORMAT_RGB16_565; + break; + } + + if (!gdi_init(instance, freerdp_local_color_format)) { + rfi->postconnect_error = REMMINA_POSTCONNECT_ERROR_GDI_INIT; + return FALSE; + } + + if (instance->context->codecs->h264 == NULL && freerdp_settings_get_bool(rfi->clientContext.context.settings, FreeRDP_GfxH264)) { + gdi_free(instance); + rfi->postconnect_error = REMMINA_POSTCONNECT_ERROR_NO_H264; + return FALSE; + } + + // pointer_cache_register_callbacks(instance->update); + rdpUpdate *update = instance->context->update; + update->BeginPaint = rf_begin_paint; + update->EndPaint = rf_end_paint; + update->DesktopResize = rf_desktop_resize; + + update->PlaySound = rf_play_sound; + update->SetKeyboardIndicators = rf_keyboard_set_indicators; + update->SetKeyboardImeStatus = rf_keyboard_set_ime_status; + + remmina_rdp_clipboard_init(rfi); + rfi->connected = true; + + ui = g_new0(RemminaPluginRdpUiObject, 1); + ui->type = REMMINA_RDP_UI_CONNECTED; + remmina_rdp_event_queue_ui_async(gp, ui); + + return TRUE; +} + +#if !defined(FREERDP_VERSION_MAJOR) || (FREERDP_VERSION_MAJOR < 3) +static BOOL remmina_rdp_authenticate(freerdp *instance, char **username, char **password, char **domain) +{ + TRACE_CALL(__func__); + gchar *s_username, *s_password, *s_domain; + gint ret; + rfContext *rfi; + RemminaProtocolWidget *gp; + gboolean save; + gboolean disablepasswordstoring; + RemminaFile *remminafile; + + rfi = (rfContext *)instance->context; + gp = rfi->protocol_widget; + 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) | REMMINA_MESSAGE_PANEL_FLAG_USERNAME | REMMINA_MESSAGE_PANEL_FLAG_DOMAIN, + _("Enter RDP authentication credentials"), + remmina_plugin_service->file_get_string(remminafile, "username"), + remmina_plugin_service->file_get_string(remminafile, "password"), + remmina_plugin_service->file_get_string(remminafile, "domain"), + NULL); + if (ret == GTK_RESPONSE_OK) { + s_username = remmina_plugin_service->protocol_plugin_init_get_username(gp); + if (s_username) freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_Username, s_username); + + s_password = remmina_plugin_service->protocol_plugin_init_get_password(gp); + if (s_password) freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_Password, s_password); + + s_domain = remmina_plugin_service->protocol_plugin_init_get_domain(gp); + if (s_domain) freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_Domain, s_domain); + + remmina_plugin_service->file_set_string(remminafile, "username", s_username); + remmina_plugin_service->file_set_string(remminafile, "domain", s_domain); + + save = remmina_plugin_service->protocol_plugin_init_get_savepassword(gp); + if (save) { + // User has requested to save credentials. We put the password + // into remminafile->settings. It will be saved later, on successful connection, by + // rcw.c + remmina_plugin_service->file_set_string(remminafile, "password", s_password); + } else { + remmina_plugin_service->file_set_string(remminafile, "password", NULL); + } + + + if (s_username) g_free(s_username); + if (s_password) g_free(s_password); + if (s_domain) g_free(s_domain); + + return TRUE; + } else { + return FALSE; + } + + return TRUE; +} + +static BOOL remmina_rdp_gw_authenticate(freerdp *instance, char **username, char **password, char **domain) +{ + TRACE_CALL(__func__); + gchar *s_username, *s_password, *s_domain; + gint ret; + rfContext *rfi; + RemminaProtocolWidget *gp; + gboolean save; + gboolean disablepasswordstoring; + gboolean basecredforgw; + RemminaFile *remminafile; + + rfi = (rfContext *)instance->context; + gp = rfi->protocol_widget; + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + + if (!remmina_plugin_service->file_get_string(remminafile, "gateway_server")) + return false; + disablepasswordstoring = remmina_plugin_service->file_get_int(remminafile, "disablepasswordstoring", FALSE); + basecredforgw = remmina_plugin_service->file_get_int(remminafile, "base-cred-for-gw", FALSE); + + if (basecredforgw) { + ret = remmina_plugin_service->protocol_plugin_init_auth(gp, + (disablepasswordstoring ? 0 : REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD) | REMMINA_MESSAGE_PANEL_FLAG_USERNAME | REMMINA_MESSAGE_PANEL_FLAG_DOMAIN, + _("Enter RDP authentication credentials"), + remmina_plugin_service->file_get_string(remminafile, "username"), + remmina_plugin_service->file_get_string(remminafile, "password"), + remmina_plugin_service->file_get_string(remminafile, "domain"), + NULL); + } else { + ret = remmina_plugin_service->protocol_plugin_init_auth(gp, + (disablepasswordstoring ? 0 : REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD) | REMMINA_MESSAGE_PANEL_FLAG_USERNAME | REMMINA_MESSAGE_PANEL_FLAG_DOMAIN, + _("Enter RDP gateway authentication credentials"), + remmina_plugin_service->file_get_string(remminafile, "gateway_username"), + remmina_plugin_service->file_get_string(remminafile, "gateway_password"), + remmina_plugin_service->file_get_string(remminafile, "gateway_domain"), + NULL); + } + + + if (ret == GTK_RESPONSE_OK) { + s_username = remmina_plugin_service->protocol_plugin_init_get_username(gp); + if (s_username) freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_GatewayUsername, s_username); + + s_password = remmina_plugin_service->protocol_plugin_init_get_password(gp); + if (s_password) freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_GatewayPassword, s_password); + + s_domain = remmina_plugin_service->protocol_plugin_init_get_domain(gp); + if (s_domain) freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_GatewayDomain, s_domain); + + save = remmina_plugin_service->protocol_plugin_init_get_savepassword(gp); + + if (basecredforgw) { + remmina_plugin_service->file_set_string(remminafile, "username", s_username); + remmina_plugin_service->file_set_string(remminafile, "domain", s_domain); + if (save) + remmina_plugin_service->file_set_string(remminafile, "password", s_password); + else + remmina_plugin_service->file_set_string(remminafile, "password", NULL); + } else { + remmina_plugin_service->file_set_string(remminafile, "gateway_username", s_username); + remmina_plugin_service->file_set_string(remminafile, "gateway_domain", s_domain); + if (save) + remmina_plugin_service->file_set_string(remminafile, "gateway_password", s_password); + else + remmina_plugin_service->file_set_string(remminafile, "gateway_password", NULL); + } + + if (s_username) g_free(s_username); + if (s_password) g_free(s_password); + if (s_domain) g_free(s_domain); + + return true; + } else { + return false; + } + + return true; +} +#else +static BOOL remmina_rdp_authenticate_ex(freerdp* instance, char** username, char** password, + char** domain, rdp_auth_reason reason) +{ + TRACE_CALL(__func__); + gchar *s_username = NULL, *s_password = NULL, *s_domain = NULL; + const gchar* key_user = NULL; + const gchar* key_domain = NULL; + const gchar* key_password = NULL; + const gchar* key_title = NULL; + gint ret; + rfContext *rfi; + RemminaProtocolWidget *gp; + gboolean save; + gboolean disablepasswordstoring; + RemminaFile *remminafile; + RemminaMessagePanelFlags flags = REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD | REMMINA_MESSAGE_PANEL_FLAG_USERNAME | REMMINA_MESSAGE_PANEL_FLAG_DOMAIN; + + rfi = (rfContext *)instance->context; + gp = rfi->protocol_widget; + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + disablepasswordstoring = remmina_plugin_service->file_get_int(remminafile, "disablepasswordstoring", FALSE); + + FreeRDP_Settings_Keys_String cfg_key_user = FreeRDP_STRING_UNUSED; + FreeRDP_Settings_Keys_String cfg_key_domain = FreeRDP_STRING_UNUSED; + FreeRDP_Settings_Keys_String cfg_key_password = FreeRDP_STRING_UNUSED; + switch(reason) { + case AUTH_NLA: + case AUTH_TLS: + case AUTH_RDP: + key_title = _("Enter RDP authentication credentials"); + key_user = "username"; + key_domain = "domain"; + key_password = "password"; + cfg_key_user = FreeRDP_Username; + cfg_key_domain = FreeRDP_Domain; + cfg_key_password = FreeRDP_Password; + break; + case GW_AUTH_HTTP: + case GW_AUTH_RDG: + case GW_AUTH_RPC: + key_title = _("Enter RDP gateway authentication credentials"); + key_user = "gateway_username"; + key_domain = "gateway_domain"; + key_password = "gateway_password"; + cfg_key_user = FreeRDP_GatewayUsername; + cfg_key_domain = FreeRDP_GatewayDomain; + cfg_key_password = FreeRDP_GatewayPassword; + break; + case AUTH_SMARTCARD_PIN: + key_title = _("Enter RDP SmartCard PIN"); + key_password = "smartcard_pin"; + flags = 0; + break; + default: + // TODO: Display an error dialog informing the user that the remote requires some mechanism FreeRDP or Remmina currently do not support + g_fprintf(stderr, "[authentication] unsupported type %d, access denied", reason); + return FALSE; + } + + if (!disablepasswordstoring) + flags |= REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD; + + ret = remmina_plugin_service->protocol_plugin_init_auth(gp, flags, + key_title, + remmina_plugin_service->file_get_string(remminafile, key_user), + remmina_plugin_service->file_get_string(remminafile, key_password), + remmina_plugin_service->file_get_string(remminafile, disablepasswordstoring ? NULL : key_domain), + NULL); + if (ret == GTK_RESPONSE_OK) { + if (cfg_key_user != FreeRDP_STRING_UNUSED) + { + s_username = remmina_plugin_service->protocol_plugin_init_get_username(gp); + if (s_username) + freerdp_settings_set_string(rfi->clientContext.context.settings, cfg_key_user, s_username); + remmina_plugin_service->file_set_string(remminafile, key_user, s_username); + } + + if (cfg_key_password != FreeRDP_STRING_UNUSED) + { + s_password = remmina_plugin_service->protocol_plugin_init_get_password(gp); + if (s_password) + freerdp_settings_set_string(rfi->clientContext.context.settings, cfg_key_password, s_password); + } + + if (cfg_key_domain != FreeRDP_STRING_UNUSED) { + s_domain = remmina_plugin_service->protocol_plugin_init_get_domain(gp); + if (s_domain) + freerdp_settings_set_string(rfi->clientContext.context.settings, cfg_key_domain, s_domain); + remmina_plugin_service->file_set_string(remminafile, key_domain, s_domain); + } + + save = remmina_plugin_service->protocol_plugin_init_get_savepassword(gp); + if (save) { + // User has requested to save credentials. We put the password + // into remminafile->settings. It will be saved later, on successful connection, by + // rcw.c + remmina_plugin_service->file_set_string(remminafile, key_password, s_password); + } else { + remmina_plugin_service->file_set_string(remminafile, key_password, NULL); + } + + + if (s_username) g_free(s_username); + if (s_password) g_free(s_password); + if (s_domain) g_free(s_domain); + + return TRUE; + } else { + return FALSE; + } + + return TRUE; +} + +static BOOL remmina_rdp_choose_smartcard(freerdp* instance, SmartcardCertInfo** cert_list, DWORD count, + DWORD* choice, BOOL gateway) +{ + // TODO: Display a simple list of smartcards/certs to choose from. + // See client_cli_choose_smartcard for a sample + return client_cli_choose_smartcard(instance, cert_list, count, choice, gateway); +} + +static BOOL remmina_rdp_get_access_token(freerdp* instance, AccessTokenType tokenType, char** token, + size_t count, ...) +{ + // TODO: Open a (currently hard coded) URL, authenticate in a webview/browser, return the access token. + // See client_cli_get_access_token or sdl_webview_get_access_token for implementations + return client_cli_get_access_token(instance, tokenType, token, count); +} + +static BOOL remmina_rdp_present_gateway_message(freerdp* instance, UINT32 type, BOOL isDisplayMandatory, + BOOL isConsentMandatory, size_t length, + const WCHAR* message) +{ + // TODO: Present a message to the user, usually terms of service or similar + // See client_cli_present_gateway_message or sdl_present_gateway_message + return client_cli_present_gateway_message(instance, type, isDisplayMandatory, isConsentMandatory, length, message); +} + +static int remmina_rdp_logon_error_info(freerdp* instance, UINT32 data, UINT32 type) +{ + // TODO: Display the reason for connection termination + // See client_cli_logon_error_info or sdl_logon_error_info + return client_cli_logon_error_info(instance, data, type); +} + +static SSIZE_T remmina_rdp_retry_dialog(freerdp* instance, const char* what, size_t current, + void* userarg) +{ + // TODO: + // See client_common_retry_dialog or + return client_common_retry_dialog(instance, what, current, userarg); +} + +static void remmina_rdp_post_final_disconnect(freerdp* instance) +{ + // Clean up resources allocated in PreConnect +} +#endif + +static DWORD remmina_rdp_verify_certificate_ex(freerdp *instance, const char *host, UINT16 port, + const char *common_name, const char *subject, + const char *issuer, const char *fingerprint, DWORD flags) +{ + TRACE_CALL(__func__); + gint status; + rfContext *rfi; + RemminaProtocolWidget *gp; + + rfi = (rfContext *)instance->context; + gp = rfi->protocol_widget; + + status = remmina_plugin_service->protocol_plugin_init_certificate(gp, subject, issuer, fingerprint); + + if (status == GTK_RESPONSE_OK) + return 1; + + return 0; +} + +static DWORD +remmina_rdp_verify_certificate(freerdp *instance, const char *common_name, const char *subject, const char *issuer, const char *fingerprint, BOOL host_mismatch) __attribute__ ((unused)); +static DWORD +remmina_rdp_verify_certificate(freerdp *instance, const char *common_name, const char *subject, + const char *issuer, const char *fingerprint, BOOL host_mismatch) +{ + TRACE_CALL(__func__); + gint status; + rfContext *rfi; + RemminaProtocolWidget *gp; + + rfi = (rfContext *)instance->context; + gp = rfi->protocol_widget; + + status = remmina_plugin_service->protocol_plugin_init_certificate(gp, subject, issuer, fingerprint); + + if (status == GTK_RESPONSE_OK) + return 1; + + return 0; +} + +static DWORD remmina_rdp_verify_changed_certificate_ex(freerdp *instance, const char *host, UINT16 port, + const char *common_name, const char *subject, + const char *issuer, const char *fingerprint, + const char *old_subject, const char *old_issuer, + const char *old_fingerprint, DWORD flags) +{ + TRACE_CALL(__func__); + gint status; + rfContext *rfi; + RemminaProtocolWidget *gp; + + rfi = (rfContext *)instance->context; + gp = rfi->protocol_widget; + + status = remmina_plugin_service->protocol_plugin_changed_certificate(gp, subject, issuer, fingerprint, old_fingerprint); + + if (status == GTK_RESPONSE_OK) + return 1; + + return 0; +} + +static void remmina_rdp_post_disconnect(freerdp *instance) +{ + TRACE_CALL(__func__); + + if (!instance || !instance->context) + return; + + PubSub_UnsubscribeChannelConnected(instance->context->pubSub, + remmina_rdp_OnChannelConnectedEventHandler); + PubSub_UnsubscribeChannelDisconnected(instance->context->pubSub, + remmina_rdp_OnChannelDisconnectedEventHandler); + + /* The remaining cleanup will be continued on main thread by complete_cleanup_on_main_thread() */ + + // With FreeRDP3 only resources allocated in PostConnect and later are cleaned up here. +} + +static void remmina_rdp_main_loop(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + DWORD status; + gchar buf[100]; + rfContext *rfi = GET_PLUGIN_DATA(gp); + RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + time_t cur_time, time_diff; + + int jitter_time = remmina_plugin_service->file_get_int(remminafile, "rdp_mouse_jitter", 0); + time(&last_time); +#if FREERDP_VERSION_MAJOR >= 3 + while (!freerdp_shall_disconnect_context(&rfi->clientContext.context)) { +#else + while (!freerdp_shall_disconnect(rfi->clientContext.context.instance)) { +#endif + // move mouse if we've been idle and option is selected + time(&cur_time); + time_diff = cur_time - last_time; + if (jitter_time > 0 && time_diff > jitter_time){ + last_time = cur_time; + remmina_rdp_mouse_jitter(gp); + } + + HANDLE handles[MAXIMUM_WAIT_OBJECTS] = {0}; + DWORD nCount = freerdp_get_event_handles(&rfi->clientContext.context, &handles[0], ARRAYSIZE(handles)); + if (rfi->event_handle) + handles[nCount++] = rfi->event_handle; + + handles[nCount++] = freerdp_abort_event(&rfi->clientContext.context); + + if (nCount == 0) { + fprintf(stderr, "freerdp_get_event_handles failed\n"); + break; + } + + status = WaitForMultipleObjects(nCount, handles, FALSE, 100); + + if (status == WAIT_FAILED) { + fprintf(stderr, "WaitForMultipleObjects failed with %lu\n", (unsigned long)status); + break; + } + + if (rfi->event_handle && WaitForSingleObject(rfi->event_handle, 0) == WAIT_OBJECT_0) { + if (!rf_process_event_queue(gp)) { + fprintf(stderr, "Could not process local keyboard/mouse event queue\n"); + break; + } + if (read(rfi->event_pipe[0], buf, sizeof(buf))) { + } + } + + /* Check if a processed event called freerdp_abort_connect() and exit if true */ + if (WaitForSingleObject(freerdp_abort_event(&rfi->clientContext.context), 0) == WAIT_OBJECT_0) + /* Session disconnected by local user action */ + break; + + if (!freerdp_check_event_handles(&rfi->clientContext.context)) { + if (rf_auto_reconnect(rfi)) { + /* Reset the possible reason/error which made us doing many reconnection reattempts and continue */ + remmina_plugin_service->protocol_plugin_set_error(gp, NULL); + continue; + } + if (freerdp_get_last_error(&rfi->clientContext.context) == FREERDP_ERROR_SUCCESS) + fprintf(stderr, "Could not check FreeRDP file descriptor\n"); + break; + } + } + const gchar *host = freerdp_settings_get_string (rfi->clientContext.context.settings, FreeRDP_ServerHostname); + // TRANSLATORS: the placeholder may be either an IP/FQDN or a server hostname + REMMINA_PLUGIN_AUDIT(_("Disconnected from %s via RDP"), host); + freerdp_disconnect(rfi->clientContext.context.instance); + REMMINA_PLUGIN_DEBUG("RDP client disconnected"); +} + +static int remmina_rdp_load_static_channel_addin(rdpChannels *channels, rdpSettings *settings, char *name, void *data) +{ + TRACE_CALL(__func__); + PVIRTUALCHANNELENTRY entry = NULL; + PVIRTUALCHANNELENTRYEX entryEx = NULL; + + entryEx = (PVIRTUALCHANNELENTRYEX)(void *)freerdp_load_channel_addin_entry( + name, NULL, NULL, FREERDP_ADDIN_CHANNEL_STATIC | FREERDP_ADDIN_CHANNEL_ENTRYEX); + + if (!entryEx) + entry = freerdp_load_channel_addin_entry(name, NULL, NULL, FREERDP_ADDIN_CHANNEL_STATIC); + + if (entryEx) { + if (freerdp_channels_client_load_ex(channels, settings, entryEx, data) == 0) { + fprintf(stderr, "loading channel %s\n", name); + return TRUE; + } + } else if (entry) { + if (freerdp_channels_client_load(channels, settings, entry, data) == 0) { + fprintf(stderr, "loading channel %s\n", name); + return TRUE; + } + } + + return FALSE; +} + +static gchar *remmina_rdp_find_prdriver(char *smap, char *prn) +{ + char c, *p, *dr; + int matching; + size_t sz; + + enum { S_WAITPR, + S_INPRINTER, + S_WAITCOLON, + S_WAITDRIVER, + S_INDRIVER, + S_WAITSEMICOLON } state = S_WAITPR; + + matching = 0; + while ((c = *smap++) != 0) { + switch (state) { + case S_WAITPR: + if (c != '\"') return NULL; + state = S_INPRINTER; + p = prn; + matching = 1; + break; + case S_INPRINTER: + if (matching && c == *p && *p != 0) { + p++; + } else if (c == '\"') { + if (*p != 0) + matching = 0; + state = S_WAITCOLON; + } else { + matching = 0; + } + break; + case S_WAITCOLON: + if (c != ':') + return NULL; + state = S_WAITDRIVER; + break; + case S_WAITDRIVER: + if (c != '\"') + return NULL; + state = S_INDRIVER; + dr = smap; + break; + case S_INDRIVER: + if (c == '\"') { + if (matching) + goto found; + else + state = S_WAITSEMICOLON; + } + break; + case S_WAITSEMICOLON: + if (c != ';') + return NULL; + state = S_WAITPR; + break; + } + } + return NULL; + +found: + sz = smap - dr; + p = (char *)malloc(sz); + memcpy(p, dr, sz); + p[sz - 1] = 0; + return p; +} + +#ifdef HAVE_CUPS +/** + * Callback function used by cupsEnumDests + * - For each enumerated local printer tries to set the Printer Name and Driver. + * @return 1 if there are other printers to scan or 0 when it's done. + */ +static int remmina_rdp_set_printers(void *user_data, unsigned flags, cups_dest_t *dest) +{ + rfContext *rfi = (rfContext *)user_data; + RemminaProtocolWidget *gp = rfi->protocol_widget; + + /** @warning printer-make-and-model is not always the same as on the Windows, + * therefore it fails finding to right one and it fails to add + * the printer. + * + * We pass NULL and we do not check for errors. The following code is + * how it is supposed to work. @todo Ask CUPS mailing list for help. + * + * @code + * const char *model = cupsGetOption("printer-make-and-model", + * dest->num_options, + * dest->options); + * @endcode + */ + + RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + const gchar *s = remmina_plugin_service->file_get_string(remminafile, "printer_overrides"); + + RDPDR_PRINTER *printer; + printer = (RDPDR_PRINTER *)calloc(1, sizeof(RDPDR_PRINTER)); + +#if FREERDP_VERSION_MAJOR >= 3 + RDPDR_DEVICE *pdev; + pdev = &(printer->device); +#else + RDPDR_PRINTER *pdev; + pdev = printer; +#endif + + pdev->Type = RDPDR_DTYP_PRINT; + REMMINA_PLUGIN_DEBUG("Printer Type: %d", pdev->Type); + + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RedirectPrinters, TRUE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_DeviceRedirection, TRUE); + + REMMINA_PLUGIN_DEBUG("Destination: %s", dest->name); + if (!(pdev->Name = _strdup(dest->name))) { + free(printer); + return 1; + } + + REMMINA_PLUGIN_DEBUG("Printer Name: %s", pdev->Name); + + if (s) { + gchar *d = remmina_rdp_find_prdriver(strdup(s), pdev->Name); + if (d) { + printer->DriverName = strdup(d); + REMMINA_PLUGIN_DEBUG("Printer DriverName set to: %s", printer->DriverName); + g_free(d); + } else { + /** + * When remmina_rdp_find_prdriver doesn't return a DriverName + * it means that we don't want to share that printer + * + */ + free(pdev->Name); + free(printer); + return 1; + } + } else { + /* We set to a default driver*/ + printer->DriverName = _strdup("MS Publisher Imagesetter"); + } + + REMMINA_PLUGIN_DEBUG("Printer Driver: %s", printer->DriverName); + if (!freerdp_device_collection_add(rfi->clientContext.context.settings, (RDPDR_DEVICE *)printer)) { + free(printer->DriverName); + free(pdev->Name); + free(printer); + return 1; + } + + return 1; +} +#endif /* HAVE_CUPS */ + +/* Send Ctrl+Alt+Del keystrokes to the plugin drawing_area widget */ +static void remmina_rdp_send_ctrlaltdel(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + guint keys[] = { GDK_KEY_Control_L, GDK_KEY_Alt_L, GDK_KEY_Delete }; + rfContext *rfi = GET_PLUGIN_DATA(gp); + + remmina_plugin_service->protocol_plugin_send_keys_signals(rfi->drawing_area, + keys, G_N_ELEMENTS(keys), GDK_KEY_PRESS | GDK_KEY_RELEASE); +} + +static gboolean remmina_rdp_set_connection_type(rdpSettings *settings, guint32 type) +{ + freerdp_settings_set_uint32(settings, FreeRDP_ConnectionType, type); + + if (type == CONNECTION_TYPE_MODEM) { + freerdp_settings_set_bool(settings, FreeRDP_DisableWallpaper, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_AllowFontSmoothing, FALSE); + freerdp_settings_set_bool(settings, FreeRDP_AllowDesktopComposition, FALSE); + freerdp_settings_set_bool(settings, FreeRDP_DisableFullWindowDrag, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_DisableMenuAnims, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_DisableThemes, TRUE); + } else if (type == CONNECTION_TYPE_BROADBAND_LOW) { + freerdp_settings_set_bool(settings, FreeRDP_DisableWallpaper, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_AllowFontSmoothing, FALSE); + freerdp_settings_set_bool(settings, FreeRDP_AllowDesktopComposition, FALSE); + freerdp_settings_set_bool(settings, FreeRDP_DisableFullWindowDrag, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_DisableMenuAnims, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_DisableThemes, FALSE); + } else if (type == CONNECTION_TYPE_SATELLITE) { + freerdp_settings_set_bool(settings, FreeRDP_DisableWallpaper, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_AllowFontSmoothing, FALSE); + freerdp_settings_set_bool(settings, FreeRDP_AllowDesktopComposition, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_DisableFullWindowDrag, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_DisableMenuAnims, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_DisableThemes, FALSE); + } else if (type == CONNECTION_TYPE_BROADBAND_HIGH) { + freerdp_settings_set_bool(settings, FreeRDP_DisableWallpaper, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_AllowFontSmoothing, FALSE); + freerdp_settings_set_bool(settings, FreeRDP_AllowDesktopComposition, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_DisableFullWindowDrag, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_DisableMenuAnims, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_DisableThemes, FALSE); + } else if (type == CONNECTION_TYPE_WAN) { + freerdp_settings_set_bool(settings, FreeRDP_DisableWallpaper, FALSE); + freerdp_settings_set_bool(settings, FreeRDP_AllowFontSmoothing, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_AllowDesktopComposition, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_DisableFullWindowDrag, FALSE); + freerdp_settings_set_bool(settings, FreeRDP_DisableMenuAnims, FALSE); + freerdp_settings_set_bool(settings, FreeRDP_DisableThemes, FALSE); + } else if (type == CONNECTION_TYPE_LAN) { + freerdp_settings_set_bool(settings, FreeRDP_DisableWallpaper, FALSE); + freerdp_settings_set_bool(settings, FreeRDP_AllowFontSmoothing, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_AllowDesktopComposition, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_DisableFullWindowDrag, FALSE); + freerdp_settings_set_bool(settings, FreeRDP_DisableMenuAnims, FALSE); + freerdp_settings_set_bool(settings, FreeRDP_DisableThemes, FALSE); + } else if (type == CONNECTION_TYPE_AUTODETECT) { + freerdp_settings_set_bool(settings, FreeRDP_DisableWallpaper, FALSE); + freerdp_settings_set_bool(settings, FreeRDP_AllowFontSmoothing, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_AllowDesktopComposition, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_DisableFullWindowDrag, FALSE); + freerdp_settings_set_bool(settings, FreeRDP_DisableMenuAnims, FALSE); + freerdp_settings_set_bool(settings, FreeRDP_DisableThemes, FALSE); + freerdp_settings_set_bool(settings, FreeRDP_NetworkAutoDetect, TRUE); + + /* Automatically activate GFX and RFX codec support */ +#ifdef WITH_GFX_H264 + freerdp_settings_set_bool(settings, FreeRDP_GfxAVC444, gfx_h264_available); + freerdp_settings_set_bool(settings, FreeRDP_GfxH264, gfx_h264_available); +#endif + freerdp_settings_set_bool(settings, FreeRDP_RemoteFxCodec, TRUE); + freerdp_settings_set_bool(settings, FreeRDP_SupportGraphicsPipeline, TRUE); + } else if (type == REMMINA_CONNECTION_TYPE_NONE) { + return FALSE; + } else { + return FALSE; + } + + return TRUE; +} + +#ifdef GDK_WINDOWING_X11 +#if FREERDP_CHECK_VERSION(2, 3, 0) +static gchar *remmina_get_rdp_kbd_remap(const gchar *keymap) +{ + TRACE_CALL(__func__); + guint *table; + gchar keys[20]; + gchar *rdp_kbd_remap = NULL; + gint i; + Display *display; + + table = remmina_plugin_service->pref_keymap_get_table(keymap); + if (!table) + return rdp_kbd_remap; + rdp_kbd_remap = g_malloc0(512); + display = XOpenDisplay(0); + for (i = 0; table[i] > 0; i += 2) { + g_snprintf(keys, sizeof(keys), "0x%02x=0x%02x", freerdp_keyboard_get_rdp_scancode_from_x11_keycode(XKeysymToKeycode(display, table[i])), + freerdp_keyboard_get_rdp_scancode_from_x11_keycode(XKeysymToKeycode(display, table[i + 1]))); + if (i > 0) + g_strlcat(rdp_kbd_remap, ",", 512); + g_strlcat(rdp_kbd_remap, keys, 512); + } + XCloseDisplay(display); + + return rdp_kbd_remap; +} +#endif +#endif + +static gboolean remmina_rdp_main(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + const gchar *s; + gchar *sm; + gchar *value; + const gchar *cs; + RemminaFile *remminafile; + rfContext *rfi = GET_PLUGIN_DATA(gp); + rdpChannels *channels; + gchar *gateway_host; + gint gateway_port; + gchar *datapath = NULL; + gboolean status = TRUE; +#ifdef GDK_WINDOWING_X11 + gchar *rdp_kbd_remap; +#endif + gint i; + + gint desktopOrientation, desktopScaleFactor, deviceScaleFactor; + + channels = rfi->clientContext.context.channels; + + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + + datapath = g_build_path("/", + remmina_plugin_service->file_get_user_datadir(), + "RDP", + NULL); + REMMINA_PLUGIN_DEBUG("RDP data path is %s", datapath); + + if ((datapath != NULL) && (datapath[0] != '\0')) + if (access(datapath, W_OK) == 0) + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_ConfigPath, datapath); + g_free(datapath); + + if (remmina_plugin_service->file_get_int(remminafile, "assistance_mode", 0)){ + rdpAssistanceFile* file = freerdp_assistance_file_new(); + if (!file){ + REMMINA_PLUGIN_DEBUG("Could not allocate assistance file structure"); + return FALSE; + } + + if (remmina_plugin_service->file_get_string(remminafile, "assistance_file") == NULL || + remmina_plugin_service->file_get_string(remminafile, "assistance_pass") == NULL ){ + + REMMINA_PLUGIN_DEBUG("Assistance file and password are not set while assistance mode is on"); + return FALSE; + } + + status = freerdp_assistance_parse_file(file, + remmina_plugin_service->file_get_string(remminafile, "assistance_file"), + remmina_plugin_service->file_get_string(remminafile, "assistance_pass")); + + if (status < 0){ + REMMINA_PLUGIN_DEBUG("Could not parse assistance file"); + return FALSE; + } + + + if (!freerdp_assistance_populate_settings_from_assistance_file(file, rfi->clientContext.context.settings)){ + REMMINA_PLUGIN_DEBUG("Could not populate settings from assistance file"); + return FALSE; + } + } + + +#if defined(PROXY_TYPE_IGNORE) + if (!remmina_plugin_service->file_get_int(remminafile, "useproxyenv", FALSE) ? TRUE : FALSE) { + REMMINA_PLUGIN_DEBUG("Not using system proxy settings"); + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_ProxyType, PROXY_TYPE_IGNORE); + } +#endif + + if (!remmina_rdp_tunnel_init(gp)) + return FALSE; + + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_AutoReconnectionEnabled, (remmina_plugin_service->file_get_int(remminafile, "disableautoreconnect", FALSE) ? FALSE : TRUE)); + /* Disable RDP auto reconnection when SSH tunnel is enabled */ + if (remmina_plugin_service->file_get_int(remminafile, "ssh_tunnel_enabled", FALSE)) + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_AutoReconnectionEnabled, FALSE); + + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth, remmina_plugin_service->file_get_int(remminafile, "colordepth", 99)); + + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_SoftwareGdi, TRUE); + REMMINA_PLUGIN_DEBUG("gfx_h264_available: %d", gfx_h264_available); + + /* Avoid using H.264 modes if they are not available on libfreerdp */ + if (!gfx_h264_available && (freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth) == 65 || freerdp_settings_get_bool(rfi->clientContext.context.settings, FreeRDP_ColorDepth == 66))) + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth, 64); // Fallback to GFX RFX + + if (freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth) == 0) { + /* RFX (Win7)*/ + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RemoteFxCodec, TRUE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_SupportGraphicsPipeline, FALSE); + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth, 32); + } else if (freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth) == 63) { + /* /gfx (RFX Progressive) (Win8) */ + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth, 32); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_SupportGraphicsPipeline, TRUE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GfxH264, FALSE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GfxAVC444, FALSE); + } else if (freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth) == 64) { + /* /gfx:rfx (Win8) */ + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth, 32); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RemoteFxCodec, TRUE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_SupportGraphicsPipeline, TRUE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GfxH264, FALSE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GfxAVC444, FALSE); + } else if (freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth) == 65) { + /* /gfx:avc420 (Win8.1) */ + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth, 32); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_SupportGraphicsPipeline, TRUE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GfxH264, gfx_h264_available); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GfxAVC444, FALSE); + } else if (freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth) == 66) { + /* /gfx:avc444 (Win10) */ + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth, 32); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_SupportGraphicsPipeline, TRUE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GfxH264, gfx_h264_available); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GfxAVC444, gfx_h264_available); + } else if (freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth) == 99) { + /* Automatic (Let the server choose its best format) */ + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth, 32); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RemoteFxCodec, TRUE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_SupportGraphicsPipeline, TRUE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GfxH264, gfx_h264_available); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GfxAVC444, gfx_h264_available); + } + + if (freerdp_settings_get_bool(rfi->clientContext.context.settings, FreeRDP_RemoteFxCodec) || + freerdp_settings_get_bool(rfi->clientContext.context.settings, FreeRDP_NSCodec) || + freerdp_settings_get_bool(rfi->clientContext.context.settings, FreeRDP_SupportGraphicsPipeline)) { + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_FastPathOutput, TRUE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_FrameMarkerCommandEnabled, TRUE); + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth, 32); + rfi->bpp = 32; + } + + gint w = remmina_plugin_service->get_profile_remote_width(gp); + gint h = remmina_plugin_service->get_profile_remote_height(gp); + /* multiple of 4 */ + w = (w + 3) & ~0x3; + h = (h + 3) & ~0x3; + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopWidth, w); + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopHeight, h); + REMMINA_PLUGIN_DEBUG("Resolution set by the user: %dx%d", w, h); + + /* Workaround for FreeRDP issue #5417: in GFX AVC modes we can't go under + * AVC_MIN_DESKTOP_WIDTH x AVC_MIN_DESKTOP_HEIGHT */ + if (freerdp_settings_get_bool(rfi->clientContext.context.settings, FreeRDP_SupportGraphicsPipeline) && + freerdp_settings_get_bool(rfi->clientContext.context.settings, FreeRDP_GfxH264)) { + if (freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopWidth) < + AVC_MIN_DESKTOP_WIDTH) + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopWidth, + AVC_MIN_DESKTOP_WIDTH); + if (freerdp_settings_get_uint32(rfi->clientContext.context.settings, + FreeRDP_DesktopHeight) < + AVC_MIN_DESKTOP_HEIGHT) + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopHeight, + AVC_MIN_DESKTOP_HEIGHT); + } + + /* Workaround for FreeRDP issue #5119. This will make our horizontal resolution + * an even value, but it will add a vertical black 1 pixel line on the + * right of the desktop */ + if ((freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopWidth) & 1) != 0) { + UINT32 tmp = freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopWidth); + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopWidth, tmp - 1); + } + + remmina_plugin_service->protocol_plugin_set_width(gp, freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopWidth)); + remmina_plugin_service->protocol_plugin_set_height(gp, freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopHeight)); + + w = freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopWidth); + h = freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopHeight); + REMMINA_PLUGIN_DEBUG("Resolution set after workarounds: %dx%d", w, h); + + + if (remmina_plugin_service->file_get_string(remminafile, "username")) + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_Username, remmina_plugin_service->file_get_string(remminafile, "username")); + + if (remmina_plugin_service->file_get_string(remminafile, "domain")) + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_Domain, remmina_plugin_service->file_get_string(remminafile, "domain")); + + s = remmina_plugin_service->file_get_string(remminafile, "password"); + if (s) freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_Password, s); + + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_AutoLogonEnabled, TRUE); + + /** + * Proxy support + * Proxy settings are hidden at the moment as an advanced feature + */ + gchar *proxy_type = g_strdup(remmina_plugin_service->file_get_string(remminafile, "proxy_type")); + gchar *proxy_username = g_strdup(remmina_plugin_service->file_get_string(remminafile, "proxy_username")); + gchar *proxy_password = g_strdup(remmina_plugin_service->file_get_string(remminafile, "proxy_password")); + gchar *proxy_hostname = g_strdup(remmina_plugin_service->file_get_string(remminafile, "proxy_hostname")); + gint proxy_port = remmina_plugin_service->file_get_int(remminafile, "proxy_port", 80); + REMMINA_PLUGIN_DEBUG("proxy_type: %s", proxy_type); + REMMINA_PLUGIN_DEBUG("proxy_username: %s", proxy_username); + REMMINA_PLUGIN_DEBUG("proxy_password: %s", proxy_password); + REMMINA_PLUGIN_DEBUG("proxy_hostname: %s", proxy_hostname); + REMMINA_PLUGIN_DEBUG("proxy_port: %d", proxy_port); + if (proxy_type && proxy_hostname) { + if (g_strcmp0(proxy_type, "no_proxy") == 0) + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_ProxyType, PROXY_TYPE_IGNORE); + else if (g_strcmp0(proxy_type, "http") == 0) + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_ProxyType, PROXY_TYPE_HTTP); + else if (g_strcmp0(proxy_type, "socks5") == 0) + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_ProxyType, PROXY_TYPE_SOCKS); + else + g_warning("Invalid proxy protocol, at the moment only no_proxy, HTTP and SOCKS5 are supported"); + REMMINA_PLUGIN_DEBUG("ProxyType set to: %" PRIu32, freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_ProxyType)); + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_ProxyHostname, proxy_hostname); + if (proxy_username) + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_ProxyUsername, proxy_username); + if (proxy_password) + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_ProxyPassword, proxy_password); + if (proxy_port) + freerdp_settings_set_uint16(rfi->clientContext.context.settings, FreeRDP_ProxyPort, proxy_port); + } + g_free(proxy_hostname); + g_free(proxy_username); + g_free(proxy_password); + + if (remmina_plugin_service->file_get_int(remminafile, "base-cred-for-gw", FALSE)) { + // Reset gateway credentials + remmina_plugin_service->file_set_string(remminafile, "gateway_username", NULL); + remmina_plugin_service->file_set_string(remminafile, "gateway_domain", NULL); + remmina_plugin_service->file_set_string(remminafile, "gateway_password", NULL); + } + + /* Remote Desktop Gateway server address */ + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GatewayEnabled, FALSE); + s = remmina_plugin_service->file_get_string(remminafile, "gateway_server"); + if (s) { + cs = remmina_plugin_service->file_get_string(remminafile, "gwtransp"); +#if FREERDP_CHECK_VERSION(2, 3, 1) + if (remmina_plugin_service->file_get_int(remminafile, "websockets", FALSE)) + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GatewayHttpUseWebsockets, TRUE); + else + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GatewayHttpUseWebsockets, FALSE); +#endif + if (g_strcmp0(cs, "http") == 0) { + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GatewayRpcTransport, FALSE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GatewayHttpTransport, TRUE); + } else if (g_strcmp0(cs, "rpc") == 0) { + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GatewayRpcTransport, TRUE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GatewayHttpTransport, FALSE); + } else if (g_strcmp0(cs, "auto") == 0) { + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GatewayRpcTransport, TRUE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GatewayHttpTransport, TRUE); + } + remmina_plugin_service->get_server_port(s, 443, &gateway_host, &gateway_port); + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_GatewayHostname, gateway_host); + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_GatewayPort, gateway_port); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GatewayEnabled, TRUE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GatewayUseSameCredentials, TRUE); + } + /* Remote Desktop Gateway domain */ + if (remmina_plugin_service->file_get_string(remminafile, "gateway_domain")) { + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_GatewayDomain, remmina_plugin_service->file_get_string(remminafile, "gateway_domain")); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GatewayUseSameCredentials, FALSE); + } + /* Remote Desktop Gateway username */ + if (remmina_plugin_service->file_get_string(remminafile, "gateway_username")) { + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_GatewayUsername, remmina_plugin_service->file_get_string(remminafile, "gateway_username")); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GatewayUseSameCredentials, FALSE); + } + /* Remote Desktop Gateway password */ + s = remmina_plugin_service->file_get_string(remminafile, "gateway_password"); + if (s) { + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_GatewayPassword, s); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GatewayUseSameCredentials, FALSE); + } + /* If no different credentials were provided for the Remote Desktop Gateway + * use the same authentication credentials for the host */ + if (freerdp_settings_get_bool(rfi->clientContext.context.settings, FreeRDP_GatewayEnabled) && freerdp_settings_get_bool(rfi->clientContext.context.settings, FreeRDP_GatewayUseSameCredentials)) { + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_GatewayDomain, freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_Domain)); + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_GatewayUsername, freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_Username)); + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_GatewayPassword, freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_Password)); + } + /* Remote Desktop Gateway usage */ + if (freerdp_settings_get_bool(rfi->clientContext.context.settings, FreeRDP_GatewayEnabled)) + freerdp_set_gateway_usage_method(rfi->clientContext.context.settings, + remmina_plugin_service->file_get_int(remminafile, "gateway_usage", FALSE) ? TSC_PROXY_MODE_DETECT : TSC_PROXY_MODE_DIRECT); + + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_GatewayAccessToken, + remmina_plugin_service->file_get_string(remminafile, "gatewayaccesstoken")); + + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_AuthenticationLevel, remmina_plugin_service->file_get_int( + remminafile, "authentication level", freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_AuthenticationLevel))); + + /* Certificate ignore */ + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_IgnoreCertificate, remmina_plugin_service->file_get_int(remminafile, "cert_ignore", 0)); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_OldLicenseBehaviour, remmina_plugin_service->file_get_int(remminafile, "old-license", 0)); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_AllowUnanouncedOrdersFromServer, remmina_plugin_service->file_get_int(remminafile, "relax-order-checks", 0)); + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_GlyphSupportLevel, (remmina_plugin_service->file_get_int(remminafile, "glyph-cache", 0) ? GLYPH_SUPPORT_FULL : GLYPH_SUPPORT_NONE)); + + if ((cs = remmina_plugin_service->file_get_string(remminafile, "clientname"))) + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_ClientHostname, cs); + else + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_ClientHostname, g_get_host_name()); + + /* Client Build number is optional, if not specified defaults to 0, allow for comments to appear after number */ + if ((cs = remmina_plugin_service->file_get_string(remminafile, "clientbuild"))) { + if (*cs) { + UINT32 val = strtoul(cs, NULL, 0); + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_ClientBuild, val); + } + } + + + if (remmina_plugin_service->file_get_string(remminafile, "loadbalanceinfo")) { + const gchar *tmp = strdup(remmina_plugin_service->file_get_string(remminafile, "loadbalanceinfo")); + freerdp_settings_set_pointer_len(rfi->clientContext.context.settings, FreeRDP_LoadBalanceInfo, tmp, strlen(tmp) + 1); + } + + if (remmina_plugin_service->file_get_string(remminafile, "exec")) + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_AlternateShell, remmina_plugin_service->file_get_string(remminafile, "exec")); + + if (remmina_plugin_service->file_get_string(remminafile, "execpath")) + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_ShellWorkingDirectory, remmina_plugin_service->file_get_string(remminafile, "execpath")); + + sm = g_strdup_printf("rdp_quality_%i", remmina_plugin_service->file_get_int(remminafile, "quality", DEFAULT_QUALITY_0)); + value = remmina_plugin_service->pref_get_value(sm); + g_free(sm); + + if (value && value[0]) { + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_PerformanceFlags, strtoul(value, NULL, 16)); + } else { + switch (remmina_plugin_service->file_get_int(remminafile, "quality", DEFAULT_QUALITY_0)) { + case 9: + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_PerformanceFlags, DEFAULT_QUALITY_9); + break; + + case 2: + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_PerformanceFlags, DEFAULT_QUALITY_2); + break; + + case 1: + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_PerformanceFlags, DEFAULT_QUALITY_1); + break; + + case 0: + default: + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_PerformanceFlags, DEFAULT_QUALITY_0); + break; + } + } + g_free(value); + + if ((cs = remmina_plugin_service->file_get_string(remminafile, "network"))) { + guint32 type = 0; + + if (g_strcmp0(cs, "modem") == 0) + type = CONNECTION_TYPE_MODEM; + else if (g_strcmp0(cs, "broadband") == 0) + type = CONNECTION_TYPE_BROADBAND_HIGH; + else if (g_strcmp0(cs, "broadband-low") == 0) + type = CONNECTION_TYPE_BROADBAND_LOW; + else if (g_strcmp0(cs, "broadband-high") == 0) + type = CONNECTION_TYPE_BROADBAND_HIGH; + else if (g_strcmp0(cs, "wan") == 0) + type = CONNECTION_TYPE_WAN; + else if (g_strcmp0(cs, "lan") == 0) + type = CONNECTION_TYPE_LAN; + else if ((g_strcmp0(cs, "autodetect") == 0)) + type = CONNECTION_TYPE_AUTODETECT; + else if ((g_strcmp0(cs, "none") == 0)) + type = REMMINA_CONNECTION_TYPE_NONE; + else + type = REMMINA_CONNECTION_TYPE_NONE; + + if (!remmina_rdp_set_connection_type(rfi->clientContext.context.settings, type)) + REMMINA_PLUGIN_DEBUG("Network settings not set"); + } + + /* PerformanceFlags bitmask need also to be splitted into BOOL variables + * like freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_DisableWallpaper, freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_AllowFontSmoothing… + * or freerdp_get_param_bool() function will return the wrong value + */ + freerdp_performance_flags_split(rfi->clientContext.context.settings); + +#ifdef GDK_WINDOWING_X11 +#if FREERDP_CHECK_VERSION(2, 3, 0) + rdp_kbd_remap = remmina_get_rdp_kbd_remap(remmina_plugin_service->file_get_string(remminafile, "keymap")); + if (rdp_kbd_remap != NULL) { + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_KeyboardRemappingList, rdp_kbd_remap); + REMMINA_PLUGIN_DEBUG( + "rdp_keyboard_remapping_list: %s", + freerdp_settings_get_string(rfi->clientContext.context.settings, + FreeRDP_KeyboardRemappingList)); + g_free(rdp_kbd_remap); + } + else { + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_KeyboardRemappingList, remmina_plugin_service->pref_get_value("rdp_kbd_remap")); + REMMINA_PLUGIN_DEBUG( + "rdp_keyboard_remapping_list: %s", + freerdp_settings_get_string(rfi->clientContext.context.settings, + FreeRDP_KeyboardRemappingList)); + } +#endif +#endif + + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_KeyboardLayout, remmina_rdp_settings_get_keyboard_layout()); + + if (remmina_plugin_service->file_get_int(remminafile, "console", FALSE)) + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_ConsoleSession, TRUE); + + if (remmina_plugin_service->file_get_int(remminafile, "restricted-admin", FALSE)) { + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_ConsoleSession, TRUE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RestrictedAdminModeRequired, TRUE); + } + + if (remmina_plugin_service->file_get_string(remminafile, "pth")) { + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_ConsoleSession, TRUE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RestrictedAdminModeRequired, TRUE); + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_PasswordHash, remmina_plugin_service->file_get_string(remminafile, "pth")); + remmina_plugin_service->file_set_int(remminafile, "restricted-admin", TRUE); + } + + cs = remmina_plugin_service->file_get_string(remminafile, "security"); + if (g_strcmp0(cs, "rdp") == 0) { + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RdpSecurity, TRUE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_TlsSecurity, FALSE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_NlaSecurity, FALSE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_ExtSecurity, FALSE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_UseRdpSecurityLayer, TRUE); + } else if (g_strcmp0(cs, "tls") == 0) { + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RdpSecurity, FALSE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_TlsSecurity, TRUE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_NlaSecurity, FALSE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_ExtSecurity, FALSE); + } else if (g_strcmp0(cs, "nla") == 0) { + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RdpSecurity, FALSE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_TlsSecurity, FALSE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_NlaSecurity, TRUE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_ExtSecurity, FALSE); + } else if (g_strcmp0(cs, "ext") == 0) { + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RdpSecurity, FALSE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_TlsSecurity, FALSE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_NlaSecurity, FALSE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_ExtSecurity, TRUE); + } else { + /* This is "-nego" switch of xfreerdp */ + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_NegotiateSecurityLayer, TRUE); + } + + cs = remmina_plugin_service->file_get_string(remminafile, "tls-seclevel"); + if (cs && g_strcmp0(cs,"")!=0) { + i = atoi(cs); + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_TlsSecLevel, i); + } + + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_CompressionEnabled, TRUE); + if (remmina_plugin_service->file_get_int(remminafile, "disable_fastpath", FALSE)) { + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_FastPathInput, FALSE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_FastPathOutput, FALSE); + } else { + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_FastPathInput, TRUE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_FastPathOutput, TRUE); + } + + /* Orientation and scaling settings */ + remmina_rdp_settings_get_orientation_scale_prefs(&desktopOrientation, &desktopScaleFactor, &deviceScaleFactor); + + freerdp_settings_set_uint16(rfi->clientContext.context.settings, FreeRDP_DesktopOrientation, desktopOrientation); + if (desktopScaleFactor != 0 && deviceScaleFactor != 0) { + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopScaleFactor, desktopScaleFactor); + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_DeviceScaleFactor, deviceScaleFactor); + } + + /* Try to enable "Display Control Virtual Channel Extension", needed to + * dynamically resize remote desktop. This will automatically open + * the "disp" dynamic channel, if available */ + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_SupportDisplayControl, TRUE); + + if (freerdp_settings_get_bool(rfi->clientContext.context.settings, FreeRDP_SupportDisplayControl)) { + CLPARAM *d[1]; + int dcount; + + dcount = 1; + d[0] = "disp"; + freerdp_client_add_dynamic_channel(rfi->clientContext.context.settings, dcount, d); + } + + if (freerdp_settings_get_bool(rfi->clientContext.context.settings, FreeRDP_SupportGraphicsPipeline)) { + CLPARAM *d[1]; + + int dcount; + + dcount = 1; + d[0] = "rdpgfx"; + freerdp_client_add_dynamic_channel(rfi->clientContext.context.settings, dcount, d); + } + + /* Sound settings */ + cs = remmina_plugin_service->file_get_string(remminafile, "sound"); + if (g_strcmp0(cs, "remote") == 0) { + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RemoteConsoleAudio, TRUE); + } else if ((cs != NULL && cs[0] != '\0') && g_str_has_prefix(cs, "local")) { + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_AudioPlayback, TRUE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_AudioCapture, TRUE); + } else { + /* Disable sound */ + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_AudioPlayback, FALSE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RemoteConsoleAudio, FALSE); + } + + cs = remmina_plugin_service->file_get_string(remminafile, "microphone"); + if (cs != NULL && cs[0] != '\0') { + if (g_strcmp0(cs, "0") == 0) { + REMMINA_PLUGIN_DEBUG("“microphone” was set to 0, setting to \"\""); + remmina_plugin_service->file_set_string(remminafile, "microphone", ""); + } else { + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_AudioCapture, TRUE); + REMMINA_PLUGIN_DEBUG("“microphone” set to “%s”", cs); + CLPARAM **p; + size_t count; + + p = remmina_rdp_CommandLineParseCommaSeparatedValuesEx("audin", g_strdup(cs), &count); + + freerdp_client_add_dynamic_channel(rfi->clientContext.context.settings, count, p); + g_free(p); + } + } + + cs = remmina_plugin_service->file_get_string(remminafile, "audio-output"); + if (cs != NULL && cs[0] != '\0') { + REMMINA_PLUGIN_DEBUG("audio output set to %s", cs); + CLPARAM **p; + size_t count; + + p = remmina_rdp_CommandLineParseCommaSeparatedValuesEx("rdpsnd", g_strdup(cs), &count); + status = freerdp_client_add_static_channel(rfi->clientContext.context.settings, count, p); + if (status) + status = freerdp_client_add_dynamic_channel(rfi->clientContext.context.settings, count, p); + g_free(p); + } + + + cs = remmina_plugin_service->file_get_string(remminafile, "freerdp_log_level"); + if (cs != NULL && cs[0] != '\0') + REMMINA_PLUGIN_DEBUG("Log level set to to %s", cs); + else + cs = g_strdup("INFO"); + wLog *root = WLog_GetRoot(); + WLog_SetStringLogLevel(root, cs); + + cs = remmina_plugin_service->file_get_string(remminafile, "freerdp_log_filters"); + if (cs != NULL && cs[0] != '\0') { + REMMINA_PLUGIN_DEBUG("Log filters set to to %s", cs); + WLog_AddStringLogFilters(cs); + } else { + WLog_AddStringLogFilters(NULL); + } + + + cs = remmina_plugin_service->file_get_string(remminafile, "usb"); + if (cs != NULL && cs[0] != '\0') { + CLPARAM **p; + size_t count; + p = remmina_rdp_CommandLineParseCommaSeparatedValuesEx("urbdrc", g_strdup(cs), &count); + freerdp_client_add_dynamic_channel(rfi->clientContext.context.settings, count, p); + g_free(p); + } + + cs = remmina_plugin_service->file_get_string(remminafile, "vc"); + if (cs != NULL && cs[0] != '\0') { + CLPARAM **p; + size_t count; + p = remmina_rdp_CommandLineParseCommaSeparatedValues(g_strdup(cs), &count); + freerdp_client_add_static_channel(rfi->clientContext.context.settings, count, p); + g_free(p); + } + + cs = remmina_plugin_service->file_get_string(remminafile, "dvc"); + if (cs != NULL && cs[0] != '\0') { + CLPARAM **p; + size_t count; + p = remmina_rdp_CommandLineParseCommaSeparatedValues(g_strdup(cs), &count); + freerdp_client_add_dynamic_channel(rfi->clientContext.context.settings, count, p); + g_free(p); + } + + cs = remmina_plugin_service->file_get_string(remminafile, RDP2TCP_DVC_CHANNEL_NAME); + if (cs != NULL && cs[0] != '\0') { + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_RDP2TCPArgs, cs); + +#if FREERDP_VERSION_MAJOR >= 3 + char* args = freerdp_settings_get_string_writable(rfi->clientContext.context.settings, FreeRDP_RDP2TCPArgs); +#else + char* args = freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_RDP2TCPArgs); +#endif + REMMINA_PLUGIN_DEBUG(RDP2TCP_DVC_CHANNEL_NAME " set to %s",args); + remmina_rdp_load_static_channel_addin(channels, rfi->clientContext.context.settings, RDP2TCP_DVC_CHANNEL_NAME, args); + } + + int vermaj, vermin, verrev; + freerdp_get_version(&vermaj, &vermin, &verrev); + +#if FREERDP_CHECK_VERSION(2, 1, 0) + cs = remmina_plugin_service->file_get_string(remminafile, "timeout"); + if (cs != NULL && cs[0] != '\0') { + const gchar *endptr = NULL; + guint64 val = g_ascii_strtoull(cs, (gchar **)&endptr, 10); + if (val > 600000 || val <= 0) + val = 600000; + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_TcpAckTimeout, (UINT32)val); + } +#endif + + if (remmina_plugin_service->file_get_int(remminafile, "preferipv6", FALSE) ? TRUE : FALSE) + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_PreferIPv6OverIPv4, TRUE); + + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RedirectClipboard, remmina_plugin_service->file_get_int(remminafile, "disableclipboard", FALSE) ? FALSE : TRUE); + + cs = remmina_plugin_service->file_get_string(remminafile, "sharefolder"); + if (cs != NULL && cs[0] != '\0') { + gchar *ccs = g_strdup(cs); + REMMINA_PLUGIN_DEBUG("[Deprecated->migrating] - Old sharefolder %s to \"drive \"", ccs); + if (!remmina_plugin_service->file_get_string(remminafile, "drive")) { + remmina_plugin_service->file_set_string(remminafile, "drive", g_strdup(ccs)); + remmina_plugin_service->file_set_string(remminafile, "sharefolder", NULL); + REMMINA_PLUGIN_DEBUG("[Deprecated->migrated] - drive set to %s", g_strdup(ccs)); + } + g_free(ccs); + //CLPARAM **p; + //size_t count; + //p = remmina_rdp_CommandLineParseCommaSeparatedValuesEx("drive", g_strdup(cs), &count); + //status = freerdp_client_add_device_channel(rfi->clientContext.context.settings, count, p); + //g_free(p); + } + cs = remmina_plugin_service->file_get_string(remminafile, "drive"); + if (cs != NULL && cs[0] != '\0') { + REMMINA_PLUGIN_DEBUG("Redirect directory set to %s", cs); + CLPARAM **p; + size_t count; + + gchar **folders = g_strsplit(cs, ";", -1); + for (i = 0; folders[i] != NULL; i++) { + REMMINA_PLUGIN_DEBUG("Parsing folder %s", folders[i]); + p = remmina_rdp_CommandLineParseCommaSeparatedValuesEx("drive", g_strdup(folders[i]), &count); + status = freerdp_client_add_device_channel(rfi->clientContext.context.settings, count, p); + g_free(p); + } + g_strfreev(folders); + } + + if (remmina_plugin_service->file_get_int(remminafile, "shareprinter", FALSE)) { +#ifdef HAVE_CUPS + REMMINA_PLUGIN_DEBUG("Sharing printers"); + const gchar *po = remmina_plugin_service->file_get_string(remminafile, "printer_overrides"); + if (po && po[0] != 0) { + /* Fallback to remmina code to override print drivers */ + if (cupsEnumDests(CUPS_DEST_FLAGS_NONE, 1000, NULL, 0, 0, remmina_rdp_set_printers, rfi)) + REMMINA_PLUGIN_DEBUG("All printers have been shared"); + else + REMMINA_PLUGIN_DEBUG("Cannot share printers, are there any available?"); + } else { + /* Use libfreerdp code to map all printers */ + CLPARAM *d[1]; + int dcount; + dcount = 1; + d[0] = "printer"; + freerdp_client_add_device_channel(rfi->clientContext.context.settings, dcount, d); + } +#endif /* HAVE_CUPS */ + } + + if (remmina_plugin_service->file_get_int(remminafile, "span", FALSE)) { + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_SpanMonitors, TRUE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_UseMultimon, TRUE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_Fullscreen, TRUE); + remmina_plugin_service->file_set_int(remminafile, "multimon", 1); + } + + if (remmina_plugin_service->file_get_int(remminafile, "multimon", FALSE)) { + guint32 maxwidth = 0; + guint32 maxheight = 0; + gchar *monitorids; + guint32 i; + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_UseMultimon, TRUE); + /* TODO Add an option for this */ + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_ForceMultimon, TRUE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_Fullscreen, TRUE); + + gchar *monitorids_string = g_strdup(remmina_plugin_service->file_get_string(remminafile, "monitorids")); + /* Otherwise we get all the attached monitors + * monitorids may contains desktop orientation values. + * But before we check if there are orientation attributes + */ + if (monitorids_string != NULL && monitorids_string[0] != '\0') { + if (g_strstr_len(monitorids_string, -1, ",") != NULL) { + if (g_strstr_len(monitorids_string, -1, ":") != NULL) { + rdpMonitor *base = (rdpMonitor *)freerdp_settings_get_pointer(rfi->clientContext.context.settings, FreeRDP_MonitorDefArray); + /* We have an ID and an orientation degree */ + gchar **temp_items; + gchar **rot_items; + temp_items = g_strsplit(monitorids_string, ",", -1); + for (i = 0; i < g_strv_length(temp_items); i++) { + rot_items = g_strsplit(temp_items[i], ":", -1); + rdpMonitor *current = &base[atoi(rot_items[0])]; + if (i == 0) + monitorids = g_strdup(rot_items[0]); + else + monitorids = g_strdup_printf("%s,%s", monitorids, rot_items[0]); + current->attributes.orientation = atoi(rot_items[1]); + REMMINA_PLUGIN_DEBUG("Monitor n %d orientation: %d", i, current->attributes.orientation); + } + } else { + monitorids = g_strdup(monitorids_string); + } + } else { + monitorids = g_strdup(monitorids_string); + } + } else { + monitorids = g_strdup(monitorids_string); + } + remmina_rdp_monitor_get(rfi, &monitorids, &maxwidth, &maxheight); + if (monitorids != NULL && monitorids[0] != '\0') { + UINT32 *base = (UINT32 *)freerdp_settings_get_pointer(rfi->clientContext.context.settings, FreeRDP_MonitorIds); + gchar **items; + items = g_strsplit(monitorids, ",", -1); + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_NumMonitorIds, g_strv_length(items)); + REMMINA_PLUGIN_DEBUG("NumMonitorIds: %d", freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_NumMonitorIds)); + for (i = 0; i < g_strv_length(items); i++) { + if (base != NULL){ + UINT32 *current = &base[i]; + *current = atoi(items[i]); + REMMINA_PLUGIN_DEBUG("Added monitor with ID %" PRIu32, *current); + } + } + g_free(monitorids); + g_strfreev(items); + } + if (maxwidth && maxheight) { + REMMINA_PLUGIN_DEBUG("Setting DesktopWidth and DesktopHeight to: %dx%d", maxwidth, maxheight); + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopWidth, maxwidth); + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopHeight, maxheight); + REMMINA_PLUGIN_DEBUG("DesktopWidth and DesktopHeight set to: %dx%d", freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopWidth), freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopHeight)); + } else { + REMMINA_PLUGIN_DEBUG("Cannot set Desktop Size, we are using the previously set values: %dx%d", freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopWidth), freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopHeight)); + } + remmina_plugin_service->protocol_plugin_set_width(gp, freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopWidth)); + remmina_plugin_service->protocol_plugin_set_height(gp, freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopHeight)); + } + + const gchar *sn = remmina_plugin_service->file_get_string(remminafile, "smartcardname"); + if (remmina_plugin_service->file_get_int(remminafile, "sharesmartcard", FALSE) || + (sn != NULL && sn[0] != '\0')) { + RDPDR_SMARTCARD *smartcard; + smartcard = (RDPDR_SMARTCARD *)calloc(1, sizeof(RDPDR_SMARTCARD)); + +#if FREERDP_VERSION_MAJOR >= 3 + RDPDR_DEVICE *sdev; + sdev = &(smartcard->device); +#else + RDPDR_SMARTCARD *sdev; + sdev = smartcard; +#endif + + sdev->Type = RDPDR_DTYP_SMARTCARD; + + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_DeviceRedirection, TRUE); + + if (sn != NULL && sn[0] != '\0') + sdev->Name = _strdup(sn); + + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RedirectSmartCards, TRUE); + + freerdp_device_collection_add(rfi->clientContext.context.settings, (RDPDR_DEVICE *)smartcard); + } + + if (remmina_plugin_service->file_get_int(remminafile, "passwordispin", FALSE)) + /* Option works only combined with Username and Domain, because FreeRDP + * doesn’t know anything about info on smart card */ + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_PasswordIsSmartcardPin, TRUE); + + /* /serial[:<name>[,<path>[,<driver>[,permissive]]]] */ + if (remmina_plugin_service->file_get_int(remminafile, "shareserial", FALSE)) { + RDPDR_SERIAL *serial; + serial = (RDPDR_SERIAL *)calloc(1, sizeof(RDPDR_SERIAL)); + +#if FREERDP_VERSION_MAJOR >= 3 + RDPDR_DEVICE *sdev; + sdev = &(serial->device); +#else + RDPDR_SERIAL *sdev; + sdev = serial; +#endif + + sdev->Type = RDPDR_DTYP_SERIAL; + + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_DeviceRedirection, TRUE); + + const gchar *sn = remmina_plugin_service->file_get_string(remminafile, "serialname"); + if (sn != NULL && sn[0] != '\0') + sdev->Name = _strdup(sn); + + const gchar *sd = remmina_plugin_service->file_get_string(remminafile, "serialdriver"); + if (sd != NULL && sd[0] != '\0') + serial->Driver = _strdup(sd); + + const gchar *sp = remmina_plugin_service->file_get_string(remminafile, "serialpath"); + if (sp != NULL && sp[0] != '\0') + serial->Path = _strdup(sp); + + if (remmina_plugin_service->file_get_int(remminafile, "serialpermissive", FALSE)) + serial->Permissive = _strdup("permissive"); + + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RedirectSerialPorts, TRUE); + + freerdp_device_collection_add(rfi->clientContext.context.settings, (RDPDR_DEVICE *)serial); + } + + if (remmina_plugin_service->file_get_int(remminafile, "shareparallel", FALSE)) { + RDPDR_PARALLEL *parallel; + parallel = (RDPDR_PARALLEL *)calloc(1, sizeof(RDPDR_PARALLEL)); + +#if FREERDP_VERSION_MAJOR >= 3 + RDPDR_DEVICE *pdev; + pdev = &(parallel->device); +#else + RDPDR_PARALLEL *pdev; + pdev = parallel; +#endif + + pdev->Type = RDPDR_DTYP_PARALLEL; + + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_DeviceRedirection, TRUE); + + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RedirectParallelPorts, TRUE); + + const gchar *pn = remmina_plugin_service->file_get_string(remminafile, "parallelname"); + if (pn != NULL && pn[0] != '\0') + pdev->Name = _strdup(pn); + const gchar *dp = remmina_plugin_service->file_get_string(remminafile, "parallelpath"); + if (dp != NULL && dp[0] != '\0') + parallel->Path = _strdup(dp); + + freerdp_device_collection_add(rfi->clientContext.context.settings, (RDPDR_DEVICE *)parallel); + } + + /** + * multitransport enables RDP8 UDP support + */ + if (remmina_plugin_service->file_get_int(remminafile, "multitransport", FALSE)) { + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_DeviceRedirection, TRUE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_SupportMultitransport, TRUE); + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_MultitransportFlags, + (TRANSPORT_TYPE_UDP_FECR | TRANSPORT_TYPE_UDP_FECL | TRANSPORT_TYPE_UDP_PREFERRED)); + } else { + freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_MultitransportFlags, 0); + } + + /* If needed, force interactive authentication by deleting all authentication fields, + * forcing libfreerdp to call our callbacks for authentication. + * This usually happens from a second attempt of connection, never on the 1st one. */ + if (rfi->attempt_interactive_authentication) { + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_Username, NULL); + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_Password, NULL); + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_Domain, NULL); + + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_GatewayDomain, NULL); + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_GatewayUsername, NULL); + freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_GatewayPassword, NULL); + + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GatewayUseSameCredentials, FALSE); + } + + gboolean orphaned; + + if (!freerdp_connect(rfi->clientContext.context.instance)) { + orphaned = (GET_PLUGIN_DATA(rfi->protocol_widget) == NULL); + if (!orphaned) { + UINT32 e; + + e = freerdp_get_last_error(&rfi->clientContext.context); + + switch (e) { + case FREERDP_ERROR_AUTHENTICATION_FAILED: + case STATUS_LOGON_FAILURE: // wrong return code from FreeRDP introduced at the end of July 2016? (fixed with b86c0ba) +#ifdef FREERDP_ERROR_CONNECT_LOGON_FAILURE + case FREERDP_ERROR_CONNECT_LOGON_FAILURE: +#endif + /* Logon failure, will retry with interactive authentication */ + rfi->attempt_interactive_authentication = TRUE; + break; + case STATUS_ACCOUNT_LOCKED_OUT: +#ifdef FREERDP_ERROR_CONNECT_ACCOUNT_LOCKED_OUT + case FREERDP_ERROR_CONNECT_ACCOUNT_LOCKED_OUT: +#endif + remmina_plugin_service->protocol_plugin_set_error(gp, _("Could not access the RDP server “%s”.\nAccount locked out."), + freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname)); + break; + case STATUS_ACCOUNT_EXPIRED: +#ifdef FREERDP_ERROR_CONNECT_ACCOUNT_EXPIRED + case FREERDP_ERROR_CONNECT_ACCOUNT_EXPIRED: +#endif + remmina_plugin_service->protocol_plugin_set_error(gp, _("Could not access the RDP server “%s”.\nAccount expired."), + freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname)); + break; + case STATUS_PASSWORD_EXPIRED: +#ifdef FREERDP_ERROR_CONNECT_PASSWORD_EXPIRED + case FREERDP_ERROR_CONNECT_PASSWORD_EXPIRED: +#endif + remmina_plugin_service->protocol_plugin_set_error(gp, _("Could not access the RDP server “%s”.\nPassword expired."), + freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname)); + break; + case STATUS_ACCOUNT_DISABLED: +#ifdef FREERDP_ERROR_CONNECT_ACCOUNT_DISABLED + case FREERDP_ERROR_CONNECT_ACCOUNT_DISABLED: +#endif + remmina_plugin_service->protocol_plugin_set_error(gp, _("Could not access the RDP server “%s”.\nAccount disabled."), + freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname)); + break; +#ifdef FREERDP_ERROR_SERVER_INSUFFICIENT_PRIVILEGES + /* https://msdn.microsoft.com/en-us/library/ee392247.aspx */ + case FREERDP_ERROR_SERVER_INSUFFICIENT_PRIVILEGES: + remmina_plugin_service->protocol_plugin_set_error(gp, _("Could not access the RDP server “%s”.\nInsufficient user privileges."), + freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname)); + break; +#endif + case STATUS_ACCOUNT_RESTRICTION: +#ifdef FREERDP_ERROR_CONNECT_ACCOUNT_RESTRICTION + case FREERDP_ERROR_CONNECT_ACCOUNT_RESTRICTION: +#endif + remmina_plugin_service->protocol_plugin_set_error(gp, _("Could not access the RDP server “%s”.\nAccount restricted."), + freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname)); + break; + + case STATUS_PASSWORD_MUST_CHANGE: +#ifdef FREERDP_ERROR_CONNECT_PASSWORD_MUST_CHANGE + case FREERDP_ERROR_CONNECT_PASSWORD_MUST_CHANGE: +#endif + remmina_plugin_service->protocol_plugin_set_error(gp, _("Could not access the RDP server “%s”.\nChange user password before connecting."), + freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname)); + break; + + case FREERDP_ERROR_CONNECT_FAILED: + remmina_plugin_service->protocol_plugin_set_error(gp, _("Lost connection to the RDP server “%s”."), freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname)); + break; + case FREERDP_ERROR_DNS_NAME_NOT_FOUND: + remmina_plugin_service->protocol_plugin_set_error(gp, _("Could not find the address for the RDP server “%s”."), freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname)); + break; + case FREERDP_ERROR_TLS_CONNECT_FAILED: + remmina_plugin_service->protocol_plugin_set_error(gp, + _("Could not connect to the RDP server “%s” via TLS. See the DEBUG traces from a terminal for more information."), freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname)); + break; + case FREERDP_ERROR_SECURITY_NEGO_CONNECT_FAILED: + // TRANSLATORS: the placeholder may be either an IP/FQDN or a server hostname + remmina_plugin_service->protocol_plugin_set_error(gp, _("Unable to establish a connection to the RDP server “%s”. Check “Security protocol negotiation”."), freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname)); + break; +#ifdef FREERDP_ERROR_POST_CONNECT_FAILED + case FREERDP_ERROR_POST_CONNECT_FAILED: + /* remmina_rdp_post_connect() returned FALSE to libfreerdp. We saved the error on rfi->postconnect_error */ + switch (rfi->postconnect_error) { + case REMMINA_POSTCONNECT_ERROR_OK: + /* We should never come here */ + remmina_plugin_service->protocol_plugin_set_error(gp, _("Cannot connect to the RDP server “%s”."), freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname)); + break; + case REMMINA_POSTCONNECT_ERROR_GDI_INIT: + remmina_plugin_service->protocol_plugin_set_error(gp, _("Could not start libfreerdp-gdi.")); + break; + case REMMINA_POSTCONNECT_ERROR_NO_H264: + remmina_plugin_service->protocol_plugin_set_error(gp, _("You requested a H.264 GFX mode for the server “%s”, but your libfreerdp does not support H.264. Please use a non-AVC colour depth setting."), freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname)); + break; + } + break; +#endif +#ifdef FREERDP_ERROR_SERVER_DENIED_CONNECTION + case FREERDP_ERROR_SERVER_DENIED_CONNECTION: + remmina_plugin_service->protocol_plugin_set_error(gp, _("The “%s” server refused the connection."), freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname)); + break; +#endif + case 0x800759DB: + // E_PROXY_NAP_ACCESSDENIED https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-tsgu/84cd92e4-592c-4219-95d8-18021ac654b0 + remmina_plugin_service->protocol_plugin_set_error(gp, _("The Remote Desktop Gateway “%s” denied the user “%s\\%s” access due to policy."), + freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_GatewayHostname), freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_GatewayDomain), freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_GatewayUsername)); + break; + + case FREERDP_ERROR_CONNECT_NO_OR_MISSING_CREDENTIALS: + rfi->user_cancelled = TRUE; + break; + + default: + g_printf("libfreerdp returned code is %08X\n", e); + remmina_plugin_service->protocol_plugin_set_error(gp, _("Cannot connect to the “%s” RDP server."), freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname)); + break; + } + } + + return FALSE; + } + + if (GET_PLUGIN_DATA(rfi->protocol_widget) == NULL) orphaned = true; else orphaned = false; + if (!orphaned && freerdp_get_last_error(&rfi->clientContext.context) == FREERDP_ERROR_SUCCESS && !rfi->user_cancelled) + remmina_rdp_main_loop(gp); + + return TRUE; +} + +static void rfi_uninit(rfContext *rfi) +{ + freerdp *instance; + + instance = rfi->clientContext.context.instance; + + if (rfi->remmina_plugin_thread) { + rfi->thread_cancelled = TRUE; // Avoid all rf_queue function to run + pthread_cancel(rfi->remmina_plugin_thread); + if (rfi->remmina_plugin_thread) + pthread_join(rfi->remmina_plugin_thread, NULL); + } + + if (instance) { + if (rfi->connected) { + freerdp_abort_connect_context(&rfi->clientContext.context); + rfi->connected = false; + } + } + + if (instance) { + RDP_CLIENT_ENTRY_POINTS *pEntryPoints = instance->pClientEntryPoints; + if (pEntryPoints) + IFCALL(pEntryPoints->GlobalUninit); + free(instance->pClientEntryPoints); + freerdp_context_free(instance); /* context is rfContext* rfi */ + freerdp_free(instance); /* This implicitly frees instance->context and rfi is no longer valid */ + } +} + +static gboolean complete_cleanup_on_main_thread(gpointer data) +{ + TRACE_CALL(__func__); + + gboolean orphaned; + rfContext *rfi = (rfContext *)data; + RemminaProtocolWidget *gp; + + remmina_rdp_clipboard_free(rfi); + + gdi_free(rfi->clientContext.context.instance); + + gp = rfi->protocol_widget; + if (GET_PLUGIN_DATA(gp) == NULL) orphaned = true; else orphaned = false; + + remmina_rdp_cliprdr_detach_owner(gp); + if (!orphaned) remmina_rdp_event_uninit(gp); + + if (!orphaned) g_object_steal_data(G_OBJECT(gp), "plugin-data"); + + rfi_uninit(rfi); + + /* Notify the RemminaProtocolWidget that we closed our connection, and the GUI interface + * can be removed */ + if (!orphaned) + remmina_plugin_service->protocol_plugin_signal_connection_closed(gp); + + return G_SOURCE_REMOVE; +} + +static gpointer remmina_rdp_main_thread(gpointer data) +{ + TRACE_CALL(__func__); + RemminaProtocolWidget *gp; + rfContext *rfi; + + pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); + CANCEL_ASYNC + + gp = (RemminaProtocolWidget *)data; + + rfi = GET_PLUGIN_DATA(gp); + + rfi->attempt_interactive_authentication = FALSE; + do + remmina_rdp_main(gp); + while (!remmina_plugin_service->protocol_plugin_has_error(gp) && rfi->attempt_interactive_authentication == TRUE && !rfi->user_cancelled); + + rfi->remmina_plugin_thread = 0; + + /* cleanup */ + g_idle_add(complete_cleanup_on_main_thread, (gpointer)rfi); + + + return NULL; +} + +static void remmina_rdp_init(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + freerdp *instance; + rfContext *rfi; + + instance = freerdp_new(); + instance->PreConnect = remmina_rdp_pre_connect; + instance->PostConnect = remmina_rdp_post_connect; + instance->PostDisconnect = remmina_rdp_post_disconnect; + //instance->VerifyCertificate = remmina_rdp_verify_certificate; + instance->VerifyCertificateEx = remmina_rdp_verify_certificate_ex; + //instance->VerifyChangedCertificate = remmina_rdp_verify_changed_certificate; + instance->VerifyChangedCertificateEx = remmina_rdp_verify_changed_certificate_ex; +#if FREERDP_VERSION_MAJOR >= 3 + instance->AuthenticateEx = remmina_rdp_authenticate_ex; + instance->ChooseSmartcard = remmina_rdp_choose_smartcard; + instance->GetAccessToken = remmina_rdp_get_access_token; + instance->LoadChannels = freerdp_client_load_channels; + instance->PresentGatewayMessage = remmina_rdp_present_gateway_message; + instance->LogonErrorInfo = remmina_rdp_logon_error_info; + instance->RetryDialog = remmina_rdp_retry_dialog; + instance->PostFinalDisconnect = remmina_rdp_post_final_disconnect; +#else + instance->Authenticate = remmina_rdp_authenticate; + instance->GatewayAuthenticate = remmina_rdp_gw_authenticate; +#endif + + instance->ContextSize = sizeof(rfContext); + freerdp_context_new(instance); + rfi = (rfContext *)instance->context; + + g_object_set_data_full(G_OBJECT(gp), "plugin-data", rfi, free); + + rfi->protocol_widget = gp; + rfi->clientContext.context.settings = instance->context->settings; + rfi->connected = false; + rfi->is_reconnecting = false; + rfi->stop_reconnecting_requested = false; + rfi->user_cancelled = FALSE; + rfi->last_x = 0; + rfi->last_y = 0; + + freerdp_register_addin_provider(freerdp_channels_load_static_addin_entry, 0); + + remmina_rdp_event_init(gp); +} + +static gboolean remmina_rdp_open_connection(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + rfContext *rfi = GET_PLUGIN_DATA(gp); + RemminaFile *remminafile; + const gchar *profile_name, *p; + gchar thname[16], c; + gint nthname = 0; + + rfi->scale = remmina_plugin_service->remmina_protocol_widget_get_current_scale_mode(gp); + + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + + + if (pthread_create(&rfi->remmina_plugin_thread, NULL, remmina_rdp_main_thread, gp)) { + remmina_plugin_service->protocol_plugin_set_error(gp, "%s", + "Could not start pthread."); + + rfi->remmina_plugin_thread = 0; + + return FALSE; + } + + /* Generate a thread name to be used with pthread_setname_np() for debugging */ + profile_name = remmina_plugin_service->file_get_string(remminafile, "name"); + p = profile_name; + strcpy(thname, "RemmRDP:"); + if (p) { + nthname = strlen(thname); + while ((c = *p) != 0 && nthname < sizeof(thname) - 1) { + if (isalnum(c)) + thname[nthname++] = c; + p++; + } + } else { + strcat(thname, "<NONAM>"); + nthname = strlen(thname); + } + thname[nthname] = 0; +#if defined(__linux__) + pthread_setname_np(rfi->remmina_plugin_thread, thname); +#elif defined(__FreeBSD__) + pthread_set_name_np(rfi->remmina_plugin_thread, thname); +#endif + + return TRUE; +} + +static gboolean remmina_rdp_close_connection(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + + REMMINA_PLUGIN_DEBUG("Requesting to close the connection"); + RemminaPluginRdpEvent rdp_event = { 0 }; + rfContext *rfi = GET_PLUGIN_DATA(gp); + + if (!remmina_plugin_service->is_main_thread()) + g_warning("WARNING: %s called on a subthread, which may not work or crash Remmina.", __func__); + + if (rfi && !rfi->connected) { + /* libfreerdp is attempting to connect, we cannot interrupt our main thread + * in the connect phase. + * So we remove "plugin-data" from gp, so our rfi remains "orphan" + */ + remmina_rdp_event_uninit(gp); + g_object_steal_data(G_OBJECT(gp), "plugin-data"); + remmina_plugin_service->protocol_plugin_signal_connection_closed(gp); + return FALSE; + } + + + if (rfi && rfi->clipboard.srv_clip_data_wait == SCDW_BUSY_WAIT) { + REMMINA_PLUGIN_DEBUG("[RDP] requesting clipboard transfer to abort"); + /* Allow clipboard transfer from server to terminate */ + rfi->clipboard.srv_clip_data_wait = SCDW_ABORTING; + usleep(100000); + } + + if (rfi->is_reconnecting) { + /* Special case: window closed when attempting to reconnect */ + rfi->stop_reconnecting_requested = TRUE; + return FALSE; + } + + rdp_event.type = REMMINA_RDP_EVENT_DISCONNECT; + remmina_rdp_event_event_push(gp, &rdp_event); + + return FALSE; +} + +static gboolean remmina_rdp_query_feature(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature) +{ + TRACE_CALL(__func__); + return TRUE; +} + +static void remmina_rdp_call_feature(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature) +{ + TRACE_CALL(__func__); + rfContext *rfi = GET_PLUGIN_DATA(gp); + + switch (feature->id) { + case REMMINA_RDP_FEATURE_UNFOCUS: + remmina_rdp_event_unfocus(gp); + break; + + case REMMINA_RDP_FEATURE_SCALE: + if (rfi) { + rfi->scale = remmina_plugin_service->remmina_protocol_widget_get_current_scale_mode(gp); + remmina_rdp_event_update_scale(gp); + } else { + REMMINA_PLUGIN_DEBUG("Remmina RDP plugin warning: Null value for rfi by REMMINA_RDP_FEATURE_SCALE"); + } + break; + + case REMMINA_RDP_FEATURE_MULTIMON: + if (rfi) { + RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + if (remmina_plugin_service->file_get_int(remminafile, "multimon", FALSE)) { + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_UseMultimon, TRUE); + /* TODO Add an option for this */ + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_ForceMultimon, TRUE); + freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_Fullscreen, TRUE); + remmina_rdp_event_send_delayed_monitor_layout(gp); + } + } else { + REMMINA_PLUGIN_DEBUG("Remmina RDP plugin warning: Null value for rfi by REMMINA_RDP_FEATURE_MULTIMON"); + } + break; + + case REMMINA_RDP_FEATURE_DYNRESUPDATE: + break; + + case REMMINA_RDP_FEATURE_TOOL_REFRESH: + if (rfi) + gtk_widget_queue_draw_area(rfi->drawing_area, 0, 0, + remmina_plugin_service->protocol_plugin_get_width(gp), + remmina_plugin_service->protocol_plugin_get_height(gp)); + else + REMMINA_PLUGIN_DEBUG("Remmina RDP plugin warning: Null value for rfi by REMMINA_RDP_FEATURE_TOOL_REFRESH"); + break; + + case REMMINA_RDP_FEATURE_TOOL_SENDCTRLALTDEL: + remmina_rdp_send_ctrlaltdel(gp); + break; + + default: + break; + } +} + +/* Send a keystroke to the plugin window */ +static void remmina_rdp_keystroke(RemminaProtocolWidget *gp, const guint keystrokes[], const gint keylen) +{ + TRACE_CALL(__func__); + rfContext *rfi = GET_PLUGIN_DATA(gp); + + remmina_plugin_service->protocol_plugin_send_keys_signals(rfi->drawing_area, + keystrokes, keylen, GDK_KEY_PRESS | GDK_KEY_RELEASE); + return; +} + +static gboolean remmina_rdp_get_screenshot(RemminaProtocolWidget *gp, RemminaPluginScreenshotData *rpsd) +{ + rfContext *rfi = GET_PLUGIN_DATA(gp); + rdpGdi *gdi; + size_t szmem; + + UINT32 bytesPerPixel; + UINT32 bitsPerPixel; + + if (!rfi) + return FALSE; + + gdi = ((rdpContext *)rfi)->gdi; + +#if FREERDP_VERSION_MAJOR >= 3 + bytesPerPixel = FreeRDPGetBytesPerPixel(gdi->hdc->format); + bitsPerPixel = FreeRDPGetBitsPerPixel(gdi->hdc->format); +#else + bytesPerPixel = GetBytesPerPixel(gdi->hdc->format); + bitsPerPixel = GetBitsPerPixel(gdi->hdc->format); +#endif + + /** @todo we should lock FreeRDP subthread to update rfi->primary_buffer, rfi->gdi and w/h, + * from here to memcpy, but… how ? */ + + szmem = gdi->width * gdi->height * bytesPerPixel; + + REMMINA_PLUGIN_DEBUG("allocating %zu bytes for a full screenshot", szmem); + rpsd->buffer = malloc(szmem); + if (!rpsd->buffer) { + REMMINA_PLUGIN_DEBUG("could not set aside %zu bytes for a full screenshot", szmem); + return FALSE; + } + rpsd->width = gdi->width; + rpsd->height = gdi->height; + rpsd->bitsPerPixel = bitsPerPixel; + rpsd->bytesPerPixel = bytesPerPixel; + + memcpy(rpsd->buffer, gdi->primary_buffer, szmem); + + /* Returning TRUE instruct also the caller to deallocate rpsd->buffer */ + return TRUE; +} + +/* Array of key/value pairs for colour depths */ +static gpointer colordepth_list[] = +{ + /* 1st one is the default in a new install */ + "99", N_("Automatic (32 bpp) (Server chooses its best format)"), + "66", N_("GFX AVC444 (32 bpp)"), + "65", N_("GFX AVC420 (32 bpp)"), + "64", N_("GFX RFX (32 bpp)"), + "63", N_("GFX RFX Progressive (32 bpp)"), + "0", N_("RemoteFX (32 bpp)"), + "32", N_("True colour (32 bpp)"), + "24", N_("True colour (24 bpp)"), + "16", N_("High colour (16 bpp)"), + "15", N_("High colour (15 bpp)"), + "8", N_("256 colours (8 bpp)"), + NULL +}; + +/* Array of key/value pairs for the FreeRDP logging level */ +static gpointer log_level[] = +{ + "INFO", "INFO", + "FATAL", "FATAL", + "ERROR", "ERROR", + "WARN", "WARN", + "DEBUG", "DEBUG", + "TRACE", "TRACE", + "OFF", "OFF", + NULL +}; + + +/* Array of key/value pairs for quality selection */ +static gpointer quality_list[] = +{ + "0", N_("Poor (fastest)"), + "1", N_("Medium"), + "2", N_("Good"), + "9", N_("Best (slowest)"), + NULL +}; + +/* Array of key/value pairs for quality selection */ +static gpointer network_list[] = +{ + "none", N_("None"), + "autodetect", N_("Auto-detect"), + "modem", N_("Modem"), + "broadband-low", N_("Low performance broadband"), + "satellite", N_("Satellite"), + "broadband-high", N_("High performance broadband"), + "wan", N_("WAN"), + "lan", N_("LAN"), + NULL +}; + +/* Array of key/value pairs for sound options */ +static gpointer sound_list[] = +{ + "off", N_("Off"), + "local", N_("Local"), + "remote", N_("Remote"), + NULL +}; + +/* Array of key/value pairs for security */ +static gpointer security_list[] = +{ + "", N_("Automatic negotiation"), + "nla", N_("NLA protocol security"), + "tls", N_("TLS protocol security"), + "rdp", N_("RDP protocol security"), + "ext", N_("NLA extended protocol security"), + NULL +}; + +/* Array of key/value pairs for mouse movement */ +static gpointer mouse_jitter_list[] = +{ + "No", N_("No"), + "60", N_("Every 1 min"), + "180", N_("Every 3 min"), + "300", N_("Every 5 min"), + "600", N_("Every 10 min"), + NULL +}; + +static gpointer gwtransp_list[] = +{ + "http", "HTTP", + "rpc", "RPC", + "auto", "Auto", + NULL +}; + +static gpointer tls_seclevel[] = +{ + "", N_("Default"), + "0", N_("0 — Windows 7 compatible"), + "1", N_("1"), + "2", N_("2"), + "3", N_("3"), + "4", N_("4"), + "5", N_("5"), + NULL +}; + +static gchar clientbuild_list[] = + N_("2600 (Windows XP), 7601 (Windows Vista/7), 9600 (Windows 8 and newer)"); + +static gchar clientbuild_tooltip[] = + N_("Used i.a. by terminal services in a smart card channel to distinguish client capabilities:\n" + " • < 4034: Windows XP base smart card functions\n" + " • 4034-7064: Windows Vista/7: SCardReadCache(),\n" + " SCardWriteCache(), SCardGetTransmitCount()\n" + " • >= 7065: Windows 8 and newer: SCardGetReaderIcon(),\n" + " SCardGetDeviceTypeId()"); + +static gchar microphone_tooltip[] = + N_("Options for redirection of audio input:\n" + " • [sys:<sys>,][dev:<dev>,][format:<format>,][rate:<rate>,]\n" + " [channel:<channel>] Audio input (microphone)\n" + " • sys:pulse\n" + " • format:1\n" + " • sys:oss,dev:1,format:1\n" + " • sys:alsa"); + +static gchar audio_tooltip[] = + N_("Options for redirection of audio output:\n" + " • [sys:<sys>,][dev:<dev>,][format:<format>,][rate:<rate>,]\n" + " [channel:<channel>] Audio output\n" + " • sys:pulse\n" + " • format:1\n" + " • sys:oss,dev:1,format:1\n" + " • sys:alsa"); + + +static gchar usb_tooltip[] = + N_("Options for redirection of USB device:\n" + " • [dbg,][id:<vid>:<pid>#…,][addr:<bus>:<addr>#…,][auto]\n" + " • auto\n" + " • id:054c:0268#4669:6e6b,addr:04:0c"); + +static gchar timeout_tooltip[] = + N_("Advanced setting for high latency links:\n" + "Adjusts the connection timeout. Use if your connection times out.\n" + "The highest possible value is 600000 ms (10 minutes).\n"); + +static gchar network_tooltip[] = + N_("Performance optimisations based on the network connection type:\n" + "Using auto-detection is advised.\n" + "If “Auto-detect” fails, choose the most appropriate option in the list.\n"); + +static gchar monitorids_tooltip[] = + N_("Comma-separated list of monitor IDs and desktop orientations:\n" + " • [<id>:<orientation-in-degrees>,]\n" + " • 0,1,2,3\n" + " • 0:270,1:90\n" + "Orientations are specified in degrees, valid values are:\n" + " • 0 (landscape)\n" + " • 90 (portrait)\n" + " • 180 (landscape flipped)\n" + " • 270 (portrait flipped)\n" + "\n"); + +static gchar drive_tooltip[] = + N_("Redirect directory <path> as named share <name>.\n" + " • <name>,<fullpath>[;<name>,<fullpath>[;…]]\n" + " • MyHome,/home/remminer\n" + " • /home/remminer\n" + " • MyHome,/home/remminer;SomePath,/path/to/somepath\n" + "Hotplug support is enabled with:\n" + " • hotplug,*\n" + "\n"); + +/* 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_rdp_basic_settings[] = +{ + { REMMINA_PROTOCOL_SETTING_TYPE_SERVER, "server", NULL, FALSE, NULL, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "username", N_("Username"), FALSE, NULL, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD, "password", N_("Password"), FALSE, NULL, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "domain", N_("Domain"), FALSE, NULL, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "drive", N_("Share folder"), FALSE, NULL, drive_tooltip, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "restricted-admin", N_("Restricted admin mode"), FALSE, NULL, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "pth", N_("Password hash"), FALSE, NULL, N_("Restricted admin mode password hash"), NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "left-handed", N_("Left-handed mouse support"), TRUE, NULL, N_("Swap left and right mouse buttons for left-handed mouse support"), NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disable-smooth-scrolling", N_("Disable smooth scrolling"), TRUE, NULL, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "multimon", N_("Enable multi monitor"), TRUE, NULL, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "span", N_("Span screen over multiple monitors"), TRUE, NULL, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "monitorids", N_("List monitor IDs"), FALSE, NULL, monitorids_tooltip, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_RESOLUTION, "resolution", NULL, FALSE, NULL, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "colordepth", N_("Colour depth"), FALSE, colordepth_list, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "network", N_("Network connection type"), FALSE, network_list, network_tooltip, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_KEYMAP, "keymap", NULL, 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_rdp_advanced_settings[] = +{ + { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "quality", N_("Quality"), FALSE, quality_list, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "security", N_("Security protocol negotiation"), FALSE, security_list, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "gwtransp", N_("Gateway transport type"), FALSE, gwtransp_list, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "tls-seclevel", N_("TLS Security Level"), FALSE, tls_seclevel, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "freerdp_log_level", N_("FreeRDP log level"), FALSE, log_level, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "freerdp_log_filters", N_("FreeRDP log filters"), FALSE, NULL, N_("tag:level[,tag:level[,…]]") }, + { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "sound", N_("Audio output mode"), FALSE, sound_list, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "audio-output", N_("Redirect local audio output"), TRUE, NULL, audio_tooltip }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "microphone", N_("Redirect local microphone"), TRUE, NULL, microphone_tooltip }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "timeout", N_("Connection timeout in ms"), TRUE, NULL, timeout_tooltip }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "gateway_server", N_("Remote Desktop Gateway server"), FALSE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "gateway_username", N_("Remote Desktop Gateway username"), FALSE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD, "gateway_password", N_("Remote Desktop Gateway password"), FALSE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "gateway_domain", N_("Remote Desktop Gateway domain"), FALSE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "clientname", N_("Client name"), FALSE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_COMBO, "clientbuild", N_("Client build"), FALSE, clientbuild_list, clientbuild_tooltip }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "exec", N_("Start-up program"), FALSE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "execpath", N_("Start-up path"), FALSE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "loadbalanceinfo", N_("Load balance info"), FALSE, NULL, NULL }, + // TRANSLATORS: Do not use typographic quotation marks, these must stay as "double quote", also know as “Typewriter ("programmer's") quote, ambidextrous.” + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "printer_overrides", N_("Override printer drivers"), FALSE, NULL, N_("\"Samsung_CLX-3300_Series\":\"Samsung CLX-3300 Series PS\";\"Canon MF410\":\"Canon MF410 Series UFR II\"") }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "usb", N_("USB device redirection"), TRUE, NULL, usb_tooltip }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "serialname", N_("Local serial name"), FALSE, NULL, N_("COM1, COM2, etc.") }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "serialdriver", N_("Local serial driver"), FALSE, NULL, N_("Serial") }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "serialpath", N_("Local serial path"), FALSE, NULL, N_("/dev/ttyS0, /dev/ttyS1, etc.") }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "parallelname", N_("Local parallel name"), FALSE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "parallelpath", N_("Local parallel device"), FALSE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "smartcardname", N_("Name of smart card"), FALSE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "dvc", N_("Dynamic virtual channel"), FALSE, NULL, N_("<channel>[,<options>]") }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "vc", N_("Static virtual channel"), FALSE, NULL, N_("<channel>[,<options>]") }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "rdp2tcp", N_("TCP redirection"), FALSE, NULL, N_("/PATH/TO/rdp2tcp") }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "rdp_reconnect_attempts", N_("Reconnect attempts number"), FALSE, NULL, N_("The maximum number of reconnect attempts upon an RDP disconnect (default: 20)") }, + { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "rdp_mouse_jitter", N_("Move mouse when connection is idle"), FALSE, mouse_jitter_list, NULL }, + + { REMMINA_PROTOCOL_SETTING_TYPE_ASSISTANCE, "assistance_mode", N_("Attempt to connect in assistance mode"), TRUE, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "preferipv6", N_("Prefer IPv6 AAAA record over IPv4 A record"), TRUE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "shareprinter", N_("Share printers"), TRUE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "shareserial", N_("Share serial ports"), TRUE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "serialpermissive", N_("(SELinux) permissive mode for serial ports"), TRUE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "shareparallel", N_("Share parallel ports"), TRUE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "sharesmartcard", N_("Share a smart card"), TRUE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disableclipboard", N_("Turn off clipboard sync"), TRUE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "cert_ignore", N_("Ignore certificate"), TRUE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "old-license", N_("Use the old license workflow"), TRUE, NULL, N_("It disables CAL and hwId is set to 0") }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disablepasswordstoring", N_("Forget passwords after use"), TRUE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "console", N_("Attach to console (2003/2003 R2)"), TRUE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disable_fastpath", N_("Turn off fast-path"), TRUE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "gateway_usage", N_("Server detection using Remote Desktop Gateway"), TRUE, NULL, NULL }, +#if defined(PROXY_TYPE_IGNORE) + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "useproxyenv", N_("Use system proxy settings"), TRUE, NULL, NULL }, +#endif + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disableautoreconnect", N_("Turn off automatic reconnection"), TRUE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "relax-order-checks", N_("Relax order checks"), TRUE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "glyph-cache", N_("Glyph cache"), TRUE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "multitransport", N_("Enable multitransport protocol (UDP)"), TRUE, NULL, N_("Using the UDP protocol may improve performance") }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "base-cred-for-gw", N_("Use base credentials for gateway too"), TRUE, NULL, NULL }, +#if FREERDP_CHECK_VERSION(2, 3, 1) + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "websockets", N_("Enable Gateway websockets support"), TRUE, NULL, NULL }, +#endif + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "no-suppress", N_("Update framebuffer even when not visible"), TRUE, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, FALSE, NULL, NULL } +}; + +/* Array for available features. + * The last element of the array must be REMMINA_PROTOCOL_FEATURE_TYPE_END. */ +static const RemminaProtocolFeature remmina_rdp_features[] = +{ + { REMMINA_PROTOCOL_FEATURE_TYPE_PREF, REMMINA_RDP_FEATURE_VIEWONLY, GINT_TO_POINTER(REMMINA_PROTOCOL_FEATURE_PREF_CHECK), "viewonly", + N_("View only") }, + { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, REMMINA_RDP_FEATURE_TOOL_REFRESH, N_("Refresh"), NULL, NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_SCALE, REMMINA_RDP_FEATURE_SCALE, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_DYNRESUPDATE, REMMINA_RDP_FEATURE_DYNRESUPDATE, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_MULTIMON, REMMINA_RDP_FEATURE_MULTIMON, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, REMMINA_RDP_FEATURE_TOOL_SENDCTRLALTDEL, N_("Send Ctrl+Alt+Delete"), NULL, NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_UNFOCUS, REMMINA_RDP_FEATURE_UNFOCUS, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_END, 0, NULL, NULL, NULL } +}; + +/* This will be filled with version info string */ +static char remmina_plugin_rdp_version[256]; + +/* Protocol plugin definition and features */ +static RemminaProtocolPlugin remmina_rdp = +{ + REMMINA_PLUGIN_TYPE_PROTOCOL, // Type + "RDP", // Name + N_("RDP - Remote Desktop Protocol"), // Description + GETTEXT_PACKAGE, // Translation domain + remmina_plugin_rdp_version, // Version number + "org.remmina.Remmina-rdp-symbolic", // Icon for normal connection + "org.remmina.Remmina-rdp-ssh-symbolic", // Icon for SSH connection + remmina_rdp_basic_settings, // Array for basic settings + remmina_rdp_advanced_settings, // Array for advanced settings + REMMINA_PROTOCOL_SSH_SETTING_TUNNEL, // SSH settings type + remmina_rdp_features, // Array for available features + remmina_rdp_init, // Plugin initialization + remmina_rdp_open_connection, // Plugin open connection + remmina_rdp_close_connection, // Plugin close connection + remmina_rdp_query_feature, // Query for available features + remmina_rdp_call_feature, // Call a feature + remmina_rdp_keystroke, // Send a keystroke + remmina_rdp_get_screenshot, // Screenshot + remmina_rdp_event_on_map, // RCW map event + remmina_rdp_event_on_unmap // RCW unmap event +}; + +/* File plugin definition and features */ +static RemminaFilePlugin remmina_rdpf = +{ + REMMINA_PLUGIN_TYPE_FILE, // Type + "RDPF", // Name + N_("RDP - RDP File Handler"), // Description + GETTEXT_PACKAGE, // Translation domain + remmina_plugin_rdp_version, // Version number + remmina_rdp_file_import_test, // Test import function + remmina_rdp_file_import, // Import function + remmina_rdp_file_export_test, // Test export function + remmina_rdp_file_export, // Export function + NULL +}; + +/* Preferences plugin definition and features */ +static RemminaPrefPlugin remmina_rdps = +{ + REMMINA_PLUGIN_TYPE_PREF, // Type + "RDPS", // Name + N_("RDP - Preferences"), // Description + GETTEXT_PACKAGE, // Translation domain + remmina_plugin_rdp_version, // Version number + "RDP", // Label + remmina_rdp_settings_new // Preferences body function +}; + +static char *buildconfig_strstr(const char *bc, const char *option) +{ + TRACE_CALL(__func__); + + char *p, *n; + + p = strcasestr(bc, option); + if (p == NULL) + return NULL; + + if (p > bc && *(p - 1) > ' ') + return NULL; + + n = p + strlen(option); + if (*n > ' ') + return NULL; + + return p; +} + +G_MODULE_EXPORT gboolean remmina_plugin_entry(RemminaPluginService *service) +{ + int vermaj, vermin, verrev; + + TRACE_CALL(__func__); + remmina_plugin_service = service; + + /* Check that we are linked to the correct version of libfreerdp */ + + freerdp_get_version(&vermaj, &vermin, &verrev); + if ((vermaj < FREERDP_REQUIRED_MAJOR) || + ((vermaj == FREERDP_REQUIRED_MAJOR) && ((vermin < FREERDP_REQUIRED_MINOR) || + ((vermin == FREERDP_REQUIRED_MINOR) && (verrev < FREERDP_REQUIRED_REVISION))))) { + g_printf("Upgrade your FreeRDP library version from %d.%d.%d to at least libfreerdp %d.%d.%d " + "to run the RDP plugin.\n", + vermaj, vermin, verrev, + FREERDP_REQUIRED_MAJOR, FREERDP_REQUIRED_MINOR, FREERDP_REQUIRED_REVISION); + return FALSE; + } + + bindtextdomain(GETTEXT_PACKAGE, REMMINA_RUNTIME_LOCALEDIR); + bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8"); + + if (!service->register_plugin((RemminaPlugin *)&remmina_rdp)) + return FALSE; + + remmina_rdpf.export_hints = _("Export connection in Windows .rdp file format"); + + if (!service->register_plugin((RemminaPlugin *)&remmina_rdpf)) + return FALSE; + + if (!service->register_plugin((RemminaPlugin *)&remmina_rdps)) + return FALSE; + + if (buildconfig_strstr(freerdp_get_build_config(), "WITH_GFX_H264=ON")) { + gfx_h264_available = TRUE; + REMMINA_PLUGIN_DEBUG("gfx_h264_available: %d", gfx_h264_available); + } else { + gfx_h264_available = FALSE; + REMMINA_PLUGIN_DEBUG("gfx_h264_available: %d", gfx_h264_available); + /* Remove values 65 and 66 from colordepth_list array by shifting it */ + gpointer *src, *dst; + dst = src = colordepth_list; + while (*src) { + if (strcmp(*src, "66") != 0 && strcmp(*src, "65") != 0) { + if (dst != src) { + *dst = *src; + *(dst + 1) = *(src + 1); + } + dst += 2; + } + src += 2; + } + *dst = NULL; + } + + snprintf(remmina_plugin_rdp_version, sizeof(remmina_plugin_rdp_version), + "RDP plugin: %s (Git %s), Compiled with libfreerdp %s (%s), Running with libfreerdp %s (rev %s), H.264 %s", + VERSION, REMMINA_GIT_REVISION, +#if FREERDP_VERSION_MAJOR >= 3 + FREERDP_VERSION_FULL, FREERDP_GIT_REVISION, +#else + FREERDP_VERSION_FULL, GIT_REVISION, +#endif + freerdp_get_version_string(), + freerdp_get_build_revision(), + gfx_h264_available ? "Yes" : "No" + ); + + remmina_rdp_settings_init(); + + return TRUE; +} diff --git a/plugins/rdp/rdp_plugin.h b/plugins/rdp/rdp_plugin.h new file mode 100644 index 0000000..a0f0cc7 --- /dev/null +++ b/plugins/rdp/rdp_plugin.h @@ -0,0 +1,400 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2010-2011 Vic Lee + * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * 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 <freerdp/freerdp.h> +#include <freerdp/version.h> +#include <freerdp/channels/channels.h> +#include <freerdp/codec/color.h> +#include <freerdp/codec/rfx.h> +#include <freerdp/gdi/gdi.h> +#include <freerdp/gdi/dc.h> +#include <freerdp/gdi/region.h> +#include <freerdp/client/cliprdr.h> +#include <freerdp/client/disp.h> +#ifdef GDK_WINDOWING_X11 +#include <gdk/gdkx.h> +#elif defined(GDK_WINDOWING_WAYLAND) +#include <gdk/gdkwayland.h> +#endif + +#include <winpr/clipboard.h> + +/** + * FREERDP_CHECK_VERSION: + * @major: major version (e.g. 2 for version 2.1.0) + * @minor: minor version (e.g. 0 for version 2.1.0) + * @release: release version (e.g. 0 for version 2.1.0) + * + * Returns %TRUE if the version of the FRERDDP header files + * is the same as or newer than the passed-in version. + * + * Returns: %TRUE if FREERDP headers are new enough + */ +#define FREERDP_CHECK_VERSION(major,minor,revision) \ + (FREERDP_VERSION_MAJOR > (major) || \ + (FREERDP_VERSION_MAJOR == (major) && FREERDP_VERSION_MINOR > (minor)) || \ + (FREERDP_VERSION_MAJOR == (major) && FREERDP_VERSION_MINOR == (minor) && \ + FREERDP_VERSION_REVISION >= (revision))) + +/** + * Constants to workaround FreeRDP issue #5417 (min resolution in AVC mode) + * Must be 4 aligned (multiple of 4). + * We should instead check destRect and be sure the condition is met + * See https://gitlab.com/Remmina/Remmina/-/issues/2507 for a deep discussion + */ +#define AVC_MIN_DESKTOP_WIDTH 644 +#define AVC_MIN_DESKTOP_HEIGHT 480 + +typedef struct rf_context rfContext; + +#define GET_PLUGIN_DATA(gp) (rfContext *)g_object_get_data(G_OBJECT(gp), "plugin-data") + +/** + * Performance Flags, from FreeRDP source + * + * PERF_FLAG_NONE 0x00000000 + * PERF_DISABLE_WALLPAPER 0x00000001 + * PERF_DISABLE_FULLWINDOWDRAG 0x00000002 + * PERF_DISABLE_MENUANIMATIONS 0x00000004 + * PERF_DISABLE_THEMING 0x00000008 + * PERF_DISABLE_CURSOR_SHADOW 0x00000020 + * PERF_DISABLE_CURSORSETTINGS 0x00000040 + * PERF_ENABLE_FONT_SMOOTHING 0x00000080 + * PERF_ENABLE_DESKTOP_COMPOSITION 0x00000100 + */ +/* Poor (default) - all disabled */ +#define DEFAULT_QUALITY_0 0x6f +/* Medium - only THEMING, CURSOR_SHADOW and CURSORSETTINGS enabled, all other disabled */ +#define DEFAULT_QUALITY_1 0x07 +/* Good - WALLPAPER, FONT_SMOOTHING, DESKTOP_COMPOSITION disabled, all other enabled */ +#define DEFAULT_QUALITY_2 0x01 +/* Best - DESKTOP_COMPOSITION disabled, all other enabled */ +#define DEFAULT_QUALITY_9 0x80 + +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__) + +struct rf_clipboard { + rfContext * rfi; + CliprdrClientContext * context; + wClipboard * system; + int requestedFormatId; + + UINT32 format; + gulong clipboard_handler; + + pthread_mutex_t transfer_clip_mutex; + pthread_cond_t transfer_clip_cond; + enum { SCDW_NONE, SCDW_BUSY_WAIT, SCDW_ABORTING } srv_clip_data_wait; + gpointer srv_data; + pthread_mutex_t srv_data_mutex; + + UINT32 server_html_format_id; + + /* Stats for clipboard download */ + struct timeval clientformatdatarequest_tv; +}; +typedef struct rf_clipboard rfClipboard; + + +struct rf_pointer { + rdpPointer pointer; + GdkCursor * cursor; +}; +typedef struct rf_pointer rfPointer; + +#ifdef RF_BITMAP +struct rf_bitmap { + rdpBitmap bitmap; + Pixmap pixmap; + cairo_surface_t * surface; +}; +typedef struct rf_bitmap rfBitmap; +#endif + +#ifdef RF_GLYPH +struct rf_glyph { + rdpGlyph glyph; + Pixmap pixmap; +}; +typedef struct rf_glyph rfGlyph; +#endif + +typedef enum { + REMMINA_RDP_EVENT_TYPE_SCANCODE, + REMMINA_RDP_EVENT_TYPE_SCANCODE_UNICODE, + REMMINA_RDP_EVENT_TYPE_MOUSE, + REMMINA_RDP_EVENT_TYPE_CLIPBOARD_SEND_CLIENT_FORMAT_LIST, + REMMINA_RDP_EVENT_TYPE_CLIPBOARD_SEND_CLIENT_FORMAT_DATA_RESPONSE, + REMMINA_RDP_EVENT_TYPE_CLIPBOARD_SEND_CLIENT_FORMAT_DATA_REQUEST, + REMMINA_RDP_EVENT_TYPE_SEND_MONITOR_LAYOUT, + REMMINA_RDP_EVENT_DISCONNECT +} RemminaPluginRdpEventType; + +struct remmina_plugin_rdp_event { + RemminaPluginRdpEventType type; + union { + struct { + BOOL up; + BOOL extended; + UINT8 key_code; + UINT32 unicode_code; + BOOL extended1; + } key_event; + struct { + UINT16 flags; + UINT16 x; + UINT16 y; + BOOL extended; + } mouse_event; + struct { + CLIPRDR_FORMAT_LIST *pFormatList; + } clipboard_formatlist; + struct { + BYTE * data; + int size; + } clipboard_formatdataresponse; + struct { + CLIPRDR_FORMAT_DATA_REQUEST *pFormatDataRequest; + } clipboard_formatdatarequest; + struct { + gint Flags; + gint Left; + gint Top; + gint width; + gint height; + gint desktopOrientation; + gint desktopScaleFactor; + gint deviceScaleFactor; + gint physicalWidth; + gint physicalHeight; + } monitor_layout; + }; +}; +typedef struct remmina_plugin_rdp_event RemminaPluginRdpEvent; + +typedef enum { + REMMINA_RDP_UI_UPDATE_REGIONS = 0, + REMMINA_RDP_UI_CONNECTED, + REMMINA_RDP_UI_RECONNECT_PROGRESS, + REMMINA_RDP_UI_CURSOR, + REMMINA_RDP_UI_NOCODEC, + REMMINA_RDP_UI_CLIPBOARD, + REMMINA_RDP_UI_EVENT +} RemminaPluginRdpUiType; + +typedef enum { + REMMINA_RDP_UI_CLIPBOARD_FORMATLIST, + REMMINA_RDP_UI_CLIPBOARD_GET_DATA, + REMMINA_RDP_UI_CLIPBOARD_SET_DATA +} RemminaPluginRdpUiClipboardType; + +typedef enum { + REMMINA_RDP_POINTER_NEW, + REMMINA_RDP_POINTER_FREE, + REMMINA_RDP_POINTER_SET, + REMMINA_RDP_POINTER_NULL, + REMMINA_RDP_POINTER_DEFAULT, + REMMINA_RDP_POINTER_SETPOS +} RemminaPluginRdpUiPointerType; + +typedef enum { + REMMINA_RDP_UI_EVENT_UPDATE_SCALE, + REMMINA_RDP_UI_EVENT_DESTROY_CAIRO_SURFACE +} RemminaPluginRdpUiEeventType; + +typedef struct { + gint x, y, w, h; +} region; + +struct remmina_plugin_rdp_ui_object { + RemminaPluginRdpUiType type; + gboolean sync; + gboolean complete; + pthread_mutex_t sync_wait_mutex; + pthread_cond_t sync_wait_cond; + union { + struct { + region *ureg; + gint ninvalid; + } reg; + struct { + rdpContext * context; + rfPointer * pointer; + RemminaPluginRdpUiPointerType type; + } cursor; + struct { + gint left; + gint top; + RFX_MESSAGE * message; + } rfx; + struct { + gint left; + gint top; + gint width; + gint height; + UINT8 * bitmap; + } nocodec; + struct { + RemminaPluginRdpUiClipboardType type; + GtkTargetList * targetlist; + UINT32 format; + rfClipboard * clipboard; + gpointer data; + } clipboard; + struct { + RemminaPluginRdpUiEeventType type; + } event; + struct { + gint x; + gint y; + } pos; + }; + /* We can also return values here, usually integers*/ + int retval; + /* Some functions also may return a pointer. */ + void * retptr; +}; + +typedef struct remmina_plugin_rdp_keymap_entry { + unsigned orig_keycode; + unsigned translated_keycode; +} RemminaPluginRdpKeymapEntry; + +struct rf_context { + rdpClientContext clientContext; + + RemminaProtocolWidget *protocol_widget; + + /* main */ + pthread_t remmina_plugin_thread; + RemminaScaleMode scale; + gboolean user_cancelled; + gboolean thread_cancelled; + + CliprdrClientContext * cliprdr; + DispClientContext * dispcontext; + + RDP_PLUGIN_DATA rdpdr_data[5]; + RDP_PLUGIN_DATA drdynvc_data[5]; + gchar rdpsnd_options[20]; + + gboolean rdpgfxchan; + + gboolean connected; + gboolean is_reconnecting; + gboolean stop_reconnecting_requested; + /* orphaned: rf_context has still one or more libfreerdp thread active, + * but no longer maintained by an open RemminaProtocolWidget/tab. + * When the orphaned thread terminates, we must cleanup rf_context. + */ + gboolean orphaned; + int reconnect_maxattempts; + int reconnect_nattempt; + + gboolean sw_gdi; + GtkWidget * drawing_area; + gint scale_width; + gint scale_height; + gdouble scale_x; + gdouble scale_y; + guint delayed_monitor_layout_handler; + gboolean use_client_keymap; + + gint srcBpp; + GdkDisplay * display; + GdkVisual * visual; + cairo_surface_t * surface; + cairo_format_t cairo_format; + gint bpp; + gint scanline_pad; + gint * colormap; + + guint object_id_seq; + GHashTable * object_table; + + GAsyncQueue * ui_queue; + pthread_mutex_t ui_queue_mutex; + guint ui_handler; + + GArray * pressed_keys; + GAsyncQueue * event_queue; + gint event_pipe[2]; + HANDLE event_handle; + UINT16 last_x; + UINT16 last_y; + + rfClipboard clipboard; + + GArray * keymap; /* Array of RemminaPluginRdpKeymapEntry */ + + gboolean attempt_interactive_authentication; + + enum { REMMINA_POSTCONNECT_ERROR_OK = 0, REMMINA_POSTCONNECT_ERROR_GDI_INIT = 1, REMMINA_POSTCONNECT_ERROR_NO_H264 } postconnect_error; +}; + +typedef struct remmina_plugin_rdp_ui_object RemminaPluginRdpUiObject; + +void rf_init(RemminaProtocolWidget *gp); +void rf_uninit(RemminaProtocolWidget *gp); +void rf_get_fds(RemminaProtocolWidget *gp, void **rfds, int *rcount); +BOOL rf_check_fds(RemminaProtocolWidget *gp); +void rf_object_free(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *obj); + +void remmina_rdp_event_event_push(RemminaProtocolWidget *gp, const RemminaPluginRdpEvent *e); diff --git a/plugins/rdp/rdp_settings.c b/plugins/rdp/rdp_settings.c new file mode 100644 index 0000000..7a3d4aa --- /dev/null +++ b/plugins/rdp/rdp_settings.c @@ -0,0 +1,759 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2010-2011 Vic Lee + * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * 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 "rdp_settings.h" +#include <freerdp/locale/keyboard.h> + +static guint keyboard_layout = 0; +static guint rdp_keyboard_layout = 0; +static gchar *rdp_keyboard_remapping_list = NULL; + +static void remmina_rdp_settings_kbd_init(void) +{ + TRACE_CALL(__func__); +#if FREERDP_CHECK_VERSION(2, 3, 0) + rdp_keyboard_remapping_list = g_strdup( + remmina_plugin_service->pref_get_value("rdp_kbd_remap")); + REMMINA_PLUGIN_DEBUG("rdp_keyboard_remapping_list: %s", rdp_keyboard_remapping_list); + keyboard_layout = freerdp_keyboard_init_ex(rdp_keyboard_layout, rdp_keyboard_remapping_list); +#else + keyboard_layout = freerdp_keyboard_init(rdp_keyboard_layout); +#endif +} + +void remmina_rdp_settings_init(void) +{ + TRACE_CALL(__func__); + gchar *value; + + value = remmina_plugin_service->pref_get_value("rdp_keyboard_layout"); + + if (value && value[0]) + rdp_keyboard_layout = strtoul(value, NULL, 16); + + g_free(value); + + remmina_rdp_settings_kbd_init(); +} + +guint remmina_rdp_settings_get_keyboard_layout(void) +{ + TRACE_CALL(__func__); + return keyboard_layout; +} + +#define REMMINA_TYPE_PLUGIN_RDPSET_GRID (remmina_rdp_settings_grid_get_type()) +#define REMMINA_RDPSET_GRID(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), REMMINA_TYPE_PLUGIN_RDPSET_GRID, RemminaPluginRdpsetGrid)) +#define REMMINA_RDPSET_GRID_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), REMMINA_TYPE_PLUGIN_RDPSET_GRID, RemminaPluginRdpsetGridClass)) +#define REMMINA_IS_PLUGIN_RDPSET_GRID(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), REMMINA_TYPE_PLUGIN_RDPSET_GRID)) +#define REMMINA_IS_PLUGIN_RDPSET_GRID_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), REMMINA_TYPE_PLUGIN_RDPSET_GRID)) +#define REMMINA_RDPSET_GRID_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), REMMINA_TYPE_PLUGIN_RDPSET_GRID, RemminaPluginRdpsetGridClass)) + +typedef struct _RemminaPluginRdpsetGrid { + GtkGrid grid; + + GtkWidget * keyboard_layout_label; + GtkWidget * keyboard_layout_combo; + GtkListStore * keyboard_layout_store; + + GtkWidget * quality_combo; + GtkListStore * quality_store; + GtkWidget * wallpaper_check; + GtkWidget * windowdrag_check; + GtkWidget * menuanimation_check; + GtkWidget * theme_check; + GtkWidget * cursorshadow_check; + GtkWidget * cursorblinking_check; + GtkWidget * fontsmoothing_check; + GtkWidget * composition_check; + GtkWidget * use_client_keymap_check; + GtkWidget * disable_smooth_scrolling_check; + GtkWidget * reconnect_attempts; + GtkWidget * kbd_remap; + + /* FreeRDP /scale-desktop: Scaling of desktop app */ + GtkWidget * desktop_scale_factor_spin; + /* FreeRDP /scale-device: Scaling of appstore app */ + GtkListStore * device_scale_factor_store; + GtkWidget * device_scale_factor_combo; + /* FreeRDP /orientation: Orientation of display */ + GtkListStore * desktop_orientation_store; + GtkWidget * desktop_orientation_combo; + + guint quality_values[10]; +} RemminaPluginRdpsetGrid; + +typedef struct _RemminaPluginRdpsetGridClass { + GtkGridClass parent_class; +} RemminaPluginRdpsetGridClass; + +GType remmina_rdp_settings_grid_get_type(void) G_GNUC_CONST; + +G_DEFINE_TYPE(RemminaPluginRdpsetGrid, remmina_rdp_settings_grid, GTK_TYPE_GRID) + +static void remmina_rdp_settings_grid_class_init(RemminaPluginRdpsetGridClass *klass) +{ + TRACE_CALL(__func__); +} + +static void remmina_rdp_settings_grid_destroy(GtkWidget *widget, gpointer data) +{ + TRACE_CALL(__func__); + gchar *s; + guint new_layout; + GtkTreeIter iter; + RemminaPluginRdpsetGrid *grid; + gint val; + + grid = REMMINA_RDPSET_GRID(widget); + + if (gtk_combo_box_get_active_iter(GTK_COMBO_BOX(grid->keyboard_layout_combo), &iter)) { + gtk_tree_model_get(GTK_TREE_MODEL(grid->keyboard_layout_store), &iter, 0, &new_layout, -1); + + if (new_layout != rdp_keyboard_layout) { + rdp_keyboard_layout = new_layout; + s = g_strdup_printf("%X", new_layout); + remmina_plugin_service->pref_set_value("rdp_keyboard_layout", s); + g_free(s); + + remmina_rdp_settings_kbd_init(); + } + } + + remmina_plugin_service->pref_set_value("rdp_use_client_keymap", + gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(grid->use_client_keymap_check)) ? "1" : "0"); + + remmina_plugin_service->pref_set_value("rdp_disable_smooth_scrolling", + gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(grid->disable_smooth_scrolling_check)) ? "1" : "0"); + + remmina_plugin_service->pref_set_value("rdp_reconnect_attempts", + gtk_entry_get_text(GTK_ENTRY(grid->reconnect_attempts))); + + remmina_plugin_service->pref_set_value("rdp_kbd_remap", + gtk_entry_get_text(GTK_ENTRY(grid->kbd_remap))); + + s = g_strdup_printf("%X", grid->quality_values[0]); + remmina_plugin_service->pref_set_value("rdp_quality_0", s); + g_free(s); + + s = g_strdup_printf("%X", grid->quality_values[1]); + remmina_plugin_service->pref_set_value("rdp_quality_1", s); + g_free(s); + + s = g_strdup_printf("%X", grid->quality_values[2]); + remmina_plugin_service->pref_set_value("rdp_quality_2", s); + g_free(s); + + s = g_strdup_printf("%X", grid->quality_values[9]); + remmina_plugin_service->pref_set_value("rdp_quality_9", s); + g_free(s); + + if (gtk_combo_box_get_active_iter(GTK_COMBO_BOX(grid->device_scale_factor_combo), &iter)) + gtk_tree_model_get(GTK_TREE_MODEL(grid->device_scale_factor_store), &iter, 0, &val, -1); + else + val = 0; + s = g_strdup_printf("%d", val); + remmina_plugin_service->pref_set_value("rdp_deviceScaleFactor", s); + g_free(s); + + val = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(grid->desktop_scale_factor_spin)); + s = g_strdup_printf("%d", val); + remmina_plugin_service->pref_set_value("rdp_desktopScaleFactor", s); + g_free(s); + + if (gtk_combo_box_get_active_iter(GTK_COMBO_BOX(grid->desktop_orientation_combo), &iter)) + gtk_tree_model_get(GTK_TREE_MODEL(grid->desktop_orientation_store), &iter, 0, &val, -1); + else + val = 0; + s = g_strdup_printf("%d", val); + remmina_plugin_service->pref_set_value("rdp_desktopOrientation", s); + g_free(s); +} + +static void remmina_rdp_settings_grid_load_layout(RemminaPluginRdpsetGrid *grid) +{ + TRACE_CALL(__func__); + gchar *s; + GtkTreeIter iter; + RDP_KEYBOARD_LAYOUT *layouts; + + gtk_list_store_append(grid->keyboard_layout_store, &iter); + gtk_list_store_set(grid->keyboard_layout_store, &iter, 0, 0, 1, _("<Auto-detect>"), -1); + + if (rdp_keyboard_layout == 0) + gtk_combo_box_set_active(GTK_COMBO_BOX(grid->keyboard_layout_combo), 0); + + gtk_label_set_text(GTK_LABEL(grid->keyboard_layout_label), "-"); + +#if FREERDP_VERSION_MAJOR >= 3 + size_t layout_count = 0; + layouts = freerdp_keyboard_get_layouts(RDP_KEYBOARD_LAYOUT_TYPE_STANDARD | RDP_KEYBOARD_LAYOUT_TYPE_VARIANT, &layout_count); + + for (DWORD i = 0; i < layout_count; i++) { + s = g_strdup_printf("%08X - %s", layouts[i].code, layouts[i].name); + gtk_list_store_append(grid->keyboard_layout_store, &iter); + gtk_list_store_set(grid->keyboard_layout_store, &iter, 0, layouts[i].code, 1, s, -1); + + if (rdp_keyboard_layout == layouts[i].code) + gtk_combo_box_set_active(GTK_COMBO_BOX(grid->keyboard_layout_combo), i + 1); + + if (keyboard_layout == layouts[i].code) + gtk_label_set_text(GTK_LABEL(grid->keyboard_layout_label), s); + + g_free(s); + } + + freerdp_keyboard_layouts_free(layouts, layout_count); +#else + layouts = freerdp_keyboard_get_layouts(RDP_KEYBOARD_LAYOUT_TYPE_STANDARD | RDP_KEYBOARD_LAYOUT_TYPE_VARIANT); + + for (DWORD i = 0; layouts[i].code; i++) { + s = g_strdup_printf("%08X - %s", layouts[i].code, layouts[i].name); + gtk_list_store_append(grid->keyboard_layout_store, &iter); + gtk_list_store_set(grid->keyboard_layout_store, &iter, 0, layouts[i].code, 1, s, -1); + + if (rdp_keyboard_layout == layouts[i].code) + gtk_combo_box_set_active(GTK_COMBO_BOX(grid->keyboard_layout_combo), i + 1); + + if (keyboard_layout == layouts[i].code) + gtk_label_set_text(GTK_LABEL(grid->keyboard_layout_label), s); + + g_free(s); + } + + freerdp_keyboard_layouts_free(layouts); +#endif +} + +static void remmina_rdp_settings_grid_load_devicescalefactor_combo(RemminaPluginRdpsetGrid *grid) +{ + TRACE_CALL(__func__); + GtkTreeIter iter; + + gtk_list_store_append(grid->device_scale_factor_store, &iter); + gtk_list_store_set(grid->device_scale_factor_store, &iter, 0, 0, 1, _("<Not set>"), -1); + gtk_list_store_append(grid->device_scale_factor_store, &iter); + gtk_list_store_set(grid->device_scale_factor_store, &iter, 0, 100, 1, "100%", -1); + gtk_list_store_append(grid->device_scale_factor_store, &iter); + gtk_list_store_set(grid->device_scale_factor_store, &iter, 0, 140, 1, "140%", -1); + gtk_list_store_append(grid->device_scale_factor_store, &iter); + gtk_list_store_set(grid->device_scale_factor_store, &iter, 0, 180, 1, "180%", -1); +} + +static void remmina_rdp_settings_grid_load_desktoporientation_combo(RemminaPluginRdpsetGrid *grid) +{ + TRACE_CALL(__func__); + GtkTreeIter iter; + + gtk_list_store_append(grid->desktop_orientation_store, &iter); + gtk_list_store_set(grid->desktop_orientation_store, &iter, 0, 0, 1, "0°", -1); + gtk_list_store_append(grid->desktop_orientation_store, &iter); + gtk_list_store_set(grid->desktop_orientation_store, &iter, 0, 90, 1, "90°", -1); + gtk_list_store_append(grid->desktop_orientation_store, &iter); + gtk_list_store_set(grid->desktop_orientation_store, &iter, 0, 180, 1, "180°", -1); + gtk_list_store_append(grid->desktop_orientation_store, &iter); + gtk_list_store_set(grid->desktop_orientation_store, &iter, 0, 270, 1, "270°", -1); +} + +static void remmina_rdp_settings_grid_load_quality(RemminaPluginRdpsetGrid *grid) +{ + TRACE_CALL(__func__); + gchar *value; + GtkTreeIter iter; + + gtk_list_store_append(grid->quality_store, &iter); + gtk_list_store_set(grid->quality_store, &iter, 0, -1, 1, _("<Choose a quality level to edit…>"), -1); + gtk_list_store_append(grid->quality_store, &iter); + gtk_list_store_set(grid->quality_store, &iter, 0, 0, 1, _("Poor (fastest)"), -1); + gtk_list_store_append(grid->quality_store, &iter); + gtk_list_store_set(grid->quality_store, &iter, 0, 1, 1, _("Medium"), -1); + gtk_list_store_append(grid->quality_store, &iter); + gtk_list_store_set(grid->quality_store, &iter, 0, 2, 1, _("Good"), -1); + gtk_list_store_append(grid->quality_store, &iter); + gtk_list_store_set(grid->quality_store, &iter, 0, 9, 1, _("Best (slowest)"), -1); + + memset(grid->quality_values, 0, sizeof(grid->quality_values)); + + value = remmina_plugin_service->pref_get_value("rdp_quality_0"); + grid->quality_values[0] = (value && value[0] ? strtoul(value, NULL, 16) : DEFAULT_QUALITY_0); + g_free(value); + + value = remmina_plugin_service->pref_get_value("rdp_quality_1"); + grid->quality_values[1] = (value && value[0] ? strtoul(value, NULL, 16) : DEFAULT_QUALITY_1); + g_free(value); + + value = remmina_plugin_service->pref_get_value("rdp_quality_2"); + grid->quality_values[2] = (value && value[0] ? strtoul(value, NULL, 16) : DEFAULT_QUALITY_2); + g_free(value); + + value = remmina_plugin_service->pref_get_value("rdp_quality_9"); + grid->quality_values[9] = (value && value[0] ? strtoul(value, NULL, 16) : DEFAULT_QUALITY_9); + g_free(value); +} + +static void remmina_rdp_settings_appscale_on_changed(GtkComboBox *widget, RemminaPluginRdpsetGrid *grid) +{ + TRACE_CALL(__func__); + GtkTreeIter iter; + guint i = 0; + + if (gtk_combo_box_get_active_iter(GTK_COMBO_BOX(grid->device_scale_factor_combo), &iter)) + gtk_tree_model_get(GTK_TREE_MODEL(grid->device_scale_factor_store), &iter, 0, &i, -1); + if (i == 0) { + gtk_widget_set_sensitive(GTK_WIDGET(grid->desktop_scale_factor_spin), FALSE); + gtk_spin_button_set_range(GTK_SPIN_BUTTON(grid->desktop_scale_factor_spin), 0, 0); + gtk_spin_button_set_value(GTK_SPIN_BUTTON(grid->desktop_scale_factor_spin), 0); + } else { + gtk_widget_set_sensitive(GTK_WIDGET(grid->desktop_scale_factor_spin), TRUE); + gtk_spin_button_set_range(GTK_SPIN_BUTTON(grid->desktop_scale_factor_spin), 100, 500); + // gtk_spin_button_set_value(GTK_SPIN_BUTTON(grid->desktop_scale_factor_spin), i); + } +} + +static void remmina_rdp_settings_quality_on_changed(GtkComboBox *widget, RemminaPluginRdpsetGrid *grid) +{ + TRACE_CALL(__func__); + guint v; + guint i = 0; + GtkTreeIter iter; + gboolean sensitive; + + if (gtk_combo_box_get_active_iter(GTK_COMBO_BOX(grid->quality_combo), &iter)) { + gtk_tree_model_get(GTK_TREE_MODEL(grid->quality_store), &iter, 0, &i, -1); + sensitive = (i != -1); + + if (sensitive) + v = grid->quality_values[i]; + else + v = 0x3f; /* All checkboxes disabled */ + + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(grid->wallpaper_check), (v & 1) == 0); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(grid->windowdrag_check), (v & 2) == 0); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(grid->menuanimation_check), (v & 4) == 0); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(grid->theme_check), (v & 8) == 0); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(grid->cursorshadow_check), (v & 0x20) == 0); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(grid->cursorblinking_check), (v & 0x40) == 0); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(grid->fontsmoothing_check), (v & 0x80) != 0); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(grid->composition_check), (v & 0x100) != 0); + + + gtk_widget_set_sensitive(GTK_WIDGET(grid->wallpaper_check), sensitive); + gtk_widget_set_sensitive(GTK_WIDGET(grid->windowdrag_check), sensitive); + gtk_widget_set_sensitive(GTK_WIDGET(grid->menuanimation_check), sensitive); + gtk_widget_set_sensitive(GTK_WIDGET(grid->theme_check), sensitive); + gtk_widget_set_sensitive(GTK_WIDGET(grid->cursorshadow_check), sensitive); + gtk_widget_set_sensitive(GTK_WIDGET(grid->cursorblinking_check), sensitive); + gtk_widget_set_sensitive(GTK_WIDGET(grid->fontsmoothing_check), sensitive); + gtk_widget_set_sensitive(GTK_WIDGET(grid->composition_check), sensitive); + } +} + +static void remmina_rdp_settings_quality_option_on_toggled(GtkToggleButton *togglebutton, RemminaPluginRdpsetGrid *grid) +{ + TRACE_CALL(__func__); + guint v; + guint i = 0; + GtkTreeIter iter; + + if (gtk_combo_box_get_active_iter(GTK_COMBO_BOX(grid->quality_combo), &iter)) { + gtk_tree_model_get(GTK_TREE_MODEL(grid->quality_store), &iter, 0, &i, -1); + if (i != -1) { + v = 0; + v |= (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(grid->wallpaper_check)) ? 0 : 1); + v |= (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(grid->windowdrag_check)) ? 0 : 2); + v |= (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(grid->menuanimation_check)) ? 0 : 4); + v |= (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(grid->theme_check)) ? 0 : 8); + v |= (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(grid->cursorshadow_check)) ? 0 : 0x20); + v |= (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(grid->cursorblinking_check)) ? 0 : 0x40); + v |= (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(grid->fontsmoothing_check)) ? 0x80 : 0); + v |= (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(grid->composition_check)) ? 0x100 : 0); + grid->quality_values[i] = v; + } + } +} + +static void remmina_rdp_settings_set_combo_active_item(GtkComboBox *combo, int itemval) +{ + GtkTreeIter iter; + int i; + GtkTreeModel *m; + gboolean valid; + + m = gtk_combo_box_get_model(combo); + if (!m) + return; + + valid = gtk_tree_model_get_iter_first(m, &iter); + while (valid) { + gtk_tree_model_get(m, &iter, 0, &i, -1); + if (i == itemval) + gtk_combo_box_set_active_iter(combo, &iter); + valid = gtk_tree_model_iter_next(m, &iter); + } +} + +static void remmina_rdp_settings_grid_init(RemminaPluginRdpsetGrid *grid) +{ + TRACE_CALL(__func__); + gchar *s; + GtkWidget *widget; + GtkCellRenderer *renderer; + int desktopOrientation, desktopScaleFactor, deviceScaleFactor; + + /* Create the grid */ + g_signal_connect(G_OBJECT(grid), "destroy", G_CALLBACK(remmina_rdp_settings_grid_destroy), NULL); + gtk_grid_set_row_homogeneous(GTK_GRID(grid), FALSE); + gtk_grid_set_column_homogeneous(GTK_GRID(grid), FALSE); + gtk_container_set_border_width(GTK_CONTAINER(grid), 8); + gtk_grid_set_row_spacing(GTK_GRID(grid), 4); + gtk_grid_set_column_spacing(GTK_GRID(grid), 4); + + /* Create the content */ + widget = gtk_label_new(_("Keyboard layout")); + gtk_widget_show(widget); + gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START); + gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER); + gtk_widget_set_margin_end(GTK_WIDGET(widget), 6); + gtk_widget_set_margin_start(GTK_WIDGET(widget), 18); + gtk_widget_set_margin_top(GTK_WIDGET(widget), 18); + gtk_grid_attach(GTK_GRID(grid), widget, 0, 0, 1, 1); + + grid->keyboard_layout_store = gtk_list_store_new(2, G_TYPE_UINT, G_TYPE_STRING); + widget = gtk_combo_box_new_with_model(GTK_TREE_MODEL(grid->keyboard_layout_store)); + gtk_widget_show(widget); + gtk_widget_set_margin_start(GTK_WIDGET(widget), 6); + gtk_grid_attach(GTK_GRID(grid), widget, 1, 0, 2, 1); + + renderer = gtk_cell_renderer_text_new(); + gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(widget), renderer, TRUE); + gtk_cell_layout_add_attribute(GTK_CELL_LAYOUT(widget), renderer, "text", 1); + grid->keyboard_layout_combo = widget; + + widget = gtk_label_new("-"); + gtk_widget_show(widget); + gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START); + gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER); + gtk_widget_set_margin_start(GTK_WIDGET(widget), 6); + gtk_grid_attach(GTK_GRID(grid), widget, 1, 1, 2, 1); + grid->keyboard_layout_label = widget; + + remmina_rdp_settings_grid_load_layout(grid); + + widget = gtk_check_button_new_with_label(_("Use client keyboard mapping")); + gtk_widget_show(widget); + gtk_widget_set_margin_start(GTK_WIDGET(widget), 6); + gtk_grid_attach(GTK_GRID(grid), widget, 1, 2, 1, 1); + grid->use_client_keymap_check = widget; + + s = remmina_plugin_service->pref_get_value("rdp_use_client_keymap"); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), + s && s[0] == '1' ? TRUE : FALSE); + g_free(s); + + widget = gtk_label_new(_("Keyboard scancode remapping")); + gtk_widget_show(widget); + gtk_widget_set_margin_start(GTK_WIDGET(widget), 6); + gtk_grid_attach(GTK_GRID(grid), widget, 1, 3, 1, 1); + gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START); + gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER); + widget = gtk_entry_new(); + gtk_widget_show(widget); + gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_END); + gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER); + gtk_grid_attach(GTK_GRID(grid), widget, 2, 3, 1, 1); + gtk_entry_set_width_chars(GTK_ENTRY(widget), 32); +#if FREERDP_CHECK_VERSION(2, 3, 0) + /* This is the default, but we set it to make things crystal clear */ + gtk_widget_set_sensitive (widget, TRUE); + gtk_widget_set_tooltip_text(widget, _("List of key=value,… pairs to remap scancodes. E.g. 0x56=0x29,0x29=0x56")); +#else + gtk_widget_set_sensitive (widget, FALSE); + gtk_widget_set_tooltip_text(widget, _("FreeRDP > 2.3.0 is required to map scancodes")); +#endif + s = remmina_plugin_service->pref_get_value("rdp_kbd_remap"); + if (s && s[0]) + gtk_entry_set_text(GTK_ENTRY(widget), s); + g_free(s); + grid->kbd_remap = widget; + + widget = gtk_label_new(_("Quality settings")); + gtk_widget_show(widget); + gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START); + gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER); + gtk_widget_set_margin_end(GTK_WIDGET(widget), 6); + gtk_widget_set_margin_start(GTK_WIDGET(widget), 18); + gtk_grid_attach(GTK_GRID(grid), widget, 0, 4, 1, 1); + + grid->quality_store = gtk_list_store_new(2, G_TYPE_UINT, G_TYPE_STRING); + widget = gtk_combo_box_new_with_model(GTK_TREE_MODEL(grid->quality_store)); + gtk_widget_show(widget); + gtk_widget_set_margin_start(GTK_WIDGET(widget), 6); + gtk_grid_attach(GTK_GRID(grid), widget, 1, 4, 2, 1); + + renderer = gtk_cell_renderer_text_new(); + gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(widget), renderer, TRUE); + gtk_cell_layout_add_attribute(GTK_CELL_LAYOUT(widget), renderer, "text", 1); + g_signal_connect(G_OBJECT(widget), "changed", + G_CALLBACK(remmina_rdp_settings_quality_on_changed), grid); + grid->quality_combo = widget; + + remmina_rdp_settings_grid_load_quality(grid); + + widget = gtk_check_button_new_with_label(_("Wallpaper")); + gtk_widget_show(widget); + gtk_widget_set_margin_start(GTK_WIDGET(widget), 6); + gtk_grid_attach(GTK_GRID(grid), widget, 1, 5, 1, 1); + g_signal_connect(G_OBJECT(widget), "toggled", + G_CALLBACK(remmina_rdp_settings_quality_option_on_toggled), grid); + grid->wallpaper_check = widget; + + widget = gtk_check_button_new_with_label(_("Window drag")); + gtk_widget_show(widget); + gtk_grid_attach(GTK_GRID(grid), widget, 2, 5, 1, 1); + g_signal_connect(G_OBJECT(widget), "toggled", + G_CALLBACK(remmina_rdp_settings_quality_option_on_toggled), grid); + grid->windowdrag_check = widget; + + widget = gtk_check_button_new_with_label(_("Menu animation")); + gtk_widget_show(widget); + gtk_widget_set_margin_start(GTK_WIDGET(widget), 6); + gtk_grid_attach(GTK_GRID(grid), widget, 1, 6, 1, 1); + g_signal_connect(G_OBJECT(widget), "toggled", + G_CALLBACK(remmina_rdp_settings_quality_option_on_toggled), grid); + grid->menuanimation_check = widget; + + widget = gtk_check_button_new_with_label(_("Theme")); + gtk_widget_show(widget); + gtk_grid_attach(GTK_GRID(grid), widget, 2, 6, 1, 1); + g_signal_connect(G_OBJECT(widget), "toggled", + G_CALLBACK(remmina_rdp_settings_quality_option_on_toggled), grid); + grid->theme_check = widget; + + widget = gtk_check_button_new_with_label(_("Cursor shadow")); + gtk_widget_show(widget); + gtk_widget_set_margin_start(GTK_WIDGET(widget), 6); + gtk_grid_attach(GTK_GRID(grid), widget, 1, 7, 1, 1); + g_signal_connect(G_OBJECT(widget), "toggled", + G_CALLBACK(remmina_rdp_settings_quality_option_on_toggled), grid); + grid->cursorshadow_check = widget; + + widget = gtk_check_button_new_with_label(_("Cursor blinking")); + gtk_widget_show(widget); + gtk_grid_attach(GTK_GRID(grid), widget, 2, 7, 1, 1); + g_signal_connect(G_OBJECT(widget), "toggled", + G_CALLBACK(remmina_rdp_settings_quality_option_on_toggled), grid); + grid->cursorblinking_check = widget; + + widget = gtk_check_button_new_with_label(_("Font smoothing")); + gtk_widget_show(widget); + gtk_widget_set_margin_start(GTK_WIDGET(widget), 6); + gtk_grid_attach(GTK_GRID(grid), widget, 1, 8, 1, 1); + g_signal_connect(G_OBJECT(widget), "toggled", + G_CALLBACK(remmina_rdp_settings_quality_option_on_toggled), grid); + grid->fontsmoothing_check = widget; + + widget = gtk_check_button_new_with_label(_("Composition")); + gtk_widget_show(widget); + gtk_grid_attach(GTK_GRID(grid), widget, 2, 8, 1, 1); + g_signal_connect(G_OBJECT(widget), "toggled", + G_CALLBACK(remmina_rdp_settings_quality_option_on_toggled), grid); + grid->composition_check = widget; + + gtk_combo_box_set_active(GTK_COMBO_BOX(grid->quality_combo), 0); + + + widget = gtk_label_new(_("Remote scale factor")); + gtk_widget_show(widget); + gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START); + gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER); + gtk_widget_set_margin_end(GTK_WIDGET(widget), 6); + gtk_widget_set_margin_start(GTK_WIDGET(widget), 18); + gtk_grid_attach(GTK_GRID(grid), widget, 0, 9, 1, 1); + + grid->device_scale_factor_store = gtk_list_store_new(2, G_TYPE_INT, G_TYPE_STRING); + grid->desktop_orientation_store = gtk_list_store_new(2, G_TYPE_INT, G_TYPE_STRING); + + remmina_rdp_settings_get_orientation_scale_prefs(&desktopOrientation, &desktopScaleFactor, &deviceScaleFactor); + remmina_rdp_settings_grid_load_devicescalefactor_combo(grid); + remmina_rdp_settings_grid_load_desktoporientation_combo(grid); + + widget = gtk_label_new(_("Desktop scale factor %")); + gtk_widget_show(widget); + gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START); + gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER); + gtk_widget_set_margin_start(GTK_WIDGET(widget), 6); + gtk_grid_attach(GTK_GRID(grid), widget, 1, 9, 1, 1); + + widget = gtk_spin_button_new_with_range(0, 10000, 1); + gtk_widget_show(widget); + gtk_grid_attach(GTK_GRID(grid), widget, 2, 9, 1, 1); + grid->desktop_scale_factor_spin = widget; + + widget = gtk_label_new(_("Device scale factor %")); + gtk_widget_show(widget); + gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START); + gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER); + gtk_widget_set_margin_start(GTK_WIDGET(widget), 6); + gtk_grid_attach(GTK_GRID(grid), widget, 1, 10, 1, 1); + + widget = gtk_combo_box_new_with_model(GTK_TREE_MODEL(grid->device_scale_factor_store)); + gtk_widget_show(widget); + gtk_grid_attach(GTK_GRID(grid), widget, 2, 10, 1, 1); + + renderer = gtk_cell_renderer_text_new(); + gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(widget), renderer, TRUE); + gtk_cell_layout_add_attribute(GTK_CELL_LAYOUT(widget), renderer, "text", 1); + grid->device_scale_factor_combo = widget; + + remmina_rdp_settings_set_combo_active_item(GTK_COMBO_BOX(grid->device_scale_factor_combo), deviceScaleFactor); + gtk_spin_button_set_value(GTK_SPIN_BUTTON(grid->desktop_scale_factor_spin), (gdouble)desktopScaleFactor); + + g_signal_connect(G_OBJECT(widget), "changed", + G_CALLBACK(remmina_rdp_settings_appscale_on_changed), grid); + remmina_rdp_settings_appscale_on_changed(GTK_COMBO_BOX(grid->device_scale_factor_combo), grid); + + widget = gtk_label_new(_("Desktop orientation")); + gtk_widget_show(widget); + gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START); + gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER); + gtk_widget_set_margin_end(GTK_WIDGET(widget), 6); + gtk_widget_set_margin_start(GTK_WIDGET(widget), 18); + gtk_grid_attach(GTK_GRID(grid), widget, 0, 11, 1, 1); + + widget = gtk_combo_box_new_with_model(GTK_TREE_MODEL(grid->desktop_orientation_store)); + gtk_widget_show(widget); + gtk_widget_set_margin_start(GTK_WIDGET(widget), 6); + gtk_grid_attach(GTK_GRID(grid), widget, 1, 11, 2, 1); + + renderer = gtk_cell_renderer_text_new(); + gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(widget), renderer, TRUE); + gtk_cell_layout_add_attribute(GTK_CELL_LAYOUT(widget), renderer, "text", 1); + grid->desktop_orientation_combo = widget; + + remmina_rdp_settings_set_combo_active_item(GTK_COMBO_BOX(grid->desktop_orientation_combo), desktopOrientation); + + widget = gtk_label_new(_("Input device settings")); + gtk_widget_show(widget); + gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START); + gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER); + gtk_widget_set_margin_end(GTK_WIDGET(widget), 6); + gtk_widget_set_margin_start(GTK_WIDGET(widget), 18); + gtk_grid_attach(GTK_GRID(grid), widget, 0, 12, 1, 1); + + widget = gtk_check_button_new_with_label(_("Disable smooth scrolling")); + gtk_widget_show(widget); + gtk_widget_set_margin_start(GTK_WIDGET(widget), 6); + gtk_grid_attach(GTK_GRID(grid), widget, 1, 12, 2, 1); + grid->disable_smooth_scrolling_check = widget; + + s = remmina_plugin_service->pref_get_value("rdp_disable_smooth_scrolling"); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), + s && s[0] == '1' ? TRUE : FALSE); + g_free(s); + + widget = gtk_label_new(_("General settings")); + gtk_widget_show(widget); + gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START); + gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER); + gtk_widget_set_margin_end(GTK_WIDGET(widget), 6); + gtk_widget_set_margin_start(GTK_WIDGET(widget), 18); + gtk_grid_attach(GTK_GRID(grid), widget, 0, 13, 1, 1); + widget = gtk_label_new(_("Reconnect attempts number")); + gtk_widget_show(widget); + gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START); + gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER); + gtk_widget_set_margin_start(GTK_WIDGET(widget), 6); + gtk_grid_attach(GTK_GRID(grid), widget, 1, 13, 1, 1); + widget = gtk_entry_new(); + gtk_widget_show(widget); + gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_END); + gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER); + gtk_grid_attach(GTK_GRID(grid), widget, 2, 13, 1, 1); + gtk_entry_set_input_purpose(GTK_ENTRY(widget), GTK_INPUT_PURPOSE_NUMBER); + gtk_entry_set_input_hints(GTK_ENTRY(widget), GTK_INPUT_HINT_NONE); + gtk_widget_set_tooltip_text(widget, _("The maximum number of reconnect attempts upon an RDP disconnect (default: 20)")); + s = remmina_plugin_service->pref_get_value("rdp_reconnect_attempts"); + if (s && s[0]) + gtk_entry_set_text(GTK_ENTRY(widget), s); + g_free(s); + grid->reconnect_attempts = widget; + +} + +GtkWidget *remmina_rdp_settings_new(RemminaPrefPlugin* plugin) +{ + TRACE_CALL(__func__); + GtkWidget *widget; + + widget = GTK_WIDGET(g_object_new(REMMINA_TYPE_PLUGIN_RDPSET_GRID, NULL)); + gtk_widget_show(widget); + + return widget; +} + +void remmina_rdp_settings_get_orientation_scale_prefs(int *desktopOrientation, int *desktopScaleFactor, int *deviceScaleFactor) +{ + TRACE_CALL(__func__); + + /* See https://msdn.microsoft.com/en-us/library/cc240510.aspx */ + + int orientation, dpsf, desf; + gchar *s; + + *desktopOrientation = *desktopScaleFactor = *deviceScaleFactor = 0; + + s = remmina_plugin_service->pref_get_value("rdp_desktopOrientation"); + orientation = s ? atoi(s) : 0; + g_free(s); + if (orientation != 90 && orientation != 180 && orientation != 270) + orientation = 0; + *desktopOrientation = orientation; + + s = remmina_plugin_service->pref_get_value("rdp_desktopScaleFactor"); + dpsf = s ? atoi(s) : 0; + g_free(s); + if (dpsf < 100 || dpsf > 500) + return; + + s = remmina_plugin_service->pref_get_value("rdp_deviceScaleFactor"); + desf = s ? atoi(s) : 0; + g_free(s); + if (desf != 100 && desf != 140 && desf != 180) + return; + + *desktopScaleFactor = dpsf; + *deviceScaleFactor = desf; +} diff --git a/plugins/rdp/rdp_settings.h b/plugins/rdp/rdp_settings.h new file mode 100644 index 0000000..e3b9bc7 --- /dev/null +++ b/plugins/rdp/rdp_settings.h @@ -0,0 +1,49 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2010-2011 Vic Lee + * Copyright (C) 2017-2023 Antenore Gatta, Giovanni Panozzo + * + * 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 <glib.h> +#include "rdp_plugin.h" + +G_BEGIN_DECLS + +void remmina_rdp_settings_init(void); +guint remmina_rdp_settings_get_keyboard_layout(void); +GtkWidget *remmina_rdp_settings_new(RemminaPrefPlugin* plugin); + +void remmina_rdp_settings_get_orientation_scale_prefs(int *desktopOrientation, int *desktopScaleFactor, int *deviceScaleFactor); + +G_END_DECLS diff --git a/plugins/rdp/scalable/emblems/org.remmina.Remmina-rdp-ssh-symbolic.svg b/plugins/rdp/scalable/emblems/org.remmina.Remmina-rdp-ssh-symbolic.svg new file mode 100644 index 0000000..6a345cf --- /dev/null +++ b/plugins/rdp/scalable/emblems/org.remmina.Remmina-rdp-ssh-symbolic.svg @@ -0,0 +1,89 @@ +<?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.458332" + version="1.1" + id="svg8" + inkscape:version="0.92.2 2405546, 2018-03-11" + sodipodi:docname="remmina-rdp-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" + inkscape:cx="25.558198" + inkscape:cy="61.982189" + 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="1600" + inkscape:window-height="879" + inkscape:window-x="0" + inkscape:window-y="0" + 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="-179.27136" + originy="-128.69822" /> + </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(-179.27135,-141.84345)"> + <g + id="g817" + transform="matrix(0.93878631,0,0,0.93878631,13.549948,10.22929)"> + <path + style="isolation:isolate;fill:#000000;stroke-width:0.93878627" + d="M 49.884766 0.27539062 C 34.886727 0.27539062 22.888672 12.506314 22.888672 27.273438 L 22.888672 32.117188 L 9.7363281 32.117188 L 9.7363281 99.724609 L 31.220703 99.724609 L 90.263672 99.724609 L 90.263672 32.117188 L 76.880859 32.117188 L 76.880859 27.273438 C 76.880859 12.275364 64.651854 0.27539062 49.884766 0.27539062 z M 49.654297 14.582031 C 56.806908 14.582031 62.34375 20.350818 62.34375 27.273438 L 62.34375 32.117188 L 36.732422 32.117188 L 36.732422 27.273438 L 36.962891 27.273438 C 36.962891 20.119868 42.732636 14.582031 49.654297 14.582031 z M 50 39.673828 C 51.038296 39.673828 52.075804 40.069448 52.867188 40.861328 L 56.373047 44.367188 L 59.878906 47.871094 C 61.462631 49.453896 61.462631 52.024619 59.878906 53.607422 L 56.373047 57.113281 L 52.867188 60.619141 C 51.28442 62.201943 48.715615 62.201943 47.132812 60.619141 L 43.625 57.113281 L 40.121094 53.607422 C 38.538291 52.024619 38.538291 49.453896 40.121094 47.871094 L 43.625 44.367188 L 47.132812 40.861328 C 47.924214 40.069448 48.961704 39.673828 50 39.673828 z M 32.560547 55.105469 C 33.593916 55.105469 34.626422 55.499469 35.414062 56.287109 L 38.933594 59.806641 L 42.451172 63.324219 C 44.02741 64.900457 44.02741 67.458918 42.451172 69.035156 L 38.933594 72.552734 L 35.414062 76.072266 C 33.838782 77.647546 31.280359 77.647546 29.705078 76.072266 L 26.185547 72.552734 L 22.666016 69.035156 C 21.090735 67.458918 21.090735 64.900457 22.666016 63.324219 L 26.185547 59.806641 L 29.705078 56.287109 C 30.492718 55.499469 31.527178 55.105469 32.560547 55.105469 z M 67.710938 55.105469 C 68.744306 55.105469 69.776813 55.499469 70.564453 56.287109 L 74.083984 59.806641 L 77.603516 63.324219 C 79.179719 64.900457 79.179719 67.458918 77.603516 69.035156 L 74.083984 72.552734 L 70.564453 76.072266 C 68.989173 77.647546 66.430749 77.647546 64.855469 76.072266 L 61.335938 72.552734 L 57.816406 69.035156 C 56.241126 67.458918 56.241126 64.900457 57.816406 63.324219 L 61.335938 59.806641 L 64.855469 56.287109 C 65.643109 55.499469 66.677569 55.105469 67.710938 55.105469 z M 50.261719 70.865234 C 51.295087 70.865234 52.329547 71.259235 53.117188 72.046875 L 56.636719 75.566406 L 60.154297 79.083984 C 61.729577 80.660223 61.729577 83.218683 60.154297 84.794922 L 56.636719 88.3125 L 53.117188 91.832031 C 51.541907 93.407312 48.983484 93.407312 47.408203 91.832031 L 43.888672 88.3125 L 40.369141 84.794922 C 38.79386 83.218683 38.79386 80.660223 40.369141 79.083984 L 43.888672 75.566406 L 47.408203 72.046875 C 48.195843 71.259235 49.22835 70.865234 50.261719 70.865234 z " + transform="matrix(0.28183552,0,0,0.28183552,176.52729,140.19608)" + id="path79" /> + </g> + </g> +</svg> diff --git a/plugins/rdp/scalable/emblems/org.remmina.Remmina-rdp-symbolic.svg b/plugins/rdp/scalable/emblems/org.remmina.Remmina-rdp-symbolic.svg new file mode 100644 index 0000000..af2b970 --- /dev/null +++ b/plugins/rdp/scalable/emblems/org.remmina.Remmina-rdp-symbolic.svg @@ -0,0 +1,112 @@ +<?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-rdp-symbolic.svg"> + <defs + id="defs2" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:zoom="2.0986883" + inkscape:cx="-49.823663" + inkscape:cy="43.808883" + 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="1600" + inkscape:window-height="879" + inkscape:window-x="0" + inkscape:window-y="0" + 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="-190.61239" + originy="-78.157086" /> + </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(-190.61238,-192.38458)"> + <g + transform="matrix(0.26458333,0,0,0.26458333,153.78079,124.26165)" + style="isolation:isolate" + id="RDP Icons"> + <g + id="Group" /> + <g + id="g50"> + <path + style="fill:#000000" + d="M 50 0.41992188 C 22.636004 0.41992188 0.41992185 22.636004 0.41992188 50 C 0.41992188 77.363996 22.636004 99.580078 50 99.580078 C 77.363996 99.580078 99.580078 77.363996 99.580078 50 C 99.580078 22.636004 77.363996 0.41992185 50 0.41992188 z M 50.339844 15.697266 C 51.446721 15.697266 52.552988 16.117441 53.396484 16.960938 L 58.603516 22.167969 L 63.808594 27.373047 C 65.496606 29.061022 65.496606 31.800306 63.808594 33.488281 L 58.603516 38.695312 L 53.396484 43.900391 C 51.709492 45.587421 48.971216 45.587421 47.283203 43.900391 L 42.078125 38.695312 L 36.871094 33.488281 C 35.183119 31.800306 35.183119 29.061022 36.871094 27.373047 L 42.078125 22.167969 L 47.283203 16.960938 C 48.127209 16.117441 49.232967 15.697266 50.339844 15.697266 z M 27.388672 34.402344 C 28.495544 34.402344 29.603259 34.824473 30.447266 35.667969 L 35.652344 40.873047 L 40.857422 46.080078 C 42.545397 47.76707 42.545397 50.507303 40.857422 52.195312 L 35.652344 57.400391 L 30.447266 62.607422 C 28.759253 64.294414 26.019061 64.294414 24.332031 62.607422 L 19.126953 57.400391 L 13.919922 52.195312 C 12.231909 50.507303 12.231909 47.76707 13.919922 46.080078 L 19.126953 40.873047 L 24.332031 35.667969 C 25.175546 34.824473 26.281799 34.402344 27.388672 34.402344 z M 72.611328 34.402344 C 73.718201 34.402344 74.824473 34.824473 75.667969 35.667969 L 80.875 40.873047 L 86.080078 46.080078 C 87.767108 47.76707 87.767108 50.507301 86.080078 52.195312 L 80.875 57.400391 L 75.667969 62.607422 C 73.980977 64.294414 71.240709 64.294414 69.552734 62.607422 L 64.347656 57.400391 L 59.140625 52.195312 C 57.453633 50.507301 57.453633 47.76707 59.140625 46.080078 L 64.347656 40.873047 L 69.552734 35.667969 C 70.396722 34.824473 71.504456 34.402344 72.611328 34.402344 z M 50.339844 54.833984 C 51.446721 54.833984 52.552988 55.255603 53.396484 56.099609 L 58.603516 61.304688 L 63.808594 66.511719 C 65.496606 68.198711 65.496606 70.938008 63.808594 72.625 L 58.603516 77.832031 L 53.396484 83.037109 C 51.709492 84.725122 48.971216 84.725122 47.283203 83.037109 L 42.078125 77.832031 L 36.871094 72.625 C 35.183119 70.938008 35.183119 68.198711 36.871094 66.511719 L 42.078125 61.304688 L 47.283203 56.099609 C 48.127209 55.255603 49.232967 54.833984 50.339844 54.833984 z " + transform="translate(139.20601,257.47249)" + id="path48" /> + </g> + <g + id="g62"> + <g + id="g52" /> + <g + id="g54" /> + <g + id="g56" /> + <g + id="g60"> + <g + id="g58" /> + </g> + </g> + </g> + <g + id="g831" + transform="translate(0.20117925,-0.3392995)" /> + </g> +</svg> diff --git a/plugins/secret/CMakeLists.txt b/plugins/secret/CMakeLists.txt new file mode 100644 index 0000000..7de80a9 --- /dev/null +++ b/plugins/secret/CMakeLists.txt @@ -0,0 +1,50 @@ +# remmina-plugin-secret - The GTK+ Remote Desktop Client +# +# Copyright (C) 2011 Marc-Andre Moreau +# Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo +# +# 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. + + +find_suggested_package(Libsecret) +if(LIBSECRET_FOUND) + set(REMMINA_PLUGIN_SECRET_SRCS src/glibsecret_plugin.c) + + add_library(remmina-plugin-secret MODULE ${REMMINA_PLUGIN_SECRET_SRCS}) + set_target_properties(remmina-plugin-secret PROPERTIES PREFIX "") + set_target_properties(remmina-plugin-secret PROPERTIES NO_SONAME 1) + + include_directories(${GTK3_INCLUDE_DIRS}) + target_link_libraries(remmina-plugin-secret ${GTK_LIBRARIES}) + + include_directories(SYSTEM ${LIBSECRET_INCLUDE_DIRS}) + target_link_libraries(remmina-plugin-secret ${LIBSECRET_LIBRARIES} ${GLIB_LIBRARY}) + + install(TARGETS remmina-plugin-secret DESTINATION ${REMMINA_PLUGINDIR}) +endif() diff --git a/plugins/secret/src/glibsecret_plugin.c b/plugins/secret/src/glibsecret_plugin.c new file mode 100644 index 0000000..dc6d8cc --- /dev/null +++ b/plugins/secret/src/glibsecret_plugin.c @@ -0,0 +1,217 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2011 Vic Lee + * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo + * + * 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 "config.h" +#include "glibsecret_plugin.h" +#include <gtk/gtk.h> +#include <glib.h> +#include <glib/gi18n-lib.h> +#include <glib/gstdio.h> +#include <libsecret/secret.h> +#include <remmina/plugin.h> + +static RemminaPluginService *remmina_plugin_service = NULL; +#define REMMINA_PLUGIN_DEBUG(fmt, ...) remmina_plugin_service->_remmina_debug(__func__, fmt, ##__VA_ARGS__) + + +static SecretSchema remmina_file_secret_schema = +{ "org.remmina.Password", SECRET_SCHEMA_NONE, + { + { "filename", SECRET_SCHEMA_ATTRIBUTE_STRING }, + { "key", SECRET_SCHEMA_ATTRIBUTE_STRING }, + { NULL, 0 } + } }; + + +#ifdef LIBSECRET_VERSION_0_18 +static SecretService* secretservice; +static SecretCollection* defaultcollection; +#endif + + +gboolean remmina_plugin_glibsecret_is_service_available(RemminaSecretPlugin* plugin) +{ +#ifdef LIBSECRET_VERSION_0_18 + if (secretservice && defaultcollection) + return TRUE; + else + return FALSE; +#else + return FALSE; +#endif +} + +static void remmina_plugin_glibsecret_unlock_secret_service(RemminaSecretPlugin* plugin) +{ + TRACE_CALL(__func__); + +#ifdef LIBSECRET_VERSION_0_18 + + GError *error = NULL; + GList *objects, *ul; + gchar* lbl; + + if (secretservice && defaultcollection) { + if (secret_collection_get_locked(defaultcollection)) { + lbl = secret_collection_get_label(defaultcollection); + REMMINA_PLUGIN_DEBUG("Requesting unlock of the default '%s' collection", lbl); + objects = g_list_append(NULL, defaultcollection); + secret_service_unlock_sync(secretservice, objects, NULL, &ul, &error); + g_list_free(objects); + g_list_free(ul); + } + } +#endif + return; +} + +void remmina_plugin_glibsecret_store_password(RemminaSecretPlugin* plugin, RemminaFile *remminafile, const gchar *key, const gchar *password) +{ + TRACE_CALL(__func__); + GError *r = NULL; + const gchar *path; + gchar *s; + + path = remmina_plugin_service->file_get_path(remminafile); + s = g_strdup_printf("Remmina: %s - %s", remmina_plugin_service->file_get_string(remminafile, "name"), key); + secret_password_store_sync(&remmina_file_secret_schema, SECRET_COLLECTION_DEFAULT, s, password, + NULL, &r, "filename", path, "key", key, NULL); + g_free(s); + if (r == NULL) { + REMMINA_PLUGIN_DEBUG("Password “%s” saved for file %s\n", key, path); + }else { + REMMINA_PLUGIN_DEBUG("Password “%s” cannot be saved for file %s\n", key, path); + g_error_free(r); + } +} + +gchar* +remmina_plugin_glibsecret_get_password(RemminaSecretPlugin* plugin, RemminaFile *remminafile, const gchar *key) +{ + TRACE_CALL(__func__); + GError *r = NULL; + const gchar *path; + gchar *password; + gchar *p; + + path = remmina_plugin_service->file_get_path(remminafile); + password = secret_password_lookup_sync(&remmina_file_secret_schema, NULL, &r, "filename", path, "key", key, NULL); + if (r == NULL) { + p = g_strdup(password); + secret_password_free(password); + return p; + }else { + REMMINA_PLUGIN_DEBUG("Password cannot be found for file %s\n", path); + return NULL; + } +} + +void remmina_plugin_glibsecret_delete_password(RemminaSecretPlugin* plugin, RemminaFile *remminafile, const gchar *key) +{ + TRACE_CALL(__func__); + GError *r = NULL; + const gchar *path; + + path = remmina_plugin_service->file_get_path(remminafile); + secret_password_clear_sync(&remmina_file_secret_schema, NULL, &r, "filename", path, "key", key, NULL); + if (r == NULL) + REMMINA_PLUGIN_DEBUG("password “%s” deleted for file %s", key, path); + else + REMMINA_PLUGIN_DEBUG("password “%s” cannot be deleted for file %s", key, path); +} + +gboolean remmina_plugin_glibsecret_init(RemminaSecretPlugin* plugin) +{ +#ifdef LIBSECRET_VERSION_0_18 + GError *error; + error = NULL; + secretservice = secret_service_get_sync(SECRET_SERVICE_LOAD_COLLECTIONS, NULL, &error); + if (error) { + g_print("[glibsecret] unable to get secret service: %s\n", error->message); + return FALSE; + } + if (secretservice == NULL) { + g_print("[glibsecret] unable to get secret service: Unknown error.\n"); + return FALSE; + } + + defaultcollection = secret_collection_for_alias_sync(secretservice, SECRET_COLLECTION_DEFAULT, SECRET_COLLECTION_NONE, NULL, &error); + if (error) { + g_print("[glibsecret] unable to get secret service default collection: %s\n", error->message); + return FALSE; + } + + remmina_plugin_glibsecret_unlock_secret_service(plugin); + return TRUE; + +#else + g_print("Libsecret was too old during compilation, disabling secret service.\n"); + return FALSE; +#endif +} + +static RemminaSecretPlugin remmina_plugin_glibsecret = +{ REMMINA_PLUGIN_TYPE_SECRET, + "glibsecret", + N_("Secured password storage in the GNOME keyring"), + NULL, + VERSION, + 2000, + remmina_plugin_glibsecret_init, + remmina_plugin_glibsecret_is_service_available, + remmina_plugin_glibsecret_store_password, + remmina_plugin_glibsecret_get_password, + remmina_plugin_glibsecret_delete_password +}; + +G_MODULE_EXPORT gboolean +remmina_plugin_entry(RemminaPluginService *service) +{ + TRACE_CALL(__func__); + + /* This function should only register the secret plugin. No init action + * should be performed here. Initialization will be done later + * with remmina_plugin_xxx_init() . */ + + remmina_plugin_service = service; + + if (!service->register_plugin((RemminaPlugin*)&remmina_plugin_glibsecret)) { + return FALSE; + } + + return TRUE; + +} + diff --git a/plugins/secret/src/glibsecret_plugin.h b/plugins/secret/src/glibsecret_plugin.h new file mode 100644 index 0000000..37099b2 --- /dev/null +++ b/plugins/secret/src/glibsecret_plugin.h @@ -0,0 +1,40 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2015-2023 Antenore Gatta, Giovanni Panozzo + * + * 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. + * + */ + +#ifndef __BACKEND_LIBSECRET_H__ +#define __BACKEND_LIBSECRET_H__ + +#include <libsecret/secret.h> + +#endif // __BACKEND_LIBSECRET_H__ 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); +} diff --git a/plugins/telepathy/CMakeLists.txt b/plugins/telepathy/CMakeLists.txt new file mode 100644 index 0000000..0314368 --- /dev/null +++ b/plugins/telepathy/CMakeLists.txt @@ -0,0 +1,66 @@ +# remmina-plugin-telepathy - The GTK+ Remote Desktop Client +# +# Copyright (C) 2011 Marc-Andre Moreau +# Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo +# +# 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_TELEPATHY_SRCS + telepathy_plugin.c + telepathy_handler.c + telepathy_channel_handler.c + ) + +add_library(remmina-plugin-telepathy MODULE ${REMMINA_PLUGIN_TELEPATHY_SRCS}) +set_target_properties(remmina-plugin-telepathy PROPERTIES PREFIX "") +set_target_properties(remmina-plugin-telepathy PROPERTIES NO_SONAME 1) + +find_required_package(GTK3) +pkg_check_modules(PC_DBUS_GLIB dbus-glib-1) + +include_directories(${REMMINA_COMMON_INCLUDE_DIRS} ${TELEPATHY_INCLUDE_DIRS}) +target_link_libraries(remmina-plugin-telepathy ${REMMINA_COMMON_LIBRARIES} + ${TELEPATHY_LIBRARIES} ${PC_DBUS_GLIB_LIBRARIES}) + +install(TARGETS remmina-plugin-telepathy DESTINATION ${REMMINA_PLUGINDIR}) + +# Telepathy client file +install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/Remmina.client + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/telepathy/clients) + +# DBus activation file +if(NOT REMMINA_BINARY_PATH) + set(REMMINA_BINARY_PATH ${CMAKE_INSTALL_FULL_BINDIR}/remmina) +endif() +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/org.freedesktop.Telepathy.Client.Remmina.service.in + ${CMAKE_CURRENT_BINARY_DIR}/org.freedesktop.Telepathy.Client.Remmina.service @ONLY) + +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.freedesktop.Telepathy.Client.Remmina.service + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/dbus-1/services) diff --git a/plugins/telepathy/Remmina.client b/plugins/telepathy/Remmina.client new file mode 100644 index 0000000..e5baf37 --- /dev/null +++ b/plugins/telepathy/Remmina.client @@ -0,0 +1,8 @@ +[org.freedesktop.Telepathy.Client] +Interfaces=org.freedesktop.Telepathy.Client.Handler;org.freedesktop.Telepathy.Client.Interface.Requests + +[org.freedesktop.Telepathy.Client.Handler.HandlerChannelFilter 0] +org.freedesktop.Telepathy.Channel.ChannelType s=org.freedesktop.Telepathy.Channel.Type.StreamTube +org.freedesktop.Telepathy.Channel.TargetHandleType u=1 +org.freedesktop.Telepathy.Channel.Requested b=false +org.freedesktop.Telepathy.Channel.Type.StreamTube.Service s=rfb diff --git a/plugins/telepathy/org.freedesktop.Telepathy.Client.Remmina.service.in b/plugins/telepathy/org.freedesktop.Telepathy.Client.Remmina.service.in new file mode 100644 index 0000000..95c0cf7 --- /dev/null +++ b/plugins/telepathy/org.freedesktop.Telepathy.Client.Remmina.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=org.freedesktop.Telepathy.Client.Remmina +Exec=@REMMINA_BINARY_PATH@ -x telepathy diff --git a/plugins/telepathy/telepathy_channel_handler.c b/plugins/telepathy/telepathy_channel_handler.c new file mode 100644 index 0000000..b690d97 --- /dev/null +++ b/plugins/telepathy/telepathy_channel_handler.c @@ -0,0 +1,373 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2010 Vic Lee + * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * 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 "common/remmina_plugin.h" +#include <telepathy-glib/account.h> +#include <telepathy-glib/channel.h> +#include <telepathy-glib/contact.h> +#include <telepathy-glib/dbus.h> +#include <telepathy-glib/defs.h> +#include <telepathy-glib/handle.h> +#include <telepathy-glib/interfaces.h> +#include <telepathy-glib/svc-client.h> +#include <telepathy-glib/util.h> +#include "telepathy_channel_handler.h" + +extern RemminaPluginService *remmina_plugin_telepathy_service; + +typedef struct _RemminaTpChannelHandler { + gchar *connection_path; + gchar *channel_path; + GHashTable *channel_properties; + DBusGMethodInvocation *context; + + GtkWidget *proto_widget; + guint disconnect_handler; + + TpDBusDaemon *bus; + TpAccount *account; + TpConnection *connection; + TpChannel *channel; + + gchar *alias; + gchar *host; + guint port; + gchar *protocol; +} RemminaTpChannelHandler; + +static void remmina_tp_channel_handler_free(RemminaTpChannelHandler *chandler) +{ + TRACE_CALL(__func__); + if (chandler->disconnect_handler) { + g_signal_handler_disconnect(chandler->proto_widget, chandler->disconnect_handler); + chandler->disconnect_handler = 0; + } + g_free(chandler->connection_path); + g_free(chandler->channel_path); + g_hash_table_destroy(chandler->channel_properties); + if (chandler->bus) { + g_object_unref(chandler->bus); + } + if (chandler->account) { + g_object_unref(chandler->account); + } + if (chandler->connection) { + g_object_unref(chandler->connection); + } + if (chandler->channel) { + g_object_unref(chandler->channel); + } + if (chandler->alias) { + g_free(chandler->alias); + } + if (chandler->host) { + g_free(chandler->host); + } + if (chandler->protocol) { + g_free(chandler->protocol); + } + g_free(chandler); +} + +static void remmina_tp_channel_handler_channel_closed(TpChannel *channel, gpointer user_data, GObject *self) +{ + TRACE_CALL(__func__); + RemminaTpChannelHandler *chandler = (RemminaTpChannelHandler*)user_data; + + g_print("%s: %s\n", __func__, chandler->channel_path); + remmina_tp_channel_handler_free(chandler); +} + +static void remmina_tp_channel_handler_on_disconnect(GtkWidget *widget, RemminaTpChannelHandler *chandler) +{ + TRACE_CALL(__func__); + g_print("%s: %s\n", __func__, chandler->channel_path); + g_signal_handler_disconnect(widget, chandler->disconnect_handler); + chandler->disconnect_handler = 0; + tp_cli_channel_call_close(chandler->channel, -1, NULL, NULL, NULL, NULL); +} + +static void remmina_tp_channel_handler_connect(RemminaTpChannelHandler *chandler) +{ + TRACE_CALL(__func__); + RemminaFile *remminafile; + gchar *s; + + remminafile = remmina_plugin_telepathy_service->file_new(); + remmina_plugin_telepathy_service->file_set_string(remminafile, "name", chandler->alias); + remmina_plugin_telepathy_service->file_set_string(remminafile, "protocol", chandler->protocol); + s = g_strdup_printf("[%s]:%i", chandler->host, chandler->port); + remmina_plugin_telepathy_service->file_set_string(remminafile, "server", s); + g_free(s); + remmina_plugin_telepathy_service->file_set_int(remminafile, "colordepth", 8); + + g_free(chandler->alias); + chandler->alias = NULL; + g_free(chandler->protocol); + chandler->protocol = NULL; + + chandler->proto_widget = remmina_plugin_telepathy_service->open_connection(remminafile, + G_CALLBACK(remmina_tp_channel_handler_on_disconnect), chandler, &chandler->disconnect_handler); +} + +static void remmina_tp_channel_handler_get_service(TpProxy *channel, const GValue *service, const GError *error, + gpointer user_data, GObject *weak_object) +{ + TRACE_CALL(__func__); + RemminaTpChannelHandler *chandler = (RemminaTpChannelHandler*)user_data; + const gchar *svc; + + if (error != NULL) { + g_print("%s: %s", __func__, error->message); + remmina_tp_channel_handler_free(chandler); + return; + } + svc = g_value_get_string(service); + g_print("%s: %s %s:%u\n", __func__, svc, chandler->host, chandler->port); + + if (g_strcmp0(svc, "rfb") == 0) { + chandler->protocol = g_strdup("VNC"); + }else { + chandler->protocol = g_ascii_strup(svc, -1); + } + remmina_tp_channel_handler_connect(chandler); +} + +static void remmina_tp_channel_handler_accept(TpChannel *channel, const GValue *address, const GError *error, + gpointer user_data, GObject *weak_object) +{ + TRACE_CALL(__func__); + RemminaTpChannelHandler *chandler = (RemminaTpChannelHandler*)user_data; + + if (error != NULL) { + g_print("%s: %s", __func__, error->message); + remmina_tp_channel_handler_free(chandler); + return; + } + + dbus_g_type_struct_get(address, 0, &chandler->host, 1, &chandler->port, G_MAXUINT); + + tp_cli_dbus_properties_call_get(channel, -1, TP_IFACE_CHANNEL_TYPE_STREAM_TUBE, "Service", + remmina_tp_channel_handler_get_service, chandler, NULL, NULL); +} + +static void remmina_tp_channel_handler_on_response(GtkDialog *dialog, gint response_id, RemminaTpChannelHandler *chandler) +{ + TRACE_CALL(__func__); + GValue noop = + { 0 }; + GError *error; + + if (response_id == GTK_RESPONSE_YES) { + g_value_init(&noop, G_TYPE_INT); + tp_cli_channel_type_stream_tube_call_accept(chandler->channel, -1, TP_SOCKET_ADDRESS_TYPE_IPV4, + TP_SOCKET_ACCESS_CONTROL_LOCALHOST, &noop, remmina_tp_channel_handler_accept, chandler, NULL, + NULL); + g_value_unset(&noop); + tp_svc_client_handler_return_from_handle_channels(chandler->context); + }else { + error = g_error_new(TP_ERROR, TP_ERROR_NOT_AVAILABLE, "Channel rejected by user."); + dbus_g_method_return_error(chandler->context, error); + g_error_free(error); + remmina_tp_channel_handler_free(chandler); + } +} + +static void remmina_tp_channel_handler_get_contacts(TpConnection *connection, guint n_contacts, TpContact * const *contacts, + guint n_failed, const TpHandle *failed, const GError *error, gpointer user_data, GObject *weak_object) +{ + TRACE_CALL(__func__); + RemminaTpChannelHandler *chandler = (RemminaTpChannelHandler*)user_data; + TpContact *contact; + gchar *token; + const gchar *cm; + const gchar *protocol; + gchar *filename; + GdkPixbuf *pixbuf; + GtkWidget *image; + GtkWidget *dialog; + + if (error != NULL) { + g_print("%s: %s", __func__, error->message); + remmina_tp_channel_handler_free(chandler); + return; + } + if (n_contacts <= 0) { + g_print("%s: no contacts\n", __func__); + remmina_tp_channel_handler_free(chandler); + return; + } + contact = contacts[0]; + chandler->alias = g_strdup(tp_contact_get_alias(contact)); + + dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO, + _("%s wants to share their desktop.\nDo you accept?"), chandler->alias); + g_signal_connect(G_OBJECT(dialog), "response", G_CALLBACK(remmina_tp_channel_handler_on_response), chandler); + g_signal_connect(G_OBJECT(dialog), "response", G_CALLBACK(gtk_widget_destroy), NULL); + gtk_window_set_title(GTK_WINDOW(dialog), _("Desktop sharing invitation")); + remmina_plugin_telepathy_service->ui_register(dialog); + gtk_widget_show(dialog); + + token = (gchar*)tp_contact_get_avatar_token(contact); + if (token == NULL) { + return; + } + protocol = tp_connection_get_protocol_name(chandler->connection); + cm = tp_connection_get_cm_name(chandler->connection); + if (!protocol || !cm) { + return; + } + token = tp_escape_as_identifier(token); + filename = g_build_filename(g_get_user_cache_dir(), "telepathy", "avatars", cm, protocol, token, NULL); + g_free(token); + if (g_file_test(filename, G_FILE_TEST_EXISTS)) { + pixbuf = gdk_pixbuf_new_from_file(filename, NULL); + if (pixbuf) { + image = gtk_image_new_from_pixbuf(pixbuf); + gtk_widget_show(image); + g_object_unref(pixbuf); + gtk_message_dialog_set_image(GTK_MESSAGE_DIALOG(dialog), image); + } + } + g_free(filename); +} + +static void remmina_tp_channel_handler_channel_ready(TpChannel *channel, const GError *channel_error, gpointer user_data) +{ + TRACE_CALL(__func__); + RemminaTpChannelHandler *chandler = (RemminaTpChannelHandler*)user_data; + TpHandle handle; + GError *error = NULL; + TpContactFeature features[] = + { TP_CONTACT_FEATURE_ALIAS, TP_CONTACT_FEATURE_AVATAR_TOKEN }; + + if (channel_error != NULL) { + g_print("%s: %s\n", __func__, channel_error->message); + remmina_tp_channel_handler_free(chandler); + return; + } + + if (tp_cli_channel_connect_to_closed(channel, remmina_tp_channel_handler_channel_closed, chandler, NULL, NULL, &error) + == NULL) { + g_print("tp_cli_channel_connect_to_closed: %s\n", channel_error->message); + remmina_tp_channel_handler_free(chandler); + return; + } + g_print("%s: %s\n", __func__, chandler->channel_path); + + handle = tp_channel_get_handle(channel, NULL); + tp_connection_get_contacts_by_handle(chandler->connection, 1, &handle, G_N_ELEMENTS(features), features, + remmina_tp_channel_handler_get_contacts, chandler, NULL, NULL); +} + +static void remmina_tp_channel_handler_connection_ready(TpConnection *connection, const GError *connection_error, + gpointer user_data) +{ + TRACE_CALL(__func__); + RemminaTpChannelHandler *chandler = (RemminaTpChannelHandler*)user_data; + GError *error = NULL; + + if (connection_error != NULL) { + g_print("%s: %s\n", __func__, connection_error->message); + remmina_tp_channel_handler_free(chandler); + return; + } + + chandler->channel = tp_channel_new_from_properties(connection, chandler->channel_path, chandler->channel_properties, + &error); + if (chandler->channel == NULL) { + g_print("tp_channel_new_from_properties: %s\n", error->message); + remmina_tp_channel_handler_free(chandler); + return; + } + tp_channel_call_when_ready(chandler->channel, remmina_tp_channel_handler_channel_ready, chandler); +} + +static void remmina_tp_channel_handler_account_ready(GObject *account, GAsyncResult *res, gpointer user_data) +{ + TRACE_CALL(__func__); + RemminaTpChannelHandler *chandler = (RemminaTpChannelHandler*)user_data; + GError *error = NULL; + + if (!tp_account_prepare_finish(TP_ACCOUNT(account), res, &error)) { + g_print("tp_account_prepare_finish: %s\n", error->message); + remmina_tp_channel_handler_free(chandler); + return; + } + + chandler->connection = tp_connection_new(chandler->bus, NULL, chandler->connection_path, &error); + if (chandler->connection == NULL) { + g_print("tp_connection_new: %s\n", error->message); + remmina_tp_channel_handler_free(chandler); + return; + } + tp_connection_call_when_ready(chandler->connection, remmina_tp_channel_handler_connection_ready, chandler); +} + +void remmina_tp_channel_handler_new(const gchar *account_path, const gchar *connection_path, const gchar *channel_path, + GHashTable *channel_properties, DBusGMethodInvocation *context) +{ + TRACE_CALL(__func__); + TpDBusDaemon *bus; + TpAccount *account; + GError *error = NULL; + RemminaTpChannelHandler *chandler; + + bus = tp_dbus_daemon_dup(&error); + if (bus == NULL) { + g_print("tp_dbus_daemon_dup: %s", error->message); + return; + } + account = tp_account_new(bus, account_path, &error); + if (account == NULL) { + g_object_unref(bus); + g_print("tp_account_new: %s", error->message); + return; + } + + chandler = g_new0(RemminaTpChannelHandler, 1); + chandler->bus = bus; + chandler->account = account; + chandler->connection_path = g_strdup(connection_path); + chandler->channel_path = g_strdup(channel_path); + chandler->channel_properties = tp_asv_new(NULL, NULL); + tp_g_hash_table_update(chandler->channel_properties, channel_properties, (GBoxedCopyFunc)g_strdup, + (GBoxedCopyFunc)tp_g_value_slice_dup); + chandler->context = context; + + tp_account_prepare_async(account, NULL, remmina_tp_channel_handler_account_ready, chandler); +} + diff --git a/plugins/telepathy/telepathy_channel_handler.h b/plugins/telepathy/telepathy_channel_handler.h new file mode 100644 index 0000000..675dcae --- /dev/null +++ b/plugins/telepathy/telepathy_channel_handler.h @@ -0,0 +1,43 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2010 Vic Lee + * Copyright (C) 2017-2023 Antenore Gatta, Giovanni Panozzo + * + * 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 + +G_BEGIN_DECLS + +void +remmina_tp_channel_handler_new(const gchar *account_path, const gchar *connection_path, const gchar *channel_path, GHashTable *channel_properties, DBusGMethodInvocation *context); + +G_END_DECLS diff --git a/plugins/telepathy/telepathy_handler.c b/plugins/telepathy/telepathy_handler.c new file mode 100644 index 0000000..5b3ef6f --- /dev/null +++ b/plugins/telepathy/telepathy_handler.c @@ -0,0 +1,128 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2010 Vic Lee + * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * 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 "common/remmina_plugin.h" +#include <gmodule.h> +#include <telepathy-glib/dbus.h> +#include <telepathy-glib/defs.h> +#include <telepathy-glib/svc-client.h> +#include "telepathy_channel_handler.h" +#include "telepathy_handler.h" + +extern RemminaPluginService *remmina_plugin_telepathy_service; + +#define REMMINA_TP_BUS_NAME TP_CLIENT_BUS_NAME_BASE "Remmina" +#define REMMINA_TP_OBJECT_PATH TP_CLIENT_OBJECT_PATH_BASE "Remmina" + +static void remmina_tp_handler_iface_init(gpointer g_iface, gpointer iface_data); + +G_DEFINE_TYPE_WITH_CODE(RemminaTpHandler, remmina_tp_handler, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(TP_TYPE_SVC_CLIENT, NULL); + G_IMPLEMENT_INTERFACE( TP_TYPE_SVC_CLIENT_HANDLER, remmina_tp_handler_iface_init); + ); + +static void remmina_tp_handler_class_init(RemminaTpHandlerClass *klass) +{ + TRACE_CALL(__func__); +} + +static void remmina_tp_handler_init(RemminaTpHandler *handler) +{ + TRACE_CALL(__func__); +} + +static void remmina_tp_handler_handle_channels(TpSvcClientHandler *handler, const char *account_path, + const char *connection_path, const GPtrArray *channels, const GPtrArray *requests_satisfied, + guint64 user_action_time, GHashTable *handler_info, DBusGMethodInvocation *context) +{ + TRACE_CALL(__func__); + gint i; + GArray *array; + + for (i = 0; i < channels->len; i++) { + array = g_ptr_array_index(channels, i); + remmina_tp_channel_handler_new(account_path, connection_path, + (const gchar*)g_value_get_boxed(&g_array_index(array, GValue, 0)), + (GHashTable*)g_value_get_boxed(&g_array_index(array, GValue, 1)), context); + } +} + +static void remmina_tp_handler_iface_init(gpointer g_iface, gpointer iface_data) +{ + TRACE_CALL(__func__); + TpSvcClientHandlerClass *klass = (TpSvcClientHandlerClass*)g_iface; + +#define IMPLEMENT(x) tp_svc_client_handler_implement_ ## x(klass, remmina_tp_handler_ ## x) + IMPLEMENT(handle_channels); +#undef IMPLEMENT +} + +static gboolean remmina_tp_handler_register(RemminaTpHandler *handler) +{ + TRACE_CALL(__func__); + TpDBusDaemon *bus; + GError *error = NULL; + + bus = tp_dbus_daemon_dup(&error); + if (bus == NULL) { + g_print("tp_dbus_daemon_dup: %s", error->message); + return FALSE; + } + if (!tp_dbus_daemon_request_name(bus, REMMINA_TP_BUS_NAME, FALSE, &error)) { + g_object_unref(bus); + g_print("tp_dbus_daemon_request_name: %s", error->message); + return FALSE; + } + dbus_g_connection_register_g_object( + tp_proxy_get_dbus_connection(TP_PROXY(bus)), + REMMINA_TP_OBJECT_PATH, G_OBJECT(handler)); + g_object_unref(bus); + g_print("%s: bus_name " REMMINA_TP_BUS_NAME + " object_path " REMMINA_TP_OBJECT_PATH "\n", __func__); + return TRUE; +} + +RemminaTpHandler* +remmina_tp_handler_new(void) +{ + TRACE_CALL(__func__); + RemminaTpHandler *handler; + + handler = REMMINA_TP_HANDLER(g_object_new(REMMINA_TYPE_TP_HANDLER, NULL)); + remmina_tp_handler_register(handler); + return handler; +} + diff --git a/plugins/telepathy/telepathy_handler.h b/plugins/telepathy/telepathy_handler.h new file mode 100644 index 0000000..c4bfb69 --- /dev/null +++ b/plugins/telepathy/telepathy_handler.h @@ -0,0 +1,57 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2010 Vic Lee + * Copyright (C) 2017-2023 Antenore Gatta, Giovanni Panozzo + * + * 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 + +G_BEGIN_DECLS + +#define REMMINA_TYPE_TP_HANDLER (remmina_tp_handler_get_type()) +#define REMMINA_TP_HANDLER(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), REMMINA_TYPE_TP_HANDLER, RemminaTpHandler)) +#define REMMINA_TP_HANDLER_CLASS(obj) (G_TYPE_CHECK_CLASS_CAST((obj), REMMINA_TYPE_TP_HANDLER, RemminaTpHandlerClass)) +#define REMMINA_IS_TP_HANDLER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), REMMINA_TYPE_TP_HANDLER)) +#define REMMINA_IS_TP_HANDLER_CLASS(obj) (G_TYPE_CHECK_CLASS_TYPE((obj), REMMINA_TYPE_TP_HANDLER)) +#define REMMINA_TP_HANDLER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), REMMINA_TYPE_TP_HANDLER, RemminaTpHandlerClass)) + +typedef struct _RemminaTpHandler { + GObject parent; +} RemminaTpHandler; + +typedef struct _RemminaTpHandlerClass { + GObjectClass parent_class; +} RemminaTpHandlerClass; + +RemminaTpHandler *remmina_tp_handler_new(void); + +G_END_DECLS diff --git a/plugins/telepathy/telepathy_plugin.c b/plugins/telepathy/telepathy_plugin.c new file mode 100644 index 0000000..263096b --- /dev/null +++ b/plugins/telepathy/telepathy_plugin.c @@ -0,0 +1,77 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2010 Vic Lee + * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * 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 "common/remmina_plugin.h" +#include "telepathy_handler.h" + +RemminaPluginService *remmina_plugin_telepathy_service = NULL; + +static RemminaTpHandler *remmina_tp_handler = NULL; + +void remmina_plugin_telepathy_entry(void) +{ + TRACE_CALL(__func__); + if (remmina_tp_handler == NULL) { + remmina_tp_handler = remmina_tp_handler_new(); + } +} + +/* Entry plugin definition and features */ +static RemminaEntryPlugin remmina_plugin_telepathy = +{ + REMMINA_PLUGIN_TYPE_ENTRY, // Type + "telepathy", // Name + N_("Telepathy - Desktop Sharing"), // Description + GETTEXT_PACKAGE, // Translation domain + VERSION, // Version number + remmina_plugin_telepathy_entry // Plugin entry function +}; + +G_MODULE_EXPORT gboolean +remmina_plugin_entry(RemminaPluginService *service) +{ + TRACE_CALL(__func__); + remmina_plugin_telepathy_service = service; + + bindtextdomain(GETTEXT_PACKAGE, REMMINA_RUNTIME_LOCALEDIR); + bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8"); + + if (!service->register_plugin((RemminaPlugin*)&remmina_plugin_telepathy)) { + return FALSE; + } + return TRUE; +} + diff --git a/plugins/tool_hello_world/CMakeLists.txt b/plugins/tool_hello_world/CMakeLists.txt new file mode 100644 index 0000000..e2c9537 --- /dev/null +++ b/plugins/tool_hello_world/CMakeLists.txt @@ -0,0 +1,55 @@ +# remmina-plugin-tool_hello_world - The GTK+ Remote Desktop Client +# +# Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo +# Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo +# +# 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_SRCS + plugin_config.h + plugin.c +) + +add_library(remmina-plugin-tool_hello_world MODULE ${REMMINA_PLUGIN_SRCS}) +set_target_properties(remmina-plugin-tool_hello_world PROPERTIES PREFIX "") +set_target_properties(remmina-plugin-tool_hello_world PROPERTIES NO_SONAME 1) + +include_directories(${REMMINA_COMMON_INCLUDE_DIRS}) +target_link_libraries(remmina-plugin-tool_hello_world ${REMMINA_COMMON_LIBRARIES}) + +install(TARGETS remmina-plugin-tool_hello_world DESTINATION ${REMMINA_PLUGINDIR}) + +install(FILES + scalable/emblems/org.remmina.Remmina-tool-symbolic.svg + DESTINATION ${APPICONSCALE_EMBLEMS_DIR}) + +if(WITH_ICON_CACHE) + gtk_update_icon_cache("${REMMINA_DATADIR}/icons/hicolor") +endif() diff --git a/plugins/tool_hello_world/plugin.c b/plugins/tool_hello_world/plugin.c new file mode 100644 index 0000000..b852fd0 --- /dev/null +++ b/plugins/tool_hello_world/plugin.c @@ -0,0 +1,130 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2010 Vic Lee + * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * 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 "plugin_config.h" + +#include "common/remmina_plugin.h" + +static RemminaPluginService *remmina_plugin_service = NULL; +#define REMMINA_PLUGIN_DEBUG(fmt, ...) remmina_plugin_service->_remmina_debug(__func__, fmt, ##__VA_ARGS__) + +static void remmina_plugin_tool_init(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + REMMINA_PLUGIN_DEBUG("[%s] Plugin init", PLUGIN_NAME); +} + +static gboolean remmina_plugin_tool_open_connection(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + REMMINA_PLUGIN_DEBUG("[%s] Plugin open connection", PLUGIN_NAME); + + GtkDialog *dialog; + dialog = GTK_DIALOG(gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL | GTK_DIALOG_USE_HEADER_BAR, + GTK_MESSAGE_INFO, GTK_BUTTONS_OK, PLUGIN_DESCRIPTION)); + gtk_dialog_run(dialog); + gtk_widget_destroy(GTK_WIDGET(dialog)); + return FALSE; +} + +static gboolean remmina_plugin_tool_close_connection(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + REMMINA_PLUGIN_DEBUG("[%s] Plugin close connection", PLUGIN_NAME); + remmina_plugin_service->protocol_plugin_emit_signal(gp, "disconnect"); + return FALSE; +} + +/* 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_tool_basic_settings[] = +{ + { REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, FALSE, NULL, NULL, NULL, NULL } +}; + +/* Protocol plugin definition and features */ +static RemminaProtocolPlugin remmina_plugin = { + REMMINA_PLUGIN_TYPE_PROTOCOL, // Type + PLUGIN_NAME, // Name + PLUGIN_DESCRIPTION, // Description + GETTEXT_PACKAGE, // Translation domain + PLUGIN_VERSION, // Version number + PLUGIN_APPICON, // Icon for normal connection + PLUGIN_APPICON, // Icon for SSH connection + remmina_plugin_tool_basic_settings, // Array for basic settings + NULL, // Array for advanced settings + REMMINA_PROTOCOL_SSH_SETTING_NONE, // SSH settings type + NULL, // Array for available features + remmina_plugin_tool_init, // Plugin initialization + remmina_plugin_tool_open_connection, // Plugin open connection + remmina_plugin_tool_close_connection, // Plugin close connection + NULL, // Query for available features + NULL, // Call a feature + NULL, // Send a keystroke + NULL, // No screenshot support available + NULL, // RCW map event + NULL // RCW unmap event +}; + +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"); + + if (!service->register_plugin((RemminaPlugin*)&remmina_plugin)) { + return FALSE; + } + + return TRUE; +} diff --git a/plugins/tool_hello_world/plugin_config.h b/plugins/tool_hello_world/plugin_config.h new file mode 100644 index 0000000..6742460 --- /dev/null +++ b/plugins/tool_hello_world/plugin_config.h @@ -0,0 +1,43 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * 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. + * + */ + +#ifndef __PLUGIN_CONFIG_H +#define __PLUGIN_CONFIG_H + +#define PLUGIN_NAME "HELLO" +#define PLUGIN_DESCRIPTION N_("Hello, World!") +#define PLUGIN_VERSION "1.0" +#define PLUGIN_APPICON "org.remmina.Remmina-tool-symbolic" +#endif diff --git a/plugins/tool_hello_world/scalable/emblems/org.remmina.Remmina-tool-symbolic.svg b/plugins/tool_hello_world/scalable/emblems/org.remmina.Remmina-tool-symbolic.svg new file mode 100644 index 0000000..a1b1987 --- /dev/null +++ b/plugins/tool_hello_world/scalable/emblems/org.remmina.Remmina-tool-symbolic.svg @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + width="24" + height="24" + viewBox="0 0 24 24" + version="1.1" + id="svg8" + inkscape:version="1.2 (dc2aedaf03, 2022-05-15)" + sodipodi:docname="org.remmina.Remmina-tool-symbolic.svg" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> + <defs + id="defs2" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:zoom="16.789507" + inkscape:cx="23.288356" + inkscape:cy="20.876135" + 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="1920" + inkscape:window-height="995" + inkscape:window-x="1080" + inkscape:window-y="427" + 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:deskcolor="#d1d1d1"> + <inkscape:grid + type="xygrid" + id="grid10" + dotted="false" + originx="-171.36869" + originy="-105.18186" /> + </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" /> + <cc:license + rdf:resource="" /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-171.36866,-165.16877)"> + <path + d="m 187.92458,165.16877 v 0.20535 l -7.92231,10.6258 c 0.2293,0.0655 0.45497,0.14268 0.67606,0.23147 -0.78468,-0.31266 -1.63838,-0.49451 -2.53674,-0.49451 -1.79575,0 -3.51692,0.70485 -4.78578,1.95455 -0.35794,0.35256 -0.67437,0.74332 -0.94373,1.1652 l 3.15125,3.09956 -2.1018,2.81919 c 0.50277,0.9546 1.29492,1.73163 2.26642,2.22354 l 2.8633,-2.06448 3.15416,3.10242 c 0.42818,-0.2659 0.82449,-0.57809 1.18172,-0.93087 1.27054,-1.24804 1.98546,-2.94122 1.98715,-4.70728 0,-0.86035 -0.18006,-1.67679 -0.482,-2.43174 0.0821,0.1961 0.15471,0.39575 0.21747,0.5985 l 10.80034,-7.99464 z m -12.17009,0.3326 -4.25646,4.1895 5.52629,5.43302 4.2567,-4.18665 z m 14.0397,13.80941 -4.25671,4.1895 5.52653,5.43587 4.2567,-4.18974 z m -17.2998,6.6616 -1.12573,1.62789 1.50491,1.48046 1.5346,-1.1075 c -0.76007,-0.49428 -1.41103,-1.13456 -1.91378,-1.88215 z" + id="path32" + inkscape:connector-curvature="0" + style="isolation:isolate;fill:#171717;stroke-width:0.239377" /> + </g> +</svg> diff --git a/plugins/vnc/CMakeLists.txt b/plugins/vnc/CMakeLists.txt new file mode 100644 index 0000000..eccc9d8 --- /dev/null +++ b/plugins/vnc/CMakeLists.txt @@ -0,0 +1,56 @@ +# remmina-plugin-vnc - The GTK+ Remote Desktop Client +# +# Copyright (C) 2011 Marc-Andre Moreau +# Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo +# +# 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_VNC_SRCS + vnc_plugin.c + vnc_plugin.h +) + +add_library(remmina-plugin-vnc MODULE ${REMMINA_PLUGIN_VNC_SRCS}) +set_target_properties(remmina-plugin-vnc PROPERTIES PREFIX "") +set_target_properties(remmina-plugin-vnc PROPERTIES NO_SONAME 1) + +include_directories(${REMMINA_COMMON_INCLUDE_DIRS} ${LIBVNCSERVER_INCLUDE_DIRS}) +target_link_libraries(remmina-plugin-vnc ${REMMINA_COMMON_LIBRARIES} ${LIBVNCSERVER_LIBRARIES}) + +install(TARGETS remmina-plugin-vnc DESTINATION ${REMMINA_PLUGINDIR}) + +install(FILES + scalable/emblems/org.remmina.Remmina-vnc-ssh-symbolic.svg + scalable/emblems/org.remmina.Remmina-vnc-symbolic.svg + DESTINATION ${APPICONSCALE_EMBLEMS_DIR}) + +if(WITH_ICON_CACHE) + gtk_update_icon_cache("${REMMINA_DATADIR}/icons/hicolor") +endif() diff --git a/plugins/vnc/scalable/emblems/org.remmina.Remmina-vnc-ssh-symbolic.svg b/plugins/vnc/scalable/emblems/org.remmina.Remmina-vnc-ssh-symbolic.svg new file mode 100644 index 0000000..6c1da59 --- /dev/null +++ b/plugins/vnc/scalable/emblems/org.remmina.Remmina-vnc-ssh-symbolic.svg @@ -0,0 +1,90 @@ +<?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="99.772003" + height="99.772079" + viewBox="0 0 26.39801 26.398028" + version="1.1" + id="svg8" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="vnc over ssh.svg"> + <defs + id="defs2" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:zoom="1.0493442" + inkscape:cx="-11.459557" + inkscape:cy="109.76653" + 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="-162.7958" + originy="-83.119086" /> + </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(-162.79579,-187.48289)"> + <path + d="m 175.99485,195.20186 c -1.73487,0 -3.13161,1.42611 -3.13161,3.18029 v 1.51686 h -0.7829 c -0.43365,0 -0.7829,0.34925 -0.7829,0.7829 v 4.69715 c 0,0.43365 0.34925,0.7829 0.7829,0.7829 h 7.82875 c 0.43366,0 0.78291,-0.34925 0.78291,-0.7829 v -4.69715 c 0,-0.43365 -0.34925,-0.7829 -0.78291,-0.7829 h -0.7829 v -1.51686 c 0,-1.75365 -1.39673,-3.18029 -3.13134,-3.18029 z m 0,1.56581 c 0.8673,0 1.56554,0.69823 1.56554,1.56554 v 1.5658 h -3.13135 v -1.5658 c 0,-0.86731 0.69824,-1.56554 1.56581,-1.56554 z" + id="path34" + inkscape:connector-curvature="0" + style="isolation:isolate;fill:#171717;stroke-width:0.26458332" /> + <path + d="m 187.30817,213.88092 c 0,0 0.14367,-0.008 0.34237,-0.0442 0.38523,-0.0569 0.67627,-0.195 0.89138,-0.38312 0.0415,-0.0317 0.082,-0.0661 0.12144,-0.10319 0.037,-0.0394 0.0712,-0.0796 0.10319,-0.12144 0.18785,-0.21484 0.32623,-0.50615 0.38285,-0.89112 0.0368,-0.1987 0.0444,-0.34263 0.0444,-0.34263 v -22.62691 c 0,-1.88542 -1.88568,-1.88542 -1.88568,-1.88542 h -22.6269 c 0,0 -1.88543,0 -1.88543,1.88542 v 22.6269 c 0,0 0.008,0.14393 0.0442,0.34264 0.0585,0.38497 0.19447,0.67627 0.38285,0.89111 0.032,0.0418 0.0675,0.082 0.10319,0.12145 0.0397,0.037 0.0802,0.0714 0.12144,0.10318 0.21511,0.18812 0.50668,0.32624 0.89138,0.38312 0.19976,0.0363 0.34238,0.0442 0.34238,0.0442 z m -1.88569,-4.5347 h -18.85553 v -18.09221 h 18.85553 z" + id="path36" + inkscape:connector-curvature="0" + style="isolation:isolate;fill:#171717;stroke-width:0.26458332" /> + </g> +</svg> diff --git a/plugins/vnc/scalable/emblems/org.remmina.Remmina-vnc-symbolic.svg b/plugins/vnc/scalable/emblems/org.remmina.Remmina-vnc-symbolic.svg new file mode 100644 index 0000000..060a62a --- /dev/null +++ b/plugins/vnc/scalable/emblems/org.remmina.Remmina-vnc-symbolic.svg @@ -0,0 +1,115 @@ +<?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.458335 26.458332" + version="1.1" + id="svg8" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="vnc protocol.svg"> + <defs + id="defs2"> + <mask + id="_mask_t2Us8opi94eYjaS69EfyEcHSNVQ03DDZ" + x="-200%" + y="-200%" + width="400%" + height="400%"> + <rect + x="0" + y="0" + width="100%" + height="100%" + style="fill:#ffffff" + id="rect40" /> + <path + d="m 850.286,202.852 c 0,0 0.452,-0.025 1.077,-0.139 1.211,-0.179 2.128,-0.614 2.804,-1.205 0.13,-0.1 0.257,-0.208 0.382,-0.324 0.116,-0.125 0.224,-0.251 0.324,-0.382 0.592,-0.677 1.026,-1.593 1.205,-2.804 0.115,-0.625 0.139,-1.077 0.139,-1.077 V 125.75 c 0,-5.931 -5.931,-5.931 -5.931,-5.931 h -71.171 c 0,0 -5.931,0 -5.931,5.931 v 71.171 c 0,0 0.026,0.452 0.139,1.077 0.184,1.211 0.612,2.127 1.205,2.804 0.101,0.131 0.212,0.257 0.324,0.382 0.125,0.116 0.252,0.224 0.383,0.324 0.676,0.591 1.593,1.026 2.803,1.205 0.628,0.114 1.077,0.139 1.077,0.139 z m -5.931,-14.263 h -59.309 v -56.908 h 59.309 z" + id="path42" + inkscape:connector-curvature="0" + style="fill:#000000;stroke:none" /> + </mask> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:zoom="1.4839967" + inkscape:cx="-65.390539" + inkscape:cy="-17.020138" + inkscape:document-units="px" + inkscape:current-layer="g820" + 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="-146.65307" + originy="-135.56714" /> + </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(-146.65306,-134.97453)"> + <g + id="g820" + transform="matrix(1.0697634,0,0,1.0697634,-9.2735492,-11.925523)"> + <path + inkscape:connector-curvature="0" + id="path38" + d="m 147.83075,137.94029 c 0,0 -1.17769,2e-5 -1.17769,1.17769 v 14.13263 c 0,0 0.005,0.09 0.0274,0.21405 0.0365,0.24056 0.12144,0.42255 0.23928,0.55686 0.02,0.0261 0.0421,0.051 0.0644,0.0757 0.0248,0.023 0.0503,0.0445 0.0761,0.0644 0.13431,0.11739 0.31608,0.20363 0.55643,0.23928 0.12474,0.0227 0.21405,0.0278 0.21405,0.0278 h 4.62807 v -2.83218 H 149.0084 V 140.2961 h 11.77727 v 4.05511 h 2.35536 v -5.23323 c 0,-1.17767 -1.17768,-1.17769 -1.17768,-1.17769 z m 12.95495,10.06883 v 3.58743 h -4.66941 v 2.83218 h 5.84709 c 0,0 0.09,-0.005 0.21405,-0.0278 0.24056,-0.0356 0.4221,-0.1219 0.55643,-0.23928 0.0258,-0.0199 0.0514,-0.0414 0.0761,-0.0644 0.023,-0.0247 0.0445,-0.0497 0.0644,-0.0757 0.11738,-0.13431 0.20386,-0.3163 0.23927,-0.55686 0.023,-0.12408 0.0274,-0.21405 0.0274,-0.21405 v -5.24149 z" + style="isolation:isolate;fill:#171717;stroke-width:0.22274615" /> + <path + style="isolation:isolate;fill:#171717;stroke-width:0.22274615" + inkscape:connector-curvature="0" + id="path38-5" + d="m 168.41828,161.43286 c 0,0 0.0898,-0.005 0.21384,-0.0276 0.24056,-0.0356 0.42232,-0.12185 0.55664,-0.23923 0.0258,-0.0199 0.0513,-0.0414 0.0759,-0.0644 0.0229,-0.0248 0.0445,-0.0499 0.0644,-0.0759 0.11739,-0.13432 0.20381,-0.31608 0.23923,-0.55665 0.023,-0.12407 0.0276,-0.21405 0.0276,-0.21405 v -14.13259 c 0,-1.17766 -1.17767,-1.17766 -1.17767,-1.17766 h -14.13279 c 0,0 -1.17765,0 -1.17765,1.17766 v 14.13258 c 0,0 0.005,0.09 0.0276,0.21406 0.0365,0.24057 0.1214,0.42233 0.23924,0.55664 0.02,0.0261 0.0421,0.0512 0.0644,0.0759 0.0248,0.023 0.0501,0.0445 0.0759,0.0644 0.13432,0.11738 0.3163,0.20359 0.55664,0.23923 0.12474,0.0227 0.21384,0.0276 0.21384,0.0276 z m -1.17788,-2.83222 v -11.30036 h -11.77704 11.77704 z" + sodipodi:nodetypes="cccccccsccsccccccccccccc" /> + </g> + </g> +</svg> diff --git a/plugins/vnc/vnc_plugin.c b/plugins/vnc/vnc_plugin.c new file mode 100644 index 0000000..633a2a9 --- /dev/null +++ b/plugins/vnc/vnc_plugin.c @@ -0,0 +1,2228 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2010-2011 Vic Lee + * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo + * Copyright (C) 2016-2022 Antenore Gatta, Giovanni Panozzo + * Copyright (C) 2022-2023 Antenore Gatta, Giovanni Panozzo, Hiroyuki Tanaka + * + * 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 <gmodule.h> +#include "vnc_plugin.h" +#include <rfb/rfbclient.h> + +#define REMMINA_PLUGIN_VNC_FEATURE_PREF_QUALITY 1 +#define REMMINA_PLUGIN_VNC_FEATURE_PREF_VIEWONLY 2 +#define REMMINA_PLUGIN_VNC_FEATURE_PREF_DISABLESERVERINPUT 3 +#define REMMINA_PLUGIN_VNC_FEATURE_TOOL_REFRESH 4 +#define REMMINA_PLUGIN_VNC_FEATURE_TOOL_CHAT 5 +#define REMMINA_PLUGIN_VNC_FEATURE_SCALE 6 +#define REMMINA_PLUGIN_VNC_FEATURE_UNFOCUS 7 +#define REMMINA_PLUGIN_VNC_FEATURE_TOOL_SENDCTRLALTDEL 8 +#define REMMINA_PLUGIN_VNC_FEATURE_PREF_COLOR 9 +#define REMMINA_PLUGIN_VNC_FEATURE_DYNRESUPDATE 10 + +#define VNC_DEFAULT_PORT 5900 + +#define GET_PLUGIN_DATA(gp) (RemminaPluginVncData *)g_object_get_data(G_OBJECT(gp), "plugin-data") + +static RemminaPluginService *remmina_plugin_service = NULL; + +static int dot_cursor_x_hot = 2; +static int dot_cursor_y_hot = 2; +static const gchar *dot_cursor_xpm[] = +{ "5 5 3 1", " c None", ". c #000000", "+ c #FFFFFF", " ... ", ".+++.", ".+ +.", ".+++.", " ... " }; + + +#define LOCK_BUFFER(t) if (t) { CANCEL_DEFER } pthread_mutex_lock(&gpdata->buffer_mutex); +#define UNLOCK_BUFFER(t) pthread_mutex_unlock(&gpdata->buffer_mutex); if (t) { CANCEL_ASYNC } + +struct onMainThread_cb_data { + enum { FUNC_UPDATE_SCALE } func; + GtkWidget * widget; + gint x, y, width, height; + RemminaProtocolWidget * gp; + gboolean scale; + + /* Mutex for thread synchronization */ + pthread_mutex_t mu; + /* Flag to catch cancellations */ + gboolean cancelled; +}; + +static gboolean onMainThread_cb(struct onMainThread_cb_data *d) +{ + TRACE_CALL(__func__); + if (!d->cancelled) { + switch (d->func) { + case FUNC_UPDATE_SCALE: + remmina_plugin_vnc_update_scale(d->gp, d->scale); + break; + } + pthread_mutex_unlock(&d->mu); + } else { + /* thread has been cancelled, so we must free d memory here */ + g_free(d); + } + return G_SOURCE_REMOVE; +} + + +static void onMainThread_cleanup_handler(gpointer data) +{ + TRACE_CALL(__func__); + struct onMainThread_cb_data *d = data; + + d->cancelled = TRUE; +} + + +static void onMainThread_schedule_callback_and_wait(struct onMainThread_cb_data *d) +{ + TRACE_CALL(__func__); + d->cancelled = FALSE; + pthread_cleanup_push(onMainThread_cleanup_handler, d); + pthread_mutex_init(&d->mu, NULL); + pthread_mutex_lock(&d->mu); + gdk_threads_add_idle((GSourceFunc)onMainThread_cb, (gpointer)d); + + pthread_mutex_lock(&d->mu); + + pthread_cleanup_pop(0); + pthread_mutex_unlock(&d->mu); + pthread_mutex_destroy(&d->mu); +} + +/** + * Function check_for_endianness() returns 1, if architecture + * is little endian, 0 in case of big endian. + */ +static gboolean check_for_endianness() +{ + unsigned int x = 1; + char *c = (char *)&x; + + return (int)*c; +} + +static void remmina_plugin_vnc_event_push(RemminaProtocolWidget *gp, gint event_type, gpointer p1, gpointer p2, gpointer p3) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + RemminaPluginVncEvent *event; + + event = g_new(RemminaPluginVncEvent, 1); + event->event_type = event_type; + switch (event_type) { + case REMMINA_PLUGIN_VNC_EVENT_KEY: + event->event_data.key.keyval = GPOINTER_TO_UINT(p1); + event->event_data.key.pressed = GPOINTER_TO_INT(p2); + break; + case REMMINA_PLUGIN_VNC_EVENT_POINTER: + event->event_data.pointer.x = GPOINTER_TO_INT(p1); + event->event_data.pointer.y = GPOINTER_TO_INT(p2); + event->event_data.pointer.button_mask = GPOINTER_TO_INT(p3); + break; + case REMMINA_PLUGIN_VNC_EVENT_CUTTEXT: + case REMMINA_PLUGIN_VNC_EVENT_CHAT_SEND: + event->event_data.text.text = g_strdup((char *)p1); + break; + default: + break; + } + + pthread_mutex_lock(&gpdata->vnc_event_queue_mutex); + g_queue_push_tail(gpdata->vnc_event_queue, event); + pthread_mutex_unlock(&gpdata->vnc_event_queue_mutex); + + if (write(gpdata->vnc_event_pipe[1], "\0", 1)) { + /* Ignore */ + } +} + +static void remmina_plugin_vnc_event_free(RemminaPluginVncEvent *event) +{ + TRACE_CALL(__func__); + switch (event->event_type) { + case REMMINA_PLUGIN_VNC_EVENT_CUTTEXT: + case REMMINA_PLUGIN_VNC_EVENT_CHAT_SEND: + g_free(event->event_data.text.text); + break; + default: + break; + } + g_free(event); +} + +static void remmina_plugin_vnc_event_free_all(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + RemminaPluginVncEvent *event; + + /* This is called from main thread after plugin thread has + * been closed, so no queue locking is necessary here */ + while ((event = g_queue_pop_head(gpdata->vnc_event_queue)) != NULL) + remmina_plugin_vnc_event_free(event); +} + +static void remmina_plugin_vnc_scale_area(RemminaProtocolWidget *gp, gint *x, gint *y, gint *w, gint *h) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + GtkAllocation widget_allocation; + gint width, height; + gint sx, sy, sw, sh; + + if (gpdata->rgb_buffer == NULL) + return; + + width = remmina_plugin_service->protocol_plugin_get_width(gp); + height = remmina_plugin_service->protocol_plugin_get_height(gp); + + gtk_widget_get_allocation(GTK_WIDGET(gp), &widget_allocation); + + if (widget_allocation.width == width && widget_allocation.height == height) + return; /* Same size, no scaling */ + + /* We have to extend the scaled region 2 scaled pixels, to avoid gaps */ + sx = MIN(MAX(0, (*x) * widget_allocation.width / width - widget_allocation.width / width - 2), widget_allocation.width - 1); + sy = MIN(MAX(0, (*y) * widget_allocation.height / height - widget_allocation.height / height - 2), widget_allocation.height - 1); + sw = MIN(widget_allocation.width - sx, (*w) * widget_allocation.width / width + widget_allocation.width / width + 4); + sh = MIN(widget_allocation.height - sy, (*h) * widget_allocation.height / height + widget_allocation.height / height + 4); + + *x = sx; + *y = sy; + *w = sw; + *h = sh; +} + +static void remmina_plugin_vnc_update_scale(RemminaProtocolWidget *gp, gboolean scale) +{ + TRACE_CALL(__func__); + /* This function can be called from a non main thread */ + + RemminaPluginVncData *gpdata; + gint width, height; + + if (!remmina_plugin_service->is_main_thread()) { + struct onMainThread_cb_data *d; + d = (struct onMainThread_cb_data *)g_malloc(sizeof(struct onMainThread_cb_data)); + d->func = FUNC_UPDATE_SCALE; + d->gp = gp; + d->scale = scale; + onMainThread_schedule_callback_and_wait(d); + g_free(d); + return; + } + + gpdata = GET_PLUGIN_DATA(gp); + + width = remmina_plugin_service->protocol_plugin_get_width(gp); + height = remmina_plugin_service->protocol_plugin_get_height(gp); + if (scale) + /* In scaled mode, drawing_area will get its dimensions from its parent */ + gtk_widget_set_size_request(GTK_WIDGET(gpdata->drawing_area), -1, -1); + else + /* In non scaled mode, the plugins forces dimensions of drawing area */ + gtk_widget_set_size_request(GTK_WIDGET(gpdata->drawing_area), width, height); + + remmina_plugin_service->protocol_plugin_update_align(gp); +} + +gboolean remmina_plugin_vnc_setcursor(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + GdkCursor *cur; + + LOCK_BUFFER(FALSE); + gpdata->queuecursor_handler = 0; + + if (gpdata->queuecursor_surface) { + cur = gdk_cursor_new_from_surface(gdk_display_get_default(), gpdata->queuecursor_surface, gpdata->queuecursor_x, + gpdata->queuecursor_y); + gdk_window_set_cursor(gtk_widget_get_window(gpdata->drawing_area), cur); + g_object_unref(cur); + cairo_surface_destroy(gpdata->queuecursor_surface); + gpdata->queuecursor_surface = NULL; + } else { + gdk_window_set_cursor(gtk_widget_get_window(gpdata->drawing_area), NULL); + } + UNLOCK_BUFFER(FALSE); + + return FALSE; +} + +static void remmina_plugin_vnc_queuecursor(RemminaProtocolWidget *gp, cairo_surface_t *surface, gint x, gint y) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + + if (gpdata->queuecursor_surface) + cairo_surface_destroy(gpdata->queuecursor_surface); + gpdata->queuecursor_surface = surface; + gpdata->queuecursor_x = x; + gpdata->queuecursor_y = y; + if (!gpdata->queuecursor_handler) + gpdata->queuecursor_handler = IDLE_ADD((GSourceFunc)remmina_plugin_vnc_setcursor, gp); +} + +typedef struct _RemminaKeyVal { + guint keyval; + guint16 keycode; +} RemminaKeyVal; + +/***************************** LibVNCClient related codes *********************************/ + +static const uint32_t remmina_plugin_vnc_no_encrypt_auth_types[] = +{ rfbNoAuth, rfbVncAuth, rfbMSLogon, 0 }; + +static RemminaPluginVncEvent *remmina_plugin_vnc_event_queue_pop_head(RemminaPluginVncData *gpdata) +{ + RemminaPluginVncEvent *event; + + CANCEL_DEFER; + pthread_mutex_lock(&gpdata->vnc_event_queue_mutex); + + event = g_queue_pop_head(gpdata->vnc_event_queue); + + pthread_mutex_unlock(&gpdata->vnc_event_queue_mutex); + CANCEL_ASYNC; + + return event; +} + +static void remmina_plugin_vnc_process_vnc_event(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncEvent *event; + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + rfbClient *cl; + gchar buf[100]; + + cl = (rfbClient *)gpdata->client; + while ((event = remmina_plugin_vnc_event_queue_pop_head(gpdata)) != NULL) { + if (cl) { + switch (event->event_type) { + case REMMINA_PLUGIN_VNC_EVENT_KEY: + SendKeyEvent(cl, event->event_data.key.keyval, event->event_data.key.pressed); + break; + case REMMINA_PLUGIN_VNC_EVENT_POINTER: + SendPointerEvent(cl, event->event_data.pointer.x, event->event_data.pointer.y, + event->event_data.pointer.button_mask); + break; + case REMMINA_PLUGIN_VNC_EVENT_CUTTEXT: + if (event->event_data.text.text) { + rfbClientLog("sending clipboard text '%s'\n", event->event_data.text.text); + SendClientCutText(cl, event->event_data.text.text, strlen(event->event_data.text.text)); + } + break; + case REMMINA_PLUGIN_VNC_EVENT_CHAT_OPEN: + TextChatOpen(cl); + break; + case REMMINA_PLUGIN_VNC_EVENT_CHAT_SEND: + TextChatSend(cl, event->event_data.text.text); + break; + case REMMINA_PLUGIN_VNC_EVENT_CHAT_CLOSE: + TextChatClose(cl); + TextChatFinish(cl); + break; + default: + rfbClientLog("Ignoring VNC event: 0x%x\n", event->event_type); + break; + } + } + remmina_plugin_vnc_event_free(event); + } + if (read(gpdata->vnc_event_pipe[0], buf, sizeof(buf))) { + /* Ignore */ + } +} + +typedef struct _RemminaPluginVncCuttextParam { + RemminaProtocolWidget * gp; + gchar * text; + gint textlen; +} RemminaPluginVncCuttextParam; + +static void remmina_plugin_vnc_update_quality(rfbClient *cl, gint quality) +{ + TRACE_CALL(__func__); + RemminaProtocolWidget *gp = rfbClientGetClientData(cl, NULL); + RemminaFile *remminafile; + gchar *enc = NULL; + + /** + * "0", "Poor (fastest)" + * "1", "Medium" + * "2", "Good" + * "9", "Best (slowest)" + */ + switch (quality) { + case 9: + cl->appData.useBGR233 = 0; + cl->appData.encodingsString = "copyrect zlib hextile raw"; + cl->appData.compressLevel = 1; + cl->appData.qualityLevel = 9; + break; + case 2: + cl->appData.useBGR233 = 0; + cl->appData.encodingsString = "tight zrle ultra copyrect hextile zlib corre rre raw"; + cl->appData.compressLevel = 2; + cl->appData.qualityLevel = 7; + break; + case 1: + cl->appData.useBGR233 = 0; + cl->appData.encodingsString = "tight zrle ultra copyrect hextile zlib corre rre raw"; + cl->appData.compressLevel = 3; + cl->appData.qualityLevel = 5; + break; + case 0: + default: + // bpp8 and tight encoding is not supported in libvnc + cl->appData.useBGR233 = 1; + cl->appData.encodingsString = "copyrect zrle ultra zlib hextile corre rre raw"; + cl->appData.qualityLevel = 1; + break; + } + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + enc = g_strdup(remmina_plugin_service->file_get_string(remminafile, "encodings")); + if (enc) { + cl->appData.encodingsString = g_strdup(enc); + g_free (enc), enc = NULL; + } + gboolean tight = remmina_plugin_service->file_get_int (remminafile, "tightencoding", FALSE); + if (tight) { + if (!g_strrstr ( g_strdup(cl->appData.encodingsString), "tight\0")) { + cl->appData.encodingsString = g_strdup_printf("%s %s", "tight", g_strdup(cl->appData.encodingsString)); + } + } + + REMMINA_PLUGIN_DEBUG("Quality: %d", quality); + REMMINA_PLUGIN_DEBUG("Encodings: %s", cl->appData.encodingsString); +} + +static void remmina_plugin_vnc_update_colordepth(rfbClient *cl, gint colordepth) +{ + TRACE_CALL(__func__); + + cl->format.depth = colordepth; + cl->appData.requestedDepth = colordepth; + + cl->format.trueColour = 1; + cl->format.bigEndian = check_for_endianness()?FALSE:TRUE; + + switch (colordepth) { + case 8: + cl->format.depth = 8; + cl->format.bitsPerPixel = 8; + cl->format.blueMax = 3; + cl->format.blueShift = 6; + cl->format.greenMax = 7; + cl->format.greenShift = 3; + cl->format.redMax = 7; + cl->format.redShift = 0; + break; + case 16: + cl->format.depth = 15; + cl->format.bitsPerPixel = 16; + cl->format.redShift = 11; + cl->format.greenShift = 6; + cl->format.blueShift = 1; + cl->format.redMax = 31; + cl->format.greenMax = 31; + cl->format.blueMax = 31; + break; + case 32: + default: + cl->format.depth = 24; + cl->format.bitsPerPixel = 32; + cl->format.blueShift = 0; + cl->format.redShift = 16; + cl->format.greenShift = 8; + cl->format.blueMax = 0xff; + cl->format.redMax = 0xff; + cl->format.greenMax = 0xff; + break; + } + + rfbClientLog("colordepth = %d\n", colordepth); + rfbClientLog("format.depth = %d\n", cl->format.depth); + rfbClientLog("format.bitsPerPixel = %d\n", cl->format.bitsPerPixel); + rfbClientLog("format.blueShift = %d\n", cl->format.blueShift); + rfbClientLog("format.redShift = %d\n", cl->format.redShift); + rfbClientLog("format.trueColour = %d\n", cl->format.trueColour); + rfbClientLog("format.greenShift = %d\n", cl->format.greenShift); + rfbClientLog("format.blueMax = %d\n", cl->format.blueMax); + rfbClientLog("format.redMax = %d\n", cl->format.redMax); + rfbClientLog("format.greenMax = %d\n", cl->format.greenMax); + rfbClientLog("format.bigEndian = %d\n", cl->format.bigEndian); +} + +static rfbBool remmina_plugin_vnc_rfb_allocfb(rfbClient *cl) +{ + TRACE_CALL(__func__); + RemminaProtocolWidget *gp = rfbClientGetClientData(cl, NULL); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + gint width, height, depth, size; + gboolean scale; + cairo_surface_t *new_surface, *old_surface; + + width = cl->width; + height = cl->height; + depth = cl->format.bitsPerPixel; + size = width * height * (depth / 8); + + new_surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + if (cairo_surface_status(new_surface) != CAIRO_STATUS_SUCCESS) + return FALSE; + old_surface = gpdata->rgb_buffer; + + LOCK_BUFFER(TRUE); + + remmina_plugin_service->protocol_plugin_set_width(gp, width); + remmina_plugin_service->protocol_plugin_set_height(gp, height); + + gpdata->rgb_buffer = new_surface; + + if (gpdata->vnc_buffer) + g_free(gpdata->vnc_buffer); + gpdata->vnc_buffer = (guchar *)g_malloc(size); + cl->frameBuffer = gpdata->vnc_buffer; + + UNLOCK_BUFFER(TRUE); + + if (old_surface) + cairo_surface_destroy(old_surface); + + scale = (remmina_plugin_service->remmina_protocol_widget_get_current_scale_mode(gp) != REMMINA_PROTOCOL_WIDGET_SCALE_MODE_NONE); + remmina_plugin_vnc_update_scale(gp, scale); + + /* Notify window of change so that scroll border can be hidden or shown if needed */ + remmina_plugin_service->protocol_plugin_desktop_resize(gp); + + /* Refresh the client’s updateRect - bug in xvncclient */ + cl->updateRect.w = width; + cl->updateRect.h = height; + + return TRUE; +} + +static gint remmina_plugin_vnc_bits(gint n) +{ + TRACE_CALL(__func__); + gint b = 0; + + while (n) { + b++; + n >>= 1; + } + return b ? b : 1; +} + +static gboolean remmina_plugin_vnc_queue_draw_area_real(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + gint x, y, w, h; + + if (GTK_IS_WIDGET(gp) && gpdata->connected) { + LOCK_BUFFER(FALSE); + x = gpdata->queuedraw_x; + y = gpdata->queuedraw_y; + w = gpdata->queuedraw_w; + h = gpdata->queuedraw_h; + gpdata->queuedraw_handler = 0; + UNLOCK_BUFFER(FALSE); + + gtk_widget_queue_draw_area(GTK_WIDGET(gp), x, y, w, h); + } + return FALSE; +} + +static void remmina_plugin_vnc_queue_draw_area(RemminaProtocolWidget *gp, gint x, gint y, gint w, gint h) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + gint nx2, ny2, ox2, oy2; + + LOCK_BUFFER(TRUE); + if (gpdata->queuedraw_handler) { + nx2 = x + w; + ny2 = y + h; + ox2 = gpdata->queuedraw_x + gpdata->queuedraw_w; + oy2 = gpdata->queuedraw_y + gpdata->queuedraw_h; + gpdata->queuedraw_x = MIN(gpdata->queuedraw_x, x); + gpdata->queuedraw_y = MIN(gpdata->queuedraw_y, y); + gpdata->queuedraw_w = MAX(ox2, nx2) - gpdata->queuedraw_x; + gpdata->queuedraw_h = MAX(oy2, ny2) - gpdata->queuedraw_y; + } else { + gpdata->queuedraw_x = x; + gpdata->queuedraw_y = y; + gpdata->queuedraw_w = w; + gpdata->queuedraw_h = h; + gpdata->queuedraw_handler = IDLE_ADD((GSourceFunc)remmina_plugin_vnc_queue_draw_area_real, gp); + } + UNLOCK_BUFFER(TRUE); +} + +static void remmina_plugin_vnc_rfb_fill_buffer(rfbClient *cl, guchar *dest, gint dest_rowstride, guchar *src, + gint src_rowstride, guchar *mask, gint w, gint h) +{ + TRACE_CALL(__func__); + guchar *srcptr; + gint bytesPerPixel; + guint32 src_pixel; + gint ix, iy; + gint i; + guchar c; + gint rs, gs, bs, rm, gm, bm, rl, gl, bl, rr, gr, br; + gint r; + guint32 *destptr; + + union { + struct { + guchar a, r, g, b; + } colors; + guint32 argb; + } dst_pixel; + + bytesPerPixel = cl->format.bitsPerPixel / 8; + switch (cl->format.bitsPerPixel) { + case 32: + /* The following codes fill in the Alpha channel swap red/green value */ + for (iy = 0; iy < h; iy++) { + destptr = (guint32 *)(dest + iy * dest_rowstride); + srcptr = src + iy * src_rowstride; + for (ix = 0; ix < w; ix++) { + if (!mask || *mask++) { + dst_pixel.colors.a = 0xff; + dst_pixel.colors.r = *(srcptr + 2); + dst_pixel.colors.g = *(srcptr + 1); + dst_pixel.colors.b = *srcptr; + *destptr++ = ntohl(dst_pixel.argb); + } else { + *destptr++ = 0; + } + srcptr += 4; + } + } + break; + default: + rm = cl->format.redMax; + gm = cl->format.greenMax; + bm = cl->format.blueMax; + rr = remmina_plugin_vnc_bits(rm); + gr = remmina_plugin_vnc_bits(gm); + br = remmina_plugin_vnc_bits(bm); + rl = 8 - rr; + gl = 8 - gr; + bl = 8 - br; + rs = cl->format.redShift; + gs = cl->format.greenShift; + bs = cl->format.blueShift; + for (iy = 0; iy < h; iy++) { + destptr = (guint32 *)(dest + iy * dest_rowstride); + srcptr = src + iy * src_rowstride; + for (ix = 0; ix < w; ix++) { + src_pixel = 0; + for (i = 0; i < bytesPerPixel; i++) + src_pixel += (*srcptr++) << (8 * i); + + if (!mask || *mask++) { + dst_pixel.colors.a = 0xff; + c = (guchar)((src_pixel >> rs) & rm) << rl; + for (r = rr; r < 8; r *= 2) + c |= c >> r; + dst_pixel.colors.r = c; + c = (guchar)((src_pixel >> gs) & gm) << gl; + for (r = gr; r < 8; r *= 2) + c |= c >> r; + dst_pixel.colors.g = c; + c = (guchar)((src_pixel >> bs) & bm) << bl; + for (r = br; r < 8; r *= 2) + c |= c >> r; + dst_pixel.colors.b = c; + *destptr++ = ntohl(dst_pixel.argb); + } else { + *destptr++ = 0; + } + } + } + break; + } +} + +static void remmina_plugin_vnc_rfb_updatefb(rfbClient *cl, int x, int y, int w, int h) +{ + TRACE_CALL(__func__); + RemminaProtocolWidget *gp = rfbClientGetClientData(cl, NULL); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + gint bytesPerPixel; + gint rowstride; + gint width; + + LOCK_BUFFER(TRUE); + + if (w >= 1 || h >= 1) { + width = remmina_plugin_service->protocol_plugin_get_width(gp); + bytesPerPixel = cl->format.bitsPerPixel / 8; + rowstride = cairo_image_surface_get_stride(gpdata->rgb_buffer); + cairo_surface_flush(gpdata->rgb_buffer); + remmina_plugin_vnc_rfb_fill_buffer(cl, cairo_image_surface_get_data(gpdata->rgb_buffer) + y * rowstride + x * 4, + rowstride, gpdata->vnc_buffer + ((y * width + x) * bytesPerPixel), width * bytesPerPixel, NULL, + w, h); + cairo_surface_mark_dirty(gpdata->rgb_buffer); + } + + if ((remmina_plugin_service->remmina_protocol_widget_get_current_scale_mode(gp) != REMMINA_PROTOCOL_WIDGET_SCALE_MODE_NONE)) + remmina_plugin_vnc_scale_area(gp, &x, &y, &w, &h); + + UNLOCK_BUFFER(TRUE); + + remmina_plugin_vnc_queue_draw_area(gp, x, y, w, h); +} + +static void remmina_plugin_vnc_rfb_finished(rfbClient *cl) __attribute__ ((unused)); +static void remmina_plugin_vnc_rfb_finished(rfbClient *cl) +{ + TRACE_CALL(__func__); + REMMINA_PLUGIN_DEBUG("FinishedFrameBufferUpdate"); +} + +static void remmina_plugin_vnc_rfb_led_state(rfbClient *cl, int value, int pad) +{ + TRACE_CALL(__func__); + REMMINA_PLUGIN_DEBUG("Led state - value: %d, pad: %d", value, pad); +} + +static gboolean remmina_plugin_vnc_queue_cuttext(RemminaPluginVncCuttextParam *param) +{ + TRACE_CALL(__func__); + RemminaProtocolWidget *gp = param->gp; + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + GDateTime *t; + glong diff; + const char *cur_charset; + gchar *text; + gsize br, bw; + + if (GTK_IS_WIDGET(gp) && gpdata->connected) { + t = g_date_time_new_now_utc(); + diff = g_date_time_difference(t, gpdata->clipboard_timer) / 100000; // tenth of second + if (diff >= 10) { + g_date_time_unref(gpdata->clipboard_timer); + gpdata->clipboard_timer = t; + /* Convert text from VNC latin-1 to current GTK charset (usually UTF-8) */ + g_get_charset(&cur_charset); + text = g_convert_with_fallback(param->text, param->textlen, cur_charset, "ISO-8859-1", "?", &br, &bw, NULL); + gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD), text, bw); + g_free(text); + } else { + g_date_time_unref(t); + } + } + g_free(param->text); + g_free(param); + return FALSE; +} + +static void remmina_plugin_vnc_rfb_cuttext(rfbClient *cl, const char *text, int textlen) +{ + TRACE_CALL(__func__); + RemminaPluginVncCuttextParam *param; + + param = g_new(RemminaPluginVncCuttextParam, 1); + param->gp = (RemminaProtocolWidget *)rfbClientGetClientData(cl, NULL); + param->text = g_malloc(textlen); + memcpy(param->text, text, textlen); + param->textlen = textlen; + IDLE_ADD((GSourceFunc)remmina_plugin_vnc_queue_cuttext, param); +} + +static char * +remmina_plugin_vnc_rfb_password(rfbClient *cl) +{ + TRACE_CALL(__func__); + RemminaProtocolWidget *gp = rfbClientGetClientData(cl, NULL); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + RemminaFile *remminafile; + gchar *pwd = NULL; + + gpdata->auth_called = TRUE; + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + + if (gpdata->auth_first) + pwd = g_strdup(remmina_plugin_service->file_get_string(remminafile, "password")); + if (!pwd) { + gboolean save; + gint ret; + gboolean 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 VNC password"), + NULL, + remmina_plugin_service->file_get_string(remminafile, "password"), + NULL, + NULL); + if (ret != GTK_RESPONSE_OK) { + gpdata->connected = FALSE; + return NULL; + } + pwd = 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", pwd); + else + remmina_plugin_service->file_set_string(remminafile, "password", NULL); + } + return pwd; +} + +static rfbCredential * +remmina_plugin_vnc_rfb_credential(rfbClient *cl, int credentialType) +{ + TRACE_CALL(__func__); + rfbCredential *cred; + RemminaProtocolWidget *gp = rfbClientGetClientData(cl, NULL); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + RemminaFile *remminafile; + gint ret; + gchar *s1, *s2; + gboolean disablepasswordstoring; + + gpdata->auth_called = TRUE; + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + + cred = g_new0(rfbCredential, 1); + + switch (credentialType) { + case rfbCredentialTypeUser: + + s1 = g_strdup(remmina_plugin_service->file_get_string(remminafile, "username")); + + s2 = g_strdup(remmina_plugin_service->file_get_string(remminafile, "password")); + + if (gpdata->auth_first && s1 && s2) { + cred->userCredential.username = s1; + cred->userCredential.password = s2; + } else { + g_free(s1); + g_free(s2); + + 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) | REMMINA_MESSAGE_PANEL_FLAG_USERNAME, + _("Enter VNC authentication credentials"), + remmina_plugin_service->file_get_string(remminafile, "username"), + remmina_plugin_service->file_get_string(remminafile, "password"), + NULL, + NULL); + if (ret == GTK_RESPONSE_OK) { + gboolean save = remmina_plugin_service->protocol_plugin_init_get_savepassword(gp); + cred->userCredential.username = remmina_plugin_service->protocol_plugin_init_get_username(gp); + cred->userCredential.password = remmina_plugin_service->protocol_plugin_init_get_password(gp); + if (save) { + remmina_plugin_service->file_set_string(remminafile, "username", cred->userCredential.username); + remmina_plugin_service->file_set_string(remminafile, "password", cred->userCredential.password); + } else { + remmina_plugin_service->file_set_string(remminafile, "username", NULL); + remmina_plugin_service->file_set_string(remminafile, "password", NULL); + } + } else { + g_free(cred); + cred = NULL; + gpdata->connected = FALSE; + } + } + break; + + case rfbCredentialTypeX509: + if (gpdata->auth_first && + remmina_plugin_service->file_get_string(remminafile, "cacert")) { + cred->x509Credential.x509CACertFile = g_strdup(remmina_plugin_service->file_get_string(remminafile, "cacert")); + cred->x509Credential.x509CACrlFile = g_strdup(remmina_plugin_service->file_get_string(remminafile, "cacrl")); + cred->x509Credential.x509ClientCertFile = g_strdup(remmina_plugin_service->file_get_string(remminafile, "clientcert")); + cred->x509Credential.x509ClientKeyFile = g_strdup(remmina_plugin_service->file_get_string(remminafile, "clientkey")); + } else { + ret = remmina_plugin_service->protocol_plugin_init_authx509(gp); + + if (ret == GTK_RESPONSE_OK) { + cred->x509Credential.x509CACertFile = remmina_plugin_service->protocol_plugin_init_get_cacert(gp); + cred->x509Credential.x509CACrlFile = remmina_plugin_service->protocol_plugin_init_get_cacrl(gp); + cred->x509Credential.x509ClientCertFile = remmina_plugin_service->protocol_plugin_init_get_clientcert(gp); + cred->x509Credential.x509ClientKeyFile = remmina_plugin_service->protocol_plugin_init_get_clientkey(gp); + } else { + g_free(cred); + cred = NULL; + gpdata->connected = FALSE; + } + } + break; + + default: + g_free(cred); + cred = NULL; + break; + } + return cred; +} + +static void remmina_plugin_vnc_rfb_cursor_shape(rfbClient *cl, int xhot, int yhot, int width, int height, int bytesPerPixel) +{ + TRACE_CALL(__func__); + RemminaProtocolWidget *gp = rfbClientGetClientData(cl, NULL); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + + if (!gtk_widget_get_window(GTK_WIDGET(gp))) + return; + + if (width && height) { + gint stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width); + guchar *data = g_malloc(stride * height); + remmina_plugin_vnc_rfb_fill_buffer(cl, data, stride, cl->rcSource, + width * cl->format.bitsPerPixel / 8, cl->rcMask, width, height); + cairo_surface_t *surface = cairo_image_surface_create_for_data(data, CAIRO_FORMAT_ARGB32, width, height, stride); + if (cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS) { + g_free(data); + return; + } + if (cairo_surface_set_user_data(surface, NULL, NULL, g_free) != CAIRO_STATUS_SUCCESS) { + g_free(data); + return; + } + + LOCK_BUFFER(TRUE); + remmina_plugin_vnc_queuecursor(gp, surface, xhot, yhot); + UNLOCK_BUFFER(TRUE); + } +} + +static void remmina_plugin_vnc_rfb_bell(rfbClient *cl) +{ + TRACE_CALL(__func__); + REMMINA_PLUGIN_DEBUG("Bell message received"); + RemminaProtocolWidget *gp; + RemminaFile *remminafile; + GdkWindow *window; + + gp = (RemminaProtocolWidget *)(rfbClientGetClientData(cl, NULL)); + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + + if (remmina_plugin_service->file_get_int(remminafile, "disableserverbell", FALSE)) + return; + + window = gtk_widget_get_window(GTK_WIDGET(gp)); + + if (window) + gdk_window_beep(window); + REMMINA_PLUGIN_DEBUG("Beep emitted"); +} + +/* Translate known VNC messages. It’s for intltool only, not for gcc */ +#ifdef __DO_NOT_COMPILE_ME__ +N_("Unable to connect to VNC server") +N_("Couldn’t convert '%s' to host address") +N_("VNC connection failed: %s") +N_("Your connection has been rejected.") +#endif +/** @todo We only store the last message at this moment. */ +#define MAX_ERROR_LENGTH 1000 +static gchar vnc_error[MAX_ERROR_LENGTH + 1]; +static gboolean vnc_encryption_disable_requested; + +static void remmina_plugin_vnc_rfb_output(const char *format, ...) +{ + TRACE_CALL(__func__); + gchar *f, *p, *ff; + + if (!rfbEnableClientLogging) + return; + + va_list args; + va_start(args, format); + /* eliminate the last \n */ + f = g_strdup(format); + if (f[strlen(f) - 1] == '\n') f[strlen(f) - 1] = '\0'; + + if (g_strcmp0(f, "VNC connection failed: %s") == 0) { + p = va_arg(args, gchar *); + g_snprintf(vnc_error, MAX_ERROR_LENGTH, _(f), _(p)); + } else if (g_strcmp0(f, "The VNC server requested an unknown authentication method. %s") == 0) { + p = va_arg(args, gchar *); + if (vnc_encryption_disable_requested) { + ff = g_strconcat(_("The VNC server requested an unknown authentication method. %s"), + ". ", + _("Please retry after turning on encryption for this profile."), + NULL); + g_snprintf(vnc_error, MAX_ERROR_LENGTH, ff, p); + g_free(ff); + } else { + g_snprintf(vnc_error, MAX_ERROR_LENGTH, _(f), p); + } + } else { + g_vsnprintf(vnc_error, MAX_ERROR_LENGTH, _(f), args); + } + g_free(f); + va_end(args); + + REMMINA_PLUGIN_DEBUG("VNC returned: %s", vnc_error); +} + +static void remmina_plugin_vnc_chat_on_send(RemminaProtocolWidget *gp, const gchar *text) +{ + TRACE_CALL(__func__); + gchar *ptr; + + /* Need to add a line-feed for UltraVNC */ + ptr = g_strdup_printf("%s\n", text); + remmina_plugin_vnc_event_push(gp, REMMINA_PLUGIN_VNC_EVENT_CHAT_SEND, ptr, NULL, NULL); + g_free(ptr); +} + +static void remmina_plugin_vnc_chat_on_destroy(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + remmina_plugin_vnc_event_push(gp, REMMINA_PLUGIN_VNC_EVENT_CHAT_CLOSE, NULL, NULL, NULL); +} + +/* Send CTRL+ALT+DEL keys keystrokes to the plugin drawing_area widget */ +static void remmina_plugin_vnc_send_ctrlaltdel(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + guint keys[] = { GDK_KEY_Control_L, GDK_KEY_Alt_L, GDK_KEY_Delete }; + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + + remmina_plugin_service->protocol_plugin_send_keys_signals(gpdata->drawing_area, + keys, G_N_ELEMENTS(keys), GDK_KEY_PRESS | GDK_KEY_RELEASE); +} + +static gboolean remmina_plugin_vnc_close_chat(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + remmina_plugin_service->protocol_plugin_chat_close(gp); + return FALSE; +} + +static gboolean remmina_plugin_vnc_open_chat(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + rfbClient *cl; + + cl = (rfbClient *)gpdata->client; + + remmina_plugin_service->protocol_plugin_chat_open(gp, cl->desktopName, remmina_plugin_vnc_chat_on_send, + remmina_plugin_vnc_chat_on_destroy); + remmina_plugin_vnc_event_push(gp, REMMINA_PLUGIN_VNC_EVENT_CHAT_OPEN, NULL, NULL, NULL); + return FALSE; +} + +static void remmina_plugin_vnc_rfb_chat(rfbClient *cl, int value, char *text) +{ + TRACE_CALL(__func__); + RemminaProtocolWidget *gp; + + gp = (RemminaProtocolWidget *)(rfbClientGetClientData(cl, NULL)); + switch (value) { + case rfbTextChatOpen: + IDLE_ADD((GSourceFunc)remmina_plugin_vnc_open_chat, gp); + break; + case rfbTextChatClose: + /* Do nothing… but wait for the next rfbTextChatFinished signal */ + break; + case rfbTextChatFinished: + IDLE_ADD((GSourceFunc)remmina_plugin_vnc_close_chat, gp); + break; + default: + /* value is the text length */ + remmina_plugin_service->protocol_plugin_chat_receive(gp, text); + break; + } +} + +static gboolean remmina_plugin_vnc_incoming_connection(RemminaProtocolWidget *gp, rfbClient *cl) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + fd_set fds; + + /** + * @fixme This may fail or not working as expected with multiple network interfaces, + * change with ListenAtTcpPortAndAddress + */ + gpdata->listen_sock = ListenAtTcpPort(cl->listenPort); + if (gpdata->listen_sock < 0) + return FALSE; + + remmina_plugin_service->protocol_plugin_init_show_listen(gp, cl->listenPort); + + remmina_plugin_service->protocol_plugin_start_reverse_tunnel(gp, cl->listenPort); + + FD_ZERO(&fds); + if (gpdata->listen_sock >= 0) + FD_SET(gpdata->listen_sock, &fds); + + select(gpdata->listen_sock + 1, &fds, NULL, NULL, NULL); + + if (!FD_ISSET(gpdata->listen_sock, &fds)) { + close(gpdata->listen_sock); + gpdata->listen_sock = -1; + return FALSE; + } + + if (FD_ISSET(gpdata->listen_sock, &fds)) + cl->sock = AcceptTcpConnection(gpdata->listen_sock); + if (cl->sock >= 0) { + close(gpdata->listen_sock); + gpdata->listen_sock = -1; + } + if (cl->sock < 0 || !SetNonBlocking(cl->sock)) + return FALSE; + + return TRUE; +} + + +static gboolean remmina_plugin_vnc_main_loop(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + gint ret; + gint i; + rfbClient *cl; + fd_set fds; + struct timeval timeout; + + if (!gpdata->connected) { + gpdata->running = FALSE; + return FALSE; + } + + cl = (rfbClient *)gpdata->client; + + /* + * Do not explicitly wait while data is on the buffer, see: + * - https://jira.glyptodon.com/browse/GUAC-1056 + * - https://jira.glyptodon.com/browse/GUAC-1056?focusedCommentId=14348&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-14348 + * - https://github.com/apache/guacamole-server/blob/67680bd2d51e7949453f0f7ffc7f4234a1136715/src/protocols/vnc/vnc.c#L155 + */ + if (cl->buffered) + goto handle_buffered; + + timeout.tv_sec = 10; + timeout.tv_usec = 0; + FD_ZERO(&fds); + FD_SET(cl->sock, &fds); + FD_SET(gpdata->vnc_event_pipe[0], &fds); + ret = select(MAX(cl->sock, gpdata->vnc_event_pipe[0]) + 1, &fds, NULL, NULL, &timeout); + + /* Sometimes it returns <0 when opening a modal dialog in other window. Absolutely weird */ + /* So we continue looping anyway */ + if (ret <= 0) + return TRUE; + + if (FD_ISSET(gpdata->vnc_event_pipe[0], &fds)) + remmina_plugin_vnc_process_vnc_event(gp); + if (FD_ISSET(cl->sock, &fds)) { + i = WaitForMessage(cl, 500); + if (i < 0) + return TRUE; +handle_buffered: + if (!HandleRFBServerMessage(cl)) { + gpdata->running = FALSE; + if (gpdata->connected && !remmina_plugin_service->protocol_plugin_is_closed(gp)) + remmina_plugin_service->protocol_plugin_signal_connection_closed(gp); + return FALSE; + } + } + + return TRUE; +} + +static gboolean remmina_plugin_vnc_main(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + RemminaFile *remminafile; + rfbClient *cl = NULL; + gchar *host; + gchar *s = NULL; + + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + gpdata->running = TRUE; + + rfbClientLog = rfbClientErr = remmina_plugin_vnc_rfb_output; + + gint colordepth = remmina_plugin_service->file_get_int(remminafile, "colordepth", 32); + gint quality = remmina_plugin_service->file_get_int(remminafile, "quality", 9); + + while (gpdata->connected) { + gpdata->auth_called = FALSE; + + host = remmina_plugin_service->protocol_plugin_start_direct_tunnel(gp, VNC_DEFAULT_PORT, TRUE); + + if (host == NULL) { + REMMINA_PLUGIN_DEBUG("host is null"); + gpdata->connected = FALSE; + break; + } + + /* int bitsPerSample,int samplesPerPixel, int bytesPerPixel */ + switch (colordepth) { + case 8: + cl = rfbGetClient(2, 3, 1); + break; + case 15: + case 16: + cl = rfbGetClient(5, 3, 2); + break; + case 24: + cl = rfbGetClient(6, 3, 3); + break; + case 32: + default: + cl = rfbGetClient(8, 3, 4); + break; + } + REMMINA_PLUGIN_DEBUG("Color depth: %d", colordepth); + cl->MallocFrameBuffer = remmina_plugin_vnc_rfb_allocfb; + cl->canHandleNewFBSize = TRUE; + cl->GetPassword = remmina_plugin_vnc_rfb_password; + cl->GetCredential = remmina_plugin_vnc_rfb_credential; + cl->GotFrameBufferUpdate = remmina_plugin_vnc_rfb_updatefb; + /** + * @fixme we have to implement FinishedFrameBufferUpdate + * This is to know when the server has finished to send a batch of frame buffer + * updates. + * cl->FinishedFrameBufferUpdate = remmina_plugin_vnc_rfb_finished; + */ + /** + * @fixme we have to implement HandleKeyboardLedState + * cl->HandleKeyboardLedState = remmina_plugin_vnc_rfb_led_state + */ + cl->HandleKeyboardLedState = remmina_plugin_vnc_rfb_led_state; + cl->GotXCutText = ( + remmina_plugin_service->file_get_int(remminafile, "disableclipboard", FALSE) ? + NULL : remmina_plugin_vnc_rfb_cuttext); + cl->GotCursorShape = remmina_plugin_vnc_rfb_cursor_shape; + cl->Bell = remmina_plugin_vnc_rfb_bell; + cl->HandleTextChat = remmina_plugin_vnc_rfb_chat; + /** + * @fixme we have to implement HandleXvpMsg + * cl->HandleXvpMsg = remmina_plugin_vnc_rfb_handle_xvp; + */ + + rfbClientSetClientData(cl, NULL, gp); + + if (host[0] == '\0') { + cl->serverHost = g_strdup(host); + cl->listenSpecified = TRUE; + if (remmina_plugin_service->file_get_int(remminafile, "ssh_tunnel_enabled", FALSE)) + /* When we use reverse tunnel, the local port does not really matter. + * Hardcode a default port just in case the remote port is customized + * to a privilege port then we will have problem listening. */ + cl->listenPort = 5500; + else + cl->listenPort = remmina_plugin_service->file_get_int(remminafile, "listenport", 5500); + + remmina_plugin_vnc_incoming_connection(gp, cl); + } else { + if (strstr(host, "unix://") == host) { + cl->serverHost = g_strdup(host + strlen("unix://")); + cl->serverPort = 0; + } else { + remmina_plugin_service->get_server_port(host, VNC_DEFAULT_PORT, &s, &cl->serverPort); + cl->serverHost = g_strdup(s); + g_free(s); + /* Support short-form (:0, :1) */ + if (cl->serverPort < 100) + cl->serverPort += VNC_DEFAULT_PORT; + } + } + g_free(host); + host = NULL; + + if (cl->serverHost && strstr(cl->serverHost, "unix://") != cl->serverHost && remmina_plugin_service->file_get_string(remminafile, "proxy")) { + remmina_plugin_service->get_server_port( + remmina_plugin_service->file_get_string(remminafile, "server"), + VNC_DEFAULT_PORT, + &cl->destHost, + &cl->destPort); + remmina_plugin_service->get_server_port( + remmina_plugin_service->file_get_string(remminafile, "proxy"), + VNC_DEFAULT_PORT, + &cl->serverHost, + &cl->serverPort); + REMMINA_PLUGIN_DEBUG("cl->serverHost: %s", cl->serverHost); + REMMINA_PLUGIN_DEBUG("cl->serverPort: %d", cl->serverPort); + REMMINA_PLUGIN_DEBUG("cl->destHost: %s", cl->destHost); + REMMINA_PLUGIN_DEBUG("cl->destPort: %d", cl->destPort); + } + + cl->appData.useRemoteCursor = ( + remmina_plugin_service->file_get_int(remminafile, "showcursor", FALSE) ? FALSE : TRUE); + + remmina_plugin_vnc_update_quality(cl, quality); + remmina_plugin_vnc_update_colordepth(cl, colordepth); + if ((cl->format.depth == 8) && (quality == 9)) + cl->appData.encodingsString = "copyrect zlib hextile raw"; + else if ((cl->format.depth == 8) && (quality == 2)) + cl->appData.encodingsString = "zrle ultra copyrect hextile zlib corre rre raw"; + else if ((cl->format.depth == 8) && (quality == 1)) + cl->appData.encodingsString = "zrle ultra copyrect hextile zlib corre rre raw"; + else if ((cl->format.depth == 8) && (quality == 0)) + cl->appData.encodingsString = "zrle ultra copyrect hextile zlib corre rre raw"; + SetFormatAndEncodings(cl); + + if (remmina_plugin_service->file_get_int(remminafile, "disableencryption", FALSE)) { + vnc_encryption_disable_requested = TRUE; + SetClientAuthSchemes(cl, remmina_plugin_vnc_no_encrypt_auth_types, -1); + } else { + vnc_encryption_disable_requested = FALSE; + } + + if (rfbInitClient(cl, NULL, NULL)) { + REMMINA_PLUGIN_DEBUG("Client initialization successfull"); + break; + } else { + REMMINA_PLUGIN_DEBUG("Client initialization failed"); + } + + /* If the authentication is not called, it has to be a fatal error and must quit */ + if (!gpdata->auth_called) { + REMMINA_PLUGIN_DEBUG("Client not authenticated"); + gpdata->connected = FALSE; + break; + } + + /* vnc4server reports "already in use" after authentication. Workaround here */ + if (strstr(vnc_error, "The server is already in use")) { + gpdata->connected = FALSE; + gpdata->auth_called = FALSE; + break; + } + /* Don't assume authentication failed for known network-related errors in + libvncclient/sockets.c. */ + if (strstr(vnc_error, "read (") || strstr(vnc_error, "select\n") || + strstr(vnc_error, "write\n") || strstr(vnc_error, "Connection timed out")) { + gpdata->connected = FALSE; + gpdata->auth_called = FALSE; + break; + } + + /* Otherwise, it’s a password error. Try to clear saved password if any */ + remmina_plugin_service->file_set_string(remminafile, "password", NULL); + + if (!gpdata->connected) + break; + + remmina_plugin_service->protocol_plugin_init_show_retry(gp); + + /* It’s safer to sleep a while before reconnect */ + sleep(2); + + gpdata->auth_first = FALSE; + } + + if (!gpdata->connected) { + REMMINA_PLUGIN_DEBUG("Client not connected with error: %s", vnc_error); + if (cl && !gpdata->auth_called && !(remmina_plugin_service->protocol_plugin_has_error(gp))) + remmina_plugin_service->protocol_plugin_set_error(gp, "%s", vnc_error); + gpdata->running = FALSE; + + remmina_plugin_service->protocol_plugin_signal_connection_closed(gp); + + return FALSE; + } + + REMMINA_PLUGIN_DEBUG("Client connected"); + remmina_plugin_service->protocol_plugin_init_save_cred(gp); + + gpdata->client = cl; + + remmina_plugin_service->protocol_plugin_signal_connection_opened(gp); + + if (remmina_plugin_service->file_get_int(remminafile, "disableserverinput", FALSE)) + PermitServerInput(cl, 1); + + if (gpdata->thread) { + while (remmina_plugin_vnc_main_loop(gp)) { + } + gpdata->running = FALSE; + } else { + IDLE_ADD((GSourceFunc)remmina_plugin_vnc_main_loop, gp); + } + + return FALSE; +} + + +static gpointer +remmina_plugin_vnc_main_thread(gpointer data) +{ + TRACE_CALL(__func__); + pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); + + CANCEL_ASYNC + remmina_plugin_vnc_main((RemminaProtocolWidget *)data); + return NULL; +} + + +static RemminaPluginVncCoordinates remmina_plugin_vnc_scale_coordinates(GtkWidget *widget, RemminaProtocolWidget *gp, gint x, gint y) +{ + GtkAllocation widget_allocation; + RemminaPluginVncCoordinates result; + + if ((remmina_plugin_service->remmina_protocol_widget_get_current_scale_mode(gp) != REMMINA_PROTOCOL_WIDGET_SCALE_MODE_NONE)) { + gtk_widget_get_allocation(widget, &widget_allocation); + result.x = x * remmina_plugin_service->protocol_plugin_get_width(gp) / widget_allocation.width; + result.y = y * remmina_plugin_service->protocol_plugin_get_height(gp) / widget_allocation.height; + } else { + result.x = x; + result.y = y; + } + + return result; +} + +static gboolean remmina_plugin_vnc_on_motion(GtkWidget *widget, GdkEventMotion *event, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + RemminaFile *remminafile; + RemminaPluginVncCoordinates coordinates; + + if (!gpdata->connected || !gpdata->client) + return FALSE; + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + if (remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE)) + return FALSE; + + coordinates = remmina_plugin_vnc_scale_coordinates(widget, gp, event->x, event->y); + remmina_plugin_vnc_event_push(gp, REMMINA_PLUGIN_VNC_EVENT_POINTER, GINT_TO_POINTER(coordinates.x), GINT_TO_POINTER(coordinates.y), + GINT_TO_POINTER(gpdata->button_mask)); + return TRUE; +} + +static gboolean remmina_plugin_vnc_on_button(GtkWidget *widget, GdkEventButton *event, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + RemminaFile *remminafile; + RemminaPluginVncCoordinates coordinates; + gint mask; + + if (!gpdata->connected || !gpdata->client) + return FALSE; + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + if (remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE)) + return FALSE; + + /* We only accept 3 buttons */ + if (event->button < 1 || event->button > 3) + return FALSE; + /* We bypass 2button-press and 3button-press events */ + if (event->type != GDK_BUTTON_PRESS && event->type != GDK_BUTTON_RELEASE) + return TRUE; + + mask = (1 << (event->button - 1)); + gpdata->button_mask = (event->type == GDK_BUTTON_PRESS ? (gpdata->button_mask | mask) : + (gpdata->button_mask & (0xff - mask))); + + coordinates = remmina_plugin_vnc_scale_coordinates(widget, gp, event->x, event->y); + remmina_plugin_vnc_event_push(gp, REMMINA_PLUGIN_VNC_EVENT_POINTER, GINT_TO_POINTER(coordinates.x), GINT_TO_POINTER(coordinates.y), + GINT_TO_POINTER(gpdata->button_mask)); + return TRUE; +} + +static gint delta_to_mask(float delta, float *accum, gint mask_plus, gint mask_minus) +{ + *accum += delta; + if (*accum >= 1.0) { + *accum = 0.0; + return mask_plus; + } else if (*accum <= -1.0) { + *accum = 0.0; + return mask_minus; + } + return 0; +} + +static gboolean remmina_plugin_vnc_on_scroll(GtkWidget *widget, GdkEventScroll *event, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + RemminaFile *remminafile; + RemminaPluginVncCoordinates coordinates; + gint mask; + + if (!gpdata->connected || !gpdata->client) + return FALSE; + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + if (remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE)) + return FALSE; + + switch (event->direction) { + case GDK_SCROLL_UP: + mask = (1 << 3); + gpdata->scroll_y_accumulator = 0; + break; + case GDK_SCROLL_DOWN: + mask = (1 << 4); + gpdata->scroll_y_accumulator = 0; + break; + case GDK_SCROLL_LEFT: + mask = (1 << 5); + gpdata->scroll_x_accumulator = 0; + break; + case GDK_SCROLL_RIGHT: + mask = (1 << 6); + gpdata->scroll_x_accumulator = 0; + break; +#if GTK_CHECK_VERSION(3, 4, 0) + case GDK_SCROLL_SMOOTH: + /* RFB does not seems to support SMOOTH scroll, so we accumulate GTK delta requested + * up to 1.0 and then send a normal RFB wheel scroll when the accumulator reaches 1.0 */ + mask = delta_to_mask(event->delta_y, &(gpdata->scroll_y_accumulator), (1 << 4), (1 << 3)); + mask |= delta_to_mask(event->delta_x, &(gpdata->scroll_x_accumulator), (1 << 6), (1 << 5)); + if (!mask) + return FALSE; + break; +#endif + default: + return FALSE; + } + + coordinates = remmina_plugin_vnc_scale_coordinates(widget, gp, event->x, event->y); + remmina_plugin_vnc_event_push(gp, REMMINA_PLUGIN_VNC_EVENT_POINTER, GINT_TO_POINTER(coordinates.x), GINT_TO_POINTER(coordinates.y), + GINT_TO_POINTER(mask | gpdata->button_mask)); + remmina_plugin_vnc_event_push(gp, REMMINA_PLUGIN_VNC_EVENT_POINTER, GINT_TO_POINTER(coordinates.x), GINT_TO_POINTER(coordinates.y), + GINT_TO_POINTER(gpdata->button_mask)); + + return TRUE; +} + +static void remmina_plugin_vnc_release_key(RemminaProtocolWidget *gp, guint16 keycode) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + RemminaKeyVal *k; + gint i; + + if (!gpdata) + return; + + if (keycode == 0) { + /* Send all release key events for previously pressed keys */ + for (i = 0; i < gpdata->pressed_keys->len; i++) { + k = g_ptr_array_index(gpdata->pressed_keys, i); + remmina_plugin_vnc_event_push(gp, REMMINA_PLUGIN_VNC_EVENT_KEY, GUINT_TO_POINTER(k->keyval), + GINT_TO_POINTER(FALSE), NULL); + g_free(k); + } + g_ptr_array_set_size(gpdata->pressed_keys, 0); + } else { + /* Unregister the keycode only */ + for (i = 0; i < gpdata->pressed_keys->len; i++) { + k = g_ptr_array_index(gpdata->pressed_keys, i); + if (k->keycode == keycode) { + g_free(k); + g_ptr_array_remove_index_fast(gpdata->pressed_keys, i); + break; + } + } + } +} + +static gboolean remmina_plugin_vnc_on_key(GtkWidget *widget, GdkEventKey *event, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + RemminaFile *remminafile; + RemminaKeyVal *k; + guint event_keyval; + guint keyval; + + if (!gpdata->connected || !gpdata->client) + return FALSE; + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + if (remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE)) + return FALSE; + + gpdata->scroll_x_accumulator = 0; + gpdata->scroll_y_accumulator = 0; + + /* When sending key release, try first to find out a previously sent keyval + * to workaround bugs like https://bugs.freedesktop.org/show_bug.cgi?id=7430 */ + + event_keyval = event->keyval; + if (event->type == GDK_KEY_RELEASE) { + for (int i = 0; i < gpdata->pressed_keys->len; i++) { + k = g_ptr_array_index(gpdata->pressed_keys, i); + if (k->keycode == event->hardware_keycode) { + event_keyval = k->keyval; + break; + } + } + } + + keyval = remmina_plugin_service->pref_keymap_get_keyval(remmina_plugin_service->file_get_string(remminafile, "keymap"), + event_keyval); + + remmina_plugin_vnc_event_push(gp, REMMINA_PLUGIN_VNC_EVENT_KEY, GUINT_TO_POINTER(keyval), + GINT_TO_POINTER(event->type == GDK_KEY_PRESS ? TRUE : FALSE), NULL); + + /* Register/unregister the pressed key */ + if (event->type == GDK_KEY_PRESS) { + k = g_new(RemminaKeyVal, 1); + k->keyval = keyval; + k->keycode = event->hardware_keycode; + g_ptr_array_add(gpdata->pressed_keys, k); + } else { + remmina_plugin_vnc_release_key(gp, event->hardware_keycode); + } + return TRUE; +} + +static void remmina_plugin_vnc_on_cuttext_request(GtkClipboard *clipboard, const gchar *text, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + GDateTime *t; + glong diff; + gsize br, bw; + gchar *latin1_text; + const char *cur_charset; + + if (text) { + /* A timer (1 second) to avoid clipboard "loopback": text cut out from VNC won’t paste back into VNC */ + t = g_date_time_new_now_utc(); + diff = g_date_time_difference(t, gpdata->clipboard_timer) / 100000; // tenth of second + if (diff < 10) + return; + g_date_time_unref(gpdata->clipboard_timer); + gpdata->clipboard_timer = t; + /* Convert text from current charset to latin-1 before sending to remote server. + * See RFC6143 7.5.6 */ + g_get_charset(&cur_charset); + latin1_text = g_convert_with_fallback(text, -1, "ISO-8859-1", cur_charset, "?", &br, &bw, NULL); + remmina_plugin_vnc_event_push(gp, REMMINA_PLUGIN_VNC_EVENT_CUTTEXT, (gpointer)latin1_text, NULL, NULL); + g_free(latin1_text); + } +} + +static void remmina_plugin_vnc_on_cuttext(GtkClipboard *clipboard, GdkEvent *event, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + RemminaFile *remminafile; + + if (!gpdata->connected || !gpdata->client) + return; + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + if (remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE)) + return; + + gtk_clipboard_request_text(clipboard, (GtkClipboardTextReceivedFunc)remmina_plugin_vnc_on_cuttext_request, gp); +} + +static void remmina_plugin_vnc_on_realize(RemminaProtocolWidget *gp, gpointer data) +{ + TRACE_CALL(__func__); + RemminaFile *remminafile; + GdkCursor *cursor; + GdkPixbuf *pixbuf; + + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + + if (remmina_plugin_service->file_get_int(remminafile, "showcursor", FALSE)) { + /* Hide local cursor (show a small dot instead) */ + pixbuf = gdk_pixbuf_new_from_xpm_data(dot_cursor_xpm); + cursor = gdk_cursor_new_from_pixbuf(gdk_display_get_default(), pixbuf, dot_cursor_x_hot, dot_cursor_y_hot); + g_object_unref(pixbuf); + gdk_window_set_cursor(gtk_widget_get_window(GTK_WIDGET(gp)), cursor); + g_object_unref(cursor); + } +} + +/******************************************************************************************/ + +static gboolean remmina_plugin_vnc_open_connection(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + RemminaFile *remminafile; + + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + + gpdata->connected = TRUE; + gchar *server; + gint port; + const gchar* raw_server; + + remmina_plugin_service->protocol_plugin_register_hostkey(gp, gpdata->drawing_area); + + g_signal_connect(G_OBJECT(gp), "realize", G_CALLBACK(remmina_plugin_vnc_on_realize), NULL); + g_signal_connect(G_OBJECT(gpdata->drawing_area), "motion-notify-event", G_CALLBACK(remmina_plugin_vnc_on_motion), gp); + g_signal_connect(G_OBJECT(gpdata->drawing_area), "button-press-event", G_CALLBACK(remmina_plugin_vnc_on_button), gp); + g_signal_connect(G_OBJECT(gpdata->drawing_area), "button-release-event", G_CALLBACK(remmina_plugin_vnc_on_button), gp); + g_signal_connect(G_OBJECT(gpdata->drawing_area), "scroll-event", G_CALLBACK(remmina_plugin_vnc_on_scroll), gp); + g_signal_connect(G_OBJECT(gpdata->drawing_area), "key-press-event", G_CALLBACK(remmina_plugin_vnc_on_key), gp); + g_signal_connect(G_OBJECT(gpdata->drawing_area), "key-release-event", G_CALLBACK(remmina_plugin_vnc_on_key), gp); + + if (!remmina_plugin_service->file_get_int(remminafile, "disableclipboard", FALSE)) + gpdata->clipboard_handler = g_signal_connect(G_OBJECT(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD)), + "owner-change", G_CALLBACK(remmina_plugin_vnc_on_cuttext), gp); + + + if (pthread_create(&gpdata->thread, NULL, remmina_plugin_vnc_main_thread, gp)) { + /* I don’t think this will ever happen… */ + g_print("Could not initialize pthread. Falling back to non-thread mode…\n"); + g_timeout_add(0, (GSourceFunc)remmina_plugin_vnc_main, gp); + gpdata->thread = 0; + } + + raw_server = remmina_plugin_service->file_get_string(remminafile, "server"); + + if (raw_server && strstr(raw_server, "unix://") == raw_server) { + REMMINA_PLUGIN_AUDIT(_("Connected to %s via VNC"), raw_server); + } else { + remmina_plugin_service->get_server_port(raw_server, + VNC_DEFAULT_PORT, + &server, + &port); + + REMMINA_PLUGIN_AUDIT(_("Connected to %s:%d via VNC"), server, port); + g_free(server), server = NULL; + } +#if LIBVNCSERVER_CHECK_VERSION_VERSION(0, 9, 14) + remmina_plugin_service->protocol_plugin_unlock_dynres(gp); +#endif + return TRUE; +} + +static gboolean remmina_plugin_vnc_close_connection_timeout(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + + gchar *server; + 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"), + VNC_DEFAULT_PORT, + &server, + &port); + + REMMINA_PLUGIN_AUDIT(_("Disconnected from %s:%d via VNC"), server, port); + g_free(server), server = NULL; + + /* wait until the running attribute is set to false by the VNC thread */ + if (gpdata->running) + return TRUE; + + /* unregister the clipboard monitor */ + if (gpdata->clipboard_handler) { + g_signal_handler_disconnect(G_OBJECT(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD)), gpdata->clipboard_handler); + gpdata->clipboard_handler = 0; + } + + if (gpdata->queuecursor_handler) { + g_source_remove(gpdata->queuecursor_handler); + gpdata->queuecursor_handler = 0; + } + if (gpdata->queuecursor_surface) { + cairo_surface_destroy(gpdata->queuecursor_surface); + gpdata->queuecursor_surface = NULL; + } + + if (gpdata->queuedraw_handler) { + g_source_remove(gpdata->queuedraw_handler); + gpdata->queuedraw_handler = 0; + } + if (gpdata->listen_sock >= 0) + close(gpdata->listen_sock); + if (gpdata->client) { + rfbClientCleanup((rfbClient *)gpdata->client); + gpdata->client = NULL; + } + if (gpdata->rgb_buffer) { + cairo_surface_destroy(gpdata->rgb_buffer); + gpdata->rgb_buffer = NULL; + } + if (gpdata->vnc_buffer) { + g_free(gpdata->vnc_buffer); + gpdata->vnc_buffer = NULL; + } + g_ptr_array_free(gpdata->pressed_keys, TRUE); + g_date_time_unref(gpdata->clipboard_timer); + remmina_plugin_vnc_event_free_all(gp); + g_queue_free(gpdata->vnc_event_queue); + pthread_mutex_destroy(&gpdata->vnc_event_queue_mutex); + close(gpdata->vnc_event_pipe[0]); + close(gpdata->vnc_event_pipe[1]); + + + pthread_mutex_destroy(&gpdata->buffer_mutex); + remmina_plugin_service->protocol_plugin_signal_connection_closed(gp); + + return FALSE; +} + +static gboolean remmina_plugin_vnc_close_connection(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + + gpdata->connected = FALSE; + + if (gpdata->thread) { + pthread_cancel(gpdata->thread); + if (gpdata->thread) pthread_join(gpdata->thread, NULL); + gpdata->running = FALSE; + remmina_plugin_vnc_close_connection_timeout(gp); + } else { + g_timeout_add(200, (GSourceFunc)remmina_plugin_vnc_close_connection_timeout, gp); + } + + return FALSE; +} + +static gboolean remmina_plugin_vnc_query_feature(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + switch (feature->id) { + case REMMINA_PLUGIN_VNC_FEATURE_PREF_DISABLESERVERINPUT: + return SupportsClient2Server((rfbClient *)(gpdata->client), rfbSetServerInput) ? TRUE : FALSE; + case REMMINA_PLUGIN_VNC_FEATURE_TOOL_CHAT: + return SupportsClient2Server((rfbClient *)(gpdata->client), rfbTextChat) ? TRUE : FALSE; + default: + return TRUE; + } +} + +static void remmina_plugin_vnc_call_feature(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + RemminaFile *remminafile; + rfbClient* client; + uint8_t previous_bpp; + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + switch (feature->id) { + case REMMINA_PLUGIN_VNC_FEATURE_PREF_QUALITY: + remmina_plugin_vnc_update_quality((rfbClient *)(gpdata->client), + remmina_plugin_service->file_get_int(remminafile, "quality", 9)); + remmina_plugin_vnc_update_colordepth((rfbClient *)(gpdata->client), + remmina_plugin_service->file_get_int(remminafile, "colordepth", 32)); + SetFormatAndEncodings((rfbClient *)(gpdata->client)); + break; + case REMMINA_PLUGIN_VNC_FEATURE_PREF_COLOR: + client = (rfbClient *)(gpdata->client); + previous_bpp = client->format.bitsPerPixel; + remmina_plugin_vnc_update_colordepth(client, + remmina_plugin_service->file_get_int(remminafile, "colordepth", 32)); + SetFormatAndEncodings(client); + //Need to clear away old and reallocate if we're increasing bpp + if (client->format.bitsPerPixel > previous_bpp){ + remmina_plugin_vnc_rfb_allocfb((rfbClient *)(gpdata->client)); + SendFramebufferUpdateRequest((rfbClient *)(gpdata->client), 0, 0, + remmina_plugin_service->protocol_plugin_get_width(gp), + remmina_plugin_service->protocol_plugin_get_height(gp), FALSE); + } + break; + case REMMINA_PLUGIN_VNC_FEATURE_PREF_VIEWONLY: + break; + case REMMINA_PLUGIN_VNC_FEATURE_PREF_DISABLESERVERINPUT: + PermitServerInput((rfbClient *)(gpdata->client), + remmina_plugin_service->file_get_int(remminafile, "disableserverinput", FALSE) ? 1 : 0); + break; + case REMMINA_PLUGIN_VNC_FEATURE_UNFOCUS: + remmina_plugin_vnc_release_key(gp, 0); + break; + case REMMINA_PLUGIN_VNC_FEATURE_SCALE: + remmina_plugin_vnc_update_scale(gp, remmina_plugin_service->file_get_int(remminafile, "scale", FALSE)); + break; + case REMMINA_PLUGIN_VNC_FEATURE_TOOL_REFRESH: + SendFramebufferUpdateRequest((rfbClient *)(gpdata->client), 0, 0, + remmina_plugin_service->protocol_plugin_get_width(gp), + remmina_plugin_service->protocol_plugin_get_height(gp), FALSE); + break; + case REMMINA_PLUGIN_VNC_FEATURE_TOOL_CHAT: + remmina_plugin_vnc_open_chat(gp); + break; + case REMMINA_PLUGIN_VNC_FEATURE_TOOL_SENDCTRLALTDEL: + remmina_plugin_vnc_send_ctrlaltdel(gp); + break; + default: + break; + } +} + +/* Send a keystroke to the plugin window */ +static void remmina_plugin_vnc_keystroke(RemminaProtocolWidget *gp, const guint keystrokes[], const gint keylen) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + + remmina_plugin_service->protocol_plugin_send_keys_signals(gpdata->drawing_area, + keystrokes, keylen, GDK_KEY_PRESS | GDK_KEY_RELEASE); + return; +} + +#if LIBVNCSERVER_CHECK_VERSION_VERSION(0, 9, 14) +static gboolean remmina_plugin_vnc_on_size_allocate(GtkWidget *widget, GtkAllocation *alloc, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaScaleMode scale_mode = remmina_plugin_service->remmina_protocol_widget_get_current_scale_mode(gp); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + + if (scale_mode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES){ + char str[1024]; + sprintf(str, "DEBUG: %d x %d", alloc->width, alloc->height); + TRACE_CALL(str); + if (gpdata->client){ + SendExtDesktopSize(gpdata->client, alloc->width, alloc->height); + } + } + return TRUE; +} +#endif + +static gboolean remmina_plugin_vnc_on_draw(GtkWidget *widget, cairo_t *context, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + cairo_surface_t *surface; + gint width, height; + GtkAllocation widget_allocation; + + LOCK_BUFFER(FALSE); + + surface = gpdata->rgb_buffer; + if (!surface) { + UNLOCK_BUFFER(FALSE); + return FALSE; + } + + width = remmina_plugin_service->protocol_plugin_get_width(gp); + height = remmina_plugin_service->protocol_plugin_get_height(gp); + + if ((remmina_plugin_service->remmina_protocol_widget_get_current_scale_mode(gp) != REMMINA_PROTOCOL_WIDGET_SCALE_MODE_NONE)) { + gtk_widget_get_allocation(widget, &widget_allocation); + cairo_scale(context, + (double)widget_allocation.width / width, + (double)widget_allocation.height / height); + } + + cairo_rectangle(context, 0, 0, width, height); + cairo_set_source_surface(context, surface, 0, 0); + cairo_fill(context); + + UNLOCK_BUFFER(FALSE); + return TRUE; +} + +static void remmina_plugin_vnc_init(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata; + gint flags; + gdouble aspect_ratio; + + gpdata = g_new0(RemminaPluginVncData, 1); + g_object_set_data_full(G_OBJECT(gp), "plugin-data", gpdata, g_free); + + gboolean disable_smooth_scrolling = FALSE; + RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + + disable_smooth_scrolling = remmina_plugin_service->file_get_int(remminafile, "disablesmoothscrolling", FALSE); + REMMINA_PLUGIN_DEBUG("Disable smooth scrolling is set to %d", disable_smooth_scrolling); + + gpdata->drawing_area = gtk_drawing_area_new(); + gtk_widget_show(gpdata->drawing_area); + + aspect_ratio = remmina_plugin_service->file_get_double(remminafile, "aspect_ratio", 0); + if (aspect_ratio > 0){ + GtkWidget* aspectframe = gtk_aspect_frame_new(NULL, 0, 0, aspect_ratio, FALSE); + + gtk_frame_set_shadow_type(GTK_FRAME(aspectframe), GTK_SHADOW_NONE); + gtk_widget_show(aspectframe); + gtk_container_add(GTK_CONTAINER(aspectframe), gpdata->drawing_area); + gtk_container_add(GTK_CONTAINER(gp), aspectframe); + } + else{ + gtk_container_add(GTK_CONTAINER(gp), gpdata->drawing_area); + } + + gtk_widget_add_events( + gpdata->drawing_area, + GDK_POINTER_MOTION_MASK | GDK_BUTTON_PRESS_MASK + | GDK_BUTTON_RELEASE_MASK | GDK_KEY_PRESS_MASK + | GDK_KEY_RELEASE_MASK + | GDK_SCROLL_MASK); + gtk_widget_set_can_focus(gpdata->drawing_area, TRUE); + + if (!disable_smooth_scrolling) { + REMMINA_PLUGIN_DEBUG("Adding GDK_SMOOTH_SCROLL_MASK"); + gtk_widget_add_events(gpdata->drawing_area, GDK_SMOOTH_SCROLL_MASK); + } + + + g_signal_connect(G_OBJECT(gpdata->drawing_area), "draw", G_CALLBACK(remmina_plugin_vnc_on_draw), gp); +#if LIBVNCSERVER_CHECK_VERSION_VERSION(0, 9, 14) + g_signal_connect(G_OBJECT(gpdata->drawing_area), "size-allocate", G_CALLBACK(remmina_plugin_vnc_on_size_allocate), gp); +#endif + gpdata->auth_first = TRUE; + gpdata->clipboard_timer = g_date_time_new_now_utc(); + gpdata->listen_sock = -1; + gpdata->pressed_keys = g_ptr_array_new(); + gpdata->vnc_event_queue = g_queue_new(); + pthread_mutex_init(&gpdata->vnc_event_queue_mutex, NULL); + if (pipe(gpdata->vnc_event_pipe)) { + g_print("Error creating pipes.\n"); + gpdata->vnc_event_pipe[0] = 0; + gpdata->vnc_event_pipe[1] = 0; + } + flags = fcntl(gpdata->vnc_event_pipe[0], F_GETFL, 0); + fcntl(gpdata->vnc_event_pipe[0], F_SETFL, flags | O_NONBLOCK); + + pthread_mutex_init(&gpdata->buffer_mutex, NULL); +} + +/* Array of key/value pairs for color depths */ +static gpointer colordepth_list[] = +{ + "32", N_("True colour (32 bpp)"), + "16", N_("High colour (16 bpp)"), + "8", N_("256 colours (8 bpp)"), + NULL +}; + +/* Array of key/value pairs for quality selection */ +static gpointer quality_list[] = +{ + "2", N_("Good"), + "9", N_("Best (slowest)"), + "1", N_("Medium"), + "0", N_("Poor (fastest)"), + NULL +}; + +static gchar repeater_tooltip[] = + N_("Connect to VNC using a repeater:\n" + " • The server field must contain the repeater ID, e.g. ID:123456789\n" + " • The repeater field have to be set to the repeater IP and port, like:\n" + " 10.10.10.12:5901\n" + " • From the remote VNC server, you will connect to\n" + " the repeater, e.g. with x11vnc:\n" + " x11vnc -connect repeater=ID:123456789+10.10.10.12:5500"); + +static gchar vnciport_tooltip[] = + N_("Listening for remote VNC connection:\n" + " • The “Listen on port” field is the port Remmina will listen to,\n" + " e.g. 8888\n" + " • From the remote VNC server, you will connect to\n" + " Remmina, e.g. with x11vnc:\n" + " x11vnc -display :0 -connect 192.168.1.36:8888"); + +static gchar aspect_ratio_tooltip[] = + N_("Lock the aspect ratio when dynamic resolution is enabled:\n" + "\n" + " • The aspect ratio should be entered as a decimal number, e.g. 1.777\n" + " • 16:9 corresponds roughly to 1.7777, 4:3 corresponds roughly to 1.333\n" + " • The default value of 0 does not enforce any aspect ratio"); + +static gchar vncencodings_tooltip[] = + N_("Overriding the pre-set VNC encoding quality:\n" + "\n" + " • “Poor (fastest)” sets encoding to “copyrect zlib hextile raw”\n" + " • “Medium” sets encoding to “tight zrle ultra copyrect hextile zlib corre rre raw”\n" + " • “Good” sets encoding to “tight zrle ultra copyrect hextile zlib corre rre raw”\n" + " • “Best (slowest)” sets encoding to “copyrect zrle ultra zlib hextile corre rre raw”"); + +/* 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_vnc_basic_settings[] = +{ + { REMMINA_PROTOCOL_SETTING_TYPE_SERVER, "server", NULL, FALSE, "_rfb._tcp", NULL, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "proxy", N_("Repeater"), FALSE, NULL, repeater_tooltip, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "username", N_("Username"), FALSE, NULL, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD, "password", N_("User password"), FALSE, NULL, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "colordepth", N_("Colour depth"), FALSE, colordepth_list, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "quality", N_("Quality"), FALSE, quality_list, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_KEYMAP, "keymap", NULL, FALSE, NULL, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, FALSE, NULL, NULL, NULL, NULL } +}; + +// Same as above. +static const RemminaProtocolSetting remmina_plugin_vnci_basic_settings[] = +{ + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "listenport", N_("Listen on port"), FALSE, NULL, vnciport_tooltip, NULL, NULL}, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "username", N_("Username"), FALSE, NULL, NULL, NULL, NULL}, + { REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD, "password", N_("User password"), FALSE, NULL, NULL, NULL, NULL}, + { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "colordepth", N_("Colour depth"), FALSE, colordepth_list, NULL, NULL, NULL}, + { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "quality", N_("Quality"), FALSE, quality_list, NULL, NULL, NULL}, + { REMMINA_PROTOCOL_SETTING_TYPE_KEYMAP, "keymap", NULL, 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_vnc_advanced_settings[] = +{ + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "encodings", N_("Override pre-set VNC encodings"), FALSE, NULL, vncencodings_tooltip }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "aspect_ratio", N_("Dynamic resolution enforced aspec ratio"), FALSE, NULL, aspect_ratio_tooltip }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "tightencoding", N_("Force tight encoding"), TRUE, NULL, N_("Enabling this may help when the remote desktop looks scrambled") }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disablesmoothscrolling", N_("Disable smooth scrolling"), FALSE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disablepasswordstoring", N_("Forget passwords after use"), TRUE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disableserverbell", N_("Ignore remote bell messages"), FALSE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disableserverinput", N_("Prevent local interaction on the server"), TRUE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "showcursor", N_("Show remote cursor"), FALSE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disableclipboard", N_("Turn off clipboard sync"), TRUE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disableencryption", N_("Turn off encryption"), FALSE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "viewonly", N_("View only"), TRUE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, FALSE, NULL, NULL } +}; + +/* Array for available features. + * The last element of the array must be REMMINA_PROTOCOL_FEATURE_TYPE_END. */ +static const RemminaProtocolFeature remmina_plugin_vnc_features[] = +{ + { REMMINA_PROTOCOL_FEATURE_TYPE_PREF, REMMINA_PLUGIN_VNC_FEATURE_PREF_QUALITY, GINT_TO_POINTER(REMMINA_PROTOCOL_FEATURE_PREF_RADIO), "quality", + quality_list }, + { REMMINA_PROTOCOL_FEATURE_TYPE_PREF, REMMINA_PLUGIN_VNC_FEATURE_PREF_COLOR, GINT_TO_POINTER(REMMINA_PROTOCOL_FEATURE_PREF_RADIO), "colordepth", + colordepth_list }, + { REMMINA_PROTOCOL_FEATURE_TYPE_PREF, REMMINA_PLUGIN_VNC_FEATURE_PREF_VIEWONLY, GINT_TO_POINTER(REMMINA_PROTOCOL_FEATURE_PREF_CHECK), "viewonly", + N_("View only") }, + { REMMINA_PROTOCOL_FEATURE_TYPE_PREF, REMMINA_PLUGIN_VNC_FEATURE_PREF_DISABLESERVERINPUT, GINT_TO_POINTER(REMMINA_PROTOCOL_FEATURE_PREF_CHECK), "disableserverinput",N_("Prevent local interaction on the server") }, + { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, REMMINA_PLUGIN_VNC_FEATURE_TOOL_REFRESH, N_("Refresh"), NULL, NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, REMMINA_PLUGIN_VNC_FEATURE_TOOL_CHAT, N_("Open Chat…"), "face-smile", NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, REMMINA_PLUGIN_VNC_FEATURE_TOOL_SENDCTRLALTDEL, N_("Send Ctrl+Alt+Delete"), NULL, NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_SCALE, REMMINA_PLUGIN_VNC_FEATURE_SCALE, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_UNFOCUS, REMMINA_PLUGIN_VNC_FEATURE_UNFOCUS, NULL, NULL, NULL }, +#if LIBVNCSERVER_CHECK_VERSION_VERSION(0, 9, 14) + { REMMINA_PROTOCOL_FEATURE_TYPE_DYNRESUPDATE, REMMINA_PLUGIN_VNC_FEATURE_DYNRESUPDATE, NULL, NULL, NULL }, +#endif + { REMMINA_PROTOCOL_FEATURE_TYPE_END, 0, NULL, NULL, NULL } +}; + +/* Protocol plugin definition and features */ +static RemminaProtocolPlugin remmina_plugin_vnc = +{ + REMMINA_PLUGIN_TYPE_PROTOCOL, // Type + VNC_PLUGIN_NAME, // Name + VNC_PLUGIN_DESCRIPTION, // Description + GETTEXT_PACKAGE, // Translation domain + VNC_PLUGIN_VERSION, // Version number + VNC_PLUGIN_APPICON, // Icon for normal connection + VNC_PLUGIN_SSH_APPICON, // Icon for SSH connection + remmina_plugin_vnc_basic_settings, // Array for basic settings + remmina_plugin_vnc_advanced_settings, // Array for advanced settings + REMMINA_PROTOCOL_SSH_SETTING_TUNNEL, // SSH settings type + remmina_plugin_vnc_features, // Array for available features + remmina_plugin_vnc_init, // Plugin initialization + remmina_plugin_vnc_open_connection, // Plugin open connection + remmina_plugin_vnc_close_connection, // Plugin close connection + remmina_plugin_vnc_query_feature, // Query for available features + remmina_plugin_vnc_call_feature, // Call a feature + remmina_plugin_vnc_keystroke // Send a keystroke +}; + +/* Protocol plugin definition and features */ +static RemminaProtocolPlugin remmina_plugin_vnci = +{ + REMMINA_PLUGIN_TYPE_PROTOCOL, // Type + VNCI_PLUGIN_NAME, // Name + VNCI_PLUGIN_DESCRIPTION, // Description + GETTEXT_PACKAGE, // Translation domain + VERSION, // Version number + VNCI_PLUGIN_APPICON, // Icon for normal connection + VNCI_PLUGIN_SSH_APPICON, // Icon for SSH connection + remmina_plugin_vnci_basic_settings, // Array for basic settings + remmina_plugin_vnc_advanced_settings, // Array for advanced settings + REMMINA_PROTOCOL_SSH_SETTING_REVERSE_TUNNEL, // SSH settings type + remmina_plugin_vnc_features, // Array for available features + remmina_plugin_vnc_init, // Plugin initialization + remmina_plugin_vnc_open_connection, // Plugin open connection + remmina_plugin_vnc_close_connection, // Plugin close connection + remmina_plugin_vnc_query_feature, // Query for available features + remmina_plugin_vnc_call_feature, // Call a feature + remmina_plugin_vnc_keystroke, // Send a keystroke + NULL, // No screenshot support available + NULL, // RCW map event + NULL // RCW unmap event +}; + +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"); + + if (!service->register_plugin((RemminaPlugin *)&remmina_plugin_vnc)) + return FALSE; + + if (!service->register_plugin((RemminaPlugin *)&remmina_plugin_vnci)) + return FALSE; + + return TRUE; +} diff --git a/plugins/vnc/vnc_plugin.h b/plugins/vnc/vnc_plugin.h new file mode 100644 index 0000000..a0a1732 --- /dev/null +++ b/plugins/vnc/vnc_plugin.h @@ -0,0 +1,158 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * 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" + +#ifndef __PLUGIN_CONFIG_H +#define __PLUGIN_CONFIG_H + +#define VNC_PLUGIN_NAME "VNC" +#define VNC_PLUGIN_DESCRIPTION N_("Remmina VNC Plugin") +#define VNC_PLUGIN_VERSION VERSION +#define VNC_PLUGIN_APPICON "org.remmina.Remmina-vnc-symbolic" +#define VNC_PLUGIN_SSH_APPICON "org.remmina.Remmina-vnc-ssh-symbolic" +#define VNCI_PLUGIN_NAME "VNCI" +#define VNCI_PLUGIN_DESCRIPTION N_("Remmina VNC listener Plugin") +#define VNCI_PLUGIN_VERSION VERSION +#define VNCI_PLUGIN_APPICON "org.remmina.Remmina-vnc-symbolic" +#define VNCI_PLUGIN_SSH_APPICON "org.remmina.Remmina-vnc-ssh-symbolic" +#endif + +#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__) + +#define LIBVNCSERVER_CHECK_VERSION_VERSION(major,minor,patchlevel) \ + (LIBVNC_INT_MAJOR > (major) || \ + (LIBVNC_INT_MAJOR == (major) && LIBVNC_INT_MINOR > (minor)) || \ + (LIBVNC_INT_MAJOR == (major) && LIBVNC_INT_MINOR == (minor) && \ + LIBVNC_INT_PATCH >= (patchlevel))) + +typedef struct _RemminaPluginVncData { + /* Whether the user requests to connect/disconnect */ + gboolean connected; + /* Whether the vnc process is running */ + gboolean running; + /* Whether the initialization calls the authentication process */ + gboolean auth_called; + /* Whether it is the first attempt for authentication. Only first attempt will try to use cached credentials */ + gboolean auth_first; + + GtkWidget * drawing_area; + guchar * vnc_buffer; + cairo_surface_t * rgb_buffer; + + gint queuedraw_x, queuedraw_y, queuedraw_w, queuedraw_h; + guint queuedraw_handler; + + gulong clipboard_handler; + GDateTime *clipboard_timer; + + cairo_surface_t * queuecursor_surface; + gint queuecursor_x, queuecursor_y; + guint queuecursor_handler; + + gpointer client; + gint listen_sock; + + gint button_mask; + + GPtrArray * pressed_keys; + + pthread_mutex_t vnc_event_queue_mutex; + GQueue * vnc_event_queue; + gint vnc_event_pipe[2]; + + pthread_t thread; + pthread_mutex_t buffer_mutex; + + float scroll_x_accumulator, scroll_y_accumulator; + +} RemminaPluginVncData; + +enum { + REMMINA_PLUGIN_VNC_EVENT_KEY, + REMMINA_PLUGIN_VNC_EVENT_POINTER, + REMMINA_PLUGIN_VNC_EVENT_CUTTEXT, + REMMINA_PLUGIN_VNC_EVENT_CHAT_OPEN, + REMMINA_PLUGIN_VNC_EVENT_CHAT_SEND, + REMMINA_PLUGIN_VNC_EVENT_CHAT_CLOSE +}; + +typedef struct _RemminaPluginVncEvent { + gint event_type; + union { + struct { + guint keyval; + gboolean pressed; + } key; + struct { + gint x; + gint y; + gint button_mask; + } pointer; + struct { + gchar *text; + } text; + } event_data; +} RemminaPluginVncEvent; + +typedef struct _RemminaPluginVncCoordinates { + gint x, y; +} RemminaPluginVncCoordinates; + +G_BEGIN_DECLS + +/* --------- Support for execution on main thread of GUI functions -------------- */ +static void remmina_plugin_vnc_update_scale(RemminaProtocolWidget *gp, gboolean scale); + +G_END_DECLS diff --git a/plugins/www/CMakeLists.txt b/plugins/www/CMakeLists.txt new file mode 100644 index 0000000..f9aa907 --- /dev/null +++ b/plugins/www/CMakeLists.txt @@ -0,0 +1,72 @@ +# remmina-plugin-www - The GTK+ Remote Desktop Client +# +# Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo +# +# 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_WWW_SRCS + www_utils.c + www_utils.h + www_config.h + www_plugin.c + www_plugin.h +) + +add_library(remmina-plugin-www MODULE ${REMMINA_PLUGIN_WWW_SRCS}) +set_target_properties(remmina-plugin-www PROPERTIES PREFIX "") +set_target_properties(remmina-plugin-www PROPERTIES NO_SONAME 1) + +add_definitions(${WEBKIT2GTK_CFLAGS_OTHER}) + +find_required_package(LIBSOUP) +if(LIBSOUP_FOUND) + include_directories(${REMMINA_COMMON_INCLUDE_DIRS} ${WEBKIT2GTK_INCLUDE_DIRS} ${LIBSOUP_INCLUDE_DIRS}) + target_link_libraries(remmina-plugin-www ${REMMINA_COMMON_LIBRARIES} ${LIBSOUP_LIBRARIES} ${WEBKIT2GTK_LIBRARIES}) +else() + message(FATAL_ERROR "libsoup library not found") +endif() + +install(TARGETS remmina-plugin-www DESTINATION ${REMMINA_PLUGINDIR}) + +install(FILES + scalable/emblems/org.remmina.Remmina-www-symbolic.svg + DESTINATION ${APPICONSCALE_EMBLEMS_DIR}) + +if(WITH_ICON_CACHE) + gtk_update_icon_cache("${REMMINA_DATADIR}/icons/hicolor") +endif() + +# In the feature we will have more resources to add, so I use a GLOB +file(GLOB WWW_RES_FILES "${CMAKE_CURRENT_SOURCE_DIR}/resources/js/www-js.js") +foreach(ITEM ${WWW_RES_FILES}) + list( APPEND FILES_TO_DEPLOY "${ITEM}" ) +endforeach() +install( FILES ${FILES_TO_DEPLOY} DESTINATION "${REMMINA_RESDIR}" ) + diff --git a/plugins/www/resources/js/.eslintrc.json b/plugins/www/resources/js/.eslintrc.json new file mode 100644 index 0000000..8f6a415 --- /dev/null +++ b/plugins/www/resources/js/.eslintrc.json @@ -0,0 +1,13 @@ +{ + "env": { + "browser": true, + "es2021": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "rules": { + } +} diff --git a/plugins/www/resources/js/package.json b/plugins/www/resources/js/package.json new file mode 100644 index 0000000..70b8fe8 --- /dev/null +++ b/plugins/www/resources/js/package.json @@ -0,0 +1,11 @@ +{ + "name": "js", + "version": "1.0.0", + "description": "", + "main": "www-js.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Antenore Gatta", + "license": "GPL-2.0" +} diff --git a/plugins/www/resources/js/www-js.js b/plugins/www/resources/js/www-js.js new file mode 100644 index 0000000..06980c9 --- /dev/null +++ b/plugins/www/resources/js/www-js.js @@ -0,0 +1,98 @@ +/* jshint esversion: 6 */ + +/** A quick function to find and fill login fields */ +function setLoginFields () { + const evt = new Event('change') + + const frames = window.frames + + let doc + let pswdField + let usrField + let formNode + let userFound = false + + if (frames.length !== 0) { + for (let i = 0; i < frames.length; i++) { + doc = frames[i].document + pswdField = doc.querySelectorAll('input[type=\'password\']') + if ((pswdField !== undefined) && (pswdField !== null)) { + break + } + } + if ((pswdField === undefined) || (pswdField === null)) { + /* What if we don't have login forms in the iFrame? -> window */ + doc = window.document + pswdField = doc.querySelectorAll('input[type=\'password\']') + } + } else { + doc = window.document + pswdField = doc.querySelectorAll('input[type=\'password\']') + } + + if (pswdField !== undefined) { + pswdField.forEach(function (pswdElement) { + if (pswdElement.getAttribute('autocomplete') !== 'new-password') { + pswdElement.value = 'PWDPLACEHOLDER' + } + + formNode = pswdElement.form + if (formNode !== null) { + console.debug('Form elements found') + usrField = formNode.querySelectorAll('input[type=\'text\']') + + usrField.forEach(function (usrElement) { + usrElement.value = 'USRPLACEHOLDER' + if (usrElement !== null) { + usrElement.dispatchEvent(evt) + userFound = true + } + }) + pswdElement.dispatchEvent(evt) + } + if (formNode === null || !userFound) { + console.debug('Form elements found') + console.debug('Inputs elements may be in other containers') + const inputs = doc.getElementsByTagName('input') + for (let i = 0; i < inputs.length; i += 1) { + console.debug('input type: ' + inputs[i].type) + switch (inputs[i].type) { + case 'new-password': + continue + case 'password': + continue + case 'hidden': + continue + case 'email': + inputs[i].value = 'USRPLACEHOLDER' + userFound = true + break + case 'text': + if (!userFound) { + inputs[i].value = 'USRPLACEHOLDER' + userFound = true + } + break + default: + console.debug('Tentativily add username if no userFound') + if (!userFound) { + inputs[i].value = 'USRPLACEHOLDER' + userFound = true + } + // code block + } + if (userFound) { + inputs[i].dispatchEvent(evt) + console.debug('Username field found and set(?)') + break + } + } + } + }) + } else { + console.debug('We already have a password') + + } +} + +setLoginFields() diff --git a/plugins/www/scalable/emblems/org.remmina.Remmina-www-symbolic.svg b/plugins/www/scalable/emblems/org.remmina.Remmina-www-symbolic.svg new file mode 100644 index 0000000..23a1e7b --- /dev/null +++ b/plugins/www/scalable/emblems/org.remmina.Remmina-www-symbolic.svg @@ -0,0 +1,117 @@ +<?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.4 5da689c313, 2019-01-14" + sodipodi:docname="remmina-www-symbolic.svg"> + <title + id="title4674">remmina-resail-symbolic</title> + <defs + id="defs2" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:zoom="4.1973765" + inkscape:cx="-123.70227" + inkscape:cy="21.504897" + 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="false" + inkscape:window-width="3840" + inkscape:window-height="2070" + inkscape:window-x="0" + inkscape:window-y="30" + 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="-190.61239" + originy="-78.157086" /> + </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>remmina-resail-symbolic</dc:title> + <cc:license + rdf:resource="https://creativecommons.org/licenses/by-sa/3.0/" /> + <dc:creator> + <cc:Agent> + <dc:title>Antenore Gatta</dc:title> + </cc:Agent> + </dc:creator> + <dc:rights> + <cc:Agent> + <dc:title>Antenore Gatta 2019</dc:title> + </cc:Agent> + </dc:rights> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-190.61238,-192.38458)"> + <g + style="isolation:isolate" + id="Group" + transform="matrix(0.26458333,0,0,0.26458333,153.78079,124.26165)" /> + <g + style="isolation:isolate" + id="g62" + transform="matrix(0.26458333,0,0,0.26458333,153.78079,124.26165)"> + <g + id="g52" /> + <g + id="g54" /> + <g + id="g56" /> + <g + id="g60"> + <g + id="g58" /> + </g> + </g> + <path + style="opacity:1;fill:#000000;fill-opacity:0.87999998;stroke:none;stroke-width:2.91110802;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 50 1.6816406 A 48.655695 48.318765 0 0 0 1.34375 50 A 48.655695 48.318765 0 0 0 50 98.318359 A 48.655695 48.318765 0 0 0 98.65625 50 A 48.655695 48.318765 0 0 0 50 1.6816406 z M 29.576172 15.396484 L 31.576172 67.396484 L 43.203125 16.888672 L 56.087891 16.888672 L 67.576172 67.396484 L 69.576172 15.396484 L 83.330078 18.236328 L 74.576172 85.396484 L 61.576172 85.132812 L 49.361328 33.128906 L 37.576172 85.396484 L 24.576172 85.396484 L 15.322266 18.236328 L 29.576172 15.396484 z " + transform="matrix(0.26458333,0,0,0.26458333,190.61238,192.38458)" + id="path4668" /> + </g> +</svg> diff --git a/plugins/www/www_config.h b/plugins/www/www_config.h new file mode 100644 index 0000000..225b7b2 --- /dev/null +++ b/plugins/www/www_config.h @@ -0,0 +1,46 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * Initially based on the plugin "Remmina Plugin EXEC", created and written by + * Fabio Castelli (Muflone) <muflone@vbsimple.net>. + * + * 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 +#ifndef __PLUGIN_CONFIG_H +#define __PLUGIN_CONFIG_H + +#define PLUGIN_NAME "WWW" +#define PLUGIN_DESCRIPTION N_("Remmina web-browser plugin") +#define PLUGIN_VERSION "0.1.0" +#define PLUGIN_APPICON "org.remmina.Remmina-www-symbolic" +#endif diff --git a/plugins/www/www_plugin.c b/plugins/www/www_plugin.c new file mode 100644 index 0000000..b248e6b --- /dev/null +++ b/plugins/www/www_plugin.c @@ -0,0 +1,987 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * 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 "www_config.h" + +#include "common/remmina_plugin.h" + +#include <gdk/gdkkeysyms.h> +#include <gtk/gtk.h> +#include <glib.h> +#include <glib.h> +#include <glib/gprintf.h> +#include <stdlib.h> + +#include <webkit2/webkit2.h> +#if WEBKIT_CHECK_VERSION(2, 21, 1) +#include <jsc/jsc.h> +#endif +#include "www_utils.h" +#include "www_plugin.h" + +#define UNUSED(x) (void)(x) + +#define GET_PLUGIN_DATA(gp) (RemminaPluginWWWData *)g_object_get_data(G_OBJECT(gp), "plugin-data") + +typedef struct _RemminaPluginWWWData { + WWWWebViewDocumentType document_type; + GtkWidget * box; + WebKitSettings * settings; + WebKitWebContext * context; + WebKitWebsiteDataManager * data_mgr; + WebKitCredential * credentials; + WebKitAuthenticationRequest * request; + WebKitWebView * webview; + WebKitLoadEvent load_event; + + gchar * url; + gboolean authenticated; + gboolean formauthenticated; +} RemminaPluginWWWData; + +RemminaPluginService *remmina_plugin_service = NULL; + +void remmina_plugin_www_download_started(WebKitWebContext *context, + WebKitDownload *download, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + webkit_download_set_allow_overwrite(download, TRUE); + g_signal_connect(G_OBJECT(download), "notify::response", + G_CALLBACK(remmina_plugin_www_response_received), gp); + g_signal_connect(download, "created-destination", + G_CALLBACK(remmina_plugin_www_notify_download), gp); +} + +void remmina_plugin_www_response_received(WebKitDownload *download, GParamSpec *ps, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + REMMINA_PLUGIN_DEBUG("Download response received"); +} + +void remmina_plugin_www_notify_download(WebKitDownload *download, gchar *destination, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + REMMINA_PLUGIN_DEBUG("Download is finished"); + const gchar *dest = webkit_download_get_destination(download); + + www_utils_send_notification("www-plugin-download-completed-id", _("File downloaded"), dest); + //download(gp, webkit_download_get_response(download)); + //webkit_download_cancel(download); +} + +static gboolean remmina_plugin_www_decide_policy_cb( + WebKitWebView * webview, + WebKitPolicyDecision * decision, + WebKitPolicyDecisionType decision_type, + RemminaProtocolWidget * gp) +{ + TRACE_CALL(__func__); + + gboolean res = TRUE; + + switch (decision_type) { + case WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION: + remmina_plugin_www_decide_nav(decision, gp); + break; + case WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION: + remmina_plugin_www_decide_newwin(decision, gp); + break; + case WEBKIT_POLICY_DECISION_TYPE_RESPONSE: + res = remmina_plugin_www_decide_resource(decision, gp); + break; + default: + webkit_policy_decision_ignore(decision); + break; + } + return res; +} +void remmina_plugin_www_decide_nav(WebKitPolicyDecision *decision, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + REMMINA_PLUGIN_DEBUG("Policy decision navigation"); + const gchar *url = NULL; + WebKitNavigationAction *a = + webkit_navigation_policy_decision_get_navigation_action( + WEBKIT_NAVIGATION_POLICY_DECISION(decision)); + + switch (webkit_navigation_action_get_navigation_type(a)) { + case WEBKIT_NAVIGATION_TYPE_LINK_CLICKED: + REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_LINK_CLICKED"); + url = webkit_uri_request_get_uri( + webkit_navigation_action_get_request(a)); + REMMINA_PLUGIN_DEBUG("url is %s ", url); + break; + case WEBKIT_NAVIGATION_TYPE_FORM_SUBMITTED: + REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_FORM_SUBMITTED"); + break; + case WEBKIT_NAVIGATION_TYPE_BACK_FORWARD: + REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_BACK_FORWARD"); + break; + case WEBKIT_NAVIGATION_TYPE_RELOAD: + REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_RELOAD"); + break; + case WEBKIT_NAVIGATION_TYPE_FORM_RESUBMITTED: + REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_FORM_RESUBMITTED"); + break; + case WEBKIT_NAVIGATION_TYPE_OTHER: + REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_OTHER"); + break; + default: + /* Do not navigate to links with a "_blank" target (popup) */ + if (webkit_navigation_policy_decision_get_frame_name( + WEBKIT_NAVIGATION_POLICY_DECISION(decision))) { + webkit_policy_decision_ignore(decision); + } else { + /* Filter out navigation to different domain ? */ + /* get action→urirequest, copy and load in new window+view + * on Ctrl+Click ? */ + webkit_policy_decision_use(decision); + } + break; + } +} + +void remmina_plugin_www_on_create(WebKitWebView *webview, WebKitNavigationAction *a, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + REMMINA_PLUGIN_DEBUG("New web-view"); + + const gchar *url = NULL; + + RemminaPluginWWWData *gpdata; + + gpdata = (RemminaPluginWWWData *)g_object_get_data(G_OBJECT(gp), "plugin-data"); + + switch (webkit_navigation_action_get_navigation_type(a)) { + case WEBKIT_NAVIGATION_TYPE_LINK_CLICKED: + REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_LINK_CLICKED"); + url = webkit_uri_request_get_uri( + webkit_navigation_action_get_request(a)); + REMMINA_PLUGIN_DEBUG("Downloading url %s ", url); + WebKitDownload *d = webkit_web_view_download_uri(gpdata->webview, url); + UNUSED(d); + + break; + case WEBKIT_NAVIGATION_TYPE_FORM_SUBMITTED: + REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_FORM_SUBMITTED"); + break; + case WEBKIT_NAVIGATION_TYPE_BACK_FORWARD: + REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_BACK_FORWARD"); + break; + case WEBKIT_NAVIGATION_TYPE_RELOAD: + REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_RELOAD"); + break; + case WEBKIT_NAVIGATION_TYPE_FORM_RESUBMITTED: + REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_FORM_RESUBMITTED"); + /* Filter domains here */ + /* If the value of “mouse-button” is not 0, then the navigation was triggered by a mouse event. + * test for link clicked but no button ? */ + url = webkit_uri_request_get_uri( + webkit_navigation_action_get_request(a)); + REMMINA_PLUGIN_DEBUG("Trying to open url: %s", url); + webkit_web_view_load_uri(gpdata->webview, url); + break; + case WEBKIT_NAVIGATION_TYPE_OTHER: /* fallthrough */ + REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_OTHER"); + /* Filter domains here */ + /* If the value of “mouse-button” is not 0, then the navigation was triggered by a mouse event. + * test for link clicked but no button ? */ + url = webkit_uri_request_get_uri( + webkit_navigation_action_get_request(a)); + REMMINA_PLUGIN_DEBUG("Trying to open url: %s", url); + webkit_web_view_load_uri(gpdata->webview, url); + break; + default: + break; + } + REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE is %d", webkit_navigation_action_get_navigation_type(a)); +} + +void remmina_plugin_www_decide_newwin(WebKitPolicyDecision *decision, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + REMMINA_PLUGIN_DEBUG("Policy decision new window"); + + const gchar *url = NULL; + + RemminaPluginWWWData *gpdata; + + gpdata = (RemminaPluginWWWData *)g_object_get_data(G_OBJECT(gp), "plugin-data"); + + WebKitNavigationAction *a = + webkit_navigation_policy_decision_get_navigation_action( + WEBKIT_NAVIGATION_POLICY_DECISION(decision)); + + switch (webkit_navigation_action_get_navigation_type(a)) { + case WEBKIT_NAVIGATION_TYPE_LINK_CLICKED: + REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_LINK_CLICKED"); + url = webkit_uri_request_get_uri( + webkit_navigation_action_get_request(a)); + REMMINA_PLUGIN_DEBUG("Downloading url %s ", url); + WebKitDownload *d = webkit_web_view_download_uri(gpdata->webview, url); + UNUSED(d); + + + break; + case WEBKIT_NAVIGATION_TYPE_FORM_SUBMITTED: + REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_FORM_SUBMITTED"); + break; + case WEBKIT_NAVIGATION_TYPE_BACK_FORWARD: + REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_BACK_FORWARD"); + break; + case WEBKIT_NAVIGATION_TYPE_RELOAD: + REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_RELOAD"); + break; + case WEBKIT_NAVIGATION_TYPE_FORM_RESUBMITTED: + REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_FORM_RESUBMITTED"); + /* Filter domains here */ + /* If the value of “mouse-button” is not 0, then the navigation was triggered by a mouse event. + * test for link clicked but no button ? */ + url = webkit_uri_request_get_uri( + webkit_navigation_action_get_request(a)); + REMMINA_PLUGIN_DEBUG("Trying to open url: %s", url); + webkit_web_view_load_uri(gpdata->webview, url); + break; + case WEBKIT_NAVIGATION_TYPE_OTHER: /* fallthrough */ + REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_OTHER"); + /* Filter domains here */ + /* If the value of “mouse-button” is not 0, then the navigation was triggered by a mouse event. + * test for link clicked but no button ? */ + url = webkit_uri_request_get_uri( + webkit_navigation_action_get_request(a)); + REMMINA_PLUGIN_DEBUG("Trying to open url: %s", url); + webkit_web_view_load_uri(gpdata->webview, url); + break; + default: + break; + } + REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE is %d", webkit_navigation_action_get_navigation_type(a)); + + webkit_policy_decision_ignore(decision); +} +gboolean remmina_plugin_www_decide_resource(WebKitPolicyDecision *decision, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + REMMINA_PLUGIN_DEBUG("Policy decision resource"); + WebKitResponsePolicyDecision *response_decision = + WEBKIT_RESPONSE_POLICY_DECISION(decision); + WebKitURIResponse *response = + webkit_response_policy_decision_get_response(response_decision);; + const gchar *request_uri = webkit_uri_response_get_uri(response); + + WebKitURIRequest *request; + WebKitWebResource *main_resource; + WWWWebViewDocumentType type; + const char *mime_type; + + RemminaPluginWWWData *gpdata; + + gpdata = (RemminaPluginWWWData *)g_object_get_data(G_OBJECT(gp), "plugin-data"); + + mime_type = webkit_uri_response_get_mime_type(response); + + REMMINA_PLUGIN_DEBUG("The media type is %s", mime_type); + + /* If WebKit can't handle the media type, start the download + * process */ + if (webkit_response_policy_decision_is_mime_type_supported(response_decision)) + return FALSE; + + /* If it's not the main resource we don't need to set the document type. */ + request = webkit_response_policy_decision_get_request(response_decision); + request_uri = webkit_uri_request_get_uri(request); + main_resource = webkit_web_view_get_main_resource(gpdata->webview); + if (g_strcmp0(webkit_web_resource_get_uri(main_resource), request_uri) != 0) + return FALSE; + + type = WWW_WEB_VIEW_DOCUMENT_OTHER; + if (!strcmp(mime_type, "text/html") || !strcmp(mime_type, "text/plain")) + type = WWW_WEB_VIEW_DOCUMENT_HTML; + else if (!strcmp(mime_type, "application/xhtml+xml")) + type = WWW_WEB_VIEW_DOCUMENT_XML; + else if (!strncmp(mime_type, "image/", 6)) + type = WWW_WEB_VIEW_DOCUMENT_IMAGE; + else if (!strncmp(mime_type, "application/octet-stream", 6)) + type = WWW_WEB_VIEW_DOCUMENT_OCTET_STREAM; + + REMMINA_PLUGIN_DEBUG("Document type is %i", type); + + /* FIXME: Maybe it makes more sense to have an API to query the media + * type when the load of a page starts than doing this here. + */ + if (gpdata->document_type != type) { + gpdata->document_type = type; + + //g_object_notify_by_pspec (G_OBJECT (webview), obj_properties[PROP_DOCUMENT_TYPE]); + } + + webkit_policy_decision_download(decision); + return TRUE; +} + +static void remmina_www_web_view_js_finished(GObject *object, GAsyncResult *result, gpointer user_data) +{ + TRACE_CALL(__func__); + + WebKitJavascriptResult *js_result; + GError *error = NULL; + + js_result = webkit_web_view_run_javascript_finish(WEBKIT_WEB_VIEW(object), result, &error); + if (!js_result) { + REMMINA_PLUGIN_DEBUG("Could not run JavaScript code: %s", error->message); + g_error_free(error); + return; + } + +#if WEBKIT_CHECK_VERSION(2, 21, 1) + gchar *str_value; + JSCValue *value = webkit_javascript_result_get_js_value(js_result); + if (jsc_value_is_string(value) || jsc_value_is_boolean(value)) { + JSCException *exception; + + str_value = jsc_value_to_string(value); + exception = jsc_context_get_exception(jsc_value_get_context(value)); + if (exception) + REMMINA_PLUGIN_DEBUG("Could not run JavaScript code: %s", jsc_exception_get_message(exception)); + else + g_print("Script result: %s\n", str_value); + g_free(str_value); + } else { + str_value = jsc_value_to_string(value); + REMMINA_PLUGIN_DEBUG("Received something other than a string from JavaScript: %s", str_value); + g_free(str_value); + } +#endif + if (js_result) webkit_javascript_result_unref(js_result); +} + +static gboolean remmina_www_query_feature(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature) +{ + TRACE_CALL(__func__); + return TRUE; +} + +static gboolean remmina_plugin_www_load_failed_tls_cb(WebKitWebView *webview, gchar *failing_uri, GTlsCertificate *certificate, GTlsCertificateFlags errors, RemminaProtocolWidget *gp) __attribute__ ((unused)); +static gboolean remmina_plugin_www_load_failed_tls_cb(WebKitWebView *webview, + gchar *failing_uri, GTlsCertificate *certificate, + GTlsCertificateFlags errors, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + /* Avoid failing if certificate is not good. TODO: Add widgets to let the user decide */ + REMMINA_PLUGIN_DEBUG("Ignoring certificate and return TRUE"); + return TRUE; +} + +static void remmina_plugin_www_init(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + + RemminaPluginWWWData *gpdata; + RemminaFile *remminafile; + gchar *datapath; + gchar *cache_dir; + + gpdata = g_new0(RemminaPluginWWWData, 1); + g_object_set_data_full(G_OBJECT(gp), "plugin-data", gpdata, g_free); + + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + + /* RemminaPluginWWWData initialization */ + + gpdata->authenticated = FALSE; + gpdata->formauthenticated = FALSE; + gpdata->document_type = WWW_WEB_VIEW_DOCUMENT_HTML; + + datapath = g_build_path("/", + g_path_get_dirname(remmina_plugin_service->file_get_path(remminafile)), + PLUGIN_NAME, + NULL); + cache_dir = g_build_path("/", datapath, "cache", NULL); + REMMINA_PLUGIN_DEBUG("WWW data path is %s", datapath); + + if (datapath) { + gchar *indexeddb_dir = g_build_filename(datapath, "indexeddb", NULL); + gchar *local_storage_dir = g_build_filename(datapath, "local_storage", NULL); + gchar *applications_dir = g_build_filename(datapath, "applications", NULL); + gchar *websql_dir = g_build_filename(datapath, "websql", NULL); + gpdata->data_mgr = webkit_website_data_manager_new( + "disk-cache-directory", cache_dir, + "indexeddb-directory", indexeddb_dir, + "local-storage-directory", local_storage_dir, + "offline-application-cache-directory", applications_dir, + "websql-directory", websql_dir, + NULL + ); + g_free(indexeddb_dir); + g_free(local_storage_dir); + g_free(applications_dir); + g_free(websql_dir); + g_free(datapath); + } else { + gpdata->data_mgr = webkit_website_data_manager_new_ephemeral(); + } + + + if (remmina_plugin_service->file_get_string(remminafile, "server")) + gpdata->url = g_strdup(remmina_plugin_service->file_get_string(remminafile, "server")); + else + gpdata->url = "https://remmina.org"; + REMMINA_PLUGIN_DEBUG("URL is set to %s", gpdata->url); + + gpdata->settings = webkit_settings_new(); + gpdata->context = webkit_web_context_new_with_website_data_manager(gpdata->data_mgr); + + /* enable-fullscreen, default TRUE, TODO: Try FALSE */ + +#ifdef DEBUG + /* Turn on the developer extras */ + webkit_settings_set_enable_developer_extras(gpdata->settings, TRUE); + webkit_settings_set_enable_write_console_messages_to_stdout(gpdata->settings, TRUE); +#endif + + /* allow-file-access-from-file-urls */ + webkit_settings_set_allow_file_access_from_file_urls(gpdata->settings, TRUE); + /* allow-modal-dialogs */ + webkit_settings_set_allow_modal_dialogs(gpdata->settings, TRUE); + /* enable-caret-browsing */ + webkit_settings_set_enable_caret_browsing(gpdata->settings, TRUE); + /* enable-html5-database */ + webkit_settings_set_enable_html5_database(gpdata->settings, TRUE); + + /* user-agent. */ + if (remmina_plugin_service->file_get_string(remminafile, "user-agent")) { + gchar *useragent = g_strdup(remmina_plugin_service->file_get_string(remminafile, "user-agent")); + webkit_settings_set_user_agent(gpdata->settings, useragent); + REMMINA_PLUGIN_DEBUG("User Agent set to: %s", useragent); + g_free(useragent); + } + /* enable-java */ + if (remmina_plugin_service->file_get_int(remminafile, "enable-java", FALSE)) { + webkit_settings_set_enable_java(gpdata->settings, TRUE); + REMMINA_PLUGIN_DEBUG("Enable Java"); + } + /* enable-smooth-scrolling */ + if (remmina_plugin_service->file_get_int(remminafile, "enable-smooth-scrolling", FALSE)) { + webkit_settings_set_enable_smooth_scrolling(gpdata->settings, TRUE); + REMMINA_PLUGIN_DEBUG("enable-smooth-scrolling enabled"); + } + /* enable-spatial-navigation */ + if (remmina_plugin_service->file_get_int(remminafile, "enable-spatial-navigation", FALSE)) { + webkit_settings_set_enable_spatial_navigation(gpdata->settings, TRUE); + REMMINA_PLUGIN_DEBUG("enable-spatial-navigation enabled"); + } + /* enable-webaudio */ + if (remmina_plugin_service->file_get_int(remminafile, "enable-webaudio", FALSE)) { + webkit_settings_set_enable_webaudio(gpdata->settings, TRUE); + REMMINA_PLUGIN_DEBUG("enable-webaudio enabled"); + } + /* enable-plugins */ +#if WEBKIT_CHECK_VERSION(2, 32, 0) +#else + if (remmina_plugin_service->file_get_int(remminafile, "enable-plugins", FALSE)) { + webkit_settings_set_enable_plugins(gpdata->settings, TRUE); + REMMINA_PLUGIN_DEBUG("Enable plugins"); + } +#endif + /* enable-webgl */ +#if WEBKIT_CHECK_VERSION(2, 32, 0) +#else + if (remmina_plugin_service->file_get_int(remminafile, "enable-webgl", FALSE)) { + webkit_settings_set_enable_webgl(gpdata->settings, TRUE); + webkit_settings_set_enable_accelerated_2d_canvas(gpdata->settings, TRUE); + REMMINA_PLUGIN_DEBUG("enable-webgl enabled"); + } +#endif + + if (remmina_plugin_service->file_get_int(remminafile, "ignore-tls-errors", FALSE)) { +#if WEBKIT_CHECK_VERSION(2, 32, 0) + webkit_website_data_manager_set_tls_errors_policy( + gpdata->data_mgr, WEBKIT_TLS_ERRORS_POLICY_IGNORE); +#else + webkit_web_context_set_tls_errors_policy( + gpdata->context, WEBKIT_TLS_ERRORS_POLICY_IGNORE); +#endif + REMMINA_PLUGIN_DEBUG("Ignore TLS errors"); + } + if (remmina_plugin_service->file_get_string(remminafile, "proxy-url")) { + gchar *proxyurl = g_strdup(remmina_plugin_service->file_get_string(remminafile, "proxy-url")); + WebKitNetworkProxySettings *proxy_settings = webkit_network_proxy_settings_new(proxyurl, NULL); +#if WEBKIT_CHECK_VERSION(2, 32, 0) + webkit_website_data_manager_set_network_proxy_settings( + gpdata->data_mgr, WEBKIT_NETWORK_PROXY_MODE_CUSTOM, proxy_settings); +#else + webkit_web_context_set_network_proxy_settings( + gpdata->context, WEBKIT_NETWORK_PROXY_MODE_CUSTOM, proxy_settings); +#endif + webkit_network_proxy_settings_free(proxy_settings); + g_free(proxyurl); + } + + webkit_web_context_set_automation_allowed(gpdata->context, TRUE); + webkit_settings_set_javascript_can_open_windows_automatically(gpdata->settings, TRUE); + webkit_settings_set_allow_modal_dialogs(gpdata->settings, TRUE); + +#if !WEBKIT_CHECK_VERSION(2, 38, 0) + /** Frames flattening + * Some websites engage in embedding frames-inside-of-frames. WebKit has + * the ability to flatten them so they behave, when scrolling, as one big + * frame. If for some reason it is not enabled, go ahead and turn it on. + */ + if (!webkit_settings_get_enable_frame_flattening(gpdata->settings)) + webkit_settings_set_enable_frame_flattening(gpdata->settings, true); +#endif + + webkit_settings_set_enable_resizable_text_areas(gpdata->settings, true); + + g_signal_connect(G_OBJECT(gpdata->context), "download-started", + G_CALLBACK(remmina_plugin_www_download_started), gp); +} + +static gboolean remmina_plugin_www_on_auth(WebKitWebView *webview, WebKitAuthenticationRequest *request, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + + gchar *s_username, *s_password; + gint ret; + RemminaPluginWWWData *gpdata; + gboolean save; + gboolean disablepasswordstoring; + RemminaFile *remminafile; + + gpdata = (RemminaPluginWWWData *)g_object_get_data(G_OBJECT(gp), "plugin-data"); + + REMMINA_PLUGIN_DEBUG("Authenticate"); + + 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) | REMMINA_MESSAGE_PANEL_FLAG_USERNAME, + _("Enter WWW authentication credentials"), + remmina_plugin_service->file_get_string(remminafile, "username"), + remmina_plugin_service->file_get_string(remminafile, "password"), + NULL, + NULL); + if (ret == GTK_RESPONSE_OK) { + s_username = remmina_plugin_service->protocol_plugin_init_get_username(gp); + 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, "username", s_username); + remmina_plugin_service->file_set_string(remminafile, "password", s_password); + } else { + remmina_plugin_service->file_set_string(remminafile, "username", NULL); + remmina_plugin_service->file_set_string(remminafile, "password", NULL); + } + if (request) { + gpdata->credentials = webkit_credential_new( + g_strdup(s_username), + g_strdup(s_password), + WEBKIT_CREDENTIAL_PERSISTENCE_FOR_SESSION); + webkit_authentication_request_authenticate(request, gpdata->credentials); + webkit_credential_free(gpdata->credentials); + } + if (s_username) g_free(s_username); + if (s_password) g_free(s_password); + gpdata->authenticated = TRUE; + } else { + gpdata->authenticated = FALSE; + } + + return gpdata->authenticated; +} + +static void remmina_plugin_www_form_auth(WebKitWebView *webview, + WebKitLoadEvent load_event, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + gchar *s_js; + GString *jsstr; + RemminaPluginWWWData *gpdata; + RemminaFile *remminafile; + gchar *remmina_dir; + gchar *www_js_file = NULL; + GError *error = NULL; + + gpdata = (RemminaPluginWWWData *)g_object_get_data(G_OBJECT(gp), "plugin-data"); + if (gpdata && !gpdata->formauthenticated) + gpdata->formauthenticated = FALSE; + + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + + REMMINA_PLUGIN_DEBUG("load-changed emitted"); + + + const gchar *const *dirs = g_get_system_data_dirs(); + + unsigned int i = 0; + + for (i = 0; dirs[i] != NULL; ++i) { + remmina_dir = g_build_path("/", dirs[i], "remmina", "res", NULL); + GDir *system_data_dir = g_dir_open(remmina_dir, 0, &error); + // ignoring this error is ok, because the folder may not exists + if (error) { + g_error_free(error); + error = NULL; + } else { + if (system_data_dir) { + g_dir_close(system_data_dir); + g_free(www_js_file); + www_js_file = g_strdup_printf("%s/www-js.js", remmina_dir); + if (g_file_test(www_js_file, G_FILE_TEST_EXISTS)) + break; + } + } + g_free(remmina_dir); + } + + switch (load_event) { + case WEBKIT_LOAD_STARTED: + REMMINA_PLUGIN_DEBUG("Load started"); + break; + case WEBKIT_LOAD_REDIRECTED: + REMMINA_PLUGIN_DEBUG("Load redirected"); + break; + case WEBKIT_LOAD_COMMITTED: + /* The load is being performed. Current URI is + * the final one and it won't change unless a new + * load is requested or a navigation within the + * same page is performed + * uri = webkit_web_view_get_uri (webview); */ + REMMINA_PLUGIN_DEBUG("Load committed"); + break; + case WEBKIT_LOAD_FINISHED: + /* Load finished, we can now set user/password + * in the HTML form */ + REMMINA_PLUGIN_DEBUG("Load finished"); + if (gpdata && gpdata->formauthenticated == TRUE) + break; + + if (remmina_plugin_service->file_get_string(remminafile, "username") || + remmina_plugin_service->file_get_string(remminafile, "password")) { + REMMINA_PLUGIN_DEBUG("Authentication is enabled"); + if (www_js_file) { + error = NULL; + if (g_file_get_contents(www_js_file, &s_js, NULL, &error)) { + jsstr = g_string_new(s_js); + if (remmina_plugin_service->file_get_string(remminafile, "username")) + www_utils_string_replace_all(jsstr, "USRPLACEHOLDER", + remmina_plugin_service->file_get_string(remminafile, "username")); + if (remmina_plugin_service->file_get_string(remminafile, "password")) + www_utils_string_replace_all(jsstr, "PWDPLACEHOLDER", + remmina_plugin_service->file_get_string(remminafile, "password")); + s_js = g_string_free(jsstr, FALSE); + + if (!s_js || s_js[0] == '\0') { + break; + } else { + REMMINA_PLUGIN_DEBUG("Trying to send this JavaScript: %s", s_js); + webkit_web_view_run_javascript( + webview, + s_js, + NULL, + remmina_www_web_view_js_finished, + gp); + g_free(s_js); + } + } else { + if (error) { + REMMINA_PLUGIN_DEBUG("Unable to read file: %s\n", error->message); + g_error_free(error); + } else { + REMMINA_PLUGIN_DEBUG("Unable to read file. No error returned from glib.\n"); + } + } + } + } + break; + } +} + +static gboolean remmina_plugin_www_close_connection(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginWWWData *gpdata; + + gpdata = (RemminaPluginWWWData *)g_object_get_data(G_OBJECT(gp), "plugin-data"); + + webkit_web_view_stop_loading(gpdata->webview); + webkit_web_view_try_close(gpdata->webview); + + if (gpdata->url) g_free(gpdata->url); + gpdata->authenticated = FALSE; + gpdata->formauthenticated = FALSE; + gpdata->webview = NULL; + gpdata->data_mgr = NULL; + gpdata->settings = NULL; + gpdata->context = NULL; + + /* Remove instance->context from gp object data to avoid double free */ + g_object_steal_data(G_OBJECT(gp), "plugin-data"); + + remmina_plugin_service->protocol_plugin_signal_connection_closed(gp); + return FALSE; +} + +static gboolean remmina_plugin_www_open_connection(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + + RemminaPluginWWWData *gpdata; + RemminaFile *remminafile; + + gpdata = (RemminaPluginWWWData *)g_object_get_data(G_OBJECT(gp), "plugin-data"); + + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + + gpdata->box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_container_add(GTK_CONTAINER(gp), gpdata->box); + + remmina_plugin_service->protocol_plugin_register_hostkey(gp, gpdata->box); + + gpdata->webview = WEBKIT_WEB_VIEW(webkit_web_view_new_with_context(gpdata->context)); + webkit_web_view_set_settings(gpdata->webview, gpdata->settings); + + if (remmina_plugin_service->file_get_string(remminafile, "username") || + remmina_plugin_service->file_get_string(remminafile, "password")) { + REMMINA_PLUGIN_DEBUG("Authentication is enabled"); + remmina_plugin_www_on_auth(gpdata->webview, NULL, gp); + } + + //"signal::load-failed-with-tls-errors", G_CALLBACK(remmina_plugin_www_load_failed_tls_cb), gp, + g_object_connect( + G_OBJECT(gpdata->webview), + "signal::create", G_CALLBACK(remmina_plugin_www_on_create), gp, + "signal::load-changed", G_CALLBACK(remmina_plugin_www_form_auth), gp, + "signal::authenticate", G_CALLBACK(remmina_plugin_www_on_auth), gp, + "signal::decide-policy", G_CALLBACK(remmina_plugin_www_decide_policy_cb), gp, + NULL); + + gtk_widget_set_hexpand(GTK_WIDGET(gpdata->webview), TRUE); + gtk_widget_set_vexpand(GTK_WIDGET(gpdata->webview), TRUE); + gtk_container_add(GTK_CONTAINER(gpdata->box), GTK_WIDGET(gpdata->webview)); + webkit_web_view_load_uri(gpdata->webview, gpdata->url); +#ifdef DEBUG + if (remmina_plugin_service->file_get_int(remminafile, "enable-webinspector", FALSE)) { + REMMINA_PLUGIN_DEBUG("WebInspector enabled"); + WebKitWebInspector *inspector = webkit_web_view_get_inspector(WEBKIT_WEB_VIEW(gpdata->webview)); + webkit_web_inspector_attach(inspector); + webkit_web_inspector_show(WEBKIT_WEB_INSPECTOR(inspector)); + } +#endif + remmina_plugin_service->protocol_plugin_signal_connection_opened(gp); + gtk_widget_show_all(gpdata->box); + + return TRUE; +} + +static void remmina_plugin_www_save_snapshot(GObject *object, GAsyncResult *result, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + + WebKitWebView *webview = WEBKIT_WEB_VIEW(object); + + RemminaFile *remminafile; + + GError *err = NULL; + cairo_surface_t *surface; + //unsigned char* buffer; + int width; + int height; + GdkPixbuf *screenshot; + GString *pngstr; + gchar *pngname; + //cairo_forma_t* cairo_format; + GDateTime *date = g_date_time_new_now_utc(); + + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + + surface = webkit_web_view_get_snapshot_finish(WEBKIT_WEB_VIEW(webview), result, &err); + if (err) + REMMINA_PLUGIN_DEBUG("An error happened generating the snapshot: %s\n", err->message); + //buffer = cairo_image_surface_get_data (surface); + width = cairo_image_surface_get_width(surface); + height = cairo_image_surface_get_height(surface); + //cairo_format = cairo_image_surface_get_format (surface); + + screenshot = gdk_pixbuf_get_from_surface(surface, 0, 0, width, height); + if (screenshot == NULL) + REMMINA_PLUGIN_DEBUG("WWW: gdk_pixbuf_get_from_surface failed"); + + pngstr = g_string_new(g_strdup_printf("%s/%s.png", + remmina_plugin_service->pref_get_value("screenshot_path"), + remmina_plugin_service->pref_get_value("screenshot_name"))); + www_utils_string_replace_all(pngstr, "%p", + remmina_plugin_service->file_get_string(remminafile, "name")); + www_utils_string_replace_all(pngstr, "%h", "URL"); + www_utils_string_replace_all(pngstr, "%Y", + g_strdup_printf("%d", g_date_time_get_year(date))); + www_utils_string_replace_all(pngstr, "%m", g_strdup_printf("%d", + g_date_time_get_month(date))); + www_utils_string_replace_all(pngstr, "%d", + g_strdup_printf("%d", g_date_time_get_day_of_month(date))); + www_utils_string_replace_all(pngstr, "%H", + g_strdup_printf("%d", g_date_time_get_hour(date))); + www_utils_string_replace_all(pngstr, "%M", + g_strdup_printf("%d", g_date_time_get_minute(date))); + www_utils_string_replace_all(pngstr, "%S", + g_strdup_printf("%f", g_date_time_get_seconds(date))); + g_date_time_unref(date); + pngname = g_string_free(pngstr, FALSE); + REMMINA_PLUGIN_DEBUG("Saving screenshot as %s", pngname); + + cairo_surface_write_to_png(surface, pngname); + if (g_file_test(pngname, G_FILE_TEST_EXISTS)) + www_utils_send_notification("www-plugin-screenshot-is-ready-id", _("Screenshot taken"), pngname); + + cairo_surface_destroy(surface); +} +static gboolean remmina_plugin_www_get_snapshot(RemminaProtocolWidget *gp, RemminaPluginScreenshotData *rpsd) +{ + TRACE_CALL(__func__); + RemminaPluginWWWData *gpdata; + + gpdata = (RemminaPluginWWWData *)g_object_get_data(G_OBJECT(gp), "plugin-data"); + + webkit_web_view_get_snapshot(gpdata->webview, + WEBKIT_SNAPSHOT_REGION_FULL_DOCUMENT, + WEBKIT_SNAPSHOT_OPTIONS_NONE, + NULL, + (GAsyncReadyCallback)remmina_plugin_www_save_snapshot, + gp); + return FALSE; +} + +/* 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_www_basic_settings[] = +{ + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "server", N_("URL"), FALSE, NULL, N_("http://address or https://address"), NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "username", N_("Username"), FALSE, NULL, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD, "password", N_("Password"), 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_www_advanced_settings[] = +{ + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "user-agent", N_("User agent"), FALSE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "proxy-url", N_("Proxy URL"), FALSE, NULL, N_("E.g. https://example.org, socks://mysocks:1080") }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "enable-java", N_("Turn on Java support"), TRUE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "enable-smooth-scrolling", N_("Turn on smooth scrolling"), TRUE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "enable-spatial-navigation", N_("Turn on spatial navigation"), TRUE, NULL, NULL }, +#if WEBKIT_CHECK_VERSION(2, 32, 0) +#else + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "enable-plugins", N_("Turn on plugin support"), TRUE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "enable-webgl", N_("Turn on WebGL support"), TRUE, NULL, NULL }, +#endif + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "enable-webaudio", N_("Turn on HTML5 audio support"), TRUE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "ignore-tls-errors", N_("Ignore TLS errors"), TRUE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disablepasswordstoring", N_("Forget passwords after use"), TRUE, NULL, NULL }, +#ifdef DEBUG + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "enable-webinspector", N_("Turn on Web Inspector"), TRUE, NULL, NULL }, +#endif + { REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, FALSE, NULL, NULL } +}; + +/* Array for available features. + * The last element of the array must be REMMINA_PROTOCOL_FEATURE_TYPE_END. */ +static const RemminaProtocolFeature remmina_www_features[] = +{ + { REMMINA_PROTOCOL_FEATURE_TYPE_END, 0, NULL, NULL, NULL } +}; + +/* Protocol plugin definition and features */ +static RemminaProtocolPlugin remmina_plugin = +{ + REMMINA_PLUGIN_TYPE_PROTOCOL, // Type + PLUGIN_NAME, // Name + PLUGIN_DESCRIPTION, // Description + GETTEXT_PACKAGE, // Translation domain + PLUGIN_VERSION, // Version number + PLUGIN_APPICON, // Icon for normal connection + NULL, // Icon for SSH connection + remmina_plugin_www_basic_settings, // Array for basic settings + remmina_plugin_www_advanced_settings, // Array for advanced settings + REMMINA_PROTOCOL_SSH_SETTING_NONE, // SSH settings type + remmina_www_features, // Array for available features + remmina_plugin_www_init, // Plugin initialization + remmina_plugin_www_open_connection, // Plugin open connection + remmina_plugin_www_close_connection, // Plugin close connection + remmina_www_query_feature, // Query for available features + NULL, // Call feature + NULL, // Send keystroke + remmina_plugin_www_get_snapshot, // Capture screenshot + NULL, // RCW map event + NULL // RCW unmap event +}; + +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"); + + + if (!service->register_plugin((RemminaPlugin *)&remmina_plugin)) + return FALSE; + return TRUE; +} diff --git a/plugins/www/www_plugin.h b/plugins/www/www_plugin.h new file mode 100644 index 0000000..73caeba --- /dev/null +++ b/plugins/www/www_plugin.h @@ -0,0 +1,80 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * 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 <glib.h> +#include <webkit2/webkit2.h> + +typedef enum { + WWW_WEB_VIEW_DOCUMENT_HTML, + WWW_WEB_VIEW_DOCUMENT_XML, + WWW_WEB_VIEW_DOCUMENT_IMAGE, + WWW_WEB_VIEW_DOCUMENT_OCTET_STREAM, + WWW_WEB_VIEW_DOCUMENT_OTHER +} WWWWebViewDocumentType; + +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__) + +G_BEGIN_DECLS +void remmina_plugin_www_decide_nav(WebKitPolicyDecision *decision, RemminaProtocolWidget *gp); +void remmina_plugin_www_decide_newwin(WebKitPolicyDecision *decision, RemminaProtocolWidget *gp); +gboolean remmina_plugin_www_decide_resource(WebKitPolicyDecision *decision, RemminaProtocolWidget *gp); +void remmina_plugin_www_response_received(WebKitDownload *download, GParamSpec *ps, RemminaProtocolWidget *gp); +void remmina_plugin_www_notify_download(WebKitDownload *download, gchar *destination, RemminaProtocolWidget *gp); + +G_END_DECLS diff --git a/plugins/www/www_utils.c b/plugins/www/www_utils.c new file mode 100644 index 0000000..fe5fa0a --- /dev/null +++ b/plugins/www/www_utils.c @@ -0,0 +1,154 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * 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. + * + */ + +/* Some utils taken form remmina_utils +* TODO: use directly remmina_utils */ + +#include <stdlib.h> +#include <unistd.h> +#include <sys/utsname.h> + +#include <glib.h> +#include <glib/gi18n.h> +#include <glib/gstdio.h> +#include <gio/gio.h> +#include "remmina/remmina_trace_calls.h" + +/** Returns @c TRUE if @a ptr is @c NULL or @c *ptr is @c FALSE. */ +#define EMPTY(ptr) \ + (!(ptr) || !*(ptr)) + +/* Used to send desktop notifications */ +void www_utils_send_notification(const gchar *notification_id, + const gchar *notification_title, const gchar *notification_message) +{ + TRACE_CALL(__func__); + + GNotification *notification = g_notification_new(notification_title); + g_notification_set_body(notification, notification_message); +#if GLIB_CHECK_VERSION(2, 42, 0) + g_notification_set_priority(notification, G_NOTIFICATION_PRIORITY_NORMAL); +#endif + g_application_send_notification(g_application_get_default(), notification_id, notification); + g_object_unref(notification); +} + +gint www_utils_strpos(const gchar *haystack, const gchar *needle) +{ + TRACE_CALL(__func__); + const gchar *sub; + + if (!*needle) + return -1; + + sub = strstr(haystack, needle); + if (!sub) + return -1; + + return sub - haystack; +} + +/* end can be -1 for haystack->len. + * returns: position of found text or -1. + * (C) Taken from geany */ +gint www_utils_string_find(GString *haystack, gint start, gint end, const gchar *needle) +{ + TRACE_CALL(__func__); + gint pos; + + g_return_val_if_fail(haystack != NULL, -1); + if (haystack->len == 0) + return -1; + + g_return_val_if_fail(start >= 0, -1); + if (start >= (gint)haystack->len) + return -1; + + g_return_val_if_fail(!EMPTY(needle), -1); + + if (end < 0) + end = haystack->len; + + pos = www_utils_strpos(haystack->str + start, needle); + if (pos == -1) + return -1; + + pos += start; + if (pos >= end) + return -1; + return pos; +} + +/* Replaces @len characters from offset @a pos. + * len can be -1 to replace the remainder of @a str. + * returns: pos + strlen(replace). + * (C) Taken from geany */ +gint www_utils_string_replace(GString *str, gint pos, gint len, const gchar *replace) +{ + TRACE_CALL(__func__); + g_string_erase(str, pos, len); + if (replace) { + g_string_insert(str, pos, replace); + pos += strlen(replace); + } + return pos; +} + +/** + * Replaces all occurrences of @a needle in @a haystack with @a replace. + * + * @param haystack The input string to operate on. This string is modified in place. + * @param needle The string which should be replaced. + * @param replace The replacement for @a needle. + * + * @return Number of replacements made. + **/ +guint www_utils_string_replace_all(GString *haystack, const gchar *needle, const gchar *replace) +{ + TRACE_CALL(__func__); + guint count = 0; + gint pos = 0; + gsize needle_length = strlen(needle); + + while (1) { + pos = www_utils_string_find(haystack, pos, -1, needle); + + if (pos == -1) + break; + + pos = www_utils_string_replace(haystack, pos, needle_length, replace); + count++; + } + return count; +} diff --git a/plugins/www/www_utils.h b/plugins/www/www_utils.h new file mode 100644 index 0000000..b5fab5e --- /dev/null +++ b/plugins/www/www_utils.h @@ -0,0 +1,47 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo + * + * 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. + * + */ + +/* Some utils taken form remmina_utils +* TODO: use directly remmina_utils */ + +#pragma once + +#include "common/remmina_plugin.h" + +G_BEGIN_DECLS +gint www_utils_string_find(GString *haystack, gint start, gint end, const gchar *needle); +gint www_utils_string_replace(GString *str, gint pos, gint len, const gchar *replace); +guint www_utils_string_replace_all(GString *haystack, const gchar *needle, const gchar *replace); +void www_utils_send_notification(const gchar *notification_id, const gchar *notification_title, const gchar *notification_message); +G_END_DECLS diff --git a/plugins/x2go/CMakeLists.txt b/plugins/x2go/CMakeLists.txt new file mode 100644 index 0000000..97df070 --- /dev/null +++ b/plugins/x2go/CMakeLists.txt @@ -0,0 +1,66 @@ +# +# Project: Remmina Plugin X2Go +# Description: Remmina protocol plugin to connect via X2Go using PyHoca +# Based on Fabio Castelli Team Viewer Plugin +# Copyright: 2013-2014 Fabio Castelli (Muflone) +# Author: Antenore Gatta <antenore@simbiosi.org> +# Copyright: 2015 Antenore Gatta +# License: GPL-2+ +# +# 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_X2GO_SRCS + x2go_plugin.c + x2go_plugin.h + ) + +add_library(remmina-plugin-x2go MODULE ${REMMINA_PLUGIN_X2GO_SRCS}) +set_target_properties(remmina-plugin-x2go PROPERTIES PREFIX "") +set_target_properties(remmina-plugin-x2go PROPERTIES NO_SONAME 1) + +find_package(X11) + +include_directories(${REMMINA_COMMON_INCLUDE_DIRS} + ${XKBFILE_INCLUDE_DIRS} ${LIBSSH_INCLUDE_DIRS}) +target_link_libraries(remmina-plugin-x2go + ${REMMINA_COMMON_LIBRARIES} + ${XKBFILE_LIBRARIES} + ${LIBSSH_LIBRARIES} + ${X11_X11_LIB}) + +install(TARGETS remmina-plugin-x2go DESTINATION ${REMMINA_PLUGINDIR}) + +install(FILES + scalable/emblems/org.remmina.Remmina-x2go-ssh-symbolic.svg + scalable/emblems/org.remmina.Remmina-x2go-symbolic.svg + DESTINATION ${APPICONSCALE_EMBLEMS_DIR}) + +if(WITH_ICON_CACHE) + gtk_update_icon_cache("${REMMINA_DATADIR}/icons/hicolor") +endif() diff --git a/plugins/x2go/scalable/emblems/org.remmina.Remmina-x2go-ssh-symbolic.svg b/plugins/x2go/scalable/emblems/org.remmina.Remmina-x2go-ssh-symbolic.svg new file mode 100644 index 0000000..2cf9b92 --- /dev/null +++ b/plugins/x2go/scalable/emblems/org.remmina.Remmina-x2go-ssh-symbolic.svg @@ -0,0 +1,297 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<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" + id="svg2" + sodipodi:version="0.32" + inkscape:version="0.46" + width="128" + height="128" + sodipodi:docname="x2go-logo.svg" + sodipodi:docbase="/Users/h1/Desktop" + inkscape:output_extension="org.inkscape.output.svg.inkscape" + version="1.0" + inkscape:export-filename="/Users/h1/Desktop/x2go-logo.png" + inkscape:export-xdpi="900" + inkscape:export-ydpi="900"> + <metadata + id="metadata87"> + <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>x2go Logo</dc:title> + <dc:date>12.06.2007</dc:date> + <dc:creator> + <cc:Agent> + <dc:title>Heinz-M. Graesing</dc:title> + </cc:Agent> + </dc:creator> + <dc:rights> + <cc:Agent> + <dc:title>obviously-nice</dc:title> + </cc:Agent> + </dc:rights> + <dc:publisher> + <cc:Agent> + <dc:title>obviously-nice</dc:title> + </cc:Agent> + </dc:publisher> + <dc:source>http://www.x2go.org/artwork</dc:source> + <dc:language>DE</dc:language> + <dc:subject> + <rdf:Bag> + <rdf:li>Logo</rdf:li> + </rdf:Bag> + </dc:subject> + <cc:license + rdf:resource="http://creativecommons.org/licenses/by-nd/3.0/" /> + </cc:Work> + <cc:License + rdf:about="http://creativecommons.org/licenses/by-nd/3.0/"> + <cc:permits + rdf:resource="http://creativecommons.org/ns#Reproduction" /> + <cc:permits + rdf:resource="http://creativecommons.org/ns#Distribution" /> + <cc:requires + rdf:resource="http://creativecommons.org/ns#Notice" /> + <cc:requires + rdf:resource="http://creativecommons.org/ns#Attribution" /> + </cc:License> + </rdf:RDF> + </metadata> + <defs + id="defs85"> + <inkscape:perspective + sodipodi:type="inkscape:persp3d" + inkscape:vp_x="0 : 64 : 1" + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="128 : 64 : 1" + inkscape:persp3d-origin="64 : 42.666667 : 1" + id="perspective46" /> + </defs> + <sodipodi:namedview + inkscape:window-height="834" + inkscape:window-width="1295" + inkscape:pageshadow="2" + inkscape:pageopacity="0.0" + guidetolerance="10.0" + gridtolerance="10.0" + objecttolerance="10.0" + borderopacity="1.0" + bordercolor="#666666" + pagecolor="#ffffff" + id="base" + showgrid="true" + inkscape:object-nodes="true" + inkscape:grid-points="true" + inkscape:guide-points="true" + width="128px" + height="128px" + inkscape:zoom="3.9375" + inkscape:cx="64" + inkscape:cy="64" + inkscape:window-x="395" + inkscape:window-y="117" + inkscape:current-layer="svg2"> + <inkscape:grid + id="GridFromPre046Settings" + type="xygrid" + originx="0px" + originy="0px" + spacingx="1px" + spacingy="1px" + color="#0000ff" + empcolor="#0000ff" + opacity="0.2" + empopacity="0.4" + empspacing="5" /> + </sodipodi:namedview> + <g + style="fill:#4d4d4d" + id="g4" + transform="translate(0,-924.362)"> + <path + d="M 90,977.626 L 103.32,951.85 C 104.256,949.33 104.688,947.53 104.688,946.594 C 104.688,945.082 104.184,943.93 103.248,943.282 C 102.24,942.562 100.512,942.202 98.064,942.202 L 98.064,939.826 L 120.168,939.826 L 120.168,942.202 C 117.144,942.202 114.984,942.85 113.472,944.146 C 111.96,945.442 109.872,948.898 106.992,954.514 L 92.376,982.954 L 110.808,1024.498 C 112.68,1028.386 114.336,1031.122 115.776,1032.634 C 117.216,1034.074 118.944,1034.866 120.888,1034.866 L 120.888,1037.17 L 94.536,1037.17 L 94.536,1034.866 C 96.912,1034.866 98.568,1034.578 99.576,1033.93 C 100.584,1033.354 101.088,1032.346 101.088,1031.05 C 101.088,1029.826 100.368,1027.666 99.072,1024.498 L 86.112,995.482 L 71.28,1024.498 C 69.912,1026.946 69.192,1028.962 69.192,1030.474 C 69.192,1033.426 71.352,1034.866 75.672,1034.866 L 75.672,1037.17 L 55.728,1037.17 L 55.728,1034.866 C 57.672,1034.866 59.4,1034.218 60.84,1032.994 C 62.28,1031.77 63.792,1029.682 65.232,1026.586 L 83.736,990.01 L 67.248,952.498 C 65.52,948.466 63.936,945.802 62.424,944.362 C 60.84,942.922 58.608,942.202 55.728,942.202 L 55.728,939.826 L 84.024,939.826 L 84.024,942.202 C 79.632,942.202 77.4,943.714 77.4,946.738 C 77.4,948.25 77.832,949.978 78.696,951.85 L 90,977.626 z " + id="path6" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g8" + transform="translate(0,-924.362)"> + <path + d="M 6.984,940.474 L 50.976,939.25 L 50.976,941.698 L 6.984,940.474 z " + id="path10" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g12" + transform="translate(0,-924.362)"> + <path + d="M 6.984,946.45 L 50.976,945.298 L 50.976,946.954 L 56.16,947.026 L 56.52,947.026 L 56.808,947.026 L 57.096,947.098 L 57.384,947.098 L 57.672,947.17 L 57.888,947.242 L 58.104,947.314 L 58.32,947.386 L 58.464,947.458 L 58.608,947.53 L 58.752,947.602 L 58.896,947.674 L 58.968,947.746 L 59.112,947.818 L 59.112,947.89 L 59.184,947.89 L 6.984,946.45 z " + id="path14" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g16" + transform="translate(0,-924.362)"> + <path + d="M 6.984,952.498 L 61.272,950.986 L 61.488,951.346 L 61.776,951.922 L 62.064,952.498 L 62.352,953.146 L 62.64,953.794 L 62.784,954.01 L 6.984,952.498 z " + id="path18" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g20" + transform="translate(0,-924.362)"> + <path + d="M 6.984,958.474 L 63.936,956.89 L 65.376,960.058 L 6.984,958.474 z " + id="path22" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g24" + transform="translate(0,-924.362)"> + <path + d="M 6.984,964.45 L 66.528,962.866 L 67.968,966.106 L 6.984,964.45 z " + id="path26" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g28" + transform="translate(0,-924.362)"> + <path + d="M 6.984,970.498 L 69.12,968.77 L 70.632,972.226 L 6.984,970.498 z " + id="path30" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g32" + transform="translate(0,-924.362)"> + <path + d="M 6.984,976.474 L 71.712,974.674 L 73.296,978.274 L 6.984,976.474 z " + id="path34" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g36" + transform="translate(0,-924.362)"> + <path + d="M 6.984,982.45 L 74.304,980.65 L 75.96,984.322 L 6.984,982.45 z " + id="path38" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g40" + transform="translate(0,-924.362)"> + <path + d="M 6.984,988.426 L 76.896,986.554 L 78.408,989.866 L 78.192,990.37 L 6.984,988.426 z " + id="path42" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g44" + transform="translate(0,-924.362)"> + <path + d="M 6.984,994.402 L 77.112,992.458 L 75.168,996.274 L 6.984,994.402 z " + id="path46" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g48" + transform="translate(0,-924.362)"> + <path + d="M 6.984,1000.378 L 74.016,998.578 L 72.216,1002.178 L 6.984,1000.378 z " + id="path50" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g52" + transform="translate(0,-924.362)"> + <path + d="M 6.984,1006.354 L 70.92,1004.626 L 69.192,1008.082 L 6.984,1006.354 z " + id="path54" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g56" + transform="translate(0,-924.362)"> + <path + d="M 6.984,1012.402 L 67.896,1010.746 L 66.168,1013.986 L 6.984,1012.402 z " + id="path58" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g60" + transform="translate(0,-924.362)"> + <path + d="M 6.984,1036.378 L 50.976,1035.154 L 50.976,1037.602 L 6.984,1036.378 z " + id="path62" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g64" + transform="translate(0,-924.362)"> + <path + d="M 6.984,1030.402 L 58.104,1028.962 L 58.032,1029.106 L 57.888,1029.178 L 57.744,1029.322 L 57.6,1029.394 L 57.528,1029.538 L 57.384,1029.61 L 57.312,1029.682 L 57.168,1029.682 L 57.024,1029.754 L 56.952,1029.826 L 56.808,1029.898 L 56.736,1029.898 L 56.592,1029.97 L 56.448,1029.97 L 56.376,1029.97 L 56.232,1030.042 L 56.088,1030.042 L 55.944,1030.042 L 50.976,1030.114 L 50.976,1031.554 L 6.984,1030.402 z " + id="path66" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g68" + transform="translate(0,-924.362)"> + <path + d="M 6.984,1024.354 L 61.776,1022.914 L 60.984,1024.498 L 60.696,1025.002 L 60.48,1025.506 L 60.264,1025.866 L 6.984,1024.354 z " + id="path70" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g72" + transform="translate(0,-924.362)"> + <path + d="M 6.984,1018.378 L 64.8,1016.794 L 63.288,1019.89 L 6.984,1018.378 z " + id="path74" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g76" + transform="translate(0,-924.362)"> + <path + d="M 63.936,924.49 L 118.872,924.49 C 123.84,924.49 127.872,928.522 127.872,933.49 L 127.872,1043.362 C 127.872,1048.33 123.84,1052.362 118.872,1052.362 L 63.936,1052.362 L 63.936,1050.418 L 117.864,1050.418 C 122.256,1050.418 125.856,1046.818 125.856,1042.426 L 125.856,934.498 C 125.856,930.106 122.256,926.506 117.864,926.506 L 63.936,926.506 L 63.936,924.49 z " + id="path78" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g80" + transform="translate(0,-924.362)"> + <path + d="M 9,924.49 L 63.936,924.49 L 63.936,926.506 L 9.936,926.506 C 5.544,926.506 1.944,930.106 1.944,934.498 L 1.944,1042.426 C 1.944,1046.818 5.544,1050.418 9.936,1050.418 L 63.936,1050.418 L 63.936,1052.362 L 9,1052.362 C 4.032,1052.362 0,1048.33 0,1043.362 L 0,933.49 C 0,928.522 4.032,924.49 9,924.49 z " + id="path82" + style="fill:#4d4d4d" /> + </g> +</svg> diff --git a/plugins/x2go/scalable/emblems/org.remmina.Remmina-x2go-symbolic.svg b/plugins/x2go/scalable/emblems/org.remmina.Remmina-x2go-symbolic.svg new file mode 100644 index 0000000..2cf9b92 --- /dev/null +++ b/plugins/x2go/scalable/emblems/org.remmina.Remmina-x2go-symbolic.svg @@ -0,0 +1,297 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<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" + id="svg2" + sodipodi:version="0.32" + inkscape:version="0.46" + width="128" + height="128" + sodipodi:docname="x2go-logo.svg" + sodipodi:docbase="/Users/h1/Desktop" + inkscape:output_extension="org.inkscape.output.svg.inkscape" + version="1.0" + inkscape:export-filename="/Users/h1/Desktop/x2go-logo.png" + inkscape:export-xdpi="900" + inkscape:export-ydpi="900"> + <metadata + id="metadata87"> + <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>x2go Logo</dc:title> + <dc:date>12.06.2007</dc:date> + <dc:creator> + <cc:Agent> + <dc:title>Heinz-M. Graesing</dc:title> + </cc:Agent> + </dc:creator> + <dc:rights> + <cc:Agent> + <dc:title>obviously-nice</dc:title> + </cc:Agent> + </dc:rights> + <dc:publisher> + <cc:Agent> + <dc:title>obviously-nice</dc:title> + </cc:Agent> + </dc:publisher> + <dc:source>http://www.x2go.org/artwork</dc:source> + <dc:language>DE</dc:language> + <dc:subject> + <rdf:Bag> + <rdf:li>Logo</rdf:li> + </rdf:Bag> + </dc:subject> + <cc:license + rdf:resource="http://creativecommons.org/licenses/by-nd/3.0/" /> + </cc:Work> + <cc:License + rdf:about="http://creativecommons.org/licenses/by-nd/3.0/"> + <cc:permits + rdf:resource="http://creativecommons.org/ns#Reproduction" /> + <cc:permits + rdf:resource="http://creativecommons.org/ns#Distribution" /> + <cc:requires + rdf:resource="http://creativecommons.org/ns#Notice" /> + <cc:requires + rdf:resource="http://creativecommons.org/ns#Attribution" /> + </cc:License> + </rdf:RDF> + </metadata> + <defs + id="defs85"> + <inkscape:perspective + sodipodi:type="inkscape:persp3d" + inkscape:vp_x="0 : 64 : 1" + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="128 : 64 : 1" + inkscape:persp3d-origin="64 : 42.666667 : 1" + id="perspective46" /> + </defs> + <sodipodi:namedview + inkscape:window-height="834" + inkscape:window-width="1295" + inkscape:pageshadow="2" + inkscape:pageopacity="0.0" + guidetolerance="10.0" + gridtolerance="10.0" + objecttolerance="10.0" + borderopacity="1.0" + bordercolor="#666666" + pagecolor="#ffffff" + id="base" + showgrid="true" + inkscape:object-nodes="true" + inkscape:grid-points="true" + inkscape:guide-points="true" + width="128px" + height="128px" + inkscape:zoom="3.9375" + inkscape:cx="64" + inkscape:cy="64" + inkscape:window-x="395" + inkscape:window-y="117" + inkscape:current-layer="svg2"> + <inkscape:grid + id="GridFromPre046Settings" + type="xygrid" + originx="0px" + originy="0px" + spacingx="1px" + spacingy="1px" + color="#0000ff" + empcolor="#0000ff" + opacity="0.2" + empopacity="0.4" + empspacing="5" /> + </sodipodi:namedview> + <g + style="fill:#4d4d4d" + id="g4" + transform="translate(0,-924.362)"> + <path + d="M 90,977.626 L 103.32,951.85 C 104.256,949.33 104.688,947.53 104.688,946.594 C 104.688,945.082 104.184,943.93 103.248,943.282 C 102.24,942.562 100.512,942.202 98.064,942.202 L 98.064,939.826 L 120.168,939.826 L 120.168,942.202 C 117.144,942.202 114.984,942.85 113.472,944.146 C 111.96,945.442 109.872,948.898 106.992,954.514 L 92.376,982.954 L 110.808,1024.498 C 112.68,1028.386 114.336,1031.122 115.776,1032.634 C 117.216,1034.074 118.944,1034.866 120.888,1034.866 L 120.888,1037.17 L 94.536,1037.17 L 94.536,1034.866 C 96.912,1034.866 98.568,1034.578 99.576,1033.93 C 100.584,1033.354 101.088,1032.346 101.088,1031.05 C 101.088,1029.826 100.368,1027.666 99.072,1024.498 L 86.112,995.482 L 71.28,1024.498 C 69.912,1026.946 69.192,1028.962 69.192,1030.474 C 69.192,1033.426 71.352,1034.866 75.672,1034.866 L 75.672,1037.17 L 55.728,1037.17 L 55.728,1034.866 C 57.672,1034.866 59.4,1034.218 60.84,1032.994 C 62.28,1031.77 63.792,1029.682 65.232,1026.586 L 83.736,990.01 L 67.248,952.498 C 65.52,948.466 63.936,945.802 62.424,944.362 C 60.84,942.922 58.608,942.202 55.728,942.202 L 55.728,939.826 L 84.024,939.826 L 84.024,942.202 C 79.632,942.202 77.4,943.714 77.4,946.738 C 77.4,948.25 77.832,949.978 78.696,951.85 L 90,977.626 z " + id="path6" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g8" + transform="translate(0,-924.362)"> + <path + d="M 6.984,940.474 L 50.976,939.25 L 50.976,941.698 L 6.984,940.474 z " + id="path10" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g12" + transform="translate(0,-924.362)"> + <path + d="M 6.984,946.45 L 50.976,945.298 L 50.976,946.954 L 56.16,947.026 L 56.52,947.026 L 56.808,947.026 L 57.096,947.098 L 57.384,947.098 L 57.672,947.17 L 57.888,947.242 L 58.104,947.314 L 58.32,947.386 L 58.464,947.458 L 58.608,947.53 L 58.752,947.602 L 58.896,947.674 L 58.968,947.746 L 59.112,947.818 L 59.112,947.89 L 59.184,947.89 L 6.984,946.45 z " + id="path14" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g16" + transform="translate(0,-924.362)"> + <path + d="M 6.984,952.498 L 61.272,950.986 L 61.488,951.346 L 61.776,951.922 L 62.064,952.498 L 62.352,953.146 L 62.64,953.794 L 62.784,954.01 L 6.984,952.498 z " + id="path18" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g20" + transform="translate(0,-924.362)"> + <path + d="M 6.984,958.474 L 63.936,956.89 L 65.376,960.058 L 6.984,958.474 z " + id="path22" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g24" + transform="translate(0,-924.362)"> + <path + d="M 6.984,964.45 L 66.528,962.866 L 67.968,966.106 L 6.984,964.45 z " + id="path26" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g28" + transform="translate(0,-924.362)"> + <path + d="M 6.984,970.498 L 69.12,968.77 L 70.632,972.226 L 6.984,970.498 z " + id="path30" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g32" + transform="translate(0,-924.362)"> + <path + d="M 6.984,976.474 L 71.712,974.674 L 73.296,978.274 L 6.984,976.474 z " + id="path34" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g36" + transform="translate(0,-924.362)"> + <path + d="M 6.984,982.45 L 74.304,980.65 L 75.96,984.322 L 6.984,982.45 z " + id="path38" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g40" + transform="translate(0,-924.362)"> + <path + d="M 6.984,988.426 L 76.896,986.554 L 78.408,989.866 L 78.192,990.37 L 6.984,988.426 z " + id="path42" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g44" + transform="translate(0,-924.362)"> + <path + d="M 6.984,994.402 L 77.112,992.458 L 75.168,996.274 L 6.984,994.402 z " + id="path46" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g48" + transform="translate(0,-924.362)"> + <path + d="M 6.984,1000.378 L 74.016,998.578 L 72.216,1002.178 L 6.984,1000.378 z " + id="path50" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g52" + transform="translate(0,-924.362)"> + <path + d="M 6.984,1006.354 L 70.92,1004.626 L 69.192,1008.082 L 6.984,1006.354 z " + id="path54" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g56" + transform="translate(0,-924.362)"> + <path + d="M 6.984,1012.402 L 67.896,1010.746 L 66.168,1013.986 L 6.984,1012.402 z " + id="path58" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g60" + transform="translate(0,-924.362)"> + <path + d="M 6.984,1036.378 L 50.976,1035.154 L 50.976,1037.602 L 6.984,1036.378 z " + id="path62" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g64" + transform="translate(0,-924.362)"> + <path + d="M 6.984,1030.402 L 58.104,1028.962 L 58.032,1029.106 L 57.888,1029.178 L 57.744,1029.322 L 57.6,1029.394 L 57.528,1029.538 L 57.384,1029.61 L 57.312,1029.682 L 57.168,1029.682 L 57.024,1029.754 L 56.952,1029.826 L 56.808,1029.898 L 56.736,1029.898 L 56.592,1029.97 L 56.448,1029.97 L 56.376,1029.97 L 56.232,1030.042 L 56.088,1030.042 L 55.944,1030.042 L 50.976,1030.114 L 50.976,1031.554 L 6.984,1030.402 z " + id="path66" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g68" + transform="translate(0,-924.362)"> + <path + d="M 6.984,1024.354 L 61.776,1022.914 L 60.984,1024.498 L 60.696,1025.002 L 60.48,1025.506 L 60.264,1025.866 L 6.984,1024.354 z " + id="path70" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g72" + transform="translate(0,-924.362)"> + <path + d="M 6.984,1018.378 L 64.8,1016.794 L 63.288,1019.89 L 6.984,1018.378 z " + id="path74" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g76" + transform="translate(0,-924.362)"> + <path + d="M 63.936,924.49 L 118.872,924.49 C 123.84,924.49 127.872,928.522 127.872,933.49 L 127.872,1043.362 C 127.872,1048.33 123.84,1052.362 118.872,1052.362 L 63.936,1052.362 L 63.936,1050.418 L 117.864,1050.418 C 122.256,1050.418 125.856,1046.818 125.856,1042.426 L 125.856,934.498 C 125.856,930.106 122.256,926.506 117.864,926.506 L 63.936,926.506 L 63.936,924.49 z " + id="path78" + style="fill:#4d4d4d" /> + </g> + <g + style="fill:#4d4d4d" + id="g80" + transform="translate(0,-924.362)"> + <path + d="M 9,924.49 L 63.936,924.49 L 63.936,926.506 L 9.936,926.506 C 5.544,926.506 1.944,930.106 1.944,934.498 L 1.944,1042.426 C 1.944,1046.818 5.544,1050.418 9.936,1050.418 L 63.936,1050.418 L 63.936,1052.362 L 9,1052.362 C 4.032,1052.362 0,1048.33 0,1043.362 L 0,933.49 C 0,928.522 4.032,924.49 9,924.49 z " + id="path82" + style="fill:#4d4d4d" /> + </g> +</svg> diff --git a/plugins/x2go/x2go_plugin.c b/plugins/x2go/x2go_plugin.c new file mode 100644 index 0000000..37ca272 --- /dev/null +++ b/plugins/x2go/x2go_plugin.c @@ -0,0 +1,3463 @@ +/* + * Project: Remmina Plugin X2Go + * Description: Remmina protocol plugin to connect via X2Go using PyHocaCLI + * Author: Mike Gabriel <mike.gabriel@das-netzwerkteam.de> + * Antenore Gatta <antenore@simbiosi.org> + * Copyright: 2010-2011 Vic Lee + * 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo + * 2015 Antenore Gatta + * 2016-2018 Antenore Gatta, Giovanni Panozzo + * 2019 Mike Gabriel + * 2021 Daniel Teichmann + * License: GPL-2+ + * + * 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 "x2go_plugin.h" +#include "common/remmina_plugin.h" + +#include <gtk/gtkx.h> +#ifdef GDK_WINDOWING_X11 +#include <gdk/gdkx.h> +#elif defined(GDK_WINDOWING_WAYLAND) +#include <gdk/gdkwayland.h> +#endif + +#include <X11/Xlib.h> +#include <X11/XKBlib.h> +#include <X11/extensions/XKBrules.h> + +#include <sys/types.h> +#include <signal.h> +#include <time.h> +#include <ctype.h> + +#define FEATURE_AVAILABLE(gpdata, feature) \ + gpdata->available_features ? (g_list_find_custom( \ + gpdata->available_features, \ + feature, \ + (GCompareFunc) g_strcmp0 \ + ) ? TRUE : FALSE) : FALSE + +#define FEATURE_NOT_AVAIL_STR(feature) \ + g_strdup_printf(_("The command-line feature '%s' is not available! Attempting " \ + "to start PyHoca-CLI without using this feature…"), feature) + +#define GET_PLUGIN_DATA(gp) \ + (RemminaPluginX2GoData*) g_object_get_data(G_OBJECT(gp), "plugin-data") + +// --------- SESSIONS ------------ +#define SET_RESUME_SESSION(gp, resume_data) \ + g_object_set_data_full(G_OBJECT(gp), "resume-session-data", \ + resume_data, \ + g_free) + +#define GET_RESUME_SESSION(gp) \ + (gchar*) g_object_get_data(G_OBJECT(gp), "resume-session-data") + +// A session is selected if the returning value is something other than 0. +#define IS_SESSION_SELECTED(gp) \ + g_object_get_data(G_OBJECT(gp), "session-selected") ? TRUE : FALSE + +// We don't use the function as a real pointer but rather as a boolean value. +#define SET_SESSION_SELECTED(gp, is_session_selected) \ + g_object_set_data_full(G_OBJECT(gp), "session-selected", \ + is_session_selected, \ + NULL) +// ------------------- + +#define REMMINA_PLUGIN_INFO(fmt, ...) \ + rm_plugin_service->_remmina_info("[%s] " fmt, \ + PLUGIN_NAME, ##__VA_ARGS__) + +#define REMMINA_PLUGIN_MESSAGE(fmt, ...) \ + rm_plugin_service->_remmina_message("[%s] " fmt, \ + PLUGIN_NAME, ##__VA_ARGS__) + +#define REMMINA_PLUGIN_DEBUG(fmt, ...) \ + rm_plugin_service->_remmina_debug(__func__, "[%s] " fmt, \ + PLUGIN_NAME, ##__VA_ARGS__) + +#define REMMINA_PLUGIN_WARNING(fmt, ...) \ + rm_plugin_service->_remmina_warning(__func__, "[%s] " fmt, \ + PLUGIN_NAME, ##__VA_ARGS__) + +#define REMMINA_PLUGIN_AUDIT(fmt, ...) \ + rm_plugin_service->_remmina_audit(__func__, fmt, ##__VA_ARGS__) + +#define REMMINA_PLUGIN_ERROR(fmt, ...) \ + rm_plugin_service->_remmina_error(__func__, "[%s] " fmt, \ + PLUGIN_NAME, ##__VA_ARGS__) + +#define REMMINA_PLUGIN_CRITICAL(fmt, ...) \ + rm_plugin_service->_remmina_critical(__func__, "[%s] " fmt, \ + PLUGIN_NAME, ##__VA_ARGS__) + +#define GET_PLUGIN_STRING(value) \ + g_strdup(rm_plugin_service->file_get_string(remminafile, value)) + +#define GET_PLUGIN_PASSWORD(value) \ + GET_PLUGIN_STRING(value) + +#define GET_PLUGIN_INT(value, default_value) \ + rm_plugin_service->file_get_int(remminafile, value, default_value) + +#define GET_PLUGIN_BOOLEAN(value) \ + rm_plugin_service->file_get_int(remminafile, value, FALSE) + +static RemminaPluginService *rm_plugin_service = NULL; + +typedef struct _RemminaPluginX2GoData { + GtkWidget *socket; + gint socket_id; + + pthread_t thread; + + Display *display; + Window window_id; + int (*orig_handler)(Display *, XErrorEvent *); + + GPid pidx2go; + + gboolean disconnected; + + GList* available_features; +} RemminaPluginX2GoData; + +/** + * @brief Can be used to pass custom user data between functions and threads. + * *AND* pass the useful RemminaProtocolWidget with it along. + */ +typedef struct _X2GoCustomUserData { + RemminaProtocolWidget* gp; + gpointer dialog_data; + gpointer connect_data; + gpointer opt1; + gpointer opt2; +} X2GoCustomUserData; + +/** + * @brief Used for the session chooser dialog (GtkListStore) + * See the example at: https://docs.gtk.org/gtk3/class.ListStore.html + * The order is the exact same as the user sees in the dialog. + * SESSION_NUM_PROPERTIES is used to keep count of the properties + * and it must be the last object. + */ +enum SESSION_PROPERTIES { + SESSION_DISPLAY = 0, + SESSION_STATUS, + SESSION_SESSION_ID, + SESSION_SUSPENDED_SINCE, + SESSION_CREATE_DATE, + SESSION_AGENT_PID, + SESSION_USERNAME, + SESSION_HOSTNAME, + SESSION_COOKIE, + SESSION_GRAPHIC_PORT, + SESSION_SND_PORT, + SESSION_SSHFS_PORT, + SESSION_DIALOG_IS_VISIBLE, + SESSION_NUM_PROPERTIES // Must be last. Counts all enum elements. +}; + +// Following str2int code was adapted from Stackoverflow: +// https://stackoverflow.com/questions/7021725/how-to-convert-a-string-to-integer-in-c +typedef enum _str2int_errno { + STR2INT_SUCCESS, + STR2INT_OVERFLOW, + STR2INT_UNDERFLOW, + STR2INT_INCONVERTIBLE, + STR2INT_INVALID_DATA +} str2int_errno; + +/** + * @brief Convert string s to int out. + * + * @param out The converted int. Cannot be NULL. + * + * @param s Input string to be converted. \n + * The format is the same as strtol, + * except that the following are inconvertible: \n + * * empty string \n + * * leading whitespace \n + * * or any trailing characters that are not part of the number \n + * Cannot be NULL. + * @param base Base to interpret string in. Same range as strtol (2 to 36). + * + * @return Indicates if the operation succeeded, or why it failed with str2int_errno enum. + */ +str2int_errno str2int(gint *out, gchar *s, gint base) +{ + gchar *end; + + if (!s || !out || base <= 0) return STR2INT_INVALID_DATA; + + if (s[0] == '\0' || isspace(s[0])) return STR2INT_INCONVERTIBLE; + + errno = 0; + glong l = strtol(s, &end, base); + + /* Both checks are needed because INT_MAX == LONG_MAX is possible. */ + if (l > INT_MAX || (errno == ERANGE && l == LONG_MAX)) return STR2INT_OVERFLOW; + if (l < INT_MIN || (errno == ERANGE && l == LONG_MIN)) return STR2INT_UNDERFLOW; + if (*end != '\0') return STR2INT_INCONVERTIBLE; + + *out = l; + return STR2INT_SUCCESS; +} + +/** + * DialogData: + * @param flags see GtkDialogFlags + * @param type see GtkMessageType + * @param buttons see GtkButtonsType + * @param title Title of the Dialog + * @param message Message of the Dialog + * @param callbackfunc A GCallback function which will be executed on the dialogs + * 'response' signal. Allowed to be NULL. \n + * The callback function is obliged to destroy the dialog widget. \n + * @param dialog_factory A user-defined callback function that is called when it is time + * to build the actual GtkDialog. \n + * Can be used to build custom dialogs. Allowed to be NULL. + * + * + * The `DialogData` structure contains all info needed to open a GTK dialog with + * rmplugin_x2go_open_dialog() + * + * Quick example of a callback function: \n + * static gboolean rmplugin_x2go_test_callback(RemminaProtocolWidget *gp, gint response_id, \n + * GtkDialog *self) { \n + * REMMINA_PLUGIN_DEBUG("response: %i", response_id); \n + * if (response_id == GTK_RESPONSE_OK) { \n + * REMMINA_PLUGIN_DEBUG("OK!"); \n + * } \n + * gtk_widget_destroy(self); \n + * return G_SOURCE_REMOVE; \n + * } + * + */ +struct _DialogData +{ + GtkWindow *parent; + GtkDialogFlags flags; + GtkMessageType type; + GtkButtonsType buttons; + gchar *title; + gchar *message; + GCallback callbackfunc; + + // If the dialog needs to be custom. + GCallback dialog_factory_func; + gpointer dialog_factory_data; +}; + +/** + * @param custom_data X2GoCustomUserData structure with the following: \n + * gp -> gp (RemminaProtocolWidget*) \n + * dialog_data -> dialog data (struct _DialogData*) + * @returns: FALSE. This source should be removed from main loop. + * #G_SOURCE_CONTINUE and #G_SOURCE_REMOVE are more memorable + * names for the return value. + */ +static gboolean rmplugin_x2go_open_dialog(X2GoCustomUserData *custom_data) +{ + REMMINA_PLUGIN_DEBUG("Function entry."); + + if (!custom_data || !custom_data->gp || !custom_data->dialog_data) { + REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( + _("Internal error: %s"), + _("Parameter 'custom_data' is not initialized!") + )); + + return G_SOURCE_REMOVE; + } + + RemminaProtocolWidget *gp = (RemminaProtocolWidget*) custom_data->gp; + struct _DialogData *ddata = (struct _DialogData*) custom_data->dialog_data; + + if (ddata) { + // Can't check type, flags or buttons + // because they are enums and '0' is a valid value + if (!ddata->title || !ddata->message) { + REMMINA_PLUGIN_CRITICAL("%s", _("Broken `DialogData`! Aborting…")); + return G_SOURCE_REMOVE; + } + } else { + REMMINA_PLUGIN_CRITICAL("%s", _("Can't retrieve `DialogData`! Aborting…")); + return G_SOURCE_REMOVE; + } + + REMMINA_PLUGIN_DEBUG("`DialogData` checks passed. Now showing dialog…"); + + GtkWidget* widget_gtk_dialog = NULL; + + if (ddata->dialog_factory_func != NULL) { + REMMINA_PLUGIN_DEBUG("Calling *custom* dialog factory function…"); + GCallback dialog_factory_func = G_CALLBACK(ddata->dialog_factory_func); + gpointer dialog_factory_data = ddata->dialog_factory_data; + + // Calling dialog_factory_func(custom_data, dialog_factory_data); + widget_gtk_dialog = ((GtkWidget* (*)(X2GoCustomUserData*, gpointer)) + dialog_factory_func)(custom_data, dialog_factory_data); + } else { + widget_gtk_dialog = gtk_message_dialog_new(ddata->parent, + ddata->flags, + ddata->type, + ddata->buttons, + "%s", ddata->title); + + gtk_message_dialog_format_secondary_text( + GTK_MESSAGE_DIALOG(widget_gtk_dialog), "%s", ddata->message); + } + + if (!widget_gtk_dialog) { + REMMINA_PLUGIN_CRITICAL("Error! Aborting."); + return G_SOURCE_REMOVE; + } + + if (ddata->callbackfunc) { + g_signal_connect_swapped(G_OBJECT(widget_gtk_dialog), "response", + G_CALLBACK(ddata->callbackfunc), + custom_data); + } else { + g_signal_connect(G_OBJECT(widget_gtk_dialog), "response", + G_CALLBACK(gtk_widget_destroy), + NULL); + } + + gtk_widget_show_all(widget_gtk_dialog); + + // Delete ddata object and reference 'dialog-data' in gp. + g_object_set_data(G_OBJECT(gp), "dialog-data", NULL); + + return G_SOURCE_REMOVE; +} + +/** + * @brief These define the responses of session-chooser-dialog's buttons. + */ +enum SESSION_CHOOSER_RESPONSE_TYPE { + SESSION_CHOOSER_RESPONSE_NEW = 0, + SESSION_CHOOSER_RESPONSE_CHOOSE, + SESSION_CHOOSER_RESPONSE_TERMINATE, +}; + +/** + * @brief Finds a child GtkWidget of a parent GtkWidget. + * Copied from https://stackoverflow.com/a/23497087 ;) + * + * @param parent Parent GtkWidget* + * @param name Name string of child. (Must be set before, er else it will be a + * default string) + * @return GtkWidget* + */ +static GtkWidget* rmplugin_x2go_find_child(GtkWidget* parent, const gchar* name) +{ + const gchar* parent_name = gtk_widget_get_name((GtkWidget*) parent); + if (g_ascii_strcasecmp(parent_name, (gchar*) name) == 0) { + return parent; + } + + if (GTK_IS_BIN(parent)) { + GtkWidget *child = gtk_bin_get_child(GTK_BIN(parent)); + return rmplugin_x2go_find_child(child, name); + } + + if (GTK_IS_CONTAINER(parent)) { + GList *children = gtk_container_get_children(GTK_CONTAINER(parent)); + while (children != NULL) { + GtkWidget *widget = rmplugin_x2go_find_child(children->data, name); + if (widget != NULL) { + return widget; + } + + children = g_list_next(children); + } + } + + return NULL; +} + +/** + * @brief Gets executed on "row-activated" signal. It is emitted when the method when + * the user double clicks a treeview row. It is also emitted when a non-editable + * row is selected and one of the keys: Space, Shift+Space, Return or Enter is + * pressed. + * + * @param custom_data X2GoCustomUserData structure with the following: \n + * gp -> gp (RemminaProtocolWidget*) \n + * opt1 -> dialog widget (GtkWidget*) + */ +static gboolean rmplugin_x2go_session_chooser_row_activated(GtkTreeView *treeview, + GtkTreePath *path, + GtkTreeViewColumn *column, + X2GoCustomUserData *custom_data) +{ + REMMINA_PLUGIN_DEBUG("Function entry."); + + if (!custom_data || !custom_data->gp || !custom_data->opt1) { + REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( + _("Internal error: %s"), + _("Parameter 'custom_data' is not initialized!") + )); + + return G_SOURCE_REMOVE; + } + + RemminaProtocolWidget* gp = (RemminaProtocolWidget*) custom_data->gp; + // dialog_data (unused) + // connect_data (unused) + GtkWidget* dialog = GTK_WIDGET(custom_data->opt1); + + gchar *session_id; + GtkTreeIter iter; + GtkTreeModel *model = gtk_tree_view_get_model(treeview); + + if (gtk_tree_model_get_iter(model, &iter, path)) { + gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, + SESSION_SESSION_ID, &session_id, -1); + + // Silent bail out. + if (!session_id || strlen(session_id) <= 0) return G_SOURCE_REMOVE; + + SET_RESUME_SESSION(gp, session_id); + + // Unstucking main process. Telling it that a session has been selected. + // We use a trick here. As long as there is something other than 0 + // stored, a session is selected. So we use the gpointer as a gboolean. + SET_SESSION_SELECTED(gp, (gpointer) TRUE); + gtk_widget_hide(GTK_WIDGET(dialog)); + gtk_widget_destroy(GTK_WIDGET(dialog)); + } + + return G_SOURCE_REMOVE; +} + +/** + * @brief Translates a session property (described by SESSION_PROPERTIES enum) to a + * string containing it's display name. + * + * @param session_property A session property. (as described by SESSION_PROPERTIES enum) + * @return gchar* Translated display name. (Can be NULL, if session_property is invalid!) + */ +static gchar *rmplugin_x2go_session_property_to_string(guint session_property) { + gchar* return_char = NULL; + + switch (session_property) { + // I think we can close one eye here regarding max line-length. + case SESSION_DISPLAY: return_char = g_strdup(_("X Display")); break; + case SESSION_STATUS: return_char = g_strdup(_("Status")); break; + case SESSION_SESSION_ID: return_char = g_strdup(_("Session ID")); break; + case SESSION_CREATE_DATE: return_char = g_strdup(_("Create date")); break; + case SESSION_SUSPENDED_SINCE: return_char = g_strdup(_("Suspended since")); break; + case SESSION_AGENT_PID: return_char = g_strdup(_("Agent PID")); break; + case SESSION_USERNAME: return_char = g_strdup(_("Username")); break; + case SESSION_HOSTNAME: return_char = g_strdup(_("Hostname")); break; + case SESSION_COOKIE: return_char = g_strdup(_("Cookie")); break; + case SESSION_GRAPHIC_PORT: return_char = g_strdup(_("Graphic port")); break; + case SESSION_SND_PORT: return_char = g_strdup(_("SND port")); break; + case SESSION_SSHFS_PORT: return_char = g_strdup(_("SSHFS port")); break; + case SESSION_DIALOG_IS_VISIBLE: return_char = g_strdup(_("Visible")); break; + } + + return return_char; +} + +/** + * @brief Builds a dialog which contains all found X2Go-Sessions of the remote server. + * The dialog gives the user the option to choose between resuming or terminating + * an existing session or to create a new one. + * + * @param custom_data X2GoCustomUserData structure with the following: \n + * gp -> gp (RemminaProtocolWidget*) \n + * dialog_data -> dialog data (struct _DialogData*) \n + * connect_data -> connection data (struct _ConnectionData*) + * @param sessions_list The GList* Should contain all found X2Go-Sessions. + * Sessions are string arrays of properties. + * The type of the GList is gchar**. + * + * @returns GtkWidget* custom dialog. + */ +static GtkWidget* rmplugin_x2go_choose_session_dialog_factory(X2GoCustomUserData *custom_data, + GList *sessions_list) +{ + REMMINA_PLUGIN_DEBUG("Function entry."); + + if (!custom_data || !custom_data->gp || + !custom_data->dialog_data || !custom_data->connect_data) { + REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( + _("Internal error: %s"), + _("Parameter 'custom_data' is not initialized!") + )); + + return NULL; + } + + struct _DialogData* ddata = (struct _DialogData*) custom_data->dialog_data; + + if (!ddata || !sessions_list || !ddata->title) { + REMMINA_PLUGIN_CRITICAL("%s", _("Could not retrieve valid `DialogData` or " + "`sessions_list`! Aborting…")); + return NULL; + } + + GtkWidget *widget_gtk_dialog = NULL; + widget_gtk_dialog = gtk_dialog_new_with_buttons(ddata->title, ddata->parent, + ddata->flags, + // TRANSLATORS: Stick to x2goclient's translation for terminate. + _("_Terminate"), + SESSION_CHOOSER_RESPONSE_TERMINATE, + // TRANSLATORS: Stick to x2goclient's translation for resume. + _("_Resume"), + SESSION_CHOOSER_RESPONSE_CHOOSE, + _("_New"), + SESSION_CHOOSER_RESPONSE_NEW, + NULL); + + GtkWidget *button = gtk_dialog_get_widget_for_response( + GTK_DIALOG(widget_gtk_dialog), + SESSION_CHOOSER_RESPONSE_TERMINATE); + // TRANSLATORS: Tooltip for terminating button inside Session-Chooser-Dialog. + // TRANSLATORS: Please stick to X2GoClient's way of translating. + gtk_widget_set_tooltip_text(button, _("Terminating X2Go sessions can take a moment.")); + + #define DEFAULT_DIALOG_WIDTH 720 + #define DEFAULT_DIALOG_HEIGHT (DEFAULT_DIALOG_WIDTH * 9) / 16 + + gtk_widget_set_size_request(GTK_WIDGET(widget_gtk_dialog), + DEFAULT_DIALOG_WIDTH, DEFAULT_DIALOG_HEIGHT); + gtk_window_set_default_size(GTK_WINDOW(widget_gtk_dialog), + DEFAULT_DIALOG_WIDTH, DEFAULT_DIALOG_HEIGHT); + + gtk_window_set_resizable(GTK_WINDOW(widget_gtk_dialog), TRUE); + + GtkWidget *scrolled_window = gtk_scrolled_window_new(NULL, NULL); + //gtk_widget_show(scrolled_window); + + gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area( + GTK_DIALOG(widget_gtk_dialog)) + ), GTK_WIDGET(scrolled_window), TRUE, TRUE, 5); + + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW (scrolled_window), + GTK_POLICY_AUTOMATIC, + GTK_POLICY_AUTOMATIC); + + + GType types[SESSION_NUM_PROPERTIES]; + + // First to last in SESSION_PROPERTIES. + for (gint i = 0; i < SESSION_NUM_PROPERTIES; ++i) { + // Everything is a String. (Except IS_VISIBLE flag) + // If that changes one day, you could extent the if statement here. + // But you would propably need a *lot* of refactoring. + // Especially in the session parser. + if (i == SESSION_DIALOG_IS_VISIBLE) { + types[i] = G_TYPE_BOOLEAN; + } else { + types[i] = G_TYPE_STRING; + } + } + + // create tree view + GtkListStore *store = gtk_list_store_newv(SESSION_NUM_PROPERTIES, types); + + GtkTreeModelFilter *filter = GTK_TREE_MODEL_FILTER( + gtk_tree_model_filter_new(GTK_TREE_MODEL(store), + NULL) + ); + gtk_tree_model_filter_set_visible_column(filter, SESSION_DIALOG_IS_VISIBLE); + + GtkWidget *tree_view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(filter)); + g_object_unref (G_OBJECT (store)); // tree now holds reference + gtk_widget_set_size_request(tree_view, -1, 300); + + // Gets name to be findable by rmplugin_x2go_find_child() + gtk_widget_set_name(GTK_WIDGET(tree_view), "session_chooser_treeview"); + + // create list view columns + gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(tree_view), TRUE); + gtk_tree_view_set_headers_clickable (GTK_TREE_VIEW(tree_view), FALSE); + gtk_tree_view_set_enable_search(GTK_TREE_VIEW(tree_view), TRUE); + gtk_widget_show (tree_view); + gtk_container_add (GTK_CONTAINER(scrolled_window), tree_view); + + GtkTreeViewColumn *tree_view_col = NULL; + GtkCellRenderer *cell_renderer = NULL; + gchar *header_title = NULL; + + // First to last in SESSION_PROPERTIES. + for (guint i = 0; i < SESSION_NUM_PROPERTIES; ++i) { + // Do not display SESSION_DIALOG_IS_VISIBLE. + if (i == SESSION_DIALOG_IS_VISIBLE) continue; + + header_title = rmplugin_x2go_session_property_to_string(i); + if (!header_title) { + REMMINA_PLUGIN_WARNING("%s", g_strdup_printf( + _("Internal error: %s"), g_strdup_printf( + _("Unknown property '%i'"), i + ))); + header_title = g_strdup_printf(_("Unknown property '%i'"), i); + } + + tree_view_col = gtk_tree_view_column_new(); + gtk_tree_view_column_set_title(tree_view_col, header_title); + gtk_tree_view_column_set_clickable(tree_view_col, FALSE); + gtk_tree_view_column_set_sizing (tree_view_col, GTK_TREE_VIEW_COLUMN_AUTOSIZE); + gtk_tree_view_column_set_resizable(tree_view_col, TRUE); + + cell_renderer = gtk_cell_renderer_text_new(); + gtk_tree_view_column_pack_start(tree_view_col, cell_renderer, TRUE); + gtk_tree_view_column_add_attribute(tree_view_col, cell_renderer, "text", i); + gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view), tree_view_col); + } + + GList *elem = NULL; + GtkTreeIter iter; + + for (elem = sessions_list; elem; elem = elem->next) { + gchar** session = (gchar**) elem->data; + g_assert(session != NULL); + + gtk_list_store_append(store, &iter); + + for (gint i = 0; i < SESSION_NUM_PROPERTIES; i++) { + gchar* property = session[i]; + GValue a = G_VALUE_INIT; + + // Everything here is a string (except SESSION_DIALOG_IS_VISIBLE) + + if (i == SESSION_DIALOG_IS_VISIBLE) { + g_value_init(&a, G_TYPE_BOOLEAN); + g_assert(G_VALUE_HOLDS_BOOLEAN(&a) && "GValue does not " + "hold a boolean!"); + // Default is to show every new session. + g_value_set_boolean(&a, TRUE); + } else { + g_value_init(&a, G_TYPE_STRING); + g_assert(G_VALUE_HOLDS_STRING(&a) && "GValue does not " + "hold a string!"); + g_value_set_static_string (&a, property); + } + + gtk_list_store_set_value(store, &iter, i, &a); + } + } + + /* Prepare X2GoCustomUserData *custom_data + * gp -> gp (RemminaProtocolWidget*) + * dialog_data -> dialog data (struct _DialogData*) + * connect_data -> connection data (struct _ConnectionData*) + * opt1 -> dialog widget (GtkWidget*) + */ + // everything else is already initialized. + custom_data->opt1 = widget_gtk_dialog; + + g_signal_connect(tree_view, "row-activated", + G_CALLBACK(rmplugin_x2go_session_chooser_row_activated), + custom_data); + + return widget_gtk_dialog; +} + +/** + * @brief Uses either 'dialog' or 'treeview' to return the GtkTreeModel of the + * Session-Chooser-Dialog. Directly giving 'treeview' as a parameter is faster. + * Only *one* parameter has to be given. The other one can be NULL. + * Error messages are all handled already. + * + * @param dialog The Session-Chooser-Dialog itself. (Slower) Can be NULL. + * @param treeview The GtkTreeView of the Session-Chooser-Dialog. (faster) Can be NULL. + * @return GtkTreeModelFilter* (Does not contains all rows, only the visible ones!!!) \n + * But you can get all rows with: \n + * gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(return_model)); + */ +static GtkTreeModelFilter* rmplugin_x2go_session_chooser_get_filter_model(GtkWidget *dialog, + GtkTreeView* treeview) +{ + //REMMINA_PLUGIN_DEBUG("Function entry."); + GtkTreeModel *return_model = NULL; + + if (!treeview && dialog) { + GtkWidget *treeview_new = rmplugin_x2go_find_child(GTK_WIDGET(dialog), + "session_chooser_treeview"); + + if (!treeview_new) { + REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( + _("Internal error: %s"), + _("Could not find child GtkTreeView of " + "session chooser dialog.") + )); + return NULL; + } + + return_model = gtk_tree_view_get_model(GTK_TREE_VIEW(treeview_new)); + } else if (treeview) { + return_model = gtk_tree_view_get_model(GTK_TREE_VIEW(treeview)); + } else { + REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( + _("Internal error: %s"), + _("Neither the 'dialog' nor 'treeview' parameters are initialized! " + "At least one of them must be given.") + )); + return NULL; + } + + if (!return_model || !GTK_TREE_MODEL_FILTER(return_model)) { + REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( + _("Internal error: %s"), + _("Could not obtain \"GtkTreeModelFilter*\" of the session chooser dialog, " + "for unknown reason.") + )); + } + + return GTK_TREE_MODEL_FILTER(return_model); +} + +/** + * @brief Gets the selected row of the Session-Chooser-Dialog. + * The path gets converted with gtk_tree_model_filter_convert_child_path_to_path() + * before it gets returned. So path describes a row of 'filter' and *not* its + * child GtkTreeModel. + * + * @param dialog The Session-Chooser-Dialog. + * @return GtkTreePath* describing the path to the row. + */ +static GtkTreePath* rmplugin_x2go_session_chooser_get_selected_row(GtkWidget *dialog) +{ + REMMINA_PLUGIN_DEBUG("Function entry."); + + GtkWidget *treeview = rmplugin_x2go_find_child(GTK_WIDGET(dialog), + "session_chooser_treeview"); + if (!treeview) { + REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( + _("Internal error: %s"), + _("Could not find child GtkTreeView of session chooser dialog.") + )); + return NULL; + } + + GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(treeview)); + if (!selection) { + REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( + _("Internal error: %s"), + _("Could not get currently selected row (session)!") + )); + return NULL; + } + + GtkTreeModelFilter *filter = rmplugin_x2go_session_chooser_get_filter_model( + NULL, GTK_TREE_VIEW(treeview)); + GtkTreeModel *model = gtk_tree_model_filter_get_model(filter); + if (!model) return NULL; // error message was already handled. + + GtkTreeModel *filter_model = GTK_TREE_MODEL(filter); + g_assert(filter_model && "Could not cast 'filter' to a GtkTreeModel!"); + GList *selected_rows = gtk_tree_selection_get_selected_rows(selection, &filter_model); + + // We only support single selection. + gint selected_rows_num = gtk_tree_selection_count_selected_rows(selection); + if (selected_rows_num != 1) { + REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( + _("Internal error: %s"), g_strdup_printf( + _("Exactly one session should be selectable but '%i' rows " + "(sessions) are selected."), + selected_rows_num + ))); + return NULL; + } + + // This would be very dangerous (we didn't check for NULL) if we hadn't just + // checked that only one row is selected. + GtkTreePath *path = selected_rows->data; + + // Convert to be path of GtkTreeModelFilter and *not* its child GtkTreeModel. + path = gtk_tree_model_filter_convert_child_path_to_path(filter, path); + + return path; +} + +/** + * @brief Finds the GtkTreeView inside of the session chooser dialog, + * determines the selected row and extracts a property. + * + * @param dialog GtkWidget* the dialog itself. + * @param property_index Index of property. + * @param row A specific row to get the property of. (Can be NULL) + * + * @return GValue The value of property. (Can be non-initialized!) + */ +static GValue rmplugin_x2go_session_chooser_get_property(GtkWidget *dialog, + gint property_index, + GtkTreePath *row) +{ + //REMMINA_PLUGIN_DEBUG("Function entry."); + + GValue ret_value = G_VALUE_INIT; + + if (!row) { + GtkTreePath *selected_row = rmplugin_x2go_session_chooser_get_selected_row(dialog); + if (!selected_row) return ret_value; // error message was already handled. + row = selected_row; + } + + GtkTreeModelFilter *filter = rmplugin_x2go_session_chooser_get_filter_model(dialog, NULL); + GtkTreeModel *model = gtk_tree_model_filter_get_model(filter); + if (!model) return ret_value; // error message was already handled. + + GtkTreeIter iter; + gboolean success = gtk_tree_model_get_iter(model, &iter, row); + if (!success) { + REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( + _("Internal error: %s"), + _("Failed to fill 'GtkTreeIter'.") + )); + + return ret_value; + } + + GValue property = G_VALUE_INIT; + gtk_tree_model_get_value(model, &iter, property_index, &property); + + return property; +} + +/** + * @brief This function dumps all properties of a session to the console. + * It can/should be used with: \n + * gtk_tree_model_foreach(GTK_TREE_MODEL(model), (GtkTreeModelForeachFunc) \n + * rmplugin_x2go_dump_session_properties, \n + * dialog); + */ +/*static void rmplugin_x2go_dump_session_properties(GtkTreeModel *model, GtkTreePath *path, + GtkTreeIter *iter, GtkWidget *dialog) +{ + //REMMINA_PLUGIN_DEBUG("Function entry."); + + g_debug(_("Properties for session with path '%s':"), gtk_tree_path_to_string(path)); + for (guint i = 0; i < SESSION_NUM_PROPERTIES; i++) { + GValue property = G_VALUE_INIT; + property = rmplugin_x2go_session_chooser_get_property(dialog, i, path); + + gchar* display_name = rmplugin_x2go_session_property_to_string(i); + g_assert(display_name && "Could not get display name for a property!"); + + if (i == SESSION_DIALOG_IS_VISIBLE) { + g_assert(G_VALUE_HOLDS_BOOLEAN(&property) && "GValue does not " + "hold a boolean!"); + g_debug("\t%s: '%s'", display_name, + g_value_get_boolean(&property) ? "TRUE" : "FALSE"); + } else { + g_assert(G_VALUE_HOLDS_STRING(&property) && "GValue does not " + "hold a string!"); + g_debug("\t%s: '%s'", display_name, g_value_get_string(&property)); + } + } +}*/ + +/** + * @brief This function synchronously spawns a pyhoca-cli process with argv as arguments. + * @param argc Number of arguments. + * @param argv Arguments as string array. \n + * Last elements has to be NULL. \n + * Strings will get freed automatically. + * @param error Will be filled with an error message on fail. + * @param env String array of enviroment variables. \n + * The list is NULL terminated and each item in + * the list is of the form `NAME=VALUE`. + * + * @returns Returns either standard output string or NULL if it failed. + */ +static gchar* rmplugin_x2go_spawn_pyhoca_process(guint argc, gchar* argv[], + GError** error, gchar** env) +{ + REMMINA_PLUGIN_DEBUG("Function entry."); + + if (!argv) { + gchar* errmsg = g_strdup_printf( + _("Internal error: %s"), + _("parameter 'argv' is 'NULL'.") + ); + REMMINA_PLUGIN_CRITICAL("%s", errmsg); + g_set_error(error, 1, 1, "%s", errmsg); + return NULL; + } + + if (!error) { + // Can't report error message back since 'error' is NULL. + REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( + _("Internal error: %s"), + _("parameter 'error' is 'NULL'.") + )); + return NULL; + } + + if (!env || !env[0]) { + gchar* errmsg = g_strdup_printf( + _("Internal error: %s"), + _("parameter 'env' is either invalid or uninitialized.") + ); + REMMINA_PLUGIN_CRITICAL("%s", errmsg); + g_set_error(error, 1, 1, "%s", errmsg); + return NULL; + } + + gint exit_code = 0; + gchar *standard_out; + // Just supresses pyhoca-cli's help message when pyhoca-cli's version is too old. + gchar *standard_err; + + gboolean success_ret = g_spawn_sync(NULL, argv, env, G_SPAWN_SEARCH_PATH, NULL, + NULL, &standard_out, &standard_err, + &exit_code, error); + + REMMINA_PLUGIN_INFO("%s", _("Started PyHoca-CLI with the following arguments:")); + // Print every argument except passwords. Free all arg strings. + for (gint i = 0; i < argc - 1; i++) { + gchar* curr_arg = argv[i]; + + if (g_str_equal(curr_arg, "--password") || + g_str_equal(curr_arg, "--ssh-passphrase")) { + g_printf("%s ", curr_arg); + g_printf("XXXXXX "); + g_free(curr_arg); + g_free(argv[++i]); + continue; + } else { + g_printf("%s ", curr_arg); + g_free(curr_arg); + } + } + g_printf("\n"); + + /* TOO VERBOSE: */ + /* + REMMINA_PLUGIN_DEBUG("%s", _("Started PyHoca-CLI with the " + "following environment variables:")); + REMMINA_PLUGIN_DEBUG("%s", g_strjoinv("\n", env)); + */ + + if (standard_err && strlen(standard_err) > 0) { + if (g_str_has_prefix(standard_err, "pyhoca-cli: error: a socket error " + "occured while establishing the connection:")) { + // Log error into GUI. + gchar* errmsg = g_strdup_printf( + _("The necessary PyHoca-CLI process has encountered a " + "internet connection problem.") + ); + + // Log error into debug window and stdout + REMMINA_PLUGIN_CRITICAL("%s:\n%s", errmsg, standard_err); + g_set_error(error, 1, 1, "%s", errmsg); + return NULL; + } else { + gchar* errmsg = g_strdup_printf( + _("Could not start " + "PyHoca-CLI:\n%s"), + standard_err + ); + REMMINA_PLUGIN_CRITICAL("%s", errmsg); + g_set_error(error, 1, 1, "%s", errmsg); + return NULL; + } + } else if (!success_ret || (*error) || strlen(standard_out) <= 0 || exit_code) { + if (!(*error)) { + gchar* errmsg = g_strdup_printf( + _("An unknown error occured while trying " + "to start PyHoca-CLI. Exit code: %i"), + exit_code); + REMMINA_PLUGIN_WARNING("%s", errmsg); + g_set_error(error, 1, 1, "%s", errmsg); + } else { + gchar* errmsg = g_strdup_printf( + _("An unknown error occured while trying to start " + "PyHoca-CLI. Exit code: %i. Error: '%s'"), + exit_code, (*error)->message); + REMMINA_PLUGIN_WARNING("%s", errmsg); + } + + return NULL; + } + + return standard_out; +} + +/** + * @brief Stores all necessary information needed for retrieving sessions from + * a X2Go server. + */ +struct _ConnectionData { + gchar* host; + gchar* username; + gchar* password; + gchar* ssh_privatekey; + gchar* ssh_passphrase; +}; + +/** + * @brief Either sets a specific row visible or invisible. + * Also handles 'terminate' and 'resume' buttons of session-chooser-dialog. + * If there are no sessions available anymore, disable all buttons which are not 'new' + * and if a session is available again, enable them. + * + * @param path Describes which row. (GtkTreePath*) \n + * Should be from GtkTreeModelFilter's perspective! + * @param value TRUE = row is visible & FALSE = row is invisible (gboolean) + * @param dialog Session-Chooser-Dialog (GtkDialog*) + * @return Returns TRUE if successful. (gboolean) + */ +static gboolean rmplugin_x2go_session_chooser_set_row_visible(GtkTreePath *path, + gboolean value, + GtkDialog *dialog) { + REMMINA_PLUGIN_DEBUG("Function entry."); + + if (!path || !dialog) { + REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( + _("Internal error: %s"), + _("Neither the 'path' nor 'dialog' parameters are initialized.") + )); + return FALSE; + } + + GtkTreeModelFilter *filter = rmplugin_x2go_session_chooser_get_filter_model( + GTK_WIDGET(dialog), NULL); + GtkTreeModel *model = gtk_tree_model_filter_get_model(filter); + + // error message was already handled. + if (!model) return FALSE; + + GtkTreeIter iter; + if (!gtk_tree_model_get_iter(GTK_TREE_MODEL(model), &iter, path)) { + REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( + _("Internal error: %s"), + _("GtkTreePath 'path' describes a non-existing row!") + )); + return FALSE; + } + + + // Make session either visible or invisible. + gtk_list_store_set(GTK_LIST_STORE(model), &iter, + SESSION_DIALOG_IS_VISIBLE, value, -1); + + // Update row. + gtk_tree_model_row_changed(GTK_TREE_MODEL(model), path, &iter); + + /* Get IS_VISIBLE flag of a session. */ + // GValue ret_value = G_VALUE_INIT; + // ret_value = rmplugin_x2go_session_chooser_get_property(GTK_WIDGET(dialog), + // SESSION_DIALOG_IS_VISIBLE, + // path); + // g_debug("Is visible: %s", g_value_get_boolean(&ret_value) ? "TRUE" : "FALSE"); + + + GtkWidget *term_button = gtk_dialog_get_widget_for_response( + GTK_DIALOG(dialog), + SESSION_CHOOSER_RESPONSE_TERMINATE); + GtkWidget *resume_button = gtk_dialog_get_widget_for_response( + GTK_DIALOG(dialog), + SESSION_CHOOSER_RESPONSE_CHOOSE); + + // If no (visible) row is left to terminate disable terminate and resume buttons. + gint rows_amount = gtk_tree_model_iter_n_children(GTK_TREE_MODEL(filter), NULL); + if (rows_amount <= 0) { + gtk_widget_set_sensitive(term_button, FALSE); + gtk_widget_set_sensitive(resume_button, FALSE); + } else { + gtk_widget_set_sensitive(term_button, TRUE); + gtk_widget_set_sensitive(resume_button, TRUE); + } + + // Success, yay! + return TRUE; +} + +static gboolean rmplugin_x2go_verify_connection_data(struct _ConnectionData *connect_data) { + /* Check connect_data. */ + if (!connect_data || + !connect_data->host || + !connect_data->username || + strlen(connect_data->host) <= 0 || + strlen(connect_data->username) <= 0) + { + REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( + _("Internal error: %s"), + _("'Invalid connection data.'") + )); + + return FALSE; + } + + if (!connect_data->password && (!connect_data->ssh_privatekey || + strlen(connect_data->ssh_privatekey) <= 0)) { + REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( + _("Internal error: %s"), + _("'Invalid connection data.'") + )); + } else { + return TRUE; + } + + return FALSE; +} + +/** + * @brief Terminates a specific X2Go session using pyhoca-cli. + * + * @param custom_data X2GoCustomUserData structure with the following: \n + * gp -> gp (RemminaProtocolWidget*) \n + * dialog_data -> dialog data (struct _DialogData*) \n + * connect_data -> connection data (struct _ConnectionData*) \n + * opt1 -> selected row (GtkTreePath*) \n + * opt2 -> session-selection-dialog (GtkDialog*) + * + * @return G_SOURCE_REMOVE (FALSE), #G_SOURCE_CONTINUE and #G_SOURCE_REMOVE are more + * memorable names for the return value. See GLib docs. \n + * https://docs.gtk.org/glib/const.SOURCE_REMOVE.html + */ +static gboolean rmplugin_x2go_pyhoca_terminate_session(X2GoCustomUserData *custom_data) +{ + REMMINA_PLUGIN_DEBUG("Function entry."); + + if (!custom_data || !custom_data->gp || + !custom_data->dialog_data || !custom_data->connect_data || + !custom_data->opt1 || !custom_data->opt2) { + REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( + _("Internal error: %s"), + _("Parameter 'custom_data' is not fully initialized!") + )); + + return G_SOURCE_REMOVE; + } + + // Extract data passed by X2GoCustomUserData *custom_data. + RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(custom_data->gp); + //struct _DialogData *ddata = (struct _DialogData*) custom_data->dialog_data; + struct _ConnectionData *connect_data = (struct _ConnectionData*) custom_data->connect_data; + GtkTreePath* selected_row = (GtkTreePath*) custom_data->opt1; + GtkDialog *dialog = GTK_DIALOG(custom_data->opt2); + + gchar *host = NULL; + gchar *username = NULL; + gchar *password = NULL; + gchar *ssh_privatekey = NULL; + gchar *ssh_passphrase = NULL; + gboolean valid = rmplugin_x2go_verify_connection_data(connect_data); + if (valid) { + if (connect_data->password) password = connect_data->password; + if (connect_data->ssh_privatekey) { + ssh_privatekey = connect_data->ssh_privatekey; + + if (connect_data->ssh_passphrase) { + ssh_passphrase = connect_data->ssh_passphrase; + } + } + + host = connect_data->host; + username = connect_data->username; + } else { + return G_SOURCE_REMOVE; + } + + GValue value = rmplugin_x2go_session_chooser_get_property(GTK_WIDGET(dialog), + SESSION_SESSION_ID, + selected_row); + // error message was handled already. + if (!G_VALUE_HOLDS_STRING(&value)) return G_SOURCE_REMOVE; + const gchar *session_id = g_value_get_string(&value); + + // We will now start pyhoca-cli with only the '--terminate $SESSION_ID' option. + // (and of course auth related stuff) + gchar *argv[50]; + gint argc = 0; + + argv[argc++] = g_strdup("pyhoca-cli"); + + argv[argc++] = g_strdup("--server"); // Not listed as feature. + argv[argc++] = g_strdup_printf("%s", host); + + if (FEATURE_AVAILABLE(gpdata, "USERNAME")) { + argv[argc++] = g_strdup("-u"); + if (username) { + argv[argc++] = g_strdup_printf("%s", username); + } else { + argv[argc++] = g_strdup_printf("%s", g_get_user_name()); + } + } else { + REMMINA_PLUGIN_CRITICAL("%s", FEATURE_NOT_AVAIL_STR("USERNAME")); + return G_SOURCE_REMOVE; + } + + if (password && FEATURE_AVAILABLE(gpdata, "PASSWORD")) { + if (FEATURE_AVAILABLE(gpdata, "AUTH_ATTEMPTS")) { + argv[argc++] = g_strdup("--auth-attempts"); + argv[argc++] = g_strdup_printf ("%i", 0); + } else { + REMMINA_PLUGIN_WARNING("%s", FEATURE_NOT_AVAIL_STR("AUTH_ATTEMPTS")); + } + if (strlen(password) > 0) { + argv[argc++] = g_strdup("--force-password"); + argv[argc++] = g_strdup("--password"); + argv[argc++] = g_strdup_printf("%s", password); + } + } else if (!password) { + REMMINA_PLUGIN_CRITICAL("%s", FEATURE_NOT_AVAIL_STR("PASSWORD")); + return G_SOURCE_REMOVE; + } + + if (FEATURE_AVAILABLE(gpdata, "TERMINATE")) { + argv[argc++] = g_strdup("--terminate"); + argv[argc++] = g_strdup_printf("%s", session_id); + } else { + REMMINA_PLUGIN_CRITICAL("%s", FEATURE_NOT_AVAIL_STR("TERMINATE")); + return G_SOURCE_REMOVE; + } + + if (FEATURE_AVAILABLE(gpdata, "NON_INTERACTIVE")) { + argv[argc++] = g_strdup("--non-interactive"); + } else { + REMMINA_PLUGIN_WARNING("%s", FEATURE_NOT_AVAIL_STR("NON_INTERACTIVE")); + } + + if (FEATURE_AVAILABLE(gpdata, "SSH_PRIVKEY")) { + if (ssh_privatekey && !g_str_equal(ssh_privatekey, "")) { + argv[argc++] = g_strdup("--ssh-privkey"); + argv[argc++] = g_strdup_printf("%s", ssh_privatekey); + + if (ssh_passphrase && !g_str_equal(ssh_passphrase, "")) { + if (FEATURE_AVAILABLE(gpdata, "SSH_PASSPHRASE")) { + argv[argc++] = g_strdup("--ssh-passphrase"); + argv[argc++] = g_strdup_printf("%s", ssh_passphrase); + } else { + REMMINA_PLUGIN_MESSAGE("%s", FEATURE_NOT_AVAIL_STR("SSH_PASSPHRASE")); + } + } + } + } else { + REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("SSH_PRIVKEY")); + } + + argv[argc++] = NULL; + + GError* error = NULL; + gchar** envp = g_get_environ(); + rmplugin_x2go_spawn_pyhoca_process(argc, argv, &error, envp); + g_strfreev(envp); + + if (error) { + gchar *err_msg = g_strdup_printf( + _("Could not terminate X2Go session '%s':\n%s"), + session_id, + error->message + ); + + REMMINA_PLUGIN_CRITICAL("%s", err_msg); + + struct _DialogData *err_ddata = g_new0(struct _DialogData, 1); + err_ddata->parent = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(dialog))); + err_ddata->flags = GTK_DIALOG_MODAL; + err_ddata->type = GTK_MESSAGE_ERROR; + err_ddata->buttons = GTK_BUTTONS_OK; + err_ddata->title = _("An error occured."); + err_ddata->message = err_msg; + // We don't need the response. + err_ddata->callbackfunc = NULL; + // We don't need a custom dialog either. + err_ddata->dialog_factory_func = NULL; + err_ddata->dialog_factory_data = NULL; + + /* Prepare X2GoCustomUserData *custom_data + * gp -> gp (RemminaProtocolWidget*) + * dialog_data -> dialog data (struct _DialogData*) + */ + custom_data->gp = custom_data->gp; + custom_data->dialog_data = err_ddata; + custom_data->connect_data = NULL; + custom_data->opt1 = NULL; + custom_data->opt2 = NULL; + + IDLE_ADD((GSourceFunc) rmplugin_x2go_open_dialog, custom_data); + + // Too verbose: + // GtkTreeModel *model = gtk_tree_model_filter_get_model( + // GTK_TREE_MODEL_FILTER(filter)); + // gtk_tree_model_foreach(GTK_TREE_MODEL(model), (GtkTreeModelForeachFunc) + // rmplugin_x2go_dump_session_properties, dialog); + + // Set row visible again since we could not terminate the session. + if (!rmplugin_x2go_session_chooser_set_row_visible(selected_row, TRUE, + dialog)) { + // error message was already handled. + return G_SOURCE_REMOVE; + } + } + + return G_SOURCE_REMOVE; +} + +/** + * @brief Gets executed on dialog's 'response' signal + * + * @param custom_data X2GoCustomUserData*: \n + * gp -> gp (RemminaProtocolWidget*) \n + * dialog_data -> dialog data (struct _DialogData*) \n + * connect_data -> connection data (struct _ConnectionData*) + * @param response_id See GTK 'response' signal. + * @param self The dialog itself. + * + * @return gboolean, #G_SOURCE_CONTINUE and #G_SOURCE_REMOVE are more memorable + * names for the return value. See GLib docs. \n + * https://docs.gtk.org/glib/const.SOURCE_REMOVE.html + */ +static gboolean rmplugin_x2go_session_chooser_callback(X2GoCustomUserData* custom_data, + gint response_id, + GtkDialog *self) +{ + REMMINA_PLUGIN_DEBUG("Function entry."); + + if (!custom_data || !custom_data->gp || !custom_data->dialog_data || + !custom_data->connect_data) { + REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( + _("Internal error: %s"), + _("Parameter 'custom_data' is not initialized!") + )); + + return G_SOURCE_REMOVE; + } + RemminaProtocolWidget *gp = (RemminaProtocolWidget*) custom_data->gp; + + // Don't need to run other stuff, if the user just wants a new session. + // Also it can happen, that no session is there anymore which can be selected! + if (response_id == SESSION_CHOOSER_RESPONSE_NEW) { + REMMINA_PLUGIN_DEBUG("The user explicitly requested a new session. " + "Creating a new session…"); + SET_RESUME_SESSION(gp, NULL); + + // Unstucking main process. Telling it that a session has been selected. + // We use a trick here. As long as there is something other + // than 0 stored, a session is selected. So we use the gpointer as a gboolean. + SET_SESSION_SELECTED(gp, (gpointer) TRUE); + + gtk_widget_destroy(GTK_WIDGET(self)); + + return G_SOURCE_REMOVE; + } + + // This assumes that there are sessions which can be selected! + GValue value = rmplugin_x2go_session_chooser_get_property( + GTK_WIDGET(self), + SESSION_SESSION_ID, + NULL // Let the function search for the selected row. + ); + + // error message was handled already. + if (!G_VALUE_HOLDS_STRING(&value)) return G_SOURCE_REMOVE; + + gchar *session_id = (gchar*) g_value_get_string(&value); + + if (response_id == SESSION_CHOOSER_RESPONSE_CHOOSE) { + if (!session_id || strlen(session_id) <= 0) { + REMMINA_PLUGIN_DEBUG( + "%s", + _("Could not get session ID from session chooser dialog.") + ); + SET_RESUME_SESSION(gp, NULL); + } else { + SET_RESUME_SESSION(gp, session_id); + + REMMINA_PLUGIN_MESSAGE("%s", g_strdup_printf( + _("Resuming session: '%s'"), + session_id + )); + } + } else if (response_id == SESSION_CHOOSER_RESPONSE_TERMINATE) { + if (!session_id || strlen(session_id) <= 0) { + REMMINA_PLUGIN_DEBUG( + "%s", + _("Could not get session ID from session chooser dialog.") + ); + SET_RESUME_SESSION(gp, NULL); + } else { + SET_RESUME_SESSION(gp, session_id); + + REMMINA_PLUGIN_MESSAGE("%s", g_strdup_printf( + _("Terminating session: '%s'"), + session_id + )); + } + + GtkTreePath *path = rmplugin_x2go_session_chooser_get_selected_row( + GTK_WIDGET(self)); + // error message was already handled. + if (!path) return G_SOURCE_REMOVE; + + // Actually set row invisible. + if (!rmplugin_x2go_session_chooser_set_row_visible(path, FALSE, self)) { + // error message was already handled. + return G_SOURCE_REMOVE; + } + + /* Prepare X2GoCustomUserData *custom_data + * gp -> gp (RemminaProtocolWidget*) + * dialog_data -> dialog data (struct _DialogData*) + * connect_data -> connection data (struct _ConnectionData*) + * opt1 -> selected row (GtkTreePath*) + * opt2 -> session selection dialog (GtkDialog*) + */ + // everything else is already initialized. + custom_data->opt1 = path; + custom_data->opt2 = self; + + // Actually start pyhoca-cli process with --terminate $session_id. + g_thread_new("terminate-session-thread", + (GThreadFunc) rmplugin_x2go_pyhoca_terminate_session, + custom_data); + + // Dialog should stay open. + return G_SOURCE_CONTINUE; + } else { + REMMINA_PLUGIN_DEBUG("User clicked dialog away. " + "Creating a new session then."); + SET_RESUME_SESSION(gp, NULL); + } + + // Unstucking main process. Telling it that a session has been selected. + // We use a trick here. As long as there is something other + // than 0 stored, a session is selected. So we use the gpointer as a gboolean. + SET_SESSION_SELECTED(gp, (gpointer) TRUE); + + gtk_widget_destroy(GTK_WIDGET(self)); + + return G_SOURCE_REMOVE; +} + +#define RMPLUGIN_X2GO_FEATURE_GTKSOCKET 1 + +/* Forward declaration */ +static RemminaProtocolPlugin rmplugin_x2go; + +/* When more than one NX sessions is connecting in progress, we need this mutex and array + * to prevent them from stealing the same window ID. + */ +static pthread_mutex_t remmina_x2go_init_mutex; +static GArray *remmina_x2go_window_id_array; + +/* ------------- Support for execution on main thread of GTK functions ------------- */ +struct onMainThread_cb_data +{ + enum { FUNC_GTK_SOCKET_ADD_ID } func; + + GtkSocket* sk; + Window w; + + /* Mutex for thread synchronization */ + pthread_mutex_t mu; + /* Flag to catch cancellations */ + gboolean cancelled; +}; + +static gboolean onMainThread_cb(struct onMainThread_cb_data *d) +{ + TRACE_CALL(__func__); + if (!d->cancelled) { + switch (d->func) { + case FUNC_GTK_SOCKET_ADD_ID: + gtk_socket_add_id(d->sk, d->w); + break; + } + pthread_mutex_unlock(&d->mu); + } else { + /* thread has been cancelled, so we must free d memory here */ + g_free(d); + } + return G_SOURCE_REMOVE; +} + + +static void onMainThread_cleanup_handler(gpointer data) +{ + TRACE_CALL(__func__); + struct onMainThread_cb_data *d = data; + d->cancelled = TRUE; +} + +static void onMainThread_schedule_callback_and_wait(struct onMainThread_cb_data *d) +{ + TRACE_CALL(__func__); + d->cancelled = FALSE; + pthread_cleanup_push(onMainThread_cleanup_handler, d); + pthread_mutex_init(&d->mu, NULL); + pthread_mutex_lock(&d->mu); + gdk_threads_add_idle((GSourceFunc)onMainThread_cb, (gpointer) d); + + pthread_mutex_lock(&d->mu); + + pthread_cleanup_pop(0); + pthread_mutex_unlock(&d->mu); + pthread_mutex_destroy(&d->mu); +} + +static void onMainThread_gtk_socket_add_id(GtkSocket* sk, Window w) +{ + TRACE_CALL(__func__); + + struct onMainThread_cb_data *d; + + d = g_new0(struct onMainThread_cb_data, 1); + d->func = FUNC_GTK_SOCKET_ADD_ID; + d->sk = sk; + d->w = w; + + onMainThread_schedule_callback_and_wait(d); + g_free(d); +} +/* /-/-/-/-/-/-/ Support for execution on main thread of GTK functions /-/-/-/-/-/-/ */ + +static void rmplugin_x2go_remove_window_id (Window window_id) +{ + gint i; + gboolean already_seen = FALSE; + + pthread_mutex_lock(&remmina_x2go_init_mutex); + for (i = 0; i < remmina_x2go_window_id_array->len; i++) { + if (g_array_index(remmina_x2go_window_id_array, Window, i) == window_id) { + already_seen = TRUE; + REMMINA_PLUGIN_DEBUG("Window of X2Go Agent with ID [0x%lx] seen already.", + window_id); + break; + } + } + + if (already_seen) { + g_array_remove_index_fast(remmina_x2go_window_id_array, i); + REMMINA_PLUGIN_DEBUG("Forgetting about window of X2Go Agent with ID [0x%lx]…", + window_id); + } + + pthread_mutex_unlock(&remmina_x2go_init_mutex); +} + +/** + * @returns: FALSE. This source should be removed from main loop. + * #G_SOURCE_CONTINUE and #G_SOURCE_REMOVE are more memorable + * names for the return value. + */ +static gboolean rmplugin_x2go_cleanup(RemminaProtocolWidget *gp) +{ + REMMINA_PLUGIN_DEBUG("Function entry."); + + gchar *server; + gint port; + + RemminaFile *remminafile = rm_plugin_service->protocol_plugin_get_file(gp); + rm_plugin_service->get_server_port(rm_plugin_service->file_get_string(remminafile, "server"), + 22, + &server, + &port); + + REMMINA_PLUGIN_AUDIT(_("Disconnected from %s:%d via X2Go"), server, port); + g_free(server), server = NULL; + + RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp); + if (gpdata == NULL) { + REMMINA_PLUGIN_DEBUG("Exiting since gpdata is already 'NULL'…"); + return G_SOURCE_REMOVE; + } + + if (gpdata->thread) { + pthread_cancel(gpdata->thread); + if (gpdata->thread) pthread_join(gpdata->thread, NULL); + } + + if (gpdata->window_id) { + rmplugin_x2go_remove_window_id(gpdata->window_id); + } + + if (gpdata->pidx2go) { + kill(gpdata->pidx2go, SIGTERM); + g_spawn_close_pid(gpdata->pidx2go); + gpdata->pidx2go = 0; + } + + if (gpdata->display) { + XSetErrorHandler(gpdata->orig_handler); + XCloseDisplay(gpdata->display); + gpdata->display = NULL; + } + + g_object_steal_data(G_OBJECT(gp), "plugin-data"); + rm_plugin_service->protocol_plugin_signal_connection_closed(gp); + + return G_SOURCE_REMOVE; +} + +static gboolean rmplugin_x2go_close_connection(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp); + + REMMINA_PLUGIN_DEBUG("Function entry."); + + if (gpdata->disconnected) { + REMMINA_PLUGIN_DEBUG("Doing nothing since the plugin is already disconnected."); + return G_SOURCE_REMOVE; + } + + rmplugin_x2go_cleanup(gp); + + // Try again. + return G_SOURCE_CONTINUE; +} + +static void rmplugin_x2go_pyhoca_cli_exited(GPid pid, + gint status, + RemminaProtocolWidget *gp) +{ + REMMINA_PLUGIN_DEBUG("Function entry."); + + RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp); + if (!gpdata) { + REMMINA_PLUGIN_DEBUG("Doing nothing as the disconnection " + "has already been handled."); + return; + } + + if (gpdata->pidx2go <= 0) { + REMMINA_PLUGIN_DEBUG("Doing nothing since pyhoca-cli was expected to stop."); + return; + } + + REMMINA_PLUGIN_CRITICAL("%s", _("PyHoca-CLI exited unexpectedly. " + "This connection will now be closed.")); + + struct _DialogData *ddata = g_new0(struct _DialogData, 1); + ddata->parent = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(gp))); + ddata->flags = GTK_DIALOG_MODAL; + ddata->type = GTK_MESSAGE_ERROR; + ddata->buttons = GTK_BUTTONS_OK; + ddata->title = _("An error occured."); + ddata->message = _("The necessary child process 'pyhoca-cli' stopped unexpectedly.\n" + "Please check your profile settings and PyHoca-CLI's output for " + "possible errors. Also ensure the remote server is " + "reachable and you're using the right credentials."); + // We don't need the response. + ddata->callbackfunc = NULL; + // We don't need a custom dialog either. + ddata->dialog_factory_func = NULL; + ddata->dialog_factory_data = NULL; + + /* Prepare X2GoCustomUserData *custom_data + * gp -> gp (RemminaProtocolWidget*) + * dialog_data -> dialog data (struct _DialogData*) + */ + X2GoCustomUserData *custom_data = g_new0(X2GoCustomUserData, 1); + g_assert(custom_data && "custom_data could not be initialized."); + + custom_data->gp = gp; + custom_data->dialog_data = ddata; + custom_data->connect_data = NULL; + custom_data->opt1 = NULL; + + IDLE_ADD((GSourceFunc) rmplugin_x2go_open_dialog, custom_data); + + // 1 Second. Give `Dialog` chance to open. + usleep(1000 * 1000); + + rmplugin_x2go_close_connection(gp); +} + +/** + * @brief Saves s_password and s_username if set. + * @returns either TRUE or FALSE. If FALSE gets returned, `errmsg` is set. + */ +static gboolean rmplugin_x2go_save_credentials(RemminaFile* remminafile, + gchar* s_username, gchar* s_password, + gchar* errmsg) +{ + // User has requested to save credentials. We put all the new credentials + // into remminafile->settings. They will be saved later, on successful + // connection, by rcw.c + if (s_password && s_username) { + if (g_strcmp0(s_username, "") == 0) { + g_strlcpy(errmsg, _("Can't save empty username!"), 512); + //REMMINA_PLUGIN_CRITICAL("%s", errmsg); // No need. + return FALSE; + } + + // We allow the possibility to set an empty password because a X2Go + // session can be still made using keyfiles or similar. + rm_plugin_service->file_set_string(remminafile, "password", + s_password); + rm_plugin_service->file_set_string(remminafile, "username", + s_username); + } else { + g_strlcpy(errmsg, g_strdup_printf( + _("Internal error: %s"), + _("Could not save new credentials.") + ), 512); + + REMMINA_PLUGIN_CRITICAL("%s", _("Could not save " + "new credentials: 's_password' or " + "'s_username' strings were not set.")); + return FALSE; + } + + return TRUE; +} + + +/** + * @brief Asks the user for a username and password. + * + * @param errmsg Pointer to error message string (set if function failed). + * @param passphrase gchar** Passphrase which will be used to unlock SSH key. + * + * @returns FALSE if auth failed and TRUE on success. + */ +static gboolean rmplugin_x2go_get_ssh_passphrase(RemminaProtocolWidget *gp, gchar *errmsg, + gchar **passphrase) +{ + REMMINA_PLUGIN_DEBUG("Function entry."); + + g_assert(errmsg != NULL); + g_assert(gp != NULL); + + if ((*passphrase) == NULL) { + // Just setting NULL password to empty password. + (*passphrase) = g_strdup(""); + } + + gint ret = rm_plugin_service->protocol_plugin_init_auth( + gp, 0, _("Enter password to unlock the SSH key:"), + NULL, *passphrase, NULL, NULL + ); + + if (ret == GTK_RESPONSE_OK) { + gchar *s_passphrase = rm_plugin_service->protocol_plugin_init_get_password(gp); + if (s_passphrase) { + (*passphrase) = g_strdup(s_passphrase); + g_free(s_passphrase); + } + } else { + g_strlcpy(errmsg, _("Password input cancelled. Aborting…"), 512); + return FALSE; + } + + return TRUE; +} + + +/** + * @brief Asks the user for a username and password. + * + * @param errmsg Pointer to error message string (set if function failed). + * @param username Pointer to default username. Gets set to new username on success. + * @param password Pointer to default password. Gets set to new password on success. + * + * @returns FALSE if auth failed and TRUE on success. + */ +static gboolean rmplugin_x2go_get_auth(RemminaProtocolWidget *gp, gchar* errmsg, + gchar** default_username, gchar** default_password) +{ + REMMINA_PLUGIN_DEBUG("Function entry."); + + g_assert(errmsg != NULL); + g_assert(gp != NULL); + g_assert(default_username != NULL); + g_assert(default_password != NULL); + + // default_username is probably NULL because the user didn't configure any + // username in the profile settings. + if ((*default_username) == NULL) { + gchar* l_errmsg = g_strdup_printf( + _("Tip: Check the 'Save password' checkbox or manually input your " + "X2Go username and password in the profile settings to store " + "them for faster logins.") + ); + REMMINA_PLUGIN_MESSAGE("%s", l_errmsg); + (*default_username) = g_strdup(""); + } + + // default_password is probably NULL because something did go wrong at the + // secret-plugin. For example: The user didn't input a password for keyring or + // the user simply didn't configure a password in the profile settings. + if ((*default_password) == NULL) { + (*default_password) = g_strdup(""); + } + + gchar *s_username, *s_password; + gint ret; + gboolean save; + gboolean disable_password_storing; + RemminaFile *remminafile; + + remminafile = rm_plugin_service->protocol_plugin_get_file(gp); + + disable_password_storing = rm_plugin_service->file_get_int( + remminafile, "disablepasswordstoring", FALSE + ); + + ret = rm_plugin_service->protocol_plugin_init_auth( + gp, (disable_password_storing ? 0 : + REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD | + REMMINA_MESSAGE_PANEL_FLAG_USERNAME), + _("Enter X2Go credentials"), + (*default_username), (*default_password), NULL, NULL + ); + + if (ret == GTK_RESPONSE_OK) { + s_username = rm_plugin_service->protocol_plugin_init_get_username(gp); + s_password = rm_plugin_service->protocol_plugin_init_get_password(gp); + if (rm_plugin_service->protocol_plugin_init_get_savepassword(gp)) + rm_plugin_service->file_set_string( + remminafile, "password", s_password + ); + + // Should be renamed to protocol_plugin_init_get_savecredentials()?! + save = rm_plugin_service->protocol_plugin_init_get_savepassword(gp); + if (save) { + if (!rmplugin_x2go_save_credentials(remminafile, s_username, + s_password, errmsg)) { + return FALSE; + } + } + if (s_username) { + (*default_username) = g_strdup(s_username); + g_free(s_username); + } + if (s_password) { + (*default_password) = g_strdup(s_password); + g_free(s_password); + } + } else { + g_strlcpy(errmsg, _("Authentication cancelled. Aborting…"), 512); + return FALSE; + } + + return TRUE; +} + +/** + * @brief Executes 'pyhoca-cli --list-sessions' for username@host. + * + * @param gp RemminaProtocolWidget* is used to get the x2go-plugin data. + * @param error This is where a error message will be when NULL gets returned. + * @param connect_data struct _ConnectionData* which stores all necessary information + * needed for retrieving sessions from a X2Go server. + * + * @returns Standard output of pyhoca-cli command. + * If NULL then errmsg is set to user-friendly error message. + */ +static gchar* rmplugin_x2go_get_pyhoca_sessions(RemminaProtocolWidget* gp, GError **error, + struct _ConnectionData* connect_data) +{ + REMMINA_PLUGIN_DEBUG("Function entry."); + RemminaPluginX2GoData* gpdata = GET_PLUGIN_DATA(gp); + + gchar *host = NULL; + gchar *username = NULL; + gchar *password = NULL; + gchar *ssh_privatekey = NULL; + gchar *ssh_passphrase = NULL; + gboolean valid = rmplugin_x2go_verify_connection_data(connect_data); + + if (valid) { + if (connect_data->password) password = connect_data->password; + if (connect_data->ssh_privatekey) { + ssh_privatekey = connect_data->ssh_privatekey; + + if (connect_data->ssh_passphrase) { + ssh_passphrase = connect_data->ssh_passphrase; + } + } + + host = connect_data->host; + username = connect_data->username; + } else { + return G_SOURCE_REMOVE; + } + + // We will now start pyhoca-cli with only the '--list-sessions' option. + + gchar *argv[50]; + gint argc = 0; + + argv[argc++] = g_strdup("pyhoca-cli"); + argv[argc++] = g_strdup("--list-sessions"); + + argv[argc++] = g_strdup("--server"); // Not listed as feature. + argv[argc++] = g_strdup_printf("%s", host); + + if (FEATURE_AVAILABLE(gpdata, "USERNAME")) { + argv[argc++] = g_strdup("-u"); + if (username) { + argv[argc++] = g_strdup_printf("%s", username); + } else { + argv[argc++] = g_strdup_printf("%s", g_get_user_name()); + } + } else { + g_set_error(error, 1, 1, "%s", FEATURE_NOT_AVAIL_STR("USERNAME")); + REMMINA_PLUGIN_CRITICAL("%s", FEATURE_NOT_AVAIL_STR("USERNAME")); + return NULL; + } + + if (FEATURE_AVAILABLE(gpdata, "NON_INTERACTIVE")) { + argv[argc++] = g_strdup("--non-interactive"); + } else { + REMMINA_PLUGIN_WARNING("%s", FEATURE_NOT_AVAIL_STR("NON_INTERACTIVE")); + } + + if (password && FEATURE_AVAILABLE(gpdata, "PASSWORD")) { + if (FEATURE_AVAILABLE(gpdata, "AUTH_ATTEMPTS")) { + argv[argc++] = g_strdup("--auth-attempts"); + argv[argc++] = g_strdup_printf ("%i", 0); + } else { + REMMINA_PLUGIN_WARNING("%s", FEATURE_NOT_AVAIL_STR("AUTH_ATTEMPTS")); + } + if (strlen(password) > 0) { + argv[argc++] = g_strdup("--force-password"); + argv[argc++] = g_strdup("--password"); + argv[argc++] = g_strdup_printf("%s", password); + } + } else if (!password) { + g_set_error(error, 1, 1, "%s", FEATURE_NOT_AVAIL_STR("PASSWORD")); + REMMINA_PLUGIN_CRITICAL("%s", FEATURE_NOT_AVAIL_STR("PASSWORD")); + return NULL; + } + + // No need to catch feature-not-available error. + // `--quiet` is not that important. + if (FEATURE_AVAILABLE(gpdata, "QUIET")) { + argv[argc++] = g_strdup("--quiet"); + } + + if (FEATURE_AVAILABLE(gpdata, "SSH_PRIVKEY")) { + if (ssh_privatekey && !g_str_equal(ssh_privatekey, "")) { + argv[argc++] = g_strdup("--ssh-privkey"); + argv[argc++] = g_strdup_printf("%s", ssh_privatekey); + + if (ssh_passphrase && !g_str_equal(ssh_passphrase, "")) { + if (FEATURE_AVAILABLE(gpdata, "SSH_PASSPHRASE")) { + argv[argc++] = g_strdup("--ssh-passphrase"); + argv[argc++] = g_strdup_printf("%s", ssh_passphrase); + } else { + REMMINA_PLUGIN_MESSAGE("%s", FEATURE_NOT_AVAIL_STR("SSH_PASSPHRASE")); + } + } + } + } else { + REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("SSH_PRIVKEY")); + } + + + argv[argc++] = NULL; + + //#ifndef GLIB_AVAILABLE_IN_2_68 + gchar** envp = g_get_environ(); + gchar* envp_splitted = g_strjoinv(";", envp); + envp_splitted = g_strconcat(envp_splitted, ";LANG=C", (void*) NULL); + envp = g_strsplit(envp_splitted, ";", 0); + /* + * #else + * // Only available after glib version 2.68. + * // TODO: FIXME: NOT TESTED! + * GStrvBuilder* builder = g_strv_builder_new(); + * g_strv_builder_add(builder, "LANG=C"); + * GStrv envp = g_strv_builder_end(builder); + * #endif + */ + + gchar* std_out = rmplugin_x2go_spawn_pyhoca_process(argc, argv, error, envp); + g_strfreev(envp); + + if (!std_out || *error) { + // If no error is set but std_out is NULL + // then something is not right at all. + // Most likely the developer forgot to add an error message. Crash. + g_assert((*error) != NULL); + return NULL; + } + + return std_out; +} + +/** + * @brief This function is used to parse the output of + * rmplugin_x2go_get_pyhoca_sessions(). + * + * @param gp RemminaProtocolWidget* is used to get the x2go-plugin data. + * @param error This is where a error message will be when NULL gets returned. + * @param connect_data struct _ConnectionData* which stores all necessary information + * needed for retrieving sessions from a X2Go server. + * + * @returns Returns either a GList containing the IDs of every already existing session + * found or if the function failes, NULL. + * + * TODO: If pyhoca-cli (python-x2go) implements `--json` or similar option -> Replace + * entire function with JSON parsing. + */ +static GList* rmplugin_x2go_parse_pyhoca_sessions(RemminaProtocolWidget* gp, + GError **error, + struct _ConnectionData* connect_data) +{ + REMMINA_PLUGIN_DEBUG("Function entry."); + + gchar *pyhoca_output = NULL; + + pyhoca_output = rmplugin_x2go_get_pyhoca_sessions(gp, error, connect_data); + if (!pyhoca_output || *error) { + // If no error is set but pyhoca_output is NULL + // then something is not right at all. + // Most likely the developer forgot to add an error message. Crash. + g_assert((*error) != NULL); + + return NULL; + } + + gchar **lines_list = g_strsplit(pyhoca_output, "\n", -1); + // Assume at least two lines of output. + if (lines_list == NULL || lines_list[0] == NULL || lines_list[1] == NULL) { + g_set_error(error, 1, 1, "%s", _("Could not parse the output of PyHoca-CLI's " + "--list-sessions option. Creating a new " + "session now.")); + return NULL; + } + + gboolean found_session = FALSE; + GList* sessions = NULL; + gchar** session = NULL; + + for (guint i = 0; lines_list[i] != NULL; i++) { + gchar* current_line = lines_list[i]; + + // TOO VERBOSE: + //REMMINA_PLUGIN_DEBUG("pyhoca-cli: %s", current_line); + + // Hardcoded string "Session Name: " comes from python-x2go. + if (!g_str_has_prefix(current_line, "Session Name: ") && !found_session) { + // Doesn't begin with "Session Name: " and + // the current line doesn't come after that either. Skipping. + continue; + } + + if (g_str_has_prefix(current_line, "Session Name: ")) { + gchar* session_id = NULL; + gchar** line_list = g_strsplit(current_line, ": ", 0); + + if (line_list == NULL || + line_list[0] == NULL || + line_list[1] == NULL || + strlen(line_list[0]) <= 0 || + strlen(line_list[1]) <= 0) + { + found_session = FALSE; + continue; + } + + session = malloc(sizeof(gchar*) * (SESSION_NUM_PROPERTIES+1)); + if (!session) { + REMMINA_PLUGIN_CRITICAL("%s", _("Could not allocate " + "enough memory!")); + } + session[SESSION_NUM_PROPERTIES] = NULL; + sessions = g_list_append(sessions, session); + + session_id = line_list[1]; + session[SESSION_SESSION_ID] = session_id; + + REMMINA_PLUGIN_INFO("%s", g_strdup_printf( + _("Found already existing X2Go session with ID: '%s'"), + session[SESSION_SESSION_ID]) + ); + + found_session = TRUE; + continue; + } + + if (!found_session) { + continue; + } + + if (g_strcmp0(current_line, "-------------") == 0) { + continue; + } + + gchar* value = NULL; + gchar** line_list = g_strsplit(current_line, ": ", 0); + + if (line_list == NULL || + line_list[0] == NULL || + line_list[1] == NULL || + strlen(line_list[0]) <= 0 || + strlen(line_list[1]) <= 0) + { + // Probably the empty line at the end of every session. + found_session = FALSE; + continue; + } + value = line_list[1]; + + if (g_str_has_prefix(current_line, "cookie: ")) { + REMMINA_PLUGIN_DEBUG("cookie:\t'%s'", value); + session[SESSION_COOKIE] = value; + } else if (g_str_has_prefix(current_line, "agent PID: ")) { + REMMINA_PLUGIN_DEBUG("agent PID:\t'%s'", value); + session[SESSION_AGENT_PID] = value; + } else if (g_str_has_prefix(current_line, "display: ")) { + REMMINA_PLUGIN_DEBUG("display:\t'%s'", value); + session[SESSION_DISPLAY] = value; + } else if (g_str_has_prefix(current_line, "status: ")) { + if (g_strcmp0(value, "S") == 0) { + // TRANSLATORS: Please stick to X2GoClient's translation. + value = _("Suspended"); + } else if (g_strcmp0(value, "R") == 0) { + // TRANSLATORS: Please stick to X2GoClient's translation. + value = _("Running"); + } else if (g_strcmp0(value, "T") == 0) { + // TRANSLATORS: Please stick to X2GoClient's translation. + value = _("Terminated"); + } + REMMINA_PLUGIN_DEBUG("status:\t'%s'", value); + session[SESSION_STATUS] = value; + } else if (g_str_has_prefix(current_line, "graphic port: ")) { + REMMINA_PLUGIN_DEBUG("graphic port:\t'%s'", value); + session[SESSION_GRAPHIC_PORT] = value; + } else if (g_str_has_prefix(current_line, "snd port: ")) { + REMMINA_PLUGIN_DEBUG("snd port:\t'%s'", value); + session[SESSION_SND_PORT] = value; + } else if (g_str_has_prefix(current_line, "sshfs port: ")) { + REMMINA_PLUGIN_DEBUG("sshfs port:\t'%s'", value); + session[SESSION_SSHFS_PORT] = value; + } else if (g_str_has_prefix(current_line, "username: ")) { + REMMINA_PLUGIN_DEBUG("username:\t'%s'", value); + session[SESSION_USERNAME] = value; + } else if (g_str_has_prefix(current_line, "hostname: ")) { + REMMINA_PLUGIN_DEBUG("hostname:\t'%s'", value); + session[SESSION_HOSTNAME] = value; + } else if (g_str_has_prefix(current_line, "create date: ")) { + REMMINA_PLUGIN_DEBUG("create date:\t'%s'", value); + session[SESSION_CREATE_DATE] = value; + } else if (g_str_has_prefix(current_line, "suspended since: ")) { + REMMINA_PLUGIN_DEBUG("suspended since:\t'%s'", value); + session[SESSION_SUSPENDED_SINCE] = value; + } else { + REMMINA_PLUGIN_DEBUG("Not supported:\t'%s'", value); + found_session = FALSE; + } + } + + if (!sessions) { + g_set_error(error, 1, 1, + "%s", _("Could not find any sessions on remote machine. Creating a new " + "session now.") + ); + + // returning NULL with `error` set. + } + + return sessions; +} + +/** + * @brief Asks the user, with the help of a dialog, to continue an already existing + * session, terminate or create a new one. + * + * @param error Is set if there is something to tell the user. \n + * Not necessarily an *error* message. + * @param connect_data Stores all necessary information needed for + * etrieving sessions from a X2Go server. + * @return gchar* ID of session. Can be 'NULL' but then 'error' is set. + */ +static gchar* rmplugin_x2go_ask_session(RemminaProtocolWidget *gp, GError **error, + struct _ConnectionData* connect_data) +{ + if (!connect_data || + !connect_data->host || + !connect_data->username || + !connect_data->password || + strlen(connect_data->host) <= 0 || + strlen(connect_data->username) <= 0) + // Allow empty passwords. Maybe the user wants to connect via public key? + { + g_set_error(error, 1, 1, "%s", g_strdup_printf( + _("Internal error: %s"), + _("'Invalid connection data.'") + )); + return NULL; + } + + GList *sessions_list = NULL; + sessions_list = rmplugin_x2go_parse_pyhoca_sessions(gp, error, connect_data); + + if (!sessions_list || *error) { + // If no error is set but sessions_list is NULL + // then something is not right at all. + // Most likely the developer forgot to add an error message. Crash. + g_assert(*error != NULL); + return NULL; + } + + // Prep new DialogData struct. + struct _DialogData *ddata = g_new0(struct _DialogData, 1); + ddata->parent = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(gp))); + ddata->flags = GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT; + //ddata->type = GTK_MESSAGE_QUESTION; + //ddata->buttons = GTK_BUTTONS_OK; // Doesn't get used in our custom factory. + ddata->title = _("Choose a session to resume:"); + ddata->message = ""; + + // gboolean factory(X2GoCustomUserData*, gpointer) + // X2GoCustomUserData*: + // gp -> gp (RemminaProtocolWidget*) + // dialog_data -> dialog data (struct _DialogData*) + // connect_data -> connection data (struct _ConnectionData*) + // gpointer: dialog_factory_data + ddata->callbackfunc = G_CALLBACK(rmplugin_x2go_session_chooser_callback); + + // gboolean factory(X2GoCustomUserData*, gpointer) + // X2GoCustomUserData*: + // gp -> gp (RemminaProtocolWidget*) + // dialog_data -> dialog data (struct _DialogData*) + // connect_data -> connection data (struct _ConnectionData*) + // gpointer: dialog_factory_data + ddata->dialog_factory_data = sessions_list; + ddata->dialog_factory_func = G_CALLBACK(rmplugin_x2go_choose_session_dialog_factory); + + /* Prepare X2GoCustomUserData *custom_data + * gp -> gp (RemminaProtocolWidget*) + * dialog_data -> dialog data (struct _DialogData*) + */ + X2GoCustomUserData *custom_data = g_new0(X2GoCustomUserData, 1); + g_assert(custom_data && "custom_data could not be initialized."); + + custom_data->gp = gp; + custom_data->dialog_data = ddata; + custom_data->connect_data = connect_data; + custom_data->opt1 = NULL; + + // Open dialog here. Dialog rmplugin_x2go_session_chooser_callback (callbackfunc) + // should set SET_RESUME_SESSION. + IDLE_ADD((GSourceFunc)rmplugin_x2go_open_dialog, custom_data); + + guint counter = 0; + while (!IS_SESSION_SELECTED(gp)) { + // 0.5 Seconds. Give dialog chance to open. + usleep(500 * 1000); + + // Every 5 seconds + if (counter % 10 == 0 || counter == 0) { + REMMINA_PLUGIN_MESSAGE("%s", _("Waiting for user to select a session…")); + } + counter++; + } + + gchar* chosen_resume_session = GET_RESUME_SESSION(gp); + + if (!chosen_resume_session || strlen(chosen_resume_session) <= 0) { + g_set_error(error, 1, 1, "%s", _("No session was selected. Creating a new one.")); + return NULL; + } + + return chosen_resume_session; +} + +static gboolean rmplugin_x2go_exec_x2go(gchar *host, + gint sshport, + gchar *username, + gchar *password, + gchar *command, + gchar *kbdlayout, + gchar *kbdtype, + gchar *audio, + gchar *clipboard, + gint dpi, + gchar *resolution, + gchar *ssh_privatekey, + RemminaProtocolWidget *gp, + gchar *errmsg) +{ + TRACE_CALL(__func__); + RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp); + + gchar *argv[50]; + gint argc = 0; + + // We don't want to save any SSH passphrases on hard drive! + // Thats why we will always ask if needed. + gchar *ssh_passphrase = NULL; + + if (!username || strlen(username) <= 0) { + // Sets `username` and `password`. + if (!rmplugin_x2go_get_auth(gp, errmsg, &username, &password)) { + return FALSE; + } + } + + // Password can be *empty* but not NULL. + if (!password) { + password = g_strdup(""); + } + + if (ssh_privatekey && strlen(ssh_privatekey) > 0) { + // FIXME: Check if file exists and is legit private key. + // See: https://security.stackexchange.com/a/245767 + + // Get ssh_privatekey now via dialog. + if (!rmplugin_x2go_get_ssh_passphrase(gp, errmsg, &ssh_passphrase)) { + return FALSE; + } + } + + struct _ConnectionData* connect_data = g_new0(struct _ConnectionData, 1); + connect_data->host = host; + connect_data->username = username; + connect_data->password = password; + connect_data->ssh_privatekey = ssh_privatekey; + connect_data->ssh_passphrase = ssh_passphrase; + + GError *session_error = NULL; + gchar* resume_session_id = rmplugin_x2go_ask_session(gp, &session_error, + connect_data); + + if (!resume_session_id || session_error || strlen(resume_session_id) <= 0) { + // If no error is set but session_id is NULL + // then something is not right at all. + // Most likely the developer forgot to add an error message. Crash. + g_assert(session_error != NULL); + + REMMINA_PLUGIN_WARNING("%s", g_strdup_printf( + _("A non-critical error happened: %s"), + session_error->message + )); + } else { + REMMINA_PLUGIN_INFO("%s", g_strdup_printf( + _("User chose to resume session with ID: '%s'"), + resume_session_id + )); + } + + argc = 0; + argv[argc++] = g_strdup("pyhoca-cli"); + + argv[argc++] = g_strdup("--server"); // Not listed as feature. + argv[argc++] = g_strdup_printf ("%s", host); + + if (FEATURE_AVAILABLE(gpdata, "REMOTE_SSH_PORT")) { + argv[argc++] = g_strdup("-p"); + argv[argc++] = g_strdup_printf ("%d", sshport); + } else { + REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("REMOTE_SSH_PORT")); + } + + if (resume_session_id && strlen(resume_session_id) > 0) { + REMMINA_PLUGIN_INFO("%s", g_strdup_printf( + // TRANSLATORS: Please stick to X2GoClient's way of translating. + _("Resuming session '%s'…"), + resume_session_id + )); + + if (FEATURE_AVAILABLE(gpdata, "RESUME")) { + argv[argc++] = g_strdup("--resume"); + argv[argc++] = g_strdup_printf("%s", resume_session_id); + } else { + REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("RESUME")); + } + } + + // Deprecated. The user either wants to continue a + // session or just not. No inbetween. + // if (!resume_session_id) { + // if (FEATURE_AVAILABLE(gpdata, "TRY_RESUME")) { + // argv[argc++] = g_strdup("--try-resume"); + // } else { + // REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("TRY_RESUME")); + // } + // } + + if (FEATURE_AVAILABLE(gpdata, "USERNAME")) { + argv[argc++] = g_strdup("-u"); + if (username){ + argv[argc++] = g_strdup_printf ("%s", username); + } else { + argv[argc++] = g_strdup_printf ("%s", g_get_user_name()); + } + } else { + REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("USERNAME")); + } + + if (password && FEATURE_AVAILABLE(gpdata, "PASSWORD")) { + if (strlen(password) > 0) { + argv[argc++] = g_strdup("--force-password"); + argv[argc++] = g_strdup("--password"); + argv[argc++] = g_strdup_printf ("%s", password); + } + } else { + REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("PASSWORD")); + } + + if (FEATURE_AVAILABLE(gpdata, "AUTH_ATTEMPTS")) { + argv[argc++] = g_strdup("--auth-attempts"); + argv[argc++] = g_strdup_printf ("%i", 0); + } else { + REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("AUTH_ATTEMPTS")); + } + + if (FEATURE_AVAILABLE(gpdata, "NON_INTERACTIVE")) { + argv[argc++] = g_strdup("--non-interactive"); + } else { + REMMINA_PLUGIN_WARNING("%s", FEATURE_NOT_AVAIL_STR("NON_INTERACTIVE")); + } + + if (FEATURE_AVAILABLE(gpdata, "COMMAND")) { + argv[argc++] = g_strdup("-c"); + // FIXME: pyhoca-cli is picky about multiple quotes around + // the command string... + // argv[argc++] = g_strdup_printf ("%s", g_shell_quote(command)); + argv[argc++] = g_strdup(command); + } else { + REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("COMMAND")); + } + + if (FEATURE_AVAILABLE(gpdata, "KBD_LAYOUT")) { + if (kbdlayout) { + argv[argc++] = g_strdup("--kbd-layout"); + argv[argc++] = g_strdup_printf ("%s", kbdlayout); + } else { + argv[argc++] = g_strdup("--kbd-layout"); + argv[argc++] = g_strdup("auto"); + } + } else { + REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("KBD_LAYOUT")); + } + + if (FEATURE_AVAILABLE(gpdata, "KBD_TYPE")) { + if (kbdtype) { + argv[argc++] = g_strdup("--kbd-type"); + argv[argc++] = g_strdup_printf ("%s", kbdtype); + } else { + argv[argc++] = g_strdup("--kbd-type"); + argv[argc++] = g_strdup("auto"); + } + } else { + REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("KBD_TYPE")); + } + + if (FEATURE_AVAILABLE(gpdata, "GEOMETRY")) { + if (!resolution) + resolution = "800x600"; + argv[argc++] = g_strdup("-g"); + argv[argc++] = g_strdup_printf ("%s", resolution); + } else { + REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("GEOMETRY")); + } + + if (FEATURE_AVAILABLE(gpdata, "TERMINATE_ON_CTRL_C")) { + argv[argc++] = g_strdup("--terminate-on-ctrl-c"); + } else { + REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("TERMINATE_ON_CTRL_C")); + } + + if (FEATURE_AVAILABLE(gpdata, "SOUND")) { + if (audio) { + argv[argc++] = g_strdup("--sound"); + argv[argc++] = g_strdup_printf ("%s", audio); + } else { + argv[argc++] = g_strdup("--sound"); + argv[argc++] = g_strdup("none"); + } + } else { + REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("SOUND")); + } + + if (FEATURE_AVAILABLE(gpdata, "CLIPBOARD_MODE")) { + if (clipboard) { + argv[argc++] = g_strdup("--clipboard-mode"); + argv[argc++] = g_strdup_printf("%s", clipboard); + } + } else { + REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("CLIPBOARD_MODE")); + } + + if (FEATURE_AVAILABLE(gpdata, "DPI")) { + // Even though we validate the users input in the Remmina Editor, + // manipulating profile files is still very possible… + // Values are extracted from pyhoca-cli. + if (dpi < 20 || dpi > 400) { + g_strlcpy(errmsg, _("DPI setting is out of bounds. Please adjust " + "it in profile settings."), 512); + // No need, start_session() will handle output. + //REMMINA_PLUGIN_CRITICAL("%s", errmsg); + return FALSE; + } + argv[argc++] = g_strdup("--dpi"); + argv[argc++] = g_strdup_printf ("%i", dpi); + } else { + REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("DPI")); + } + + if (FEATURE_AVAILABLE(gpdata, "SSH_PRIVKEY")) { + if (ssh_privatekey && !g_str_equal(ssh_privatekey, "")) { + argv[argc++] = g_strdup("--ssh-privkey"); + argv[argc++] = g_strdup_printf("%s", ssh_privatekey); + + if (ssh_passphrase && !g_str_equal(ssh_passphrase, "")) { + if (FEATURE_AVAILABLE(gpdata, "SSH_PASSPHRASE")) { + argv[argc++] = g_strdup("--ssh-passphrase"); + argv[argc++] = g_strdup_printf("%s", ssh_passphrase); + } else { + REMMINA_PLUGIN_MESSAGE("%s", FEATURE_NOT_AVAIL_STR("SSH_PASSPHRASE")); + } + } + } + } else { + REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("SSH_PRIVKEY")); + } + + argv[argc++] = NULL; + + GError *error = NULL; + gchar **envp = g_get_environ(); + gboolean success = g_spawn_async_with_pipes (NULL, argv, envp, + (G_SPAWN_DO_NOT_REAP_CHILD | + G_SPAWN_SEARCH_PATH), NULL, + NULL, &gpdata->pidx2go, + NULL, NULL, NULL, &error); + + REMMINA_PLUGIN_INFO("%s", _("Started PyHoca-CLI with the following arguments:")); + // Print every argument except passwords. Free all arg strings. + for (gint i = 0; i < argc - 1; i++) { + gchar* curr_arg = argv[i]; + + if (g_str_equal(curr_arg, "--password") || + g_str_equal(curr_arg, "--ssh-passphrase")) { + g_printf("%s ", curr_arg); + g_printf("XXXXXX "); + g_free(curr_arg); + g_free(argv[++i]); + continue; + } else { + g_printf("%s ", curr_arg); + g_free(curr_arg); + } + } + g_printf("\n"); + + if (!success || error) { + // TRANSLATORS: Meta-error. Shouldn't be visible. + if (!error) error = g_error_new(0, 0, _("Internal error.")); + + gchar *error_title = _("Could not start X2Go session…"); + + struct _DialogData* ddata = g_new0(struct _DialogData, 1); + ddata->parent = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(gp))); + ddata->flags = GTK_DIALOG_MODAL; + ddata->type = GTK_MESSAGE_ERROR; + ddata->buttons = GTK_BUTTONS_OK; + ddata->title = _("Could not start X2Go session."); + ddata->message = g_strdup_printf(_("Could not start PyHoca-CLI (%i): '%s'"), + error->code, + error->message); + // We don't need the response. + ddata->callbackfunc = NULL; + // We don't need a custom dialog either. + ddata->dialog_factory_func = NULL; + ddata->dialog_factory_data = NULL; + + /* Prepare X2GoCustomUserData *custom_data + * gp -> gp (RemminaProtocolWidget*) + * dialog_data -> dialog data (struct _DialogData*) + */ + X2GoCustomUserData *custom_data = g_new0(X2GoCustomUserData, 1); + g_assert(custom_data && "Could not initialise Custom_data."); + + custom_data->gp = gp; + custom_data->dialog_data = ddata; + custom_data->connect_data = NULL; + custom_data->opt1 = NULL; + + IDLE_ADD((GSourceFunc) rmplugin_x2go_open_dialog, custom_data); + + g_strlcpy(errmsg, error_title, 512); + + // No need to output here. rmplugin_x2go_start_session will do this. + + g_error_free(error); + + return FALSE; + } + + // Prevent a race condition where pyhoca-cli is not + // started yet (pidx2go == 0) but a watcher is added. + + struct timespec ts; + // 0.001 seconds. + ts.tv_nsec = 1 * 1000 * 1000; + ts.tv_sec = 0; + while (gpdata->pidx2go == 0) { + nanosleep(&ts, NULL); + REMMINA_PLUGIN_DEBUG("Waiting for PyHoca-CLI to start…"); + }; + + REMMINA_PLUGIN_DEBUG("Watching child 'pyhoca-cli' process now…"); + g_child_watch_add(gpdata->pidx2go, + (GChildWatchFunc) rmplugin_x2go_pyhoca_cli_exited, + gp); + + return TRUE; +} + +/** + * @returns a GList* with all features which pyhoca-cli had before the feature system. + */ +static GList* rmplugin_x2go_old_pyhoca_features() +{ + REMMINA_PLUGIN_DEBUG("Function entry."); + + #define AMOUNT_FEATURES 43 + gchar* features[AMOUNT_FEATURES] = { + "ADD_TO_KNOWN_HOSTS", "AUTH_ATTEMPTS", "BROKER_PASSWORD", "BROKER_URL", + "CLEAN_SESSIONS", "COMMAND", "DEBUG", "FORCE_PASSWORD", "FORWARD_SSHAGENT", + "GEOMETRY", "KBD_LAYOUT", "KBD_TYPE", "LIBDEBUG", "LIBDEBUG_SFTPXFER", "LINK", + "LIST_CLIENT_FEATURES", "LIST_DESKTOPS", "LIST_SESSIONS", "NEW", "PACK", + "PASSWORD", "PDFVIEW_CMD", "PRINTER", "PRINTING", "PRINT_ACTION", "PRINT_CMD", + "QUIET", "REMOTE_SSH_PORT", "RESUME", "SAVE_TO_FOLDER", "SESSION_PROFILE", + "SESSION_TYPE", "SHARE_DESKTOP", "SHARE_LOCAL_FOLDERS", "SHARE_MODE", "SOUND", + "SSH_PRIVKEY", "SUSPEND", "TERMINATE", "TERMINATE_ON_CTRL_C", "TRY_RESUME", + "USERNAME", "XINERAMA" + }; + + GList *features_list = NULL; + for (int i = 0; i < AMOUNT_FEATURES; i++) { + features_list = g_list_append(features_list, features[i]); + } + + return features_list; +} + +/** + * @returns a GList* which includes all pyhoca-cli command line features we can use. + */ +static GList* rmplugin_x2go_populate_available_features_list() +{ + REMMINA_PLUGIN_DEBUG("Function entry."); + + GList* returning_glist = NULL; + + // We will now start pyhoca-cli with only the '--list-cmdline-features' option + // and depending on the exit code and standard output we will determine if some + // features are available or not. + + gchar* argv[50]; + gint argc = 0; + + argv[argc++] = g_strdup("pyhoca-cli"); + argv[argc++] = g_strdup("--list-cmdline-features"); + argv[argc++] = NULL; + + GError* error = NULL; // Won't be actually used. + + // Querying pyhoca-cli's command line features. + gchar** envp = g_get_environ(); + gchar* features_string = rmplugin_x2go_spawn_pyhoca_process(argc, argv, + &error, envp); + g_strfreev(envp); + + if (!features_string || error) { + // We added the '--list-cmdline-features' on commit 17d1be1319ba6 of + // pyhoca-cli. In order to protect setups which don't have the newest + // version of pyhoca-cli available yet we artificially create a list + // of an old limited set of features. + + REMMINA_PLUGIN_WARNING("%s", + _("Could not get PyHoca-CLI's command-line features. This " + "indicates it is either too old, or not installed. " + "An old limited set of features will be used for now.")); + + return rmplugin_x2go_old_pyhoca_features(); + } else { + gchar **features_list = g_strsplit(features_string, "\n", 0); + + if (features_list == NULL) { + gchar *error_msg = _("Could not parse PyHoca-CLI's command-line " + "features. Using a limited feature-set for now."); + REMMINA_PLUGIN_WARNING("%s", error_msg); + return rmplugin_x2go_old_pyhoca_features(); + } + + REMMINA_PLUGIN_INFO("%s", _("Retrieved the following PyHoca-CLI " + "command-line features:")); + + for(int k = 0; features_list[k] != NULL; k++) { + // Filter out empty strings + if (strlen(features_list[k]) <= 0) continue; + + REMMINA_PLUGIN_INFO("%s", + g_strdup_printf(_("Available feature[%i]: '%s'"), + k+1, features_list[k])); + returning_glist = g_list_append(returning_glist, features_list[k]); + } + return returning_glist; + } +} + +static void rmplugin_x2go_on_plug_added(GtkSocket *socket, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + + gchar *server; + gint port; + + RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp); + REMMINA_PLUGIN_DEBUG("Socket %d", gpdata->socket_id); + rm_plugin_service->protocol_plugin_signal_connection_opened(gp); + + RemminaFile *remminafile = rm_plugin_service->protocol_plugin_get_file(gp); + rm_plugin_service->get_server_port(rm_plugin_service->file_get_string(remminafile, "server"), + 22, + &server, + &port); + + REMMINA_PLUGIN_AUDIT(_("Connected to %s:%d via X2Go"), server, port); + g_free(server), server = NULL; + + return; +} + +static gboolean rmplugin_x2go_on_plug_removed(GtkSocket *socket, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + REMMINA_PLUGIN_DEBUG("Function entry."); + rmplugin_x2go_close_connection(gp); + return G_SOURCE_CONTINUE; +} + +static void rmplugin_x2go_init(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + REMMINA_PLUGIN_DEBUG("Function entry.", PLUGIN_NAME); + RemminaPluginX2GoData *gpdata; + + gpdata = g_new0(RemminaPluginX2GoData, 1); + g_object_set_data_full(G_OBJECT(gp), "plugin-data", gpdata, g_free); + + if (!rm_plugin_service->gtksocket_available()) { + /* report this in open_connection, not reportable here... */ + return; + } + + GList* available_features = rmplugin_x2go_populate_available_features_list(); + + // available_features can't be NULL cause if it fails, it gets populated with an + // old standard feature set. + gpdata->available_features = available_features; + + gpdata->socket_id = 0; + gpdata->thread = 0; + + gpdata->display = NULL; + gpdata->window_id = 0; + gpdata->pidx2go = 0; + gpdata->orig_handler = NULL; + + gpdata->socket = gtk_socket_new(); + rm_plugin_service->protocol_plugin_register_hostkey(gp, gpdata->socket); + gtk_widget_show(gpdata->socket); + + g_signal_connect(G_OBJECT(gpdata->socket), "plug-added", + G_CALLBACK(rmplugin_x2go_on_plug_added), gp); + g_signal_connect(G_OBJECT(gpdata->socket), "plug-removed", + G_CALLBACK(rmplugin_x2go_on_plug_removed), gp); + gtk_container_add(GTK_CONTAINER(gp), gpdata->socket); +} + +static gboolean rmplugin_x2go_try_window_id(Window window_id) +{ + TRACE_CALL(__func__); + gint i; + gboolean already_seen = FALSE; + + REMMINA_PLUGIN_DEBUG("Check if the window of X2Go Agent with ID [0x%lx] is already known or if " + "it needs registration", window_id); + + pthread_mutex_lock(&remmina_x2go_init_mutex); + for (i = 0; i < remmina_x2go_window_id_array->len; i++) { + if (g_array_index(remmina_x2go_window_id_array, Window, i) == window_id) { + already_seen = TRUE; + REMMINA_PLUGIN_DEBUG("Window of X2Go Agent with ID [0x%lx] " + "already seen.", window_id); + break; + } + } + if (!already_seen) { + g_array_append_val(remmina_x2go_window_id_array, window_id); + REMMINA_PLUGIN_DEBUG("Registered new window for X2Go Agent with " + "ID [0x%lx].", window_id); + } + pthread_mutex_unlock(&remmina_x2go_init_mutex); + + return (!already_seen); +} + +static int rmplugin_x2go_dummy_handler(Display *dsp, XErrorEvent *err) +{ + TRACE_CALL(__func__); + return 0; +} + +static gboolean rmplugin_x2go_start_create_notify(RemminaProtocolWidget *gp, gchar *errmsg) +{ + TRACE_CALL(__func__); + RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp); + + gpdata->display = XOpenDisplay(gdk_display_get_name(gdk_display_get_default())); + if (gpdata->display == NULL) { + g_strlcpy(errmsg, _("Could not open X11 DISPLAY."), 512); + return FALSE; + } + + gpdata->orig_handler = XSetErrorHandler(rmplugin_x2go_dummy_handler); + + XSelectInput(gpdata->display, + XDefaultRootWindow(gpdata->display), + SubstructureNotifyMask); + + REMMINA_PLUGIN_DEBUG("X11 event-watcher created."); + + return TRUE; +} + +static gboolean rmplugin_x2go_monitor_create_notify(RemminaProtocolWidget *gp, + const gchar *cmd, + gchar *errmsg) +{ + TRACE_CALL(__func__); + RemminaPluginX2GoData *gpdata; + + gboolean agent_window_found = FALSE; + Atom atom; + XEvent xev; + Window w; + Atom type; + int format; + unsigned long nitems, rest; + unsigned char *data = NULL; + + guint16 non_createnotify_count = 0; + + struct timespec ts; + // wait_amount * ts.tv_nsec = 20s + // 100 * 0.2s = 20s + int wait_amount = 100; + + CANCEL_DEFER + + REMMINA_PLUGIN_DEBUG("%s", _("Waiting for window of X2Go Agent to appear…")); + + gpdata = GET_PLUGIN_DATA(gp); + atom = XInternAtom(gpdata->display, "WM_COMMAND", True); + if (atom == None) { + CANCEL_ASYNC + return FALSE; + } + + ts.tv_sec = 0; + // 0.2s = 200000000ns + ts.tv_nsec = 200000000; + + while (wait_amount > 0) { + pthread_testcancel(); + if (!(gpdata->pidx2go > 0)) { + nanosleep(&ts, NULL); + REMMINA_PLUGIN_DEBUG("Waiting for X2Go session to start…"); + continue; + } + + while (!XPending(gpdata->display)) { + nanosleep(&ts, NULL); + wait_amount--; + // Don't spam the console. Print every second though. + if (wait_amount % 5 == 0) { + REMMINA_PLUGIN_INFO("%s", _("Waiting for PyHoca-CLI to " + "show the session's window…")); + } + continue; + } + + XNextEvent(gpdata->display, &xev); + // Just ignore non CreatNotify events. + if (xev.type != CreateNotify) { + non_createnotify_count++; + if (non_createnotify_count % 5 == 0) { + REMMINA_PLUGIN_DEBUG("Saw '%i' X11 events, which weren't " + "CreateNotify.", non_createnotify_count); + } + continue; + } + + w = xev.xcreatewindow.window; + if (XGetWindowProperty(gpdata->display, w, atom, 0, 255, False, + AnyPropertyType, &type, &format, &nitems, &rest, + &data) != Success) { + REMMINA_PLUGIN_DEBUG("Could not get WM_COMMAND property from X11 " + "window ID [0x%lx].", w); + continue; + } + + if (data) { + REMMINA_PLUGIN_DEBUG("Saw '%i' X11 events, which weren't " + "CreateNotify.", non_createnotify_count); + REMMINA_PLUGIN_DEBUG("Found X11 window with WM_COMMAND set " + "to '%s', the window ID is [0x%lx].", + (char*)data, w); + } + if (data && g_strrstr((gchar*)data, cmd) && + rmplugin_x2go_try_window_id(w)) { + gpdata->window_id = w; + agent_window_found = TRUE; + XFree(data); + break; + } + if (data) + XFree(data); + } + + XSetErrorHandler(gpdata->orig_handler); + XCloseDisplay(gpdata->display); + gpdata->display = NULL; + + CANCEL_ASYNC + + if (!agent_window_found) { + g_strlcpy(errmsg, _("No X2Go session window appeared. " + "Something went wrong…"), 512); + return FALSE; + } + + return TRUE; +} + +static gboolean rmplugin_x2go_start_session(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + REMMINA_PLUGIN_DEBUG("Function entry."); + + RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp);; + RemminaFile *remminafile; + const gchar errmsg[512] = {0}; + gboolean ret = TRUE; + + gchar *servstr, *host, *username, *password, *command, *kbdlayout, *kbdtype, + *audio, *clipboard, *res, *ssh_privatekey; + gint sshport, dpi; + GdkDisplay *default_dsp; + gint width, height; + + // We save the X Display name (:0) as we will need to synchronize the clipboards + default_dsp = gdk_display_get_default(); + const gchar *default_dsp_name = gdk_display_get_name(default_dsp); + REMMINA_PLUGIN_DEBUG("Default display is '%s'.", default_dsp_name); + + remminafile = rm_plugin_service->protocol_plugin_get_file(gp); + + servstr = GET_PLUGIN_STRING("server"); + if (servstr) { + rm_plugin_service->get_server_port(servstr, 22, &host, &sshport); + } else { + return FALSE; + } + + if (!sshport) sshport=22; + + username = GET_PLUGIN_STRING("username"); + password = GET_PLUGIN_PASSWORD("password"); + + command = GET_PLUGIN_STRING("command"); + if (!command) command = "TERMINAL"; + + kbdlayout = GET_PLUGIN_STRING("kbdlayout"); + kbdtype = GET_PLUGIN_STRING("kbdtype"); + + audio = GET_PLUGIN_STRING("audio"); + + clipboard = GET_PLUGIN_STRING("clipboard"); + + dpi = GET_PLUGIN_INT("dpi", 80); + + ssh_privatekey = GET_PLUGIN_STRING("ssh_privatekey"); + + // If empty set to NULL + if(ssh_privatekey && g_str_equal(ssh_privatekey, "")) { + ssh_privatekey = NULL; + } + + width = rm_plugin_service->get_profile_remote_width(gp); + height = rm_plugin_service->get_profile_remote_height(gp); + /* multiple of 4 */ + width = (width + 3) & ~0x3; + height = (height + 3) & ~0x3; + if ((width > 0) && (height > 0)) { + res = g_strdup_printf ("%dx%d", width, height); + } else { + res = "800x600"; + } + REMMINA_PLUGIN_DEBUG("Resolution set by user: '%s'.", res); + + REMMINA_PLUGIN_DEBUG("Attached window to socket '%d'.", gpdata->socket_id); + + /* register for notifications of window creation events */ + if (ret) ret = rmplugin_x2go_start_create_notify(gp, (gchar*)&errmsg); + + /* trigger the session start, session window should appear soon after this */ + if (ret) ret = rmplugin_x2go_exec_x2go(host, sshport, username, password, command, + kbdlayout, kbdtype, audio, clipboard, dpi, + res, ssh_privatekey, gp, + (gchar*)&errmsg); + + /* get the window ID of the remote x2goagent */ + if (ret) ret = rmplugin_x2go_monitor_create_notify(gp, "x2goagent", + (gchar*)&errmsg); + + if (!ret) { + REMMINA_PLUGIN_CRITICAL("%s", errmsg); + rm_plugin_service->protocol_plugin_set_error(gp, "%s", &errmsg); + return FALSE; + } + + /* embed it */ + onMainThread_gtk_socket_add_id(GTK_SOCKET(gpdata->socket), gpdata->window_id); + + return TRUE; +} + +static gboolean rmplugin_x2go_main(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp); + gboolean ret = FALSE; + + ret = rmplugin_x2go_start_session(gp); + + gpdata->thread = 0; + return ret; +} + +static gpointer rmplugin_x2go_main_thread(RemminaProtocolWidget* gp) +{ + TRACE_CALL(__func__); + if (!gp) { + REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( + _("Internal error: %s"), + _("RemminaProtocolWidget* gp is 'NULL'!") + )); + return NULL; + } + + pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); + + CANCEL_ASYNC + if (!rmplugin_x2go_main(gp)) { + IDLE_ADD((GSourceFunc) rmplugin_x2go_cleanup, gp); + } + + return NULL; +} + +static gboolean rmplugin_x2go_open_connection(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp); + + if (!rm_plugin_service->gtksocket_available()) { + rm_plugin_service->protocol_plugin_set_error(gp, _("The %s protocol is " + "unavailable because GtkSocket only works under X.org"), + PLUGIN_NAME); + return FALSE; + } + + gpdata->socket_id = gtk_socket_get_id(GTK_SOCKET(gpdata->socket)); + // casting to void* is allowed since return type 'gpointer' is actually void*. + if (pthread_create(&gpdata->thread, NULL, (void*) rmplugin_x2go_main_thread, gp)) { + rm_plugin_service->protocol_plugin_set_error(gp, _("Could not initialize " + "pthread. Falling back to non-threaded mode…")); + gpdata->thread = 0; + return FALSE; + } else { + return TRUE; + } +} + +static gboolean rmplugin_x2go_query_feature(RemminaProtocolWidget* gp, + const RemminaProtocolFeature* feature) +{ + TRACE_CALL(__func__); + return TRUE; +} + +static const RemminaProtocolFeature rmplugin_x2go_features[] = { + {REMMINA_PROTOCOL_FEATURE_TYPE_GTKSOCKET, RMPLUGIN_X2GO_FEATURE_GTKSOCKET, NULL, NULL, NULL}, + {REMMINA_PROTOCOL_FEATURE_TYPE_END, 0, NULL, NULL, NULL} +}; + + +/** + * @brief This function builds a string like: "'value1', 'value2' and 'value3'" \n + * To be used in a loop. \n + * See rmplugin_x2go_string_setting_validator() for an example. + * + * @param max_elements Number of maximum elements. + * @param element_to_add Next element to add to the string + * @param current_element Which element is element_to_add? + * @param string The string to which `element_to_add` will be added. + */ +static gchar* rmplugin_x2go_enumeration_prettifier(const guint max_elements, + const guint current_element, + gchar* element_to_add, + gchar* string) +{ + if (max_elements > 2) { + if (current_element == max_elements - 1) { + // TRANSLATORS: Presumably you just want to translate 'and' into + // your language. + // (Except your listing-grammar differs from english.) + // 'value1', 'value2', 'valueN-1' and 'valueN' + return g_strdup_printf(_("%sand '%s'"), string, element_to_add); + } else if (current_element == max_elements - 2) { + // TRANSLATORS: Presumably you just want to leave it english. + // (Except your listing-grammar differs from english.) + // 'value1', 'value2', 'valueN-1' and 'valueN' + return g_strdup_printf(_("%s'%s' "), string, element_to_add); + } else { + // TRANSLATORS: Presumably you just want to leave it english. + // (Except your listing-grammar differs from english.) + // 'value1', 'value2', 'valueN-1' and 'valueN' + return g_strdup_printf(_("%s'%s', "), string, element_to_add); + } + } else if (max_elements == 2) { + if (current_element == max_elements - 1) { + // TRANSLATORS: Presumably you just want to translate 'and' into + // your language. + // (Except your listing-grammar differs from english.) + // 'value1' and 'value2' + return g_strdup_printf(_("%sand '%s'"), string, element_to_add); + } else { + // TRANSLATORS: Presumably you just want to leave it english. + // (Except your listing-grammar differs from english.) + // 'value1' and 'value2' + return g_strdup_printf(_("%s'%s' "), string, element_to_add); + } + } else { + return g_strdup(element_to_add); + } +} + +/** + * @brief Validator-functions are getting executed when the user wants to save profile + * settings. It uses the given data (See RemminaProtocolSetting array) to determine + * which strings are allowed and returns a end-user friendly error message. + * + * @param key Key is the setting's name. + * @param value Value to validate. + * @param data Data needed for validation process. See RemminaProtocolSetting array. + * + * @returns End-user friendly and translated error message, explaining why the given + * value is invalid. If the given value is error-free then NULL gets returned. + * + */ +static GError* rmplugin_x2go_string_setting_validator(gchar* key, gchar* value, + gchar* data) +{ + GError *error = NULL; + + if (!data) { + gchar *error_msg = _("Invalid validation data in ProtocolSettings array!"); + REMMINA_PLUGIN_CRITICAL("%s", error_msg); + g_set_error(&error, 1, 1, "%s", error_msg); + return error; + } + + gchar **elements_list = g_strsplit(data, ",", 0); + + guint elements_amount = 0; + elements_amount = g_strv_length(elements_list); + + if (elements_list == NULL || + elements_list[0] == NULL || + strlen(elements_list[0]) <= 0) + { + gchar *error_msg = _("Validation data in ProtocolSettings array is invalid!"); + REMMINA_PLUGIN_CRITICAL("%s", error_msg); + g_set_error(&error, 1, 1, "%s", error_msg); + return error; + } + + gchar *data_str = ""; + + if (!key || !value) { + REMMINA_PLUGIN_CRITICAL("%s", _("Parameters 'key' or 'value' are 'NULL'!")); + g_set_error(&error, 1, 1, "%s", _("Internal error.")); + return error; + } + + for (guint i = 0; elements_list[i] != NULL; i++) { + // Don't wanna crash if elements_list[i] is NULL. + gchar* element = elements_list[i] ? elements_list[i] : ""; + if (g_strcmp0(value, element) == 0) { + // We found value in elements_list. Value passed validation. + return NULL; + } + + data_str = rmplugin_x2go_enumeration_prettifier(elements_amount, i, + element, data_str); + } + + if (elements_amount > 1) { + g_set_error(&error, 1, 1, _("Allowed values are %s."), data_str); + } else { + g_set_error(&error, 1, 1, _("The only allowed value is '%s'."), data_str); + } + + g_free(data_str); + g_strfreev(elements_list); + + return error; +} + +/** + * @brief Validator-functions are getting executed when the user wants to save profile + * settings. It uses the given data (See RemminaProtocolSetting array) to determine + * if the given value is a valid integer is in range and returns a end-user + * friendly error message. + * + * @param key Key is the setting's name. + * @param value Value to validate. + * @param data Data needed for validation process. See RemminaProtocolSetting array. + * + * @returns End-user friendly and translated error message, explaining why the given + * value is invalid. If the given value is error-free then NULL gets returned. + * + */ +static GError* rmplugin_x2go_int_setting_validator(gchar* key, gpointer value, + gchar* data) +{ + GError *error = NULL; + + gchar **integer_list = g_strsplit(data, ";", 0); + + if (integer_list == NULL || + integer_list[0] == NULL || + integer_list[1] == NULL || + strlen(integer_list[0]) <= 0 || + strlen(integer_list[1]) <= 0) + { + gchar *error_msg = _("Validation data in ProtocolSettings array is invalid!"); + REMMINA_PLUGIN_CRITICAL("%s", error_msg); + g_set_error(&error, 1, 1, "%s", error_msg); + return error; + } + + gint minimum; + str2int_errno err = str2int(&minimum, integer_list[0], 10); + if (err == STR2INT_INCONVERTIBLE) { + g_set_error(&error, 1, 1, "%s", g_strdup_printf( + _("Internal error: %s"), + _("The lower limit is not a valid integer!") + )); + } else if (err == STR2INT_OVERFLOW) { + g_set_error(&error, 1, 1, "%s", g_strdup_printf( + _("Internal error: %s"), + _("The lower limit is too high!") + )); + } else if (err == STR2INT_UNDERFLOW) { + g_set_error(&error, 1, 1, "%s", g_strdup_printf( + _("Internal error: %s"), + _("The lower limit is too low!") + )); + } else if (err == STR2INT_INVALID_DATA) { + g_set_error(&error, 1, 1, "%s", g_strdup_printf( + _("Internal error: %s"), + _("Something unknown went wrong.") + )); + } + + if (error) { + REMMINA_PLUGIN_CRITICAL("%s", _("Please check the RemminaProtocolSetting " + "array for possible errors.")); + return error; + } + + gint maximum; + err = str2int(&maximum, integer_list[1], 10); + if (err == STR2INT_INCONVERTIBLE) { + g_set_error(&error, 1, 1, "%s", g_strdup_printf( + _("Internal error: %s"), + _("The upper limit is not a valid integer!") + )); + } else if (err == STR2INT_OVERFLOW) { + g_set_error(&error, 1, 1, "%s", g_strdup_printf( + _("Internal error: %s"), + _("The upper limit is too high!") + )); + } else if (err == STR2INT_UNDERFLOW) { + g_set_error(&error, 1, 1, "%s", g_strdup_printf( + _("Internal error: %s"), + _("The upper limit is too low!") + )); + } else if (err == STR2INT_INVALID_DATA) { + g_set_error(&error, 1, 1, "%s", g_strdup_printf( + _("Internal error: %s"), + _("Something unknown went wrong.") + )); + } + + if (error) { + REMMINA_PLUGIN_CRITICAL("%s", _("Please check the RemminaProtocolSetting " + "array for possible errors.")); + return error; + } + + gint int_value; + err = str2int(&int_value, value, 10); + if (err == STR2INT_INCONVERTIBLE) { + // non-numerical characters are can't be entered but, the user can + // input an empty string. + g_set_error(&error, 1, 1, "%s", _("The input is not a valid integer!")); + } else if (err == STR2INT_OVERFLOW || err == STR2INT_UNDERFLOW) { + g_set_error(&error, 1, 1, _("Input must be a number between %i and %i."), + minimum, maximum); + } else if (err == STR2INT_INVALID_DATA) { + g_set_error(&error, 1, 1, "%s", _("Something unknown went wrong.")); + } + + if (error) { + return error; + } + + /*REMMINA_PLUGIN_DEBUG("Key: \t%s", (gchar*) key); + REMMINA_PLUGIN_DEBUG("Value:\t%s", (gchar*) value); + REMMINA_PLUGIN_DEBUG("Data: \t%s", data); + REMMINA_PLUGIN_DEBUG("Min: %i, Max: %i", minimum, maximum); + REMMINA_PLUGIN_DEBUG("Value converted:\t%i", int_value);*/ + + if (err == STR2INT_SUCCESS && (minimum > int_value || int_value > maximum)) { + g_set_error(&error, 1, 1, _("Input must be a number between %i and %i."), + minimum, maximum); + } + + // Should be NULL. + return error; +} + +/* 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 rmplugin_x2go_basic_settings[] = { + {REMMINA_PROTOCOL_SETTING_TYPE_SERVER, "server", NULL, FALSE, NULL, NULL, NULL, NULL}, + {REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "username", N_("Username"), FALSE, NULL, NULL, NULL, NULL}, + {REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD, "password", N_("Password"), FALSE, NULL, NULL, NULL, NULL}, + {REMMINA_PROTOCOL_SETTING_TYPE_COMBO, "command", N_("Startup program"), FALSE, + /* SELECT & COMBO Values */ "MATE,KDE,XFCE,LXDE,TERMINAL", + /* Tooltip */ N_("Which command should be executed after creating the X2Go session?"), NULL, NULL}, + {REMMINA_PROTOCOL_SETTING_TYPE_RESOLUTION, "resolution", NULL, FALSE, NULL, NULL, NULL, NULL}, + {REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "kbdlayout", N_("Keyboard Layout (auto)"), FALSE, NULL, NULL, NULL, NULL}, + {REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "kbdtype", N_("Keyboard type (auto)"), FALSE, NULL, NULL, NULL, NULL}, + {REMMINA_PROTOCOL_SETTING_TYPE_COMBO, "audio", N_("Audio support"), FALSE, + /* SELECT & COMBO Values */ "pulse,esd,none", + /* Tooltip */ N_("The sound system of the X2Go server (default: 'pulse')."), + /* Validation data */ "pulse,esd,none", + /* Validation method */ G_CALLBACK(rmplugin_x2go_string_setting_validator)}, + {REMMINA_PROTOCOL_SETTING_TYPE_COMBO, "clipboard", N_("Clipboard direction"), FALSE, + /* SELECT & COMBO Values */ "none,server,client,both", + /* Tooltip */ N_("Which direction should clipboard content be copied? " + "(default: 'both')."), + /* Validation data */ "none,server,client,both", + /* Validation method */ G_CALLBACK(rmplugin_x2go_string_setting_validator)}, + {REMMINA_PROTOCOL_SETTING_TYPE_INT, "dpi", N_("DPI resolution"), FALSE, NULL, + /* Tooltip */ N_("Launch session with a specific resolution (in dots per inch). " + "Must be between 20 and 400."), + /* Validation data */ "20;400", // "<min>;<max>;" + /* Validation method */ G_CALLBACK(rmplugin_x2go_int_setting_validator)}, + {REMMINA_PROTOCOL_SETTING_TYPE_FILE, "ssh_privatekey", N_("SSH identity file"), FALSE, NULL, N_("Your private key"), NULL, NULL }, + {REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, FALSE, NULL, NULL, NULL, NULL}}; + +/* Protocol plugin definition and features */ +static RemminaProtocolPlugin rmplugin_x2go = { + REMMINA_PLUGIN_TYPE_PROTOCOL, // Type + PLUGIN_NAME, // Name + PLUGIN_DESCRIPTION, // Description + GETTEXT_PACKAGE, // Translation domain + PLUGIN_VERSION, // Version number + PLUGIN_APPICON, // Icon for normal connection + PLUGIN_SSH_APPICON, // Icon for SSH connection + rmplugin_x2go_basic_settings, // Array for basic settings + NULL, // Array for advanced settings + REMMINA_PROTOCOL_SSH_SETTING_NONE, // SSH settings type + rmplugin_x2go_features, // Array for available features + rmplugin_x2go_init, // Plugin initialization method + rmplugin_x2go_open_connection, // Plugin open connection method + rmplugin_x2go_close_connection, // Plugin connection-method closure + rmplugin_x2go_query_feature, // Query for available features + NULL, // Call a feature + NULL, // Send a keystroke + NULL, // Screenshot +}; + +G_MODULE_EXPORT gboolean remmina_plugin_entry(RemminaPluginService *service) +{ + TRACE_CALL("remmina_plugin_entry"); + rm_plugin_service = service; + + bindtextdomain(GETTEXT_PACKAGE, REMMINA_RUNTIME_LOCALEDIR); + bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8"); + + if (!service->register_plugin((RemminaPlugin *) &rmplugin_x2go)) { + return FALSE; + } + + pthread_mutex_init(&remmina_x2go_init_mutex, NULL); + remmina_x2go_window_id_array = g_array_new(FALSE, TRUE, sizeof(Window)); + + REMMINA_PLUGIN_MESSAGE("%s", _("X2Go plugin loaded.")); + + return TRUE; +} diff --git a/plugins/x2go/x2go_plugin.h b/plugins/x2go/x2go_plugin.h new file mode 100644 index 0000000..5fc53a6 --- /dev/null +++ b/plugins/x2go/x2go_plugin.h @@ -0,0 +1,46 @@ +/* + * Project: Remmina Plugin X2Go + * Description: Remmina protocol plugin to connect via X2Go using PyHoca + * Based on Fabio Castelli Team Viewer Plugin + * Copyright: 2013-2014 Fabio Castelli (Muflone) + * Author: Antenore Gatta <antenore@simbiosi.org> + * Copyright: 2015 Antenore Gatta + * License: GPL-2+ + * + * 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 + +#define PLUGIN_NAME "X2GO" +#define PLUGIN_DESCRIPTION N_("X2Go - Launch an X2Go session") +#define PLUGIN_VERSION "2.0.0" +#define PLUGIN_APPICON "org.remmina.Remmina-x2go-symbolic" +#define PLUGIN_SSH_APPICON "org.remmina.Remmina-x2go-ssh-symbolic" |