/* ssh-base.c
 * ssh-base has base utility functions to connect to hosts via ssh
 *
 * Copyright 2016, Dario Lombardo
 *
 * Wireshark - Network traffic analyzer
 * By Gerald Combs <gerald@wireshark.org>
 * Copyright 1998 Gerald Combs
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

#include "config.h"
#define WS_LOG_DOMAIN LOG_DOMAIN_EXTCAP

#include "ssh-base.h"

#include <extcap/extcap-base.h>
#include <string.h>
#include <libssh/callbacks.h>
#include <ws_attributes.h>
#include <wsutil/wslog.h>

/*
 * The unreleased 0.11.0 version of libssh has the ability to
 * add algorithms to the default supported list by prepending
 * "+" to the configuration list. For older versions, we have
 * to specify all the algorithms we want, but as long as at
 * least one succeeds the command won't fail. (That means that
 * it's possible that we won't actually add support for SHA-1,
 * say if it's running on a system in FIPS mode. We could parse
 * the returned list to check.)
 */
#if LIBSSH_VERSION_INT >= SSH_VERSION_INT(0,11,0)
#define HOSTKEYS_SHA1 "+ssh-rsa"
#define KEY_EXCHANGE_SHA1 "+diffie-hellman-group14-sha1,diffie-hellman-group1-sha1,diffie-hellman-group-exchange-sha1"
#define HMAC_SHA1 "+hmac-sha1-etm@openssh.com,hmac-sha1"
#else
#define HOSTKEYS_SHA1 \
	"ssh-ed25519," \
	"ecdsa-sha2-nistp521," \
	"ecdsa-sha2-nistp384," \
	"ecdsa-sha2-nistp256," \
	"sk-ssh-ed25519@openssh.com," \
	"sk-ecdsa-sha2-nistp256@openssh.com," \
	"rsa-sha2-512," \
	"rsa-sha2-256," \
	"ssh-rsa"
#define KEY_EXCHANGE_SHA1 \
	"curve25519-sha256,curve25519-sha256@libssh.org," \
	"ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521," \
	"diffie-hellman-group18-sha512,diffie-hellman-group16-sha512," \
	"diffie-hellman-group-exchange-sha256," \
	"diffie-hellman-group14-sha256," \
	"diffie-hellman-group-exchange-sha1," \
	"diffie-hellman-group14-sha1,diffie-hellman-group1-sha1"
#define HMAC_SHA1 \
	"hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com," \
	"hmac-sha2-256,hmac-sha2-512," \
	"hmac-sha1-etm@openssh.com,hmac-sha1"
#endif

static void extcap_log(int priority, const char *function, const char *buffer, void *userdata _U_)
{
	enum ws_log_level level = LOG_LEVEL_DEBUG;
	switch (priority) {
	case SSH_LOG_TRACE:
		level = LOG_LEVEL_NOISY;
		break;
	case SSH_LOG_DEBUG:
		level = LOG_LEVEL_DEBUG;
		break;
	case SSH_LOG_INFO:
		level = LOG_LEVEL_INFO;
		break;
	case SSH_LOG_WARN:
	default:
	/* Prior to 0.11.0 libssh prints far too much at SSH_LOG_WARN,
	 * including merely informational messages.
	 * Lower them to LOG_LEVEL_INFO, which won't get shown in the GUI
	 * and aren't shown by default. (Anything INFO and below goes to
	 * stdout due to ws_log_console_writer_set_use_stdout in extcap-base.c)
	 * After the following commit libssh only uses LOG_LEVEL_WARN for
	 * serious issues:
	 * https://gitlab.com/libssh/libssh-mirror/-/commit/657d9143d121dfff74f5a63f734d0096c7f37194
	 *
	 * In 0.11.4 libssh added a common "ssh_strict_fopen" function that
	 * is used to open various configuration files. Unfortunately, it
	 * issues SSH_LOG_WARN (aka RARE) level logs for any errors, including
	 * a file not existing. Some files, like known_hosts, are optional
	 * and so it's harmless if they're missing. This was fixed in 0.12.1[1],
	 * but before then we downgrade just the WARN level log messages from
	 * ssh_strict_open. Since we only downgrade messages from one function,
	 * and all the logs in that function are low level in 0.12.1 and later,
	 * we can lower them even more than the WARN mesages pre 0.11.0, which
	 * were changed to a wider variety of levels upstream. We don't lower
	 * all the way to NOISY because the same change added a WARN in the
	 * parent function for problems opening files recursively included by
	 * a main config file. Also see #21051.
	 *
	 * 1 - https://gitlab.com/libssh/libssh-mirror/-/commit/a7fd80795e21b8c894b54409496ea6b569f7f4a3
	 */
#if LIBSSH_VERSION_INT < SSH_VERSION_INT(0,11,0)
		level = LOG_LEVEL_INFO;
#elif LIBSSH_VERSION_INT < SSH_VERSION_INT(0,12,1)
		if (strncmp(function, "ssh_strict_fopen", strlen("ssh_strict_fopen")) == 0) {
			level = LOG_LEVEL_DEBUG;
		} else {
			level = LOG_LEVEL_WARNING;
		}
#else
		level = LOG_LEVEL_WARNING;
#endif
		break;
	}
	/* We set the libssh log level to specifically ask for this, so don't
	 * both checking the log level a second time.
	 */
	ws_log_write_always_full("libssh", level, NULL, 0, function, "%s", buffer);
}

void add_libssh_info(extcap_parameters * extcap_conf)
{
	extcap_base_set_compiled_with(extcap_conf, "libssh version %s", SSH_STRINGIFY(LIBSSH_VERSION));
	extcap_base_set_running_with(extcap_conf, "libssh version %s", ssh_version(0));
}

ssh_session create_ssh_connection(const ssh_params_t* ssh_params, char** err_info)
{
	ssh_session sshs;
	char* username = NULL;
	unsigned port;

	/* Open session and set options */
	sshs = ssh_new();
	if (sshs == NULL) {
		*err_info = g_strdup("Can't create ssh session");
		return NULL;
	}

	if (!ssh_params->host) {
		*err_info = g_strdup("Hostname needed");
		goto failure;
	}

	ssh_options_set(sshs, SSH_OPTIONS_LOG_VERBOSITY, &ssh_params->debug);
	ssh_set_log_callback(extcap_log);

	if (ssh_options_set(sshs, SSH_OPTIONS_HOST, ssh_params->host)) {
		*err_info = ws_strdup_printf("Can't set the host: %s", ssh_params->host);
		goto failure;
	}

	/* Load the configurations already present in the system configuration file. */
	/* They will be overwritten by the user-provided configurations. */
#ifdef _WIN32
	/* libssh < 0.11.4 tries to read C:\etc\ssh\ssh_config by default
	 * on Windows (i.e., if the second parameter is NULL.), and later
	 * versions try to read from %PROGRAMDATA%\ssh\ssh_config. Neither
	 * of those is a good choice for us, since anyone can create them
	 * if they don't exist. Just look for %HOME%\.ssh\config instead.
	 *
	 * libssh < 0.12.0 treats %d as "%HOME%\.ssh" (or "~/.ssh" on UN*X),
	 * which doesn't match ssh_config(5) or openssh. Fixed in
	 * https://gitlab.com/libssh/libssh-mirror/-/commit/ce0b616bc648e4d510bf8ea20af5572861b412a0
	 * https://gitlab.com/libssh/libssh-mirror/-/work_items/349
	 *
	 * We have to check the runtime version here.
	 */
	const char *user_config;
	if (NULL == ssh_version(SSH_VERSION_INT(0,12,0))) {
		user_config = "%d/config";
	} else {
		user_config = "%d/.ssh/config";
	}
	if (ssh_options_parse_config(sshs, user_config) != 0) {
#else
	if (ssh_options_parse_config(sshs, NULL) != 0) {
#endif
		*err_info = g_strdup("Unable to load the configuration file");
		goto failure;
	}

	if (ssh_params->ssh_sha1) {
		if (ssh_options_set(sshs, SSH_OPTIONS_HOSTKEYS, HOSTKEYS_SHA1)) {
			*err_info = ws_strdup_printf("Can't set host keys to allow SHA-1.");
			goto failure;
		}
		if (ssh_options_set(sshs, SSH_OPTIONS_PUBLICKEY_ACCEPTED_TYPES, HOSTKEYS_SHA1)) {
			*err_info = ws_strdup_printf("Can't set public key algorithms to allow SSH-RSA (SHA-1).");
			goto failure;
		}
		if (ssh_options_set(sshs, SSH_OPTIONS_KEY_EXCHANGE, KEY_EXCHANGE_SHA1)) {
			*err_info = ws_strdup_printf("Can't set key exchange methods to allow SHA-1.");
			goto failure;
		}
		if (ssh_options_set(sshs, SSH_OPTIONS_HMAC_C_S, HMAC_SHA1)) {
			*err_info = ws_strdup_printf("Can't set MAC client to server algorithms to allow SHA-1.");
			goto failure;
		}
		if (ssh_options_set(sshs, SSH_OPTIONS_HMAC_S_C, HMAC_SHA1)) {
			*err_info = ws_strdup_printf("Can't set MAC server to client algorithms to allow SHA-1.");
			goto failure;
		}
	}

	if (ssh_params->port != 0) {
		port = ssh_params->port;
		if (ssh_options_set(sshs, SSH_OPTIONS_PORT, &port)) {
			*err_info = ws_strdup_printf("Can't set the port: %u", port);
			goto failure;
		}
	}

	if (ssh_params->proxycommand) {
#if defined(_WIN32) && (LIBSSH_VERSION_INT < SSH_VERSION_INT(0,11,0))
		// ProxyCommand isn't supported on Windows:
		// https://gitlab.com/libssh/libssh-mirror/-/issues/75
		// ssh_options_set "succeeds" in setting it, though.
		// On 0.11.0 and later, ssh_connect will fail.
		// Before 0.11.0, libssh silently ignores the option.
		// https://gitlab.com/libssh/libssh-mirror/-/commit/bed4438695df2f4635617fc45f802d782fdd8479
		*err_info = ws_strdup_printf("Can't set the ProxyCommand: %s (libssh does not support proxy commands on Windows.)", ssh_params->proxycommand);
		goto failure;
#endif
		if (ssh_options_set(sshs, SSH_OPTIONS_PROXYCOMMAND, ssh_params->proxycommand)) {
			*err_info = ws_strdup_printf("Can't set the ProxyCommand: %s", ssh_params->proxycommand);
			goto failure;
		}
	}

	if (ssh_params->username) {
		if (ssh_options_set(sshs, SSH_OPTIONS_USER, ssh_params->username)) {
			*err_info = ws_strdup_printf("Can't set the username: %s", ssh_params->username);
			goto failure;
		}
	}

	ssh_options_get(sshs, SSH_OPTIONS_USER, &username);
	ssh_options_get_port(sshs, &port);

	ws_log(LOG_DOMAIN_CAPCHILD, LOG_LEVEL_INFO, "Opening ssh connection to %s@%s:%u", username,
		ssh_params->host, port);

	ssh_string_free_char(username);

	/* Connect to server */
	if (ssh_connect(sshs) != SSH_OK) {
		*err_info = ws_strdup_printf("Connection error: %s", ssh_get_error(sshs));
		goto failure;
	}

	/* If a public key path has been provided, try to authenticate using it */
	if (ssh_params->sshkey_path) {
		ssh_key pkey = ssh_key_new();
		int ret;

		ws_info("Connecting using public key in %s...", ssh_params->sshkey_path);
		ret = ssh_pki_import_privkey_file(ssh_params->sshkey_path, ssh_params->sshkey_passphrase, NULL, NULL, &pkey);

		switch (ret) {

		case SSH_OK:
			if (ssh_userauth_publickey(sshs, NULL, pkey) == SSH_AUTH_SUCCESS) {
				ws_info("done");
				ssh_key_free(pkey);
				return sshs;
			}
			ws_info("failed (%s)", ssh_get_error(sshs));
			break;
		case SSH_EOF:
			ws_warning("Error importing key from %s. File doesn't exist or permission denied.",
				ssh_params->sshkey_path);
			break;
		case SSH_ERROR:
			/* Unfortunately we can't call ssh_get_error() on the
			 * key to determine why import failed.
			 */
			ws_warning("Error importing key from %s. Make sure it is a valid"
				" private key file and any necessary passphrase is configured.",
				ssh_params->sshkey_path);
			break;
		default:
			ws_warning("Unknown error from ssh_pki_import_privkey_file");
		}
		ssh_key_free(pkey);
	}

	/* Workaround: it may happen that libssh closes socket in meantime and any next ssh_ call fails so we should detect it in advance */
	if (ssh_get_fd(sshs) != (socket_t)-1) {
		/* If a password has been provided and all previous attempts failed, try to use it */
		if (ssh_params->password) {
			ws_info("Connecting using password...");
			if (ssh_userauth_password(sshs, ssh_params->username, ssh_params->password) == SSH_AUTH_SUCCESS) {
				ws_info("done");
				return sshs;
			}
			ws_info("failed");
		}
	} else {
		ws_info("ssh connection closed before password authentication");
	}

	/* Workaround: it may happen that libssh closes socket in meantime and any next ssh_ call fails so we should detect it in advance */
	if (ssh_get_fd(sshs) != (socket_t)-1) {
		/* Try to authenticate using standard public key */
		ws_info("Connecting using standard public key...");
		if (ssh_userauth_publickey_auto(sshs, NULL, NULL) == SSH_AUTH_SUCCESS) {
			ws_info("done");
			return sshs;
		}
		ws_info("failed");
	} else {
		ws_info("ssh connection closed before public key authentication");
	}

	*err_info = ws_strdup_printf("Can't find a valid authentication. Disconnecting.");

	/* All authentication failed. Disconnect and return */
	ssh_disconnect(sshs);

failure:
	ssh_free(sshs);
	return NULL;
}

int ssh_channel_printf(ssh_channel channel, const char* fmt, ...)
{
	char* buf;
	va_list arg;
	int ret = EXIT_SUCCESS;

	va_start(arg, fmt);
	buf = ws_strdup_vprintf(fmt, arg);
	ws_debug("%s", buf);
	if (ssh_channel_write(channel, buf, (uint32_t)strlen(buf)) == SSH_ERROR)
		ret = EXIT_FAILURE;
	va_end(arg);
	g_free(buf);

	return ret;
}

void ssh_cleanup(ssh_session* sshs, ssh_channel* channel)
{
	if (*channel) {
		ssh_channel_send_eof(*channel);
		ssh_channel_close(*channel);
		ssh_channel_free(*channel);
		*channel = NULL;
	}

	if (*sshs) {
		ssh_disconnect(*sshs);
		ssh_free(*sshs);
		*sshs = NULL;
	}
}

ssh_params_t* ssh_params_new(void)
{
	return g_new0(ssh_params_t, 1);
}

void ssh_params_free(ssh_params_t* ssh_params)
{
	if (!ssh_params)
		return;
	g_free(ssh_params->host);
	g_free(ssh_params->username);
	g_free(ssh_params->password);
	g_free(ssh_params->sshkey_path);
	g_free(ssh_params->sshkey_passphrase);
	g_free(ssh_params->proxycommand);
	g_free(ssh_params);
}

void ssh_params_set_log_level(ssh_params_t* ssh_params, enum ws_log_level level)
{
	switch (level) {
	case LOG_LEVEL_NOISY:
		ssh_params->debug = SSH_LOG_TRACE;
		break;
	case LOG_LEVEL_DEBUG:
		ssh_params->debug = SSH_LOG_DEBUG;
		break;
	case LOG_LEVEL_INFO:
		ssh_params->debug = SSH_LOG_INFO;
		break;
	default:
		ssh_params->debug = SSH_LOG_WARN;
	}
}

/*
 * Editor modelines  -  https://www.wireshark.org/tools/modelines.html
 *
 * Local variables:
 * c-basic-offset: 8
 * tab-width: 8
 * indent-tabs-mode: t
 * End:
 *
 * vi: set shiftwidth=8 tabstop=8 noexpandtab:
 * :indentSize=8:tabSize=8:noTabs=false:
 */