rofi 1.7.5
ssh.c
Go to the documentation of this file.
1/*
2 * rofi
3 *
4 * MIT/X11 License
5 * Copyright © 2013-2022 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 "Modes.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 "modes/ssh.h"
55#include "history.h"
56#include "rofi.h"
57#include "settings.h"
58
62typedef struct _SshEntry {
64 char *hostname;
66 int port;
78
82#define SSH_CACHE_FILE "rofi-2.sshcache"
83
88#define SSH_TOKEN_DELIM "= \t\r\n"
89
97static 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
126static 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
154static 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
172static 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
266static 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
331static 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
342static 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
456static 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
529static 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
545static unsigned int ssh_mode_get_num_entries(const Mode *sw) {
546 const SSHModePrivateData *rmpd =
548 return rmpd->hosts_list_length;
549}
556static 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
579static 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;
595 ssh_mode_init(sw);
596 } else if (mretv & MENU_CUSTOM_COMMAND) {
597 retv = (mretv & MENU_LOWER_MASK);
598 }
599 return retv;
600}
601
614static 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
631static 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"
637Mode 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:1002
int helper_parse_setup(char *string, char ***output, int *length,...)
Definition helper.c:75
char * rofi_expand_path(const char *input)
Definition helper.c:740
int helper_token_match(rofi_int_matcher *const *tokens, const char *input)
Definition helper.c:518
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_get_private_data(const Mode *mode)
Definition mode.c:159
void mode_set_private_data(Mode *mode, void *pd)
Definition mode.c:164
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 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 int execshssh(const SshEntry *entry)
Definition ssh.c:97
static void delete_ssh(const char *host)
Definition ssh.c:154
static SshEntry * read_known_hosts_file(const char *path, SshEntry *retv, unsigned int *length)
Definition ssh.c:172
static SshEntry * read_hosts_file(SshEntry *retv, unsigned int *length)
Definition ssh.c:266
#define SSH_TOKEN_DELIM
Definition ssh.c:88
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 ModeMode ssh_mode_result(Mode *sw, int mretv, char **input, unsigned int selected_line)
Definition ssh.c:579
static SshEntry * get_ssh(SSHModePrivateData *pd, unsigned int *length)
Definition ssh.c:456
struct _SshEntry SshEntry
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:288
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
int port
Definition ssh.c:66
char * hostname
Definition ssh.c:64
char * name