Remmina - The GTK+ Remote Desktop Client  v1.4.2
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.
rmnews.c
Go to the documentation of this file.
1 /*
2  * Remmina - The GTK+ Remote Desktop Client
3  * Copyright (C) 2016-2020 Antenore Gatta, Giovanni Panozzo
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor,
18  * Boston, MA 02110-1301, USA.
19  *
20  * In addition, as a special exception, the copyright holders give
21  * permission to link the code of portions of this program with the
22  * OpenSSL library under certain conditions as described in each
23  * individual source file, and distribute linked combinations
24  * including the two.
25  * You must obey the GNU General Public License in all respects
26  * for all of the code used other than OpenSSL. * If you modify
27  * file(s) with this exception, you may extend this exception to your
28  * version of the file(s), but you are not obligated to do so. * If you
29  * do not wish to do so, delete this exception statement from your
30  * version. * If you delete this exception statement from all source
31  * files in the program, then also delete it here.
32  *
33  */
34 
35 #include "config.h"
37 
38 #include <stdlib.h>
39 #include <stdio.h>
40 #include <gtk/gtk.h>
41 #include <gio/gio.h>
42 #include <gio/gdesktopappinfo.h>
43 #include <glib/gi18n.h>
44 #include <libsoup/soup.h>
45 #include <glib/gstdio.h>
46 #include <fcntl.h>
47 #include <sys/types.h>
48 #include <sys/stat.h>
49 #include <time.h>
50 
51 #include "remmina.h"
52 #include "remmina_main.h"
53 #include "remmina_pref.h"
54 #include "remmina_public.h"
55 #include "remmina_utils.h"
56 #include "remmina_scheduler.h"
57 #include "remmina_stats_sender.h"
58 #include "remmina_stats.h"
59 #include "remmina_sysinfo.h"
60 #include "rmnews.h"
61 
62 #define ARR_SIZE(arr) ( sizeof((arr)) / sizeof((arr[0])) )
63 /* Neas file buffer */
64 #define READ_BUFFER_LEN 1024
65 /* Timers */
66 #define RMNEWS_CHECK_1ST_MS 3000
67 #define RMNEWS_CHECK_INTERVAL_MS 12000
68 /* How many seconds before to get news */
69 #define RMNEWS_INTERVAL_SEC 604800
70 /* TODO: move in config.h */
71 #define REMMINA_URL "https://remmina.org/"
72 #define RMNEWS_OUTPUT "/var/tmp/latest_news.md"
73 
75 #define GET_OBJ(object_name) gtk_builder_get_object(rmnews_news_dialog->builder, object_name)
76 
77 static SoupSession *session;
78 //static const gchar *rmnews_url = NULL;
79 static const gchar *output_file_path = NULL;
80 
81 static
82 const gchar *supported_mime_types[] = {
83  "x-scheme-handler/rdp",
84  "x-scheme-handler/spice",
85  "x-scheme-handler/vnc",
86  "x-scheme-handler/remmina",
87  "application/x-remmina",
88  NULL
89 };
90 
91 gint eweekdays[7] = {
92  86400,
93  172800,
94  259200,
95  345600,
96  432000,
97  518400,
98  604800
99 };
100 
102 {
103  TRACE_CALL(__func__);
104  if (gtk_switch_get_active(rmnews_news_dialog->rmnews_stats_switch)) {
106  if (remmina_pref_save()) {
108  }
109  } else {
112  }
113 
114 }
115 
117 {
118  TRACE_CALL(__func__);
119  g_autoptr(GError) error = NULL;
120  GDesktopAppInfo *desktop_info;
121  GAppInfo *info = NULL;
122  g_autofree gchar *id = g_strconcat (REMMINA_APP_ID, ".desktop", NULL);
123  int i;
124 
125  desktop_info = g_desktop_app_info_new (id);
126  if (!desktop_info)
127  return;
128 
129  info = G_APP_INFO (desktop_info);
130 
131  for (i = 0; supported_mime_types[i]; i++) {
132  if (!g_app_info_set_as_default_for_type (info, supported_mime_types[i], &error))
133  g_warning ("Failed to set '%s' as the default application for secondary content type '%s': %s",
134  g_app_info_get_name (info), supported_mime_types[i], error->message);
135  else
136  g_debug ("Set '%s' as the default application for '%s'",
137  g_app_info_get_name (info),
139  }
140 }
141 
142 static gchar *rmnews_get_file_contents(gchar *path)
143 {
144  gsize size;
145  gchar *content;
146 
147  if (g_file_get_contents(path, &content, &size, NULL)) {
148  if (!g_utf8_validate(content, size, NULL)) {
149  g_warning("%s content is not UTF-8", path);
150  g_free(content);
151  content = NULL;
152  }
153  }
154  //return g_markup_escape_text(content, strlen(content));
155  return content;
156 }
157 
158 static void rmnews_close_clicked(GtkButton *btn, gpointer user_data)
159 {
160  TRACE_CALL(__func__);
161  gtk_widget_destroy(GTK_WIDGET(rmnews_news_dialog->dialog));
162  rmnews_news_dialog->dialog = NULL;
163  g_free(rmnews_news_dialog);
164  rmnews_news_dialog = NULL;
165 
166 }
167 
168 static gboolean rmnews_dialog_deleted(GtkButton *btn, gpointer user_data)
169 {
170  TRACE_CALL(__func__);
171  gtk_widget_destroy(GTK_WIDGET(rmnews_news_dialog->dialog));
172  rmnews_news_dialog->dialog = NULL;
173  g_free(rmnews_news_dialog);
174  rmnews_news_dialog = NULL;
175 
176  return FALSE;
177 }
178 
179 void rmnews_show_news(GtkWindow *parent)
180 {
181  TRACE_CALL(__func__);
182 
183  rmnews_news_dialog = g_new0(RemminaNewsDialog, 1);
184  rmnews_news_dialog->retval = 1;
185 
186  rmnews_news_dialog->builder = remmina_public_gtk_builder_new_from_file("remmina_news.glade");
187  rmnews_news_dialog->dialog = GTK_DIALOG(gtk_builder_get_object(rmnews_news_dialog->builder, "RemminaNewsDialog"));
188 
189  rmnews_news_dialog->rmnews_text_view = GTK_TEXT_VIEW(GET_OBJ("rmnews_text_view"));
190  rmnews_news_dialog->rmnews_label = GTK_LABEL(GET_OBJ("rmnews_label"));
191  //rmnews_news_dialog->rmnews_stats_label = GTK_LABEL(GET_OBJ("rmnews_stats_label"));
192  rmnews_news_dialog->rmnews_stats_switch = GTK_SWITCH(GET_OBJ("rmnews_stats_switch"));
194  gtk_switch_set_active(rmnews_news_dialog->rmnews_stats_switch, TRUE);
195  }
196  rmnews_news_dialog->rmnews_defaultcl_label = GTK_LABEL(GET_OBJ("rmnews_defaultcl_label"));
197  rmnews_news_dialog->rmnews_defaultcl_button = GTK_BUTTON(GET_OBJ("rmnews_defaultcl_switch"));
198  rmnews_news_dialog->rmnews_button_close = GTK_BUTTON(GET_OBJ("rmnews_button_close"));
199  gtk_widget_set_can_default(GTK_WIDGET(rmnews_news_dialog->rmnews_button_close), TRUE);
200  gtk_widget_grab_default(GTK_WIDGET(rmnews_news_dialog->rmnews_button_close));
201 
202  gchar *contents = rmnews_get_file_contents(g_strdup(output_file_path));
203  if (contents) {
204  gtk_label_set_markup(rmnews_news_dialog->rmnews_label, contents);
205  g_free(contents);
206  }
207 
208  g_signal_connect(rmnews_news_dialog->rmnews_button_close, "clicked",
209  G_CALLBACK(rmnews_close_clicked), (gpointer)rmnews_news_dialog);
210  g_signal_connect(rmnews_news_dialog->dialog, "close",
211  G_CALLBACK(rmnews_close_clicked), NULL);
212  g_signal_connect(rmnews_news_dialog->dialog, "delete-event",
213  G_CALLBACK(rmnews_dialog_deleted), NULL);
214 
215  /* Connect signals */
216  gtk_builder_connect_signals(rmnews_news_dialog->builder, NULL);
217 
218  /* Show the non-modal news dialog */
219  gtk_widget_show_all(GTK_WIDGET(rmnews_news_dialog->dialog));
220  gtk_window_present(GTK_WINDOW(rmnews_news_dialog->dialog));
221  if (parent)
222  gtk_window_set_transient_for(GTK_WINDOW(rmnews_news_dialog->dialog), parent);
223  gtk_window_set_modal (GTK_WINDOW(rmnews_news_dialog->dialog), TRUE);
224 
225 
226 }
227 
228 static void rmnews_get_url_cb(SoupSession *session, SoupMessage *msg, gpointer data)
229 {
230  TRACE_CALL(__func__);
231  const char *name;
232  const char *header;
233  SoupBuffer *sb;
234  FILE *output_file = NULL;
235  gchar *filesha = NULL;
236  gchar *filesha_after = NULL;
237  GTimeVal t;
238 
239  g_info("Status code %d", msg->status_code);
240 
241  name = soup_message_get_uri(msg)->path;
242 
243  if (SOUP_STATUS_IS_CLIENT_ERROR(msg->status_code)) {
244  g_info("Status 404 - Release file not available");
245  g_get_current_time(&t);
248  return;
249  }
250 
251  if (SOUP_STATUS_IS_SERVER_ERROR(msg->status_code)) {
252  g_info("Server not available");
253  g_get_current_time(&t);
256  return;
257  }
258 
259  if (SOUP_STATUS_IS_TRANSPORT_ERROR(msg->status_code)) {
260  g_info("Transport Error");
261  g_get_current_time(&t);
264  return;
265  }
266 
267  if (msg->status_code == SOUP_STATUS_SSL_FAILED) {
268  GTlsCertificateFlags flags;
269 
270  if (soup_message_get_https_status(msg, NULL, &flags))
271  g_warning("%s: %d %s (0x%x)\n", name, msg->status_code, msg->reason_phrase, flags);
272  else
273  g_warning("%s: %d %s (no handshake status)\n", name, msg->status_code, msg->reason_phrase);
274  g_get_current_time(&t);
277  return;
278  } else if (SOUP_STATUS_IS_TRANSPORT_ERROR(msg->status_code)) {
279  g_warning("%s: %d %s\n", name, msg->status_code, msg->reason_phrase);
280  }
281 
282  if (SOUP_STATUS_IS_REDIRECTION(msg->status_code)) {
283  header = soup_message_headers_get_one(msg->response_headers,
284  "Location");
285  g_warning("Redirection detected");
286  if (header) {
287  SoupURI *uri;
288  char *uri_string;
289 
290  g_info(" -> %s\n", header);
291 
292  uri = soup_uri_new_with_base(soup_message_get_uri(msg), header);
293  uri_string = soup_uri_to_string(uri, FALSE);
294  rmnews_get_url(uri_string);
295  g_free(uri_string);
296  soup_uri_free(uri);
297  }
298  g_get_current_time(&t);
301  return;
302  } else if (SOUP_STATUS_IS_SUCCESSFUL(msg->status_code)) {
303  g_info("Status 200");
304  if (output_file_path) {
305  g_info("Calculating the SHA1 of the local file");
307  g_info("SHA1 is %s", filesha);
308  if (filesha == NULL || filesha[0] == 0)
309  filesha = "0\0";
310  g_info("Opening %s output file for writing", output_file_path);
311  output_file = fopen(output_file_path, "w");
312  if (!output_file) {
313  g_printerr("Error trying to create file %s.\n", output_file_path);
314  g_get_current_time(&t);
317  return;
318  }
319  } else {
320  g_warning("Cannot open output file for writing, because output_file_path is NULL");
321  g_get_current_time(&t);
324  return;
325  }
326 
327 
328  sb = soup_message_body_flatten (msg->response_body);
329  if (output_file) {
330  fwrite(sb->data, 1, sb->length, output_file);
331 
332  if (output_file_path) {
333  fclose(output_file);
334  filesha_after = remmina_sha1_file(output_file_path);
335  }
336  g_info("SHA1 after download is %s", filesha_after);
337  if (g_strcmp0(filesha, filesha_after) != 0) {
338  g_info("SHA1 differs, we show the news and reset the counter");
340  GtkWindow *parent = remmina_main_get_window();
341  if (!kioskmode && kioskmode == FALSE)
342  rmnews_show_news(parent);
343  } else {
344  g_get_current_time(&t);
346  }
347  /* Increase counter with number of successful GETs */
350  }
351  }
352  g_object_unref(msg);
353 }
361 {
362  TRACE_CALL(__func__);
363  GChecksum *chs;
364  const gchar *uname, *hname;
365  const gchar *uid_suffix;
366  gchar *uid_prefix;
367  gchar *uid;
368 
369  /* This code is very similar to remmina_stats_get_uid() */
370 
372  /* Generate a new UUID_PREFIX for news on this installation */
373  uid_prefix = remmina_gen_random_uuid();
378  }
379 
380  uname = g_get_user_name();
381  hname = g_get_host_name();
382  chs = g_checksum_new(G_CHECKSUM_SHA256);
383  g_checksum_update(chs, (const guchar*)uname, strlen(uname));
384  g_checksum_update(chs, (const guchar*)hname, strlen(hname));
385  uid_suffix = g_checksum_get_string(chs);
386 
387  uid = g_strdup_printf("02-%s-%.10s", remmina_pref.periodic_rmnews_uuid_prefix, uid_suffix);
388  g_checksum_free(chs);
389 
390  return uid;
391 }
392 
393 
394 void rmnews_get_url(const char *url)
395 {
396  TRACE_CALL(__func__);
397 
398  SoupMessage *msg;
399 
400  msg = soup_message_new("GET", url);
401  soup_message_set_flags(msg, SOUP_MESSAGE_NO_REDIRECT);
402 
403  g_debug("Fetching %s", url);
404 
405  g_object_ref(msg);
406  soup_session_queue_message(session, msg, rmnews_get_url_cb, NULL);
407 }
408 
410 {
411  TRACE_CALL(__func__);
412 
413  SoupLogger *logger = NULL;
414  int fd;
415  gchar *uid;
416  gchar mage[20], gcount[20];
417  gboolean sa;
418  struct stat sb;
419 
420  gchar *cachedir = g_build_path("/", g_get_user_cache_dir(), REMMINA_APP_ID, NULL);
421  gint d = g_mkdir_with_parents(cachedir, 0750);
422  if (d < 0)
423  output_file_path = RMNEWS_OUTPUT;
424  else
425  output_file_path = g_build_path("/", cachedir, "latest_news.md", NULL);
426 
427  g_info("Output file set to %s", output_file_path);
428 
431  g_file_set_contents (output_file_path, "", 0, NULL);
432  /* Just a symolic date */
433  remmina_pref.periodic_rmnews_last_get = 191469343000;
434  }
435 
436  fd = g_open (output_file_path, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR);
437  g_debug ("Returned %d while creating %s", fd, output_file_path);
438  /* If we cannot create the remmina_news file, we avoid connections */
439  if (fd < 0) {
440  g_debug ("Cannot store the remmina news file");
441  return;
442  }
443  g_close(fd, NULL);
444 
445  g_info("Output file %s created successfully", output_file_path);
446 
447  if (output_file_path) {
448  } else {
449  g_warning("Output file set to %s", output_file_path);
450  }
451 
452  g_info("Gathering news");
453  session = g_object_new(SOUP_TYPE_SESSION,
454  SOUP_SESSION_ADD_FEATURE_BY_TYPE, SOUP_TYPE_CONTENT_DECODER,
455  SOUP_SESSION_ADD_FEATURE_BY_TYPE, SOUP_TYPE_COOKIE_JAR,
456  SOUP_SESSION_USER_AGENT, "get ",
457  SOUP_SESSION_ACCEPT_LANGUAGE_AUTO, TRUE,
458  NULL);
459  /* TODO: Catch log level and set SOUP_LOGGER_LOG_MINIMAL or more */
460  logger = soup_logger_new(SOUP_LOGGER_LOG_NONE, -1);
461  soup_session_add_feature(session, SOUP_SESSION_FEATURE(logger));
462  g_object_unref(logger);
463 
464  gchar *lang = remmina_utils_get_lang();
465  g_debug("Language %s", lang);
466 
467  uid = rmnews_get_uid();
468 
469  sa = FALSE;
473  sa = TRUE;
474  }
475 
476  if (stat("/etc/machine-id", &sb) == 0)
477  sprintf(mage, "%ld", (long)(time(NULL) - sb.st_mtim.tv_sec));
478  else
479  strcpy(mage, "0");
480 
481  sprintf(gcount, "%ld", remmina_pref.periodic_rmnews_get_count);
482 
483  rmnews_get_url(g_strconcat(REMMINA_URL,
484  "news/remmina_news.php?lang=",
485  lang,
486  "&ver="
487  VERSION,
488  "&uid=",
489  uid,
490  "&sa=",
491  sa ? "1" : "0",
492  "&mage=",
493  mage,
494  "&gcount=",
495  gcount,
496  NULL));
497 
498  g_free(uid);
499  g_object_unref(session);
500 }
501 
502 static gboolean rmnews_periodic_check(gpointer user_data)
503 {
504  TRACE_CALL(__func__);
505  GTimeVal t;
506  glong next = 0;
507 
508  srand(time(NULL));
509 
510  g_get_current_time(&t);
511 
512  /* if remmina_pref is not writable ... */
514  gint randidx = rand() % 7;
515  /* We randmoly set periodic_rmnews_last_get to a a day between today
516  * and 7 days ago */
517  g_debug ("Setting a random periodic_rmnews_last_get");
518  remmina_pref.periodic_rmnews_last_get = t.tv_sec - eweekdays[randidx];
519  }
520  g_debug ("periodic_rmnews_last_get is %ld", remmina_pref.periodic_rmnews_last_get);
521 
522  next = remmina_pref.periodic_rmnews_last_get + RMNEWS_INTERVAL_SEC;
523  if (t.tv_sec > next || (t.tv_sec < remmina_pref.periodic_rmnews_last_get && t.tv_sec > 1514764800))
524  rmnews_get_news();
525  return G_SOURCE_CONTINUE;
526 }
527 
529 {
530  TRACE_CALL(__func__);
532  NULL,
533  RMNEWS_CHECK_1ST_MS,
534  RMNEWS_CHECK_INTERVAL_MS);
535 }
GtkWindow * remmina_main_get_window()
void rmnews_show_news(GtkWindow *parent)
Definition: rmnews.c:179
static void rmnews_close_clicked(GtkButton *btn, gpointer user_data)
Definition: rmnews.c:158
GtkLabel * rmnews_defaultcl_label
Definition: rmnews.h:44
GtkBuilder * remmina_public_gtk_builder_new_from_file(gchar *filename)
GtkButton * rmnews_button_close
Definition: rmnews.h:41
GtkDialog * dialog
Definition: rmnews.h:37
static const gchar * supported_mime_types[]
Definition: rmnews.c:82
static gboolean rmnews_dialog_deleted(GtkButton *btn, gpointer user_data)
Definition: rmnews.c:168
void * remmina_scheduler_setup(GSourceFunc cb, gpointer cb_data, guint first_interval, guint interval)
GtkTextView * rmnews_text_view
Definition: rmnews.h:39
General utility functions, non-GTK related.
void rmnews_stats_switch_state_set_cb()
Definition: rmnews.c:101
gchar * remmina_gen_random_uuid()
Generate a random sting of chars to be used as part of UID for news or stats.
static SoupSession * session
Definition: rmnews.c:77
gchar * periodic_usage_stats_uuid_prefix
Definition: remmina_pref.h:202
gchar * remmina_utils_get_lang()
Return the current language defined in the LC_ALL.
gchar * periodic_rmnews_uuid_prefix
Definition: remmina_pref.h:208
gchar * remmina_sha1_file(const gchar *filename)
Create a hexadecimal string version of the SHA-1 digest of the contents of the named file...
gint eweekdays[7]
Definition: rmnews.c:91
static const gchar * output_file_path
Definition: rmnews.c:79
static RemminaNewsDialog * rmnews_news_dialog
Definition: rmnews.c:74
glong periodic_rmnews_get_count
Definition: remmina_pref.h:207
GtkSwitch * rmnews_stats_switch
Definition: rmnews.h:43
gboolean periodic_usage_stats_permitted
Definition: remmina_pref.h:200
static void rmnews_get_url_cb(SoupSession *session, SoupMessage *msg, gpointer data)
Definition: rmnews.c:228
static gchar * cachedir
GtkLabel * rmnews_label
Definition: rmnews.h:40
static gboolean rmnews_periodic_check(gpointer user_data)
Definition: rmnews.c:502
void rmnews_schedule()
Definition: rmnews.c:528
RemminaPref remmina_pref
Definition: rcw.c:73
void remmina_stats_sender_schedule()
GtkBuilder * builder
Definition: rmnews.h:36
gboolean remmina_pref_save(void)
Definition: remmina_pref.c:707
static gchar * rmnews_get_file_contents(gchar *path)
Definition: rmnews.c:142
void rmnews_get_url(const char *url)
Definition: rmnews.c:394
gchar * rmnews_get_uid()
Try to get a unique system+user ID to identify this remmina user and avoid some duplicated task...
Definition: rmnews.c:360
gboolean kioskmode
Definition: remmina.c:79
void rmnews_defaultcl_on_click()
Definition: rmnews.c:116
gboolean remmina_pref_is_rw(void)
Definition: remmina_pref.c:697
glong periodic_rmnews_last_get
Definition: remmina_pref.h:206
GtkButton * rmnews_defaultcl_button
Definition: rmnews.h:45
void rmnews_get_news()
Definition: rmnews.c:409