diff options
Diffstat (limited to 'plugins/nx/nx_session.c')
-rw-r--r-- | plugins/nx/nx_session.c | 1008 |
1 files changed, 1008 insertions, 0 deletions
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); +} + |