Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/Remmina/remmina-plugins.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAntenore Gatta (tmow) <antenore@simbiosi.org>2021-06-30 08:37:59 +0300
committerAntenore Gatta (tmow) <antenore@simbiosi.org>2021-06-30 08:37:59 +0300
commit7148df83c03e50a3bb049a1f425e40c68a0b24c9 (patch)
tree2cf28bd6586e55c0e3a414d8bb3ea0573e6c6576
parentae75b74ca7215fc44eee5558b51262e99a504fd8 (diff)
parent251a2ef5210b2527ec2a959e7356f6cd1a8fc1d2 (diff)
Add 'plugins/nx/' from commit '251a2ef5210b2527ec2a959e7356f6cd1a8fc1d2'
git-subtree-dir: plugins/nx git-subtree-mainline: ae75b74ca7215fc44eee5558b51262e99a504fd8 git-subtree-split: 251a2ef5210b2527ec2a959e7356f6cd1a8fc1d2
-rw-r--r--plugins/nx/CMakeLists.txt67
-rw-r--r--plugins/nx/nx_plugin.c831
-rw-r--r--plugins/nx/nx_plugin.h81
-rw-r--r--plugins/nx/nx_session.c1008
-rw-r--r--plugins/nx/nx_session.h104
-rw-r--r--plugins/nx/nx_session_manager.c244
-rw-r--r--plugins/nx/nx_session_manager.h42
-rw-r--r--plugins/nx/scalable/emblems/remmina-nx-symbolic.svg99
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>