rofi  1.7.0
ssh.c
Go to the documentation of this file.
1 /*
2  * rofi
3  *
4  * MIT/X11 License
5  * Copyright © 2013-2021 Qball Cow <qball@gmpclient.org>
6  *
7  * Permission is hereby granted, free of charge, to any person obtaining
8  * a copy of this software and associated documentation files (the
9  * "Software"), to deal in the Software without restriction, including
10  * without limitation the rights to use, copy, modify, merge, publish,
11  * distribute, sublicense, and/or sell copies of the Software, and to
12  * permit persons to whom the Software is furnished to do so, subject to
13  * the following conditions:
14  *
15  * The above copyright notice and this permission notice shall be
16  * included in all copies or substantial portions of the Software.
17  *
18  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
19  * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20  * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
21  * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
22  * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
23  * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
24  * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25  *
26  */
27 
36 #define G_LOG_DOMAIN "Dialogs.Ssh"
37 
38 #include <config.h>
39 #include <glib.h>
40 #include <stdio.h>
41 #include <stdlib.h>
42 
43 #include <ctype.h>
44 #include <dirent.h>
45 #include <errno.h>
46 #include <glob.h>
47 #include <helper.h>
48 #include <signal.h>
49 #include <string.h>
50 #include <strings.h>
51 #include <sys/types.h>
52 #include <unistd.h>
53 
54 #include "dialogs/ssh.h"
55 #include "history.h"
56 #include "rofi.h"
57 #include "settings.h"
58 
62 typedef struct _SshEntry {
64  char *hostname;
66  int port;
71 typedef struct {
76  unsigned int hosts_list_length;
78 
82 #define SSH_CACHE_FILE "rofi-2.sshcache"
83 
88 #define SSH_TOKEN_DELIM "= \t\r\n"
89 
97 static int execshssh(const SshEntry *entry) {
98  char **args = NULL;
99  int argsv = 0;
100  gchar *portstr = NULL;
101  if (entry->port > 0) {
102  portstr = g_strdup_printf("%d", entry->port);
103  }
104  helper_parse_setup(config.ssh_command, &args, &argsv, "{host}",
105  entry->hostname, "{port}", portstr, (char *)0);
106  g_free(portstr);
107 
108  gsize l = strlen("Connecting to '' via rofi") + strlen(entry->hostname) + 1;
109  gchar *desc = g_newa(gchar, l);
110 
111  g_snprintf(desc, l, "Connecting to '%s' via rofi", entry->hostname);
112 
113  RofiHelperExecuteContext context = {
114  .name = "ssh",
115  .description = desc,
116  .command = "ssh",
117  };
118  return helper_execute(NULL, args, "ssh ", entry->hostname, &context);
119 }
120 
126 static void exec_ssh(const SshEntry *entry) {
127  if (!(entry->hostname) || !(entry->hostname[0])) {
128  return;
129  }
130 
131  if (!execshssh(entry)) {
132  return;
133  }
134 
135  // This happens in non-critical time (After launching app)
136  // It is allowed to be a bit slower.
137  char *path = g_build_filename(cache_dir, SSH_CACHE_FILE, NULL);
138  // TODO update.
139  if (entry->port > 0) {
140  char *store = g_strdup_printf("%s\x1F%d", entry->hostname, entry->port);
141  history_set(path, store);
142  g_free(store);
143  } else {
144  history_set(path, entry->hostname);
145  }
146  g_free(path);
147 }
148 
154 static void delete_ssh(const char *host) {
155  if (!host || !host[0]) {
156  return;
157  }
158  char *path = g_build_filename(cache_dir, SSH_CACHE_FILE, NULL);
159  history_remove(path, host);
160  g_free(path);
161 }
162 
172 static SshEntry *read_known_hosts_file(const char *path, SshEntry *retv,
173  unsigned int *length) {
174  FILE *fd = fopen(path, "r");
175  if (fd != NULL) {
176  char *buffer = NULL;
177  size_t buffer_length = 0;
178  // Reading one line per time.
179  while (getline(&buffer, &buffer_length, fd) > 0) {
180  // Strip whitespace.
181  char *start = g_strstrip(&(buffer[0]));
182  // Find start.
183  if (*start == '#' || *start == '@') {
184  // skip comments or cert-authority or revoked items.
185  continue;
186  }
187  if (*start == '|') {
188  // Skip hashed hostnames.
189  continue;
190  }
191  // Find end of hostname set.
192  char *end = strstr(start, " ");
193  if (end == NULL) {
194  // Something is wrong.
195  continue;
196  }
197  *end = '\0';
198  char *sep = start;
199  start = strsep(&sep, ", ");
200  while (start) {
201  int port = 0;
202  if (start[0] == '[') {
203  start++;
204  char *strend = strchr(start, ']');
205  if (strend[1] == ':') {
206  *strend = '\0';
207  errno = 0;
208  gchar *endptr = NULL;
209  gint64 number = g_ascii_strtoll(&(strend[2]), &endptr, 10);
210  if (errno != 0) {
211  g_warning("Failed to parse port number: %s.", &(strend[2]));
212  } else if (endptr == &(strend[2])) {
213  g_warning("Failed to parse port number: %s, invalid number.",
214  &(strend[2]));
215  } else if (number < 0 || number > 65535) {
216  g_warning("Failed to parse port number: %s, out of range.",
217  &(strend[2]));
218  } else {
219  port = number;
220  }
221  }
222  }
223  // Is this host name already in the list?
224  // We often get duplicates in hosts file, so lets check this.
225  int found = 0;
226  for (unsigned int j = 0; j < (*length); j++) {
227  if (!g_ascii_strcasecmp(start, retv[j].hostname)) {
228  found = 1;
229  break;
230  }
231  }
232 
233  if (!found) {
234  // Add this host name to the list.
235  retv = g_realloc(retv, ((*length) + 2) * sizeof(SshEntry));
236  retv[(*length)].hostname = g_strdup(start);
237  retv[(*length)].port = port;
238  retv[(*length) + 1].hostname = NULL;
239  retv[(*length) + 1].port = 0;
240  (*length)++;
241  }
242  start = strsep(&sep, ", ");
243  }
244  }
245  if (buffer != NULL) {
246  free(buffer);
247  }
248  if (fclose(fd) != 0) {
249  g_warning("Failed to close hosts file: '%s'", g_strerror(errno));
250  }
251  } else {
252  g_debug("Failed to open KnownHostFile: '%s'", path);
253  }
254 
255  return retv;
256 }
257 
266 static SshEntry *read_hosts_file(SshEntry *retv, unsigned int *length) {
267  // Read the hosts file.
268  FILE *fd = fopen("/etc/hosts", "r");
269  if (fd != NULL) {
270  char *buffer = NULL;
271  size_t buffer_length = 0;
272  // Reading one line per time.
273  while (getline(&buffer, &buffer_length, fd) > 0) {
274  // Evaluate one line.
275  unsigned int index = 0, ti = 0;
276  char *token = buffer;
277 
278  // Tokenize it.
279  do {
280  char c = buffer[index];
281  // Break on space, tab, newline and \0.
282  if (c == ' ' || c == '\t' || c == '\n' || c == '\0' || c == '#') {
283  buffer[index] = '\0';
284  // Ignore empty tokens
285  if (token[0] != '\0') {
286  ti++;
287  // and first token.
288  if (ti > 1) {
289  // Is this host name already in the list?
290  // We often get duplicates in hosts file, so lets check this.
291  int found = 0;
292  for (unsigned int j = 0; j < (*length); j++) {
293  if (!g_ascii_strcasecmp(token, retv[j].hostname)) {
294  found = 1;
295  break;
296  }
297  }
298 
299  if (!found) {
300  // Add this host name to the list.
301  retv = g_realloc(retv, ((*length) + 2) * sizeof(SshEntry));
302  retv[(*length)].hostname = g_strdup(token);
303  retv[(*length)].port = 0;
304  retv[(*length) + 1].hostname = NULL;
305  (*length)++;
306  }
307  }
308  }
309  // Set start to next element.
310  token = &buffer[index + 1];
311  // Everything after comment ignore.
312  if (c == '#') {
313  break;
314  }
315  }
316  // Skip to the next entry.
317  index++;
318  } while (buffer[index] != '\0' && buffer[index] != '#');
319  }
320  if (buffer != NULL) {
321  free(buffer);
322  }
323  if (fclose(fd) != 0) {
324  g_warning("Failed to close hosts file: '%s'", g_strerror(errno));
325  }
326  }
327 
328  return retv;
329 }
330 
331 static void add_known_hosts_file(SSHModePrivateData *pd, const char *token) {
332  GList *item =
333  g_list_find_custom(pd->user_known_hosts, token, (GCompareFunc)g_strcmp0);
334  if (item == NULL) {
335  g_debug("Add '%s' to UserKnownHost list", token);
336  pd->user_known_hosts = g_list_append(pd->user_known_hosts, g_strdup(token));
337  } else {
338  g_debug("File '%s' already in UserKnownHostsFile list", token);
339  }
340 }
341 
342 static void parse_ssh_config_file(SSHModePrivateData *pd, const char *filename,
343  SshEntry **retv, unsigned int *length,
344  unsigned int num_favorites) {
345  FILE *fd = fopen(filename, "r");
346 
347  g_debug("Parsing ssh config file: %s", filename);
348  if (fd != NULL) {
349  char *buffer = NULL;
350  size_t buffer_length = 0;
351  char *strtok_pointer = NULL;
352  while (getline(&buffer, &buffer_length, fd) > 0) {
353  // Each line is either empty, a comment line starting with a '#'
354  // character or of the form "keyword [=] arguments", where there may
355  // be multiple (possibly quoted) arguments separated by whitespace.
356  // The keyword is separated from its arguments by whitespace OR by
357  // optional whitespace and a '=' character.
358  char *token = strtok_r(buffer, SSH_TOKEN_DELIM, &strtok_pointer);
359  // Skip empty lines and comment lines. Also skip lines where the
360  // keyword is not "Host".
361  if (!token || *token == '#') {
362  continue;
363  }
364  char *low_token = g_ascii_strdown(token, -1);
365  if (g_strcmp0(low_token, "include") == 0) {
366  token = strtok_r(NULL, SSH_TOKEN_DELIM, &strtok_pointer);
367  g_debug("Found Include: %s", token);
368  gchar *path = rofi_expand_path(token);
369  gchar *full_path = NULL;
370  if (!g_path_is_absolute(path)) {
371  char *dirname = g_path_get_dirname(filename);
372  full_path = g_build_filename(dirname, path, NULL);
373  g_free(dirname);
374  } else {
375  full_path = g_strdup(path);
376  }
377  glob_t globbuf = {.gl_pathc = 0, .gl_pathv = NULL, .gl_offs = 0};
378 
379  if (glob(full_path, 0, NULL, &globbuf) == 0) {
380  for (size_t iter = 0; iter < globbuf.gl_pathc; iter++) {
381  parse_ssh_config_file(pd, globbuf.gl_pathv[iter], retv, length,
382  num_favorites);
383  }
384  }
385  globfree(&globbuf);
386 
387  g_free(full_path);
388  g_free(path);
389  } else if (g_strcmp0(low_token, "userknownhostsfile") == 0) {
390  while ((token = strtok_r(NULL, SSH_TOKEN_DELIM, &strtok_pointer))) {
391  g_debug("Found extra UserKnownHostsFile: %s", token);
392  add_known_hosts_file(pd, token);
393  }
394  } else if (g_strcmp0(low_token, "host") == 0) {
395  // Now we know that this is a "Host" line.
396  // The "Host" keyword is followed by one more host names separated
397  // by whitespace; while host names may be quoted with double quotes
398  // to represent host names containing spaces, we don't support this
399  // (how many host names contain spaces?).
400  while ((token = strtok_r(NULL, SSH_TOKEN_DELIM, &strtok_pointer))) {
401  // We do not want to show wildcard entries, as you cannot ssh to them.
402  const char *const sep = "*?";
403  if (*token == '!' || strpbrk(token, sep)) {
404  continue;
405  }
406 
407  // If comment, skip from now on.
408  if (*token == '#') {
409  break;
410  }
411 
412  // Is this host name already in the history file?
413  // This is a nice little penalty, but doable? time will tell.
414  // given num_favorites is max 25.
415  int found = 0;
416  for (unsigned int j = 0; j < num_favorites; j++) {
417  if (!g_ascii_strcasecmp(token, (*retv)[j].hostname)) {
418  found = 1;
419  break;
420  }
421  }
422 
423  if (found) {
424  continue;
425  }
426 
427  // Add this host name to the list.
428  (*retv) = g_realloc((*retv), ((*length) + 2) * sizeof(SshEntry));
429  (*retv)[(*length)].hostname = g_strdup(token);
430  (*retv)[(*length)].port = 0;
431  (*retv)[(*length) + 1].hostname = NULL;
432  (*length)++;
433  }
434  }
435  g_free(low_token);
436  }
437  if (buffer != NULL) {
438  free(buffer);
439  }
440 
441  if (fclose(fd) != 0) {
442  g_warning("Failed to close ssh configuration file: '%s'",
443  g_strerror(errno));
444  }
445  }
446 }
447 
456 static SshEntry *get_ssh(SSHModePrivateData *pd, unsigned int *length) {
457  SshEntry *retv = NULL;
458  unsigned int num_favorites = 0;
459  char *path;
460 
461  if (g_get_home_dir() == NULL) {
462  return NULL;
463  }
464 
465  path = g_build_filename(cache_dir, SSH_CACHE_FILE, NULL);
466  char **h = history_get_list(path, length);
467 
468  retv = malloc((*length) * sizeof(SshEntry));
469  for (unsigned int i = 0; i < (*length); i++) {
470  int port = 0;
471  char *portstr = strchr(h[i], '\x1F');
472  if (portstr != NULL) {
473  *portstr = '\0';
474  errno = 0;
475  gchar *endptr = NULL;
476  gint64 number = g_ascii_strtoll(&(portstr[1]), &endptr, 10);
477  if (errno != 0) {
478  g_warning("Failed to parse port number: %s.", &(portstr[1]));
479  } else if (endptr == &(portstr[1])) {
480  g_warning("Failed to parse port number: %s, invalid number.",
481  &(portstr[1]));
482  } else if (number < 0 || number > 65535) {
483  g_warning("Failed to parse port number: %s, out of range.",
484  &(portstr[1]));
485  } else {
486  port = number;
487  }
488  }
489  retv[i].hostname = h[i];
490  retv[i].port = port;
491  }
492  g_free(h);
493 
494  g_free(path);
495  num_favorites = (*length);
496 
497  const char *hd = g_get_home_dir();
498  path = g_build_filename(hd, ".ssh", "config", NULL);
499  parse_ssh_config_file(pd, path, &retv, length, num_favorites);
500 
501  if (config.parse_known_hosts == TRUE) {
502  char *known_hosts_path =
503  g_build_filename(g_get_home_dir(), ".ssh", "known_hosts", NULL);
504  retv = read_known_hosts_file(known_hosts_path, retv, length);
505  g_free(known_hosts_path);
506  for (GList *iter = g_list_first(pd->user_known_hosts); iter;
507  iter = g_list_next(iter)) {
508  char *user_known_hosts_path = rofi_expand_path((const char *)iter->data);
509  retv = read_known_hosts_file((const char *)user_known_hosts_path, retv,
510  length);
511  g_free(user_known_hosts_path);
512  }
513  }
514  if (config.parse_hosts == TRUE) {
515  retv = read_hosts_file(retv, length);
516  }
517 
518  g_free(path);
519 
520  return retv;
521 }
522 
529 static int ssh_mode_init(Mode *sw) {
530  if (mode_get_private_data(sw) == NULL) {
531  SSHModePrivateData *pd = g_malloc0(sizeof(*pd));
532  mode_set_private_data(sw, (void *)pd);
533  pd->hosts_list = get_ssh(pd, &(pd->hosts_list_length));
534  }
535  return TRUE;
536 }
537 
545 static unsigned int ssh_mode_get_num_entries(const Mode *sw) {
546  const SSHModePrivateData *rmpd =
548  return rmpd->hosts_list_length;
549 }
556 static void ssh_mode_destroy(Mode *sw) {
558  if (rmpd != NULL) {
559  for (unsigned int i = 0; i < rmpd->hosts_list_length; i++) {
560  g_free(rmpd->hosts_list[i].hostname);
561  }
562  g_list_free_full(rmpd->user_known_hosts, g_free);
563  g_free(rmpd->hosts_list);
564  g_free(rmpd);
565  mode_set_private_data(sw, NULL);
566  }
567 }
568 
579 static ModeMode ssh_mode_result(Mode *sw, int mretv, char **input,
580  unsigned int selected_line) {
581  ModeMode retv = MODE_EXIT;
583  if ((mretv & MENU_OK) && rmpd->hosts_list[selected_line].hostname != NULL) {
584  exec_ssh(&(rmpd->hosts_list[selected_line]));
585  } else if ((mretv & MENU_CUSTOM_INPUT) && *input != NULL &&
586  *input[0] != '\0') {
587  SshEntry entry = {.hostname = *input, .port = 0};
588  exec_ssh(&entry);
589  } else if ((mretv & MENU_ENTRY_DELETE) &&
590  rmpd->hosts_list[selected_line].hostname) {
591  delete_ssh(rmpd->hosts_list[selected_line].hostname);
592  // Stay
593  retv = RELOAD_DIALOG;
594  ssh_mode_destroy(sw);
595  ssh_mode_init(sw);
596  } else if (mretv & MENU_CUSTOM_COMMAND) {
597  retv = (mretv & MENU_LOWER_MASK);
598  }
599  return retv;
600 }
601 
614 static char *_get_display_value(const Mode *sw, unsigned int selected_line,
615  G_GNUC_UNUSED int *state,
616  G_GNUC_UNUSED GList **attr_list,
617  int get_entry) {
619  return get_entry ? g_strdup(rmpd->hosts_list[selected_line].hostname) : NULL;
620 }
621 
631 static int ssh_token_match(const Mode *sw, rofi_int_matcher **tokens,
632  unsigned int index) {
634  return helper_token_match(tokens, rmpd->hosts_list[index].hostname);
635 }
636 #include "mode-private.h"
637 Mode ssh_mode = {.name = "ssh",
638  .cfg_name_key = "display-ssh",
639  ._init = ssh_mode_init,
640  ._get_num_entries = ssh_mode_get_num_entries,
641  ._result = ssh_mode_result,
642  ._destroy = ssh_mode_destroy,
643  ._token_match = ssh_token_match,
644  ._get_display_value = _get_display_value,
645  ._get_completion = NULL,
646  ._preprocess_input = NULL,
647  .private_data = NULL,
648  .free = NULL};
gboolean helper_execute(const char *wd, char **args, const char *error_precmd, const char *error_cmd, RofiHelperExecuteContext *context)
Definition: helper.c:984
int helper_parse_setup(char *string, char ***output, int *length,...)
Definition: helper.c:75
char * rofi_expand_path(const char *input)
Definition: helper.c:713
int helper_token_match(rofi_int_matcher *const *tokens, const char *input)
Definition: helper.c:494
void history_set(const char *filename, const char *entry)
Definition: history.c:178
void history_remove(const char *filename, const char *entry)
Definition: history.c:259
char ** history_get_list(const char *filename, unsigned int *length)
Definition: history.c:323
void mode_set_private_data(Mode *mode, void *pd)
Definition: mode.c:136
void * mode_get_private_data(const Mode *mode)
Definition: mode.c:131
ModeMode
Definition: mode.h:49
@ MENU_CUSTOM_COMMAND
Definition: mode.h:79
@ MENU_LOWER_MASK
Definition: mode.h:87
@ MENU_ENTRY_DELETE
Definition: mode.h:75
@ MENU_OK
Definition: mode.h:67
@ MENU_CUSTOM_INPUT
Definition: mode.h:73
@ MODE_EXIT
Definition: mode.h:51
@ RELOAD_DIALOG
Definition: mode.h:55
const char * cache_dir
Definition: rofi.c:83
static int ssh_mode_init(Mode *sw)
Definition: ssh.c:529
static SshEntry * read_known_hosts_file(const char *path, SshEntry *retv, unsigned int *length)
Definition: ssh.c:172
static int ssh_token_match(const Mode *sw, rofi_int_matcher **tokens, unsigned int index)
Definition: ssh.c:631
static void add_known_hosts_file(SSHModePrivateData *pd, const char *token)
Definition: ssh.c:331
static void exec_ssh(const SshEntry *entry)
Definition: ssh.c:126
#define SSH_CACHE_FILE
Definition: ssh.c:82
static char * _get_display_value(const Mode *sw, unsigned int selected_line, G_GNUC_UNUSED int *state, G_GNUC_UNUSED GList **attr_list, int get_entry)
Definition: ssh.c:614
static SshEntry * read_hosts_file(SshEntry *retv, unsigned int *length)
Definition: ssh.c:266
static int execshssh(const SshEntry *entry)
Definition: ssh.c:97
static void delete_ssh(const char *host)
Definition: ssh.c:154
#define SSH_TOKEN_DELIM
Definition: ssh.c:88
static ModeMode ssh_mode_result(Mode *sw, int mretv, char **input, unsigned int selected_line)
Definition: ssh.c:579
struct _SshEntry SshEntry
static SshEntry * get_ssh(SSHModePrivateData *pd, unsigned int *length)
Definition: ssh.c:456
static void ssh_mode_destroy(Mode *sw)
Definition: ssh.c:556
static void parse_ssh_config_file(SSHModePrivateData *pd, const char *filename, SshEntry **retv, unsigned int *length, unsigned int num_favorites)
Definition: ssh.c:342
Mode ssh_mode
Definition: ssh.c:637
static unsigned int ssh_mode_get_num_entries(const Mode *sw)
Definition: ssh.c:545
Settings config
const gchar * name
Definition: helper.h:296
SshEntry * hosts_list
Definition: ssh.c:74
unsigned int hosts_list_length
Definition: ssh.c:76
GList * user_known_hosts
Definition: ssh.c:72
unsigned int parse_known_hosts
Definition: settings.h:130
unsigned int parse_hosts
Definition: settings.h:128
char * ssh_command
Definition: settings.h:69
Definition: ssh.c:62
int port
Definition: ssh.c:66
char * hostname
Definition: ssh.c:64
char * name
Definition: mode-private.h:163