rofi  1.6.1
ssh.c
Go to the documentation of this file.
1 /*
2  * rofi
3  *
4  * MIT/X11 License
5  * Copyright © 2013-2020 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 <stdlib.h>
41 #include <stdio.h>
42 
43 #include <unistd.h>
44 #include <signal.h>
45 #include <sys/types.h>
46 #include <dirent.h>
47 #include <strings.h>
48 #include <string.h>
49 #include <ctype.h>
50 #include <errno.h>
51 #include <helper.h>
52 #include <glob.h>
53 
54 #include "rofi.h"
55 #include "settings.h"
56 #include "history.h"
57 #include "dialogs/ssh.h"
58 
62 typedef struct _SshEntry
63 {
65  char *hostname;
67  int port;
72 typedef struct
73 {
78  unsigned int hosts_list_length;
80 
84 #define SSH_CACHE_FILE "rofi-2.sshcache"
85 
90 #define SSH_TOKEN_DELIM "= \t\r\n"
91 
99 static int execshssh ( const SshEntry *entry )
100 {
101  char **args = NULL;
102  int argsv = 0;
103  gchar *portstr = NULL;
104  if ( entry->port > 0 ) {
105  portstr = g_strdup_printf ( "%d", entry->port );
106  }
107  helper_parse_setup ( config.ssh_command, &args, &argsv,
108  "{host}", entry->hostname,
109  "{port}", portstr,
110  (char *) 0 );
111  g_free ( portstr );
112 
113  gsize l = strlen ( "Connecting to '' via rofi" ) + strlen ( entry->hostname ) + 1;
114  gchar *desc = g_newa ( gchar, l );
115 
116  g_snprintf ( desc, l, "Connecting to '%s' via rofi", entry->hostname );
117 
118  RofiHelperExecuteContext context = {
119  .name = "ssh",
120  .description = desc,
121  .command = "ssh",
122  };
123  return helper_execute ( NULL, args, "ssh ", entry->hostname, &context );
124 }
125 
131 static void exec_ssh ( const SshEntry *entry )
132 {
133  if ( !( entry->hostname ) || !( entry->hostname[0] ) ) {
134  return;
135  }
136 
137  if ( !execshssh ( entry ) ) {
138  return;
139  }
140 
141  // This happens in non-critical time (After launching app)
142  // It is allowed to be a bit slower.
143  char *path = g_build_filename ( cache_dir, SSH_CACHE_FILE, NULL );
144  // TODO update.
145  if ( entry->port > 0 ) {
146  char *store = g_strdup_printf ( "%s\x1F%d", entry->hostname, entry->port );
147  history_set ( path, store );
148  g_free ( store );
149  }
150  else {
151  history_set ( path, entry->hostname );
152  }
153  g_free ( path );
154 }
155 
161 static void delete_ssh ( const char *host )
162 {
163  if ( !host || !host[0] ) {
164  return;
165  }
166  char *path = g_build_filename ( cache_dir, SSH_CACHE_FILE, NULL );
167  history_remove ( path, host );
168  g_free ( path );
169 }
170 
180 static SshEntry *read_known_hosts_file ( const char *path, SshEntry * retv, unsigned int *length )
181 {
182  FILE *fd = fopen ( path, "r" );
183  if ( fd != NULL ) {
184  char *buffer = NULL;
185  size_t buffer_length = 0;
186  // Reading one line per time.
187  while ( getline ( &buffer, &buffer_length, fd ) > 0 ) {
188  // Strip whitespace.
189  char *start = g_strstrip ( &( buffer[0] ) );
190  // Find start.
191  if ( *start == '#' || *start == '@' ) {
192  // skip comments or cert-authority or revoked items.
193  continue;
194  }
195  if ( *start == '|' ) {
196  // Skip hashed hostnames.
197  continue;
198  }
199  // Find end of hostname set.
200  char *end = strstr ( start, " " );
201  if ( end == NULL ) {
202  // Something is wrong.
203  continue;
204  }
205  *end = '\0';
206  char *sep = start;
207  start = strsep ( &sep, ", " );
208  while ( start ) {
209  int port = 0;
210  if ( start[0] == '[' ) {
211  start++;
212  char *end = strchr ( start, ']' );
213  if ( end[1] == ':' ) {
214  *end = '\0';
215  errno = 0;
216  gchar *endptr = NULL;
217  gint64 number = g_ascii_strtoll ( &( end[2] ), &endptr, 10 );
218  if ( errno != 0 ) {
219  g_warning ( "Failed to parse port number: %s.", &( end[2] ) );
220  }
221  else if ( endptr == &( end[2] ) ) {
222  g_warning ( "Failed to parse port number: %s, invalid number.", &( end[2] ) );
223  }
224  else if ( number < 0 || number > 65535 ) {
225  g_warning ( "Failed to parse port number: %s, out of range.", &( end[2] ) );
226  }
227  else {
228  port = number;
229  }
230  }
231  }
232  // Is this host name already in the list?
233  // We often get duplicates in hosts file, so lets check this.
234  int found = 0;
235  for ( unsigned int j = 0; j < ( *length ); j++ ) {
236  if ( !g_ascii_strcasecmp ( start, retv[j].hostname ) ) {
237  found = 1;
238  break;
239  }
240  }
241 
242  if ( !found ) {
243  // Add this host name to the list.
244  retv = g_realloc ( retv, ( ( *length ) + 2 ) * sizeof ( SshEntry ) );
245  retv[( *length )].hostname = g_strdup ( start );
246  retv[( *length )].port = port;
247  retv[( *length ) + 1].hostname = NULL;
248  retv[( *length ) + 1].port = 0;
249  ( *length )++;
250  }
251  start = strsep ( &sep, ", " );
252  }
253  }
254  if ( buffer != NULL ) {
255  free ( buffer );
256  }
257  if ( fclose ( fd ) != 0 ) {
258  g_warning ( "Failed to close hosts file: '%s'", g_strerror ( errno ) );
259  }
260  }
261  else {
262  g_debug ( "Failed to open KnownHostFile: '%s'", path );
263  }
264 
265  return retv;
266 }
267 
276 static SshEntry *read_hosts_file ( SshEntry * retv, unsigned int *length )
277 {
278  // Read the hosts file.
279  FILE *fd = fopen ( "/etc/hosts", "r" );
280  if ( fd != NULL ) {
281  char *buffer = NULL;
282  size_t buffer_length = 0;
283  // Reading one line per time.
284  while ( getline ( &buffer, &buffer_length, fd ) > 0 ) {
285  // Evaluate one line.
286  unsigned int index = 0, ti = 0;
287  char *token = buffer;
288 
289  // Tokenize it.
290  do {
291  char c = buffer[index];
292  // Break on space, tab, newline and \0.
293  if ( c == ' ' || c == '\t' || c == '\n' || c == '\0' || c == '#' ) {
294  buffer[index] = '\0';
295  // Ignore empty tokens
296  if ( token[0] != '\0' ) {
297  ti++;
298  // and first token.
299  if ( ti > 1 ) {
300  // Is this host name already in the list?
301  // We often get duplicates in hosts file, so lets check this.
302  int found = 0;
303  for ( unsigned int j = 0; j < ( *length ); j++ ) {
304  if ( !g_ascii_strcasecmp ( token, retv[j].hostname ) ) {
305  found = 1;
306  break;
307  }
308  }
309 
310  if ( !found ) {
311  // Add this host name to the list.
312  retv = g_realloc ( retv,
313  ( ( *length ) + 2 ) * sizeof ( SshEntry ) );
314  retv[( *length )].hostname = g_strdup ( token );
315  retv[( *length )].port = 0;
316  retv[( *length ) + 1].hostname = NULL;
317  ( *length )++;
318  }
319  }
320  }
321  // Set start to next element.
322  token = &buffer[index + 1];
323  // Everything after comment ignore.
324  if ( c == '#' ) {
325  break;
326  }
327  }
328  // Skip to the next entry.
329  index++;
330  } while ( buffer[index] != '\0' && buffer[index] != '#' );
331  }
332  if ( buffer != NULL ) {
333  free ( buffer );
334  }
335  if ( fclose ( fd ) != 0 ) {
336  g_warning ( "Failed to close hosts file: '%s'", g_strerror ( errno ) );
337  }
338  }
339 
340  return retv;
341 }
342 
343 static void add_known_hosts_file ( SSHModePrivateData *pd, const char *token )
344 {
345  GList *item = g_list_find_custom ( pd->user_known_hosts, token, (GCompareFunc) g_strcmp0 );
346  if ( item == NULL ) {
347  g_debug ( "Add '%s' to UserKnownHost list", token );
348  pd->user_known_hosts = g_list_append ( pd->user_known_hosts, g_strdup ( token ) );
349  }
350  else {
351  g_debug ( "File '%s' already in UserKnownHostsFile list", token );
352  }
353 }
354 
355 static void parse_ssh_config_file ( SSHModePrivateData *pd, const char *filename, SshEntry **retv, unsigned int *length, unsigned int num_favorites )
356 {
357  FILE *fd = fopen ( filename, "r" );
358 
359  g_debug ( "Parsing ssh config file: %s", filename );
360  if ( fd != NULL ) {
361  char *buffer = NULL;
362  size_t buffer_length = 0;
363  char *strtok_pointer = NULL;
364  while ( getline ( &buffer, &buffer_length, fd ) > 0 ) {
365  // Each line is either empty, a comment line starting with a '#'
366  // character or of the form "keyword [=] arguments", where there may
367  // be multiple (possibly quoted) arguments separated by whitespace.
368  // The keyword is separated from its arguments by whitespace OR by
369  // optional whitespace and a '=' character.
370  char *token = strtok_r ( buffer, SSH_TOKEN_DELIM, &strtok_pointer );
371  // Skip empty lines and comment lines. Also skip lines where the
372  // keyword is not "Host".
373  if ( !token || *token == '#' ) {
374  continue;
375  }
376  char *low_token = g_ascii_strdown ( token, -1 );
377  if ( g_strcmp0 ( low_token, "include" ) == 0 ) {
378  token = strtok_r ( NULL, SSH_TOKEN_DELIM, &strtok_pointer );
379  g_debug ( "Found Include: %s", token );
380  gchar *path = rofi_expand_path ( token );
381  gchar *full_path = NULL;
382  if ( !g_path_is_absolute ( path ) ) {
383  char *dirname = g_path_get_dirname ( filename );
384  full_path = g_build_filename ( dirname, path, NULL );
385  g_free ( dirname );
386  }
387  else {
388  full_path = g_strdup ( path );
389  }
390  glob_t globbuf = { .gl_pathc = 0, .gl_pathv = NULL, .gl_offs = 0 };
391 
392  if ( glob ( full_path, 0, NULL, &globbuf ) == 0 ) {
393  for ( size_t iter = 0; iter < globbuf.gl_pathc; iter++ ) {
394  parse_ssh_config_file ( pd, globbuf.gl_pathv[iter], retv, length, num_favorites );
395  }
396  }
397  globfree ( &globbuf );
398 
399  g_free ( full_path );
400  g_free ( path );
401  }
402  else if ( g_strcmp0 ( low_token, "userknownhostsfile" ) == 0 ) {
403  while ( ( token = strtok_r ( NULL, SSH_TOKEN_DELIM, &strtok_pointer ) ) ) {
404  g_debug ( "Found extra UserKnownHostsFile: %s", token );
405  add_known_hosts_file ( pd, token );
406  }
407  }
408  else if ( g_strcmp0 ( low_token, "host" ) == 0 ) {
409  // Now we know that this is a "Host" line.
410  // The "Host" keyword is followed by one more host names separated
411  // by whitespace; while host names may be quoted with double quotes
412  // to represent host names containing spaces, we don't support this
413  // (how many host names contain spaces?).
414  while ( ( token = strtok_r ( NULL, SSH_TOKEN_DELIM, &strtok_pointer ) ) ) {
415  // We do not want to show wildcard entries, as you cannot ssh to them.
416  const char *const sep = "*?";
417  if ( *token == '!' || strpbrk ( token, sep ) ) {
418  continue;
419  }
420 
421  // If comment, skip from now on.
422  if ( *token == '#' ) {
423  break;
424  }
425 
426  // Is this host name already in the history file?
427  // This is a nice little penalty, but doable? time will tell.
428  // given num_favorites is max 25.
429  int found = 0;
430  for ( unsigned int j = 0; j < num_favorites; j++ ) {
431  if ( !g_ascii_strcasecmp ( token, ( *retv )[j].hostname ) ) {
432  found = 1;
433  break;
434  }
435  }
436 
437  if ( found ) {
438  continue;
439  }
440 
441  // Add this host name to the list.
442  ( *retv ) = g_realloc ( ( *retv ), ( ( *length ) + 2 ) * sizeof ( SshEntry ) );
443  ( *retv )[( *length )].hostname = g_strdup ( token );
444  ( *retv )[( *length )].port = 0;
445  ( *retv )[( *length ) + 1].hostname = NULL;
446  ( *length )++;
447  }
448  }
449  g_free ( low_token );
450  }
451  if ( buffer != NULL ) {
452  free ( buffer );
453  }
454 
455  if ( fclose ( fd ) != 0 ) {
456  g_warning ( "Failed to close ssh configuration file: '%s'", g_strerror ( errno ) );
457  }
458  }
459 }
460 
469 static SshEntry * get_ssh ( SSHModePrivateData *pd, unsigned int *length )
470 {
471  SshEntry *retv = NULL;
472  unsigned int num_favorites = 0;
473  char *path;
474 
475  if ( g_get_home_dir () == NULL ) {
476  return NULL;
477  }
478 
479  path = g_build_filename ( cache_dir, SSH_CACHE_FILE, NULL );
480  char **h = history_get_list ( path, length );
481 
482  retv = malloc ( ( *length ) * sizeof ( SshEntry ) );
483  for ( unsigned int i = 0; i < ( *length ); i++ ) {
484  int port = 0;
485  char *portstr = strchr ( h[i], '\x1F' );
486  if ( portstr != NULL ) {
487  *portstr = '\0';
488  errno = 0;
489  gchar *endptr = NULL;
490  gint64 number = g_ascii_strtoll ( &( portstr[1] ), &endptr, 10 );
491  if ( errno != 0 ) {
492  g_warning ( "Failed to parse port number: %s.", &( portstr[1] ) );
493  }
494  else if ( endptr == &( portstr[1] ) ) {
495  g_warning ( "Failed to parse port number: %s, invalid number.", &( portstr[1] ) );
496  }
497  else if ( number < 0 || number > 65535 ) {
498  g_warning ( "Failed to parse port number: %s, out of range.", &( portstr[1] ) );
499  }
500  else {
501  port = number;
502  }
503  }
504  retv[i].hostname = h[i];
505  retv[i].port = port;
506  }
507  g_free ( h );
508 
509  g_free ( path );
510  num_favorites = ( *length );
511 
512  const char *hd = g_get_home_dir ();
513  path = g_build_filename ( hd, ".ssh", "config", NULL );
514  parse_ssh_config_file ( pd, path, &retv, length, num_favorites );
515 
516  if ( config.parse_known_hosts == TRUE ) {
517  char *path = g_build_filename ( g_get_home_dir (), ".ssh", "known_hosts", NULL );
518  retv = read_known_hosts_file ( path, retv, length );
519  g_free ( path );
520  for ( GList *iter = g_list_first ( pd->user_known_hosts ); iter; iter = g_list_next ( iter ) ) {
521  char *path = rofi_expand_path ( (const char *) iter->data );
522  retv = read_known_hosts_file ( (const char *) path, retv, length );
523  g_free ( path );
524  }
525  }
526  if ( config.parse_hosts == TRUE ) {
527  retv = read_hosts_file ( retv, length );
528  }
529 
530  g_free ( path );
531 
532  return retv;
533 }
534 
541 static int ssh_mode_init ( Mode *sw )
542 {
543  if ( mode_get_private_data ( sw ) == NULL ) {
544  SSHModePrivateData *pd = g_malloc0 ( sizeof ( *pd ) );
545  mode_set_private_data ( sw, (void *) pd );
546  pd->hosts_list = get_ssh ( pd, &( pd->hosts_list_length ) );
547  }
548  return TRUE;
549 }
550 
558 static unsigned int ssh_mode_get_num_entries ( const Mode *sw )
559 {
560  const SSHModePrivateData *rmpd = (const SSHModePrivateData *) mode_get_private_data ( sw );
561  return rmpd->hosts_list_length;
562 }
568 static void ssh_mode_destroy ( Mode *sw )
569 {
571  if ( rmpd != NULL ) {
572  for ( unsigned int i = 0; i < rmpd->hosts_list_length; i++ ) {
573  g_free ( rmpd->hosts_list[i].hostname );
574  }
575  g_list_free_full ( rmpd->user_known_hosts, g_free );
576  g_free ( rmpd->hosts_list );
577  g_free ( rmpd );
578  mode_set_private_data ( sw, NULL );
579  }
580 }
581 
592 static ModeMode ssh_mode_result ( Mode *sw, int mretv, char **input, unsigned int selected_line )
593 {
594  ModeMode retv = MODE_EXIT;
596  if ( ( mretv & MENU_OK ) && rmpd->hosts_list[selected_line].hostname != NULL ) {
597  exec_ssh ( &( rmpd->hosts_list[selected_line] ) );
598  }
599  else if ( ( mretv & MENU_CUSTOM_INPUT ) && *input != NULL && *input[0] != '\0' ) {
600  SshEntry entry = { .hostname = *input, .port = 0 };
601  exec_ssh ( &entry );
602  }
603  else if ( ( mretv & MENU_ENTRY_DELETE ) && rmpd->hosts_list[selected_line].hostname ) {
604  delete_ssh ( rmpd->hosts_list[selected_line].hostname );
605  // Stay
606  retv = RELOAD_DIALOG;
607  ssh_mode_destroy ( sw );
608  ssh_mode_init ( sw );
609  }
610  return retv;
611 }
612 
625 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 )
626 {
628  return get_entry ? g_strdup ( rmpd->hosts_list[selected_line].hostname ) : NULL;
629 }
630 
640 static int ssh_token_match ( const Mode *sw, rofi_int_matcher **tokens, unsigned int index )
641 {
643  return helper_token_match ( tokens, rmpd->hosts_list[index].hostname );
644 }
645 #include "mode-private.h"
647 {
648  .name = "ssh",
649  .cfg_name_key = "display-ssh",
650  ._init = ssh_mode_init,
651  ._get_num_entries = ssh_mode_get_num_entries,
652  ._result = ssh_mode_result,
653  ._destroy = ssh_mode_destroy,
654  ._token_match = ssh_token_match,
655  ._get_display_value = _get_display_value,
656  ._get_completion = NULL,
657  ._preprocess_input = NULL,
658  .private_data = NULL,
659  .free = NULL
660 };
history.h
cache_dir
const char * cache_dir
Definition: rofi.c:84
ssh_mode_get_num_entries
static unsigned int ssh_mode_get_num_entries(const Mode *sw)
Definition: ssh.c:558
rofi_mode::name
char * name
Definition: mode-private.h:156
settings.h
parse_ssh_config_file
static void parse_ssh_config_file(SSHModePrivateData *pd, const char *filename, SshEntry **retv, unsigned int *length, unsigned int num_favorites)
Definition: ssh.c:355
SshEntry
struct _SshEntry SshEntry
SSHModePrivateData::hosts_list_length
unsigned int hosts_list_length
Definition: ssh.c:78
RofiHelperExecuteContext::name
const gchar * name
Definition: helper.h:269
MENU_OK
@ MENU_OK
Definition: mode.h:69
SSHModePrivateData::user_known_hosts
GList * user_known_hosts
Definition: ssh.c:74
rofi_int_matcher_t
Definition: rofi-types.h:269
read_known_hosts_file
static SshEntry * read_known_hosts_file(const char *path, SshEntry *retv, unsigned int *length)
Definition: ssh.c:180
mode_set_private_data
void mode_set_private_data(Mode *mode, void *pd)
Definition: mode.c:145
RofiHelperExecuteContext
Definition: helper.h:267
ssh_mode_destroy
static void ssh_mode_destroy(Mode *sw)
Definition: ssh.c:568
execshssh
static int execshssh(const SshEntry *entry)
Definition: ssh.c:99
_SshEntry::hostname
char * hostname
Definition: ssh.c:65
ssh_mode_init
static int ssh_mode_init(Mode *sw)
Definition: ssh.c:541
exec_ssh
static void exec_ssh(const SshEntry *entry)
Definition: ssh.c:131
helper_parse_setup
int helper_parse_setup(char *string, char ***output, int *length,...)
Definition: helper.c:81
delete_ssh
static void delete_ssh(const char *host)
Definition: ssh.c:161
mode-private.h
helper_execute
gboolean helper_execute(const char *wd, char **args, const char *error_precmd, const char *error_cmd, RofiHelperExecuteContext *context)
Definition: helper.c:1017
Settings::parse_hosts
unsigned int parse_hosts
Definition: settings.h:147
MODE_EXIT
@ MODE_EXIT
Definition: mode.h:52
_SshEntry::port
int port
Definition: ssh.c:67
MENU_ENTRY_DELETE
@ MENU_ENTRY_DELETE
Definition: mode.h:77
SSHModePrivateData
Definition: ssh.c:73
_SshEntry
Definition: ssh.c:63
Settings::ssh_command
char * ssh_command
Definition: settings.h:88
rofi_expand_path
char * rofi_expand_path(const char *input)
Definition: helper.c:711
rofi_mode
Definition: mode-private.h:152
rofi.h
ssh_mode
Mode ssh_mode
Definition: ssh.c:646
read_hosts_file
static SshEntry * read_hosts_file(SshEntry *retv, unsigned int *length)
Definition: ssh.c:276
SSHModePrivateData::hosts_list
SshEntry * hosts_list
Definition: ssh.c:76
history_set
void history_set(const char *filename, const char *entry)
Definition: history.c:178
history_get_list
char ** history_get_list(const char *filename, unsigned int *length)
Definition: history.c:325
SSH_TOKEN_DELIM
#define SSH_TOKEN_DELIM
Definition: ssh.c:90
get_ssh
static SshEntry * get_ssh(SSHModePrivateData *pd, unsigned int *length)
Definition: ssh.c:469
MENU_CUSTOM_INPUT
@ MENU_CUSTOM_INPUT
Definition: mode.h:75
Settings::parse_known_hosts
unsigned int parse_known_hosts
Definition: settings.h:149
mode_get_private_data
void * mode_get_private_data(const Mode *mode)
Definition: mode.c:139
add_known_hosts_file
static void add_known_hosts_file(SSHModePrivateData *pd, const char *token)
Definition: ssh.c:343
ssh_token_match
static int ssh_token_match(const Mode *sw, rofi_int_matcher **tokens, unsigned int index)
Definition: ssh.c:640
helper_token_match
int helper_token_match(rofi_int_matcher *const *tokens, const char *input)
Definition: helper.c:488
ModeMode
ModeMode
Definition: mode.h:50
RELOAD_DIALOG
@ RELOAD_DIALOG
Definition: mode.h:56
helper.h
ssh_mode_result
static ModeMode ssh_mode_result(Mode *sw, int mretv, char **input, unsigned int selected_line)
Definition: ssh.c:592
ssh.h
config
Settings config
history_remove
void history_remove(const char *filename, const char *entry)
Definition: history.c:259
_get_display_value
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:625
SSH_CACHE_FILE
#define SSH_CACHE_FILE
Definition: ssh.c:84