diff options
-rw-r--r-- | plugins/nx/CMakeLists.txt | 67 | ||||
-rw-r--r-- | plugins/nx/nx_plugin.c | 831 | ||||
-rw-r--r-- | plugins/nx/nx_plugin.h | 81 | ||||
-rw-r--r-- | plugins/nx/nx_session.c | 1008 | ||||
-rw-r--r-- | plugins/nx/nx_session.h | 104 | ||||
-rw-r--r-- | plugins/nx/nx_session_manager.c | 244 | ||||
-rw-r--r-- | plugins/nx/nx_session_manager.h | 42 | ||||
-rw-r--r-- | plugins/nx/scalable/emblems/remmina-nx-symbolic.svg | 99 |
8 files changed, 2476 insertions, 0 deletions
diff --git a/plugins/nx/CMakeLists.txt b/plugins/nx/CMakeLists.txt new file mode 100644 index 0000000..275a3a6 --- /dev/null +++ b/plugins/nx/CMakeLists.txt @@ -0,0 +1,67 @@ +# remmina-plugin-nx - The GTK+ Remote Desktop Client +# +# Copyright (C) 2011 Marc-Andre Moreau +# Copyright (C) 2014-2021 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_NX_SRCS + nx_session.c + nx_session.h + nx_session_manager.c + nx_session_manager.h + nx_plugin.c + nx_plugin.h + ) + +add_library(remmina-plugin-nx ${REMMINA_PLUGIN_NX_SRCS}) +set_target_properties(remmina-plugin-nx PROPERTIES PREFIX "") +set_target_properties(remmina-plugin-nx PROPERTIES NO_SONAME 1) + +find_package(X11) + +include_directories(${REMMINA_COMMON_INCLUDE_DIRS} + ${XKBFILE_INCLUDE_DIRS} ${LIBSSH_INCLUDE_DIRS}) +target_link_libraries(remmina-plugin-nx + ${REMMINA_COMMON_LIBRARIES} + ${XKBFILE_LIBRARIES} + ${LIBSSH_LIBRARIES} + ${Intl_LIBRARIES} + ${X11_X11_LIB}) + +install(TARGETS remmina-plugin-nx DESTINATION ${REMMINA_PLUGINDIR}) + +install(FILES + scalable/emblems/remmina-nx-symbolic.svg + DESTINATION ${APPICONSCALE_EMBLEMS_DIR}) + +if(WITH_ICON_CACHE) + gtk_update_icon_cache("${REMMINA_DATADIR}/icons/hicolor") +endif() diff --git a/plugins/nx/nx_plugin.c b/plugins/nx/nx_plugin.c new file mode 100644 index 0000000..01791ac --- /dev/null +++ b/plugins/nx/nx_plugin.c @@ -0,0 +1,831 @@ +/* + * 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-2021 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 <errno.h> +#include <pthread.h> +#include <stdarg.h> +#include "common/remmina_plugin.h" +#include <gtk/gtkx.h> +#include <time.h> +#define LIBSSH_STATIC 1 +#include <libssh/libssh.h> +#include <X11/Xlib.h> +#include <X11/XKBlib.h> +#include <X11/extensions/XKBrules.h> +#include "nx_plugin.h" +#include "nx_session_manager.h" + +#define REMMINA_PLUGIN_NX_FEATURE_TOOL_SENDCTRLALTDEL 1 +#define REMMINA_PLUGIN_NX_FEATURE_GTKSOCKET 1 + +/* Forward declaration */ +static RemminaProtocolPlugin remmina_plugin_nx; + +RemminaPluginService *remmina_plugin_nx_service = NULL; + +static gchar *remmina_kbtype = "pc102/us"; + +/* 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_nx_init_mutex; +static GArray *remmina_nx_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 = (struct onMainThread_cb_data *)g_malloc( sizeof(struct onMainThread_cb_data) ); + d->func = FUNC_GTK_SOCKET_ADD_ID; + d->sk = sk; + d->w = w; + + onMainThread_schedule_callback_and_wait( d ); + g_free(d); + +} + + +/* --------------------------------------- */ + + +static gboolean remmina_plugin_nx_try_window_id(Window window_id) +{ + TRACE_CALL(__func__); + gint i; + gboolean found = FALSE; + + pthread_mutex_lock(&remmina_nx_init_mutex); + for (i = 0; i < remmina_nx_window_id_array->len; i++) { + if (g_array_index(remmina_nx_window_id_array, Window, i) == window_id) { + found = TRUE; + break; + } + } + if (!found) { + g_array_append_val(remmina_nx_window_id_array, window_id); + } + pthread_mutex_unlock(&remmina_nx_init_mutex); + + return (!found); +} + +static void remmina_plugin_nx_remove_window_id(Window window_id) +{ + TRACE_CALL(__func__); + gint i; + gboolean found = FALSE; + + pthread_mutex_lock(&remmina_nx_init_mutex); + for (i = 0; i < remmina_nx_window_id_array->len; i++) { + if (g_array_index(remmina_nx_window_id_array, Window, i) == window_id) { + found = TRUE; + break; + } + } + if (found) { + g_array_remove_index_fast(remmina_nx_window_id_array, i); + } + pthread_mutex_unlock(&remmina_nx_init_mutex); +} + +static void remmina_plugin_nx_on_plug_added(GtkSocket *socket, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + remmina_plugin_nx_service->protocol_plugin_signal_connection_opened(gp); +} + +static void remmina_plugin_nx_on_plug_removed(GtkSocket *socket, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + remmina_plugin_nx_service->protocol_plugin_signal_connection_closed(gp); +} + +gboolean remmina_plugin_nx_ssh_auth_callback(gchar **passphrase, gpointer userdata) +{ + TRACE_CALL(__func__); + RemminaProtocolWidget *gp = (RemminaProtocolWidget*)userdata; + gint ret; + + /* SSH passwords must not be saved */ + ret = remmina_plugin_nx_service->protocol_plugin_init_auth(gp, 0, + _("SSH credentials"), NULL, + NULL, + NULL, + _("Password for private SSH key")); + if (ret == GTK_RESPONSE_OK) { + *passphrase = remmina_plugin_nx_service->protocol_plugin_init_get_password(gp); + return TRUE; + } else + return FALSE; +} + +static void remmina_plugin_nx_on_proxy_exit(GPid pid, gint status, gpointer data) +{ + TRACE_CALL(__func__); + RemminaProtocolWidget *gp = (RemminaProtocolWidget*)data; + + remmina_plugin_nx_service->protocol_plugin_signal_connection_closed(gp); +} + +static int remmina_plugin_nx_dummy_handler(Display *dsp, XErrorEvent *err) +{ + TRACE_CALL(__func__); + return 0; +} + +static gboolean remmina_plugin_nx_start_create_notify(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginNxData *gpdata = GET_PLUGIN_DATA(gp); + + gpdata->display = XOpenDisplay(gdk_display_get_name(gdk_display_get_default())); + if (gpdata->display == NULL) + return FALSE; + + gpdata->orig_handler = XSetErrorHandler(remmina_plugin_nx_dummy_handler); + + XSelectInput(gpdata->display, XDefaultRootWindow(gpdata->display), SubstructureNotifyMask); + + return TRUE; +} + +static gboolean remmina_plugin_nx_monitor_create_notify(RemminaProtocolWidget *gp, const gchar *cmd) +{ + TRACE_CALL(__func__); + RemminaPluginNxData *gpdata; + Atom atom; + XEvent xev; + Window w; + Atom type; + int format; + unsigned long nitems, rest; + unsigned char *data = NULL; + struct timespec ts; + + CANCEL_DEFER + + gpdata = GET_PLUGIN_DATA(gp); + atom = XInternAtom(gpdata->display, "WM_COMMAND", True); + if (atom == None) + return FALSE; + + ts.tv_sec = 0; + ts.tv_nsec = 200000000; + + while (1) { + pthread_testcancel(); + while (!XPending(gpdata->display)) { + nanosleep(&ts, NULL); + continue; + } + XNextEvent(gpdata->display, &xev); + if (xev.type != CreateNotify) + continue; + w = xev.xcreatewindow.window; + if (XGetWindowProperty(gpdata->display, w, atom, 0, 255, False, AnyPropertyType, &type, &format, &nitems, &rest, + &data) != Success) + continue; + if (data && strstr((char*)data, cmd) && remmina_plugin_nx_try_window_id(w)) { + gpdata->window_id = w; + XFree(data); + break; + } + if (data) + XFree(data); + } + + XSetErrorHandler(gpdata->orig_handler); + XCloseDisplay(gpdata->display); + gpdata->display = NULL; + + CANCEL_ASYNC + return TRUE; +} + +static gint remmina_plugin_nx_wait_signal(RemminaPluginNxData *gpdata) +{ + TRACE_CALL(__func__); + fd_set set; + guchar dummy = 0; + + FD_ZERO(&set); + FD_SET(gpdata->event_pipe[0], &set); + select(gpdata->event_pipe[0] + 1, &set, NULL, NULL, NULL); + if (read(gpdata->event_pipe[0], &dummy, 1)) { + } + return (gint)dummy; +} + +static void remmina_plugin_nx_log_callback(const gchar *fmt, ...) +{ + char buffer[256]; + va_list args; + va_start(args, fmt); + vsnprintf(buffer, sizeof(buffer), fmt, args); + REMMINA_PLUGIN_DEBUG(buffer); + va_end(args); +} + + +static gboolean remmina_plugin_nx_start_session(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginNxData *gpdata = GET_PLUGIN_DATA(gp); + RemminaFile *remminafile; + RemminaNXSession *nx; + const gchar *type, *app; + gchar *s1, *s2; + gint port; + gint ret; + gboolean is_empty_list; + gint event_type = 0; + const gchar *cs; + gint i; + gboolean disablepasswordstoring; + + remminafile = remmina_plugin_nx_service->protocol_plugin_get_file(gp); + nx = gpdata->nx; + + /* Connect */ + + remmina_nx_session_set_encryption(nx, + remmina_plugin_nx_service->file_get_int(remminafile, "disableencryption", FALSE) ? 0 : 1); + remmina_nx_session_set_localport(nx, remmina_plugin_nx_service->pref_get_sshtunnel_port()); + remmina_nx_session_set_log_callback(nx, remmina_plugin_nx_log_callback); + + s2 = remmina_plugin_nx_service->protocol_plugin_start_direct_tunnel(gp, 22, FALSE); + if (s2 == NULL) { + return FALSE; + } + remmina_plugin_nx_service->get_server_port(s2, 22, &s1, &port); + g_free(s2); + + if (!remmina_nx_session_open(nx, s1, port, remmina_plugin_nx_service->file_get_string(remminafile, "nx_privatekey"), + remmina_plugin_nx_ssh_auth_callback, gp)) { + g_free(s1); + return FALSE; + } + g_free(s1); + + /* Login */ + + s1 = g_strdup(remmina_plugin_nx_service->file_get_string(remminafile, "username")); + s2 = g_strdup(remmina_plugin_nx_service->file_get_string(remminafile, "password")); + + if (s1 && s2) { + ret = remmina_nx_session_login(nx, s1, s2); + } else { + gchar *s_username, *s_password; + + disablepasswordstoring = remmina_plugin_nx_service->file_get_int(remminafile, "disablepasswordstoring", FALSE); + + ret = remmina_plugin_nx_service->protocol_plugin_init_auth(gp, + (disablepasswordstoring ? 0 : REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD) | REMMINA_MESSAGE_PANEL_FLAG_USERNAME, + _("Enter NX authentication credentials"), + remmina_plugin_nx_service->file_get_string(remminafile, "username"), + remmina_plugin_nx_service->file_get_string(remminafile, "password"), + NULL, + NULL); + if (ret == GTK_RESPONSE_OK) { + gboolean save; + s_username = remmina_plugin_nx_service->protocol_plugin_init_get_username(gp); + s_password = remmina_plugin_nx_service->protocol_plugin_init_get_password(gp); + save = remmina_plugin_nx_service->protocol_plugin_init_get_savepassword(gp); + if (save) { + remmina_plugin_nx_service->file_set_string(remminafile, "username", s_username); + remmina_plugin_nx_service->file_set_string(remminafile, "password", s_password); + } else + remmina_plugin_nx_service->file_unsave_passwords(remminafile); + } else { + return False; + } + + ret = remmina_nx_session_login(nx, s_username, s_password); + g_free(s_username); + g_free(s_password); + } + g_free(s1); + g_free(s2); + + if (!ret) + return FALSE; + + remmina_plugin_nx_service->protocol_plugin_init_save_cred(gp); + + /* Prepare the session type and application */ + cs = remmina_plugin_nx_service->file_get_string(remminafile, "exec"); + if (!cs || g_strcmp0(cs, "GNOME") == 0) { + type = "unix-gnome"; + app = NULL; + }else + if (g_strcmp0(cs, "KDE") == 0) { + type = "unix-kde"; + app = NULL; + }else + if (g_strcmp0(cs, "Xfce") == 0) { + /* NX does not know Xfce. So we simply launch the Xfce start-up program. */ + type = "unix-application"; + app = "startxfce4"; + }else + if (g_strcmp0(cs, "Shadow") == 0) { + type = "shadow"; + app = NULL; + }else { + type = "unix-application"; + app = cs; + } + + /* List sessions */ + + gpdata->attach_session = (g_strcmp0(type, "shadow") == 0); + while (1) { + remmina_nx_session_add_parameter(nx, "type", type); + if (!gpdata->attach_session) { + remmina_nx_session_add_parameter(nx, "user", + remmina_plugin_nx_service->file_get_string(remminafile, "username")); + remmina_nx_session_add_parameter(nx, "status", "suspended,running"); + } + + if (!remmina_nx_session_list(nx)) { + return FALSE; + } + + is_empty_list = !remmina_nx_session_iter_first(nx, &gpdata->iter); + if (is_empty_list && !gpdata->manager_started && !gpdata->attach_session) { + event_type = REMMINA_NX_EVENT_START; + }else { + remmina_nx_session_manager_start(gp); + event_type = remmina_plugin_nx_wait_signal(gpdata); + if (event_type == REMMINA_NX_EVENT_CANCEL) { + return FALSE; + } + if (event_type == REMMINA_NX_EVENT_TERMINATE) { + if (!is_empty_list) { + s1 = remmina_nx_session_iter_get(nx, &gpdata->iter, REMMINA_NX_SESSION_COLUMN_ID); + remmina_nx_session_add_parameter(nx, "sessionid", s1); + g_free(s1); + if (!remmina_nx_session_terminate(nx)) { + remmina_nx_session_manager_start(gp); + remmina_plugin_nx_wait_signal(gpdata); + } + } + continue; + } + } + + break; + } + + /* Start, Restore or Attach, based on the setting and existing session */ + remmina_nx_session_add_parameter(nx, "type", type); + i = remmina_plugin_nx_service->file_get_int(remminafile, "quality", 0); + remmina_nx_session_add_parameter(nx, "link", i > 2 ? "lan" : i == 2 ? "adsl" : i == 1 ? "isdn" : "modem"); + remmina_nx_session_add_parameter(nx, "geometry", "%ix%i", + remmina_plugin_nx_service->get_profile_remote_width(gp), + remmina_plugin_nx_service->get_profile_remote_height(gp)); + remmina_nx_session_add_parameter(nx, "keyboard", remmina_kbtype); + remmina_nx_session_add_parameter(nx, "client", "linux"); + remmina_nx_session_add_parameter(nx, "media", "0"); + remmina_nx_session_add_parameter(nx, "clipboard", + remmina_plugin_nx_service->file_get_int(remminafile, "disableclipboard", FALSE) ? "none" : "both"); + + switch (event_type) { + + case REMMINA_NX_EVENT_START: + if (app) + remmina_nx_session_add_parameter(nx, "application", app); + + remmina_nx_session_add_parameter(nx, "session", + remmina_plugin_nx_service->file_get_string(remminafile, "name")); + remmina_nx_session_add_parameter(nx, "screeninfo", "%ix%ix24+render", + remmina_plugin_nx_service->file_get_int(remminafile, "resolution_width", 0), + remmina_plugin_nx_service->file_get_int(remminafile, "resolution_height", 0)); + + if (!remmina_nx_session_start(nx)) + return FALSE; + break; + + case REMMINA_NX_EVENT_ATTACH: + s1 = remmina_nx_session_iter_get(nx, &gpdata->iter, REMMINA_NX_SESSION_COLUMN_ID); + remmina_nx_session_add_parameter(nx, "id", s1); + g_free(s1); + + s1 = remmina_nx_session_iter_get(nx, &gpdata->iter, REMMINA_NX_SESSION_COLUMN_DISPLAY); + remmina_nx_session_add_parameter(nx, "display", s1); + g_free(s1); + + if (!remmina_nx_session_attach(nx)) + return FALSE; + break; + + case REMMINA_NX_EVENT_RESTORE: + s1 = remmina_nx_session_iter_get(nx, &gpdata->iter, REMMINA_NX_SESSION_COLUMN_ID); + remmina_nx_session_add_parameter(nx, "id", s1); + g_free(s1); + + remmina_nx_session_add_parameter(nx, "session", + remmina_plugin_nx_service->file_get_string(remminafile, "name")); + + if (!remmina_nx_session_restore(nx)) + return FALSE; + break; + + default: + return FALSE; + } + + if (!remmina_nx_session_tunnel_open(nx)) + return FALSE; + + if (!remmina_plugin_nx_start_create_notify(gp)) + return FALSE; + + /* nxproxy */ + if (!remmina_nx_session_invoke_proxy(nx, -1, remmina_plugin_nx_on_proxy_exit, gp)) + return FALSE; + + /* get the window id of the remote nxagent */ + if (!remmina_plugin_nx_monitor_create_notify(gp, "nxagent")) + return FALSE; + + /* embed it */ + onMainThread_gtk_socket_add_id(GTK_SOCKET(gpdata->socket), gpdata->window_id); + + + return TRUE; +} + +static gboolean remmina_plugin_nx_main(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginNxData *gpdata = GET_PLUGIN_DATA(gp); + gboolean ret; + const gchar *err; + + gpdata->nx = remmina_nx_session_new(); + ret = remmina_plugin_nx_start_session(gp); + if (!ret) { + err = remmina_nx_session_get_error(gpdata->nx); + if (err) { + remmina_plugin_nx_service->protocol_plugin_set_error(gp, "%s", err); + } + } + + gpdata->thread = 0; + return ret; +} + +static gpointer remmina_plugin_nx_main_thread(gpointer data) +{ + TRACE_CALL(__func__); + RemminaProtocolWidget *gp = (RemminaProtocolWidget *)data; + pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); + + CANCEL_ASYNC + if (!remmina_plugin_nx_main(gp)) { + remmina_plugin_nx_service->protocol_plugin_signal_connection_closed(gp); + } + return NULL; +} + +static void remmina_plugin_nx_init(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginNxData *gpdata; + gint flags; + + gpdata = g_new0(RemminaPluginNxData, 1); + g_object_set_data_full(G_OBJECT(gp), "plugin-data", gpdata, g_free); + + gpdata->socket = gtk_socket_new(); + remmina_plugin_nx_service->protocol_plugin_register_hostkey(gp, gpdata->socket); + gtk_widget_show(gpdata->socket); + g_signal_connect(G_OBJECT(gpdata->socket), "plug-added", G_CALLBACK(remmina_plugin_nx_on_plug_added), gp); + g_signal_connect(G_OBJECT(gpdata->socket), "plug-removed", G_CALLBACK(remmina_plugin_nx_on_plug_removed), gp); + gtk_container_add(GTK_CONTAINER(gp), gpdata->socket); + + if (pipe(gpdata->event_pipe)) { + g_print("Error creating pipes.\n"); + gpdata->event_pipe[0] = -1; + gpdata->event_pipe[1] = -1; + }else { + flags = fcntl(gpdata->event_pipe[0], F_GETFL, 0); + fcntl(gpdata->event_pipe[0], F_SETFL, flags | O_NONBLOCK); + } +} + +static gboolean remmina_plugin_nx_open_connection(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginNxData *gpdata = GET_PLUGIN_DATA(gp); + gint width, height; + + if (!remmina_plugin_nx_service->gtksocket_available()) { + remmina_plugin_nx_service->protocol_plugin_set_error(gp, + _("The protocol “%s” is unavailable because GtkSocket only works under X.Org."), + remmina_plugin_nx.name); + return FALSE; + } + + width = remmina_plugin_nx_service->get_profile_remote_width(gp); + height = remmina_plugin_nx_service->get_profile_remote_height(gp); + + remmina_plugin_nx_service->protocol_plugin_set_width(gp, width); + remmina_plugin_nx_service->protocol_plugin_set_height(gp, height); + gtk_widget_set_size_request(GTK_WIDGET(gp), width, height); + + gpdata->socket_id = gtk_socket_get_id(GTK_SOCKET(gpdata->socket)); + + if (pthread_create(&gpdata->thread, NULL, remmina_plugin_nx_main_thread, gp)) { + remmina_plugin_nx_service->protocol_plugin_set_error(gp, + "Failed to initialize pthread. Falling back to non-thread mode…"); + gpdata->thread = 0; + return FALSE; + }else { + return TRUE; + } +} + +static gboolean remmina_plugin_nx_close_connection(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginNxData *gpdata = GET_PLUGIN_DATA(gp); + + if (gpdata->thread) { + pthread_cancel(gpdata->thread); + if (gpdata->thread) + pthread_join(gpdata->thread, NULL); + } + if (gpdata->session_manager_start_handler) { + g_source_remove(gpdata->session_manager_start_handler); + gpdata->session_manager_start_handler = 0; + } + + if (gpdata->window_id) { + remmina_plugin_nx_remove_window_id(gpdata->window_id); + } + + if (gpdata->nx) { + remmina_nx_session_free(gpdata->nx); + gpdata->nx = NULL; + } + + if (gpdata->display) { + XSetErrorHandler(gpdata->orig_handler); + XCloseDisplay(gpdata->display); + gpdata->display = NULL; + } + close(gpdata->event_pipe[0]); + close(gpdata->event_pipe[1]); + + remmina_plugin_nx_service->protocol_plugin_signal_connection_closed(gp); + + return FALSE; +} + +/* Send CTRL+ALT+DEL keys keystrokes to the plugin socket widget */ +static void remmina_plugin_nx_send_ctrlaltdel(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + guint keys[] = { GDK_KEY_Control_L, GDK_KEY_Alt_L, GDK_KEY_Delete }; + RemminaPluginNxData *gpdata = GET_PLUGIN_DATA(gp); + + remmina_plugin_nx_service->protocol_plugin_send_keys_signals(gpdata->socket, + keys, G_N_ELEMENTS(keys), GDK_KEY_PRESS | GDK_KEY_RELEASE); +} + +static gboolean remmina_plugin_nx_query_feature(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature) +{ + TRACE_CALL(__func__); + return TRUE; +} + +static void remmina_plugin_nx_call_feature(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature) +{ + TRACE_CALL(__func__); + switch (feature->id) { + case REMMINA_PLUGIN_NX_FEATURE_TOOL_SENDCTRLALTDEL: + remmina_plugin_nx_send_ctrlaltdel(gp); + break; + default: + break; + } +} + +/* 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 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 + */ +static const RemminaProtocolSetting remmina_plugin_nx_basic_settings[] = +{ + { REMMINA_PROTOCOL_SETTING_TYPE_SERVER, "server", NULL, FALSE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_FILE, "nx_privatekey", N_("SSH identity file"), FALSE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "username", N_("Username"), FALSE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD, "password", N_("User password"), FALSE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_RESOLUTION, "resolution", NULL, FALSE, GINT_TO_POINTER(1), NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "quality", N_("Quality"), FALSE, quality_list, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_COMBO, "exec", N_("Start-up program"), FALSE, "GNOME,KDE,Xfce,Shadow", NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, FALSE, 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_nx_advanced_settings[] = +{ + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disableclipboard", N_("Disable clipboard sync"), FALSE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disableencryption", N_("Disable encryption"), FALSE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "showcursor", N_("Use local cursor"), FALSE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disablepasswordstoring", N_("Forget passwords after use"), FALSE, 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_nx_features[] = +{ + { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, REMMINA_PLUGIN_NX_FEATURE_TOOL_SENDCTRLALTDEL, N_("Send Ctrl+Alt+Del"), NULL, NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_GTKSOCKET, REMMINA_PLUGIN_NX_FEATURE_GTKSOCKET, NULL, NULL, NULL}, + { REMMINA_PROTOCOL_FEATURE_TYPE_END, 0, NULL, NULL, NULL } +}; + +/* Protocol plugin definition and features */ +static RemminaProtocolPlugin remmina_plugin_nx = +{ + REMMINA_PLUGIN_TYPE_PROTOCOL, // Type + "NX", // Name + N_("NX - NX Technology"), // Description + GETTEXT_PACKAGE, // Translation domain + VERSION, // Version number + "remmina-nx-symbolic", // Icon for normal connection + "remmina-nx-symbolic", // Icon for SSH connection + remmina_plugin_nx_basic_settings, // Array for basic settings + remmina_plugin_nx_advanced_settings, // Array for advanced settings + REMMINA_PROTOCOL_SSH_SETTING_TUNNEL, // SSH settings type + remmina_plugin_nx_features, // Array for available features + remmina_plugin_nx_init, // Plugin initialization + remmina_plugin_nx_open_connection, // Plugin open connection + remmina_plugin_nx_close_connection, // Plugin close connection + remmina_plugin_nx_query_feature, // Query for available features + remmina_plugin_nx_call_feature, // 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__); + Display *dpy; + XkbRF_VarDefsRec vd; + gchar *s; + + remmina_plugin_nx_service = service; + + bindtextdomain(GETTEXT_PACKAGE, REMMINA_RUNTIME_LOCALEDIR); + bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8"); + + if ((dpy = XkbOpenDisplay(NULL, NULL, NULL, NULL, NULL, NULL)) != NULL) { + if (XkbRF_GetNamesProp(dpy, NULL, &vd)) { + remmina_kbtype = g_strdup_printf("%s/%s", vd.model, vd.layout); + if (vd.layout) + XFree(vd.layout); + if (vd.model) + XFree(vd.model); + if (vd.variant) + XFree(vd.variant); + if (vd.options) + XFree(vd.options); + s = strchr(remmina_kbtype, ','); + if (s) + *s = '\0'; + /* g_print("NX: Detected “%s” keyboard type\n", remmina_kbtype); */ + } + XCloseDisplay(dpy); + } + + if (!service->register_plugin((RemminaPlugin*)&remmina_plugin_nx)) { + return FALSE; + } + + ssh_init(); + pthread_mutex_init(&remmina_nx_init_mutex, NULL); + remmina_nx_window_id_array = g_array_new(FALSE, TRUE, sizeof(Window)); + + return TRUE; +} diff --git a/plugins/nx/nx_plugin.h b/plugins/nx/nx_plugin.h new file mode 100644 index 0000000..2611751 --- /dev/null +++ b/plugins/nx/nx_plugin.h @@ -0,0 +1,81 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2010 Vic Lee + * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo + * Copyright (C) 2016-2021 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 + +#define GET_PLUGIN_DATA(gp) (RemminaPluginNxData *)g_object_get_data(G_OBJECT(gp), "plugin-data"); + +G_BEGIN_DECLS + +#include "nx_session.h" + +typedef enum { + REMMINA_NX_EVENT_CANCEL, + REMMINA_NX_EVENT_START, + REMMINA_NX_EVENT_RESTORE, + REMMINA_NX_EVENT_ATTACH, + REMMINA_NX_EVENT_TERMINATE +} RemminaNXEventType; + +typedef struct _RemminaPluginNxData { + GtkWidget * socket; + gint socket_id; + + pthread_t thread; + + RemminaNXSession * nx; + + Display * display; + Window window_id; + int (*orig_handler)(Display *, XErrorEvent *); + + /* Session Manager data */ + gboolean manager_started; + GtkWidget * manager_dialog; + gboolean manager_selected; + + /* Communication between the NX thread and the session manager */ + gint event_pipe[2]; + guint session_manager_start_handler; + gboolean attach_session; + GtkTreeIter iter; + gint default_response; +} RemminaPluginNxData; + +extern RemminaPluginService *remmina_plugin_nx_service; +#define REMMINA_PLUGIN_DEBUG(fmt, ...) remmina_plugin_nx_service->_remmina_debug(__func__, fmt, ##__VA_ARGS__) + +G_END_DECLS diff --git a/plugins/nx/nx_session.c b/plugins/nx/nx_session.c new file mode 100644 index 0000000..8162dd1 --- /dev/null +++ b/plugins/nx/nx_session.c @@ -0,0 +1,1008 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2010 Vic Lee + * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo + * Copyright (C) 2016-2021 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 <errno.h> +#include <pthread.h> +#include "common/remmina_plugin.h" +#include <glib/gstdio.h> +#define LIBSSH_STATIC 1 +#include <libssh/libssh.h> +#include "nx_session.h" + +/* Some missing stuff in libssh */ +#define REMMINA_SSH_TYPE_DSS 1 +#define REMMINA_SSH_TYPE_RSA 2 + +static gboolean remmina_get_keytype(const gchar *private_key_file, gint *keytype, gboolean *encrypted) +{ + TRACE_CALL(__func__); + FILE *fp; + gchar buf1[100], buf2[100]; + + if ((fp = g_fopen(private_key_file, "r")) == NULL) { + return FALSE; + } + if (!fgets(buf1, sizeof(buf1), fp) || !fgets(buf2, sizeof(buf2), fp)) { + fclose(fp); + return FALSE; + } + fclose(fp); + + if (strstr(buf1, "BEGIN RSA")) + *keytype = REMMINA_SSH_TYPE_RSA; + else if (strstr(buf1, "BEGIN DSA")) + *keytype = REMMINA_SSH_TYPE_DSS; + else + return FALSE; + + *encrypted = (strstr(buf2, "ENCRYPTED") ? TRUE : FALSE); + + return TRUE; +} + +/*****/ + +static const gchar nx_default_private_key[] = "-----BEGIN DSA PRIVATE KEY-----\n" + "MIIBuwIBAAKBgQCXv9AzQXjxvXWC1qu3CdEqskX9YomTfyG865gb4D02ZwWuRU/9\n" + "C3I9/bEWLdaWgJYXIcFJsMCIkmWjjeSZyTmeoypI1iLifTHUxn3b7WNWi8AzKcVF\n" + "aBsBGiljsop9NiD1mEpA0G+nHHrhvTXz7pUvYrsrXcdMyM6rxqn77nbbnwIVALCi\n" + "xFdHZADw5KAVZI7r6QatEkqLAoGBAI4L1TQGFkq5xQ/nIIciW8setAAIyrcWdK/z\n" + "5/ZPeELdq70KDJxoLf81NL/8uIc4PoNyTRJjtT3R4f8Az1TsZWeh2+ReCEJxDWgG\n" + "fbk2YhRqoQTtXPFsI4qvzBWct42WonWqyyb1bPBHk+JmXFscJu5yFQ+JUVNsENpY\n" + "+Gkz3HqTAoGANlgcCuA4wrC+3Cic9CFkqiwO/Rn1vk8dvGuEQqFJ6f6LVfPfRTfa\n" + "QU7TGVLk2CzY4dasrwxJ1f6FsT8DHTNGnxELPKRuLstGrFY/PR7KeafeFZDf+fJ3\n" + "mbX5nxrld3wi5titTnX+8s4IKv29HJguPvOK/SI7cjzA+SqNfD7qEo8CFDIm1xRf\n" + "8xAPsSKs6yZ6j1FNklfu\n" + "-----END DSA PRIVATE KEY-----\n"; + +static const gchar nx_hello_server_msg[] = "hello nxserver - version "; + +struct _RemminaNXSession { + /* Common SSH members */ + ssh_session session; + ssh_channel channel; + gchar *server; + gchar *error; + RemminaNXLogCallback log_callback; + + /* Tunnel related members */ + pthread_t thread; + gboolean running; + gint server_sock; + + /* NX related members */ + GHashTable *session_parameters; + + GString *response; + gint response_pos; + gint status; + gint encryption; + gint localport; + + gchar *version; + gchar *session_id; + gint session_display; + gchar *proxy_cookie; + + gboolean allow_start; + GtkListStore *session_list; + gint session_list_state; + + GPid proxy_pid; + guint proxy_watch_source; +}; + +RemminaNXSession* +remmina_nx_session_new(void) +{ + TRACE_CALL(__func__); + RemminaNXSession *nx; + + nx = g_new0(RemminaNXSession, 1); + + nx->session_parameters = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); + nx->response = g_string_new(NULL); + nx->status = -1; + nx->encryption = 1; + nx->server_sock = -1; + + return nx; +} + +void remmina_nx_session_free(RemminaNXSession *nx) +{ + TRACE_CALL(__func__); + pthread_t thread; + + if (nx->proxy_watch_source) { + g_source_remove(nx->proxy_watch_source); + nx->proxy_watch_source = 0; + } + if (nx->proxy_pid) { + kill(nx->proxy_pid, SIGTERM); + g_spawn_close_pid(nx->proxy_pid); + nx->proxy_pid = 0; + } + thread = nx->thread; + if (thread) { + nx->running = FALSE; + pthread_cancel(thread); + pthread_join(thread, NULL); + nx->thread = 0; + } + if (nx->channel) { + ssh_channel_close(nx->channel); + ssh_channel_free(nx->channel); + } + if (nx->server_sock >= 0) { + close(nx->server_sock); + nx->server_sock = -1; + } + + g_free(nx->server); + g_free(nx->error); + g_hash_table_destroy(nx->session_parameters); + g_string_free(nx->response, TRUE); + g_free(nx->version); + g_free(nx->session_id); + g_free(nx->proxy_cookie); + + if (nx->session_list) { + g_object_unref(nx->session_list); + nx->session_list = NULL; + } + if (nx->session) { + ssh_free(nx->session); + nx->session = NULL; + } + g_free(nx); +} + +static void remmina_nx_session_set_error(RemminaNXSession *nx, const gchar *fmt) +{ + TRACE_CALL(__func__); + const gchar *err; + + if (nx->error) + g_free(nx->error); + err = ssh_get_error(nx->session); + nx->error = g_strdup_printf(fmt, err); +} + +static void remmina_nx_session_set_application_error(RemminaNXSession *nx, const gchar *fmt, ...) +{ + TRACE_CALL(__func__); + va_list args; + + if (nx->error) g_free(nx->error); + va_start(args, fmt); + nx->error = g_strdup_vprintf(fmt, args); + va_end(args); +} + +gboolean remmina_nx_session_has_error(RemminaNXSession *nx) +{ + TRACE_CALL(__func__); + return (nx->error != NULL); +} + +const gchar* +remmina_nx_session_get_error(RemminaNXSession *nx) +{ + TRACE_CALL(__func__); + return nx->error; +} + +void remmina_nx_session_clear_error(RemminaNXSession *nx) +{ + TRACE_CALL(__func__); + if (nx->error) { + g_free(nx->error); + nx->error = NULL; + } +} + +void remmina_nx_session_set_encryption(RemminaNXSession *nx, gint encryption) +{ + TRACE_CALL(__func__); + nx->encryption = encryption; +} + +void remmina_nx_session_set_localport(RemminaNXSession *nx, gint localport) +{ + TRACE_CALL(__func__); + nx->localport = localport; +} + +void remmina_nx_session_set_log_callback(RemminaNXSession *nx, RemminaNXLogCallback log_callback) +{ + TRACE_CALL(__func__); + nx->log_callback = log_callback; +} + +static gboolean remmina_nx_session_get_response(RemminaNXSession *nx) +{ + TRACE_CALL(__func__); + struct timeval timeout; + ssh_channel ch[2]; + gchar *buffer; + gint len; + gint is_stderr; + + timeout.tv_sec = 60; + timeout.tv_usec = 0; + ch[0] = nx->channel; + ch[1] = NULL; + ssh_channel_select(ch, NULL, NULL, &timeout); + + is_stderr = 0; + while (is_stderr <= 1) { + len = ssh_channel_poll(nx->channel, is_stderr); + if (len == SSH_ERROR) { + remmina_nx_session_set_error(nx, "Error reading channel: %s"); + return FALSE; + } + if (len > 0) + break; + is_stderr++; + } + if (is_stderr > 1) + return FALSE; + + buffer = g_malloc(sizeof(*buffer) * len); + len = ssh_channel_read(nx->channel, buffer, len, is_stderr); + if (len <= 0) { + remmina_nx_session_set_application_error(nx, "Channel closed."); + return FALSE; + } + + g_string_append_len(nx->response, buffer, len); + + g_free(buffer); + return TRUE; +} + +static void remmina_nx_session_parse_session_list_line(RemminaNXSession *nx, const gchar *line) +{ + TRACE_CALL(__func__); + gchar *p1, *p2; + gchar *val; + gint i; + GtkTreeIter iter; + + p1 = (char*)line; + while (*p1 == ' ') + p1++; + if (*p1 == '\0') + return; + + gtk_list_store_append(nx->session_list, &iter); + + p1 = (char*)line; + for (i = 0; i < 7; i++) { + p2 = strchr(p1, ' '); + if (!p2) + return; + val = g_strndup(p1, (gint)(p2 - p1)); + switch (i) { + case 0: + gtk_list_store_set(nx->session_list, &iter, REMMINA_NX_SESSION_COLUMN_DISPLAY, val, -1); + break; + case 1: + gtk_list_store_set(nx->session_list, &iter, REMMINA_NX_SESSION_COLUMN_TYPE, val, -1); + break; + case 2: + gtk_list_store_set(nx->session_list, &iter, REMMINA_NX_SESSION_COLUMN_ID, val, -1); + break; + case 6: + gtk_list_store_set(nx->session_list, &iter, REMMINA_NX_SESSION_COLUMN_STATUS, val, -1); + break; + default: + break; + } + g_free(val); + + while (*p2 == ' ') + p2++; + p1 = p2; + } + /* The last name column might contains space so it’s not in the above loop. We simply rtrim it here. */ + i = strlen(p1); + if (i < 1) + return; + p2 = p1 + i - 1; + while (*p2 == ' ' && p2 > p1) + p2--; + val = g_strndup(p1, (gint)(p2 - p1 + 1)); + gtk_list_store_set(nx->session_list, &iter, REMMINA_NX_SESSION_COLUMN_NAME, val, -1); + g_free(val); +} + +static gint remmina_nx_session_parse_line(RemminaNXSession *nx, const gchar *line, gchar **valueptr) +{ + TRACE_CALL(__func__); + gchar *s; + gchar *ptr; + gint status; + + *valueptr = NULL; + + /* Get the server version from the initial line */ + if (!nx->version) { + s = g_ascii_strdown(line, -1); + ptr = strstr(s, nx_hello_server_msg); + if (!ptr) { + /* Try to use a default version */ + nx->version = g_strdup("3.3.0"); + } else { + nx->version = g_strdup(ptr + strlen(nx_hello_server_msg)); + ptr = strchr(nx->version, ' '); + if (ptr) + *ptr = '\0'; + /* NoMachine NX append a dash+subversion. Need to be removed. */ + ptr = strchr(nx->version, '-'); + if (ptr) + *ptr = '\0'; + } + g_free(s); + return nx->status; + } + + if (sscanf(line, "NX> %i ", &status) < 1) { + if (nx->session_list_state && nx->session_list) { + if (nx->session_list_state == 1 && strncmp(line, "----", 4) == 0) { + nx->session_list_state = 2; + } else if (nx->session_list_state == 2) { + remmina_nx_session_parse_session_list_line(nx, line); + } + return -1; + } + return nx->status; + } + + nx->session_list_state = 0; + nx->status = status; + ptr = strchr(line, ':'); + if (!ptr) + return status; + *valueptr = ptr + 2; + return status; +} + +static gchar* +remmina_nx_session_get_line(RemminaNXSession *nx) +{ + TRACE_CALL(__func__); + gchar *line; + gchar *pos, *ptr; + gint len; + gint l; + + if (nx->response_pos >= nx->response->len) + return NULL; + + pos = nx->response->str + nx->response_pos; + if ((ptr = strchr(pos, '\n')) == NULL) + return NULL; + + len = ((gint)(ptr - pos)) + 1; + line = g_strndup(pos, len - 1); + + l = strlen(line); + if (l > 0 && line[l - 1] == '\r') { + line[l - 1] = '\0'; + } + + nx->response_pos += len; + + return line; +} + +static gint remmina_nx_session_parse_response(RemminaNXSession *nx) +{ + TRACE_CALL(__func__); + gchar *line; + gchar *pos, *p; + gint status = -1; + + if (nx->response_pos >= nx->response->len) + return -1; + + while ((line = remmina_nx_session_get_line(nx)) != NULL) { + if (nx->log_callback) + nx->log_callback(line); + + status = remmina_nx_session_parse_line(nx, line, &p); + if (status == 500) { + /* 500: Last operation failed. Should be ignored. */ + } else if (status >= 400 && status <= 599) { + remmina_nx_session_set_application_error(nx, "%s", line); + } else { + switch (status) { + case 127: /* Session list */ + nx->session_list_state = 1; + break; + case 148: /* Server capacity not reached for user xxx */ + nx->session_list_state = 0; + nx->allow_start = TRUE; + break; + case 700: + nx->session_id = g_strdup(p); + break; + case 705: + nx->session_display = atoi(p); + break; + case 701: + nx->proxy_cookie = g_strdup(p); + break; + } + } + g_free(line); + + nx->status = status; + } + + pos = nx->response->str + nx->response_pos; + if (sscanf(pos, "NX> %i ", &status) < 1) { + status = nx->status; + } else { + if (nx->log_callback) + nx->log_callback(pos); + nx->response_pos += 8; + } + nx->status = -1; + return status; +} + +static gint remmina_nx_session_expect_status2(RemminaNXSession *nx, gint status, gint status2) +{ + TRACE_CALL(__func__); + gint response; + + while ((response = remmina_nx_session_parse_response(nx)) != status && response != status2) { + if (response == 999) + break; + if (!remmina_nx_session_get_response(nx)) + return -1; + } + nx->session_list_state = 0; + if (remmina_nx_session_has_error(nx)) + return -1; + return response; +} + +static gboolean remmina_nx_session_expect_status(RemminaNXSession *nx, gint status) +{ + TRACE_CALL(__func__); + return (remmina_nx_session_expect_status2(nx, status, 0) == status); +} + +static void remmina_nx_session_send_command(RemminaNXSession *nx, const gchar *cmdfmt, ...) +{ + TRACE_CALL(__func__); + va_list args; + gchar *cmd; + + va_start(args, cmdfmt); + cmd = g_strdup_vprintf(cmdfmt, args); + ssh_channel_write(nx->channel, cmd, strlen(cmd)); + g_free(cmd); + + ssh_set_fd_towrite(nx->session); + ssh_channel_write(nx->channel, "\n", 1); + va_end(args); +} + +gboolean remmina_nx_session_open(RemminaNXSession *nx, const gchar *server, guint port, const gchar *private_key_file, + RemminaNXPassphraseCallback passphrase_func, gpointer userdata) +{ + TRACE_CALL(__func__); + gint ret; + ssh_key priv_key; + gint keytype; + gboolean encrypted; + gchar *passphrase = NULL; + + nx->session = ssh_new(); + ssh_options_set(nx->session, SSH_OPTIONS_HOST, server); + ssh_options_set(nx->session, SSH_OPTIONS_PORT, &port); + ssh_options_set(nx->session, SSH_OPTIONS_USER, "nx"); + + if (private_key_file && private_key_file[0]) { + if (!remmina_get_keytype(private_key_file, &keytype, &encrypted)) { + remmina_nx_session_set_application_error(nx, "Invalid private key file."); + return FALSE; + } + if (encrypted && !passphrase_func(&passphrase, userdata)) { + return FALSE; + } + if ( ssh_pki_import_privkey_file(private_key_file, (passphrase ? passphrase : ""), NULL, NULL, &priv_key) != SSH_OK ) { + remmina_nx_session_set_application_error(nx, "Error importing private key from file."); + g_free(passphrase); + return FALSE; + } + g_free(passphrase); + } else { + /* Use NoMachine’s default nx private key */ + if ( ssh_pki_import_privkey_base64(nx_default_private_key, NULL, NULL, NULL, &priv_key) != SSH_OK ) { + remmina_nx_session_set_application_error(nx, "Failed to import NX default private key."); + return FALSE; + } + } + + if (ssh_connect(nx->session)) { + ssh_key_free(priv_key); + remmina_nx_session_set_error(nx, "Failed to startup SSH session: %s"); + return FALSE; + } + + ret = ssh_userauth_publickey(nx->session, NULL, priv_key); + + ssh_key_free(priv_key); + + if (ret != SSH_AUTH_SUCCESS) { + remmina_nx_session_set_error(nx, "NX SSH authentication failed: %s"); + return FALSE; + } + + if ((nx->channel = ssh_channel_new(nx->session)) == NULL || ssh_channel_open_session(nx->channel) != SSH_OK) { + return FALSE; + } + + if (ssh_channel_request_shell(nx->channel) != SSH_OK) { + return FALSE; + } + + /* NX server starts the session with an initial 105 status */ + if (!remmina_nx_session_expect_status(nx, 105)) + return FALSE; + + /* Say hello to the NX server */ + remmina_nx_session_send_command(nx, "HELLO NXCLIENT - Version %s", nx->version); + if (!remmina_nx_session_expect_status(nx, 105)) + return FALSE; + + /* Set the NX session environment */ + remmina_nx_session_send_command(nx, "SET SHELL_MODE SHELL"); + if (!remmina_nx_session_expect_status(nx, 105)) + return FALSE; + remmina_nx_session_send_command(nx, "SET AUTH_MODE PASSWORD"); + if (!remmina_nx_session_expect_status(nx, 105)) + return FALSE; + + nx->server = g_strdup(server); + + return TRUE; +} + +gboolean remmina_nx_session_login(RemminaNXSession *nx, const gchar *username, const gchar *password) +{ + TRACE_CALL(__func__); + gint response; + + /* Login to the NX server */ + remmina_nx_session_send_command(nx, "login"); + if (!remmina_nx_session_expect_status(nx, 101)) + return FALSE; + remmina_nx_session_send_command(nx, username); + /* NoMachine Testdrive does not prompt for password, in which case 105 response is received without 102 */ + response = remmina_nx_session_expect_status2(nx, 102, 105); + if (response == 102) { + remmina_nx_session_send_command(nx, password); + if (!remmina_nx_session_expect_status(nx, 105)) + return FALSE; + } else if (response != 105) { + return FALSE; + } + + return TRUE; +} + +void remmina_nx_session_add_parameter(RemminaNXSession *nx, const gchar *name, const gchar *valuefmt, ...) +{ + TRACE_CALL(__func__); + va_list args; + gchar *value; + + va_start(args, valuefmt); + value = g_strdup_vprintf(valuefmt, args); + g_hash_table_insert(nx->session_parameters, g_strdup(name), value); + va_end(args); +} + +static gboolean remmina_nx_session_send_session_command(RemminaNXSession *nx, const gchar *cmd_type, gint response) +{ + TRACE_CALL(__func__); + GString *cmd; + GHashTableIter iter; + gchar *key, *value; + + cmd = g_string_new(cmd_type); + g_hash_table_iter_init(&iter, nx->session_parameters); + while (g_hash_table_iter_next(&iter, (gpointer*)&key, (gpointer*)&value)) { + g_string_append_printf(cmd, " --%s=\"%s\"", key, value); + } + + remmina_nx_session_send_command(nx, cmd->str); + g_string_free(cmd, TRUE); + + g_hash_table_remove_all(nx->session_parameters); + + return remmina_nx_session_expect_status(nx, response); +} + +gboolean remmina_nx_session_list(RemminaNXSession *nx) +{ + TRACE_CALL(__func__); + gboolean ret; + + if (nx->session_list == NULL) { + nx->session_list = gtk_list_store_new(REMMINA_NX_SESSION_N_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, + G_TYPE_STRING, G_TYPE_STRING); + } else { + gtk_list_store_clear(nx->session_list); + } + ret = remmina_nx_session_send_session_command(nx, "listsession", 105); + + return ret; +} + +void remmina_nx_session_set_tree_view(RemminaNXSession *nx, GtkTreeView *tree) +{ + TRACE_CALL(__func__); + gtk_tree_view_set_model(tree, GTK_TREE_MODEL(nx->session_list)); +} + +gboolean remmina_nx_session_iter_first(RemminaNXSession *nx, GtkTreeIter *iter) +{ + TRACE_CALL(__func__); + if (!nx->session_list) + return FALSE; + return gtk_tree_model_get_iter_first(GTK_TREE_MODEL(nx->session_list), iter); +} + +gboolean remmina_nx_session_iter_next(RemminaNXSession *nx, GtkTreeIter *iter) +{ + TRACE_CALL(__func__); + if (!nx->session_list) + return FALSE; + return gtk_tree_model_iter_next(GTK_TREE_MODEL(nx->session_list), iter); +} + +gchar* +remmina_nx_session_iter_get(RemminaNXSession *nx, GtkTreeIter *iter, gint column) +{ + TRACE_CALL(__func__); + gchar *val; + + gtk_tree_model_get(GTK_TREE_MODEL(nx->session_list), iter, column, &val, -1); + return val; +} + +void remmina_nx_session_iter_set(RemminaNXSession *nx, GtkTreeIter *iter, gint column, const gchar *data) +{ + TRACE_CALL(__func__); + gtk_list_store_set(nx->session_list, iter, column, data, -1); +} + +gboolean remmina_nx_session_allow_start(RemminaNXSession *nx) +{ + TRACE_CALL(__func__); + return nx->allow_start; +} + +static void remmina_nx_session_add_common_parameters(RemminaNXSession *nx) +{ + TRACE_CALL(__func__); + gchar *value; + + /* Add fixed session parameters for startsession */ + remmina_nx_session_add_parameter(nx, "cache", "16M"); + remmina_nx_session_add_parameter(nx, "images", "64M"); + remmina_nx_session_add_parameter(nx, "render", "1"); + remmina_nx_session_add_parameter(nx, "backingstore", "1"); + remmina_nx_session_add_parameter(nx, "agent_server", ""); + remmina_nx_session_add_parameter(nx, "agent_user", ""); + remmina_nx_session_add_parameter(nx, "agent_password", ""); + + value = g_strdup_printf("%i", nx->encryption); + remmina_nx_session_add_parameter(nx, "encryption", value); + g_free(value); +} + +gboolean remmina_nx_session_start(RemminaNXSession *nx) +{ + TRACE_CALL(__func__); + remmina_nx_session_add_common_parameters(nx); + return remmina_nx_session_send_session_command(nx, "startsession", 105); +} + +gboolean remmina_nx_session_attach(RemminaNXSession *nx) +{ + TRACE_CALL(__func__); + remmina_nx_session_add_common_parameters(nx); + return remmina_nx_session_send_session_command(nx, "attachsession", 105); +} + +gboolean remmina_nx_session_restore(RemminaNXSession *nx) +{ + TRACE_CALL(__func__); + remmina_nx_session_add_common_parameters(nx); + return remmina_nx_session_send_session_command(nx, "restoresession", 105); +} + +gboolean remmina_nx_session_terminate(RemminaNXSession *nx) +{ + TRACE_CALL(__func__); + return remmina_nx_session_send_session_command(nx, "terminate", 105); +} + +static gpointer remmina_nx_session_tunnel_main_thread(gpointer data) +{ + TRACE_CALL(__func__); + RemminaNXSession *nx = (RemminaNXSession*)data; + gchar *ptr; + ssize_t len = 0, lenw = 0; + fd_set set; + struct timeval timeout; + ssh_channel channels[2]; + ssh_channel channels_out[2]; + gint sock; + gint ret; + gchar buffer[10240]; + gchar socketbuffer[10240]; + gchar *socketbuffer_ptr = NULL; + gint socketbuffer_len = 0; + + pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); + pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL); + + /* Accept a local connection */ + sock = accept(nx->server_sock, NULL, NULL); + if (sock < 0) { + remmina_nx_session_set_application_error(nx, "Failed to accept local socket"); + nx->thread = 0; + return NULL; + } + close(nx->server_sock); + nx->server_sock = -1; + + channels[0] = nx->channel; + channels[1] = NULL; + + /* Start the tunnel data transmission */ + while (nx->running) { + timeout.tv_sec = 1; + timeout.tv_usec = 0; + + FD_ZERO(&set); + FD_SET(sock, &set); + + ret = ssh_select(channels, channels_out, sock + 1, &set, &timeout); + if (!nx->running) + break; + if (ret == SSH_EINTR) + continue; + if (ret == -1) + break; + + if (FD_ISSET(sock, &set)) { + len = read(sock, buffer, sizeof(buffer)); + if (len == 0) + nx->running = FALSE; + else if (len > 0) { + for (ptr = buffer, lenw = 0; len > 0; len -= lenw, ptr += lenw) { + ssh_set_fd_towrite(nx->session); + lenw = ssh_channel_write(channels[0], (char*)ptr, len); + if (lenw <= 0) { + nx->running = FALSE; + break; + } + } + } + } + + if (!nx->running) + break; + + if (channels_out[0] && socketbuffer_len <= 0) { + len = ssh_channel_read_nonblocking(channels_out[0], socketbuffer, sizeof(socketbuffer), 0); + if (len == SSH_ERROR || len == SSH_EOF) { + nx->running = FALSE; + break; + } else if (len > 0) { + socketbuffer_ptr = socketbuffer; + socketbuffer_len = len; + } else { + /* Clean up the stderr buffer in case FreeNX send something there */ + len = ssh_channel_read_nonblocking(channels_out[0], buffer, sizeof(buffer), 1); + if (len == SSH_ERROR || len == SSH_EOF) { + nx->running = FALSE; + break; + } + } + } + + if (nx->running && socketbuffer_len > 0) { + for (lenw = 0; socketbuffer_len > 0; socketbuffer_len -= lenw, socketbuffer_ptr += lenw) { + lenw = write(sock, socketbuffer_ptr, socketbuffer_len); + if (lenw == -1 && errno == EAGAIN && nx->running) { + /* Sometimes we cannot write to a socket (always EAGAIN), probably because it’s internal + * buffer is full. We need read the pending bytes from the socket first. so here we simply + * break, leave the buffer there, and continue with other data */ + break; + } + if (lenw <= 0) { + nx->running = FALSE; + break; + } + } + } + } + + nx->thread = 0; + + return NULL; +} + +gboolean remmina_nx_session_tunnel_open(RemminaNXSession *nx) +{ + TRACE_CALL(__func__); + gint port; + gint sock; + gint sockopt = 1; + struct sockaddr_in sin; + + if (!nx->encryption) + return TRUE; + + remmina_nx_session_send_command(nx, "bye"); + if (!remmina_nx_session_expect_status(nx, 999)) { + /* Shoud not happen, just in case */ + remmina_nx_session_set_application_error(nx, "Server won’t say bye to us?"); + return FALSE; + } + + port = (nx->localport ? nx->localport : nx->session_display) + 4000; + + /* Create the server socket that listens on the local port */ + sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock < 0) { + remmina_nx_session_set_application_error(nx, "Failed to create socket."); + return FALSE; + } + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &sockopt, sizeof(sockopt)); + + sin.sin_family = AF_INET; + sin.sin_port = htons(port); + sin.sin_addr.s_addr = inet_addr("127.0.0.1"); + + if (bind(sock, (struct sockaddr *)&sin, sizeof(sin))) { + remmina_nx_session_set_application_error(nx, "Failed to bind on local port."); + close(sock); + return FALSE; + } + + if (listen(sock, 1)) { + remmina_nx_session_set_application_error(nx, "Failed to listen on local port."); + close(sock); + return FALSE; + } + + nx->server_sock = sock; + nx->running = TRUE; + + if (pthread_create(&nx->thread, NULL, remmina_nx_session_tunnel_main_thread, nx)) { + remmina_nx_session_set_application_error(nx, "Failed to initialize pthread."); + nx->thread = 0; + return FALSE; + } + return TRUE; +} + +static gchar* +remmina_nx_session_get_proxy_option(RemminaNXSession *nx) +{ + TRACE_CALL(__func__); + if (nx->encryption) { + return g_strdup_printf("nx,session=%s,cookie=%s,id=%s,shmem=1,shpix=1,connect=127.0.0.1:%i", + (gchar*)g_hash_table_lookup(nx->session_parameters, "session"), nx->proxy_cookie, + nx->session_id, (nx->localport ? nx->localport : nx->session_display)); + } else { + return g_strdup_printf("nx,session=%s,cookie=%s,id=%s,shmem=1,shpix=1,connect=%s:%i", + (gchar*)g_hash_table_lookup(nx->session_parameters, "session"), nx->proxy_cookie, + nx->session_id, nx->server, nx->session_display); + } +} + +gboolean remmina_nx_session_invoke_proxy(RemminaNXSession *nx, gint display, GChildWatchFunc exit_func, gpointer user_data) +{ + TRACE_CALL(__func__); + gchar *argv[50]; + gint argc; + GError *error = NULL; + gboolean ret; + gchar **envp; + gchar *s; + gint i; + + /* Copy all current environment variable, but change DISPLAY. Assume we should always have DISPLAY… */ + if (display >= 0) { + envp = g_listenv(); + for (i = 0; envp[i]; i++) { + if (g_strcmp0(envp[i], "DISPLAY") == 0) { + s = g_strdup_printf("DISPLAY=:%i", display); + } else { + s = g_strdup_printf("%s=%s", envp[i], g_getenv(envp[i])); + } + g_free(envp[i]); + envp[i] = s; + } + } else { + envp = NULL; + } + + argc = 0; + argv[argc++] = g_strdup("nxproxy"); + argv[argc++] = g_strdup("-S"); + argv[argc++] = remmina_nx_session_get_proxy_option(nx); + argv[argc++] = NULL; + + ret = g_spawn_async(NULL, argv, envp, G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL, &nx->proxy_pid, + &error); + g_strfreev(envp); + for (i = 0; i < argc; i++) + g_free(argv[i]); + + if (!ret) { + remmina_nx_session_set_application_error(nx, "%s", error->message); + return FALSE; + } + + if (exit_func) { + nx->proxy_watch_source = g_child_watch_add(nx->proxy_pid, exit_func, user_data); + } + + return TRUE; +} + +void remmina_nx_session_bye(RemminaNXSession *nx) +{ + TRACE_CALL(__func__); + remmina_nx_session_send_command(nx, "bye"); + remmina_nx_session_get_response(nx); +} + diff --git a/plugins/nx/nx_session.h b/plugins/nx/nx_session.h new file mode 100644 index 0000000..9f80919 --- /dev/null +++ b/plugins/nx/nx_session.h @@ -0,0 +1,104 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2010 Vic Lee + * Copyright (C) 2017-2021 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 + +enum { + REMMINA_NX_SESSION_COLUMN_DISPLAY, + REMMINA_NX_SESSION_COLUMN_TYPE, + REMMINA_NX_SESSION_COLUMN_ID, + REMMINA_NX_SESSION_COLUMN_STATUS, + REMMINA_NX_SESSION_COLUMN_NAME, + REMMINA_NX_SESSION_N_COLUMNS +}; + +typedef struct _RemminaNXSession RemminaNXSession; + +typedef gboolean (*RemminaNXPassphraseCallback)(gchar **passphrase, gpointer userdata); +typedef void (*RemminaNXLogCallback)(const gchar *fmt, ...); + +RemminaNXSession *remmina_nx_session_new(void); + +void remmina_nx_session_free(RemminaNXSession *nx); + +gboolean remmina_nx_session_has_error(RemminaNXSession *nx); + +const gchar *remmina_nx_session_get_error(RemminaNXSession *nx); + +void remmina_nx_session_clear_error(RemminaNXSession *nx); + +void remmina_nx_session_set_encryption(RemminaNXSession *nx, gint encryption); + +void remmina_nx_session_set_localport(RemminaNXSession *nx, gint localport); + +void remmina_nx_session_set_log_callback(RemminaNXSession *nx, RemminaNXLogCallback log_callback); + +gboolean remmina_nx_session_open(RemminaNXSession *nx, const gchar *server, guint port, const gchar *private_key_file, RemminaNXPassphraseCallback passphrase_func, gpointer userdata); + +gboolean remmina_nx_session_login(RemminaNXSession *nx, const gchar *username, const gchar *password); + +void remmina_nx_session_add_parameter(RemminaNXSession *nx, const gchar *name, const gchar *valuefmt, ...); + +gboolean remmina_nx_session_list(RemminaNXSession *nx); + +void remmina_nx_session_set_tree_view(RemminaNXSession *nx, GtkTreeView *tree); + +gboolean remmina_nx_session_iter_first(RemminaNXSession *nx, GtkTreeIter *iter); + +gboolean remmina_nx_session_iter_next(RemminaNXSession *nx, GtkTreeIter *iter); + +gchar *remmina_nx_session_iter_get(RemminaNXSession *nx, GtkTreeIter *iter, gint column); + +void remmina_nx_session_iter_set(RemminaNXSession *nx, GtkTreeIter *iter, gint column, const gchar *data); + +gboolean remmina_nx_session_allow_start(RemminaNXSession *nx); + +gboolean remmina_nx_session_start(RemminaNXSession *nx); + +gboolean remmina_nx_session_attach(RemminaNXSession *nx); + +gboolean remmina_nx_session_restore(RemminaNXSession *nx); + +gboolean remmina_nx_session_terminate(RemminaNXSession *nx); + +gboolean remmina_nx_session_tunnel_open(RemminaNXSession *nx); + +gboolean remmina_nx_session_invoke_proxy(RemminaNXSession *nx, gint display, GChildWatchFunc exit_func, gpointer user_data); + +void remmina_nx_session_bye(RemminaNXSession *nx); + +G_END_DECLS diff --git a/plugins/nx/nx_session_manager.c b/plugins/nx/nx_session_manager.c new file mode 100644 index 0000000..7245527 --- /dev/null +++ b/plugins/nx/nx_session_manager.c @@ -0,0 +1,244 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2010 Vic Lee + * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo + * Copyright (C) 2016-2021 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 <X11/Xlib.h> +#include "nx_plugin.h" +#include "nx_session_manager.h" + +static void remmina_nx_session_manager_set_sensitive(RemminaProtocolWidget *gp, gboolean sensitive) +{ + TRACE_CALL(__func__); + RemminaPluginNxData *gpdata = GET_PLUGIN_DATA(gp); + + if (gpdata->attach_session) { + gtk_dialog_set_response_sensitive(GTK_DIALOG(gpdata->manager_dialog), REMMINA_NX_EVENT_TERMINATE, sensitive); + gtk_dialog_set_response_sensitive(GTK_DIALOG(gpdata->manager_dialog), REMMINA_NX_EVENT_ATTACH, sensitive); + }else { + gtk_dialog_set_response_sensitive(GTK_DIALOG(gpdata->manager_dialog), REMMINA_NX_EVENT_TERMINATE, sensitive); + gtk_dialog_set_response_sensitive(GTK_DIALOG(gpdata->manager_dialog), REMMINA_NX_EVENT_RESTORE, sensitive); + } +} + +static gboolean remmina_nx_session_manager_selection_func(GtkTreeSelection *selection, GtkTreeModel *model, GtkTreePath *path, + gboolean path_currently_selected, gpointer user_data) +{ + TRACE_CALL(__func__); + RemminaProtocolWidget *gp = (RemminaProtocolWidget*)user_data; + RemminaPluginNxData *gpdata = GET_PLUGIN_DATA(gp); + + gpdata->manager_selected = FALSE; + if (path_currently_selected) { + remmina_nx_session_manager_set_sensitive(gp, FALSE); + return TRUE; + } + + if (!gtk_tree_model_get_iter(model, &gpdata->iter, path)) + return TRUE; + gpdata->manager_selected = TRUE; + remmina_nx_session_manager_set_sensitive(gp, TRUE); + return TRUE; +} + +static void remmina_nx_session_manager_send_signal(RemminaPluginNxData *gpdata, gint event_type) +{ + TRACE_CALL(__func__); + guchar dummy = (guchar)event_type; + /* Signal the NX thread to resume execution */ + if (write(gpdata->event_pipe[1], &dummy, 1)) { + } +} + +static void remmina_nx_session_manager_on_response(GtkWidget *dialog, gint response_id, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginNxData *gpdata = GET_PLUGIN_DATA(gp); + gint event_type; + + remmina_nx_session_manager_set_sensitive(gp, FALSE); + if (response_id <= 0) { + event_type = REMMINA_NX_EVENT_CANCEL; + }else { + event_type = response_id; + } + if (response_id == REMMINA_NX_EVENT_TERMINATE && gpdata->manager_selected) { + remmina_nx_session_iter_set(gpdata->nx, &gpdata->iter, REMMINA_NX_SESSION_COLUMN_STATUS, _("Terminating…")); + } + if (response_id != REMMINA_NX_EVENT_TERMINATE) { + gtk_widget_destroy(dialog); + gpdata->manager_dialog = NULL; + } + if (response_id != REMMINA_NX_EVENT_TERMINATE && response_id != REMMINA_NX_EVENT_CANCEL) { + remmina_plugin_nx_service->protocol_plugin_init_show(gp); + } + remmina_nx_session_manager_send_signal(gpdata, event_type); +} + +/* Handle double click on a row in the NX Session manager + * Automatically close the dialog using the default response ID */ +void remmina_nx_session_manager_on_row_activated(GtkTreeView *tree, GtkTreePath *path, GtkTreeViewColumn *column, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginNxData *gpdata = GET_PLUGIN_DATA(gp); + REMMINA_PLUGIN_DEBUG("Default response_id %d", + gpdata->default_response); + + if (gpdata->default_response >= 0) { + gtk_dialog_response(GTK_DIALOG(gpdata->manager_dialog), gpdata->default_response); + } +} + +static gboolean remmina_nx_session_manager_main(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginNxData *gpdata = GET_PLUGIN_DATA(gp); + RemminaFile *remminafile; + GtkWidget *dialog; + gchar *s; + GtkWidget *scrolledwindow; + GtkWidget *tree; + GtkCellRenderer *renderer; + GtkTreeViewColumn *column; + + remminafile = remmina_plugin_nx_service->protocol_plugin_get_file(gp); + + gpdata->default_response = -1; + if (!gpdata->manager_started) { + remmina_plugin_nx_service->protocol_plugin_init_hide(gp); + + dialog = gtk_dialog_new(); + s = g_strdup_printf(_("NX sessions on %s"), remmina_plugin_nx_service->file_get_string(remminafile, "server")); + gtk_window_set_title(GTK_WINDOW(dialog), s); + g_free(s); + if (gpdata->attach_session) { + gtk_dialog_add_button(GTK_DIALOG(dialog), _("Attach"), REMMINA_NX_EVENT_ATTACH); + /* Set default response id for attach */ + gpdata->default_response = REMMINA_NX_EVENT_ATTACH; + }else { + gtk_dialog_add_button(GTK_DIALOG(dialog), _("Restore"), REMMINA_NX_EVENT_RESTORE); + gtk_dialog_add_button(GTK_DIALOG(dialog), _("Start"), REMMINA_NX_EVENT_START); + /* Set default response id for restore */ + gpdata->default_response = REMMINA_NX_EVENT_RESTORE; + } + gtk_dialog_add_button(GTK_DIALOG(dialog), _("_Cancel"), REMMINA_NX_EVENT_CANCEL); + + gtk_dialog_add_button(GTK_DIALOG(dialog), _("Terminate"), REMMINA_NX_EVENT_TERMINATE); + + gtk_window_set_default_size(GTK_WINDOW(dialog), 640, 300); + gpdata->manager_dialog = dialog; + + scrolledwindow = gtk_scrolled_window_new(NULL, NULL); + gtk_widget_show(scrolledwindow); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolledwindow), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(dialog))), scrolledwindow, TRUE, TRUE, 0); + + tree = gtk_tree_view_new(); + gtk_container_add(GTK_CONTAINER(scrolledwindow), tree); + gtk_widget_show(tree); + remmina_nx_session_set_tree_view(gpdata->nx, GTK_TREE_VIEW(tree)); + /* Handle double click on the row */ + g_signal_connect(G_OBJECT(tree), "row-activated", + G_CALLBACK(remmina_nx_session_manager_on_row_activated), gp); + + renderer = gtk_cell_renderer_text_new(); + column = gtk_tree_view_column_new_with_attributes("#", renderer, "text", REMMINA_NX_SESSION_COLUMN_ID, NULL); + gtk_tree_view_column_set_resizable(column, TRUE); + gtk_tree_view_column_set_sort_column_id(column, REMMINA_NX_SESSION_COLUMN_ID); + gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column); + + renderer = gtk_cell_renderer_text_new(); + column = gtk_tree_view_column_new_with_attributes(_("Type"), renderer, "text", REMMINA_NX_SESSION_COLUMN_TYPE, + NULL); + gtk_tree_view_column_set_resizable(column, TRUE); + gtk_tree_view_column_set_sort_column_id(column, REMMINA_NX_SESSION_COLUMN_TYPE); + gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column); + + renderer = gtk_cell_renderer_text_new(); + column = gtk_tree_view_column_new_with_attributes(_("Display"), renderer, "text", + REMMINA_NX_SESSION_COLUMN_DISPLAY, NULL); + gtk_tree_view_column_set_resizable(column, TRUE); + gtk_tree_view_column_set_sort_column_id(column, REMMINA_NX_SESSION_COLUMN_DISPLAY); + gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column); + + renderer = gtk_cell_renderer_text_new(); + column = gtk_tree_view_column_new_with_attributes(_("Status"), renderer, "text", + REMMINA_NX_SESSION_COLUMN_STATUS, NULL); + gtk_tree_view_column_set_resizable(column, TRUE); + gtk_tree_view_column_set_sort_column_id(column, REMMINA_NX_SESSION_COLUMN_STATUS); + gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column); + + renderer = gtk_cell_renderer_text_new(); + column = gtk_tree_view_column_new_with_attributes(_("Name"), renderer, "text", REMMINA_NX_SESSION_COLUMN_NAME, + NULL); + gtk_tree_view_column_set_resizable(column, TRUE); + gtk_tree_view_column_set_sort_column_id(column, REMMINA_NX_SESSION_COLUMN_NAME); + gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column); + + gtk_tree_selection_set_select_function(gtk_tree_view_get_selection(GTK_TREE_VIEW(tree)), + remmina_nx_session_manager_selection_func, gp, NULL); + + g_signal_connect(G_OBJECT(dialog), "response", G_CALLBACK(remmina_nx_session_manager_on_response), gp); + gpdata->manager_started = TRUE; + } + gpdata->manager_selected = FALSE; + if (gpdata->manager_dialog) { + remmina_nx_session_manager_set_sensitive(gp, FALSE); + gtk_widget_show(gpdata->manager_dialog); + } + if (remmina_nx_session_has_error(gpdata->nx)) { + dialog = gtk_message_dialog_new((gpdata->manager_dialog ? GTK_WINDOW(gpdata->manager_dialog) : NULL), + GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "%s", + remmina_nx_session_get_error(gpdata->nx)); + remmina_nx_session_clear_error(gpdata->nx); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + remmina_nx_session_manager_send_signal(gpdata, 0); + } + + gpdata->session_manager_start_handler = 0; + return FALSE; +} + +void remmina_nx_session_manager_start(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginNxData *gpdata = GET_PLUGIN_DATA(gp); + + if (gpdata->session_manager_start_handler == 0) { + gpdata->session_manager_start_handler = IDLE_ADD((GSourceFunc)remmina_nx_session_manager_main, gp); + } +} + diff --git a/plugins/nx/nx_session_manager.h b/plugins/nx/nx_session_manager.h new file mode 100644 index 0000000..302b9d3 --- /dev/null +++ b/plugins/nx/nx_session_manager.h @@ -0,0 +1,42 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2010 Vic Lee + * Copyright (C) 2017-2021 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_nx_session_manager_start(RemminaProtocolWidget *gp); + +G_END_DECLS diff --git a/plugins/nx/scalable/emblems/remmina-nx-symbolic.svg b/plugins/nx/scalable/emblems/remmina-nx-symbolic.svg new file mode 100644 index 0000000..a6c7bd1 --- /dev/null +++ b/plugins/nx/scalable/emblems/remmina-nx-symbolic.svg @@ -0,0 +1,99 @@ +<?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="nx protocol.svg"> + <defs + id="defs2" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:zoom="1.4839967" + inkscape:cx="5.8174509" + inkscape:cy="56.488684" + 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="-190.52733" + originy="-102.24438" /> + </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.52732,-168.29729)"> + <g + id="g822" + transform="translate(0.02606175,-4.0269583)"> + <path + style="isolation:isolate;fill:#171717;stroke-width:0.26458332" + inkscape:connector-curvature="0" + id="path25" + d="m 190.52732,192.75511 v -2.00025 h 2.00052 2.00025 v 2.00025 2.00051 h -2.00025 -2.00052 z" /> + <path + style="isolation:isolate;fill:#171717;stroke-width:0.26458332" + inkscape:connector-curvature="0" + id="path27" + d="m 197.72902,185.55341 v -9.2022 h 9.60225 9.60226 v 9.2022 9.20221 h -2.00025 -2.00051 v -7.20169 -7.20169 h -1.60047 -1.60046 v 7.20169 7.20169 h -2.40057 -2.40056 v -7.20169 -7.20169 h -1.20015 -1.20042 v 7.20169 7.20169 h -2.40056 -2.40056 z" /> + <path + style="isolation:isolate;fill:#171717;stroke-width:0.26458332" + inkscape:connector-curvature="0" + id="path29" + d="m 190.52732,182.7528 v -6.40159 h 2.00052 2.00025 v 6.40159 2.9972 3.40439 h -2.00025 -2.00052 z" /> + </g> + </g> +</svg> |