/* * Project: Remmina Plugin X2Go * Description: Remmina protocol plugin to connect via X2Go using PyHocaCLI * Author: Mike Gabriel * Antenore Gatta * Copyright: 2010-2011 Vic Lee * 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo * 2015 Antenore Gatta * 2016-2018 Antenore Gatta, Giovanni Panozzo * 2019 Mike Gabriel * 2021 Daniel Teichmann * License: GPL-2+ * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * * In addition, as a special exception, the copyright holders give * permission to link the code of portions of this program with the * OpenSSL library under certain conditions as described in each * individual source file, and distribute linked combinations * including the two. * You must obey the GNU General Public License in all respects * for all of the code used other than OpenSSL. * If you modify * file(s) with this exception, you may extend this exception to your * version of the file(s), but you are not obligated to do so. * If you * do not wish to do so, delete this exception statement from your * version. * If you delete this exception statement from all source * files in the program, then also delete it here. * */ #include "x2go_plugin.h" #include "common/remmina_plugin.h" #include #ifdef GDK_WINDOWING_X11 #include #elif defined(GDK_WINDOWING_WAYLAND) #include #endif #include #include #include #include #include #include #include #define FEATURE_AVAILABLE(gpdata, feature) \ gpdata->available_features ? (g_list_find_custom( \ gpdata->available_features, \ feature, \ (GCompareFunc) g_strcmp0 \ ) ? TRUE : FALSE) : FALSE #define FEATURE_NOT_AVAIL_STR(feature) \ g_strdup_printf(_("The command-line feature '%s' is not available! Attempting " \ "to start PyHoca-CLI without using this feature…"), feature) #define GET_PLUGIN_DATA(gp) \ (RemminaPluginX2GoData*) g_object_get_data(G_OBJECT(gp), "plugin-data") // --------- SESSIONS ------------ #define SET_RESUME_SESSION(gp, resume_data) \ g_object_set_data_full(G_OBJECT(gp), "resume-session-data", \ resume_data, \ g_free) #define GET_RESUME_SESSION(gp) \ (gchar*) g_object_get_data(G_OBJECT(gp), "resume-session-data") // A session is selected if the returning value is something other than 0. #define IS_SESSION_SELECTED(gp) \ g_object_get_data(G_OBJECT(gp), "session-selected") ? TRUE : FALSE // We don't use the function as a real pointer but rather as a boolean value. #define SET_SESSION_SELECTED(gp, is_session_selected) \ g_object_set_data_full(G_OBJECT(gp), "session-selected", \ is_session_selected, \ NULL) // ------------------- #define REMMINA_PLUGIN_INFO(fmt, ...) \ rm_plugin_service->_remmina_info("[%s] " fmt, \ PLUGIN_NAME, ##__VA_ARGS__) #define REMMINA_PLUGIN_MESSAGE(fmt, ...) \ rm_plugin_service->_remmina_message("[%s] " fmt, \ PLUGIN_NAME, ##__VA_ARGS__) #define REMMINA_PLUGIN_DEBUG(fmt, ...) \ rm_plugin_service->_remmina_debug(__func__, "[%s] " fmt, \ PLUGIN_NAME, ##__VA_ARGS__) #define REMMINA_PLUGIN_WARNING(fmt, ...) \ rm_plugin_service->_remmina_warning(__func__, "[%s] " fmt, \ PLUGIN_NAME, ##__VA_ARGS__) #define REMMINA_PLUGIN_AUDIT(fmt, ...) \ rm_plugin_service->_remmina_audit(__func__, fmt, ##__VA_ARGS__) #define REMMINA_PLUGIN_ERROR(fmt, ...) \ rm_plugin_service->_remmina_error(__func__, "[%s] " fmt, \ PLUGIN_NAME, ##__VA_ARGS__) #define REMMINA_PLUGIN_CRITICAL(fmt, ...) \ rm_plugin_service->_remmina_critical(__func__, "[%s] " fmt, \ PLUGIN_NAME, ##__VA_ARGS__) #define GET_PLUGIN_STRING(value) \ g_strdup(rm_plugin_service->file_get_string(remminafile, value)) #define GET_PLUGIN_PASSWORD(value) \ GET_PLUGIN_STRING(value) #define GET_PLUGIN_INT(value, default_value) \ rm_plugin_service->file_get_int(remminafile, value, default_value) #define GET_PLUGIN_BOOLEAN(value) \ rm_plugin_service->file_get_int(remminafile, value, FALSE) static RemminaPluginService *rm_plugin_service = NULL; typedef struct _RemminaPluginX2GoData { GtkWidget *socket; gint socket_id; pthread_t thread; Display *display; Window window_id; int (*orig_handler)(Display *, XErrorEvent *); GPid pidx2go; gboolean disconnected; GList* available_features; } RemminaPluginX2GoData; /** * @brief Can be used to pass custom user data between functions and threads. * *AND* pass the useful RemminaProtocolWidget with it along. */ typedef struct _X2GoCustomUserData { RemminaProtocolWidget* gp; gpointer dialog_data; gpointer connect_data; gpointer opt1; gpointer opt2; } X2GoCustomUserData; /** * @brief Used for the session chooser dialog (GtkListStore) * See the example at: https://docs.gtk.org/gtk3/class.ListStore.html * The order is the exact same as the user sees in the dialog. * SESSION_NUM_PROPERTIES is used to keep count of the properties * and it must be the last object. */ enum SESSION_PROPERTIES { SESSION_DISPLAY = 0, SESSION_STATUS, SESSION_SESSION_ID, SESSION_SUSPENDED_SINCE, SESSION_CREATE_DATE, SESSION_AGENT_PID, SESSION_USERNAME, SESSION_HOSTNAME, SESSION_COOKIE, SESSION_GRAPHIC_PORT, SESSION_SND_PORT, SESSION_SSHFS_PORT, SESSION_DIALOG_IS_VISIBLE, SESSION_NUM_PROPERTIES // Must be last. Counts all enum elements. }; // Following str2int code was adapted from Stackoverflow: // https://stackoverflow.com/questions/7021725/how-to-convert-a-string-to-integer-in-c typedef enum _str2int_errno { STR2INT_SUCCESS, STR2INT_OVERFLOW, STR2INT_UNDERFLOW, STR2INT_INCONVERTIBLE, STR2INT_INVALID_DATA } str2int_errno; /** * @brief Convert string s to int out. * * @param out The converted int. Cannot be NULL. * * @param s Input string to be converted. \n * The format is the same as strtol, * except that the following are inconvertible: \n * * empty string \n * * leading whitespace \n * * or any trailing characters that are not part of the number \n * Cannot be NULL. * @param base Base to interpret string in. Same range as strtol (2 to 36). * * @return Indicates if the operation succeeded, or why it failed with str2int_errno enum. */ str2int_errno str2int(gint *out, gchar *s, gint base) { gchar *end; if (!s || !out || base <= 0) return STR2INT_INVALID_DATA; if (s[0] == '\0' || isspace(s[0])) return STR2INT_INCONVERTIBLE; errno = 0; glong l = strtol(s, &end, base); /* Both checks are needed because INT_MAX == LONG_MAX is possible. */ if (l > INT_MAX || (errno == ERANGE && l == LONG_MAX)) return STR2INT_OVERFLOW; if (l < INT_MIN || (errno == ERANGE && l == LONG_MIN)) return STR2INT_UNDERFLOW; if (*end != '\0') return STR2INT_INCONVERTIBLE; *out = l; return STR2INT_SUCCESS; } /** * DialogData: * @param flags see GtkDialogFlags * @param type see GtkMessageType * @param buttons see GtkButtonsType * @param title Title of the Dialog * @param message Message of the Dialog * @param callbackfunc A GCallback function which will be executed on the dialogs * 'response' signal. Allowed to be NULL. \n * The callback function is obliged to destroy the dialog widget. \n * @param dialog_factory A user-defined callback function that is called when it is time * to build the actual GtkDialog. \n * Can be used to build custom dialogs. Allowed to be NULL. * * * The `DialogData` structure contains all info needed to open a GTK dialog with * rmplugin_x2go_open_dialog() * * Quick example of a callback function: \n * static gboolean rmplugin_x2go_test_callback(RemminaProtocolWidget *gp, gint response_id, \n * GtkDialog *self) { \n * REMMINA_PLUGIN_DEBUG("response: %i", response_id); \n * if (response_id == GTK_RESPONSE_OK) { \n * REMMINA_PLUGIN_DEBUG("OK!"); \n * } \n * gtk_widget_destroy(self); \n * return G_SOURCE_REMOVE; \n * } * */ struct _DialogData { GtkWindow *parent; GtkDialogFlags flags; GtkMessageType type; GtkButtonsType buttons; gchar *title; gchar *message; GCallback callbackfunc; // If the dialog needs to be custom. GCallback dialog_factory_func; gpointer dialog_factory_data; }; /** * @param custom_data X2GoCustomUserData structure with the following: \n * gp -> gp (RemminaProtocolWidget*) \n * dialog_data -> dialog data (struct _DialogData*) * @returns: FALSE. This source should be removed from main loop. * #G_SOURCE_CONTINUE and #G_SOURCE_REMOVE are more memorable * names for the return value. */ static gboolean rmplugin_x2go_open_dialog(X2GoCustomUserData *custom_data) { REMMINA_PLUGIN_DEBUG("Function entry."); if (!custom_data || !custom_data->gp || !custom_data->dialog_data) { REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( _("Internal error: %s"), _("Parameter 'custom_data' is not initialized!") )); return G_SOURCE_REMOVE; } RemminaProtocolWidget *gp = (RemminaProtocolWidget*) custom_data->gp; struct _DialogData *ddata = (struct _DialogData*) custom_data->dialog_data; if (ddata) { // Can't check type, flags or buttons // because they are enums and '0' is a valid value if (!ddata->title || !ddata->message) { REMMINA_PLUGIN_CRITICAL("%s", _("Broken `DialogData`! Aborting…")); return G_SOURCE_REMOVE; } } else { REMMINA_PLUGIN_CRITICAL("%s", _("Can't retrieve `DialogData`! Aborting…")); return G_SOURCE_REMOVE; } REMMINA_PLUGIN_DEBUG("`DialogData` checks passed. Now showing dialog…"); GtkWidget* widget_gtk_dialog = NULL; if (ddata->dialog_factory_func != NULL) { REMMINA_PLUGIN_DEBUG("Calling *custom* dialog factory function…"); GCallback dialog_factory_func = G_CALLBACK(ddata->dialog_factory_func); gpointer dialog_factory_data = ddata->dialog_factory_data; // Calling dialog_factory_func(custom_data, dialog_factory_data); widget_gtk_dialog = ((GtkWidget* (*)(X2GoCustomUserData*, gpointer)) dialog_factory_func)(custom_data, dialog_factory_data); } else { widget_gtk_dialog = gtk_message_dialog_new(ddata->parent, ddata->flags, ddata->type, ddata->buttons, "%s", ddata->title); gtk_message_dialog_format_secondary_text( GTK_MESSAGE_DIALOG(widget_gtk_dialog), "%s", ddata->message); } if (!widget_gtk_dialog) { REMMINA_PLUGIN_CRITICAL("Error! Aborting."); return G_SOURCE_REMOVE; } if (ddata->callbackfunc) { g_signal_connect_swapped(G_OBJECT(widget_gtk_dialog), "response", G_CALLBACK(ddata->callbackfunc), custom_data); } else { g_signal_connect(G_OBJECT(widget_gtk_dialog), "response", G_CALLBACK(gtk_widget_destroy), NULL); } gtk_widget_show_all(widget_gtk_dialog); // Delete ddata object and reference 'dialog-data' in gp. g_object_set_data(G_OBJECT(gp), "dialog-data", NULL); return G_SOURCE_REMOVE; } /** * @brief These define the responses of session-chooser-dialog's buttons. */ enum SESSION_CHOOSER_RESPONSE_TYPE { SESSION_CHOOSER_RESPONSE_NEW = 0, SESSION_CHOOSER_RESPONSE_CHOOSE, SESSION_CHOOSER_RESPONSE_TERMINATE, }; /** * @brief Finds a child GtkWidget of a parent GtkWidget. * Copied from https://stackoverflow.com/a/23497087 ;) * * @param parent Parent GtkWidget* * @param name Name string of child. (Must be set before, er else it will be a * default string) * @return GtkWidget* */ static GtkWidget* rmplugin_x2go_find_child(GtkWidget* parent, const gchar* name) { const gchar* parent_name = gtk_widget_get_name((GtkWidget*) parent); if (g_ascii_strcasecmp(parent_name, (gchar*) name) == 0) { return parent; } if (GTK_IS_BIN(parent)) { GtkWidget *child = gtk_bin_get_child(GTK_BIN(parent)); return rmplugin_x2go_find_child(child, name); } if (GTK_IS_CONTAINER(parent)) { GList *children = gtk_container_get_children(GTK_CONTAINER(parent)); while (children != NULL) { GtkWidget *widget = rmplugin_x2go_find_child(children->data, name); if (widget != NULL) { return widget; } children = g_list_next(children); } } return NULL; } /** * @brief Gets executed on "row-activated" signal. It is emitted when the method when * the user double clicks a treeview row. It is also emitted when a non-editable * row is selected and one of the keys: Space, Shift+Space, Return or Enter is * pressed. * * @param custom_data X2GoCustomUserData structure with the following: \n * gp -> gp (RemminaProtocolWidget*) \n * opt1 -> dialog widget (GtkWidget*) */ static gboolean rmplugin_x2go_session_chooser_row_activated(GtkTreeView *treeview, GtkTreePath *path, GtkTreeViewColumn *column, X2GoCustomUserData *custom_data) { REMMINA_PLUGIN_DEBUG("Function entry."); if (!custom_data || !custom_data->gp || !custom_data->opt1) { REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( _("Internal error: %s"), _("Parameter 'custom_data' is not initialized!") )); return G_SOURCE_REMOVE; } RemminaProtocolWidget* gp = (RemminaProtocolWidget*) custom_data->gp; // dialog_data (unused) // connect_data (unused) GtkWidget* dialog = GTK_WIDGET(custom_data->opt1); gchar *session_id; GtkTreeIter iter; GtkTreeModel *model = gtk_tree_view_get_model(treeview); if (gtk_tree_model_get_iter(model, &iter, path)) { gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, SESSION_SESSION_ID, &session_id, -1); // Silent bail out. if (!session_id || strlen(session_id) <= 0) return G_SOURCE_REMOVE; SET_RESUME_SESSION(gp, session_id); // Unstucking main process. Telling it that a session has been selected. // We use a trick here. As long as there is something other than 0 // stored, a session is selected. So we use the gpointer as a gboolean. SET_SESSION_SELECTED(gp, (gpointer) TRUE); gtk_widget_hide(GTK_WIDGET(dialog)); gtk_widget_destroy(GTK_WIDGET(dialog)); } return G_SOURCE_REMOVE; } /** * @brief Translates a session property (described by SESSION_PROPERTIES enum) to a * string containing it's display name. * * @param session_property A session property. (as described by SESSION_PROPERTIES enum) * @return gchar* Translated display name. (Can be NULL, if session_property is invalid!) */ static gchar *rmplugin_x2go_session_property_to_string(guint session_property) { gchar* return_char = NULL; switch (session_property) { // I think we can close one eye here regarding max line-length. case SESSION_DISPLAY: return_char = g_strdup(_("X Display")); break; case SESSION_STATUS: return_char = g_strdup(_("Status")); break; case SESSION_SESSION_ID: return_char = g_strdup(_("Session ID")); break; case SESSION_CREATE_DATE: return_char = g_strdup(_("Create date")); break; case SESSION_SUSPENDED_SINCE: return_char = g_strdup(_("Suspended since")); break; case SESSION_AGENT_PID: return_char = g_strdup(_("Agent PID")); break; case SESSION_USERNAME: return_char = g_strdup(_("Username")); break; case SESSION_HOSTNAME: return_char = g_strdup(_("Hostname")); break; case SESSION_COOKIE: return_char = g_strdup(_("Cookie")); break; case SESSION_GRAPHIC_PORT: return_char = g_strdup(_("Graphic port")); break; case SESSION_SND_PORT: return_char = g_strdup(_("SND port")); break; case SESSION_SSHFS_PORT: return_char = g_strdup(_("SSHFS port")); break; case SESSION_DIALOG_IS_VISIBLE: return_char = g_strdup(_("Visible")); break; } return return_char; } /** * @brief Builds a dialog which contains all found X2Go-Sessions of the remote server. * The dialog gives the user the option to choose between resuming or terminating * an existing session or to create a new one. * * @param custom_data X2GoCustomUserData structure with the following: \n * gp -> gp (RemminaProtocolWidget*) \n * dialog_data -> dialog data (struct _DialogData*) \n * connect_data -> connection data (struct _ConnectionData*) * @param sessions_list The GList* Should contain all found X2Go-Sessions. * Sessions are string arrays of properties. * The type of the GList is gchar**. * * @returns GtkWidget* custom dialog. */ static GtkWidget* rmplugin_x2go_choose_session_dialog_factory(X2GoCustomUserData *custom_data, GList *sessions_list) { REMMINA_PLUGIN_DEBUG("Function entry."); if (!custom_data || !custom_data->gp || !custom_data->dialog_data || !custom_data->connect_data) { REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( _("Internal error: %s"), _("Parameter 'custom_data' is not initialized!") )); return NULL; } struct _DialogData* ddata = (struct _DialogData*) custom_data->dialog_data; if (!ddata || !sessions_list || !ddata->title) { REMMINA_PLUGIN_CRITICAL("%s", _("Could not retrieve valid `DialogData` or " "`sessions_list`! Aborting…")); return NULL; } GtkWidget *widget_gtk_dialog = NULL; widget_gtk_dialog = gtk_dialog_new_with_buttons(ddata->title, ddata->parent, ddata->flags, // TRANSLATORS: Stick to x2goclient's translation for terminate. _("_Terminate"), SESSION_CHOOSER_RESPONSE_TERMINATE, // TRANSLATORS: Stick to x2goclient's translation for resume. _("_Resume"), SESSION_CHOOSER_RESPONSE_CHOOSE, _("_New"), SESSION_CHOOSER_RESPONSE_NEW, NULL); GtkWidget *button = gtk_dialog_get_widget_for_response( GTK_DIALOG(widget_gtk_dialog), SESSION_CHOOSER_RESPONSE_TERMINATE); // TRANSLATORS: Tooltip for terminating button inside Session-Chooser-Dialog. // TRANSLATORS: Please stick to X2GoClient's way of translating. gtk_widget_set_tooltip_text(button, _("Terminating X2Go sessions can take a moment.")); #define DEFAULT_DIALOG_WIDTH 720 #define DEFAULT_DIALOG_HEIGHT (DEFAULT_DIALOG_WIDTH * 9) / 16 gtk_widget_set_size_request(GTK_WIDGET(widget_gtk_dialog), DEFAULT_DIALOG_WIDTH, DEFAULT_DIALOG_HEIGHT); gtk_window_set_default_size(GTK_WINDOW(widget_gtk_dialog), DEFAULT_DIALOG_WIDTH, DEFAULT_DIALOG_HEIGHT); gtk_window_set_resizable(GTK_WINDOW(widget_gtk_dialog), TRUE); GtkWidget *scrolled_window = gtk_scrolled_window_new(NULL, NULL); //gtk_widget_show(scrolled_window); gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area( GTK_DIALOG(widget_gtk_dialog)) ), GTK_WIDGET(scrolled_window), TRUE, TRUE, 5); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW (scrolled_window), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); GType types[SESSION_NUM_PROPERTIES]; // First to last in SESSION_PROPERTIES. for (gint i = 0; i < SESSION_NUM_PROPERTIES; ++i) { // Everything is a String. (Except IS_VISIBLE flag) // If that changes one day, you could extent the if statement here. // But you would propably need a *lot* of refactoring. // Especially in the session parser. if (i == SESSION_DIALOG_IS_VISIBLE) { types[i] = G_TYPE_BOOLEAN; } else { types[i] = G_TYPE_STRING; } } // create tree view GtkListStore *store = gtk_list_store_newv(SESSION_NUM_PROPERTIES, types); GtkTreeModelFilter *filter = GTK_TREE_MODEL_FILTER( gtk_tree_model_filter_new(GTK_TREE_MODEL(store), NULL) ); gtk_tree_model_filter_set_visible_column(filter, SESSION_DIALOG_IS_VISIBLE); GtkWidget *tree_view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(filter)); g_object_unref (G_OBJECT (store)); // tree now holds reference gtk_widget_set_size_request(tree_view, -1, 300); // Gets name to be findable by rmplugin_x2go_find_child() gtk_widget_set_name(GTK_WIDGET(tree_view), "session_chooser_treeview"); // create list view columns gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(tree_view), TRUE); gtk_tree_view_set_headers_clickable (GTK_TREE_VIEW(tree_view), FALSE); gtk_tree_view_set_enable_search(GTK_TREE_VIEW(tree_view), TRUE); gtk_widget_show (tree_view); gtk_container_add (GTK_CONTAINER(scrolled_window), tree_view); GtkTreeViewColumn *tree_view_col = NULL; GtkCellRenderer *cell_renderer = NULL; gchar *header_title = NULL; // First to last in SESSION_PROPERTIES. for (guint i = 0; i < SESSION_NUM_PROPERTIES; ++i) { // Do not display SESSION_DIALOG_IS_VISIBLE. if (i == SESSION_DIALOG_IS_VISIBLE) continue; header_title = rmplugin_x2go_session_property_to_string(i); if (!header_title) { REMMINA_PLUGIN_WARNING("%s", g_strdup_printf( _("Internal error: %s"), g_strdup_printf( _("Unknown property '%i'"), i ))); header_title = g_strdup_printf(_("Unknown property '%i'"), i); } tree_view_col = gtk_tree_view_column_new(); gtk_tree_view_column_set_title(tree_view_col, header_title); gtk_tree_view_column_set_clickable(tree_view_col, FALSE); gtk_tree_view_column_set_sizing (tree_view_col, GTK_TREE_VIEW_COLUMN_AUTOSIZE); gtk_tree_view_column_set_resizable(tree_view_col, TRUE); cell_renderer = gtk_cell_renderer_text_new(); gtk_tree_view_column_pack_start(tree_view_col, cell_renderer, TRUE); gtk_tree_view_column_add_attribute(tree_view_col, cell_renderer, "text", i); gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view), tree_view_col); } GList *elem = NULL; GtkTreeIter iter; for (elem = sessions_list; elem; elem = elem->next) { gchar** session = (gchar**) elem->data; g_assert(session != NULL); gtk_list_store_append(store, &iter); for (gint i = 0; i < SESSION_NUM_PROPERTIES; i++) { gchar* property = session[i]; GValue a = G_VALUE_INIT; // Everything here is a string (except SESSION_DIALOG_IS_VISIBLE) if (i == SESSION_DIALOG_IS_VISIBLE) { g_value_init(&a, G_TYPE_BOOLEAN); g_assert(G_VALUE_HOLDS_BOOLEAN(&a) && "GValue does not " "hold a boolean!"); // Default is to show every new session. g_value_set_boolean(&a, TRUE); } else { g_value_init(&a, G_TYPE_STRING); g_assert(G_VALUE_HOLDS_STRING(&a) && "GValue does not " "hold a string!"); g_value_set_static_string (&a, property); } gtk_list_store_set_value(store, &iter, i, &a); } } /* Prepare X2GoCustomUserData *custom_data * gp -> gp (RemminaProtocolWidget*) * dialog_data -> dialog data (struct _DialogData*) * connect_data -> connection data (struct _ConnectionData*) * opt1 -> dialog widget (GtkWidget*) */ // everything else is already initialized. custom_data->opt1 = widget_gtk_dialog; g_signal_connect(tree_view, "row-activated", G_CALLBACK(rmplugin_x2go_session_chooser_row_activated), custom_data); return widget_gtk_dialog; } /** * @brief Uses either 'dialog' or 'treeview' to return the GtkTreeModel of the * Session-Chooser-Dialog. Directly giving 'treeview' as a parameter is faster. * Only *one* parameter has to be given. The other one can be NULL. * Error messages are all handled already. * * @param dialog The Session-Chooser-Dialog itself. (Slower) Can be NULL. * @param treeview The GtkTreeView of the Session-Chooser-Dialog. (faster) Can be NULL. * @return GtkTreeModelFilter* (Does not contains all rows, only the visible ones!!!) \n * But you can get all rows with: \n * gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(return_model)); */ static GtkTreeModelFilter* rmplugin_x2go_session_chooser_get_filter_model(GtkWidget *dialog, GtkTreeView* treeview) { //REMMINA_PLUGIN_DEBUG("Function entry."); GtkTreeModel *return_model = NULL; if (!treeview && dialog) { GtkWidget *treeview_new = rmplugin_x2go_find_child(GTK_WIDGET(dialog), "session_chooser_treeview"); if (!treeview_new) { REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( _("Internal error: %s"), _("Could not find child GtkTreeView of " "session chooser dialog.") )); return NULL; } return_model = gtk_tree_view_get_model(GTK_TREE_VIEW(treeview_new)); } else if (treeview) { return_model = gtk_tree_view_get_model(GTK_TREE_VIEW(treeview)); } else { REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( _("Internal error: %s"), _("Neither the 'dialog' nor 'treeview' parameters are initialized! " "At least one of them must be given.") )); return NULL; } if (!return_model || !GTK_TREE_MODEL_FILTER(return_model)) { REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( _("Internal error: %s"), _("Could not obtain \"GtkTreeModelFilter*\" of the session chooser dialog, " "for unknown reason.") )); } return GTK_TREE_MODEL_FILTER(return_model); } /** * @brief Gets the selected row of the Session-Chooser-Dialog. * The path gets converted with gtk_tree_model_filter_convert_child_path_to_path() * before it gets returned. So path describes a row of 'filter' and *not* its * child GtkTreeModel. * * @param dialog The Session-Chooser-Dialog. * @return GtkTreePath* describing the path to the row. */ static GtkTreePath* rmplugin_x2go_session_chooser_get_selected_row(GtkWidget *dialog) { REMMINA_PLUGIN_DEBUG("Function entry."); GtkWidget *treeview = rmplugin_x2go_find_child(GTK_WIDGET(dialog), "session_chooser_treeview"); if (!treeview) { REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( _("Internal error: %s"), _("Could not find child GtkTreeView of session chooser dialog.") )); return NULL; } GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(treeview)); if (!selection) { REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( _("Internal error: %s"), _("Could not get currently selected row (session)!") )); return NULL; } GtkTreeModelFilter *filter = rmplugin_x2go_session_chooser_get_filter_model( NULL, GTK_TREE_VIEW(treeview)); GtkTreeModel *model = gtk_tree_model_filter_get_model(filter); if (!model) return NULL; // error message was already handled. GtkTreeModel *filter_model = GTK_TREE_MODEL(filter); g_assert(filter_model && "Could not cast 'filter' to a GtkTreeModel!"); GList *selected_rows = gtk_tree_selection_get_selected_rows(selection, &filter_model); // We only support single selection. gint selected_rows_num = gtk_tree_selection_count_selected_rows(selection); if (selected_rows_num != 1) { REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( _("Internal error: %s"), g_strdup_printf( _("Exactly one session should be selectable but '%i' rows " "(sessions) are selected."), selected_rows_num ))); return NULL; } // This would be very dangerous (we didn't check for NULL) if we hadn't just // checked that only one row is selected. GtkTreePath *path = selected_rows->data; // Convert to be path of GtkTreeModelFilter and *not* its child GtkTreeModel. path = gtk_tree_model_filter_convert_child_path_to_path(filter, path); return path; } /** * @brief Finds the GtkTreeView inside of the session chooser dialog, * determines the selected row and extracts a property. * * @param dialog GtkWidget* the dialog itself. * @param property_index Index of property. * @param row A specific row to get the property of. (Can be NULL) * * @return GValue The value of property. (Can be non-initialized!) */ static GValue rmplugin_x2go_session_chooser_get_property(GtkWidget *dialog, gint property_index, GtkTreePath *row) { //REMMINA_PLUGIN_DEBUG("Function entry."); GValue ret_value = G_VALUE_INIT; if (!row) { GtkTreePath *selected_row = rmplugin_x2go_session_chooser_get_selected_row(dialog); if (!selected_row) return ret_value; // error message was already handled. row = selected_row; } GtkTreeModelFilter *filter = rmplugin_x2go_session_chooser_get_filter_model(dialog, NULL); GtkTreeModel *model = gtk_tree_model_filter_get_model(filter); if (!model) return ret_value; // error message was already handled. GtkTreeIter iter; gboolean success = gtk_tree_model_get_iter(model, &iter, row); if (!success) { REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( _("Internal error: %s"), _("Failed to fill 'GtkTreeIter'.") )); return ret_value; } GValue property = G_VALUE_INIT; gtk_tree_model_get_value(model, &iter, property_index, &property); return property; } /** * @brief This function dumps all properties of a session to the console. * It can/should be used with: \n * gtk_tree_model_foreach(GTK_TREE_MODEL(model), (GtkTreeModelForeachFunc) \n * rmplugin_x2go_dump_session_properties, \n * dialog); */ /*static void rmplugin_x2go_dump_session_properties(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, GtkWidget *dialog) { //REMMINA_PLUGIN_DEBUG("Function entry."); g_debug(_("Properties for session with path '%s':"), gtk_tree_path_to_string(path)); for (guint i = 0; i < SESSION_NUM_PROPERTIES; i++) { GValue property = G_VALUE_INIT; property = rmplugin_x2go_session_chooser_get_property(dialog, i, path); gchar* display_name = rmplugin_x2go_session_property_to_string(i); g_assert(display_name && "Could not get display name for a property!"); if (i == SESSION_DIALOG_IS_VISIBLE) { g_assert(G_VALUE_HOLDS_BOOLEAN(&property) && "GValue does not " "hold a boolean!"); g_debug("\t%s: '%s'", display_name, g_value_get_boolean(&property) ? "TRUE" : "FALSE"); } else { g_assert(G_VALUE_HOLDS_STRING(&property) && "GValue does not " "hold a string!"); g_debug("\t%s: '%s'", display_name, g_value_get_string(&property)); } } }*/ /** * @brief This function synchronously spawns a pyhoca-cli process with argv as arguments. * @param argc Number of arguments. * @param argv Arguments as string array. \n * Last elements has to be NULL. \n * Strings will get freed automatically. * @param error Will be filled with an error message on fail. * @param env String array of enviroment variables. \n * The list is NULL terminated and each item in * the list is of the form `NAME=VALUE`. * * @returns Returns either standard output string or NULL if it failed. */ static gchar* rmplugin_x2go_spawn_pyhoca_process(guint argc, gchar* argv[], GError** error, gchar** env) { REMMINA_PLUGIN_DEBUG("Function entry."); if (!argv) { gchar* errmsg = g_strdup_printf( _("Internal error: %s"), _("parameter 'argv' is 'NULL'.") ); REMMINA_PLUGIN_CRITICAL("%s", errmsg); g_set_error(error, 1, 1, "%s", errmsg); return NULL; } if (!error) { // Can't report error message back since 'error' is NULL. REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( _("Internal error: %s"), _("parameter 'error' is 'NULL'.") )); return NULL; } if (!env || !env[0]) { gchar* errmsg = g_strdup_printf( _("Internal error: %s"), _("parameter 'env' is either invalid or uninitialized.") ); REMMINA_PLUGIN_CRITICAL("%s", errmsg); g_set_error(error, 1, 1, "%s", errmsg); return NULL; } gint exit_code = 0; gchar *standard_out; // Just supresses pyhoca-cli's help message when pyhoca-cli's version is too old. gchar *standard_err; gboolean success_ret = g_spawn_sync(NULL, argv, env, G_SPAWN_SEARCH_PATH, NULL, NULL, &standard_out, &standard_err, &exit_code, error); REMMINA_PLUGIN_INFO("%s", _("Started PyHoca-CLI with the following arguments:")); // Print every argument except passwords. Free all arg strings. for (gint i = 0; i < argc - 1; i++) { gchar* curr_arg = argv[i]; if (g_str_equal(curr_arg, "--password") || g_str_equal(curr_arg, "--ssh-passphrase")) { g_printf("%s ", curr_arg); g_printf("XXXXXX "); g_free(curr_arg); g_free(argv[++i]); continue; } else { g_printf("%s ", curr_arg); g_free(curr_arg); } } g_printf("\n"); /* TOO VERBOSE: */ /* REMMINA_PLUGIN_DEBUG("%s", _("Started PyHoca-CLI with the " "following environment variables:")); REMMINA_PLUGIN_DEBUG("%s", g_strjoinv("\n", env)); */ if (standard_err && strlen(standard_err) > 0) { if (g_str_has_prefix(standard_err, "pyhoca-cli: error: a socket error " "occured while establishing the connection:")) { // Log error into GUI. gchar* errmsg = g_strdup_printf( _("The necessary PyHoca-CLI process has encountered a " "internet connection problem.") ); // Log error into debug window and stdout REMMINA_PLUGIN_CRITICAL("%s:\n%s", errmsg, standard_err); g_set_error(error, 1, 1, "%s", errmsg); return NULL; } else { gchar* errmsg = g_strdup_printf( _("Could not start " "PyHoca-CLI:\n%s"), standard_err ); REMMINA_PLUGIN_CRITICAL("%s", errmsg); g_set_error(error, 1, 1, "%s", errmsg); return NULL; } } else if (!success_ret || (*error) || strlen(standard_out) <= 0 || exit_code) { if (!(*error)) { gchar* errmsg = g_strdup_printf( _("An unknown error occured while trying " "to start PyHoca-CLI. Exit code: %i"), exit_code); REMMINA_PLUGIN_WARNING("%s", errmsg); g_set_error(error, 1, 1, "%s", errmsg); } else { gchar* errmsg = g_strdup_printf( _("An unknown error occured while trying to start " "PyHoca-CLI. Exit code: %i. Error: '%s'"), exit_code, (*error)->message); REMMINA_PLUGIN_WARNING("%s", errmsg); } return NULL; } return standard_out; } /** * @brief Stores all necessary information needed for retrieving sessions from * a X2Go server. */ struct _ConnectionData { gchar* host; gchar* username; gchar* password; gchar* ssh_privatekey; gchar* ssh_passphrase; }; /** * @brief Either sets a specific row visible or invisible. * Also handles 'terminate' and 'resume' buttons of session-chooser-dialog. * If there are no sessions available anymore, disable all buttons which are not 'new' * and if a session is available again, enable them. * * @param path Describes which row. (GtkTreePath*) \n * Should be from GtkTreeModelFilter's perspective! * @param value TRUE = row is visible & FALSE = row is invisible (gboolean) * @param dialog Session-Chooser-Dialog (GtkDialog*) * @return Returns TRUE if successful. (gboolean) */ static gboolean rmplugin_x2go_session_chooser_set_row_visible(GtkTreePath *path, gboolean value, GtkDialog *dialog) { REMMINA_PLUGIN_DEBUG("Function entry."); if (!path || !dialog) { REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( _("Internal error: %s"), _("Neither the 'path' nor 'dialog' parameters are initialized.") )); return FALSE; } GtkTreeModelFilter *filter = rmplugin_x2go_session_chooser_get_filter_model( GTK_WIDGET(dialog), NULL); GtkTreeModel *model = gtk_tree_model_filter_get_model(filter); // error message was already handled. if (!model) return FALSE; GtkTreeIter iter; if (!gtk_tree_model_get_iter(GTK_TREE_MODEL(model), &iter, path)) { REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( _("Internal error: %s"), _("GtkTreePath 'path' describes a non-existing row!") )); return FALSE; } // Make session either visible or invisible. gtk_list_store_set(GTK_LIST_STORE(model), &iter, SESSION_DIALOG_IS_VISIBLE, value, -1); // Update row. gtk_tree_model_row_changed(GTK_TREE_MODEL(model), path, &iter); /* Get IS_VISIBLE flag of a session. */ // GValue ret_value = G_VALUE_INIT; // ret_value = rmplugin_x2go_session_chooser_get_property(GTK_WIDGET(dialog), // SESSION_DIALOG_IS_VISIBLE, // path); // g_debug("Is visible: %s", g_value_get_boolean(&ret_value) ? "TRUE" : "FALSE"); GtkWidget *term_button = gtk_dialog_get_widget_for_response( GTK_DIALOG(dialog), SESSION_CHOOSER_RESPONSE_TERMINATE); GtkWidget *resume_button = gtk_dialog_get_widget_for_response( GTK_DIALOG(dialog), SESSION_CHOOSER_RESPONSE_CHOOSE); // If no (visible) row is left to terminate disable terminate and resume buttons. gint rows_amount = gtk_tree_model_iter_n_children(GTK_TREE_MODEL(filter), NULL); if (rows_amount <= 0) { gtk_widget_set_sensitive(term_button, FALSE); gtk_widget_set_sensitive(resume_button, FALSE); } else { gtk_widget_set_sensitive(term_button, TRUE); gtk_widget_set_sensitive(resume_button, TRUE); } // Success, yay! return TRUE; } static gboolean rmplugin_x2go_verify_connection_data(struct _ConnectionData *connect_data) { /* Check connect_data. */ if (!connect_data || !connect_data->host || !connect_data->username || strlen(connect_data->host) <= 0 || strlen(connect_data->username) <= 0) { REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( _("Internal error: %s"), _("'Invalid connection data.'") )); return FALSE; } if (!connect_data->password && (!connect_data->ssh_privatekey || strlen(connect_data->ssh_privatekey) <= 0)) { REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( _("Internal error: %s"), _("'Invalid connection data.'") )); } else { return TRUE; } return FALSE; } /** * @brief Terminates a specific X2Go session using pyhoca-cli. * * @param custom_data X2GoCustomUserData structure with the following: \n * gp -> gp (RemminaProtocolWidget*) \n * dialog_data -> dialog data (struct _DialogData*) \n * connect_data -> connection data (struct _ConnectionData*) \n * opt1 -> selected row (GtkTreePath*) \n * opt2 -> session-selection-dialog (GtkDialog*) * * @return G_SOURCE_REMOVE (FALSE), #G_SOURCE_CONTINUE and #G_SOURCE_REMOVE are more * memorable names for the return value. See GLib docs. \n * https://docs.gtk.org/glib/const.SOURCE_REMOVE.html */ static gboolean rmplugin_x2go_pyhoca_terminate_session(X2GoCustomUserData *custom_data) { REMMINA_PLUGIN_DEBUG("Function entry."); if (!custom_data || !custom_data->gp || !custom_data->dialog_data || !custom_data->connect_data || !custom_data->opt1 || !custom_data->opt2) { REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( _("Internal error: %s"), _("Parameter 'custom_data' is not fully initialized!") )); return G_SOURCE_REMOVE; } // Extract data passed by X2GoCustomUserData *custom_data. RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(custom_data->gp); //struct _DialogData *ddata = (struct _DialogData*) custom_data->dialog_data; struct _ConnectionData *connect_data = (struct _ConnectionData*) custom_data->connect_data; GtkTreePath* selected_row = (GtkTreePath*) custom_data->opt1; GtkDialog *dialog = GTK_DIALOG(custom_data->opt2); gchar *host = NULL; gchar *username = NULL; gchar *password = NULL; gchar *ssh_privatekey = NULL; gchar *ssh_passphrase = NULL; gboolean valid = rmplugin_x2go_verify_connection_data(connect_data); if (valid) { if (connect_data->password) password = connect_data->password; if (connect_data->ssh_privatekey) { ssh_privatekey = connect_data->ssh_privatekey; if (connect_data->ssh_passphrase) { ssh_passphrase = connect_data->ssh_passphrase; } } host = connect_data->host; username = connect_data->username; } else { return G_SOURCE_REMOVE; } GValue value = rmplugin_x2go_session_chooser_get_property(GTK_WIDGET(dialog), SESSION_SESSION_ID, selected_row); // error message was handled already. if (!G_VALUE_HOLDS_STRING(&value)) return G_SOURCE_REMOVE; const gchar *session_id = g_value_get_string(&value); // We will now start pyhoca-cli with only the '--terminate $SESSION_ID' option. // (and of course auth related stuff) gchar *argv[50]; gint argc = 0; argv[argc++] = g_strdup("pyhoca-cli"); argv[argc++] = g_strdup("--server"); // Not listed as feature. argv[argc++] = g_strdup_printf("%s", host); if (FEATURE_AVAILABLE(gpdata, "USERNAME")) { argv[argc++] = g_strdup("-u"); if (username) { argv[argc++] = g_strdup_printf("%s", username); } else { argv[argc++] = g_strdup_printf("%s", g_get_user_name()); } } else { REMMINA_PLUGIN_CRITICAL("%s", FEATURE_NOT_AVAIL_STR("USERNAME")); return G_SOURCE_REMOVE; } if (password && FEATURE_AVAILABLE(gpdata, "PASSWORD")) { if (FEATURE_AVAILABLE(gpdata, "AUTH_ATTEMPTS")) { argv[argc++] = g_strdup("--auth-attempts"); argv[argc++] = g_strdup_printf ("%i", 0); } else { REMMINA_PLUGIN_WARNING("%s", FEATURE_NOT_AVAIL_STR("AUTH_ATTEMPTS")); } if (strlen(password) > 0) { argv[argc++] = g_strdup("--force-password"); argv[argc++] = g_strdup("--password"); argv[argc++] = g_strdup_printf("%s", password); } } else if (!password) { REMMINA_PLUGIN_CRITICAL("%s", FEATURE_NOT_AVAIL_STR("PASSWORD")); return G_SOURCE_REMOVE; } if (FEATURE_AVAILABLE(gpdata, "TERMINATE")) { argv[argc++] = g_strdup("--terminate"); argv[argc++] = g_strdup_printf("%s", session_id); } else { REMMINA_PLUGIN_CRITICAL("%s", FEATURE_NOT_AVAIL_STR("TERMINATE")); return G_SOURCE_REMOVE; } if (FEATURE_AVAILABLE(gpdata, "NON_INTERACTIVE")) { argv[argc++] = g_strdup("--non-interactive"); } else { REMMINA_PLUGIN_WARNING("%s", FEATURE_NOT_AVAIL_STR("NON_INTERACTIVE")); } if (FEATURE_AVAILABLE(gpdata, "SSH_PRIVKEY")) { if (ssh_privatekey && !g_str_equal(ssh_privatekey, "")) { argv[argc++] = g_strdup("--ssh-privkey"); argv[argc++] = g_strdup_printf("%s", ssh_privatekey); if (ssh_passphrase && !g_str_equal(ssh_passphrase, "")) { if (FEATURE_AVAILABLE(gpdata, "SSH_PASSPHRASE")) { argv[argc++] = g_strdup("--ssh-passphrase"); argv[argc++] = g_strdup_printf("%s", ssh_passphrase); } else { REMMINA_PLUGIN_MESSAGE("%s", FEATURE_NOT_AVAIL_STR("SSH_PASSPHRASE")); } } } } else { REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("SSH_PRIVKEY")); } argv[argc++] = NULL; GError* error = NULL; gchar** envp = g_get_environ(); rmplugin_x2go_spawn_pyhoca_process(argc, argv, &error, envp); g_strfreev(envp); if (error) { gchar *err_msg = g_strdup_printf( _("Could not terminate X2Go session '%s':\n%s"), session_id, error->message ); REMMINA_PLUGIN_CRITICAL("%s", err_msg); struct _DialogData *err_ddata = g_new0(struct _DialogData, 1); err_ddata->parent = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(dialog))); err_ddata->flags = GTK_DIALOG_MODAL; err_ddata->type = GTK_MESSAGE_ERROR; err_ddata->buttons = GTK_BUTTONS_OK; err_ddata->title = _("An error occured."); err_ddata->message = err_msg; // We don't need the response. err_ddata->callbackfunc = NULL; // We don't need a custom dialog either. err_ddata->dialog_factory_func = NULL; err_ddata->dialog_factory_data = NULL; /* Prepare X2GoCustomUserData *custom_data * gp -> gp (RemminaProtocolWidget*) * dialog_data -> dialog data (struct _DialogData*) */ custom_data->gp = custom_data->gp; custom_data->dialog_data = err_ddata; custom_data->connect_data = NULL; custom_data->opt1 = NULL; custom_data->opt2 = NULL; IDLE_ADD((GSourceFunc) rmplugin_x2go_open_dialog, custom_data); // Too verbose: // GtkTreeModel *model = gtk_tree_model_filter_get_model( // GTK_TREE_MODEL_FILTER(filter)); // gtk_tree_model_foreach(GTK_TREE_MODEL(model), (GtkTreeModelForeachFunc) // rmplugin_x2go_dump_session_properties, dialog); // Set row visible again since we could not terminate the session. if (!rmplugin_x2go_session_chooser_set_row_visible(selected_row, TRUE, dialog)) { // error message was already handled. return G_SOURCE_REMOVE; } } return G_SOURCE_REMOVE; } /** * @brief Gets executed on dialog's 'response' signal * * @param custom_data X2GoCustomUserData*: \n * gp -> gp (RemminaProtocolWidget*) \n * dialog_data -> dialog data (struct _DialogData*) \n * connect_data -> connection data (struct _ConnectionData*) * @param response_id See GTK 'response' signal. * @param self The dialog itself. * * @return gboolean, #G_SOURCE_CONTINUE and #G_SOURCE_REMOVE are more memorable * names for the return value. See GLib docs. \n * https://docs.gtk.org/glib/const.SOURCE_REMOVE.html */ static gboolean rmplugin_x2go_session_chooser_callback(X2GoCustomUserData* custom_data, gint response_id, GtkDialog *self) { REMMINA_PLUGIN_DEBUG("Function entry."); if (!custom_data || !custom_data->gp || !custom_data->dialog_data || !custom_data->connect_data) { REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( _("Internal error: %s"), _("Parameter 'custom_data' is not initialized!") )); return G_SOURCE_REMOVE; } RemminaProtocolWidget *gp = (RemminaProtocolWidget*) custom_data->gp; // Don't need to run other stuff, if the user just wants a new session. // Also it can happen, that no session is there anymore which can be selected! if (response_id == SESSION_CHOOSER_RESPONSE_NEW) { REMMINA_PLUGIN_DEBUG("The user explicitly requested a new session. " "Creating a new session…"); SET_RESUME_SESSION(gp, NULL); // Unstucking main process. Telling it that a session has been selected. // We use a trick here. As long as there is something other // than 0 stored, a session is selected. So we use the gpointer as a gboolean. SET_SESSION_SELECTED(gp, (gpointer) TRUE); gtk_widget_destroy(GTK_WIDGET(self)); return G_SOURCE_REMOVE; } // This assumes that there are sessions which can be selected! GValue value = rmplugin_x2go_session_chooser_get_property( GTK_WIDGET(self), SESSION_SESSION_ID, NULL // Let the function search for the selected row. ); // error message was handled already. if (!G_VALUE_HOLDS_STRING(&value)) return G_SOURCE_REMOVE; gchar *session_id = (gchar*) g_value_get_string(&value); if (response_id == SESSION_CHOOSER_RESPONSE_CHOOSE) { if (!session_id || strlen(session_id) <= 0) { REMMINA_PLUGIN_DEBUG( "%s", _("Could not get session ID from session chooser dialog.") ); SET_RESUME_SESSION(gp, NULL); } else { SET_RESUME_SESSION(gp, session_id); REMMINA_PLUGIN_MESSAGE("%s", g_strdup_printf( _("Resuming session: '%s'"), session_id )); } } else if (response_id == SESSION_CHOOSER_RESPONSE_TERMINATE) { if (!session_id || strlen(session_id) <= 0) { REMMINA_PLUGIN_DEBUG( "%s", _("Could not get session ID from session chooser dialog.") ); SET_RESUME_SESSION(gp, NULL); } else { SET_RESUME_SESSION(gp, session_id); REMMINA_PLUGIN_MESSAGE("%s", g_strdup_printf( _("Terminating session: '%s'"), session_id )); } GtkTreePath *path = rmplugin_x2go_session_chooser_get_selected_row( GTK_WIDGET(self)); // error message was already handled. if (!path) return G_SOURCE_REMOVE; // Actually set row invisible. if (!rmplugin_x2go_session_chooser_set_row_visible(path, FALSE, self)) { // error message was already handled. return G_SOURCE_REMOVE; } /* Prepare X2GoCustomUserData *custom_data * gp -> gp (RemminaProtocolWidget*) * dialog_data -> dialog data (struct _DialogData*) * connect_data -> connection data (struct _ConnectionData*) * opt1 -> selected row (GtkTreePath*) * opt2 -> session selection dialog (GtkDialog*) */ // everything else is already initialized. custom_data->opt1 = path; custom_data->opt2 = self; // Actually start pyhoca-cli process with --terminate $session_id. g_thread_new("terminate-session-thread", (GThreadFunc) rmplugin_x2go_pyhoca_terminate_session, custom_data); // Dialog should stay open. return G_SOURCE_CONTINUE; } else { REMMINA_PLUGIN_DEBUG("User clicked dialog away. " "Creating a new session then."); SET_RESUME_SESSION(gp, NULL); } // Unstucking main process. Telling it that a session has been selected. // We use a trick here. As long as there is something other // than 0 stored, a session is selected. So we use the gpointer as a gboolean. SET_SESSION_SELECTED(gp, (gpointer) TRUE); gtk_widget_destroy(GTK_WIDGET(self)); return G_SOURCE_REMOVE; } #define RMPLUGIN_X2GO_FEATURE_GTKSOCKET 1 /* Forward declaration */ static RemminaProtocolPlugin rmplugin_x2go; /* When more than one NX sessions is connecting in progress, we need this mutex and array * to prevent them from stealing the same window ID. */ static pthread_mutex_t remmina_x2go_init_mutex; static GArray *remmina_x2go_window_id_array; /* ------------- Support for execution on main thread of GTK functions ------------- */ struct onMainThread_cb_data { enum { FUNC_GTK_SOCKET_ADD_ID } func; GtkSocket* sk; Window w; /* Mutex for thread synchronization */ pthread_mutex_t mu; /* Flag to catch cancellations */ gboolean cancelled; }; static gboolean onMainThread_cb(struct onMainThread_cb_data *d) { TRACE_CALL(__func__); if (!d->cancelled) { switch (d->func) { case FUNC_GTK_SOCKET_ADD_ID: gtk_socket_add_id(d->sk, d->w); break; } pthread_mutex_unlock(&d->mu); } else { /* thread has been cancelled, so we must free d memory here */ g_free(d); } return G_SOURCE_REMOVE; } static void onMainThread_cleanup_handler(gpointer data) { TRACE_CALL(__func__); struct onMainThread_cb_data *d = data; d->cancelled = TRUE; } static void onMainThread_schedule_callback_and_wait(struct onMainThread_cb_data *d) { TRACE_CALL(__func__); d->cancelled = FALSE; pthread_cleanup_push(onMainThread_cleanup_handler, d); pthread_mutex_init(&d->mu, NULL); pthread_mutex_lock(&d->mu); gdk_threads_add_idle((GSourceFunc)onMainThread_cb, (gpointer) d); pthread_mutex_lock(&d->mu); pthread_cleanup_pop(0); pthread_mutex_unlock(&d->mu); pthread_mutex_destroy(&d->mu); } static void onMainThread_gtk_socket_add_id(GtkSocket* sk, Window w) { TRACE_CALL(__func__); struct onMainThread_cb_data *d; d = g_new0(struct onMainThread_cb_data, 1); d->func = FUNC_GTK_SOCKET_ADD_ID; d->sk = sk; d->w = w; onMainThread_schedule_callback_and_wait(d); g_free(d); } /* /-/-/-/-/-/-/ Support for execution on main thread of GTK functions /-/-/-/-/-/-/ */ static void rmplugin_x2go_remove_window_id (Window window_id) { gint i; gboolean already_seen = FALSE; pthread_mutex_lock(&remmina_x2go_init_mutex); for (i = 0; i < remmina_x2go_window_id_array->len; i++) { if (g_array_index(remmina_x2go_window_id_array, Window, i) == window_id) { already_seen = TRUE; REMMINA_PLUGIN_DEBUG("Window of X2Go Agent with ID [0x%lx] seen already.", window_id); break; } } if (already_seen) { g_array_remove_index_fast(remmina_x2go_window_id_array, i); REMMINA_PLUGIN_DEBUG("Forgetting about window of X2Go Agent with ID [0x%lx]…", window_id); } pthread_mutex_unlock(&remmina_x2go_init_mutex); } /** * @returns: FALSE. This source should be removed from main loop. * #G_SOURCE_CONTINUE and #G_SOURCE_REMOVE are more memorable * names for the return value. */ static gboolean rmplugin_x2go_cleanup(RemminaProtocolWidget *gp) { REMMINA_PLUGIN_DEBUG("Function entry."); gchar *server; gint port; RemminaFile *remminafile = rm_plugin_service->protocol_plugin_get_file(gp); rm_plugin_service->get_server_port(rm_plugin_service->file_get_string(remminafile, "server"), 22, &server, &port); REMMINA_PLUGIN_AUDIT(_("Disconnected from %s:%d via X2Go"), server, port); g_free(server), server = NULL; RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp); if (gpdata == NULL) { REMMINA_PLUGIN_DEBUG("Exiting since gpdata is already 'NULL'…"); return G_SOURCE_REMOVE; } if (gpdata->thread) { pthread_cancel(gpdata->thread); if (gpdata->thread) pthread_join(gpdata->thread, NULL); } if (gpdata->window_id) { rmplugin_x2go_remove_window_id(gpdata->window_id); } if (gpdata->pidx2go) { kill(gpdata->pidx2go, SIGTERM); g_spawn_close_pid(gpdata->pidx2go); gpdata->pidx2go = 0; } if (gpdata->display) { XSetErrorHandler(gpdata->orig_handler); XCloseDisplay(gpdata->display); gpdata->display = NULL; } g_object_steal_data(G_OBJECT(gp), "plugin-data"); rm_plugin_service->protocol_plugin_signal_connection_closed(gp); return G_SOURCE_REMOVE; } static gboolean rmplugin_x2go_close_connection(RemminaProtocolWidget *gp) { TRACE_CALL(__func__); RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp); REMMINA_PLUGIN_DEBUG("Function entry."); if (gpdata->disconnected) { REMMINA_PLUGIN_DEBUG("Doing nothing since the plugin is already disconnected."); return G_SOURCE_REMOVE; } rmplugin_x2go_cleanup(gp); // Try again. return G_SOURCE_CONTINUE; } static void rmplugin_x2go_pyhoca_cli_exited(GPid pid, gint status, RemminaProtocolWidget *gp) { REMMINA_PLUGIN_DEBUG("Function entry."); RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp); if (!gpdata) { REMMINA_PLUGIN_DEBUG("Doing nothing as the disconnection " "has already been handled."); return; } if (gpdata->pidx2go <= 0) { REMMINA_PLUGIN_DEBUG("Doing nothing since pyhoca-cli was expected to stop."); return; } REMMINA_PLUGIN_CRITICAL("%s", _("PyHoca-CLI exited unexpectedly. " "This connection will now be closed.")); struct _DialogData *ddata = g_new0(struct _DialogData, 1); ddata->parent = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(gp))); ddata->flags = GTK_DIALOG_MODAL; ddata->type = GTK_MESSAGE_ERROR; ddata->buttons = GTK_BUTTONS_OK; ddata->title = _("An error occured."); ddata->message = _("The necessary child process 'pyhoca-cli' stopped unexpectedly.\n" "Please check your profile settings and PyHoca-CLI's output for " "possible errors. Also ensure the remote server is " "reachable and you're using the right credentials."); // We don't need the response. ddata->callbackfunc = NULL; // We don't need a custom dialog either. ddata->dialog_factory_func = NULL; ddata->dialog_factory_data = NULL; /* Prepare X2GoCustomUserData *custom_data * gp -> gp (RemminaProtocolWidget*) * dialog_data -> dialog data (struct _DialogData*) */ X2GoCustomUserData *custom_data = g_new0(X2GoCustomUserData, 1); g_assert(custom_data && "custom_data could not be initialized."); custom_data->gp = gp; custom_data->dialog_data = ddata; custom_data->connect_data = NULL; custom_data->opt1 = NULL; IDLE_ADD((GSourceFunc) rmplugin_x2go_open_dialog, custom_data); // 1 Second. Give `Dialog` chance to open. usleep(1000 * 1000); rmplugin_x2go_close_connection(gp); } /** * @brief Saves s_password and s_username if set. * @returns either TRUE or FALSE. If FALSE gets returned, `errmsg` is set. */ static gboolean rmplugin_x2go_save_credentials(RemminaFile* remminafile, gchar* s_username, gchar* s_password, gchar* errmsg) { // User has requested to save credentials. We put all the new credentials // into remminafile->settings. They will be saved later, on successful // connection, by rcw.c if (s_password && s_username) { if (g_strcmp0(s_username, "") == 0) { g_strlcpy(errmsg, _("Can't save empty username!"), 512); //REMMINA_PLUGIN_CRITICAL("%s", errmsg); // No need. return FALSE; } // We allow the possibility to set an empty password because a X2Go // session can be still made using keyfiles or similar. rm_plugin_service->file_set_string(remminafile, "password", s_password); rm_plugin_service->file_set_string(remminafile, "username", s_username); } else { g_strlcpy(errmsg, g_strdup_printf( _("Internal error: %s"), _("Could not save new credentials.") ), 512); REMMINA_PLUGIN_CRITICAL("%s", _("Could not save " "new credentials: 's_password' or " "'s_username' strings were not set.")); return FALSE; } return TRUE; } /** * @brief Asks the user for a username and password. * * @param errmsg Pointer to error message string (set if function failed). * @param passphrase gchar** Passphrase which will be used to unlock SSH key. * * @returns FALSE if auth failed and TRUE on success. */ static gboolean rmplugin_x2go_get_ssh_passphrase(RemminaProtocolWidget *gp, gchar *errmsg, gchar **passphrase) { REMMINA_PLUGIN_DEBUG("Function entry."); g_assert(errmsg != NULL); g_assert(gp != NULL); if ((*passphrase) == NULL) { // Just setting NULL password to empty password. (*passphrase) = g_strdup(""); } gint ret = rm_plugin_service->protocol_plugin_init_auth( gp, 0, _("Enter password to unlock the SSH key:"), NULL, *passphrase, NULL, NULL ); if (ret == GTK_RESPONSE_OK) { gchar *s_passphrase = rm_plugin_service->protocol_plugin_init_get_password(gp); if (s_passphrase) { (*passphrase) = g_strdup(s_passphrase); g_free(s_passphrase); } } else { g_strlcpy(errmsg, _("Password input cancelled. Aborting…"), 512); return FALSE; } return TRUE; } /** * @brief Asks the user for a username and password. * * @param errmsg Pointer to error message string (set if function failed). * @param username Pointer to default username. Gets set to new username on success. * @param password Pointer to default password. Gets set to new password on success. * * @returns FALSE if auth failed and TRUE on success. */ static gboolean rmplugin_x2go_get_auth(RemminaProtocolWidget *gp, gchar* errmsg, gchar** default_username, gchar** default_password) { REMMINA_PLUGIN_DEBUG("Function entry."); g_assert(errmsg != NULL); g_assert(gp != NULL); g_assert(default_username != NULL); g_assert(default_password != NULL); // default_username is probably NULL because the user didn't configure any // username in the profile settings. if ((*default_username) == NULL) { gchar* l_errmsg = g_strdup_printf( _("Tip: Check the 'Save password' checkbox or manually input your " "X2Go username and password in the profile settings to store " "them for faster logins.") ); REMMINA_PLUGIN_MESSAGE("%s", l_errmsg); (*default_username) = g_strdup(""); } // default_password is probably NULL because something did go wrong at the // secret-plugin. For example: The user didn't input a password for keyring or // the user simply didn't configure a password in the profile settings. if ((*default_password) == NULL) { (*default_password) = g_strdup(""); } gchar *s_username, *s_password; gint ret; gboolean save; gboolean disable_password_storing; RemminaFile *remminafile; remminafile = rm_plugin_service->protocol_plugin_get_file(gp); disable_password_storing = rm_plugin_service->file_get_int( remminafile, "disablepasswordstoring", FALSE ); ret = rm_plugin_service->protocol_plugin_init_auth( gp, (disable_password_storing ? 0 : REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD | REMMINA_MESSAGE_PANEL_FLAG_USERNAME), _("Enter X2Go credentials"), (*default_username), (*default_password), NULL, NULL ); if (ret == GTK_RESPONSE_OK) { s_username = rm_plugin_service->protocol_plugin_init_get_username(gp); s_password = rm_plugin_service->protocol_plugin_init_get_password(gp); if (rm_plugin_service->protocol_plugin_init_get_savepassword(gp)) rm_plugin_service->file_set_string( remminafile, "password", s_password ); // Should be renamed to protocol_plugin_init_get_savecredentials()?! save = rm_plugin_service->protocol_plugin_init_get_savepassword(gp); if (save) { if (!rmplugin_x2go_save_credentials(remminafile, s_username, s_password, errmsg)) { return FALSE; } } if (s_username) { (*default_username) = g_strdup(s_username); g_free(s_username); } if (s_password) { (*default_password) = g_strdup(s_password); g_free(s_password); } } else { g_strlcpy(errmsg, _("Authentication cancelled. Aborting…"), 512); return FALSE; } return TRUE; } /** * @brief Executes 'pyhoca-cli --list-sessions' for username@host. * * @param gp RemminaProtocolWidget* is used to get the x2go-plugin data. * @param error This is where a error message will be when NULL gets returned. * @param connect_data struct _ConnectionData* which stores all necessary information * needed for retrieving sessions from a X2Go server. * * @returns Standard output of pyhoca-cli command. * If NULL then errmsg is set to user-friendly error message. */ static gchar* rmplugin_x2go_get_pyhoca_sessions(RemminaProtocolWidget* gp, GError **error, struct _ConnectionData* connect_data) { REMMINA_PLUGIN_DEBUG("Function entry."); RemminaPluginX2GoData* gpdata = GET_PLUGIN_DATA(gp); gchar *host = NULL; gchar *username = NULL; gchar *password = NULL; gchar *ssh_privatekey = NULL; gchar *ssh_passphrase = NULL; gboolean valid = rmplugin_x2go_verify_connection_data(connect_data); if (valid) { if (connect_data->password) password = connect_data->password; if (connect_data->ssh_privatekey) { ssh_privatekey = connect_data->ssh_privatekey; if (connect_data->ssh_passphrase) { ssh_passphrase = connect_data->ssh_passphrase; } } host = connect_data->host; username = connect_data->username; } else { return G_SOURCE_REMOVE; } // We will now start pyhoca-cli with only the '--list-sessions' option. gchar *argv[50]; gint argc = 0; argv[argc++] = g_strdup("pyhoca-cli"); argv[argc++] = g_strdup("--list-sessions"); argv[argc++] = g_strdup("--server"); // Not listed as feature. argv[argc++] = g_strdup_printf("%s", host); if (FEATURE_AVAILABLE(gpdata, "USERNAME")) { argv[argc++] = g_strdup("-u"); if (username) { argv[argc++] = g_strdup_printf("%s", username); } else { argv[argc++] = g_strdup_printf("%s", g_get_user_name()); } } else { g_set_error(error, 1, 1, "%s", FEATURE_NOT_AVAIL_STR("USERNAME")); REMMINA_PLUGIN_CRITICAL("%s", FEATURE_NOT_AVAIL_STR("USERNAME")); return NULL; } if (FEATURE_AVAILABLE(gpdata, "NON_INTERACTIVE")) { argv[argc++] = g_strdup("--non-interactive"); } else { REMMINA_PLUGIN_WARNING("%s", FEATURE_NOT_AVAIL_STR("NON_INTERACTIVE")); } if (password && FEATURE_AVAILABLE(gpdata, "PASSWORD")) { if (FEATURE_AVAILABLE(gpdata, "AUTH_ATTEMPTS")) { argv[argc++] = g_strdup("--auth-attempts"); argv[argc++] = g_strdup_printf ("%i", 0); } else { REMMINA_PLUGIN_WARNING("%s", FEATURE_NOT_AVAIL_STR("AUTH_ATTEMPTS")); } if (strlen(password) > 0) { argv[argc++] = g_strdup("--force-password"); argv[argc++] = g_strdup("--password"); argv[argc++] = g_strdup_printf("%s", password); } } else if (!password) { g_set_error(error, 1, 1, "%s", FEATURE_NOT_AVAIL_STR("PASSWORD")); REMMINA_PLUGIN_CRITICAL("%s", FEATURE_NOT_AVAIL_STR("PASSWORD")); return NULL; } // No need to catch feature-not-available error. // `--quiet` is not that important. if (FEATURE_AVAILABLE(gpdata, "QUIET")) { argv[argc++] = g_strdup("--quiet"); } if (FEATURE_AVAILABLE(gpdata, "SSH_PRIVKEY")) { if (ssh_privatekey && !g_str_equal(ssh_privatekey, "")) { argv[argc++] = g_strdup("--ssh-privkey"); argv[argc++] = g_strdup_printf("%s", ssh_privatekey); if (ssh_passphrase && !g_str_equal(ssh_passphrase, "")) { if (FEATURE_AVAILABLE(gpdata, "SSH_PASSPHRASE")) { argv[argc++] = g_strdup("--ssh-passphrase"); argv[argc++] = g_strdup_printf("%s", ssh_passphrase); } else { REMMINA_PLUGIN_MESSAGE("%s", FEATURE_NOT_AVAIL_STR("SSH_PASSPHRASE")); } } } } else { REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("SSH_PRIVKEY")); } argv[argc++] = NULL; //#ifndef GLIB_AVAILABLE_IN_2_68 gchar** envp = g_get_environ(); gchar* envp_splitted = g_strjoinv(";", envp); envp_splitted = g_strconcat(envp_splitted, ";LANG=C", (void*) NULL); envp = g_strsplit(envp_splitted, ";", 0); /* * #else * // Only available after glib version 2.68. * // TODO: FIXME: NOT TESTED! * GStrvBuilder* builder = g_strv_builder_new(); * g_strv_builder_add(builder, "LANG=C"); * GStrv envp = g_strv_builder_end(builder); * #endif */ gchar* std_out = rmplugin_x2go_spawn_pyhoca_process(argc, argv, error, envp); g_strfreev(envp); if (!std_out || *error) { // If no error is set but std_out is NULL // then something is not right at all. // Most likely the developer forgot to add an error message. Crash. g_assert((*error) != NULL); return NULL; } return std_out; } /** * @brief This function is used to parse the output of * rmplugin_x2go_get_pyhoca_sessions(). * * @param gp RemminaProtocolWidget* is used to get the x2go-plugin data. * @param error This is where a error message will be when NULL gets returned. * @param connect_data struct _ConnectionData* which stores all necessary information * needed for retrieving sessions from a X2Go server. * * @returns Returns either a GList containing the IDs of every already existing session * found or if the function failes, NULL. * * TODO: If pyhoca-cli (python-x2go) implements `--json` or similar option -> Replace * entire function with JSON parsing. */ static GList* rmplugin_x2go_parse_pyhoca_sessions(RemminaProtocolWidget* gp, GError **error, struct _ConnectionData* connect_data) { REMMINA_PLUGIN_DEBUG("Function entry."); gchar *pyhoca_output = NULL; pyhoca_output = rmplugin_x2go_get_pyhoca_sessions(gp, error, connect_data); if (!pyhoca_output || *error) { // If no error is set but pyhoca_output is NULL // then something is not right at all. // Most likely the developer forgot to add an error message. Crash. g_assert((*error) != NULL); return NULL; } gchar **lines_list = g_strsplit(pyhoca_output, "\n", -1); // Assume at least two lines of output. if (lines_list == NULL || lines_list[0] == NULL || lines_list[1] == NULL) { g_set_error(error, 1, 1, "%s", _("Could not parse the output of PyHoca-CLI's " "--list-sessions option. Creating a new " "session now.")); return NULL; } gboolean found_session = FALSE; GList* sessions = NULL; gchar** session = NULL; for (guint i = 0; lines_list[i] != NULL; i++) { gchar* current_line = lines_list[i]; // TOO VERBOSE: //REMMINA_PLUGIN_DEBUG("pyhoca-cli: %s", current_line); // Hardcoded string "Session Name: " comes from python-x2go. if (!g_str_has_prefix(current_line, "Session Name: ") && !found_session) { // Doesn't begin with "Session Name: " and // the current line doesn't come after that either. Skipping. continue; } if (g_str_has_prefix(current_line, "Session Name: ")) { gchar* session_id = NULL; gchar** line_list = g_strsplit(current_line, ": ", 0); if (line_list == NULL || line_list[0] == NULL || line_list[1] == NULL || strlen(line_list[0]) <= 0 || strlen(line_list[1]) <= 0) { found_session = FALSE; continue; } session = malloc(sizeof(gchar*) * (SESSION_NUM_PROPERTIES+1)); if (!session) { REMMINA_PLUGIN_CRITICAL("%s", _("Could not allocate " "enough memory!")); } session[SESSION_NUM_PROPERTIES] = NULL; sessions = g_list_append(sessions, session); session_id = line_list[1]; session[SESSION_SESSION_ID] = session_id; REMMINA_PLUGIN_INFO("%s", g_strdup_printf( _("Found already existing X2Go session with ID: '%s'"), session[SESSION_SESSION_ID]) ); found_session = TRUE; continue; } if (!found_session) { continue; } if (g_strcmp0(current_line, "-------------") == 0) { continue; } gchar* value = NULL; gchar** line_list = g_strsplit(current_line, ": ", 0); if (line_list == NULL || line_list[0] == NULL || line_list[1] == NULL || strlen(line_list[0]) <= 0 || strlen(line_list[1]) <= 0) { // Probably the empty line at the end of every session. found_session = FALSE; continue; } value = line_list[1]; if (g_str_has_prefix(current_line, "cookie: ")) { REMMINA_PLUGIN_DEBUG("cookie:\t'%s'", value); session[SESSION_COOKIE] = value; } else if (g_str_has_prefix(current_line, "agent PID: ")) { REMMINA_PLUGIN_DEBUG("agent PID:\t'%s'", value); session[SESSION_AGENT_PID] = value; } else if (g_str_has_prefix(current_line, "display: ")) { REMMINA_PLUGIN_DEBUG("display:\t'%s'", value); session[SESSION_DISPLAY] = value; } else if (g_str_has_prefix(current_line, "status: ")) { if (g_strcmp0(value, "S") == 0) { // TRANSLATORS: Please stick to X2GoClient's translation. value = _("Suspended"); } else if (g_strcmp0(value, "R") == 0) { // TRANSLATORS: Please stick to X2GoClient's translation. value = _("Running"); } else if (g_strcmp0(value, "T") == 0) { // TRANSLATORS: Please stick to X2GoClient's translation. value = _("Terminated"); } REMMINA_PLUGIN_DEBUG("status:\t'%s'", value); session[SESSION_STATUS] = value; } else if (g_str_has_prefix(current_line, "graphic port: ")) { REMMINA_PLUGIN_DEBUG("graphic port:\t'%s'", value); session[SESSION_GRAPHIC_PORT] = value; } else if (g_str_has_prefix(current_line, "snd port: ")) { REMMINA_PLUGIN_DEBUG("snd port:\t'%s'", value); session[SESSION_SND_PORT] = value; } else if (g_str_has_prefix(current_line, "sshfs port: ")) { REMMINA_PLUGIN_DEBUG("sshfs port:\t'%s'", value); session[SESSION_SSHFS_PORT] = value; } else if (g_str_has_prefix(current_line, "username: ")) { REMMINA_PLUGIN_DEBUG("username:\t'%s'", value); session[SESSION_USERNAME] = value; } else if (g_str_has_prefix(current_line, "hostname: ")) { REMMINA_PLUGIN_DEBUG("hostname:\t'%s'", value); session[SESSION_HOSTNAME] = value; } else if (g_str_has_prefix(current_line, "create date: ")) { REMMINA_PLUGIN_DEBUG("create date:\t'%s'", value); session[SESSION_CREATE_DATE] = value; } else if (g_str_has_prefix(current_line, "suspended since: ")) { REMMINA_PLUGIN_DEBUG("suspended since:\t'%s'", value); session[SESSION_SUSPENDED_SINCE] = value; } else { REMMINA_PLUGIN_DEBUG("Not supported:\t'%s'", value); found_session = FALSE; } } if (!sessions) { g_set_error(error, 1, 1, "%s", _("Could not find any sessions on remote machine. Creating a new " "session now.") ); // returning NULL with `error` set. } return sessions; } /** * @brief Asks the user, with the help of a dialog, to continue an already existing * session, terminate or create a new one. * * @param error Is set if there is something to tell the user. \n * Not necessarily an *error* message. * @param connect_data Stores all necessary information needed for * etrieving sessions from a X2Go server. * @return gchar* ID of session. Can be 'NULL' but then 'error' is set. */ static gchar* rmplugin_x2go_ask_session(RemminaProtocolWidget *gp, GError **error, struct _ConnectionData* connect_data) { if (!connect_data || !connect_data->host || !connect_data->username || !connect_data->password || strlen(connect_data->host) <= 0 || strlen(connect_data->username) <= 0) // Allow empty passwords. Maybe the user wants to connect via public key? { g_set_error(error, 1, 1, "%s", g_strdup_printf( _("Internal error: %s"), _("'Invalid connection data.'") )); return NULL; } GList *sessions_list = NULL; sessions_list = rmplugin_x2go_parse_pyhoca_sessions(gp, error, connect_data); if (!sessions_list || *error) { // If no error is set but sessions_list is NULL // then something is not right at all. // Most likely the developer forgot to add an error message. Crash. g_assert(*error != NULL); return NULL; } // Prep new DialogData struct. struct _DialogData *ddata = g_new0(struct _DialogData, 1); ddata->parent = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(gp))); ddata->flags = GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT; //ddata->type = GTK_MESSAGE_QUESTION; //ddata->buttons = GTK_BUTTONS_OK; // Doesn't get used in our custom factory. ddata->title = _("Choose a session to resume:"); ddata->message = ""; // gboolean factory(X2GoCustomUserData*, gpointer) // X2GoCustomUserData*: // gp -> gp (RemminaProtocolWidget*) // dialog_data -> dialog data (struct _DialogData*) // connect_data -> connection data (struct _ConnectionData*) // gpointer: dialog_factory_data ddata->callbackfunc = G_CALLBACK(rmplugin_x2go_session_chooser_callback); // gboolean factory(X2GoCustomUserData*, gpointer) // X2GoCustomUserData*: // gp -> gp (RemminaProtocolWidget*) // dialog_data -> dialog data (struct _DialogData*) // connect_data -> connection data (struct _ConnectionData*) // gpointer: dialog_factory_data ddata->dialog_factory_data = sessions_list; ddata->dialog_factory_func = G_CALLBACK(rmplugin_x2go_choose_session_dialog_factory); /* Prepare X2GoCustomUserData *custom_data * gp -> gp (RemminaProtocolWidget*) * dialog_data -> dialog data (struct _DialogData*) */ X2GoCustomUserData *custom_data = g_new0(X2GoCustomUserData, 1); g_assert(custom_data && "custom_data could not be initialized."); custom_data->gp = gp; custom_data->dialog_data = ddata; custom_data->connect_data = connect_data; custom_data->opt1 = NULL; // Open dialog here. Dialog rmplugin_x2go_session_chooser_callback (callbackfunc) // should set SET_RESUME_SESSION. IDLE_ADD((GSourceFunc)rmplugin_x2go_open_dialog, custom_data); guint counter = 0; while (!IS_SESSION_SELECTED(gp)) { // 0.5 Seconds. Give dialog chance to open. usleep(500 * 1000); // Every 5 seconds if (counter % 10 == 0 || counter == 0) { REMMINA_PLUGIN_MESSAGE("%s", _("Waiting for user to select a session…")); } counter++; } gchar* chosen_resume_session = GET_RESUME_SESSION(gp); if (!chosen_resume_session || strlen(chosen_resume_session) <= 0) { g_set_error(error, 1, 1, "%s", _("No session was selected. Creating a new one.")); return NULL; } return chosen_resume_session; } static gboolean rmplugin_x2go_exec_x2go(gchar *host, gint sshport, gchar *username, gchar *password, gchar *command, gchar *kbdlayout, gchar *kbdtype, gchar *audio, gchar *clipboard, gint dpi, gchar *resolution, gchar *ssh_privatekey, RemminaProtocolWidget *gp, gchar *errmsg) { TRACE_CALL(__func__); RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp); gchar *argv[50]; gint argc = 0; // We don't want to save any SSH passphrases on hard drive! // Thats why we will always ask if needed. gchar *ssh_passphrase = NULL; if (!username || strlen(username) <= 0) { // Sets `username` and `password`. if (!rmplugin_x2go_get_auth(gp, errmsg, &username, &password)) { return FALSE; } } // Password can be *empty* but not NULL. if (!password) { password = g_strdup(""); } if (ssh_privatekey && strlen(ssh_privatekey) > 0) { // FIXME: Check if file exists and is legit private key. // See: https://security.stackexchange.com/a/245767 // Get ssh_privatekey now via dialog. if (!rmplugin_x2go_get_ssh_passphrase(gp, errmsg, &ssh_passphrase)) { return FALSE; } } struct _ConnectionData* connect_data = g_new0(struct _ConnectionData, 1); connect_data->host = host; connect_data->username = username; connect_data->password = password; connect_data->ssh_privatekey = ssh_privatekey; connect_data->ssh_passphrase = ssh_passphrase; GError *session_error = NULL; gchar* resume_session_id = rmplugin_x2go_ask_session(gp, &session_error, connect_data); if (!resume_session_id || session_error || strlen(resume_session_id) <= 0) { // If no error is set but session_id is NULL // then something is not right at all. // Most likely the developer forgot to add an error message. Crash. g_assert(session_error != NULL); REMMINA_PLUGIN_WARNING("%s", g_strdup_printf( _("A non-critical error happened: %s"), session_error->message )); } else { REMMINA_PLUGIN_INFO("%s", g_strdup_printf( _("User chose to resume session with ID: '%s'"), resume_session_id )); } argc = 0; argv[argc++] = g_strdup("pyhoca-cli"); argv[argc++] = g_strdup("--server"); // Not listed as feature. argv[argc++] = g_strdup_printf ("%s", host); if (FEATURE_AVAILABLE(gpdata, "REMOTE_SSH_PORT")) { argv[argc++] = g_strdup("-p"); argv[argc++] = g_strdup_printf ("%d", sshport); } else { REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("REMOTE_SSH_PORT")); } if (resume_session_id && strlen(resume_session_id) > 0) { REMMINA_PLUGIN_INFO("%s", g_strdup_printf( // TRANSLATORS: Please stick to X2GoClient's way of translating. _("Resuming session '%s'…"), resume_session_id )); if (FEATURE_AVAILABLE(gpdata, "RESUME")) { argv[argc++] = g_strdup("--resume"); argv[argc++] = g_strdup_printf("%s", resume_session_id); } else { REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("RESUME")); } } // Deprecated. The user either wants to continue a // session or just not. No inbetween. // if (!resume_session_id) { // if (FEATURE_AVAILABLE(gpdata, "TRY_RESUME")) { // argv[argc++] = g_strdup("--try-resume"); // } else { // REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("TRY_RESUME")); // } // } if (FEATURE_AVAILABLE(gpdata, "USERNAME")) { argv[argc++] = g_strdup("-u"); if (username){ argv[argc++] = g_strdup_printf ("%s", username); } else { argv[argc++] = g_strdup_printf ("%s", g_get_user_name()); } } else { REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("USERNAME")); } if (password && FEATURE_AVAILABLE(gpdata, "PASSWORD")) { if (strlen(password) > 0) { argv[argc++] = g_strdup("--force-password"); argv[argc++] = g_strdup("--password"); argv[argc++] = g_strdup_printf ("%s", password); } } else { REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("PASSWORD")); } if (FEATURE_AVAILABLE(gpdata, "AUTH_ATTEMPTS")) { argv[argc++] = g_strdup("--auth-attempts"); argv[argc++] = g_strdup_printf ("%i", 0); } else { REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("AUTH_ATTEMPTS")); } if (FEATURE_AVAILABLE(gpdata, "NON_INTERACTIVE")) { argv[argc++] = g_strdup("--non-interactive"); } else { REMMINA_PLUGIN_WARNING("%s", FEATURE_NOT_AVAIL_STR("NON_INTERACTIVE")); } if (FEATURE_AVAILABLE(gpdata, "COMMAND")) { argv[argc++] = g_strdup("-c"); // FIXME: pyhoca-cli is picky about multiple quotes around // the command string... // argv[argc++] = g_strdup_printf ("%s", g_shell_quote(command)); argv[argc++] = g_strdup(command); } else { REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("COMMAND")); } if (FEATURE_AVAILABLE(gpdata, "KBD_LAYOUT")) { if (kbdlayout) { argv[argc++] = g_strdup("--kbd-layout"); argv[argc++] = g_strdup_printf ("%s", kbdlayout); } else { argv[argc++] = g_strdup("--kbd-layout"); argv[argc++] = g_strdup("auto"); } } else { REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("KBD_LAYOUT")); } if (FEATURE_AVAILABLE(gpdata, "KBD_TYPE")) { if (kbdtype) { argv[argc++] = g_strdup("--kbd-type"); argv[argc++] = g_strdup_printf ("%s", kbdtype); } else { argv[argc++] = g_strdup("--kbd-type"); argv[argc++] = g_strdup("auto"); } } else { REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("KBD_TYPE")); } if (FEATURE_AVAILABLE(gpdata, "GEOMETRY")) { if (!resolution) resolution = "800x600"; argv[argc++] = g_strdup("-g"); argv[argc++] = g_strdup_printf ("%s", resolution); } else { REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("GEOMETRY")); } if (FEATURE_AVAILABLE(gpdata, "TERMINATE_ON_CTRL_C")) { argv[argc++] = g_strdup("--terminate-on-ctrl-c"); } else { REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("TERMINATE_ON_CTRL_C")); } if (FEATURE_AVAILABLE(gpdata, "SOUND")) { if (audio) { argv[argc++] = g_strdup("--sound"); argv[argc++] = g_strdup_printf ("%s", audio); } else { argv[argc++] = g_strdup("--sound"); argv[argc++] = g_strdup("none"); } } else { REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("SOUND")); } if (FEATURE_AVAILABLE(gpdata, "CLIPBOARD_MODE")) { if (clipboard) { argv[argc++] = g_strdup("--clipboard-mode"); argv[argc++] = g_strdup_printf("%s", clipboard); } } else { REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("CLIPBOARD_MODE")); } if (FEATURE_AVAILABLE(gpdata, "DPI")) { // Even though we validate the users input in the Remmina Editor, // manipulating profile files is still very possible… // Values are extracted from pyhoca-cli. if (dpi < 20 || dpi > 400) { g_strlcpy(errmsg, _("DPI setting is out of bounds. Please adjust " "it in profile settings."), 512); // No need, start_session() will handle output. //REMMINA_PLUGIN_CRITICAL("%s", errmsg); return FALSE; } argv[argc++] = g_strdup("--dpi"); argv[argc++] = g_strdup_printf ("%i", dpi); } else { REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("DPI")); } if (FEATURE_AVAILABLE(gpdata, "SSH_PRIVKEY")) { if (ssh_privatekey && !g_str_equal(ssh_privatekey, "")) { argv[argc++] = g_strdup("--ssh-privkey"); argv[argc++] = g_strdup_printf("%s", ssh_privatekey); if (ssh_passphrase && !g_str_equal(ssh_passphrase, "")) { if (FEATURE_AVAILABLE(gpdata, "SSH_PASSPHRASE")) { argv[argc++] = g_strdup("--ssh-passphrase"); argv[argc++] = g_strdup_printf("%s", ssh_passphrase); } else { REMMINA_PLUGIN_MESSAGE("%s", FEATURE_NOT_AVAIL_STR("SSH_PASSPHRASE")); } } } } else { REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("SSH_PRIVKEY")); } argv[argc++] = NULL; GError *error = NULL; gchar **envp = g_get_environ(); gboolean success = g_spawn_async_with_pipes (NULL, argv, envp, (G_SPAWN_DO_NOT_REAP_CHILD | G_SPAWN_SEARCH_PATH), NULL, NULL, &gpdata->pidx2go, NULL, NULL, NULL, &error); REMMINA_PLUGIN_INFO("%s", _("Started PyHoca-CLI with the following arguments:")); // Print every argument except passwords. Free all arg strings. for (gint i = 0; i < argc - 1; i++) { gchar* curr_arg = argv[i]; if (g_str_equal(curr_arg, "--password") || g_str_equal(curr_arg, "--ssh-passphrase")) { g_printf("%s ", curr_arg); g_printf("XXXXXX "); g_free(curr_arg); g_free(argv[++i]); continue; } else { g_printf("%s ", curr_arg); g_free(curr_arg); } } g_printf("\n"); if (!success || error) { // TRANSLATORS: Meta-error. Shouldn't be visible. if (!error) error = g_error_new(0, 0, _("Internal error.")); gchar *error_title = _("Could not start X2Go session…"); struct _DialogData* ddata = g_new0(struct _DialogData, 1); ddata->parent = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(gp))); ddata->flags = GTK_DIALOG_MODAL; ddata->type = GTK_MESSAGE_ERROR; ddata->buttons = GTK_BUTTONS_OK; ddata->title = _("Could not start X2Go session."); ddata->message = g_strdup_printf(_("Could not start PyHoca-CLI (%i): '%s'"), error->code, error->message); // We don't need the response. ddata->callbackfunc = NULL; // We don't need a custom dialog either. ddata->dialog_factory_func = NULL; ddata->dialog_factory_data = NULL; /* Prepare X2GoCustomUserData *custom_data * gp -> gp (RemminaProtocolWidget*) * dialog_data -> dialog data (struct _DialogData*) */ X2GoCustomUserData *custom_data = g_new0(X2GoCustomUserData, 1); g_assert(custom_data && "Could not initialise Custom_data."); custom_data->gp = gp; custom_data->dialog_data = ddata; custom_data->connect_data = NULL; custom_data->opt1 = NULL; IDLE_ADD((GSourceFunc) rmplugin_x2go_open_dialog, custom_data); g_strlcpy(errmsg, error_title, 512); // No need to output here. rmplugin_x2go_start_session will do this. g_error_free(error); return FALSE; } // Prevent a race condition where pyhoca-cli is not // started yet (pidx2go == 0) but a watcher is added. struct timespec ts; // 0.001 seconds. ts.tv_nsec = 1 * 1000 * 1000; ts.tv_sec = 0; while (gpdata->pidx2go == 0) { nanosleep(&ts, NULL); REMMINA_PLUGIN_DEBUG("Waiting for PyHoca-CLI to start…"); }; REMMINA_PLUGIN_DEBUG("Watching child 'pyhoca-cli' process now…"); g_child_watch_add(gpdata->pidx2go, (GChildWatchFunc) rmplugin_x2go_pyhoca_cli_exited, gp); return TRUE; } /** * @returns a GList* with all features which pyhoca-cli had before the feature system. */ static GList* rmplugin_x2go_old_pyhoca_features() { REMMINA_PLUGIN_DEBUG("Function entry."); #define AMOUNT_FEATURES 43 gchar* features[AMOUNT_FEATURES] = { "ADD_TO_KNOWN_HOSTS", "AUTH_ATTEMPTS", "BROKER_PASSWORD", "BROKER_URL", "CLEAN_SESSIONS", "COMMAND", "DEBUG", "FORCE_PASSWORD", "FORWARD_SSHAGENT", "GEOMETRY", "KBD_LAYOUT", "KBD_TYPE", "LIBDEBUG", "LIBDEBUG_SFTPXFER", "LINK", "LIST_CLIENT_FEATURES", "LIST_DESKTOPS", "LIST_SESSIONS", "NEW", "PACK", "PASSWORD", "PDFVIEW_CMD", "PRINTER", "PRINTING", "PRINT_ACTION", "PRINT_CMD", "QUIET", "REMOTE_SSH_PORT", "RESUME", "SAVE_TO_FOLDER", "SESSION_PROFILE", "SESSION_TYPE", "SHARE_DESKTOP", "SHARE_LOCAL_FOLDERS", "SHARE_MODE", "SOUND", "SSH_PRIVKEY", "SUSPEND", "TERMINATE", "TERMINATE_ON_CTRL_C", "TRY_RESUME", "USERNAME", "XINERAMA" }; GList *features_list = NULL; for (int i = 0; i < AMOUNT_FEATURES; i++) { features_list = g_list_append(features_list, features[i]); } return features_list; } /** * @returns a GList* which includes all pyhoca-cli command line features we can use. */ static GList* rmplugin_x2go_populate_available_features_list() { REMMINA_PLUGIN_DEBUG("Function entry."); GList* returning_glist = NULL; // We will now start pyhoca-cli with only the '--list-cmdline-features' option // and depending on the exit code and standard output we will determine if some // features are available or not. gchar* argv[50]; gint argc = 0; argv[argc++] = g_strdup("pyhoca-cli"); argv[argc++] = g_strdup("--list-cmdline-features"); argv[argc++] = NULL; GError* error = NULL; // Won't be actually used. // Querying pyhoca-cli's command line features. gchar** envp = g_get_environ(); gchar* features_string = rmplugin_x2go_spawn_pyhoca_process(argc, argv, &error, envp); g_strfreev(envp); if (!features_string || error) { // We added the '--list-cmdline-features' on commit 17d1be1319ba6 of // pyhoca-cli. In order to protect setups which don't have the newest // version of pyhoca-cli available yet we artificially create a list // of an old limited set of features. REMMINA_PLUGIN_WARNING("%s", _("Could not get PyHoca-CLI's command-line features. This " "indicates it is either too old, or not installed. " "An old limited set of features will be used for now.")); return rmplugin_x2go_old_pyhoca_features(); } else { gchar **features_list = g_strsplit(features_string, "\n", 0); if (features_list == NULL) { gchar *error_msg = _("Could not parse PyHoca-CLI's command-line " "features. Using a limited feature-set for now."); REMMINA_PLUGIN_WARNING("%s", error_msg); return rmplugin_x2go_old_pyhoca_features(); } REMMINA_PLUGIN_INFO("%s", _("Retrieved the following PyHoca-CLI " "command-line features:")); for(int k = 0; features_list[k] != NULL; k++) { // Filter out empty strings if (strlen(features_list[k]) <= 0) continue; REMMINA_PLUGIN_INFO("%s", g_strdup_printf(_("Available feature[%i]: '%s'"), k+1, features_list[k])); returning_glist = g_list_append(returning_glist, features_list[k]); } return returning_glist; } } static void rmplugin_x2go_on_plug_added(GtkSocket *socket, RemminaProtocolWidget *gp) { TRACE_CALL(__func__); gchar *server; gint port; RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp); REMMINA_PLUGIN_DEBUG("Socket %d", gpdata->socket_id); rm_plugin_service->protocol_plugin_signal_connection_opened(gp); RemminaFile *remminafile = rm_plugin_service->protocol_plugin_get_file(gp); rm_plugin_service->get_server_port(rm_plugin_service->file_get_string(remminafile, "server"), 22, &server, &port); REMMINA_PLUGIN_AUDIT(_("Connected to %s:%d via X2Go"), server, port); g_free(server), server = NULL; return; } static gboolean rmplugin_x2go_on_plug_removed(GtkSocket *socket, RemminaProtocolWidget *gp) { TRACE_CALL(__func__); REMMINA_PLUGIN_DEBUG("Function entry."); rmplugin_x2go_close_connection(gp); return G_SOURCE_CONTINUE; } static void rmplugin_x2go_init(RemminaProtocolWidget *gp) { TRACE_CALL(__func__); REMMINA_PLUGIN_DEBUG("Function entry.", PLUGIN_NAME); RemminaPluginX2GoData *gpdata; gpdata = g_new0(RemminaPluginX2GoData, 1); g_object_set_data_full(G_OBJECT(gp), "plugin-data", gpdata, g_free); if (!rm_plugin_service->gtksocket_available()) { /* report this in open_connection, not reportable here... */ return; } GList* available_features = rmplugin_x2go_populate_available_features_list(); // available_features can't be NULL cause if it fails, it gets populated with an // old standard feature set. gpdata->available_features = available_features; gpdata->socket_id = 0; gpdata->thread = 0; gpdata->display = NULL; gpdata->window_id = 0; gpdata->pidx2go = 0; gpdata->orig_handler = NULL; gpdata->socket = gtk_socket_new(); rm_plugin_service->protocol_plugin_register_hostkey(gp, gpdata->socket); gtk_widget_show(gpdata->socket); g_signal_connect(G_OBJECT(gpdata->socket), "plug-added", G_CALLBACK(rmplugin_x2go_on_plug_added), gp); g_signal_connect(G_OBJECT(gpdata->socket), "plug-removed", G_CALLBACK(rmplugin_x2go_on_plug_removed), gp); gtk_container_add(GTK_CONTAINER(gp), gpdata->socket); } static gboolean rmplugin_x2go_try_window_id(Window window_id) { TRACE_CALL(__func__); gint i; gboolean already_seen = FALSE; REMMINA_PLUGIN_DEBUG("Check if the window of X2Go Agent with ID [0x%lx] is already known or if " "it needs registration", window_id); pthread_mutex_lock(&remmina_x2go_init_mutex); for (i = 0; i < remmina_x2go_window_id_array->len; i++) { if (g_array_index(remmina_x2go_window_id_array, Window, i) == window_id) { already_seen = TRUE; REMMINA_PLUGIN_DEBUG("Window of X2Go Agent with ID [0x%lx] " "already seen.", window_id); break; } } if (!already_seen) { g_array_append_val(remmina_x2go_window_id_array, window_id); REMMINA_PLUGIN_DEBUG("Registered new window for X2Go Agent with " "ID [0x%lx].", window_id); } pthread_mutex_unlock(&remmina_x2go_init_mutex); return (!already_seen); } static int rmplugin_x2go_dummy_handler(Display *dsp, XErrorEvent *err) { TRACE_CALL(__func__); return 0; } static gboolean rmplugin_x2go_start_create_notify(RemminaProtocolWidget *gp, gchar *errmsg) { TRACE_CALL(__func__); RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp); gpdata->display = XOpenDisplay(gdk_display_get_name(gdk_display_get_default())); if (gpdata->display == NULL) { g_strlcpy(errmsg, _("Could not open X11 DISPLAY."), 512); return FALSE; } gpdata->orig_handler = XSetErrorHandler(rmplugin_x2go_dummy_handler); XSelectInput(gpdata->display, XDefaultRootWindow(gpdata->display), SubstructureNotifyMask); REMMINA_PLUGIN_DEBUG("X11 event-watcher created."); return TRUE; } static gboolean rmplugin_x2go_monitor_create_notify(RemminaProtocolWidget *gp, const gchar *cmd, gchar *errmsg) { TRACE_CALL(__func__); RemminaPluginX2GoData *gpdata; gboolean agent_window_found = FALSE; Atom atom; XEvent xev; Window w; Atom type; int format; unsigned long nitems, rest; unsigned char *data = NULL; guint16 non_createnotify_count = 0; struct timespec ts; // wait_amount * ts.tv_nsec = 20s // 100 * 0.2s = 20s int wait_amount = 100; CANCEL_DEFER REMMINA_PLUGIN_DEBUG("%s", _("Waiting for window of X2Go Agent to appear…")); gpdata = GET_PLUGIN_DATA(gp); atom = XInternAtom(gpdata->display, "WM_COMMAND", True); if (atom == None) { CANCEL_ASYNC return FALSE; } ts.tv_sec = 0; // 0.2s = 200000000ns ts.tv_nsec = 200000000; while (wait_amount > 0) { pthread_testcancel(); if (!(gpdata->pidx2go > 0)) { nanosleep(&ts, NULL); REMMINA_PLUGIN_DEBUG("Waiting for X2Go session to start…"); continue; } while (!XPending(gpdata->display)) { nanosleep(&ts, NULL); wait_amount--; // Don't spam the console. Print every second though. if (wait_amount % 5 == 0) { REMMINA_PLUGIN_INFO("%s", _("Waiting for PyHoca-CLI to " "show the session's window…")); } continue; } XNextEvent(gpdata->display, &xev); // Just ignore non CreatNotify events. if (xev.type != CreateNotify) { non_createnotify_count++; if (non_createnotify_count % 5 == 0) { REMMINA_PLUGIN_DEBUG("Saw '%i' X11 events, which weren't " "CreateNotify.", non_createnotify_count); } continue; } w = xev.xcreatewindow.window; if (XGetWindowProperty(gpdata->display, w, atom, 0, 255, False, AnyPropertyType, &type, &format, &nitems, &rest, &data) != Success) { REMMINA_PLUGIN_DEBUG("Could not get WM_COMMAND property from X11 " "window ID [0x%lx].", w); continue; } if (data) { REMMINA_PLUGIN_DEBUG("Saw '%i' X11 events, which weren't " "CreateNotify.", non_createnotify_count); REMMINA_PLUGIN_DEBUG("Found X11 window with WM_COMMAND set " "to '%s', the window ID is [0x%lx].", (char*)data, w); } if (data && g_strrstr((gchar*)data, cmd) && rmplugin_x2go_try_window_id(w)) { gpdata->window_id = w; agent_window_found = TRUE; XFree(data); break; } if (data) XFree(data); } XSetErrorHandler(gpdata->orig_handler); XCloseDisplay(gpdata->display); gpdata->display = NULL; CANCEL_ASYNC if (!agent_window_found) { g_strlcpy(errmsg, _("No X2Go session window appeared. " "Something went wrong…"), 512); return FALSE; } return TRUE; } static gboolean rmplugin_x2go_start_session(RemminaProtocolWidget *gp) { TRACE_CALL(__func__); REMMINA_PLUGIN_DEBUG("Function entry."); RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp);; RemminaFile *remminafile; const gchar errmsg[512] = {0}; gboolean ret = TRUE; gchar *servstr, *host, *username, *password, *command, *kbdlayout, *kbdtype, *audio, *clipboard, *res, *ssh_privatekey; gint sshport, dpi; GdkDisplay *default_dsp; gint width, height; // We save the X Display name (:0) as we will need to synchronize the clipboards default_dsp = gdk_display_get_default(); const gchar *default_dsp_name = gdk_display_get_name(default_dsp); REMMINA_PLUGIN_DEBUG("Default display is '%s'.", default_dsp_name); remminafile = rm_plugin_service->protocol_plugin_get_file(gp); servstr = GET_PLUGIN_STRING("server"); if (servstr) { rm_plugin_service->get_server_port(servstr, 22, &host, &sshport); } else { return FALSE; } if (!sshport) sshport=22; username = GET_PLUGIN_STRING("username"); password = GET_PLUGIN_PASSWORD("password"); command = GET_PLUGIN_STRING("command"); if (!command) command = "TERMINAL"; kbdlayout = GET_PLUGIN_STRING("kbdlayout"); kbdtype = GET_PLUGIN_STRING("kbdtype"); audio = GET_PLUGIN_STRING("audio"); clipboard = GET_PLUGIN_STRING("clipboard"); dpi = GET_PLUGIN_INT("dpi", 80); ssh_privatekey = GET_PLUGIN_STRING("ssh_privatekey"); // If empty set to NULL if(ssh_privatekey && g_str_equal(ssh_privatekey, "")) { ssh_privatekey = NULL; } width = rm_plugin_service->get_profile_remote_width(gp); height = rm_plugin_service->get_profile_remote_height(gp); /* multiple of 4 */ width = (width + 3) & ~0x3; height = (height + 3) & ~0x3; if ((width > 0) && (height > 0)) { res = g_strdup_printf ("%dx%d", width, height); } else { res = "800x600"; } REMMINA_PLUGIN_DEBUG("Resolution set by user: '%s'.", res); REMMINA_PLUGIN_DEBUG("Attached window to socket '%d'.", gpdata->socket_id); /* register for notifications of window creation events */ if (ret) ret = rmplugin_x2go_start_create_notify(gp, (gchar*)&errmsg); /* trigger the session start, session window should appear soon after this */ if (ret) ret = rmplugin_x2go_exec_x2go(host, sshport, username, password, command, kbdlayout, kbdtype, audio, clipboard, dpi, res, ssh_privatekey, gp, (gchar*)&errmsg); /* get the window ID of the remote x2goagent */ if (ret) ret = rmplugin_x2go_monitor_create_notify(gp, "x2goagent", (gchar*)&errmsg); if (!ret) { REMMINA_PLUGIN_CRITICAL("%s", errmsg); rm_plugin_service->protocol_plugin_set_error(gp, "%s", &errmsg); return FALSE; } /* embed it */ onMainThread_gtk_socket_add_id(GTK_SOCKET(gpdata->socket), gpdata->window_id); return TRUE; } static gboolean rmplugin_x2go_main(RemminaProtocolWidget *gp) { TRACE_CALL(__func__); RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp); gboolean ret = FALSE; ret = rmplugin_x2go_start_session(gp); gpdata->thread = 0; return ret; } static gpointer rmplugin_x2go_main_thread(RemminaProtocolWidget* gp) { TRACE_CALL(__func__); if (!gp) { REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf( _("Internal error: %s"), _("RemminaProtocolWidget* gp is 'NULL'!") )); return NULL; } pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); CANCEL_ASYNC if (!rmplugin_x2go_main(gp)) { IDLE_ADD((GSourceFunc) rmplugin_x2go_cleanup, gp); } return NULL; } static gboolean rmplugin_x2go_open_connection(RemminaProtocolWidget *gp) { TRACE_CALL(__func__); RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp); if (!rm_plugin_service->gtksocket_available()) { rm_plugin_service->protocol_plugin_set_error(gp, _("The %s protocol is " "unavailable because GtkSocket only works under X.org"), PLUGIN_NAME); return FALSE; } gpdata->socket_id = gtk_socket_get_id(GTK_SOCKET(gpdata->socket)); // casting to void* is allowed since return type 'gpointer' is actually void*. if (pthread_create(&gpdata->thread, NULL, (void*) rmplugin_x2go_main_thread, gp)) { rm_plugin_service->protocol_plugin_set_error(gp, _("Could not initialize " "pthread. Falling back to non-threaded mode…")); gpdata->thread = 0; return FALSE; } else { return TRUE; } } static gboolean rmplugin_x2go_query_feature(RemminaProtocolWidget* gp, const RemminaProtocolFeature* feature) { TRACE_CALL(__func__); return TRUE; } static const RemminaProtocolFeature rmplugin_x2go_features[] = { {REMMINA_PROTOCOL_FEATURE_TYPE_GTKSOCKET, RMPLUGIN_X2GO_FEATURE_GTKSOCKET, NULL, NULL, NULL}, {REMMINA_PROTOCOL_FEATURE_TYPE_END, 0, NULL, NULL, NULL} }; /** * @brief This function builds a string like: "'value1', 'value2' and 'value3'" \n * To be used in a loop. \n * See rmplugin_x2go_string_setting_validator() for an example. * * @param max_elements Number of maximum elements. * @param element_to_add Next element to add to the string * @param current_element Which element is element_to_add? * @param string The string to which `element_to_add` will be added. */ static gchar* rmplugin_x2go_enumeration_prettifier(const guint max_elements, const guint current_element, gchar* element_to_add, gchar* string) { if (max_elements > 2) { if (current_element == max_elements - 1) { // TRANSLATORS: Presumably you just want to translate 'and' into // your language. // (Except your listing-grammar differs from english.) // 'value1', 'value2', 'valueN-1' and 'valueN' return g_strdup_printf(_("%sand '%s'"), string, element_to_add); } else if (current_element == max_elements - 2) { // TRANSLATORS: Presumably you just want to leave it english. // (Except your listing-grammar differs from english.) // 'value1', 'value2', 'valueN-1' and 'valueN' return g_strdup_printf(_("%s'%s' "), string, element_to_add); } else { // TRANSLATORS: Presumably you just want to leave it english. // (Except your listing-grammar differs from english.) // 'value1', 'value2', 'valueN-1' and 'valueN' return g_strdup_printf(_("%s'%s', "), string, element_to_add); } } else if (max_elements == 2) { if (current_element == max_elements - 1) { // TRANSLATORS: Presumably you just want to translate 'and' into // your language. // (Except your listing-grammar differs from english.) // 'value1' and 'value2' return g_strdup_printf(_("%sand '%s'"), string, element_to_add); } else { // TRANSLATORS: Presumably you just want to leave it english. // (Except your listing-grammar differs from english.) // 'value1' and 'value2' return g_strdup_printf(_("%s'%s' "), string, element_to_add); } } else { return g_strdup(element_to_add); } } /** * @brief Validator-functions are getting executed when the user wants to save profile * settings. It uses the given data (See RemminaProtocolSetting array) to determine * which strings are allowed and returns a end-user friendly error message. * * @param key Key is the setting's name. * @param value Value to validate. * @param data Data needed for validation process. See RemminaProtocolSetting array. * * @returns End-user friendly and translated error message, explaining why the given * value is invalid. If the given value is error-free then NULL gets returned. * */ static GError* rmplugin_x2go_string_setting_validator(gchar* key, gchar* value, gchar* data) { GError *error = NULL; if (!data) { gchar *error_msg = _("Invalid validation data in ProtocolSettings array!"); REMMINA_PLUGIN_CRITICAL("%s", error_msg); g_set_error(&error, 1, 1, "%s", error_msg); return error; } gchar **elements_list = g_strsplit(data, ",", 0); guint elements_amount = 0; elements_amount = g_strv_length(elements_list); if (elements_list == NULL || elements_list[0] == NULL || strlen(elements_list[0]) <= 0) { gchar *error_msg = _("Validation data in ProtocolSettings array is invalid!"); REMMINA_PLUGIN_CRITICAL("%s", error_msg); g_set_error(&error, 1, 1, "%s", error_msg); return error; } gchar *data_str = ""; if (!key || !value) { REMMINA_PLUGIN_CRITICAL("%s", _("Parameters 'key' or 'value' are 'NULL'!")); g_set_error(&error, 1, 1, "%s", _("Internal error.")); return error; } for (guint i = 0; elements_list[i] != NULL; i++) { // Don't wanna crash if elements_list[i] is NULL. gchar* element = elements_list[i] ? elements_list[i] : ""; if (g_strcmp0(value, element) == 0) { // We found value in elements_list. Value passed validation. return NULL; } data_str = rmplugin_x2go_enumeration_prettifier(elements_amount, i, element, data_str); } if (elements_amount > 1) { g_set_error(&error, 1, 1, _("Allowed values are %s."), data_str); } else { g_set_error(&error, 1, 1, _("The only allowed value is '%s'."), data_str); } g_free(data_str); g_strfreev(elements_list); return error; } /** * @brief Validator-functions are getting executed when the user wants to save profile * settings. It uses the given data (See RemminaProtocolSetting array) to determine * if the given value is a valid integer is in range and returns a end-user * friendly error message. * * @param key Key is the setting's name. * @param value Value to validate. * @param data Data needed for validation process. See RemminaProtocolSetting array. * * @returns End-user friendly and translated error message, explaining why the given * value is invalid. If the given value is error-free then NULL gets returned. * */ static GError* rmplugin_x2go_int_setting_validator(gchar* key, gpointer value, gchar* data) { GError *error = NULL; gchar **integer_list = g_strsplit(data, ";", 0); if (integer_list == NULL || integer_list[0] == NULL || integer_list[1] == NULL || strlen(integer_list[0]) <= 0 || strlen(integer_list[1]) <= 0) { gchar *error_msg = _("Validation data in ProtocolSettings array is invalid!"); REMMINA_PLUGIN_CRITICAL("%s", error_msg); g_set_error(&error, 1, 1, "%s", error_msg); return error; } gint minimum; str2int_errno err = str2int(&minimum, integer_list[0], 10); if (err == STR2INT_INCONVERTIBLE) { g_set_error(&error, 1, 1, "%s", g_strdup_printf( _("Internal error: %s"), _("The lower limit is not a valid integer!") )); } else if (err == STR2INT_OVERFLOW) { g_set_error(&error, 1, 1, "%s", g_strdup_printf( _("Internal error: %s"), _("The lower limit is too high!") )); } else if (err == STR2INT_UNDERFLOW) { g_set_error(&error, 1, 1, "%s", g_strdup_printf( _("Internal error: %s"), _("The lower limit is too low!") )); } else if (err == STR2INT_INVALID_DATA) { g_set_error(&error, 1, 1, "%s", g_strdup_printf( _("Internal error: %s"), _("Something unknown went wrong.") )); } if (error) { REMMINA_PLUGIN_CRITICAL("%s", _("Please check the RemminaProtocolSetting " "array for possible errors.")); return error; } gint maximum; err = str2int(&maximum, integer_list[1], 10); if (err == STR2INT_INCONVERTIBLE) { g_set_error(&error, 1, 1, "%s", g_strdup_printf( _("Internal error: %s"), _("The upper limit is not a valid integer!") )); } else if (err == STR2INT_OVERFLOW) { g_set_error(&error, 1, 1, "%s", g_strdup_printf( _("Internal error: %s"), _("The upper limit is too high!") )); } else if (err == STR2INT_UNDERFLOW) { g_set_error(&error, 1, 1, "%s", g_strdup_printf( _("Internal error: %s"), _("The upper limit is too low!") )); } else if (err == STR2INT_INVALID_DATA) { g_set_error(&error, 1, 1, "%s", g_strdup_printf( _("Internal error: %s"), _("Something unknown went wrong.") )); } if (error) { REMMINA_PLUGIN_CRITICAL("%s", _("Please check the RemminaProtocolSetting " "array for possible errors.")); return error; } gint int_value; err = str2int(&int_value, value, 10); if (err == STR2INT_INCONVERTIBLE) { // non-numerical characters are can't be entered but, the user can // input an empty string. g_set_error(&error, 1, 1, "%s", _("The input is not a valid integer!")); } else if (err == STR2INT_OVERFLOW || err == STR2INT_UNDERFLOW) { g_set_error(&error, 1, 1, _("Input must be a number between %i and %i."), minimum, maximum); } else if (err == STR2INT_INVALID_DATA) { g_set_error(&error, 1, 1, "%s", _("Something unknown went wrong.")); } if (error) { return error; } /*REMMINA_PLUGIN_DEBUG("Key: \t%s", (gchar*) key); REMMINA_PLUGIN_DEBUG("Value:\t%s", (gchar*) value); REMMINA_PLUGIN_DEBUG("Data: \t%s", data); REMMINA_PLUGIN_DEBUG("Min: %i, Max: %i", minimum, maximum); REMMINA_PLUGIN_DEBUG("Value converted:\t%i", int_value);*/ if (err == STR2INT_SUCCESS && (minimum > int_value || int_value > maximum)) { g_set_error(&error, 1, 1, _("Input must be a number between %i and %i."), minimum, maximum); } // Should be NULL. return error; } /* Array of RemminaProtocolSetting for basic settings. * Each item is composed by: * a) RemminaProtocolSettingType for setting type * b) Setting name * c) Setting description * d) Compact disposition * e) Values for REMMINA_PROTOCOL_SETTING_TYPE_SELECT or REMMINA_PROTOCOL_SETTING_TYPE_COMBO * f) Setting tooltip * g) Validation data pointer, will be passed to the validation callback method. * h) Validation callback method (Can be NULL. Every entry will be valid then.) * use following prototype: * gboolean mysetting_validator_method(gpointer key, gpointer value, * gpointer validator_data); * gpointer key is a gchar* containing the setting's name, * gpointer value contains the value which should be validated, * gpointer validator_data contains your passed data. */ static const RemminaProtocolSetting rmplugin_x2go_basic_settings[] = { {REMMINA_PROTOCOL_SETTING_TYPE_SERVER, "server", NULL, FALSE, NULL, NULL, NULL, NULL}, {REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "username", N_("Username"), FALSE, NULL, NULL, NULL, NULL}, {REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD, "password", N_("Password"), FALSE, NULL, NULL, NULL, NULL}, {REMMINA_PROTOCOL_SETTING_TYPE_COMBO, "command", N_("Startup program"), FALSE, /* SELECT & COMBO Values */ "MATE,KDE,XFCE,LXDE,TERMINAL", /* Tooltip */ N_("Which command should be executed after creating the X2Go session?"), NULL, NULL}, {REMMINA_PROTOCOL_SETTING_TYPE_RESOLUTION, "resolution", NULL, FALSE, NULL, NULL, NULL, NULL}, {REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "kbdlayout", N_("Keyboard Layout (auto)"), FALSE, NULL, NULL, NULL, NULL}, {REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "kbdtype", N_("Keyboard type (auto)"), FALSE, NULL, NULL, NULL, NULL}, {REMMINA_PROTOCOL_SETTING_TYPE_COMBO, "audio", N_("Audio support"), FALSE, /* SELECT & COMBO Values */ "pulse,esd,none", /* Tooltip */ N_("The sound system of the X2Go server (default: 'pulse')."), /* Validation data */ "pulse,esd,none", /* Validation method */ G_CALLBACK(rmplugin_x2go_string_setting_validator)}, {REMMINA_PROTOCOL_SETTING_TYPE_COMBO, "clipboard", N_("Clipboard direction"), FALSE, /* SELECT & COMBO Values */ "none,server,client,both", /* Tooltip */ N_("Which direction should clipboard content be copied? " "(default: 'both')."), /* Validation data */ "none,server,client,both", /* Validation method */ G_CALLBACK(rmplugin_x2go_string_setting_validator)}, {REMMINA_PROTOCOL_SETTING_TYPE_INT, "dpi", N_("DPI resolution"), FALSE, NULL, /* Tooltip */ N_("Launch session with a specific resolution (in dots per inch). " "Must be between 20 and 400."), /* Validation data */ "20;400", // ";;" /* Validation method */ G_CALLBACK(rmplugin_x2go_int_setting_validator)}, {REMMINA_PROTOCOL_SETTING_TYPE_FILE, "ssh_privatekey", N_("SSH identity file"), FALSE, NULL, N_("Your private key"), NULL, NULL }, {REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, FALSE, NULL, NULL, NULL, NULL}}; /* Protocol plugin definition and features */ static RemminaProtocolPlugin rmplugin_x2go = { REMMINA_PLUGIN_TYPE_PROTOCOL, // Type PLUGIN_NAME, // Name PLUGIN_DESCRIPTION, // Description GETTEXT_PACKAGE, // Translation domain PLUGIN_VERSION, // Version number PLUGIN_APPICON, // Icon for normal connection PLUGIN_SSH_APPICON, // Icon for SSH connection rmplugin_x2go_basic_settings, // Array for basic settings NULL, // Array for advanced settings REMMINA_PROTOCOL_SSH_SETTING_NONE, // SSH settings type rmplugin_x2go_features, // Array for available features rmplugin_x2go_init, // Plugin initialization method rmplugin_x2go_open_connection, // Plugin open connection method rmplugin_x2go_close_connection, // Plugin connection-method closure rmplugin_x2go_query_feature, // Query for available features NULL, // Call a feature NULL, // Send a keystroke NULL, // Screenshot }; G_MODULE_EXPORT gboolean remmina_plugin_entry(RemminaPluginService *service) { TRACE_CALL("remmina_plugin_entry"); rm_plugin_service = service; bindtextdomain(GETTEXT_PACKAGE, REMMINA_RUNTIME_LOCALEDIR); bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8"); if (!service->register_plugin((RemminaPlugin *) &rmplugin_x2go)) { return FALSE; } pthread_mutex_init(&remmina_x2go_init_mutex, NULL); remmina_x2go_window_id_array = g_array_new(FALSE, TRUE, sizeof(Window)); REMMINA_PLUGIN_MESSAGE("%s", _("X2Go plugin loaded.")); return TRUE; }