/*
 * This file is part of RTRlib.
 *
 * This file is subject to the terms and conditions of the MIT license.
 * See the file LICENSE in the top level directory for more details.
 *
 * Website: http://rtrlib.realmv6.org/
 */

#include "ssh_transport_private.h"

#include "rtrlib/lib/alloc_utils_private.h"
#include "rtrlib/lib/log_private.h"
#include "rtrlib/lib/utils_private.h"
#include "rtrlib/rtrlib_export_private.h"
#include "rtrlib/transport/transport_private.h"

#include <assert.h>
#include <errno.h>
#include <libssh/libssh.h>
#include <pthread.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>

#define SSH_DBG(fmt, sock, ...)                                                                                     \
	do {                                                                                                        \
		const struct tr_ssh_socket *tmp = sock;                                                             \
		lrtr_dbg("SSH Transport(%s@%s:%u): " fmt, tmp->config.username, tmp->config.host, tmp->config.port, \
			 ##__VA_ARGS__);                                                                            \
	} while (0)
#define SSH_DBG1(a, sock) SSH_DBG(a, sock)

struct tr_ssh_socket {
	ssh_session session;
	ssh_channel channel;
	struct tr_ssh_config config;
	char *ident;
};

static int tr_ssh_open(void *tr_ssh_sock);
static void tr_ssh_close(void *tr_ssh_sock);
static void tr_ssh_free(struct tr_socket *tr_sock);
static int tr_ssh_recv(const void *tr_ssh_sock, void *buf, const size_t buf_len, const time_t timeout);
static int tr_ssh_send(const void *tr_ssh_sock, const void *pdu, const size_t len, const time_t timeout);
static int tr_ssh_recv_async(const struct tr_ssh_socket *tr_ssh_sock, void *buf, const size_t buf_len);
static const char *tr_ssh_ident(void *tr_ssh_sock);

/* WARNING: This function has cancelable sections! */
int tr_ssh_open(void *socket)
{
	struct tr_ssh_socket *ssh_socket = socket;
	const struct tr_ssh_config *config = &ssh_socket->config;

	assert(!ssh_socket->channel);
	assert(!ssh_socket->session);

	ssh_socket->session = ssh_new();
	if (!ssh_socket->session) {
		SSH_DBG("%s: can't create ssh_session", ssh_socket, __func__);
		goto error;
	}

	const int verbosity = SSH_LOG_NOLOG;

	ssh_options_set(ssh_socket->session, SSH_OPTIONS_LOG_VERBOSITY, &verbosity);

	ssh_options_set(ssh_socket->session, SSH_OPTIONS_HOST, config->host);
	ssh_options_set(ssh_socket->session, SSH_OPTIONS_PORT, &(config->port));
	ssh_options_set(ssh_socket->session, SSH_OPTIONS_BINDADDR, config->bindaddr);
	ssh_options_set(ssh_socket->session, SSH_OPTIONS_USER, config->username);

	if (config->server_hostkey_path)
		ssh_options_set(ssh_socket->session, SSH_OPTIONS_KNOWNHOSTS, config->server_hostkey_path);

	if (config->client_privkey_path)
		ssh_options_set(ssh_socket->session, SSH_OPTIONS_IDENTITY, config->client_privkey_path);
	if (config->new_socket) {
		int fd;

		fd = config->new_socket(config->data);
		if (fd >= 0) {
			ssh_options_set(ssh_socket->session, SSH_OPTIONS_FD, &fd);
		} else {
			SSH_DBG1("tr_ssh_init: opening SSH connection failed", ssh_socket);
			goto error;
		}
	}

	ssh_set_blocking(ssh_socket->session, 0);
	int ret;

	do {
		ret = ssh_connect(ssh_socket->session);

		if (ret == SSH_ERROR) {
			SSH_DBG("%s: opening SSH connection failed", ssh_socket, __func__);
			goto error;
		} else if (ret == SSH_AGAIN) {
			socket_t fd = ssh_get_fd(ssh_socket->session);

			fd_set rfds;

			FD_ZERO(&rfds);
			FD_SET(fd, &rfds);
			struct timeval timeout = {.tv_sec = ssh_socket->config.connect_timeout, .tv_usec = 0};
			int oldcancelstate;

			/* Enable cancellability for the select call
			 * to prevent rtr_stop from blocking for a long time.
			 * It must be the only blocking call in this function.
			 * Since local resources have all been freed this should be safe.
			 */
			pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &oldcancelstate);
			int sret = select(fd + 1, &rfds, NULL, NULL, &timeout);

			pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldcancelstate);

			if (sret < 0) {
				SSH_DBG("could not select ssh socket, %s", ssh_socket, strerror(errno));

			} else if (sret == 0) {
				SSH_DBG1("connection attempt timed out", ssh_socket);
				goto error;
			}
		}

	} while (ret != SSH_OK);

	ssh_set_blocking(ssh_socket->session, 1);

	// check server identity
#if LIBSSH_VERSION_MAJOR > 0 || LIBSSH_VERSION_MINOR > 8
	if ((config->server_hostkey_path) && (ssh_session_is_known_server(ssh_socket->session) != SSH_KNOWN_HOSTS_OK)) {
#else
	if ((config->server_hostkey_path) && (ssh_is_server_known(ssh_socket->session) != SSH_SERVER_KNOWN_OK)) {
#endif
		SSH_DBG("%s: Wrong hostkey", ssh_socket, __func__);
		goto error;
	}

	if (config->client_privkey_path) {
		SSH_DBG("%s: Trying publickey authentication", ssh_socket, __func__);
		ssh_options_set(ssh_socket->session, SSH_OPTIONS_IDENTITY, config->client_privkey_path);

		int rtval;

#if LIBSSH_VERSION_MAJOR > 0 || LIBSSH_VERSION_MINOR > 5
		rtval = ssh_userauth_publickey_auto(ssh_socket->session, NULL, NULL);
#else /* else use libSSH version 0.5.0 */
		rtval = ssh_userauth_autopubkey(ssh_socket->session, NULL);
#endif
		if (rtval != SSH_AUTH_SUCCESS) {
			SSH_DBG("%s: Publickey authentication failed", ssh_socket, __func__);
			goto error;
		}

	} else {
		SSH_DBG("%s: Trying password authentication", ssh_socket, __func__);

		if (ssh_userauth_password(ssh_socket->session, NULL, config->password) != SSH_AUTH_SUCCESS) {
			SSH_DBG("%s: Password authentication failed", ssh_socket, __func__);
			goto error;
		}
	}

	ssh_socket->channel = ssh_channel_new(ssh_socket->session);
	if (!ssh_socket->channel)
		goto error;

	if (ssh_channel_open_session(ssh_socket->channel) == SSH_ERROR)
		goto error;

	if (ssh_channel_request_subsystem(ssh_socket->channel, "rpki-rtr") == SSH_ERROR) {
		SSH_DBG("%s: Error requesting subsystem rpki-rtr", ssh_socket, __func__);
		goto error;
	}
	SSH_DBG1("Connection established", ssh_socket);

	return TR_SUCCESS;

error:
	tr_ssh_close(ssh_socket);
	return TR_ERROR;
}

void tr_ssh_close(void *tr_ssh_sock)
{
	struct tr_ssh_socket *socket = tr_ssh_sock;

	if (socket->channel) {
		if (ssh_channel_is_open(socket->channel))
			ssh_channel_close(socket->channel);
		ssh_channel_free(socket->channel);
		socket->channel = NULL;
	}
	if (socket->session) {
		ssh_disconnect(socket->session);
		ssh_free(socket->session);
		socket->session = NULL;
	}
	SSH_DBG1("Socket closed", socket);
}

void tr_ssh_free(struct tr_socket *tr_sock)
{
	struct tr_ssh_socket *tr_ssh_sock = tr_sock->socket;

	assert(!tr_ssh_sock->channel);
	assert(!tr_ssh_sock->session);

	SSH_DBG1("Freeing socket", tr_ssh_sock);

	lrtr_free(tr_ssh_sock->config.host);
	lrtr_free(tr_ssh_sock->config.bindaddr);
	lrtr_free(tr_ssh_sock->config.username);
	lrtr_free(tr_ssh_sock->config.client_privkey_path);
	lrtr_free(tr_ssh_sock->config.server_hostkey_path);

	if (tr_ssh_sock->ident)
		lrtr_free(tr_ssh_sock->ident);
	lrtr_free(tr_ssh_sock);
	tr_sock->socket = NULL;
}

int tr_ssh_recv_async(const struct tr_ssh_socket *tr_ssh_sock, void *buf, const size_t buf_len)
{
	const int rtval = ssh_channel_read_nonblocking(tr_ssh_sock->channel, buf, buf_len, false);

	if (rtval == 0) {
		if (ssh_channel_is_eof(tr_ssh_sock->channel) != 0) {
			SSH_DBG1("remote has sent EOF", tr_ssh_sock);
			return TR_CLOSED;
		} else {
			return TR_WOULDBLOCK;
		}
	} else if (rtval == SSH_ERROR) {
		SSH_DBG1("recv(..) error", tr_ssh_sock);
		return TR_ERROR;
	}
	return rtval;
}

int tr_ssh_recv(const void *tr_ssh_sock, void *buf, const size_t buf_len, const time_t timeout)
{
	ssh_channel rchans[2] = {((struct tr_ssh_socket *)tr_ssh_sock)->channel, NULL};
	struct timeval timev = {timeout, 0};
	int ret;

	ret = ssh_channel_select(rchans, NULL, NULL, &timev);
	if (ret == SSH_EINTR)
		return TR_INTR;
	else if (ret == SSH_ERROR)
		return TR_ERROR;

	if (ssh_channel_is_eof(((struct tr_ssh_socket *)tr_ssh_sock)->channel) != 0)
		return TR_ERROR;

	if (!rchans[0])
		return TR_WOULDBLOCK;

	return tr_ssh_recv_async(tr_ssh_sock, buf, buf_len);
}

int tr_ssh_send(const void *tr_ssh_sock, const void *pdu, const size_t len,
		const time_t timeout __attribute__((unused)))
{
	int ret = ssh_channel_write(((struct tr_ssh_socket *)tr_ssh_sock)->channel, pdu, len);

	if (ret == SSH_ERROR)
		return TR_ERROR;

	return ret;
}

const char *tr_ssh_ident(void *tr_ssh_sock)
{
	size_t len;
	struct tr_ssh_socket *sock = tr_ssh_sock;

	assert(sock);

	if (sock->ident)
		return sock->ident;

	len = strlen(sock->config.username) + 1 + strlen(sock->config.host) + 1 + 5 + 1;
	sock->ident = lrtr_malloc(len);
	if (!sock->ident)
		return NULL;
	snprintf(sock->ident, len, "%s@%s:%u", sock->config.username, sock->config.host, sock->config.port);
	return sock->ident;
}

RTRLIB_EXPORT int tr_ssh_init(const struct tr_ssh_config *config, struct tr_socket *socket)
{
	socket->close_fp = &tr_ssh_close;
	socket->free_fp = &tr_ssh_free;
	socket->open_fp = &tr_ssh_open;
	socket->recv_fp = &tr_ssh_recv;
	socket->send_fp = &tr_ssh_send;
	socket->ident_fp = &tr_ssh_ident;

	socket->socket = lrtr_calloc(1, sizeof(struct tr_ssh_socket));
	struct tr_ssh_socket *ssh_socket = socket->socket;

	ssh_socket->channel = NULL;
	ssh_socket->session = NULL;
	ssh_socket->config.host = lrtr_strdup(config->host);
	if (!ssh_socket->config.host)
		goto error;
	ssh_socket->config.port = config->port;

	ssh_socket->config.username = lrtr_strdup(config->username);
	if (!ssh_socket->config.username)
		goto error;

	if ((config->password && config->client_privkey_path) || (!config->password && !config->client_privkey_path))
		return TR_ERROR;

	if (config->bindaddr) {
		ssh_socket->config.bindaddr = lrtr_strdup(config->bindaddr);

		if (!ssh_socket->config.bindaddr)
			goto error;

	} else {
		ssh_socket->config.bindaddr = NULL;
	}

	if (config->client_privkey_path) {
		ssh_socket->config.client_privkey_path = lrtr_strdup(config->client_privkey_path);
		if (!ssh_socket->config.client_privkey_path)
			goto error;

	} else {
		ssh_socket->config.client_privkey_path = NULL;
	}

	if (config->server_hostkey_path) {
		ssh_socket->config.server_hostkey_path = lrtr_strdup(config->server_hostkey_path);

		if (!ssh_socket->config.client_privkey_path)
			goto error;

	} else {
		ssh_socket->config.server_hostkey_path = NULL;
	}

	if (config->connect_timeout == 0)
		ssh_socket->config.connect_timeout = RTRLIB_TRANSPORT_CONNECT_TIMEOUT_DEFAULT;
	else
		ssh_socket->config.connect_timeout = config->connect_timeout;

	if (config->password) {
		ssh_socket->config.password = lrtr_strdup(config->password);

		if (!ssh_socket->config.password)
			goto error;

	} else {
		ssh_socket->config.password = NULL;
	}

	ssh_socket->ident = NULL;
	ssh_socket->config.data = config->data;
	ssh_socket->config.new_socket = config->new_socket;

	return TR_SUCCESS;

error:
	if (ssh_socket->config.host)
		free(ssh_socket->config.host);

	if (ssh_socket->config.username)
		free(ssh_socket->config.username);

	if (ssh_socket->config.bindaddr)
		free(ssh_socket->config.bindaddr);

	if (ssh_socket->config.client_privkey_path)
		free(ssh_socket->config.client_privkey_path);

	if (ssh_socket->config.server_hostkey_path)
		free(ssh_socket->config.server_hostkey_path);

	if (ssh_socket->config.password)
		free(ssh_socket->config.password);

	return TR_ERROR;
}
