summaryrefslogtreecommitdiffstats
path: root/source/modes/ssh.c
diff options
context:
space:
mode:
authorDave Davenport <qball@blame.services>2022-02-23 23:18:53 +0100
committerDave Davenport <qball@blame.services>2022-02-23 23:18:53 +0100
commit534aa6ad5439809f46b0fece92c7923dcd2b9e8f (patch)
tree4ab2708087c8da50108c688a60d398f5b6ea7f31 /source/modes/ssh.c
parent32d33ade16c3b962b4957c37edf114476cdd22ce (diff)
Rename Dialogs -> Modes
Try to fix some of old syntax.
Diffstat (limited to 'source/modes/ssh.c')
-rw-r--r--source/modes/ssh.c649
1 files changed, 649 insertions, 0 deletions
diff --git a/source/modes/ssh.c b/source/modes/ssh.c
new file mode 100644
index 00000000..261e5c3c
--- /dev/null
+++ b/source/modes/ssh.c
@@ -0,0 +1,649 @@
+/*
+ * rofi
+ *
+ * MIT/X11 License
+ * Copyright © 2013-2022 Qball Cow <qball@gmpclient.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ */
+
+/**
+ * \ingroup SSHMode
+ * @{
+ */
+
+/**
+ * Log domain for the ssh mode.
+ */
+#define G_LOG_DOMAIN "Modes.Ssh"
+
+#include <config.h>
+#include <glib.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <ctype.h>
+#include <dirent.h>
+#include <errno.h>
+#include <glob.h>
+#include <helper.h>
+#include <signal.h>
+#include <string.h>
+#include <strings.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "modes/ssh.h"
+#include "history.h"
+#include "rofi.h"
+#include "settings.h"
+
+/**
+ * Holding an ssh entry.
+ */
+typedef struct _SshEntry {
+ /** SSH hostname */
+ char *hostname;
+ /** SSH port number */
+ int port;
+} SshEntry;
+/**
+ * The internal data structure holding the private data of the SSH Mode.
+ */
+typedef struct {
+ GList *user_known_hosts;
+ /** List if available ssh hosts.*/
+ SshEntry *hosts_list;
+ /** Length of the #hosts_list.*/
+ unsigned int hosts_list_length;
+} SSHModePrivateData;
+
+/**
+ * Name of the history file where previously chosen hosts are stored.
+ */
+#define SSH_CACHE_FILE "rofi-2.sshcache"
+
+/**
+ * Used in get_ssh() when splitting lines from the user's
+ * SSH config file into tokens.
+ */
+#define SSH_TOKEN_DELIM "= \t\r\n"
+
+/**
+ * @param entry The host to connect too
+ *
+ * SSH into the selected host.
+ *
+ * @returns FALSE On failure, TRUE on success
+ */
+static int execshssh(const SshEntry *entry) {
+ char **args = NULL;
+ int argsv = 0;
+ gchar *portstr = NULL;
+ if (entry->port > 0) {
+ portstr = g_strdup_printf("%d", entry->port);
+ }
+ helper_parse_setup(config.ssh_command, &args, &argsv, "{host}",
+ entry->hostname, "{port}", portstr, (char *)0);
+ g_free(portstr);
+
+ gsize l = strlen("Connecting to '' via rofi") + strlen(entry->hostname) + 1;
+ gchar *desc = g_newa(gchar, l);
+
+ g_snprintf(desc, l, "Connecting to '%s' via rofi", entry->hostname);
+
+ RofiHelperExecuteContext context = {
+ .name = "ssh",
+ .description = desc,
+ .command = "ssh",
+ };
+ return helper_execute(NULL, args, "ssh ", entry->hostname, &context);
+}
+
+/**
+ * @param entry The host to connect too
+ *
+ * SSH into the selected host, if successful update history.
+ */
+static void exec_ssh(const SshEntry *entry) {
+ if (!(entry->hostname) || !(entry->hostname[0])) {
+ return;
+ }
+
+ if (!execshssh(entry)) {
+ return;
+ }
+
+ // This happens in non-critical time (After launching app)
+ // It is allowed to be a bit slower.
+ char *path = g_build_filename(cache_dir, SSH_CACHE_FILE, NULL);
+ // TODO update.
+ if (entry->port > 0) {
+ char *store = g_strdup_printf("%s\x1F%d", entry->hostname, entry->port);
+ history_set(path, store);
+ g_free(store);
+ } else {
+ history_set(path, entry->hostname);
+ }
+ g_free(path);
+}
+
+/**
+ * @param host The host to remove from history
+ *
+ * Remove host from history.
+ */
+static void delete_ssh(const char *host) {
+ if (!host || !host[0]) {
+ return;
+ }
+ char *path = g_build_filename(cache_dir, SSH_CACHE_FILE, NULL);
+ history_remove(path, host);
+ g_free(path);
+}
+
+/**
+ * @param path Path of the known host file.
+ * @param retv list of hosts
+ * @param length pointer to length of list [in][out]
+ *
+ * Read 'known_hosts' file when entries are not hashed.
+ *
+ * @returns updated list of hosts.
+ */
+static SshEntry *read_known_hosts_file(const char *path, SshEntry *retv,
+ unsigned int *length) {
+ FILE *fd = fopen(path, "r");
+ if (fd != NULL) {
+ char *buffer = NULL;
+ size_t buffer_length = 0;
+ // Reading one line per time.
+ while (getline(&buffer, &buffer_length, fd) > 0) {
+ // Strip whitespace.
+ char *start = g_strstrip(&(buffer[0]));
+ // Find start.
+ if (*start == '#' || *start == '@') {
+ // skip comments or cert-authority or revoked items.
+ continue;
+ }
+ if (*start == '|') {
+ // Skip hashed hostnames.
+ continue;
+ }
+ // Find end of hostname set.
+ char *end = strstr(start, " ");
+ if (end == NULL) {
+ // Something is wrong.
+ continue;
+ }
+ *end = '\0';
+ char *sep = start;
+ start = strsep(&sep, ", ");
+ while (start) {
+ int port = 0;
+ if (start[0] == '[') {
+ start++;
+ char *strend = strchr(start, ']');
+ if (strend[1] == ':') {
+ *strend = '\0';
+ errno = 0;
+ gchar *endptr = NULL;
+ gint64 number = g_ascii_strtoll(&(strend[2]), &endptr, 10);
+ if (errno != 0) {
+ g_warning("Failed to parse port number: %s.", &(strend[2]));
+ } else if (endptr == &(strend[2])) {
+ g_warning("Failed to parse port number: %s, invalid number.",
+ &(strend[2]));
+ } else if (number < 0 || number > 65535) {
+ g_warning("Failed to parse port number: %s, out of range.",
+ &(strend[2]));
+ } else {
+ port = number;
+ }
+ }
+ }
+ // Is this host name already in the list?
+ // We often get duplicates in hosts file, so lets check this.
+ int found = 0;
+ for (unsigned int j = 0; j < (*length); j++) {
+ if (!g_ascii_strcasecmp(start, retv[j].hostname)) {
+ found = 1;
+ break;
+ }
+ }
+
+ if (!found) {
+ // Add this host name to the list.
+ retv = g_realloc(retv, ((*length) + 2) * sizeof(SshEntry));
+ retv[(*length)].hostname = g_strdup(start);
+ retv[(*length)].port = port;
+ retv[(*length) + 1].hostname = NULL;
+ retv[(*length) + 1].port = 0;
+ (*length)++;
+ }
+ start = strsep(&sep, ", ");
+ }
+ }
+ if (buffer != NULL) {
+ free(buffer);
+ }
+ if (fclose(fd) != 0) {
+ g_warning("Failed to close hosts file: '%s'", g_strerror(errno));
+ }
+ } else {
+ g_debug("Failed to open KnownHostFile: '%s'", path);
+ }
+
+ return retv;
+}
+
+/**
+ * @param retv The list of hosts to update.
+ * @param length The length of the list retv [in][out]
+ *
+ * Read `/etc/hosts` and appends them to the list retv
+ *
+ * @returns an updated list with the added hosts.
+ */
+static SshEntry *read_hosts_file(SshEntry *retv, unsigned int *length) {
+ // Read the hosts file.
+ FILE *fd = fopen("/etc/hosts", "r");
+ if (fd != NULL) {
+ char *buffer = NULL;
+ size_t buffer_length = 0;
+ // Reading one line per time.
+ while (getline(&buffer, &buffer_length, fd) > 0) {
+ // Evaluate one line.
+ unsigned int index = 0, ti = 0;
+ char *token = buffer;
+
+ // Tokenize it.
+ do {
+ char c = buffer[index];
+ // Break on space, tab, newline and \0.
+ if (c == ' ' || c == '\t' || c == '\n' || c == '\0' || c == '#') {
+ buffer[index] = '\0';
+ // Ignore empty tokens
+ if (token[0] != '\0') {
+ ti++;
+ // and first token.
+ if (ti > 1) {
+ // Is this host name already in the list?
+ // We often get duplicates in hosts file, so lets check this.
+ int found = 0;
+ for (unsigned int j = 0; j < (*length); j++) {
+ if (!g_ascii_strcasecmp(token, retv[j].hostname)) {
+ found = 1;
+ break;
+ }
+ }
+
+ if (!found) {
+ // Add this host name to the list.
+ retv = g_realloc(retv, ((*length) + 2) * sizeof(SshEntry));
+ retv[(*length)].hostname = g_strdup(token);
+ retv[(*length)].port = 0;
+ retv[(*length) + 1].hostname = NULL;
+ (*length)++;
+ }
+ }
+ }
+ // Set start to next element.
+ token = &buffer[index + 1];
+ // Everything after comment ignore.
+ if (c == '#') {
+ break;
+ }
+ }
+ // Skip to the next entry.
+ index++;
+ } while (buffer[index] != '\0' && buffer[index] != '#');
+ }
+ if (buffer != NULL) {
+ free(buffer);
+ }
+ if (fclose(fd) != 0) {
+ g_warning("Failed to close hosts file: '%s'", g_strerror(errno));
+ }
+ }
+
+ return retv;
+}
+
+static void add_known_hosts_file(SSHModePrivateData *pd, const char *token) {
+ GList *item =
+ g_list_find_custom(pd->user_known_hosts, token, (GCompareFunc)g_strcmp0);
+ if (item == NULL) {
+ g_debug("Add '%s' to UserKnownHost list", token);
+ pd->user_known_hosts = g_list_append(pd->user_known_hosts, g_strdup(token));
+ } else {
+ g_debug("File '%s' already in UserKnownHostsFile list", token);
+ }
+}
+
+static void parse_ssh_config_file(SSHModePrivateData *pd, const char *filename,
+ SshEntry **retv, unsigned int *length,
+ unsigned int num_favorites) {
+ FILE *fd = fopen(filename, "r");
+
+ g_debug("Parsing ssh config file: %s", filename);
+ if (fd != NULL) {
+ char *buffer = NULL;
+ size_t buffer_length = 0;
+ char *strtok_pointer = NULL;
+ while (getline(&buffer, &buffer_length, fd) > 0) {
+ // Each line is either empty, a comment line starting with a '#'
+ // character or of the form "keyword [=] arguments", where there may
+ // be multiple (possibly quoted) arguments separated by whitespace.
+ // The keyword is separated from its arguments by whitespace OR by
+ // optional whitespace and a '=' character.
+ char *token = strtok_r(buffer, SSH_TOKEN_DELIM, &strtok_pointer);
+ // Skip empty lines and comment lines. Also skip lines where the
+ // keyword is not "Host".
+ if (!token || *token == '#') {
+ continue;
+ }
+ char *low_token = g_ascii_strdown(token, -1);
+ if (g_strcmp0(low_token, "include") == 0) {
+ token = strtok_r(NULL, SSH_TOKEN_DELIM, &strtok_pointer);
+ g_debug("Found Include: %s", token);
+ gchar *path = rofi_expand_path(token);
+ gchar *full_path = NULL;
+ if (!g_path_is_absolute(path)) {
+ char *dirname = g_path_get_dirname(filename);
+ full_path = g_build_filename(dirname, path, NULL);
+ g_free(dirname);
+ } else {
+ full_path = g_strdup(path);
+ }
+ glob_t globbuf = {.gl_pathc = 0, .gl_pathv = NULL, .gl_offs = 0};
+
+ if (glob(full_path, 0, NULL, &globbuf) == 0) {
+ for (size_t iter = 0; iter < globbuf.gl_pathc; iter++) {
+ parse_ssh_config_file(pd, globbuf.gl_pathv[iter], retv, length,
+ num_favorites);
+ }
+ }
+ globfree(&globbuf);
+
+ g_free(full_path);
+ g_free(path);
+ } else if (g_strcmp0(low_token, "userknownhostsfile") == 0) {
+ while ((token = strtok_r(NULL, SSH_TOKEN_DELIM, &strtok_pointer))) {
+ g_debug("Found extra UserKnownHostsFile: %s", token);
+ add_known_hosts_file(pd, token);
+ }
+ } else if (g_strcmp0(low_token, "host") == 0) {
+ // Now we know that this is a "Host" line.
+ // The "Host" keyword is followed by one more host names separated
+ // by whitespace; while host names may be quoted with double quotes
+ // to represent host names containing spaces, we don't support this
+ // (how many host names contain spaces?).
+ while ((token = strtok_r(NULL, SSH_TOKEN_DELIM, &strtok_pointer))) {
+ // We do not want to show wildcard entries, as you cannot ssh to them.
+ const char *const sep = "*?";
+ if (*token == '!' || strpbrk(token, sep)) {
+ continue;
+ }
+
+ // If comment, skip from now on.
+ if (*token == '#') {
+ break;
+ }
+
+ // Is this host name already in the history file?
+ // This is a nice little penalty, but doable? time will tell.
+ // given num_favorites is max 25.
+ int found = 0;
+ for (unsigned int j = 0; j < num_favorites; j++) {
+ if (!g_ascii_strcasecmp(token, (*retv)[j].hostname)) {
+ found = 1;
+ break;
+ }
+ }
+
+ if (found) {
+ continue;
+ }
+
+ // Add this host name to the list.
+ (*retv) = g_realloc((*retv), ((*length) + 2) * sizeof(SshEntry));
+ (*retv)[(*length)].hostname = g_strdup(token);
+ (*retv)[(*length)].port = 0;
+ (*retv)[(*length) + 1].hostname = NULL;
+ (*length)++;
+ }
+ }
+ g_free(low_token);
+ }
+ if (buffer != NULL) {
+ free(buffer);
+ }
+
+ if (fclose(fd) != 0) {
+ g_warning("Failed to close ssh configuration file: '%s'",
+ g_strerror(errno));
+ }
+ }
+}
+
+/**
+ * @param pd The plugin data handle
+ * @param length The number of found ssh hosts [out]
+ *
+ * Gets the list available SSH hosts.
+ *
+ * @returns an array of strings containing all the hosts.
+ */
+static SshEntry *get_ssh(SSHModePrivateData *pd, unsigned int *length) {
+ SshEntry *retv = NULL;
+ unsigned int num_favorites = 0;
+ char *path;
+
+ if (g_get_home_dir() == NULL) {
+ return NULL;
+ }
+
+ path = g_build_filename(cache_dir, SSH_CACHE_FILE, NULL);
+ char **h = history_get_list(path, length);
+
+ retv = malloc((*length) * sizeof(SshEntry));
+ for (unsigned int i = 0; i < (*length); i++) {
+ int port = 0;
+ char *portstr = strchr(h[i], '\x1F');
+ if (portstr != NULL) {
+ *portstr = '\0';
+ errno = 0;
+ gchar *endptr = NULL;
+ gint64 number = g_ascii_strtoll(&(portstr[1]), &endptr, 10);
+ if (errno != 0) {
+ g_warning("Failed to parse port number: %s.", &(portstr[1]));
+ } else if (endptr == &(portstr[1])) {
+ g_warning("Failed to parse port number: %s, invalid number.",
+ &(portstr[1]));
+ } else if (number < 0 || number > 65535) {
+ g_warning("Failed to parse port number: %s, out of range.",
+ &(portstr[1]));
+ } else {
+ port = number;
+ }
+ }
+ retv[i].hostname = h[i];
+ retv[i].port = port;
+ }
+ g_free(h);
+
+ g_free(path);
+ num_favorites = (*length);
+
+ const char *hd = g_get_home_dir();
+ path = g_build_filename(hd, ".ssh", "config", NULL);
+ parse_ssh_config_file(pd, path, &retv, length, num_favorites);
+
+ if (config.parse_known_hosts == TRUE) {
+ char *known_hosts_path =
+ g_build_filename(g_get_home_dir(), ".ssh", "known_hosts", NULL);
+ retv = read_known_hosts_file(known_hosts_path, retv, length);
+ g_free(known_hosts_path);
+ for (GList *iter = g_list_first(pd->user_known_hosts); iter;
+ iter = g_list_next(iter)) {
+ char *user_known_hosts_path = rofi_expand_path((const char *)iter->data);
+ retv = read_known_hosts_file((const char *)user_known_hosts_path, retv,
+ length);
+ g_free(user_known_hosts_path);
+ }
+ }
+ if (config.parse_hosts == TRUE) {
+ retv = read_hosts_file(retv, length);
+ }
+
+ g_free(path);
+
+ return retv;
+}
+
+/**
+ * @param sw Object handle to the SSH Mode object
+ *
+ * Initializes the SSH Mode private data object and
+ * loads the relevant ssh information.
+ */
+static int ssh_mode_init(Mode *sw) {
+ if (mode_get_private_data(sw) == NULL) {
+ SSHModePrivateData *pd = g_malloc0(sizeof(*pd));
+ mode_set_private_data(sw, (void *)pd);
+ pd->hosts_list = get_ssh(pd, &(pd->hosts_list_length));
+ }
+ return TRUE;
+}
+
+/**
+ * @param sw Object handle to the SSH Mode object
+ *
+ * Get the number of SSH entries.
+ *
+ * @returns the number of ssh entries.
+ */
+static unsigned int ssh_mode_get_num_entries(const Mode *sw) {
+ const SSHModePrivateData *rmpd =
+ (const SSHModePrivateData *)mode_get_private_data(sw);
+ return rmpd->hosts_list_length;
+}
+/**
+ * @param sw Object handle to the SSH Mode object
+ *
+ * Cleanup the SSH Mode. Free all allocated memory and NULL the private data
+ * pointer.
+ */
+static void ssh_mode_destroy(Mode *sw) {
+ SSHModePrivateData *rmpd = (SSHModePrivateData *)mode_get_private_data(sw);
+ if (rmpd != NULL) {
+ for (unsigned int i = 0; i < rmpd->hosts_list_length; i++) {
+ g_free(rmpd->hosts_list[i].hostname);
+ }
+ g_list_free_full(rmpd->user_known_hosts, g_free);
+ g_free(rmpd->hosts_list);
+ g_free(rmpd);
+ mode_set_private_data(sw, NULL);
+ }
+}
+
+/**
+ * @param sw Object handle to the SSH Mode object
+ * @param mretv The menu return value.
+ * @param input Pointer to the user input string.
+ * @param selected_line the line selected by the user.
+ *
+ * Acts on the user interaction.
+ *
+ * @returns the next #ModeMode.
+ */
+static ModeMode ssh_mode_result(Mode *sw, int mretv, char **input,
+ unsigned int selected_line) {
+ ModeMode retv = MODE_EXIT;
+ SSHModePrivateData *rmpd = (SSHModePrivateData *)mode_get_private_data(sw);
+ if ((mretv & MENU_OK) && rmpd->hosts_list[selected_line].hostname != NULL) {
+ exec_ssh(&(rmpd->hosts_list[selected_line]));
+ } else if ((mretv & MENU_CUSTOM_INPUT) && *input != NULL &&
+ *input[0] != '\0') {
+ SshEntry entry = {.hostname = *input, .port = 0};
+ exec_ssh(&entry);
+ } else if ((mretv & MENU_ENTRY_DELETE) &&
+ rmpd->hosts_list[selected_line].hostname) {
+ delete_ssh(rmpd->hosts_list[selected_line].hostname);
+ // Stay
+ retv = RELOAD_DIALOG;
+ ssh_mode_destroy(sw);
+ ssh_mode_init(sw);
+ } else if (mretv & MENU_CUSTOM_COMMAND) {
+ retv = (mretv & MENU_LOWER_MASK);
+ }
+ return retv;
+}
+
+/**
+ * @param sw Object handle to the SSH Mode object
+ * @param selected_line The line to view
+ * @param state The state of the entry [out]
+ * @param attr_list List of extra rendering attributes to set [out]
+ * @param get_entry
+ *
+ * Gets the string as it should be displayed and the display state.
+ * If get_entry is FALSE only the state is set.
+ *
+ * @return the string as it should be displayed and the display state.
+ */
+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) {
+ SSHModePrivateData *rmpd = (SSHModePrivateData *)mode_get_private_data(sw);
+ return get_entry ? g_strdup(rmpd->hosts_list[selected_line].hostname) : NULL;
+}
+
+/**
+ * @param sw Object handle to the SSH Mode object
+ * @param tokens The set of tokens to match against
+ * @param index The index of the entry to match
+ *
+ * Match entry against the set of tokens.
+ *
+ * @returns TRUE if matches
+ */
+static int ssh_token_match(const Mode *sw, rofi_int_matcher **tokens,
+ unsigned int index) {
+ SSHModePrivateData *rmpd = (SSHModePrivateData *)mode_get_private_data(sw);
+ return helper_token_match(tokens, rmpd->hosts_list[index].hostname);
+}
+#include "mode-private.h"
+Mode ssh_mode = {.name = "ssh",
+ .cfg_name_key = "display-ssh",
+ ._init = ssh_mode_init,
+ ._get_num_entries = ssh_mode_get_num_entries,
+ ._result = ssh_mode_result,
+ ._destroy = ssh_mode_destroy,
+ ._token_match = ssh_token_match,
+ ._get_display_value = _get_display_value,
+ ._get_completion = NULL,
+ ._preprocess_input = NULL,
+ .private_data = NULL,
+ .free = NULL};
+/**@}*/