Remmina - The GTK+ Remote Desktop Client  v1.4.31
Remmina is a remote desktop client written in GTK+, aiming to be useful for system administrators and travellers, who need to work with lots of remote computers in front of either large monitors or tiny netbooks. Remmina supports multiple network protocols in an integrated and consistent user interface. Currently RDP, VNC, NX, XDMCP and SSH are supported.
x2go_plugin.c
Go to the documentation of this file.
1 /*
2  * Project: Remmina Plugin X2Go
3  * Description: Remmina protocol plugin to connect via X2Go using PyHocaCLI
4  * Author: Mike Gabriel <mike.gabriel@das-netzwerkteam.de>
5  * Antenore Gatta <antenore@simbiosi.org>
6  * Copyright: 2010-2011 Vic Lee
7  * 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo
8  * 2015 Antenore Gatta
9  * 2016-2018 Antenore Gatta, Giovanni Panozzo
10  * 2019 Mike Gabriel
11  * 2021 Daniel Teichmann
12  * License: GPL-2+
13  *
14  * This program is free software; you can redistribute it and/or modify
15  * it under the terms of the GNU General Public License as published by
16  * the Free Software Foundation; either version 2 of the License, or
17  * (at your option) any later version.
18  *
19  * This program is distributed in the hope that it will be useful,
20  * but WITHOUT ANY WARRANTY; without even the implied warranty of
21  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22  * GNU General Public License for more details.
23  *
24  * You should have received a copy of the GNU General Public License
25  * along with this program; if not, write to the Free Software
26  * Foundation, Inc., 51 Franklin Street, Fifth Floor,
27  * Boston, MA 02110-1301, USA.
28  *
29  * In addition, as a special exception, the copyright holders give
30  * permission to link the code of portions of this program with the
31  * OpenSSL library under certain conditions as described in each
32  * individual source file, and distribute linked combinations
33  * including the two.
34  * You must obey the GNU General Public License in all respects
35  * for all of the code used other than OpenSSL. * If you modify
36  * file(s) with this exception, you may extend this exception to your
37  * version of the file(s), but you are not obligated to do so. * If you
38  * do not wish to do so, delete this exception statement from your
39  * version. * If you delete this exception statement from all source
40  * files in the program, then also delete it here.
41  *
42  */
43 
44 #include "x2go_plugin.h"
45 #include "common/remmina_plugin.h"
46 
47 #include <gtk/gtkx.h>
48 #ifdef GDK_WINDOWING_X11
49 #include <gdk/gdkx.h>
50 #elif defined(GDK_WINDOWING_WAYLAND)
51 #include <gdk/gdkwayland.h>
52 #endif
53 
54 #include <X11/Xlib.h>
55 #include <X11/XKBlib.h>
56 #include <X11/extensions/XKBrules.h>
57 
58 #include <sys/types.h>
59 #include <signal.h>
60 #include <time.h>
61 #include <ctype.h>
62 
63 #define FEATURE_AVAILABLE(gpdata, feature) \
64  gpdata->available_features ? (g_list_find_custom( \
65  gpdata->available_features, \
66  feature, \
67  (GCompareFunc) g_strcmp0 \
68  ) ? TRUE : FALSE) : FALSE
69 
70 #define FEATURE_NOT_AVAIL_STR(feature) \
71  g_strdup_printf(_("The command-line feature '%s' is not available! Attempting " \
72  "to start PyHoca-CLI without using this feature…"), feature)
73 
74 #define GET_PLUGIN_DATA(gp) \
75  (RemminaPluginX2GoData*) g_object_get_data(G_OBJECT(gp), "plugin-data")
76 
77 // --------- SESSIONS ------------
78 #define SET_RESUME_SESSION(gp, resume_data) \
79  g_object_set_data_full(G_OBJECT(gp), "resume-session-data", \
80  resume_data, \
81  g_free)
82 
83 #define GET_RESUME_SESSION(gp) \
84  (gchar*) g_object_get_data(G_OBJECT(gp), "resume-session-data")
85 
86 // A session is selected if the returning value is something other than 0.
87 #define IS_SESSION_SELECTED(gp) \
88  g_object_get_data(G_OBJECT(gp), "session-selected") ? TRUE : FALSE
89 
90 // We don't use the function as a real pointer but rather as a boolean value.
91 #define SET_SESSION_SELECTED(gp, is_session_selected) \
92  g_object_set_data_full(G_OBJECT(gp), "session-selected", \
93  is_session_selected, \
94  NULL)
95 // -------------------
96 
97 #define REMMINA_PLUGIN_INFO(fmt, ...) \
98  rm_plugin_service->_remmina_info("[%s] " fmt, \
99  PLUGIN_NAME, ##__VA_ARGS__)
100 
101 #define REMMINA_PLUGIN_MESSAGE(fmt, ...) \
102  rm_plugin_service->_remmina_message("[%s] " fmt, \
103  PLUGIN_NAME, ##__VA_ARGS__)
104 
105 #define REMMINA_PLUGIN_DEBUG(fmt, ...) \
106  rm_plugin_service->_remmina_debug(__func__, "[%s] " fmt, \
107  PLUGIN_NAME, ##__VA_ARGS__)
108 
109 #define REMMINA_PLUGIN_WARNING(fmt, ...) \
110  rm_plugin_service->_remmina_warning(__func__, "[%s] " fmt, \
111  PLUGIN_NAME, ##__VA_ARGS__)
112 
113 #define REMMINA_PLUGIN_AUDIT(fmt, ...) \
114  rm_plugin_service->_remmina_audit(__func__, fmt, ##__VA_ARGS__)
115 
116 #define REMMINA_PLUGIN_ERROR(fmt, ...) \
117  rm_plugin_service->_remmina_error(__func__, "[%s] " fmt, \
118  PLUGIN_NAME, ##__VA_ARGS__)
119 
120 #define REMMINA_PLUGIN_CRITICAL(fmt, ...) \
121  rm_plugin_service->_remmina_critical(__func__, "[%s] " fmt, \
122  PLUGIN_NAME, ##__VA_ARGS__)
123 
124 #define GET_PLUGIN_STRING(value) \
125  g_strdup(rm_plugin_service->file_get_string(remminafile, value))
126 
127 #define GET_PLUGIN_PASSWORD(value) \
128  GET_PLUGIN_STRING(value)
129 
130 #define GET_PLUGIN_INT(value, default_value) \
131  rm_plugin_service->file_get_int(remminafile, value, default_value)
132 
133 #define GET_PLUGIN_BOOLEAN(value) \
134  rm_plugin_service->file_get_int(remminafile, value, FALSE)
135 
137 
138 typedef struct _RemminaPluginX2GoData {
139  GtkWidget *socket;
140  gint socket_id;
141 
142  pthread_t thread;
143 
144  Display *display;
145  Window window_id;
146  int (*orig_handler)(Display *, XErrorEvent *);
147 
148  GPid pidx2go;
149 
150  gboolean disconnected;
151 
154 
159 typedef struct _X2GoCustomUserData {
161  gpointer dialog_data;
162  gpointer connect_data;
163  gpointer opt1;
164  gpointer opt2;
166 
188  SESSION_NUM_PROPERTIES // Must be last. Counts all enum elements.
189 };
190 
191 // Following str2int code was adapted from Stackoverflow:
192 // https://stackoverflow.com/questions/7021725/how-to-convert-a-string-to-integer-in-c
193 typedef enum _str2int_errno {
199 } str2int_errno;
200 
217 str2int_errno str2int(gint *out, gchar *s, gint base)
218 {
219  gchar *end;
220 
221  if (!s || !out || base <= 0) return STR2INT_INVALID_DATA;
222 
223  if (s[0] == '\0' || isspace(s[0])) return STR2INT_INCONVERTIBLE;
224 
225  errno = 0;
226  glong l = strtol(s, &end, base);
227 
228  /* Both checks are needed because INT_MAX == LONG_MAX is possible. */
229  if (l > INT_MAX || (errno == ERANGE && l == LONG_MAX)) return STR2INT_OVERFLOW;
230  if (l < INT_MIN || (errno == ERANGE && l == LONG_MIN)) return STR2INT_UNDERFLOW;
231  if (*end != '\0') return STR2INT_INCONVERTIBLE;
232 
233  *out = l;
234  return STR2INT_SUCCESS;
235 }
236 
268 {
269  GtkWindow *parent;
270  GtkDialogFlags flags;
271  GtkMessageType type;
272  GtkButtonsType buttons;
273  gchar *title;
274  gchar *message;
275  GCallback callbackfunc;
276 
277  // If the dialog needs to be custom.
280 };
281 
290 static gboolean rmplugin_x2go_open_dialog(X2GoCustomUserData *custom_data)
291 {
292  REMMINA_PLUGIN_DEBUG("Function entry.");
293 
294  if (!custom_data || !custom_data->gp || !custom_data->dialog_data) {
295  REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
296  _("Internal error: %s"),
297  _("Parameter 'custom_data' is not initialized!")
298  ));
299 
300  return G_SOURCE_REMOVE;
301  }
302 
303  RemminaProtocolWidget *gp = (RemminaProtocolWidget*) custom_data->gp;
304  struct _DialogData *ddata = (struct _DialogData*) custom_data->dialog_data;
305 
306  if (ddata) {
307  // Can't check type, flags or buttons
308  // because they are enums and '0' is a valid value
309  if (!ddata->title || !ddata->message) {
310  REMMINA_PLUGIN_CRITICAL("%s", _("Broken `DialogData`! Aborting…"));
311  return G_SOURCE_REMOVE;
312  }
313  } else {
314  REMMINA_PLUGIN_CRITICAL("%s", _("Can't retrieve `DialogData`! Aborting…"));
315  return G_SOURCE_REMOVE;
316  }
317 
318  REMMINA_PLUGIN_DEBUG("`DialogData` checks passed. Now showing dialog…");
319 
320  GtkWidget* widget_gtk_dialog = NULL;
321 
322  if (ddata->dialog_factory_func != NULL) {
323  REMMINA_PLUGIN_DEBUG("Calling *custom* dialog factory function…");
324  GCallback dialog_factory_func = G_CALLBACK(ddata->dialog_factory_func);
325  gpointer dialog_factory_data = ddata->dialog_factory_data;
326 
327  // Calling dialog_factory_func(custom_data, dialog_factory_data);
328  widget_gtk_dialog = ((GtkWidget* (*)(X2GoCustomUserData*, gpointer))
329  dialog_factory_func)(custom_data, dialog_factory_data);
330  } else {
331  widget_gtk_dialog = gtk_message_dialog_new(ddata->parent,
332  ddata->flags,
333  ddata->type,
334  ddata->buttons,
335  "%s", ddata->title);
336 
337  gtk_message_dialog_format_secondary_text(
338  GTK_MESSAGE_DIALOG(widget_gtk_dialog), "%s", ddata->message);
339  }
340 
341  if (!widget_gtk_dialog) {
342  REMMINA_PLUGIN_CRITICAL("Error! Aborting.");
343  return G_SOURCE_REMOVE;
344  }
345 
346  if (ddata->callbackfunc) {
347  g_signal_connect_swapped(G_OBJECT(widget_gtk_dialog), "response",
348  G_CALLBACK(ddata->callbackfunc),
349  custom_data);
350  } else {
351  g_signal_connect(G_OBJECT(widget_gtk_dialog), "response",
352  G_CALLBACK(gtk_widget_destroy),
353  NULL);
354  }
355 
356  gtk_widget_show_all(widget_gtk_dialog);
357 
358  // Delete ddata object and reference 'dialog-data' in gp.
359  g_object_set_data(G_OBJECT(gp), "dialog-data", NULL);
360 
361  return G_SOURCE_REMOVE;
362 }
363 
371 };
372 
382 static GtkWidget* rmplugin_x2go_find_child(GtkWidget* parent, const gchar* name)
383 {
384  const gchar* parent_name = gtk_widget_get_name((GtkWidget*) parent);
385  if (g_ascii_strcasecmp(parent_name, (gchar*) name) == 0) {
386  return parent;
387  }
388 
389  if (GTK_IS_BIN(parent)) {
390  GtkWidget *child = gtk_bin_get_child(GTK_BIN(parent));
391  return rmplugin_x2go_find_child(child, name);
392  }
393 
394  if (GTK_IS_CONTAINER(parent)) {
395  GList *children = gtk_container_get_children(GTK_CONTAINER(parent));
396  while (children != NULL) {
397  GtkWidget *widget = rmplugin_x2go_find_child(children->data, name);
398  if (widget != NULL) {
399  return widget;
400  }
401 
402  children = g_list_next(children);
403  }
404  }
405 
406  return NULL;
407 }
408 
419 static gboolean rmplugin_x2go_session_chooser_row_activated(GtkTreeView *treeview,
420  GtkTreePath *path,
421  GtkTreeViewColumn *column,
422  X2GoCustomUserData *custom_data)
423 {
424  REMMINA_PLUGIN_DEBUG("Function entry.");
425 
426  if (!custom_data || !custom_data->gp || !custom_data->opt1) {
427  REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
428  _("Internal error: %s"),
429  _("Parameter 'custom_data' is not initialized!")
430  ));
431 
432  return G_SOURCE_REMOVE;
433  }
434 
435  RemminaProtocolWidget* gp = (RemminaProtocolWidget*) custom_data->gp;
436  // dialog_data (unused)
437  // connect_data (unused)
438  GtkWidget* dialog = GTK_WIDGET(custom_data->opt1);
439 
440  gchar *session_id;
441  GtkTreeIter iter;
442  GtkTreeModel *model = gtk_tree_view_get_model(treeview);
443 
444  if (gtk_tree_model_get_iter(model, &iter, path)) {
445  gtk_tree_model_get(GTK_TREE_MODEL(model), &iter,
446  SESSION_SESSION_ID, &session_id, -1);
447 
448  // Silent bail out.
449  if (!session_id || strlen(session_id) <= 0) return G_SOURCE_REMOVE;
450 
451  SET_RESUME_SESSION(gp, session_id);
452 
453  // Unstucking main process. Telling it that a session has been selected.
454  // We use a trick here. As long as there is something other than 0
455  // stored, a session is selected. So we use the gpointer as a gboolean.
456  SET_SESSION_SELECTED(gp, (gpointer) TRUE);
457  gtk_widget_hide(GTK_WIDGET(dialog));
458  gtk_widget_destroy(GTK_WIDGET(dialog));
459  }
460 
461  return G_SOURCE_REMOVE;
462 }
463 
471 static gchar *rmplugin_x2go_session_property_to_string(guint session_property) {
472  gchar* return_char = NULL;
473 
474  switch (session_property) {
475  // I think we can close one eye here regarding max line-length.
476  case SESSION_DISPLAY: return_char = g_strdup(_("X Display")); break;
477  case SESSION_STATUS: return_char = g_strdup(_("Status")); break;
478  case SESSION_SESSION_ID: return_char = g_strdup(_("Session ID")); break;
479  case SESSION_CREATE_DATE: return_char = g_strdup(_("Create date")); break;
480  case SESSION_SUSPENDED_SINCE: return_char = g_strdup(_("Suspended since")); break;
481  case SESSION_AGENT_PID: return_char = g_strdup(_("Agent PID")); break;
482  case SESSION_USERNAME: return_char = g_strdup(_("Username")); break;
483  case SESSION_HOSTNAME: return_char = g_strdup(_("Hostname")); break;
484  case SESSION_COOKIE: return_char = g_strdup(_("Cookie")); break;
485  case SESSION_GRAPHIC_PORT: return_char = g_strdup(_("Graphic port")); break;
486  case SESSION_SND_PORT: return_char = g_strdup(_("SND port")); break;
487  case SESSION_SSHFS_PORT: return_char = g_strdup(_("SSHFS port")); break;
488  case SESSION_DIALOG_IS_VISIBLE: return_char = g_strdup(_("Visible")); break;
489  }
490 
491  return return_char;
492 }
493 
510  GList *sessions_list)
511 {
512  REMMINA_PLUGIN_DEBUG("Function entry.");
513 
514  if (!custom_data || !custom_data->gp ||
515  !custom_data->dialog_data || !custom_data->connect_data) {
516  REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
517  _("Internal error: %s"),
518  _("Parameter 'custom_data' is not initialized!")
519  ));
520 
521  return NULL;
522  }
523 
524  struct _DialogData* ddata = (struct _DialogData*) custom_data->dialog_data;
525 
526  if (!ddata || !sessions_list || !ddata->title) {
527  REMMINA_PLUGIN_CRITICAL("%s", _("Could not retrieve valid `DialogData` or "
528  "`sessions_list`! Aborting…"));
529  return NULL;
530  }
531 
532  GtkWidget *widget_gtk_dialog = NULL;
533  widget_gtk_dialog = gtk_dialog_new_with_buttons(ddata->title, ddata->parent,
534  ddata->flags,
535  // TRANSLATORS: Stick to x2goclient's translation for terminate.
536  _("_Terminate"),
538  // TRANSLATORS: Stick to x2goclient's translation for resume.
539  _("_Resume"),
541  _("_New"),
543  NULL);
544 
545  GtkWidget *button = gtk_dialog_get_widget_for_response(
546  GTK_DIALOG(widget_gtk_dialog),
548  // TRANSLATORS: Tooltip for terminating button inside Session-Chooser-Dialog.
549  // TRANSLATORS: Please stick to X2GoClient's way of translating.
550  gtk_widget_set_tooltip_text(button, _("Terminating X2Go sessions can take a moment."));
551 
552  #define DEFAULT_DIALOG_WIDTH 720
553  #define DEFAULT_DIALOG_HEIGHT (DEFAULT_DIALOG_WIDTH * 9) / 16
554 
555  gtk_widget_set_size_request(GTK_WIDGET(widget_gtk_dialog),
556  DEFAULT_DIALOG_WIDTH, DEFAULT_DIALOG_HEIGHT);
557  gtk_window_set_default_size(GTK_WINDOW(widget_gtk_dialog),
558  DEFAULT_DIALOG_WIDTH, DEFAULT_DIALOG_HEIGHT);
559 
560  gtk_window_set_resizable(GTK_WINDOW(widget_gtk_dialog), TRUE);
561 
562  GtkWidget *scrolled_window = gtk_scrolled_window_new(NULL, NULL);
563  //gtk_widget_show(scrolled_window);
564 
565  gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(
566  GTK_DIALOG(widget_gtk_dialog))
567  ), GTK_WIDGET(scrolled_window), TRUE, TRUE, 5);
568 
569  gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW (scrolled_window),
570  GTK_POLICY_AUTOMATIC,
571  GTK_POLICY_AUTOMATIC);
572 
573 
574  GType types[SESSION_NUM_PROPERTIES];
575 
576  // First to last in SESSION_PROPERTIES.
577  for (gint i = 0; i < SESSION_NUM_PROPERTIES; ++i) {
578  // Everything is a String. (Except IS_VISIBLE flag)
579  // If that changes one day, you could extent the if statement here.
580  // But you would propably need a *lot* of refactoring.
581  // Especially in the session parser.
582  if (i == SESSION_DIALOG_IS_VISIBLE) {
583  types[i] = G_TYPE_BOOLEAN;
584  } else {
585  types[i] = G_TYPE_STRING;
586  }
587  }
588 
589  // create tree view
590  GtkListStore *store = gtk_list_store_newv(SESSION_NUM_PROPERTIES, types);
591 
592  GtkTreeModelFilter *filter = GTK_TREE_MODEL_FILTER(
593  gtk_tree_model_filter_new(GTK_TREE_MODEL(store),
594  NULL)
595  );
596  gtk_tree_model_filter_set_visible_column(filter, SESSION_DIALOG_IS_VISIBLE);
597 
598  GtkWidget *tree_view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(filter));
599  g_object_unref (G_OBJECT (store)); // tree now holds reference
600  gtk_widget_set_size_request(tree_view, -1, 300);
601 
602  // Gets name to be findable by rmplugin_x2go_find_child()
603  gtk_widget_set_name(GTK_WIDGET(tree_view), "session_chooser_treeview");
604 
605  // create list view columns
606  gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(tree_view), TRUE);
607  gtk_tree_view_set_headers_clickable (GTK_TREE_VIEW(tree_view), FALSE);
608  gtk_tree_view_set_enable_search(GTK_TREE_VIEW(tree_view), TRUE);
609  gtk_widget_show (tree_view);
610  gtk_container_add (GTK_CONTAINER(scrolled_window), tree_view);
611 
612  GtkTreeViewColumn *tree_view_col = NULL;
613  GtkCellRenderer *cell_renderer = NULL;
614  gchar *header_title = NULL;
615 
616  // First to last in SESSION_PROPERTIES.
617  for (guint i = 0; i < SESSION_NUM_PROPERTIES; ++i) {
618  // Do not display SESSION_DIALOG_IS_VISIBLE.
619  if (i == SESSION_DIALOG_IS_VISIBLE) continue;
620 
622  if (!header_title) {
623  REMMINA_PLUGIN_WARNING("%s", g_strdup_printf(
624  _("Internal error: %s"), g_strdup_printf(
625  _("Unknown property '%i'"), i
626  )));
627  header_title = g_strdup_printf(_("Unknown property '%i'"), i);
628  }
629 
630  tree_view_col = gtk_tree_view_column_new();
631  gtk_tree_view_column_set_title(tree_view_col, header_title);
632  gtk_tree_view_column_set_clickable(tree_view_col, FALSE);
633  gtk_tree_view_column_set_sizing (tree_view_col, GTK_TREE_VIEW_COLUMN_AUTOSIZE);
634  gtk_tree_view_column_set_resizable(tree_view_col, TRUE);
635 
636  cell_renderer = gtk_cell_renderer_text_new();
637  gtk_tree_view_column_pack_start(tree_view_col, cell_renderer, TRUE);
638  gtk_tree_view_column_add_attribute(tree_view_col, cell_renderer, "text", i);
639  gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view), tree_view_col);
640  }
641 
642  GList *elem = NULL;
643  GtkTreeIter iter;
644 
645  for (elem = sessions_list; elem; elem = elem->next) {
646  gchar** session = (gchar**) elem->data;
647  g_assert(session != NULL);
648 
649  gtk_list_store_append(store, &iter);
650 
651  for (gint i = 0; i < SESSION_NUM_PROPERTIES; i++) {
652  gchar* property = session[i];
653  GValue a = G_VALUE_INIT;
654 
655  // Everything here is a string (except SESSION_DIALOG_IS_VISIBLE)
656 
657  if (i == SESSION_DIALOG_IS_VISIBLE) {
658  g_value_init(&a, G_TYPE_BOOLEAN);
659  g_assert(G_VALUE_HOLDS_BOOLEAN(&a) && "GValue does not "
660  "hold a boolean!");
661  // Default is to show every new session.
662  g_value_set_boolean(&a, TRUE);
663  } else {
664  g_value_init(&a, G_TYPE_STRING);
665  g_assert(G_VALUE_HOLDS_STRING(&a) && "GValue does not "
666  "hold a string!");
667  g_value_set_static_string (&a, property);
668  }
669 
670  gtk_list_store_set_value(store, &iter, i, &a);
671  }
672  }
673 
674  /* Prepare X2GoCustomUserData *custom_data
675  * gp -> gp (RemminaProtocolWidget*)
676  * dialog_data -> dialog data (struct _DialogData*)
677  * connect_data -> connection data (struct _ConnectionData*)
678  * opt1 -> dialog widget (GtkWidget*)
679  */
680  // everything else is already initialized.
681  custom_data->opt1 = widget_gtk_dialog;
682 
683  g_signal_connect(tree_view, "row-activated",
685  custom_data);
686 
687  return widget_gtk_dialog;
688 }
689 
702 static GtkTreeModelFilter* rmplugin_x2go_session_chooser_get_filter_model(GtkWidget *dialog,
703  GtkTreeView* treeview)
704 {
705  //REMMINA_PLUGIN_DEBUG("Function entry.");
706  GtkTreeModel *return_model = NULL;
707 
708  if (!treeview && dialog) {
709  GtkWidget *treeview_new = rmplugin_x2go_find_child(GTK_WIDGET(dialog),
710  "session_chooser_treeview");
711 
712  if (!treeview_new) {
713  REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
714  _("Internal error: %s"),
715  _("Could not find child GtkTreeView of "
716  "session chooser dialog.")
717  ));
718  return NULL;
719  }
720 
721  return_model = gtk_tree_view_get_model(GTK_TREE_VIEW(treeview_new));
722  } else if (treeview) {
723  return_model = gtk_tree_view_get_model(GTK_TREE_VIEW(treeview));
724  } else {
725  REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
726  _("Internal error: %s"),
727  _("Neither the 'dialog' nor 'treeview' parameters are initialized! "
728  "At least one of them must be given.")
729  ));
730  return NULL;
731  }
732 
733  if (!return_model || !GTK_TREE_MODEL_FILTER(return_model)) {
734  REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
735  _("Internal error: %s"),
736  _("Could not obtain \"GtkTreeModelFilter*\" of the session chooser dialog, "
737  "for unknown reason.")
738  ));
739  }
740 
741  return GTK_TREE_MODEL_FILTER(return_model);
742 }
743 
753 static GtkTreePath* rmplugin_x2go_session_chooser_get_selected_row(GtkWidget *dialog)
754 {
755  REMMINA_PLUGIN_DEBUG("Function entry.");
756 
757  GtkWidget *treeview = rmplugin_x2go_find_child(GTK_WIDGET(dialog),
758  "session_chooser_treeview");
759  if (!treeview) {
760  REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
761  _("Internal error: %s"),
762  _("Could not find child GtkTreeView of session chooser dialog.")
763  ));
764  return NULL;
765  }
766 
767  GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(treeview));
768  if (!selection) {
769  REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
770  _("Internal error: %s"),
771  _("Could not get currently selected row (session)!")
772  ));
773  return NULL;
774  }
775 
776  GtkTreeModelFilter *filter = rmplugin_x2go_session_chooser_get_filter_model(
777  NULL, GTK_TREE_VIEW(treeview));
778  GtkTreeModel *model = gtk_tree_model_filter_get_model(filter);
779  if (!model) return NULL; // error message was already handled.
780 
781  GtkTreeModel *filter_model = GTK_TREE_MODEL(filter);
782  g_assert(filter_model && "Could not cast 'filter' to a GtkTreeModel!");
783  GList *selected_rows = gtk_tree_selection_get_selected_rows(selection, &filter_model);
784 
785  // We only support single selection.
786  gint selected_rows_num = gtk_tree_selection_count_selected_rows(selection);
787  if (selected_rows_num != 1) {
788  REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
789  _("Internal error: %s"), g_strdup_printf(
790  _("Exactly one session should be selectable but '%i' rows "
791  "(sessions) are selected."),
792  selected_rows_num
793  )));
794  return NULL;
795  }
796 
797  // This would be very dangerous (we didn't check for NULL) if we hadn't just
798  // checked that only one row is selected.
799  GtkTreePath *path = selected_rows->data;
800 
801  // Convert to be path of GtkTreeModelFilter and *not* its child GtkTreeModel.
802  path = gtk_tree_model_filter_convert_child_path_to_path(filter, path);
803 
804  return path;
805 }
806 
817 static GValue rmplugin_x2go_session_chooser_get_property(GtkWidget *dialog,
818  gint property_index,
819  GtkTreePath *row)
820 {
821  //REMMINA_PLUGIN_DEBUG("Function entry.");
822 
823  GValue ret_value = G_VALUE_INIT;
824 
825  if (!row) {
826  GtkTreePath *selected_row = rmplugin_x2go_session_chooser_get_selected_row(dialog);
827  if (!selected_row) return ret_value; // error message was already handled.
828  row = selected_row;
829  }
830 
831  GtkTreeModelFilter *filter = rmplugin_x2go_session_chooser_get_filter_model(dialog, NULL);
832  GtkTreeModel *model = gtk_tree_model_filter_get_model(filter);
833  if (!model) return ret_value; // error message was already handled.
834 
835  GtkTreeIter iter;
836  gboolean success = gtk_tree_model_get_iter(model, &iter, row);
837  if (!success) {
838  REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
839  _("Internal error: %s"),
840  _("Failed to fill 'GtkTreeIter'.")
841  ));
842 
843  return ret_value;
844  }
845 
846  GValue property = G_VALUE_INIT;
847  gtk_tree_model_get_value(model, &iter, property_index, &property);
848 
849  return property;
850 }
851 
859 /*static void rmplugin_x2go_dump_session_properties(GtkTreeModel *model, GtkTreePath *path,
860  GtkTreeIter *iter, GtkWidget *dialog)
861 {
862  //REMMINA_PLUGIN_DEBUG("Function entry.");
863 
864  g_debug(_("Properties for session with path '%s':"), gtk_tree_path_to_string(path));
865  for (guint i = 0; i < SESSION_NUM_PROPERTIES; i++) {
866  GValue property = G_VALUE_INIT;
867  property = rmplugin_x2go_session_chooser_get_property(dialog, i, path);
868 
869  gchar* display_name = rmplugin_x2go_session_property_to_string(i);
870  g_assert(display_name && "Could not get display name for a property!");
871 
872  if (i == SESSION_DIALOG_IS_VISIBLE) {
873  g_assert(G_VALUE_HOLDS_BOOLEAN(&property) && "GValue does not "
874  "hold a boolean!");
875  g_debug("\t%s: '%s'", display_name,
876  g_value_get_boolean(&property) ? "TRUE" : "FALSE");
877  } else {
878  g_assert(G_VALUE_HOLDS_STRING(&property) && "GValue does not "
879  "hold a string!");
880  g_debug("\t%s: '%s'", display_name, g_value_get_string(&property));
881  }
882  }
883 }*/
884 
898 static gchar* rmplugin_x2go_spawn_pyhoca_process(guint argc, gchar* argv[],
899  GError** error, gchar** env)
900 {
901  REMMINA_PLUGIN_DEBUG("Function entry.");
902 
903  if (!argv) {
904  gchar* errmsg = g_strdup_printf(
905  _("Internal error: %s"),
906  _("parameter 'argv' is 'NULL'.")
907  );
908  REMMINA_PLUGIN_CRITICAL("%s", errmsg);
909  g_set_error(error, 1, 1, "%s", errmsg);
910  return NULL;
911  }
912 
913  if (!error) {
914  // Can't report error message back since 'error' is NULL.
915  REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
916  _("Internal error: %s"),
917  _("parameter 'error' is 'NULL'.")
918  ));
919  return NULL;
920  }
921 
922  if (!env || !env[0]) {
923  gchar* errmsg = g_strdup_printf(
924  _("Internal error: %s"),
925  _("parameter 'env' is either invalid or uninitialized.")
926  );
927  REMMINA_PLUGIN_CRITICAL("%s", errmsg);
928  g_set_error(error, 1, 1, "%s", errmsg);
929  return NULL;
930  }
931 
932  gint exit_code = 0;
933  gchar *standard_out;
934  // Just supresses pyhoca-cli's help message when pyhoca-cli's version is too old.
935  gchar *standard_err;
936 
937  gboolean success_ret = g_spawn_sync(NULL, argv, env, G_SPAWN_SEARCH_PATH, NULL,
938  NULL, &standard_out, &standard_err,
939  &exit_code, error);
940 
941  REMMINA_PLUGIN_INFO("%s", _("Started PyHoca-CLI with the following arguments:"));
942  // Print every argument except passwords. Free all arg strings.
943  for (gint i = 0; i < argc - 1; i++) {
944  gchar* curr_arg = argv[i];
945 
946  if (g_str_equal(curr_arg, "--password") ||
947  g_str_equal(curr_arg, "--ssh-passphrase")) {
948  g_printf("%s ", curr_arg);
949  g_printf("XXXXXX ");
950  g_free(curr_arg);
951  g_free(argv[++i]);
952  continue;
953  } else {
954  g_printf("%s ", curr_arg);
955  g_free(curr_arg);
956  }
957  }
958  g_printf("\n");
959 
960  /* TOO VERBOSE: */
961  /*
962  REMMINA_PLUGIN_DEBUG("%s", _("Started PyHoca-CLI with the "
963  "following environment variables:"));
964  REMMINA_PLUGIN_DEBUG("%s", g_strjoinv("\n", env));
965  */
966 
967  if (standard_err && strlen(standard_err) > 0) {
968  if (g_str_has_prefix(standard_err, "pyhoca-cli: error: a socket error "
969  "occured while establishing the connection:")) {
970  // Log error into GUI.
971  gchar* errmsg = g_strdup_printf(
972  _("The necessary PyHoca-CLI process has encountered a "
973  "internet connection problem.")
974  );
975 
976  // Log error into debug window and stdout
977  REMMINA_PLUGIN_CRITICAL("%s:\n%s", errmsg, standard_err);
978  g_set_error(error, 1, 1, "%s", errmsg);
979  return NULL;
980  } else {
981  gchar* errmsg = g_strdup_printf(
982  _("Could not start "
983  "PyHoca-CLI:\n%s"),
984  standard_err
985  );
986  REMMINA_PLUGIN_CRITICAL("%s", errmsg);
987  g_set_error(error, 1, 1, "%s", errmsg);
988  return NULL;
989  }
990  } else if (!success_ret || (*error) || strlen(standard_out) <= 0 || exit_code) {
991  if (!(*error)) {
992  gchar* errmsg = g_strdup_printf(
993  _("An unknown error occured while trying "
994  "to start PyHoca-CLI. Exit code: %i"),
995  exit_code);
996  REMMINA_PLUGIN_WARNING("%s", errmsg);
997  g_set_error(error, 1, 1, "%s", errmsg);
998  } else {
999  gchar* errmsg = g_strdup_printf(
1000  _("An unknown error occured while trying to start "
1001  "PyHoca-CLI. Exit code: %i. Error: '%s'"),
1002  exit_code, (*error)->message);
1003  REMMINA_PLUGIN_WARNING("%s", errmsg);
1004  }
1005 
1006  return NULL;
1007  }
1008 
1009  return standard_out;
1010 }
1011 
1017  gchar* host;
1018  gchar* username;
1019  gchar* password;
1022 };
1023 
1036 static gboolean rmplugin_x2go_session_chooser_set_row_visible(GtkTreePath *path,
1037  gboolean value,
1038  GtkDialog *dialog) {
1039  REMMINA_PLUGIN_DEBUG("Function entry.");
1040 
1041  if (!path || !dialog) {
1042  REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
1043  _("Internal error: %s"),
1044  _("Neither the 'path' nor 'dialog' parameters are initialized.")
1045  ));
1046  return FALSE;
1047  }
1048 
1049  GtkTreeModelFilter *filter = rmplugin_x2go_session_chooser_get_filter_model(
1050  GTK_WIDGET(dialog), NULL);
1051  GtkTreeModel *model = gtk_tree_model_filter_get_model(filter);
1052 
1053  // error message was already handled.
1054  if (!model) return FALSE;
1055 
1056  GtkTreeIter iter;
1057  if (!gtk_tree_model_get_iter(GTK_TREE_MODEL(model), &iter, path)) {
1058  REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
1059  _("Internal error: %s"),
1060  _("GtkTreePath 'path' describes a non-existing row!")
1061  ));
1062  return FALSE;
1063  }
1064 
1065 
1066  // Make session either visible or invisible.
1067  gtk_list_store_set(GTK_LIST_STORE(model), &iter,
1068  SESSION_DIALOG_IS_VISIBLE, value, -1);
1069 
1070  // Update row.
1071  gtk_tree_model_row_changed(GTK_TREE_MODEL(model), path, &iter);
1072 
1073  /* Get IS_VISIBLE flag of a session. */
1074  // GValue ret_value = G_VALUE_INIT;
1075  // ret_value = rmplugin_x2go_session_chooser_get_property(GTK_WIDGET(dialog),
1076  // SESSION_DIALOG_IS_VISIBLE,
1077  // path);
1078  // g_debug("Is visible: %s", g_value_get_boolean(&ret_value) ? "TRUE" : "FALSE");
1079 
1080 
1081  GtkWidget *term_button = gtk_dialog_get_widget_for_response(
1082  GTK_DIALOG(dialog),
1084  GtkWidget *resume_button = gtk_dialog_get_widget_for_response(
1085  GTK_DIALOG(dialog),
1087 
1088  // If no (visible) row is left to terminate disable terminate and resume buttons.
1089  gint rows_amount = gtk_tree_model_iter_n_children(GTK_TREE_MODEL(filter), NULL);
1090  if (rows_amount <= 0) {
1091  gtk_widget_set_sensitive(term_button, FALSE);
1092  gtk_widget_set_sensitive(resume_button, FALSE);
1093  } else {
1094  gtk_widget_set_sensitive(term_button, TRUE);
1095  gtk_widget_set_sensitive(resume_button, TRUE);
1096  }
1097 
1098  // Success, yay!
1099  return TRUE;
1100 }
1101 
1102 static gboolean rmplugin_x2go_verify_connection_data(struct _ConnectionData *connect_data) {
1103  /* Check connect_data. */
1104  if (!connect_data ||
1105  !connect_data->host ||
1106  !connect_data->username ||
1107  strlen(connect_data->host) <= 0 ||
1108  strlen(connect_data->username) <= 0)
1109  {
1110  REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
1111  _("Internal error: %s"),
1112  _("'Invalid connection data.'")
1113  ));
1114 
1115  return FALSE;
1116  }
1117 
1118  if (!connect_data->password && (!connect_data->ssh_privatekey ||
1119  strlen(connect_data->ssh_privatekey) <= 0)) {
1120  REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
1121  _("Internal error: %s"),
1122  _("'Invalid connection data.'")
1123  ));
1124  } else {
1125  return TRUE;
1126  }
1127 
1128  return FALSE;
1129 }
1130 
1146 {
1147  REMMINA_PLUGIN_DEBUG("Function entry.");
1148 
1149  if (!custom_data || !custom_data->gp ||
1150  !custom_data->dialog_data || !custom_data->connect_data ||
1151  !custom_data->opt1 || !custom_data->opt2) {
1152  REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
1153  _("Internal error: %s"),
1154  _("Parameter 'custom_data' is not fully initialized!")
1155  ));
1156 
1157  return G_SOURCE_REMOVE;
1158  }
1159 
1160  // Extract data passed by X2GoCustomUserData *custom_data.
1161  RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(custom_data->gp);
1162  //struct _DialogData *ddata = (struct _DialogData*) custom_data->dialog_data;
1163  struct _ConnectionData *connect_data = (struct _ConnectionData*) custom_data->connect_data;
1164  GtkTreePath* selected_row = (GtkTreePath*) custom_data->opt1;
1165  GtkDialog *dialog = GTK_DIALOG(custom_data->opt2);
1166 
1167  gchar *host = NULL;
1168  gchar *username = NULL;
1169  gchar *password = NULL;
1170  gchar *ssh_privatekey = NULL;
1171  gchar *ssh_passphrase = NULL;
1172  gboolean valid = rmplugin_x2go_verify_connection_data(connect_data);
1173  if (valid) {
1174  if (connect_data->password) password = connect_data->password;
1175  if (connect_data->ssh_privatekey) {
1176  ssh_privatekey = connect_data->ssh_privatekey;
1177 
1178  if (connect_data->ssh_passphrase) {
1179  ssh_passphrase = connect_data->ssh_passphrase;
1180  }
1181  }
1182 
1183  host = connect_data->host;
1184  username = connect_data->username;
1185  } else {
1186  return G_SOURCE_REMOVE;
1187  }
1188 
1189  GValue value = rmplugin_x2go_session_chooser_get_property(GTK_WIDGET(dialog),
1191  selected_row);
1192  // error message was handled already.
1193  if (!G_VALUE_HOLDS_STRING(&value)) return G_SOURCE_REMOVE;
1194  const gchar *session_id = g_value_get_string(&value);
1195 
1196  // We will now start pyhoca-cli with only the '--terminate $SESSION_ID' option.
1197  // (and of course auth related stuff)
1198  gchar *argv[50];
1199  gint argc = 0;
1200 
1201  argv[argc++] = g_strdup("pyhoca-cli");
1202 
1203  argv[argc++] = g_strdup("--server"); // Not listed as feature.
1204  argv[argc++] = g_strdup_printf("%s", host);
1205 
1206  if (FEATURE_AVAILABLE(gpdata, "USERNAME")) {
1207  argv[argc++] = g_strdup("-u");
1208  if (username) {
1209  argv[argc++] = g_strdup_printf("%s", username);
1210  } else {
1211  argv[argc++] = g_strdup_printf("%s", g_get_user_name());
1212  }
1213  } else {
1214  REMMINA_PLUGIN_CRITICAL("%s", FEATURE_NOT_AVAIL_STR("USERNAME"));
1215  return G_SOURCE_REMOVE;
1216  }
1217 
1218  if (password && FEATURE_AVAILABLE(gpdata, "PASSWORD")) {
1219  if (FEATURE_AVAILABLE(gpdata, "AUTH_ATTEMPTS")) {
1220  argv[argc++] = g_strdup("--auth-attempts");
1221  argv[argc++] = g_strdup_printf ("%i", 0);
1222  } else {
1223  REMMINA_PLUGIN_WARNING("%s", FEATURE_NOT_AVAIL_STR("AUTH_ATTEMPTS"));
1224  }
1225  if (strlen(password) > 0) {
1226  argv[argc++] = g_strdup("--force-password");
1227  argv[argc++] = g_strdup("--password");
1228  argv[argc++] = g_strdup_printf("%s", password);
1229  }
1230  } else if (!password) {
1231  REMMINA_PLUGIN_CRITICAL("%s", FEATURE_NOT_AVAIL_STR("PASSWORD"));
1232  return G_SOURCE_REMOVE;
1233  }
1234 
1235  if (FEATURE_AVAILABLE(gpdata, "TERMINATE")) {
1236  argv[argc++] = g_strdup("--terminate");
1237  argv[argc++] = g_strdup_printf("%s", session_id);
1238  } else {
1239  REMMINA_PLUGIN_CRITICAL("%s", FEATURE_NOT_AVAIL_STR("TERMINATE"));
1240  return G_SOURCE_REMOVE;
1241  }
1242 
1243  if (FEATURE_AVAILABLE(gpdata, "NON_INTERACTIVE")) {
1244  argv[argc++] = g_strdup("--non-interactive");
1245  } else {
1246  REMMINA_PLUGIN_WARNING("%s", FEATURE_NOT_AVAIL_STR("NON_INTERACTIVE"));
1247  }
1248 
1249  if (FEATURE_AVAILABLE(gpdata, "SSH_PRIVKEY")) {
1250  if (ssh_privatekey && !g_str_equal(ssh_privatekey, "")) {
1251  argv[argc++] = g_strdup("--ssh-privkey");
1252  argv[argc++] = g_strdup_printf("%s", ssh_privatekey);
1253 
1254  if (ssh_passphrase && !g_str_equal(ssh_passphrase, "")) {
1255  if (FEATURE_AVAILABLE(gpdata, "SSH_PASSPHRASE")) {
1256  argv[argc++] = g_strdup("--ssh-passphrase");
1257  argv[argc++] = g_strdup_printf("%s", ssh_passphrase);
1258  } else {
1259  REMMINA_PLUGIN_MESSAGE("%s", FEATURE_NOT_AVAIL_STR("SSH_PASSPHRASE"));
1260  }
1261  }
1262  }
1263  } else {
1264  REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("SSH_PRIVKEY"));
1265  }
1266 
1267  argv[argc++] = NULL;
1268 
1269  GError* error = NULL;
1270  gchar** envp = g_get_environ();
1271  rmplugin_x2go_spawn_pyhoca_process(argc, argv, &error, envp);
1272  g_strfreev(envp);
1273 
1274  if (error) {
1275  gchar *err_msg = g_strdup_printf(
1276  _("Could not terminate X2Go session '%s':\n%s"),
1277  session_id,
1278  error->message
1279  );
1280 
1281  REMMINA_PLUGIN_CRITICAL("%s", err_msg);
1282 
1283  struct _DialogData *err_ddata = g_new0(struct _DialogData, 1);
1284  err_ddata->parent = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(dialog)));
1285  err_ddata->flags = GTK_DIALOG_MODAL;
1286  err_ddata->type = GTK_MESSAGE_ERROR;
1287  err_ddata->buttons = GTK_BUTTONS_OK;
1288  err_ddata->title = _("An error occured.");
1289  err_ddata->message = err_msg;
1290  // We don't need the response.
1291  err_ddata->callbackfunc = NULL;
1292  // We don't need a custom dialog either.
1293  err_ddata->dialog_factory_func = NULL;
1294  err_ddata->dialog_factory_data = NULL;
1295 
1296  /* Prepare X2GoCustomUserData *custom_data
1297  * gp -> gp (RemminaProtocolWidget*)
1298  * dialog_data -> dialog data (struct _DialogData*)
1299  */
1300  custom_data->gp = custom_data->gp;
1301  custom_data->dialog_data = err_ddata;
1302  custom_data->connect_data = NULL;
1303  custom_data->opt1 = NULL;
1304  custom_data->opt2 = NULL;
1305 
1306  IDLE_ADD((GSourceFunc) rmplugin_x2go_open_dialog, custom_data);
1307 
1308  // Too verbose:
1309  // GtkTreeModel *model = gtk_tree_model_filter_get_model(
1310  // GTK_TREE_MODEL_FILTER(filter));
1311  // gtk_tree_model_foreach(GTK_TREE_MODEL(model), (GtkTreeModelForeachFunc)
1312  // rmplugin_x2go_dump_session_properties, dialog);
1313 
1314  // Set row visible again since we could not terminate the session.
1315  if (!rmplugin_x2go_session_chooser_set_row_visible(selected_row, TRUE,
1316  dialog)) {
1317  // error message was already handled.
1318  return G_SOURCE_REMOVE;
1319  }
1320  }
1321 
1322  return G_SOURCE_REMOVE;
1323 }
1324 
1340  gint response_id,
1341  GtkDialog *self)
1342 {
1343  REMMINA_PLUGIN_DEBUG("Function entry.");
1344 
1345  if (!custom_data || !custom_data->gp || !custom_data->dialog_data ||
1346  !custom_data->connect_data) {
1347  REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
1348  _("Internal error: %s"),
1349  _("Parameter 'custom_data' is not initialized!")
1350  ));
1351 
1352  return G_SOURCE_REMOVE;
1353  }
1354  RemminaProtocolWidget *gp = (RemminaProtocolWidget*) custom_data->gp;
1355 
1356  // Don't need to run other stuff, if the user just wants a new session.
1357  // Also it can happen, that no session is there anymore which can be selected!
1358  if (response_id == SESSION_CHOOSER_RESPONSE_NEW) {
1359  REMMINA_PLUGIN_DEBUG("The user explicitly requested a new session. "
1360  "Creating a new session…");
1361  SET_RESUME_SESSION(gp, NULL);
1362 
1363  // Unstucking main process. Telling it that a session has been selected.
1364  // We use a trick here. As long as there is something other
1365  // than 0 stored, a session is selected. So we use the gpointer as a gboolean.
1366  SET_SESSION_SELECTED(gp, (gpointer) TRUE);
1367 
1368  gtk_widget_destroy(GTK_WIDGET(self));
1369 
1370  return G_SOURCE_REMOVE;
1371  }
1372 
1373  // This assumes that there are sessions which can be selected!
1375  GTK_WIDGET(self),
1377  NULL // Let the function search for the selected row.
1378  );
1379 
1380  // error message was handled already.
1381  if (!G_VALUE_HOLDS_STRING(&value)) return G_SOURCE_REMOVE;
1382 
1383  gchar *session_id = (gchar*) g_value_get_string(&value);
1384 
1385  if (response_id == SESSION_CHOOSER_RESPONSE_CHOOSE) {
1386  if (!session_id || strlen(session_id) <= 0) {
1387  REMMINA_PLUGIN_DEBUG(
1388  "%s",
1389  _("Could not get session ID from session chooser dialog.")
1390  );
1391  SET_RESUME_SESSION(gp, NULL);
1392  } else {
1393  SET_RESUME_SESSION(gp, session_id);
1394 
1395  REMMINA_PLUGIN_MESSAGE("%s", g_strdup_printf(
1396  _("Resuming session: '%s'"),
1397  session_id
1398  ));
1399  }
1400  } else if (response_id == SESSION_CHOOSER_RESPONSE_TERMINATE) {
1401  if (!session_id || strlen(session_id) <= 0) {
1402  REMMINA_PLUGIN_DEBUG(
1403  "%s",
1404  _("Could not get session ID from session chooser dialog.")
1405  );
1406  SET_RESUME_SESSION(gp, NULL);
1407  } else {
1408  SET_RESUME_SESSION(gp, session_id);
1409 
1410  REMMINA_PLUGIN_MESSAGE("%s", g_strdup_printf(
1411  _("Terminating session: '%s'"),
1412  session_id
1413  ));
1414  }
1415 
1417  GTK_WIDGET(self));
1418  // error message was already handled.
1419  if (!path) return G_SOURCE_REMOVE;
1420 
1421  // Actually set row invisible.
1422  if (!rmplugin_x2go_session_chooser_set_row_visible(path, FALSE, self)) {
1423  // error message was already handled.
1424  return G_SOURCE_REMOVE;
1425  }
1426 
1427  /* Prepare X2GoCustomUserData *custom_data
1428  * gp -> gp (RemminaProtocolWidget*)
1429  * dialog_data -> dialog data (struct _DialogData*)
1430  * connect_data -> connection data (struct _ConnectionData*)
1431  * opt1 -> selected row (GtkTreePath*)
1432  * opt2 -> session selection dialog (GtkDialog*)
1433  */
1434  // everything else is already initialized.
1435  custom_data->opt1 = path;
1436  custom_data->opt2 = self;
1437 
1438  // Actually start pyhoca-cli process with --terminate $session_id.
1439  g_thread_new("terminate-session-thread",
1441  custom_data);
1442 
1443  // Dialog should stay open.
1444  return G_SOURCE_CONTINUE;
1445  } else {
1446  REMMINA_PLUGIN_DEBUG("User clicked dialog away. "
1447  "Creating a new session then.");
1448  SET_RESUME_SESSION(gp, NULL);
1449  }
1450 
1451  // Unstucking main process. Telling it that a session has been selected.
1452  // We use a trick here. As long as there is something other
1453  // than 0 stored, a session is selected. So we use the gpointer as a gboolean.
1454  SET_SESSION_SELECTED(gp, (gpointer) TRUE);
1455 
1456  gtk_widget_destroy(GTK_WIDGET(self));
1457 
1458  return G_SOURCE_REMOVE;
1459 }
1460 
1461 #define RMPLUGIN_X2GO_FEATURE_GTKSOCKET 1
1462 
1463 /* Forward declaration */
1465 
1466 /* When more than one NX sessions is connecting in progress, we need this mutex and array
1467  * to prevent them from stealing the same window ID.
1468  */
1469 static pthread_mutex_t remmina_x2go_init_mutex;
1471 
1472 /* ------------- Support for execution on main thread of GTK functions ------------- */
1473 struct onMainThread_cb_data
1474 {
1475  enum { FUNC_GTK_SOCKET_ADD_ID } func;
1476 
1477  GtkSocket* sk;
1478  Window w;
1479 
1480  /* Mutex for thread synchronization */
1481  pthread_mutex_t mu;
1482  /* Flag to catch cancellations */
1483  gboolean cancelled;
1484 };
1485 
1486 static gboolean onMainThread_cb(struct onMainThread_cb_data *d)
1487 {
1488  TRACE_CALL(__func__);
1489  if (!d->cancelled) {
1490  switch (d->func) {
1491  case FUNC_GTK_SOCKET_ADD_ID:
1492  gtk_socket_add_id(d->sk, d->w);
1493  break;
1494  }
1495  pthread_mutex_unlock(&d->mu);
1496  } else {
1497  /* thread has been cancelled, so we must free d memory here */
1498  g_free(d);
1499  }
1500  return G_SOURCE_REMOVE;
1501 }
1502 
1503 
1504 static void onMainThread_cleanup_handler(gpointer data)
1505 {
1506  TRACE_CALL(__func__);
1507  struct onMainThread_cb_data *d = data;
1508  d->cancelled = TRUE;
1509 }
1510 
1512 {
1513  TRACE_CALL(__func__);
1514  d->cancelled = FALSE;
1515  pthread_cleanup_push(onMainThread_cleanup_handler, d);
1516  pthread_mutex_init(&d->mu, NULL);
1517  pthread_mutex_lock(&d->mu);
1518  gdk_threads_add_idle((GSourceFunc)onMainThread_cb, (gpointer) d);
1519 
1520  pthread_mutex_lock(&d->mu);
1521 
1522  pthread_cleanup_pop(0);
1523  pthread_mutex_unlock(&d->mu);
1524  pthread_mutex_destroy(&d->mu);
1525 }
1526 
1527 static void onMainThread_gtk_socket_add_id(GtkSocket* sk, Window w)
1528 {
1529  TRACE_CALL(__func__);
1530 
1531  struct onMainThread_cb_data *d;
1532 
1533  d = g_new0(struct onMainThread_cb_data, 1);
1535  d->sk = sk;
1536  d->w = w;
1537 
1539  g_free(d);
1540 }
1541 /* /-/-/-/-/-/-/ Support for execution on main thread of GTK functions /-/-/-/-/-/-/ */
1542 
1544 {
1545  gint i;
1546  gboolean already_seen = FALSE;
1547 
1548  pthread_mutex_lock(&remmina_x2go_init_mutex);
1549  for (i = 0; i < remmina_x2go_window_id_array->len; i++) {
1550  if (g_array_index(remmina_x2go_window_id_array, Window, i) == window_id) {
1551  already_seen = TRUE;
1552  REMMINA_PLUGIN_DEBUG("Window of X2Go Agent with ID [0x%lx] seen already.",
1553  window_id);
1554  break;
1555  }
1556  }
1557 
1558  if (already_seen) {
1559  g_array_remove_index_fast(remmina_x2go_window_id_array, i);
1560  REMMINA_PLUGIN_DEBUG("Forgetting about window of X2Go Agent with ID [0x%lx]…",
1561  window_id);
1562  }
1563 
1564  pthread_mutex_unlock(&remmina_x2go_init_mutex);
1565 }
1566 
1573 {
1574  REMMINA_PLUGIN_DEBUG("Function entry.");
1575 
1576  gchar *server;
1577  gint port;
1578 
1579  RemminaFile *remminafile = rm_plugin_service->protocol_plugin_get_file(gp);
1580  rm_plugin_service->get_server_port(rm_plugin_service->file_get_string(remminafile, "server"),
1581  22,
1582  &server,
1583  &port);
1584 
1585  REMMINA_PLUGIN_AUDIT(_("Disconnected from %s:%d via X2Go"), server, port);
1586  g_free(server), server = NULL;
1587 
1588  RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp);
1589  if (gpdata == NULL) {
1590  REMMINA_PLUGIN_DEBUG("Exiting since gpdata is already 'NULL'…");
1591  return G_SOURCE_REMOVE;
1592  }
1593 
1594  if (gpdata->thread) {
1595  pthread_cancel(gpdata->thread);
1596  if (gpdata->thread) pthread_join(gpdata->thread, NULL);
1597  }
1598 
1599  if (gpdata->window_id) {
1601  }
1602 
1603  if (gpdata->pidx2go) {
1604  kill(gpdata->pidx2go, SIGTERM);
1605  g_spawn_close_pid(gpdata->pidx2go);
1606  gpdata->pidx2go = 0;
1607  }
1608 
1609  if (gpdata->display) {
1610  XSetErrorHandler(gpdata->orig_handler);
1611  XCloseDisplay(gpdata->display);
1612  gpdata->display = NULL;
1613  }
1614 
1615  g_object_steal_data(G_OBJECT(gp), "plugin-data");
1616  rm_plugin_service->protocol_plugin_signal_connection_closed(gp);
1617 
1618  return G_SOURCE_REMOVE;
1619 }
1620 
1622 {
1623  TRACE_CALL(__func__);
1624  RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp);
1625 
1626  REMMINA_PLUGIN_DEBUG("Function entry.");
1627 
1628  if (gpdata->disconnected) {
1629  REMMINA_PLUGIN_DEBUG("Doing nothing since the plugin is already disconnected.");
1630  return G_SOURCE_REMOVE;
1631  }
1632 
1634 
1635  // Try again.
1636  return G_SOURCE_CONTINUE;
1637 }
1638 
1639 static void rmplugin_x2go_pyhoca_cli_exited(GPid pid,
1640  gint status,
1642 {
1643  REMMINA_PLUGIN_DEBUG("Function entry.");
1644 
1645  RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp);
1646  if (!gpdata) {
1647  REMMINA_PLUGIN_DEBUG("Doing nothing as the disconnection "
1648  "has already been handled.");
1649  return;
1650  }
1651 
1652  if (gpdata->pidx2go <= 0) {
1653  REMMINA_PLUGIN_DEBUG("Doing nothing since pyhoca-cli was expected to stop.");
1654  return;
1655  }
1656 
1657  REMMINA_PLUGIN_CRITICAL("%s", _("PyHoca-CLI exited unexpectedly. "
1658  "This connection will now be closed."));
1659 
1660  struct _DialogData *ddata = g_new0(struct _DialogData, 1);
1661  ddata->parent = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(gp)));
1662  ddata->flags = GTK_DIALOG_MODAL;
1663  ddata->type = GTK_MESSAGE_ERROR;
1664  ddata->buttons = GTK_BUTTONS_OK;
1665  ddata->title = _("An error occured.");
1666  ddata->message = _("The necessary child process 'pyhoca-cli' stopped unexpectedly.\n"
1667  "Please check your profile settings and PyHoca-CLI's output for "
1668  "possible errors. Also ensure the remote server is "
1669  "reachable and you're using the right credentials.");
1670  // We don't need the response.
1671  ddata->callbackfunc = NULL;
1672  // We don't need a custom dialog either.
1673  ddata->dialog_factory_func = NULL;
1674  ddata->dialog_factory_data = NULL;
1675 
1676  /* Prepare X2GoCustomUserData *custom_data
1677  * gp -> gp (RemminaProtocolWidget*)
1678  * dialog_data -> dialog data (struct _DialogData*)
1679  */
1680  X2GoCustomUserData *custom_data = g_new0(X2GoCustomUserData, 1);
1681  g_assert(custom_data && "custom_data could not be initialized.");
1682 
1683  custom_data->gp = gp;
1684  custom_data->dialog_data = ddata;
1685  custom_data->connect_data = NULL;
1686  custom_data->opt1 = NULL;
1687 
1688  IDLE_ADD((GSourceFunc) rmplugin_x2go_open_dialog, custom_data);
1689 
1690  // 1 Second. Give `Dialog` chance to open.
1691  usleep(1000 * 1000);
1692 
1694 }
1695 
1700 static gboolean rmplugin_x2go_save_credentials(RemminaFile* remminafile,
1701  gchar* s_username, gchar* s_password,
1702  gchar* errmsg)
1703 {
1704  // User has requested to save credentials. We put all the new credentials
1705  // into remminafile->settings. They will be saved later, on successful
1706  // connection, by rcw.c
1707  if (s_password && s_username) {
1708  if (g_strcmp0(s_username, "") == 0) {
1709  g_strlcpy(errmsg, _("Can't save empty username!"), 512);
1710  //REMMINA_PLUGIN_CRITICAL("%s", errmsg); // No need.
1711  return FALSE;
1712  }
1713 
1714  // We allow the possibility to set an empty password because a X2Go
1715  // session can be still made using keyfiles or similar.
1716  rm_plugin_service->file_set_string(remminafile, "password",
1717  s_password);
1718  rm_plugin_service->file_set_string(remminafile, "username",
1719  s_username);
1720  } else {
1721  g_strlcpy(errmsg, g_strdup_printf(
1722  _("Internal error: %s"),
1723  _("Could not save new credentials.")
1724  ), 512);
1725 
1726  REMMINA_PLUGIN_CRITICAL("%s", _("Could not save "
1727  "new credentials: 's_password' or "
1728  "'s_username' strings were not set."));
1729  return FALSE;
1730  }
1731 
1732  return TRUE;
1733 }
1734 
1735 
1744 static gboolean rmplugin_x2go_get_ssh_passphrase(RemminaProtocolWidget *gp, gchar *errmsg,
1745  gchar **passphrase)
1746 {
1747  REMMINA_PLUGIN_DEBUG("Function entry.");
1748 
1749  g_assert(errmsg != NULL);
1750  g_assert(gp != NULL);
1751 
1752  if ((*passphrase) == NULL) {
1753  // Just setting NULL password to empty password.
1754  (*passphrase) = g_strdup("");
1755  }
1756 
1757  gint ret = rm_plugin_service->protocol_plugin_init_auth(
1758  gp, 0, _("Enter password to unlock the SSH key:"),
1759  NULL, *passphrase, NULL, NULL
1760  );
1761 
1762  if (ret == GTK_RESPONSE_OK) {
1763  gchar *s_passphrase = rm_plugin_service->protocol_plugin_init_get_password(gp);
1764  if (s_passphrase) {
1765  (*passphrase) = g_strdup(s_passphrase);
1766  g_free(s_passphrase);
1767  }
1768  } else {
1769  g_strlcpy(errmsg, _("Password input cancelled. Aborting…"), 512);
1770  return FALSE;
1771  }
1772 
1773  return TRUE;
1774 }
1775 
1776 
1786 static gboolean rmplugin_x2go_get_auth(RemminaProtocolWidget *gp, gchar* errmsg,
1787  gchar** default_username, gchar** default_password)
1788 {
1789  REMMINA_PLUGIN_DEBUG("Function entry.");
1790 
1791  g_assert(errmsg != NULL);
1792  g_assert(gp != NULL);
1793  g_assert(default_username != NULL);
1794  g_assert(default_password != NULL);
1795 
1796  // default_username is probably NULL because the user didn't configure any
1797  // username in the profile settings.
1798  if ((*default_username) == NULL) {
1799  gchar* l_errmsg = g_strdup_printf(
1800  _("Tip: Check the 'Save password' checkbox or manually input your "
1801  "X2Go username and password in the profile settings to store "
1802  "them for faster logins.")
1803  );
1804  REMMINA_PLUGIN_MESSAGE("%s", l_errmsg);
1805  (*default_username) = g_strdup("");
1806  }
1807 
1808  // default_password is probably NULL because something did go wrong at the
1809  // secret-plugin. For example: The user didn't input a password for keyring or
1810  // the user simply didn't configure a password in the profile settings.
1811  if ((*default_password) == NULL) {
1812  (*default_password) = g_strdup("");
1813  }
1814 
1815  gchar *s_username, *s_password;
1816  gint ret;
1817  gboolean save;
1818  gboolean disable_password_storing;
1819  RemminaFile *remminafile;
1820 
1821  remminafile = rm_plugin_service->protocol_plugin_get_file(gp);
1822 
1823  disable_password_storing = rm_plugin_service->file_get_int(
1824  remminafile, "disablepasswordstoring", FALSE
1825  );
1826 
1827  ret = rm_plugin_service->protocol_plugin_init_auth(
1828  gp, (disable_password_storing ? 0 :
1831  _("Enter X2Go credentials"),
1832  (*default_username), (*default_password), NULL, NULL
1833  );
1834 
1835  if (ret == GTK_RESPONSE_OK) {
1836  s_username = rm_plugin_service->protocol_plugin_init_get_username(gp);
1837  s_password = rm_plugin_service->protocol_plugin_init_get_password(gp);
1838  if (rm_plugin_service->protocol_plugin_init_get_savepassword(gp))
1839  rm_plugin_service->file_set_string(
1840  remminafile, "password", s_password
1841  );
1842 
1843  // Should be renamed to protocol_plugin_init_get_savecredentials()?!
1844  save = rm_plugin_service->protocol_plugin_init_get_savepassword(gp);
1845  if (save) {
1846  if (!rmplugin_x2go_save_credentials(remminafile, s_username,
1847  s_password, errmsg)) {
1848  return FALSE;
1849  }
1850  }
1851  if (s_username) {
1852  (*default_username) = g_strdup(s_username);
1853  g_free(s_username);
1854  }
1855  if (s_password) {
1856  (*default_password) = g_strdup(s_password);
1857  g_free(s_password);
1858  }
1859  } else {
1860  g_strlcpy(errmsg, _("Authentication cancelled. Aborting…"), 512);
1861  return FALSE;
1862  }
1863 
1864  return TRUE;
1865 }
1866 
1879  struct _ConnectionData* connect_data)
1880 {
1881  REMMINA_PLUGIN_DEBUG("Function entry.");
1882  RemminaPluginX2GoData* gpdata = GET_PLUGIN_DATA(gp);
1883 
1884  gchar *host = NULL;
1885  gchar *username = NULL;
1886  gchar *password = NULL;
1887  gchar *ssh_privatekey = NULL;
1888  gchar *ssh_passphrase = NULL;
1889  gboolean valid = rmplugin_x2go_verify_connection_data(connect_data);
1890 
1891  if (valid) {
1892  if (connect_data->password) password = connect_data->password;
1893  if (connect_data->ssh_privatekey) {
1894  ssh_privatekey = connect_data->ssh_privatekey;
1895 
1896  if (connect_data->ssh_passphrase) {
1897  ssh_passphrase = connect_data->ssh_passphrase;
1898  }
1899  }
1900 
1901  host = connect_data->host;
1902  username = connect_data->username;
1903  } else {
1904  return G_SOURCE_REMOVE;
1905  }
1906 
1907  // We will now start pyhoca-cli with only the '--list-sessions' option.
1908 
1909  gchar *argv[50];
1910  gint argc = 0;
1911 
1912  argv[argc++] = g_strdup("pyhoca-cli");
1913  argv[argc++] = g_strdup("--list-sessions");
1914 
1915  argv[argc++] = g_strdup("--server"); // Not listed as feature.
1916  argv[argc++] = g_strdup_printf("%s", host);
1917 
1918  if (FEATURE_AVAILABLE(gpdata, "USERNAME")) {
1919  argv[argc++] = g_strdup("-u");
1920  if (username) {
1921  argv[argc++] = g_strdup_printf("%s", username);
1922  } else {
1923  argv[argc++] = g_strdup_printf("%s", g_get_user_name());
1924  }
1925  } else {
1926  g_set_error(error, 1, 1, "%s", FEATURE_NOT_AVAIL_STR("USERNAME"));
1927  REMMINA_PLUGIN_CRITICAL("%s", FEATURE_NOT_AVAIL_STR("USERNAME"));
1928  return NULL;
1929  }
1930 
1931  if (FEATURE_AVAILABLE(gpdata, "NON_INTERACTIVE")) {
1932  argv[argc++] = g_strdup("--non-interactive");
1933  } else {
1934  REMMINA_PLUGIN_WARNING("%s", FEATURE_NOT_AVAIL_STR("NON_INTERACTIVE"));
1935  }
1936 
1937  if (password && FEATURE_AVAILABLE(gpdata, "PASSWORD")) {
1938  if (FEATURE_AVAILABLE(gpdata, "AUTH_ATTEMPTS")) {
1939  argv[argc++] = g_strdup("--auth-attempts");
1940  argv[argc++] = g_strdup_printf ("%i", 0);
1941  } else {
1942  REMMINA_PLUGIN_WARNING("%s", FEATURE_NOT_AVAIL_STR("AUTH_ATTEMPTS"));
1943  }
1944  if (strlen(password) > 0) {
1945  argv[argc++] = g_strdup("--force-password");
1946  argv[argc++] = g_strdup("--password");
1947  argv[argc++] = g_strdup_printf("%s", password);
1948  }
1949  } else if (!password) {
1950  g_set_error(error, 1, 1, "%s", FEATURE_NOT_AVAIL_STR("PASSWORD"));
1951  REMMINA_PLUGIN_CRITICAL("%s", FEATURE_NOT_AVAIL_STR("PASSWORD"));
1952  return NULL;
1953  }
1954 
1955  // No need to catch feature-not-available error.
1956  // `--quiet` is not that important.
1957  if (FEATURE_AVAILABLE(gpdata, "QUIET")) {
1958  argv[argc++] = g_strdup("--quiet");
1959  }
1960 
1961  if (FEATURE_AVAILABLE(gpdata, "SSH_PRIVKEY")) {
1962  if (ssh_privatekey && !g_str_equal(ssh_privatekey, "")) {
1963  argv[argc++] = g_strdup("--ssh-privkey");
1964  argv[argc++] = g_strdup_printf("%s", ssh_privatekey);
1965 
1966  if (ssh_passphrase && !g_str_equal(ssh_passphrase, "")) {
1967  if (FEATURE_AVAILABLE(gpdata, "SSH_PASSPHRASE")) {
1968  argv[argc++] = g_strdup("--ssh-passphrase");
1969  argv[argc++] = g_strdup_printf("%s", ssh_passphrase);
1970  } else {
1971  REMMINA_PLUGIN_MESSAGE("%s", FEATURE_NOT_AVAIL_STR("SSH_PASSPHRASE"));
1972  }
1973  }
1974  }
1975  } else {
1976  REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("SSH_PRIVKEY"));
1977  }
1978 
1979 
1980  argv[argc++] = NULL;
1981 
1982  //#ifndef GLIB_AVAILABLE_IN_2_68
1983  gchar** envp = g_get_environ();
1984  gchar* envp_splitted = g_strjoinv(";", envp);
1985  envp_splitted = g_strconcat(envp_splitted, ";LANG=C", (void*) NULL);
1986  envp = g_strsplit(envp_splitted, ";", 0);
1987  /*
1988  * #else
1989  * // Only available after glib version 2.68.
1990  * // TODO: FIXME: NOT TESTED!
1991  * GStrvBuilder* builder = g_strv_builder_new();
1992  * g_strv_builder_add(builder, "LANG=C");
1993  * GStrv envp = g_strv_builder_end(builder);
1994  * #endif
1995  */
1996 
1997  gchar* std_out = rmplugin_x2go_spawn_pyhoca_process(argc, argv, error, envp);
1998  g_strfreev(envp);
1999 
2000  if (!std_out || *error) {
2001  // If no error is set but std_out is NULL
2002  // then something is not right at all.
2003  // Most likely the developer forgot to add an error message. Crash.
2004  g_assert((*error) != NULL);
2005  return NULL;
2006  }
2007 
2008  return std_out;
2009 }
2010 
2027  GError **error,
2028  struct _ConnectionData* connect_data)
2029 {
2030  REMMINA_PLUGIN_DEBUG("Function entry.");
2031 
2032  gchar *pyhoca_output = NULL;
2033 
2034  pyhoca_output = rmplugin_x2go_get_pyhoca_sessions(gp, error, connect_data);
2035  if (!pyhoca_output || *error) {
2036  // If no error is set but pyhoca_output is NULL
2037  // then something is not right at all.
2038  // Most likely the developer forgot to add an error message. Crash.
2039  g_assert((*error) != NULL);
2040 
2041  return NULL;
2042  }
2043 
2044  gchar **lines_list = g_strsplit(pyhoca_output, "\n", -1);
2045  // Assume at least two lines of output.
2046  if (lines_list == NULL || lines_list[0] == NULL || lines_list[1] == NULL) {
2047  g_set_error(error, 1, 1, "%s", _("Could not parse the output of PyHoca-CLI's "
2048  "--list-sessions option. Creating a new "
2049  "session now."));
2050  return NULL;
2051  }
2052 
2053  gboolean found_session = FALSE;
2054  GList* sessions = NULL;
2055  gchar** session = NULL;
2056 
2057  for (guint i = 0; lines_list[i] != NULL; i++) {
2058  gchar* current_line = lines_list[i];
2059 
2060  // TOO VERBOSE:
2061  //REMMINA_PLUGIN_DEBUG("pyhoca-cli: %s", current_line);
2062 
2063  // Hardcoded string "Session Name: " comes from python-x2go.
2064  if (!g_str_has_prefix(current_line, "Session Name: ") && !found_session) {
2065  // Doesn't begin with "Session Name: " and
2066  // the current line doesn't come after that either. Skipping.
2067  continue;
2068  }
2069 
2070  if (g_str_has_prefix(current_line, "Session Name: ")) {
2071  gchar* session_id = NULL;
2072  gchar** line_list = g_strsplit(current_line, ": ", 0);
2073 
2074  if (line_list == NULL ||
2075  line_list[0] == NULL ||
2076  line_list[1] == NULL ||
2077  strlen(line_list[0]) <= 0 ||
2078  strlen(line_list[1]) <= 0)
2079  {
2080  found_session = FALSE;
2081  continue;
2082  }
2083 
2084  session = malloc(sizeof(gchar*) * (SESSION_NUM_PROPERTIES+1));
2085  if (!session) {
2086  REMMINA_PLUGIN_CRITICAL("%s", _("Could not allocate "
2087  "enough memory!"));
2088  }
2089  session[SESSION_NUM_PROPERTIES] = NULL;
2090  sessions = g_list_append(sessions, session);
2091 
2092  session_id = line_list[1];
2093  session[SESSION_SESSION_ID] = session_id;
2094 
2095  REMMINA_PLUGIN_INFO("%s", g_strdup_printf(
2096  _("Found already existing X2Go session with ID: '%s'"),
2097  session[SESSION_SESSION_ID])
2098  );
2099 
2100  found_session = TRUE;
2101  continue;
2102  }
2103 
2104  if (!found_session) {
2105  continue;
2106  }
2107 
2108  if (g_strcmp0(current_line, "-------------") == 0) {
2109  continue;
2110  }
2111 
2112  gchar* value = NULL;
2113  gchar** line_list = g_strsplit(current_line, ": ", 0);
2114 
2115  if (line_list == NULL ||
2116  line_list[0] == NULL ||
2117  line_list[1] == NULL ||
2118  strlen(line_list[0]) <= 0 ||
2119  strlen(line_list[1]) <= 0)
2120  {
2121  // Probably the empty line at the end of every session.
2122  found_session = FALSE;
2123  continue;
2124  }
2125  value = line_list[1];
2126 
2127  if (g_str_has_prefix(current_line, "cookie: ")) {
2128  REMMINA_PLUGIN_DEBUG("cookie:\t'%s'", value);
2129  session[SESSION_COOKIE] = value;
2130  } else if (g_str_has_prefix(current_line, "agent PID: ")) {
2131  REMMINA_PLUGIN_DEBUG("agent PID:\t'%s'", value);
2132  session[SESSION_AGENT_PID] = value;
2133  } else if (g_str_has_prefix(current_line, "display: ")) {
2134  REMMINA_PLUGIN_DEBUG("display:\t'%s'", value);
2135  session[SESSION_DISPLAY] = value;
2136  } else if (g_str_has_prefix(current_line, "status: ")) {
2137  if (g_strcmp0(value, "S") == 0) {
2138  // TRANSLATORS: Please stick to X2GoClient's translation.
2139  value = _("Suspended");
2140  } else if (g_strcmp0(value, "R") == 0) {
2141  // TRANSLATORS: Please stick to X2GoClient's translation.
2142  value = _("Running");
2143  } else if (g_strcmp0(value, "T") == 0) {
2144  // TRANSLATORS: Please stick to X2GoClient's translation.
2145  value = _("Terminated");
2146  }
2147  REMMINA_PLUGIN_DEBUG("status:\t'%s'", value);
2148  session[SESSION_STATUS] = value;
2149  } else if (g_str_has_prefix(current_line, "graphic port: ")) {
2150  REMMINA_PLUGIN_DEBUG("graphic port:\t'%s'", value);
2151  session[SESSION_GRAPHIC_PORT] = value;
2152  } else if (g_str_has_prefix(current_line, "snd port: ")) {
2153  REMMINA_PLUGIN_DEBUG("snd port:\t'%s'", value);
2154  session[SESSION_SND_PORT] = value;
2155  } else if (g_str_has_prefix(current_line, "sshfs port: ")) {
2156  REMMINA_PLUGIN_DEBUG("sshfs port:\t'%s'", value);
2157  session[SESSION_SSHFS_PORT] = value;
2158  } else if (g_str_has_prefix(current_line, "username: ")) {
2159  REMMINA_PLUGIN_DEBUG("username:\t'%s'", value);
2160  session[SESSION_USERNAME] = value;
2161  } else if (g_str_has_prefix(current_line, "hostname: ")) {
2162  REMMINA_PLUGIN_DEBUG("hostname:\t'%s'", value);
2163  session[SESSION_HOSTNAME] = value;
2164  } else if (g_str_has_prefix(current_line, "create date: ")) {
2165  REMMINA_PLUGIN_DEBUG("create date:\t'%s'", value);
2166  session[SESSION_CREATE_DATE] = value;
2167  } else if (g_str_has_prefix(current_line, "suspended since: ")) {
2168  REMMINA_PLUGIN_DEBUG("suspended since:\t'%s'", value);
2169  session[SESSION_SUSPENDED_SINCE] = value;
2170  } else {
2171  REMMINA_PLUGIN_DEBUG("Not supported:\t'%s'", value);
2172  found_session = FALSE;
2173  }
2174  }
2175 
2176  if (!sessions) {
2177  g_set_error(error, 1, 1,
2178  "%s", _("Could not find any sessions on remote machine. Creating a new "
2179  "session now.")
2180  );
2181 
2182  // returning NULL with `error` set.
2183  }
2184 
2185  return sessions;
2186 }
2187 
2198 static gchar* rmplugin_x2go_ask_session(RemminaProtocolWidget *gp, GError **error,
2199  struct _ConnectionData* connect_data)
2200 {
2201  if (!connect_data ||
2202  !connect_data->host ||
2203  !connect_data->username ||
2204  !connect_data->password ||
2205  strlen(connect_data->host) <= 0 ||
2206  strlen(connect_data->username) <= 0)
2207  // Allow empty passwords. Maybe the user wants to connect via public key?
2208  {
2209  g_set_error(error, 1, 1, "%s", g_strdup_printf(
2210  _("Internal error: %s"),
2211  _("'Invalid connection data.'")
2212  ));
2213  return NULL;
2214  }
2215 
2216  GList *sessions_list = NULL;
2217  sessions_list = rmplugin_x2go_parse_pyhoca_sessions(gp, error, connect_data);
2218 
2219  if (!sessions_list || *error) {
2220  // If no error is set but sessions_list is NULL
2221  // then something is not right at all.
2222  // Most likely the developer forgot to add an error message. Crash.
2223  g_assert(*error != NULL);
2224  return NULL;
2225  }
2226 
2227  // Prep new DialogData struct.
2228  struct _DialogData *ddata = g_new0(struct _DialogData, 1);
2229  ddata->parent = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(gp)));
2230  ddata->flags = GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT;
2231  //ddata->type = GTK_MESSAGE_QUESTION;
2232  //ddata->buttons = GTK_BUTTONS_OK; // Doesn't get used in our custom factory.
2233  ddata->title = _("Choose a session to resume:");
2234  ddata->message = "";
2235 
2236  // gboolean factory(X2GoCustomUserData*, gpointer)
2237  // X2GoCustomUserData*:
2238  // gp -> gp (RemminaProtocolWidget*)
2239  // dialog_data -> dialog data (struct _DialogData*)
2240  // connect_data -> connection data (struct _ConnectionData*)
2241  // gpointer: dialog_factory_data
2243 
2244  // gboolean factory(X2GoCustomUserData*, gpointer)
2245  // X2GoCustomUserData*:
2246  // gp -> gp (RemminaProtocolWidget*)
2247  // dialog_data -> dialog data (struct _DialogData*)
2248  // connect_data -> connection data (struct _ConnectionData*)
2249  // gpointer: dialog_factory_data
2250  ddata->dialog_factory_data = sessions_list;
2252 
2253  /* Prepare X2GoCustomUserData *custom_data
2254  * gp -> gp (RemminaProtocolWidget*)
2255  * dialog_data -> dialog data (struct _DialogData*)
2256  */
2257  X2GoCustomUserData *custom_data = g_new0(X2GoCustomUserData, 1);
2258  g_assert(custom_data && "custom_data could not be initialized.");
2259 
2260  custom_data->gp = gp;
2261  custom_data->dialog_data = ddata;
2262  custom_data->connect_data = connect_data;
2263  custom_data->opt1 = NULL;
2264 
2265  // Open dialog here. Dialog rmplugin_x2go_session_chooser_callback (callbackfunc)
2266  // should set SET_RESUME_SESSION.
2267  IDLE_ADD((GSourceFunc)rmplugin_x2go_open_dialog, custom_data);
2268 
2269  guint counter = 0;
2270  while (!IS_SESSION_SELECTED(gp)) {
2271  // 0.5 Seconds. Give dialog chance to open.
2272  usleep(500 * 1000);
2273 
2274  // Every 5 seconds
2275  if (counter % 10 == 0 || counter == 0) {
2276  REMMINA_PLUGIN_MESSAGE("%s", _("Waiting for user to select a session…"));
2277  }
2278  counter++;
2279  }
2280 
2281  gchar* chosen_resume_session = GET_RESUME_SESSION(gp);
2282 
2283  if (!chosen_resume_session || strlen(chosen_resume_session) <= 0) {
2284  g_set_error(error, 1, 1, "%s", _("No session was selected. Creating a new one."));
2285  return NULL;
2286  }
2287 
2288  return chosen_resume_session;
2289 }
2290 
2291 static gboolean rmplugin_x2go_exec_x2go(gchar *host,
2292  gint sshport,
2293  gchar *username,
2294  gchar *password,
2295  gchar *command,
2296  gchar *kbdlayout,
2297  gchar *kbdtype,
2298  gchar *audio,
2299  gchar *clipboard,
2300  gint dpi,
2301  gchar *resolution,
2302  gchar *ssh_privatekey,
2304  gchar *errmsg)
2305 {
2306  TRACE_CALL(__func__);
2307  RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp);
2308 
2309  gchar *argv[50];
2310  gint argc = 0;
2311 
2312  // We don't want to save any SSH passphrases on hard drive!
2313  // Thats why we will always ask if needed.
2314  gchar *ssh_passphrase = NULL;
2315 
2316  if (!username || strlen(username) <= 0) {
2317  // Sets `username` and `password`.
2318  if (!rmplugin_x2go_get_auth(gp, errmsg, &username, &password)) {
2319  return FALSE;
2320  }
2321  }
2322 
2323  // Password can be *empty* but not NULL.
2324  if (!password) {
2325  password = g_strdup("");
2326  }
2327 
2328  if (ssh_privatekey && strlen(ssh_privatekey) > 0) {
2329  // FIXME: Check if file exists and is legit private key.
2330  // See: https://security.stackexchange.com/a/245767
2331 
2332  // Get ssh_privatekey now via dialog.
2333  if (!rmplugin_x2go_get_ssh_passphrase(gp, errmsg, &ssh_passphrase)) {
2334  return FALSE;
2335  }
2336  }
2337 
2338  struct _ConnectionData* connect_data = g_new0(struct _ConnectionData, 1);
2339  connect_data->host = host;
2340  connect_data->username = username;
2341  connect_data->password = password;
2342  connect_data->ssh_privatekey = ssh_privatekey;
2343  connect_data->ssh_passphrase = ssh_passphrase;
2344 
2345  GError *session_error = NULL;
2346  gchar* resume_session_id = rmplugin_x2go_ask_session(gp, &session_error,
2347  connect_data);
2348 
2349  if (!resume_session_id || session_error || strlen(resume_session_id) <= 0) {
2350  // If no error is set but session_id is NULL
2351  // then something is not right at all.
2352  // Most likely the developer forgot to add an error message. Crash.
2353  g_assert(session_error != NULL);
2354 
2355  REMMINA_PLUGIN_WARNING("%s", g_strdup_printf(
2356  _("A non-critical error happened: %s"),
2357  session_error->message
2358  ));
2359  } else {
2360  REMMINA_PLUGIN_INFO("%s", g_strdup_printf(
2361  _("User chose to resume session with ID: '%s'"),
2362  resume_session_id
2363  ));
2364  }
2365 
2366  argc = 0;
2367  argv[argc++] = g_strdup("pyhoca-cli");
2368 
2369  argv[argc++] = g_strdup("--server"); // Not listed as feature.
2370  argv[argc++] = g_strdup_printf ("%s", host);
2371 
2372  if (FEATURE_AVAILABLE(gpdata, "REMOTE_SSH_PORT")) {
2373  argv[argc++] = g_strdup("-p");
2374  argv[argc++] = g_strdup_printf ("%d", sshport);
2375  } else {
2376  REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("REMOTE_SSH_PORT"));
2377  }
2378 
2379  if (resume_session_id && strlen(resume_session_id) > 0) {
2380  REMMINA_PLUGIN_INFO("%s", g_strdup_printf(
2381  // TRANSLATORS: Please stick to X2GoClient's way of translating.
2382  _("Resuming session '%s'…"),
2383  resume_session_id
2384  ));
2385 
2386  if (FEATURE_AVAILABLE(gpdata, "RESUME")) {
2387  argv[argc++] = g_strdup("--resume");
2388  argv[argc++] = g_strdup_printf("%s", resume_session_id);
2389  } else {
2390  REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("RESUME"));
2391  }
2392  }
2393 
2394  // Deprecated. The user either wants to continue a
2395  // session or just not. No inbetween.
2396  // if (!resume_session_id) {
2397  // if (FEATURE_AVAILABLE(gpdata, "TRY_RESUME")) {
2398  // argv[argc++] = g_strdup("--try-resume");
2399  // } else {
2400  // REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("TRY_RESUME"));
2401  // }
2402  // }
2403 
2404  if (FEATURE_AVAILABLE(gpdata, "USERNAME")) {
2405  argv[argc++] = g_strdup("-u");
2406  if (username){
2407  argv[argc++] = g_strdup_printf ("%s", username);
2408  } else {
2409  argv[argc++] = g_strdup_printf ("%s", g_get_user_name());
2410  }
2411  } else {
2412  REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("USERNAME"));
2413  }
2414 
2415  if (password && FEATURE_AVAILABLE(gpdata, "PASSWORD")) {
2416  if (strlen(password) > 0) {
2417  argv[argc++] = g_strdup("--force-password");
2418  argv[argc++] = g_strdup("--password");
2419  argv[argc++] = g_strdup_printf ("%s", password);
2420  }
2421  } else {
2422  REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("PASSWORD"));
2423  }
2424 
2425  if (FEATURE_AVAILABLE(gpdata, "AUTH_ATTEMPTS")) {
2426  argv[argc++] = g_strdup("--auth-attempts");
2427  argv[argc++] = g_strdup_printf ("%i", 0);
2428  } else {
2429  REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("AUTH_ATTEMPTS"));
2430  }
2431 
2432  if (FEATURE_AVAILABLE(gpdata, "NON_INTERACTIVE")) {
2433  argv[argc++] = g_strdup("--non-interactive");
2434  } else {
2435  REMMINA_PLUGIN_WARNING("%s", FEATURE_NOT_AVAIL_STR("NON_INTERACTIVE"));
2436  }
2437 
2438  if (FEATURE_AVAILABLE(gpdata, "COMMAND")) {
2439  argv[argc++] = g_strdup("-c");
2440  // FIXME: pyhoca-cli is picky about multiple quotes around
2441  // the command string...
2442  // argv[argc++] = g_strdup_printf ("%s", g_shell_quote(command));
2443  argv[argc++] = g_strdup(command);
2444  } else {
2445  REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("COMMAND"));
2446  }
2447 
2448  if (FEATURE_AVAILABLE(gpdata, "KBD_LAYOUT")) {
2449  if (kbdlayout) {
2450  argv[argc++] = g_strdup("--kbd-layout");
2451  argv[argc++] = g_strdup_printf ("%s", kbdlayout);
2452  } else {
2453  argv[argc++] = g_strdup("--kbd-layout");
2454  argv[argc++] = g_strdup("auto");
2455  }
2456  } else {
2457  REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("KBD_LAYOUT"));
2458  }
2459 
2460  if (FEATURE_AVAILABLE(gpdata, "KBD_TYPE")) {
2461  if (kbdtype) {
2462  argv[argc++] = g_strdup("--kbd-type");
2463  argv[argc++] = g_strdup_printf ("%s", kbdtype);
2464  } else {
2465  argv[argc++] = g_strdup("--kbd-type");
2466  argv[argc++] = g_strdup("auto");
2467  }
2468  } else {
2469  REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("KBD_TYPE"));
2470  }
2471 
2472  if (FEATURE_AVAILABLE(gpdata, "GEOMETRY")) {
2473  if (!resolution)
2474  resolution = "800x600";
2475  argv[argc++] = g_strdup("-g");
2476  argv[argc++] = g_strdup_printf ("%s", resolution);
2477  } else {
2478  REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("GEOMETRY"));
2479  }
2480 
2481  if (FEATURE_AVAILABLE(gpdata, "TERMINATE_ON_CTRL_C")) {
2482  argv[argc++] = g_strdup("--terminate-on-ctrl-c");
2483  } else {
2484  REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("TERMINATE_ON_CTRL_C"));
2485  }
2486 
2487  if (FEATURE_AVAILABLE(gpdata, "SOUND")) {
2488  if (audio) {
2489  argv[argc++] = g_strdup("--sound");
2490  argv[argc++] = g_strdup_printf ("%s", audio);
2491  } else {
2492  argv[argc++] = g_strdup("--sound");
2493  argv[argc++] = g_strdup("none");
2494  }
2495  } else {
2496  REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("SOUND"));
2497  }
2498 
2499  if (FEATURE_AVAILABLE(gpdata, "CLIPBOARD_MODE")) {
2500  if (clipboard) {
2501  argv[argc++] = g_strdup("--clipboard-mode");
2502  argv[argc++] = g_strdup_printf("%s", clipboard);
2503  }
2504  } else {
2505  REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("CLIPBOARD_MODE"));
2506  }
2507 
2508  if (FEATURE_AVAILABLE(gpdata, "DPI")) {
2509  // Even though we validate the users input in the Remmina Editor,
2510  // manipulating profile files is still very possible…
2511  // Values are extracted from pyhoca-cli.
2512  if (dpi < 20 || dpi > 400) {
2513  g_strlcpy(errmsg, _("DPI setting is out of bounds. Please adjust "
2514  "it in profile settings."), 512);
2515  // No need, start_session() will handle output.
2516  //REMMINA_PLUGIN_CRITICAL("%s", errmsg);
2517  return FALSE;
2518  }
2519  argv[argc++] = g_strdup("--dpi");
2520  argv[argc++] = g_strdup_printf ("%i", dpi);
2521  } else {
2522  REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("DPI"));
2523  }
2524 
2525  if (FEATURE_AVAILABLE(gpdata, "SSH_PRIVKEY")) {
2526  if (ssh_privatekey && !g_str_equal(ssh_privatekey, "")) {
2527  argv[argc++] = g_strdup("--ssh-privkey");
2528  argv[argc++] = g_strdup_printf("%s", ssh_privatekey);
2529 
2530  if (ssh_passphrase && !g_str_equal(ssh_passphrase, "")) {
2531  if (FEATURE_AVAILABLE(gpdata, "SSH_PASSPHRASE")) {
2532  argv[argc++] = g_strdup("--ssh-passphrase");
2533  argv[argc++] = g_strdup_printf("%s", ssh_passphrase);
2534  } else {
2535  REMMINA_PLUGIN_MESSAGE("%s", FEATURE_NOT_AVAIL_STR("SSH_PASSPHRASE"));
2536  }
2537  }
2538  }
2539  } else {
2540  REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("SSH_PRIVKEY"));
2541  }
2542 
2543  argv[argc++] = NULL;
2544 
2545  GError *error = NULL;
2546  gchar **envp = g_get_environ();
2547  gboolean success = g_spawn_async_with_pipes (NULL, argv, envp,
2548  (G_SPAWN_DO_NOT_REAP_CHILD |
2549  G_SPAWN_SEARCH_PATH), NULL,
2550  NULL, &gpdata->pidx2go,
2551  NULL, NULL, NULL, &error);
2552 
2553  REMMINA_PLUGIN_INFO("%s", _("Started PyHoca-CLI with the following arguments:"));
2554  // Print every argument except passwords. Free all arg strings.
2555  for (gint i = 0; i < argc - 1; i++) {
2556  gchar* curr_arg = argv[i];
2557 
2558  if (g_str_equal(curr_arg, "--password") ||
2559  g_str_equal(curr_arg, "--ssh-passphrase")) {
2560  g_printf("%s ", curr_arg);
2561  g_printf("XXXXXX ");
2562  g_free(curr_arg);
2563  g_free(argv[++i]);
2564  continue;
2565  } else {
2566  g_printf("%s ", curr_arg);
2567  g_free(curr_arg);
2568  }
2569  }
2570  g_printf("\n");
2571 
2572  if (!success || error) {
2573  // TRANSLATORS: Meta-error. Shouldn't be visible.
2574  if (!error) error = g_error_new(0, 0, _("Internal error."));
2575 
2576  gchar *error_title = _("Could not start X2Go session…");
2577 
2578  struct _DialogData* ddata = g_new0(struct _DialogData, 1);
2579  ddata->parent = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(gp)));
2580  ddata->flags = GTK_DIALOG_MODAL;
2581  ddata->type = GTK_MESSAGE_ERROR;
2582  ddata->buttons = GTK_BUTTONS_OK;
2583  ddata->title = _("Could not start X2Go session.");
2584  ddata->message = g_strdup_printf(_("Could not start PyHoca-CLI (%i): '%s'"),
2585  error->code,
2586  error->message);
2587  // We don't need the response.
2588  ddata->callbackfunc = NULL;
2589  // We don't need a custom dialog either.
2590  ddata->dialog_factory_func = NULL;
2591  ddata->dialog_factory_data = NULL;
2592 
2593  /* Prepare X2GoCustomUserData *custom_data
2594  * gp -> gp (RemminaProtocolWidget*)
2595  * dialog_data -> dialog data (struct _DialogData*)
2596  */
2597  X2GoCustomUserData *custom_data = g_new0(X2GoCustomUserData, 1);
2598  g_assert(custom_data && "Could not initialise Custom_data.");
2599 
2600  custom_data->gp = gp;
2601  custom_data->dialog_data = ddata;
2602  custom_data->connect_data = NULL;
2603  custom_data->opt1 = NULL;
2604 
2605  IDLE_ADD((GSourceFunc) rmplugin_x2go_open_dialog, custom_data);
2606 
2607  g_strlcpy(errmsg, error_title, 512);
2608 
2609  // No need to output here. rmplugin_x2go_start_session will do this.
2610 
2611  g_error_free(error);
2612 
2613  return FALSE;
2614  }
2615 
2616  // Prevent a race condition where pyhoca-cli is not
2617  // started yet (pidx2go == 0) but a watcher is added.
2618 
2619  struct timespec ts;
2620  // 0.001 seconds.
2621  ts.tv_nsec = 1 * 1000 * 1000;
2622  ts.tv_sec = 0;
2623  while (gpdata->pidx2go == 0) {
2624  nanosleep(&ts, NULL);
2625  REMMINA_PLUGIN_DEBUG("Waiting for PyHoca-CLI to start…");
2626  };
2627 
2628  REMMINA_PLUGIN_DEBUG("Watching child 'pyhoca-cli' process now…");
2629  g_child_watch_add(gpdata->pidx2go,
2630  (GChildWatchFunc) rmplugin_x2go_pyhoca_cli_exited,
2631  gp);
2632 
2633  return TRUE;
2634 }
2635 
2640 {
2641  REMMINA_PLUGIN_DEBUG("Function entry.");
2642 
2643  #define AMOUNT_FEATURES 43
2644  gchar* features[AMOUNT_FEATURES] = {
2645  "ADD_TO_KNOWN_HOSTS", "AUTH_ATTEMPTS", "BROKER_PASSWORD", "BROKER_URL",
2646  "CLEAN_SESSIONS", "COMMAND", "DEBUG", "FORCE_PASSWORD", "FORWARD_SSHAGENT",
2647  "GEOMETRY", "KBD_LAYOUT", "KBD_TYPE", "LIBDEBUG", "LIBDEBUG_SFTPXFER", "LINK",
2648  "LIST_CLIENT_FEATURES", "LIST_DESKTOPS", "LIST_SESSIONS", "NEW", "PACK",
2649  "PASSWORD", "PDFVIEW_CMD", "PRINTER", "PRINTING", "PRINT_ACTION", "PRINT_CMD",
2650  "QUIET", "REMOTE_SSH_PORT", "RESUME", "SAVE_TO_FOLDER", "SESSION_PROFILE",
2651  "SESSION_TYPE", "SHARE_DESKTOP", "SHARE_LOCAL_FOLDERS", "SHARE_MODE", "SOUND",
2652  "SSH_PRIVKEY", "SUSPEND", "TERMINATE", "TERMINATE_ON_CTRL_C", "TRY_RESUME",
2653  "USERNAME", "XINERAMA"
2654  };
2655 
2656  GList *features_list = NULL;
2657  for (int i = 0; i < AMOUNT_FEATURES; i++) {
2658  features_list = g_list_append(features_list, features[i]);
2659  }
2660 
2661  return features_list;
2662 }
2663 
2668 {
2669  REMMINA_PLUGIN_DEBUG("Function entry.");
2670 
2671  GList* returning_glist = NULL;
2672 
2673  // We will now start pyhoca-cli with only the '--list-cmdline-features' option
2674  // and depending on the exit code and standard output we will determine if some
2675  // features are available or not.
2676 
2677  gchar* argv[50];
2678  gint argc = 0;
2679 
2680  argv[argc++] = g_strdup("pyhoca-cli");
2681  argv[argc++] = g_strdup("--list-cmdline-features");
2682  argv[argc++] = NULL;
2683 
2684  GError* error = NULL; // Won't be actually used.
2685 
2686  // Querying pyhoca-cli's command line features.
2687  gchar** envp = g_get_environ();
2688  gchar* features_string = rmplugin_x2go_spawn_pyhoca_process(argc, argv,
2689  &error, envp);
2690  g_strfreev(envp);
2691 
2692  if (!features_string || error) {
2693  // We added the '--list-cmdline-features' on commit 17d1be1319ba6 of
2694  // pyhoca-cli. In order to protect setups which don't have the newest
2695  // version of pyhoca-cli available yet we artificially create a list
2696  // of an old limited set of features.
2697 
2698  REMMINA_PLUGIN_WARNING("%s",
2699  _("Could not get PyHoca-CLI's command-line features. This "
2700  "indicates it is either too old, or not installed. "
2701  "An old limited set of features will be used for now."));
2702 
2704  } else {
2705  gchar **features_list = g_strsplit(features_string, "\n", 0);
2706 
2707  if (features_list == NULL) {
2708  gchar *error_msg = _("Could not parse PyHoca-CLI's command-line "
2709  "features. Using a limited feature-set for now.");
2710  REMMINA_PLUGIN_WARNING("%s", error_msg);
2712  }
2713 
2714  REMMINA_PLUGIN_INFO("%s", _("Retrieved the following PyHoca-CLI "
2715  "command-line features:"));
2716 
2717  for(int k = 0; features_list[k] != NULL; k++) {
2718  // Filter out empty strings
2719  if (strlen(features_list[k]) <= 0) continue;
2720 
2721  REMMINA_PLUGIN_INFO("%s",
2722  g_strdup_printf(_("Available feature[%i]: '%s'"),
2723  k+1, features_list[k]));
2724  returning_glist = g_list_append(returning_glist, features_list[k]);
2725  }
2726  return returning_glist;
2727  }
2728 }
2729 
2731 {
2732  TRACE_CALL(__func__);
2733 
2734  gchar *server;
2735  gint port;
2736 
2737  RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp);
2738  REMMINA_PLUGIN_DEBUG("Socket %d", gpdata->socket_id);
2739  rm_plugin_service->protocol_plugin_signal_connection_opened(gp);
2740 
2741  RemminaFile *remminafile = rm_plugin_service->protocol_plugin_get_file(gp);
2742  rm_plugin_service->get_server_port(rm_plugin_service->file_get_string(remminafile, "server"),
2743  22,
2744  &server,
2745  &port);
2746 
2747  REMMINA_PLUGIN_AUDIT(_("Connected to %s:%d via X2Go"), server, port);
2748  g_free(server), server = NULL;
2749 
2750  return;
2751 }
2752 
2754 {
2755  TRACE_CALL(__func__);
2756  REMMINA_PLUGIN_DEBUG("Function entry.");
2758  return G_SOURCE_CONTINUE;
2759 }
2760 
2762 {
2763  TRACE_CALL(__func__);
2764  REMMINA_PLUGIN_DEBUG("Function entry.", PLUGIN_NAME);
2765  RemminaPluginX2GoData *gpdata;
2766 
2767  gpdata = g_new0(RemminaPluginX2GoData, 1);
2768  g_object_set_data_full(G_OBJECT(gp), "plugin-data", gpdata, g_free);
2769 
2770  if (!rm_plugin_service->gtksocket_available()) {
2771  /* report this in open_connection, not reportable here... */
2772  return;
2773  }
2774 
2776 
2777  // available_features can't be NULL cause if it fails, it gets populated with an
2778  // old standard feature set.
2780 
2781  gpdata->socket_id = 0;
2782  gpdata->thread = 0;
2783 
2784  gpdata->display = NULL;
2785  gpdata->window_id = 0;
2786  gpdata->pidx2go = 0;
2787  gpdata->orig_handler = NULL;
2788 
2789  gpdata->socket = gtk_socket_new();
2790  rm_plugin_service->protocol_plugin_register_hostkey(gp, gpdata->socket);
2791  gtk_widget_show(gpdata->socket);
2792 
2793  g_signal_connect(G_OBJECT(gpdata->socket), "plug-added",
2794  G_CALLBACK(rmplugin_x2go_on_plug_added), gp);
2795  g_signal_connect(G_OBJECT(gpdata->socket), "plug-removed",
2796  G_CALLBACK(rmplugin_x2go_on_plug_removed), gp);
2797  gtk_container_add(GTK_CONTAINER(gp), gpdata->socket);
2798 }
2799 
2801 {
2802  TRACE_CALL(__func__);
2803  gint i;
2804  gboolean already_seen = FALSE;
2805 
2806  REMMINA_PLUGIN_DEBUG("Check if the window of X2Go Agent with ID [0x%lx] is already known or if "
2807  "it needs registration", window_id);
2808 
2809  pthread_mutex_lock(&remmina_x2go_init_mutex);
2810  for (i = 0; i < remmina_x2go_window_id_array->len; i++) {
2811  if (g_array_index(remmina_x2go_window_id_array, Window, i) == window_id) {
2812  already_seen = TRUE;
2813  REMMINA_PLUGIN_DEBUG("Window of X2Go Agent with ID [0x%lx] "
2814  "already seen.", window_id);
2815  break;
2816  }
2817  }
2818  if (!already_seen) {
2819  g_array_append_val(remmina_x2go_window_id_array, window_id);
2820  REMMINA_PLUGIN_DEBUG("Registered new window for X2Go Agent with "
2821  "ID [0x%lx].", window_id);
2822  }
2823  pthread_mutex_unlock(&remmina_x2go_init_mutex);
2824 
2825  return (!already_seen);
2826 }
2827 
2828 static int rmplugin_x2go_dummy_handler(Display *dsp, XErrorEvent *err)
2829 {
2830  TRACE_CALL(__func__);
2831  return 0;
2832 }
2833 
2835 {
2836  TRACE_CALL(__func__);
2837  RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp);
2838 
2839  gpdata->display = XOpenDisplay(gdk_display_get_name(gdk_display_get_default()));
2840  if (gpdata->display == NULL) {
2841  g_strlcpy(errmsg, _("Could not open X11 DISPLAY."), 512);
2842  return FALSE;
2843  }
2844 
2845  gpdata->orig_handler = XSetErrorHandler(rmplugin_x2go_dummy_handler);
2846 
2847  XSelectInput(gpdata->display,
2848  XDefaultRootWindow(gpdata->display),
2849  SubstructureNotifyMask);
2850 
2851  REMMINA_PLUGIN_DEBUG("X11 event-watcher created.");
2852 
2853  return TRUE;
2854 }
2855 
2857  const gchar *cmd,
2858  gchar *errmsg)
2859 {
2860  TRACE_CALL(__func__);
2861  RemminaPluginX2GoData *gpdata;
2862 
2863  gboolean agent_window_found = FALSE;
2864  Atom atom;
2865  XEvent xev;
2866  Window w;
2867  Atom type;
2868  int format;
2869  unsigned long nitems, rest;
2870  unsigned char *data = NULL;
2871 
2872  guint16 non_createnotify_count = 0;
2873 
2874  struct timespec ts;
2875  // wait_amount * ts.tv_nsec = 20s
2876  // 100 * 0.2s = 20s
2877  int wait_amount = 100;
2878 
2879  CANCEL_DEFER
2880 
2881  REMMINA_PLUGIN_DEBUG("%s", _("Waiting for window of X2Go Agent to appear…"));
2882 
2883  gpdata = GET_PLUGIN_DATA(gp);
2884  atom = XInternAtom(gpdata->display, "WM_COMMAND", True);
2885  if (atom == None) {
2886  CANCEL_ASYNC
2887  return FALSE;
2888  }
2889 
2890  ts.tv_sec = 0;
2891  // 0.2s = 200000000ns
2892  ts.tv_nsec = 200000000;
2893 
2894  while (wait_amount > 0) {
2895  pthread_testcancel();
2896  if (!(gpdata->pidx2go > 0)) {
2897  nanosleep(&ts, NULL);
2898  REMMINA_PLUGIN_DEBUG("Waiting for X2Go session to start…");
2899  continue;
2900  }
2901 
2902  while (!XPending(gpdata->display)) {
2903  nanosleep(&ts, NULL);
2904  wait_amount--;
2905  // Don't spam the console. Print every second though.
2906  if (wait_amount % 5 == 0) {
2907  REMMINA_PLUGIN_INFO("%s", _("Waiting for PyHoca-CLI to "
2908  "show the session's window…"));
2909  }
2910  continue;
2911  }
2912 
2913  XNextEvent(gpdata->display, &xev);
2914  // Just ignore non CreatNotify events.
2915  if (xev.type != CreateNotify) {
2916  non_createnotify_count++;
2917  if (non_createnotify_count % 5 == 0) {
2918  REMMINA_PLUGIN_DEBUG("Saw '%i' X11 events, which weren't "
2919  "CreateNotify.", non_createnotify_count);
2920  }
2921  continue;
2922  }
2923 
2924  w = xev.xcreatewindow.window;
2925  if (XGetWindowProperty(gpdata->display, w, atom, 0, 255, False,
2926  AnyPropertyType, &type, &format, &nitems, &rest,
2927  &data) != Success) {
2928  REMMINA_PLUGIN_DEBUG("Could not get WM_COMMAND property from X11 "
2929  "window ID [0x%lx].", w);
2930  continue;
2931  }
2932 
2933  if (data) {
2934  REMMINA_PLUGIN_DEBUG("Saw '%i' X11 events, which weren't "
2935  "CreateNotify.", non_createnotify_count);
2936  REMMINA_PLUGIN_DEBUG("Found X11 window with WM_COMMAND set "
2937  "to '%s', the window ID is [0x%lx].",
2938  (char*)data, w);
2939  }
2940  if (data && g_strrstr((gchar*)data, cmd) &&
2942  gpdata->window_id = w;
2943  agent_window_found = TRUE;
2944  XFree(data);
2945  break;
2946  }
2947  if (data)
2948  XFree(data);
2949  }
2950 
2951  XSetErrorHandler(gpdata->orig_handler);
2952  XCloseDisplay(gpdata->display);
2953  gpdata->display = NULL;
2954 
2955  CANCEL_ASYNC
2956 
2957  if (!agent_window_found) {
2958  g_strlcpy(errmsg, _("No X2Go session window appeared. "
2959  "Something went wrong…"), 512);
2960  return FALSE;
2961  }
2962 
2963  return TRUE;
2964 }
2965 
2967 {
2968  TRACE_CALL(__func__);
2969  REMMINA_PLUGIN_DEBUG("Function entry.");
2970 
2971  RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp);;
2972  RemminaFile *remminafile;
2973  const gchar errmsg[512] = {0};
2974  gboolean ret = TRUE;
2975 
2976  gchar *servstr, *host, *username, *password, *command, *kbdlayout, *kbdtype,
2977  *audio, *clipboard, *res, *ssh_privatekey;
2978  gint sshport, dpi;
2979  GdkDisplay *default_dsp;
2980  gint width, height;
2981 
2982  // We save the X Display name (:0) as we will need to synchronize the clipboards
2983  default_dsp = gdk_display_get_default();
2984  const gchar *default_dsp_name = gdk_display_get_name(default_dsp);
2985  REMMINA_PLUGIN_DEBUG("Default display is '%s'.", default_dsp_name);
2986 
2987  remminafile = rm_plugin_service->protocol_plugin_get_file(gp);
2988 
2989  servstr = GET_PLUGIN_STRING("server");
2990  if (servstr) {
2991  rm_plugin_service->get_server_port(servstr, 22, &host, &sshport);
2992  } else {
2993  return FALSE;
2994  }
2995 
2996  if (!sshport) sshport=22;
2997 
2998  username = GET_PLUGIN_STRING("username");
2999  password = GET_PLUGIN_PASSWORD("password");
3000 
3001  command = GET_PLUGIN_STRING("command");
3002  if (!command) command = "TERMINAL";
3003 
3004  kbdlayout = GET_PLUGIN_STRING("kbdlayout");
3005  kbdtype = GET_PLUGIN_STRING("kbdtype");
3006 
3007  audio = GET_PLUGIN_STRING("audio");
3008 
3009  clipboard = GET_PLUGIN_STRING("clipboard");
3010 
3011  dpi = GET_PLUGIN_INT("dpi", 80);
3012 
3013  ssh_privatekey = GET_PLUGIN_STRING("ssh_privatekey");
3014 
3015  // If empty set to NULL
3016  if(ssh_privatekey && g_str_equal(ssh_privatekey, "")) {
3017  ssh_privatekey = NULL;
3018  }
3019 
3020  width = rm_plugin_service->get_profile_remote_width(gp);
3021  height = rm_plugin_service->get_profile_remote_height(gp);
3022  /* multiple of 4 */
3023  width = (width + 3) & ~0x3;
3024  height = (height + 3) & ~0x3;
3025  if ((width > 0) && (height > 0)) {
3026  res = g_strdup_printf ("%dx%d", width, height);
3027  } else {
3028  res = "800x600";
3029  }
3030  REMMINA_PLUGIN_DEBUG("Resolution set by user: '%s'.", res);
3031 
3032  REMMINA_PLUGIN_DEBUG("Attached window to socket '%d'.", gpdata->socket_id);
3033 
3034  /* register for notifications of window creation events */
3035  if (ret) ret = rmplugin_x2go_start_create_notify(gp, (gchar*)&errmsg);
3036 
3037  /* trigger the session start, session window should appear soon after this */
3038  if (ret) ret = rmplugin_x2go_exec_x2go(host, sshport, username, password, command,
3039  kbdlayout, kbdtype, audio, clipboard, dpi,
3040  res, ssh_privatekey, gp,
3041  (gchar*)&errmsg);
3042 
3043  /* get the window ID of the remote x2goagent */
3044  if (ret) ret = rmplugin_x2go_monitor_create_notify(gp, "x2goagent",
3045  (gchar*)&errmsg);
3046 
3047  if (!ret) {
3048  REMMINA_PLUGIN_CRITICAL("%s", errmsg);
3049  rm_plugin_service->protocol_plugin_set_error(gp, "%s", &errmsg);
3050  return FALSE;
3051  }
3052 
3053  /* embed it */
3054  onMainThread_gtk_socket_add_id(GTK_SOCKET(gpdata->socket), gpdata->window_id);
3055 
3056  return TRUE;
3057 }
3058 
3060 {
3061  TRACE_CALL(__func__);
3062  RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp);
3063  gboolean ret = FALSE;
3064 
3065  ret = rmplugin_x2go_start_session(gp);
3066 
3067  gpdata->thread = 0;
3068  return ret;
3069 }
3070 
3072 {
3073  TRACE_CALL(__func__);
3074  if (!gp) {
3075  REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
3076  _("Internal error: %s"),
3077  _("RemminaProtocolWidget* gp is 'NULL'!")
3078  ));
3079  return NULL;
3080  }
3081 
3082  pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
3083 
3084  CANCEL_ASYNC
3085  if (!rmplugin_x2go_main(gp)) {
3086  IDLE_ADD((GSourceFunc) rmplugin_x2go_cleanup, gp);
3087  }
3088 
3089  return NULL;
3090 }
3091 
3093 {
3094  TRACE_CALL(__func__);
3095  RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp);
3096 
3097  if (!rm_plugin_service->gtksocket_available()) {
3098  rm_plugin_service->protocol_plugin_set_error(gp, _("The %s protocol is "
3099  "unavailable because GtkSocket only works under X.org"),
3100  PLUGIN_NAME);
3101  return FALSE;
3102  }
3103 
3104  gpdata->socket_id = gtk_socket_get_id(GTK_SOCKET(gpdata->socket));
3105  // casting to void* is allowed since return type 'gpointer' is actually void*.
3106  if (pthread_create(&gpdata->thread, NULL, (void*) rmplugin_x2go_main_thread, gp)) {
3107  rm_plugin_service->protocol_plugin_set_error(gp, _("Could not initialize "
3108  "pthread. Falling back to non-threaded mode…"));
3109  gpdata->thread = 0;
3110  return FALSE;
3111  } else {
3112  return TRUE;
3113  }
3114 }
3115 
3117  const RemminaProtocolFeature* feature)
3118 {
3119  TRACE_CALL(__func__);
3120  return TRUE;
3121 }
3122 
3124  {REMMINA_PROTOCOL_FEATURE_TYPE_GTKSOCKET, RMPLUGIN_X2GO_FEATURE_GTKSOCKET, NULL, NULL, NULL},
3125  {REMMINA_PROTOCOL_FEATURE_TYPE_END, 0, NULL, NULL, NULL}
3126 };
3127 
3128 
3139 static gchar* rmplugin_x2go_enumeration_prettifier(const guint max_elements,
3140  const guint current_element,
3141  gchar* element_to_add,
3142  gchar* string)
3143 {
3144  if (max_elements > 2) {
3145  if (current_element == max_elements - 1) {
3146  // TRANSLATORS: Presumably you just want to translate 'and' into
3147  // your language.
3148  // (Except your listing-grammar differs from english.)
3149  // 'value1', 'value2', 'valueN-1' and 'valueN'
3150  return g_strdup_printf(_("%sand '%s'"), string, element_to_add);
3151  } else if (current_element == max_elements - 2) {
3152  // TRANSLATORS: Presumably you just want to leave it english.
3153  // (Except your listing-grammar differs from english.)
3154  // 'value1', 'value2', 'valueN-1' and 'valueN'
3155  return g_strdup_printf(_("%s'%s' "), string, element_to_add);
3156  } else {
3157  // TRANSLATORS: Presumably you just want to leave it english.
3158  // (Except your listing-grammar differs from english.)
3159  // 'value1', 'value2', 'valueN-1' and 'valueN'
3160  return g_strdup_printf(_("%s'%s', "), string, element_to_add);
3161  }
3162  } else if (max_elements == 2) {
3163  if (current_element == max_elements - 1) {
3164  // TRANSLATORS: Presumably you just want to translate 'and' into
3165  // your language.
3166  // (Except your listing-grammar differs from english.)
3167  // 'value1' and 'value2'
3168  return g_strdup_printf(_("%sand '%s'"), string, element_to_add);
3169  } else {
3170  // TRANSLATORS: Presumably you just want to leave it english.
3171  // (Except your listing-grammar differs from english.)
3172  // 'value1' and 'value2'
3173  return g_strdup_printf(_("%s'%s' "), string, element_to_add);
3174  }
3175  } else {
3176  return g_strdup(element_to_add);
3177  }
3178 }
3179 
3193 static GError* rmplugin_x2go_string_setting_validator(gchar* key, gchar* value,
3194  gchar* data)
3195 {
3196  GError *error = NULL;
3197 
3198  if (!data) {
3199  gchar *error_msg = _("Invalid validation data in ProtocolSettings array!");
3200  REMMINA_PLUGIN_CRITICAL("%s", error_msg);
3201  g_set_error(&error, 1, 1, "%s", error_msg);
3202  return error;
3203  }
3204 
3205  gchar **elements_list = g_strsplit(data, ",", 0);
3206 
3207  guint elements_amount = 0;
3208  elements_amount = g_strv_length(elements_list);
3209 
3210  if (elements_list == NULL ||
3211  elements_list[0] == NULL ||
3212  strlen(elements_list[0]) <= 0)
3213  {
3214  gchar *error_msg = _("Validation data in ProtocolSettings array is invalid!");
3215  REMMINA_PLUGIN_CRITICAL("%s", error_msg);
3216  g_set_error(&error, 1, 1, "%s", error_msg);
3217  return error;
3218  }
3219 
3220  gchar *data_str = "";
3221 
3222  if (!key || !value) {
3223  REMMINA_PLUGIN_CRITICAL("%s", _("Parameters 'key' or 'value' are 'NULL'!"));
3224  g_set_error(&error, 1, 1, "%s", _("Internal error."));
3225  return error;
3226  }
3227 
3228  for (guint i = 0; elements_list[i] != NULL; i++) {
3229  // Don't wanna crash if elements_list[i] is NULL.
3230  gchar* element = elements_list[i] ? elements_list[i] : "";
3231  if (g_strcmp0(value, element) == 0) {
3232  // We found value in elements_list. Value passed validation.
3233  return NULL;
3234  }
3235 
3236  data_str = rmplugin_x2go_enumeration_prettifier(elements_amount, i,
3237  element, data_str);
3238  }
3239 
3240  if (elements_amount > 1) {
3241  g_set_error(&error, 1, 1, _("Allowed values are %s."), data_str);
3242  } else {
3243  g_set_error(&error, 1, 1, _("The only allowed value is '%s'."), data_str);
3244  }
3245 
3246  g_free(data_str);
3247  g_strfreev(elements_list);
3248 
3249  return error;
3250 }
3251 
3266 static GError* rmplugin_x2go_int_setting_validator(gchar* key, gpointer value,
3267  gchar* data)
3268 {
3269  GError *error = NULL;
3270 
3271  gchar **integer_list = g_strsplit(data, ";", 0);
3272 
3273  if (integer_list == NULL ||
3274  integer_list[0] == NULL ||
3275  integer_list[1] == NULL ||
3276  strlen(integer_list[0]) <= 0 ||
3277  strlen(integer_list[1]) <= 0)
3278  {
3279  gchar *error_msg = _("Validation data in ProtocolSettings array is invalid!");
3280  REMMINA_PLUGIN_CRITICAL("%s", error_msg);
3281  g_set_error(&error, 1, 1, "%s", error_msg);
3282  return error;
3283  }
3284 
3285  gint minimum;
3286  str2int_errno err = str2int(&minimum, integer_list[0], 10);
3287  if (err == STR2INT_INCONVERTIBLE) {
3288  g_set_error(&error, 1, 1, "%s", g_strdup_printf(
3289  _("Internal error: %s"),
3290  _("The lower limit is not a valid integer!")
3291  ));
3292  } else if (err == STR2INT_OVERFLOW) {
3293  g_set_error(&error, 1, 1, "%s", g_strdup_printf(
3294  _("Internal error: %s"),
3295  _("The lower limit is too high!")
3296  ));
3297  } else if (err == STR2INT_UNDERFLOW) {
3298  g_set_error(&error, 1, 1, "%s", g_strdup_printf(
3299  _("Internal error: %s"),
3300  _("The lower limit is too low!")
3301  ));
3302  } else if (err == STR2INT_INVALID_DATA) {
3303  g_set_error(&error, 1, 1, "%s", g_strdup_printf(
3304  _("Internal error: %s"),
3305  _("Something unknown went wrong.")
3306  ));
3307  }
3308 
3309  if (error) {
3310  REMMINA_PLUGIN_CRITICAL("%s", _("Please check the RemminaProtocolSetting "
3311  "array for possible errors."));
3312  return error;
3313  }
3314 
3315  gint maximum;
3316  err = str2int(&maximum, integer_list[1], 10);
3317  if (err == STR2INT_INCONVERTIBLE) {
3318  g_set_error(&error, 1, 1, "%s", g_strdup_printf(
3319  _("Internal error: %s"),
3320  _("The upper limit is not a valid integer!")
3321  ));
3322  } else if (err == STR2INT_OVERFLOW) {
3323  g_set_error(&error, 1, 1, "%s", g_strdup_printf(
3324  _("Internal error: %s"),
3325  _("The upper limit is too high!")
3326  ));
3327  } else if (err == STR2INT_UNDERFLOW) {
3328  g_set_error(&error, 1, 1, "%s", g_strdup_printf(
3329  _("Internal error: %s"),
3330  _("The upper limit is too low!")
3331  ));
3332  } else if (err == STR2INT_INVALID_DATA) {
3333  g_set_error(&error, 1, 1, "%s", g_strdup_printf(
3334  _("Internal error: %s"),
3335  _("Something unknown went wrong.")
3336  ));
3337  }
3338 
3339  if (error) {
3340  REMMINA_PLUGIN_CRITICAL("%s", _("Please check the RemminaProtocolSetting "
3341  "array for possible errors."));
3342  return error;
3343  }
3344 
3345  gint int_value;
3346  err = str2int(&int_value, value, 10);
3347  if (err == STR2INT_INCONVERTIBLE) {
3348  // non-numerical characters are can't be entered but, the user can
3349  // input an empty string.
3350  g_set_error(&error, 1, 1, "%s", _("The input is not a valid integer!"));
3351  } else if (err == STR2INT_OVERFLOW || err == STR2INT_UNDERFLOW) {
3352  g_set_error(&error, 1, 1, _("Input must be a number between %i and %i."),
3353  minimum, maximum);
3354  } else if (err == STR2INT_INVALID_DATA) {
3355  g_set_error(&error, 1, 1, "%s", _("Something unknown went wrong."));
3356  }
3357 
3358  if (error) {
3359  return error;
3360  }
3361 
3362  /*REMMINA_PLUGIN_DEBUG("Key: \t%s", (gchar*) key);
3363  REMMINA_PLUGIN_DEBUG("Value:\t%s", (gchar*) value);
3364  REMMINA_PLUGIN_DEBUG("Data: \t%s", data);
3365  REMMINA_PLUGIN_DEBUG("Min: %i, Max: %i", minimum, maximum);
3366  REMMINA_PLUGIN_DEBUG("Value converted:\t%i", int_value);*/
3367 
3368  if (err == STR2INT_SUCCESS && (minimum > int_value || int_value > maximum)) {
3369  g_set_error(&error, 1, 1, _("Input must be a number between %i and %i."),
3370  minimum, maximum);
3371  }
3372 
3373  // Should be NULL.
3374  return error;
3375 }
3376 
3377 /* Array of RemminaProtocolSetting for basic settings.
3378  * Each item is composed by:
3379  * a) RemminaProtocolSettingType for setting type
3380  * b) Setting name
3381  * c) Setting description
3382  * d) Compact disposition
3383  * e) Values for REMMINA_PROTOCOL_SETTING_TYPE_SELECT or REMMINA_PROTOCOL_SETTING_TYPE_COMBO
3384  * f) Setting tooltip
3385  * g) Validation data pointer, will be passed to the validation callback method.
3386  * h) Validation callback method (Can be NULL. Every entry will be valid then.)
3387  * use following prototype:
3388  * gboolean mysetting_validator_method(gpointer key, gpointer value,
3389  * gpointer validator_data);
3390  * gpointer key is a gchar* containing the setting's name,
3391  * gpointer value contains the value which should be validated,
3392  * gpointer validator_data contains your passed data.
3393  */
3395  {REMMINA_PROTOCOL_SETTING_TYPE_SERVER, "server", NULL, FALSE, NULL, NULL, NULL, NULL},
3396  {REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "username", N_("Username"), FALSE, NULL, NULL, NULL, NULL},
3397  {REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD, "password", N_("Password"), FALSE, NULL, NULL, NULL, NULL},
3398  {REMMINA_PROTOCOL_SETTING_TYPE_COMBO, "command", N_("Startup program"), FALSE,
3399  /* SELECT & COMBO Values */ "MATE,KDE,XFCE,LXDE,TERMINAL",
3400  /* Tooltip */ N_("Which command should be executed after creating the X2Go session?"), NULL, NULL},
3401  {REMMINA_PROTOCOL_SETTING_TYPE_RESOLUTION, "resolution", NULL, FALSE, NULL, NULL, NULL, NULL},
3402  {REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "kbdlayout", N_("Keyboard Layout (auto)"), FALSE, NULL, NULL, NULL, NULL},
3403  {REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "kbdtype", N_("Keyboard type (auto)"), FALSE, NULL, NULL, NULL, NULL},
3404  {REMMINA_PROTOCOL_SETTING_TYPE_COMBO, "audio", N_("Audio support"), FALSE,
3405  /* SELECT & COMBO Values */ "pulse,esd,none",
3406  /* Tooltip */ N_("The sound system of the X2Go server (default: 'pulse')."),
3407  /* Validation data */ "pulse,esd,none",
3408  /* Validation method */ G_CALLBACK(rmplugin_x2go_string_setting_validator)},
3409  {REMMINA_PROTOCOL_SETTING_TYPE_COMBO, "clipboard", N_("Clipboard direction"), FALSE,
3410  /* SELECT & COMBO Values */ "none,server,client,both",
3411  /* Tooltip */ N_("Which direction should clipboard content be copied? "
3412  "(default: 'both')."),
3413  /* Validation data */ "none,server,client,both",
3414  /* Validation method */ G_CALLBACK(rmplugin_x2go_string_setting_validator)},
3415  {REMMINA_PROTOCOL_SETTING_TYPE_INT, "dpi", N_("DPI resolution"), FALSE, NULL,
3416  /* Tooltip */ N_("Launch session with a specific resolution (in dots per inch). "
3417  "Must be between 20 and 400."),
3418  /* Validation data */ "20;400", // "<min>;<max>;"
3419  /* Validation method */ G_CALLBACK(rmplugin_x2go_int_setting_validator)},
3420  {REMMINA_PROTOCOL_SETTING_TYPE_FILE, "ssh_privatekey", N_("SSH identity file"), FALSE, NULL, N_("Your private key"), NULL, NULL },
3421  {REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, FALSE, NULL, NULL, NULL, NULL}};
3422 
3423 /* Protocol plugin definition and features */
3424 static RemminaProtocolPlugin rmplugin_x2go = {
3426  PLUGIN_NAME, // Name
3427  PLUGIN_DESCRIPTION, // Description
3428  GETTEXT_PACKAGE, // Translation domain
3429  PLUGIN_VERSION, // Version number
3430  PLUGIN_APPICON, // Icon for normal connection
3431  PLUGIN_SSH_APPICON, // Icon for SSH connection
3432  rmplugin_x2go_basic_settings, // Array for basic settings
3433  NULL, // Array for advanced settings
3434  REMMINA_PROTOCOL_SSH_SETTING_NONE, // SSH settings type
3435  rmplugin_x2go_features, // Array for available features
3436  rmplugin_x2go_init, // Plugin initialization method
3437  rmplugin_x2go_open_connection, // Plugin open connection method
3438  rmplugin_x2go_close_connection, // Plugin connection-method closure
3439  rmplugin_x2go_query_feature, // Query for available features
3440  NULL, // Call a feature
3441  NULL, // Send a keystroke
3442  NULL, // Screenshot
3443 };
3444 
3445 G_MODULE_EXPORT gboolean remmina_plugin_entry(RemminaPluginService *service)
3446 {
3447  TRACE_CALL("remmina_plugin_entry");
3448  rm_plugin_service = service;
3449 
3450  bindtextdomain(GETTEXT_PACKAGE, REMMINA_RUNTIME_LOCALEDIR);
3451  bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
3452 
3453  if (!service->register_plugin((RemminaPlugin *) &rmplugin_x2go)) {
3454  return FALSE;
3455  }
3456 
3457  pthread_mutex_init(&remmina_x2go_init_mutex, NULL);
3458  remmina_x2go_window_id_array = g_array_new(FALSE, TRUE, sizeof(Window));
3459 
3460  REMMINA_PLUGIN_MESSAGE("%s", _("X2Go plugin loaded."));
3461 
3462  return TRUE;
3463 }
static gboolean rmplugin_x2go_start_create_notify(RemminaProtocolWidget *gp, gchar *errmsg)
Definition: x2go_plugin.c:2834
enum _str2int_errno str2int_errno
static const RemminaProtocolFeature rmplugin_x2go_features[]
Definition: x2go_plugin.c:3123
static gboolean rmplugin_x2go_monitor_create_notify(RemminaProtocolWidget *gp, const gchar *cmd, gchar *errmsg)
Definition: x2go_plugin.c:2856
Stores all necessary information needed for retrieving sessions from a X2Go server.
Definition: x2go_plugin.c:1016
static GList * rmplugin_x2go_parse_pyhoca_sessions(RemminaProtocolWidget *gp, GError **error, struct _ConnectionData *connect_data)
This function is used to parse the output of rmplugin_x2go_get_pyhoca_sessions(). ...
Definition: x2go_plugin.c:2026
static void rmplugin_x2go_init(RemminaProtocolWidget *gp)
Definition: x2go_plugin.c:2761
void(* protocol_plugin_set_error)(RemminaProtocolWidget *gp, const gchar *fmt,...)
Definition: plugin.h:176
static RemminaProtocolPlugin rmplugin_x2go
Definition: x2go_plugin.c:1464
static gboolean rmplugin_x2go_verify_connection_data(struct _ConnectionData *connect_data)
Definition: x2go_plugin.c:1102
typedefG_BEGIN_DECLS struct _RemminaFile RemminaFile
Definition: types.h:44
static GError * rmplugin_x2go_int_setting_validator(gchar *key, gpointer value, gchar *data)
Validator-functions are getting executed when the user wants to save profile settings.
Definition: x2go_plugin.c:3266
static GValue rmplugin_x2go_session_chooser_get_property(GtkWidget *dialog, gint property_index, GtkTreePath *row)
Finds the GtkTreeView inside of the session chooser dialog, determines the selected row and extracts ...
Definition: x2go_plugin.c:817
GCallback dialog_factory_func
Definition: x2go_plugin.c:278
static gchar * rmplugin_x2go_spawn_pyhoca_process(guint argc, gchar *argv[], GError **error, gchar **env)
This function dumps all properties of a session to the console.
Definition: x2go_plugin.c:898
static GList * rmplugin_x2go_old_pyhoca_features()
Definition: x2go_plugin.c:2639
RemminaProtocolWidget * gp
Definition: x2go_plugin.c:160
static const RemminaProtocolSetting rmplugin_x2go_basic_settings[]
Definition: x2go_plugin.c:3394
static pthread_mutex_t remmina_x2go_init_mutex
Definition: x2go_plugin.c:1469
static gboolean rmplugin_x2go_close_connection(RemminaProtocolWidget *gp)
Definition: x2go_plugin.c:1621
static gchar * rmplugin_x2go_get_pyhoca_sessions(RemminaProtocolWidget *gp, GError **error, struct _ConnectionData *connect_data)
Executes &#39;pyhoca-cli –list-sessions&#39; for username.
Definition: x2go_plugin.c:1878
GCallback callbackfunc
Definition: x2go_plugin.c:275
SESSION_PROPERTIES
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.
Definition: x2go_plugin.c:174
static gboolean rmplugin_x2go_session_chooser_set_row_visible(GtkTreePath *path, gboolean value, GtkDialog *dialog)
Either sets a specific row visible or invisible.
Definition: x2go_plugin.c:1036
_str2int_errno
Definition: x2go_plugin.c:193
gchar * message
Definition: x2go_plugin.c:274
static void rmplugin_x2go_remove_window_id(Window window_id)
Definition: x2go_plugin.c:1543
gchar * ssh_passphrase
Definition: x2go_plugin.c:1021
static gboolean rmplugin_x2go_get_ssh_passphrase(RemminaProtocolWidget *gp, gchar *errmsg, gchar **passphrase)
Asks the user for a username and password.
Definition: x2go_plugin.c:1744
static gchar * rmplugin_x2go_ask_session(RemminaProtocolWidget *gp, GError **error, struct _ConnectionData *connect_data)
Asks the user, with the help of a dialog, to continue an already existing session, terminate or create a new one.
Definition: x2go_plugin.c:2198
static gboolean rmplugin_x2go_try_window_id(Window window_id)
Definition: x2go_plugin.c:2800
int(* orig_handler)(Display *, XErrorEvent *)
Definition: x2go_plugin.c:146
static gboolean onMainThread_cb(struct onMainThread_cb_data *d)
Definition: x2go_plugin.c:1486
void(* protocol_plugin_signal_connection_closed)(RemminaProtocolWidget *gp)
Definition: plugin.h:185
static gboolean rmplugin_x2go_start_session(RemminaProtocolWidget *gp)
Definition: x2go_plugin.c:2966
void(* get_server_port)(const gchar *server, gint defaultport, gchar **host, gint *port)
Definition: plugin.h:249
static GtkTreePath * rmplugin_x2go_session_chooser_get_selected_row(GtkWidget *dialog)
Gets the selected row of the Session-Chooser-Dialog.
Definition: x2go_plugin.c:753
gint(* file_get_int)(RemminaFile *remminafile, const gchar *setting, gint default_value)
Definition: plugin.h:222
static GError * rmplugin_x2go_string_setting_validator(gchar *key, gchar *value, gchar *data)
Validator-functions are getting executed when the user wants to save profile settings.
Definition: x2go_plugin.c:3193
gchar * ssh_privatekey
Definition: x2go_plugin.c:1020
void(* protocol_plugin_signal_connection_opened)(RemminaProtocolWidget *gp)
Definition: plugin.h:186
gpointer dialog_factory_data
Definition: x2go_plugin.c:279
static gchar * rmplugin_x2go_enumeration_prettifier(const guint max_elements, const guint current_element, gchar *element_to_add, gchar *string)
This function builds a string like: "&#39;value1&#39;, &#39;value2&#39; and &#39;value3&#39;" To be used in a loop...
Definition: x2go_plugin.c:3139
static gboolean rmplugin_x2go_on_plug_removed(GtkSocket *socket, RemminaProtocolWidget *gp)
Definition: x2go_plugin.c:2753
gboolean(* protocol_plugin_init_get_savepassword)(RemminaProtocolWidget *gp)
Definition: plugin.h:197
gboolean(* register_plugin)(RemminaPlugin *plugin)
Definition: plugin.h:166
gboolean(* gtksocket_available)(void)
Definition: plugin.h:251
static gpointer rmplugin_x2go_main_thread(RemminaProtocolWidget *gp)
Definition: x2go_plugin.c:3071
SESSION_CHOOSER_RESPONSE_TYPE
These define the responses of session-chooser-dialog&#39;s buttons.
Definition: x2go_plugin.c:367
static GtkTreeModelFilter * rmplugin_x2go_session_chooser_get_filter_model(GtkWidget *dialog, GtkTreeView *treeview)
Uses either &#39;dialog&#39; or &#39;treeview&#39; to return the GtkTreeModel of the Session-Chooser-Dialog.
Definition: x2go_plugin.c:702
static gboolean rmplugin_x2go_pyhoca_terminate_session(X2GoCustomUserData *custom_data)
Terminates a specific X2Go session using pyhoca-cli.
Definition: x2go_plugin.c:1145
enum onMainThread_cb_data::@61 func
static GtkWidget * rmplugin_x2go_choose_session_dialog_factory(X2GoCustomUserData *custom_data, GList *sessions_list)
Builds a dialog which contains all found X2Go-Sessions of the remote server.
Definition: x2go_plugin.c:509
GtkDialogFlags flags
Definition: x2go_plugin.c:270
static GtkWidget * rmplugin_x2go_find_child(GtkWidget *parent, const gchar *name)
Finds a child GtkWidget of a parent GtkWidget.
Definition: x2go_plugin.c:382
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)
Definition: x2go_plugin.c:2291
static gboolean rmplugin_x2go_save_credentials(RemminaFile *remminafile, gchar *s_username, gchar *s_password, gchar *errmsg)
Saves s_password and s_username if set.
Definition: x2go_plugin.c:1700
static gboolean rmplugin_x2go_open_dialog(X2GoCustomUserData *custom_data)
Definition: x2go_plugin.c:290
static gboolean rmplugin_x2go_session_chooser_row_activated(GtkTreeView *treeview, GtkTreePath *path, GtkTreeViewColumn *column, X2GoCustomUserData *custom_data)
Gets executed on "row-activated" signal.
Definition: x2go_plugin.c:419
static gboolean rmplugin_x2go_get_auth(RemminaProtocolWidget *gp, gchar *errmsg, gchar **default_username, gchar **default_password)
Asks the user for a username and password.
Definition: x2go_plugin.c:1786
gint(* protocol_plugin_init_auth)(RemminaProtocolWidget *gp, RemminaMessagePanelFlags pflags, const gchar *title, const gchar *default_username, const gchar *default_password, const gchar *default_domain, const gchar *password_prompt)
Definition: plugin.h:191
pthread_mutex_t mu
Definition: vnc_plugin.c:76
static gboolean rmplugin_x2go_query_feature(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature)
Definition: x2go_plugin.c:3116
gint(* get_profile_remote_height)(RemminaProtocolWidget *gp)
Definition: plugin.h:253
static void rmplugin_x2go_pyhoca_cli_exited(GPid pid, gint status, RemminaProtocolWidget *gp)
Definition: x2go_plugin.c:1639
static gboolean rmplugin_x2go_open_connection(RemminaProtocolWidget *gp)
Definition: x2go_plugin.c:3092
RemminaFile *(* protocol_plugin_get_file)(RemminaProtocolWidget *gp)
Definition: plugin.h:178
static void rmplugin_x2go_on_plug_added(GtkSocket *socket, RemminaProtocolWidget *gp)
Definition: x2go_plugin.c:2730
static void onMainThread_schedule_callback_and_wait(struct onMainThread_cb_data *d)
Definition: x2go_plugin.c:1511
gchar *(* protocol_plugin_init_get_username)(RemminaProtocolWidget *gp)
Definition: plugin.h:194
GtkButtonsType buttons
Definition: x2go_plugin.c:272
static gchar * rmplugin_x2go_session_property_to_string(guint session_property)
Translates a session property (described by SESSION_PROPERTIES enum) to a string containing it&#39;s disp...
Definition: x2go_plugin.c:471
gint(* get_profile_remote_width)(RemminaProtocolWidget *gp)
Definition: plugin.h:252
gchar *(* protocol_plugin_init_get_password)(RemminaProtocolWidget *gp)
Definition: plugin.h:195
struct _X2GoCustomUserData X2GoCustomUserData
Can be used to pass custom user data between functions and threads.
static gboolean rmplugin_x2go_cleanup(RemminaProtocolWidget *gp)
Definition: x2go_plugin.c:1572
Can be used to pass custom user data between functions and threads.
Definition: x2go_plugin.c:159
void(* protocol_plugin_register_hostkey)(RemminaProtocolWidget *gp, GtkWidget *widget)
Definition: plugin.h:180
static GArray * remmina_x2go_window_id_array
Definition: x2go_plugin.c:1470
G_MODULE_EXPORT gboolean remmina_plugin_entry(RemminaPluginService *service)
Definition: x2go_plugin.c:3445
RemminaProtocolWidget * gp
Definition: vnc_plugin.c:72
void(* file_set_string)(RemminaFile *remminafile, const gchar *setting, const gchar *value)
Definition: plugin.h:218
static RemminaPluginService * rm_plugin_service
Definition: x2go_plugin.c:136
GtkWindow * parent
Definition: x2go_plugin.c:269
gchar * title
Definition: x2go_plugin.c:273
static gboolean rmplugin_x2go_session_chooser_callback(X2GoCustomUserData *custom_data, gint response_id, GtkDialog *self)
Gets executed on dialog&#39;s &#39;response&#39; signal.
Definition: x2go_plugin.c:1339
static void onMainThread_cleanup_handler(gpointer data)
Definition: x2go_plugin.c:1504
struct _RemminaPluginX2GoData RemminaPluginX2GoData
static gboolean rmplugin_x2go_main(RemminaProtocolWidget *gp)
Definition: x2go_plugin.c:3059
DialogData:
Definition: x2go_plugin.c:267
const gchar *(* file_get_string)(RemminaFile *remminafile, const gchar *setting)
Definition: plugin.h:219
GtkMessageType type
Definition: x2go_plugin.c:271
static void onMainThread_gtk_socket_add_id(GtkSocket *sk, Window w)
Definition: x2go_plugin.c:1527
N_("Unable to connect to VNC server")
Definition: vnc_plugin.c:953
str2int_errno str2int(gint *out, gchar *s, gint base)
Convert string s to int out.
Definition: x2go_plugin.c:217
static GList * rmplugin_x2go_populate_available_features_list()
Definition: x2go_plugin.c:2667
static int rmplugin_x2go_dummy_handler(Display *dsp, XErrorEvent *err)
Definition: x2go_plugin.c:2828