/*
 * 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 "templates.h"

#include "rtrlib/rtrlib.h"

#include "third-party/mustach.h"
#include "third-party/tommyds/tommyarray.h"
#include "third-party/tommyds/tommyhashlin.h"

#include <arpa/inet.h>
#include <ctype.h>
#include <errno.h>
#include <netdb.h>
#include <pthread.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#ifdef RTRLIB_HAVE_LIBSSH
// constants for utf8 validation
static const uint8_t UTF8_ONE_BYTE_PREFIX = 0b00000000;
static const uint8_t UTF8_ONE_BYTE_MASK = 0b10000000;
static const uint8_t UTF8_TWO_BYTE_PREFIX = 0b11000000;
static const uint8_t UTF8_TWO_BYTE_MASK = 0b11100000;
static const uint8_t UTF8_THREE_BYTE_PREFIX = 0b11100000;
static const uint8_t UTF8_THREE_BYTE_MASK = 0b11110000;
static const uint8_t UTF8_FOUR_BYTE_PREFIX = 0b11110000;
static const uint8_t UTF8_FOUR_BYTE_MASK = 0b11111000;
static const uint8_t UTF8_SUBSEQUENT_BYTE_PREFIX = 0b10000000;
static const uint8_t UTF8_SUBSEQUENT_BYTE_MASK = 0b11000000;
#endif

__attribute__((format(printf, 1, 2), noreturn)) static void print_error_exit(const char *fmt, ...);

static bool is_readable_file(const char *str);

enum socket_type {
	SOCKET_TYPE_TCP,
#ifdef RTRLIB_HAVE_LIBSSH
	SOCKET_TYPE_SSH,
#endif
};

struct socket_config {
	struct rtr_socket socket;
	struct tr_socket tr_socket;
	enum socket_type type;
	bool print_pfx_updates;
	bool print_spki_updates;
	bool print_aspa_updates;
	char *bindaddr;
	char *host;
	char *port;
#ifdef RTRLIB_HAVE_LIBSSH
	char *ssh_username;
	char *ssh_private_key;
	char *ssh_host_key;
	char *ssh_password;
	bool force_password;
	bool force_key;
#endif
};

/* activate update callback */
bool activate_pfx_update_cb = false;
bool activate_spki_update_cb = false;
bool activate_aspa_update_cb = false;

/* print all updates regardless of socket configuration */
bool print_all_pfx_updates = false;
bool print_all_spki_updates = false;
bool print_all_aspa_updates = false;

bool print_status_updates = false;

bool export_pfx = false;
char *export_file_path = NULL;
const char *template_name = NULL;

struct socket_config **socket_config = NULL;
size_t socket_count = 0;

pthread_mutex_t stdout_mutex = PTHREAD_MUTEX_INITIALIZER;

/**
 * @brief Check if argument is NULL and exit if it is
 */
static void *check_alloc_ret(void *ptr)
{
	if (ptr)
		return ptr;

	fprintf(stderr, "Memory allocation error\n");
	exit(-1);
}

/**
 * @brief Return value checking malloc wrapper. Exits program on error
 */
static inline void *checked_malloc(size_t size)
{
	return check_alloc_ret(malloc(size));
}

/**
 * @brief Return value checking realloc wrapper. Exits program on error.
 */
static inline void *checked_realloc(void *ptr, size_t size)
{
	return check_alloc_ret(realloc(ptr, size));
}

static struct socket_config *extend_socket_config()
{
	socket_config = checked_realloc(socket_config, sizeof(struct socket_config *) * (socket_count + 1));
	struct socket_config *config = socket_config[socket_count] = checked_malloc(sizeof(struct socket_config));

	memset(config, 0, sizeof(*config));
	++socket_count;

	return config;
}

static const char *get_template(const char *name)
{
	const struct pfx_output_template *current = templates;

	while (current->name) {
		if (strcmp(name, current->name) == 0)
			return current->template;

		++current;
	}

	if (is_readable_file(name)) {
		FILE *template_file = fopen(name, "r");

		if (fseek(template_file, 0, SEEK_END) == -1)
			goto read_error;

		long file_size = ftell(template_file);

		if (file_size == -1)
			goto read_error;

		rewind(template_file);

		char *template = checked_malloc(file_size);

		size_t bytes_read = fread(template, 1, file_size, template_file);

		if (bytes_read != (unsigned long)file_size)
			goto read_error;

		fclose(template_file);
		return template;

	read_error:
		fclose(template_file);
		print_error_exit("Could not read template");
	}

	print_error_exit("Template \"%s\" not found", name);
}

static void print_templates(void)
{
	const struct pfx_output_template *current = templates;

	while (current->name) {
		if (template_name && strcmp(current->name, template_name) == 0)
			printf("%s", current->template);
		else if (!template_name)
			printf("%s\n", current->name);

		++current;
	}
}

static bool is_numeric(const char *str)
{
	for (size_t i = 0; i < strlen(str); ++i) {
		if (!isdigit(str[i]))
			return false;
	}
	return true;
}

static bool is_valid_port_number(const char *str)
{
	if (!is_numeric(str))
		return false;

	int port = atoi(str);

	if (port < 1 || port > 65535)
		return false;

	return true;
}

static bool is_resolveable_host(const char *str)
{
	struct addrinfo hints;
	struct addrinfo *result = NULL;

	memset(&hints, 0, sizeof(hints));
	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_flags = 0;
	hints.ai_protocol = 0;

	int ret = getaddrinfo(str, NULL, &hints, &result);

	freeaddrinfo(result);

	if (ret != 0)
		return false;
	return true;
}

static bool is_file(const char *str)
{
	struct stat stat_buf;

	if (stat(str, &stat_buf) == -1)
		return false;

	return S_ISREG(stat_buf.st_mode);
}

static bool is_readable_file(const char *str)
{
	if (access(str, R_OK) != 0)
		return false;

	return is_file(str);
}

#ifdef RTRLIB_HAVE_LIBSSH
static bool is_utf8(const char *str)
{
	size_t len = strlen(str);
	size_t i = 0;

	while (i < len) {
		// check if current byte is a single utf8 char
		if ((str[i] & UTF8_ONE_BYTE_MASK) == UTF8_ONE_BYTE_PREFIX) {
			i += 1;

			// check if current byte is the start of a two byte utf8 char and validate subsequent bytes
		} else if ((str[i] & UTF8_TWO_BYTE_MASK) == UTF8_TWO_BYTE_PREFIX && i + 1 < len &&
			   (str[i + 1] & UTF8_SUBSEQUENT_BYTE_MASK) == UTF8_SUBSEQUENT_BYTE_PREFIX) {
			i += 2;

			// check if current byte is the start of a three byte utf8 char and validate subsequent bytes
		} else if ((str[i] & UTF8_THREE_BYTE_MASK) == UTF8_THREE_BYTE_PREFIX && i + 2 < len &&
			   (str[i + 1] & UTF8_SUBSEQUENT_BYTE_MASK) == UTF8_SUBSEQUENT_BYTE_PREFIX &&
			   (str[i + 2] & UTF8_SUBSEQUENT_BYTE_MASK) == UTF8_SUBSEQUENT_BYTE_PREFIX) {
			i += 3;

			// check if current byte is the start of a four byte utf8 char and validate subsequent bytes
		} else if ((str[i] & UTF8_FOUR_BYTE_MASK) == UTF8_FOUR_BYTE_PREFIX && i + 3 < len &&
			   (str[i + 1] & UTF8_SUBSEQUENT_BYTE_MASK) == UTF8_SUBSEQUENT_BYTE_PREFIX &&
			   (str[i + 2] & UTF8_SUBSEQUENT_BYTE_MASK) == UTF8_SUBSEQUENT_BYTE_PREFIX &&
			   (str[i + 3] & UTF8_SUBSEQUENT_BYTE_MASK) == UTF8_SUBSEQUENT_BYTE_PREFIX) {
			i += 4;

			// if none of the conditions matched. The string contains at least one byte that is not valid utf8
		} else {
			return false;
		}
	}
	return true;
}
#endif

struct exporter_state {
	bool roa_section;
	tommy_count_t current_roa;
	tommy_array *roas;
};

struct pfx_export_cb_arg {
	tommy_array *array;
	tommy_hashlin *hashtable;
};

static void pfx_export_cb(const struct pfx_record *pfx_record, void *data);

static tommy_uint32_t hash_pfx_record(const struct pfx_record *record)
{
	struct pfx_record_packed {
		uint8_t ip[16];
		uint32_t asn;
		uint8_t min_len;
		uint8_t max_len;
	} __attribute__((packed));

	struct pfx_record_packed packed_record;

	memset(&packed_record, 0, sizeof(packed_record));

	if (record->prefix.ver == LRTR_IPV4)
		memcpy(&packed_record.ip, &record->prefix.u.addr4, sizeof(struct lrtr_ipv4_addr));
	else
		memcpy(&packed_record.ip, &record->prefix.u.addr6, sizeof(struct lrtr_ipv6_addr));

	packed_record.asn = record->asn;
	packed_record.min_len = record->min_len;
	packed_record.max_len = record->max_len;

	return tommy_hash_u32(0, &packed_record, sizeof(packed_record));
}

static int pfx_record_cmp(const void *data1, const void *data2)
{
	const struct pfx_record *arg1 = data1;
	const struct pfx_record *arg2 = data2;

	if ((arg1->asn == arg2->asn) && (arg1->max_len == arg2->max_len) && (arg1->min_len == arg2->min_len) &&
	    lrtr_ip_addr_equal(arg1->prefix, arg2->prefix))
		return 0;

	return 1;
}

static int template_enter(void *closure, const char *name)
{
	struct exporter_state *state = closure;

	if (strncmp("roas", name, strlen(name)) == 0) {
		state->roa_section = true;
		state->current_roa = 0;

		return 1;

	} else if (state->current_roa && strcmp("last", name) == 0 &&
		   state->current_roa == tommy_array_size(state->roas) - 1) {
		return 1;
	}

	return 0;
}

static int template_leave(void *closure)
{
	struct exporter_state *state = closure;

	state->roa_section = false;
	return 0;
}

static int template_next(void *closure)
{
	struct exporter_state *state = closure;

	if (++state->current_roa >= tommy_array_size(state->roas))
		return 0;

	return 1;
}

static int template_put(void *closure, const char *name, __attribute__((unused)) int escape, FILE *file)
{
	struct exporter_state *state = closure;

	size_t namelen = strlen(name);

	if (!state->roa_section || namelen == 0)
		return MUSTACH_ERROR_SYSTEM;

	struct pfx_record *pfx_record = tommy_array_get(state->roas, state->current_roa);

	if (strncmp("prefix", name, namelen) == 0) {
		char prefix_str[INET6_ADDRSTRLEN];

		lrtr_ip_addr_to_str(&pfx_record->prefix, prefix_str, sizeof(prefix_str));
		fputs(prefix_str, file);

	} else if (strncmp("length", name, namelen) == 0) {
		fprintf(file, "%d", pfx_record->min_len);

	} else if (strncmp("maxlen", name, namelen) == 0) {
		fprintf(file, "%d", pfx_record->max_len);

	} else if (strncmp("origin", name, namelen) == 0) {
		fprintf(file, "%d", pfx_record->asn);
	}

	return MUSTACH_OK;
}

struct mustach_itf template_itf = {.start = NULL,
				   .put = template_put,
				   .enter = template_enter,
				   .next = template_next,
				   .leave = template_leave};

static void print_usage(char **argv)
{
	printf("Usage:\n");
	printf(" %s [-hpkels] [-o file] [-t template] <socket>...\n", argv[0]);
	printf("\nSocket:\n");
	printf(" tcp [-hpkb bindaddr] <host> <port>\n");
#ifdef RTRLIB_HAVE_LIBSSH
	printf(" ssh [-hpkb bindaddr] <host> <port> <username> (<private_key> | <password>) [<host_key>]\n");
#endif
	printf("\nOptions:\n");
	printf("-b  bindaddr Hostnamne or IP address to connect from\n\n");

#ifdef RTRLIB_HAVE_LIBSSH
	printf("-w  force ssh authentication information to be interpreted as a password\n");
	printf("-r  force ssh authentication information to be interpreted as a private key\n\n");
#endif

	printf("-k  Print information about SPKI updates.\n");
	printf("-p  Print information about PFX updates.\n");
	printf("-a  Print information about ASPA updates.\n");
	printf("-s  Print information about connection status updates.\n\n");

	printf("-e  export pfx table and exit\n");
	printf("-o  output file for export\n");
	printf("-t  template used for export\n");
	printf("-l  list available templates\n\n");

	printf("-h  Print this help message.\n");

	printf("\nExamples:\n");
	printf(" %s tcp rpki-validator.realmv6.org 8283\n", argv[0]);
	printf(" %s tcp -k -p rpki-validator.realmv6.org 8283\n", argv[0]);
	printf(" %s tcp -k rpki-validator.realmv6.org 8283 tcp -s example.com 323\n", argv[0]);
	printf(" %s -kp tcp rpki-validator.realmv6.org 8283 tcp example.com 323\n", argv[0]);
#ifdef RTRLIB_HAVE_LIBSSH
	printf(" %s ssh rpki-validator.realmv6.org 22 rtr-ssh", argv[0]);
	printf(" ~/.ssh/id_rsa ~/.ssh/known_hosts\n");
	printf(" %s ssh -k -p rpki-validator.realmv6.org 22 rtr-ssh ~/.ssh/id_rsa ~/.ssh/known_hosts\n", argv[0]);
	printf(" %s ssh -k -p rpki-validator.realmv6.org 22 rtr-ssh ~/.ssh/id_rsa ~/.ssh/known_hosts", argv[0]);
	printf(" ssh -k -p example.com 22 rtr-ssh ~/.ssh/id_rsa_example\n");
	printf(" %s ssh -k -p rpki-validator.realmv6.org 22 rtr-ssh ~/.ssh/id_rsa ~/.ssh/known_hosts", argv[0]);
	printf(" tcp -k -p example.com 323\n");
#endif

	printf(" %s -e tcp rpki-validator.realmv6.org 8283\n", argv[0]);
	printf(" %s -e -t csv -o roa.csv tcp rpki-validator.realmv6.org 8283\n", argv[0]);
}

static void status_fp(const struct rtr_mgr_group *group __attribute__((unused)), enum rtr_mgr_status mgr_status,
		      const struct rtr_socket *rtr_sock, void *data __attribute__((unused)))
{
	if (print_status_updates) {
		pthread_mutex_lock(&stdout_mutex);
		printf("RTR-Socket changed connection status to: %s, Mgr Status: %s\n",
		       rtr_state_to_str(rtr_sock->state), rtr_mgr_status_to_str(mgr_status));
		pthread_mutex_unlock(&stdout_mutex);
	}
}

static void update_cb(struct pfx_table *p __attribute__((unused)), const struct pfx_record rec, const bool added)
{
	char ip[INET6_ADDRSTRLEN];

	const struct socket_config *config = (const struct socket_config *)rec.socket;

	if (!print_all_pfx_updates && !config->print_pfx_updates)
		return;

	pthread_mutex_lock(&stdout_mutex);
	if (added)
		printf("+ ");
	else
		printf("- ");
	lrtr_ip_addr_to_str(&rec.prefix, ip, sizeof(ip));
	if (socket_count > 1)
		printf("%s:%s %-40s   %3u - %3u   %10u\n", config->host, config->port, ip, rec.min_len, rec.max_len,
		       rec.asn);
	else
		printf("%-40s   %3u - %3u   %10u\n", ip, rec.min_len, rec.max_len, rec.asn);

	pthread_mutex_unlock(&stdout_mutex);
}

static void update_spki(struct spki_table *s __attribute__((unused)), const struct spki_record record, const bool added)
{
	const struct socket_config *config = (const struct socket_config *)record.socket;

	if (!print_all_spki_updates && !config->print_spki_updates)
		return;

	pthread_mutex_lock(&stdout_mutex);

	char c = added ? '+' : '-';

	printf("%c ", c);
	printf("HOST:  %s:%s\n", config->host, config->port);
	printf("ASN:  %u\n  ", record.asn);

	int i;
	int size = sizeof(record.ski);

	printf("SKI:  ");
	for (i = 0; i < size; i++) {
		printf("%02x", record.ski[i]);
		if (i < size - 1)
			printf(":");
	}
	printf("\n  ");

	i = 0;
	size = sizeof(record.spki);
	printf("SPKI: ");
	for (i = 0; i < size; i++) {
		if ((i % 40 == 0) && (i != 0))
			printf("\n	");

		printf("%02x", record.spki[i]);
		if (i < size - 1)
			printf(":");
	}
	printf("\n");

	pthread_mutex_unlock(&stdout_mutex);
}

static void update_aspa(struct aspa_table *s __attribute__((unused)), const struct aspa_record record,
			const struct rtr_socket *rtr_sockt, const enum aspa_operation_type operation_type)
{
	const struct socket_config *config = (const struct socket_config *)rtr_sockt;

	if (!print_all_aspa_updates && !config->print_aspa_updates)
		return;

	pthread_mutex_lock(&stdout_mutex);

	printf("HOST:  %s:%s\n", config->host, config->port);

	char c;

	switch (operation_type) {
	case ASPA_ADD:
		c = '+';
		break;
	case ASPA_REMOVE:
		c = '-';
		break;
	default:
		c = '?';
		break;
	}

	printf("%c ASPA %u => [ ", c, record.customer_asn);

	size_t i;
	size_t count = record.provider_count;

	for (i = 0; i < count; i++) {
		printf("%u", record.provider_asns[i]);
		if (i < count - 1)
			printf(", ");
	}

	printf(" ]\n");
	pthread_mutex_unlock(&stdout_mutex);
	free(record.provider_asns);
}

static void parse_global_opts(int argc, char **argv)
{
	int opt;

	bool print_template = false;

	while ((opt = getopt(argc, argv, "+kpahelo:t:s")) != -1) {
		switch (opt) {
		case 'k':
			activate_spki_update_cb = true;
			print_all_spki_updates = true;
			break;

		case 'p':
			activate_pfx_update_cb = true;
			print_all_pfx_updates = true;
			break;

		case 'a':
			activate_aspa_update_cb = true;
			print_all_aspa_updates = true;
			break;

		case 'e':
			export_pfx = true;
			break;

		case 'o':
			if (export_file_path)
				print_error_exit("output file can not be specified more than once");

			export_file_path = optarg;
			break;

		case 't':
			template_name = optarg;
			break;

		case 'l':
			print_template = true;
			break;

		case 's':
			print_status_updates = true;
			break;

		default:
			print_usage(argv);
			exit(EXIT_FAILURE);
		}
	}

	if (print_template) {
		print_templates();
		exit(EXIT_SUCCESS);
	}

	if ((export_file_path || template_name) && !export_pfx)
		print_error_exit("Specifying -o or -t without -e does not make sense");
}

static void parse_socket_opts(int argc, char **argv, struct socket_config *config)
{
	int opt;

	while ((opt = getopt(argc, argv, "+kpahwrb:")) != -1) {
		switch (opt) {
		case 'k':
			activate_spki_update_cb = true;
			config->print_spki_updates = true;
			break;

		case 'p':
			activate_pfx_update_cb = true;
			config->print_pfx_updates = true;
			break;

		case 'a':
			activate_aspa_update_cb = true;
			config->print_aspa_updates = true;
			break;

		case 'b':
			config->bindaddr = optarg;
			break;

#ifdef RTRLIB_HAVE_LIBSSH
		case 'w':
			if (config->force_key)
				print_error_exit("-w and -r are mutually exclusive");

			config->force_password = true;
			break;

		case 'r':
			if (config->force_password)
				print_error_exit("-w and -r are mutually exclusive");

			config->force_key = true;
			break;
#endif

		default:
			print_usage(argv);
			exit(EXIT_FAILURE);
		}
	}
}

static void print_error_exit(const char *fmt, ...)
{
	va_list argptr;

	va_start(argptr, fmt);

	fprintf(stderr, "error: ");

	vfprintf(stderr, fmt, argptr);

	fprintf(stderr, "\n");

	va_end(argptr);

	exit(EXIT_FAILURE);
}

static int parse_cli(int argc, char **argv)
{
	enum cli_parse_state {
		CLI_PARSE_STATE_BEGIN,
		CLI_PARSE_STATE_GLOBAL_OPTS,
		CLI_PARSE_STATE_SOCKET_BEGIN,
		CLI_PARSE_STATE_SOCKET_TCP_OPTIONS,
		CLI_PARSE_STATE_SOCKET_TCP_HOST,
		CLI_PARSE_STATE_SOCKET_TCP_PORT,
#ifdef RTRLIB_HAVE_LIBSSH
		CLI_PARSE_STATE_SOCKET_SSH_OPTIONS,
		CLI_PARSE_STATE_SOCKET_SSH_HOST,
		CLI_PARSE_STATE_SOCKET_SSH_PORT,
		CLI_PARSE_STATE_SOCKET_SSH_USERNAME,
		CLI_PARSE_STATE_SOCKET_SSH_PRIVATE_KEY_PASSWORD,
		CLI_PARSE_STATE_SOCKET_SSH_HOST_KEY,
#endif
		CLI_PARSE_STATE_END,
	};

	enum cli_parse_state state = CLI_PARSE_STATE_BEGIN;

	struct socket_config *current_config;

	while (true) {
		switch (state) {
		case CLI_PARSE_STATE_BEGIN:
			optind = 1;
			state = CLI_PARSE_STATE_GLOBAL_OPTS;
			break;

		case CLI_PARSE_STATE_GLOBAL_OPTS:
			parse_global_opts(argc, argv);

			state = CLI_PARSE_STATE_SOCKET_BEGIN;
			break;

		case CLI_PARSE_STATE_SOCKET_BEGIN:
			current_config = extend_socket_config();

			if (strncasecmp(argv[optind], "tcp", strlen(argv[optind])) == 0) {
				state = CLI_PARSE_STATE_SOCKET_TCP_OPTIONS;
				current_config->type = SOCKET_TYPE_TCP;

				/* Check if enough arguments are available
				 * to configure this socket.
				 * This does not account for options.
				 */
				if ((argc - optind) < 2)
					print_error_exit("Not enough arguments for tcp socket");

			} else if (strncasecmp(argv[optind], "ssh", strlen(argv[optind])) == 0) {
#ifdef RTRLIB_HAVE_LIBSSH
				state = CLI_PARSE_STATE_SOCKET_SSH_OPTIONS;
				current_config->type = SOCKET_TYPE_SSH;

				/* Check if enough arguments are available
				 * to configure this socket.
				 * This does not account for options.
				 */
				if ((argc - optind) < 4)
					print_error_exit("Not enough arguments for ssh socket");
#else
				print_error_exit("ssh support disabled at compile time\n");
#endif

			} else {
				print_error_exit("\"%s\" is not a valid socket type\n", argv[optind]);
				break;
			}

			++optind;
			break;

		case CLI_PARSE_STATE_SOCKET_TCP_OPTIONS:
			parse_socket_opts(argc, argv, current_config);

			/* Check again if enough arguments are available,
			 * accounting for options
			 */
			if ((argc - optind) < 2)
				print_error_exit("Not enough arguments for tcp socket");

			state = CLI_PARSE_STATE_SOCKET_TCP_HOST;
			break;

		case CLI_PARSE_STATE_SOCKET_TCP_HOST:
			if (!is_resolveable_host(argv[optind]))
				print_error_exit("cannot resolve \"%s\"\n", argv[optind]);

			current_config->host = argv[optind++];

			state = CLI_PARSE_STATE_SOCKET_TCP_PORT;
			break;

		case CLI_PARSE_STATE_SOCKET_TCP_PORT:
			if (!is_valid_port_number(argv[optind]))
				print_error_exit("\"%s\" is not a valid port number\n", argv[optind]);

			current_config->port = argv[optind++];

			state = CLI_PARSE_STATE_SOCKET_BEGIN;
			break;

#ifdef RTRLIB_HAVE_LIBSSH
		case CLI_PARSE_STATE_SOCKET_SSH_OPTIONS:
			parse_socket_opts(argc, argv, current_config);

			/* Check again if enough arguments are available,
			 * accounting for options
			 */
			if ((argc - optind) < 4)
				print_error_exit("Not enough arguments for ssh socket");

			state = CLI_PARSE_STATE_SOCKET_SSH_HOST;
			break;

		case CLI_PARSE_STATE_SOCKET_SSH_HOST:
			if (!is_resolveable_host(argv[optind]))
				print_error_exit("cannot resolve \"%s\"\n", argv[optind]);

			current_config->host = argv[optind++];

			state = CLI_PARSE_STATE_SOCKET_SSH_PORT;
			break;

		case CLI_PARSE_STATE_SOCKET_SSH_PORT:
			if (!is_valid_port_number(argv[optind]))
				print_error_exit("\"%s\" is not a valid port number\n", argv[optind]);

			current_config->port = argv[optind++];

			state = CLI_PARSE_STATE_SOCKET_SSH_USERNAME;
			break;

		case CLI_PARSE_STATE_SOCKET_SSH_USERNAME:
			current_config->ssh_username = argv[optind++];

			state = CLI_PARSE_STATE_SOCKET_SSH_PRIVATE_KEY_PASSWORD;
			break;

		case CLI_PARSE_STATE_SOCKET_SSH_PRIVATE_KEY_PASSWORD:
			if (current_config->force_key && !is_readable_file(argv[optind])) {
				print_error_exit("\"%s\" is not a readable file\n", argv[optind]);

			} else if (!current_config->force_password && !current_config->force_key &&
				   is_readable_file(argv[optind])) {
				current_config->ssh_private_key = argv[optind];

			} else if (!current_config->force_password && is_utf8(argv[optind])) {
				fprintf(stderr, "\"%s\" does not seem to be a file. Trying password authentication.\n",
					argv[optind]);
				fprintf(stderr, "Use -r to force key authentication or -w to silence this warning");
				current_config->ssh_password = argv[optind];

			} else if (current_config->force_password && !is_utf8(argv[optind])) {
				print_error_exit("\"%s\" is not a valid utf8 string", argv[optind]);

			} else if (current_config->force_password && !current_config->force_key &&
				   is_utf8(argv[optind])) {
				current_config->ssh_password = argv[optind];

			} else {
				print_error_exit("\"%s\" is neither a readable file nor a valid utf8 string",
						 argv[optind]);
			}

			++optind;
			state = CLI_PARSE_STATE_SOCKET_SSH_HOST_KEY;
			break;

		case CLI_PARSE_STATE_SOCKET_SSH_HOST_KEY:
			if (strncasecmp(argv[optind], "tcp", strlen(argv[optind])) == 0 ||
			    strncasecmp(argv[optind], "ssh", strlen(argv[optind])) == 0) {
				state = CLI_PARSE_STATE_SOCKET_BEGIN;
				break;
			}

			if (!is_readable_file(argv[optind]))
				print_error_exit("\"%s\" is not a readable file\n", argv[optind]);

			current_config->ssh_host_key = argv[optind++];

			state = CLI_PARSE_STATE_SOCKET_BEGIN;
			break;
#endif

		case CLI_PARSE_STATE_END:
			return 0;
		}

		if (optind >= argc)
			state = CLI_PARSE_STATE_END;
	}
}

static void init_sockets(void)
{
	for (size_t i = 0; i < socket_count; ++i) {
		struct socket_config *config = socket_config[i];
		struct tr_tcp_config tcp_config = {};
#ifdef RTRLIB_HAVE_LIBSSH
		struct tr_ssh_config ssh_config = {};
#endif

		switch (config->type) {
		case SOCKET_TYPE_TCP:
			tcp_config.host = config->host;
			tcp_config.port = config->port;
			tcp_config.bindaddr = config->bindaddr;

			tr_tcp_init(&tcp_config, &config->tr_socket);
			config->socket.tr_socket = &config->tr_socket;
			break;

#ifdef RTRLIB_HAVE_LIBSSH
		case SOCKET_TYPE_SSH:
			ssh_config.host = config->host;
			ssh_config.port = atoi(config->port);
			ssh_config.bindaddr = config->bindaddr;
			ssh_config.username = config->ssh_username;
			ssh_config.client_privkey_path = config->ssh_private_key;
			ssh_config.server_hostkey_path = config->ssh_host_key;
			ssh_config.password = config->ssh_password;

			tr_ssh_init(&ssh_config, &config->tr_socket);
			config->socket.tr_socket = &config->tr_socket;
			break;
#endif
		}
	}
}

int main(int argc, char **argv)
{
	parse_cli(argc, argv);

	if (socket_count == 0) {
		print_usage(argv);
		exit(EXIT_FAILURE);
	}

	init_sockets();

	struct rtr_mgr_config *conf;
	struct rtr_mgr_group groups[1];

	groups[0].sockets_len = socket_count;
	groups[0].sockets = (struct rtr_socket **)socket_config;
	groups[0].preference = 1;

	spki_update_fp spki_update_fp = activate_spki_update_cb ? update_spki : NULL;
	pfx_update_fp pfx_update_fp = activate_pfx_update_cb ? update_cb : NULL;
	aspa_update_fp aspa_update_fp = activate_aspa_update_cb ? update_aspa : NULL;

	int ret = rtr_mgr_init(&conf, groups, 1, status_fp, NULL);

	if (ret == RTR_ERROR)
		fprintf(stderr, "Error in rtr_mgr_init!\n");
	else if (ret == RTR_INVALID_PARAM)
		fprintf(stderr, "Invalid params passed to rtr_mgr_init\n");

	if (rtr_mgr_add_roa_support(conf, pfx_update_fp) == RTR_ERROR) {
		fprintf(stderr, "Failed initializing ROA support\n");
	}

	if (rtr_mgr_add_aspa_support(conf, aspa_update_fp) == RTR_ERROR) {
		fprintf(stderr, "Failed initializing ASPA support\n");
	}

	if (rtr_mgr_add_spki_support(conf, spki_update_fp) == RTR_ERROR) {
		fprintf(stderr, "Failed initializing BGPSEC support\n");
	}

	rtr_mgr_setup_sockets(conf, groups, 1, 50, 600, 600);

	if (!conf)
		return EXIT_FAILURE;

	if (!export_pfx && activate_pfx_update_cb && socket_count > 1)
		printf("%-40s %-40s   %3s   %3s   %3s\n", "host", "Prefix", "Prefix Length", "", "ASN");
	else if (!export_pfx && activate_pfx_update_cb)
		printf("%-40s   %3s   %3s   %3s\n", "Prefix", "Prefix Length", "", "ASN");

	if (export_pfx) {
		const char *template;

		if (template_name)
			template = get_template(template_name);
		else
			template = templates->template;
		FILE *export_file = stdout;

		if (export_file_path) {
			export_file = fopen(export_file_path, "w");
			if (!export_file) {
				char *errormsg = strerror(errno);

				print_error_exit("\"%s\" could not be opened for writing: %s", optarg, errormsg);
			}
		}

		rtr_mgr_start(conf);

		while (!rtr_mgr_conf_in_sync(conf))
			sleep(1);

		printf("Sync done\n");
		tommy_array prefixes;
		tommy_hashlin prefix_hash;
		struct pfx_export_cb_arg arg = {
			.array = &prefixes,
			.hashtable = &prefix_hash,
		};

		tommy_array_init(&prefixes);
		tommy_hashlin_init(&prefix_hash);

		pfx_table_for_each_ipv4_record(conf->pfx_table, pfx_export_cb, &arg);
		pfx_table_for_each_ipv6_record(conf->pfx_table, pfx_export_cb, &arg);

		struct exporter_state state = {
			.roa_section = false,
			.current_roa = 0,
			.roas = &prefixes,
		};

		fmustach(template, &template_itf, &state, export_file);

		tommy_hashlin_foreach(&prefix_hash, free);
		tommy_hashlin_done(&prefix_hash);
		tommy_array_done(&prefixes);

		if (export_file != stdout && fclose(export_file) == EOF) {
			char *errormsg = strerror(errno);

			print_error_exit("\"Error during write into output file: %s\"", errormsg);
		}

	} else {
		rtr_mgr_start(conf);
		pause();
	}

	rtr_mgr_stop(conf);
	rtr_mgr_free(conf);
	free(groups[0].sockets);

	return EXIT_SUCCESS;
}

struct pfx_record_entry {
	struct pfx_record record;
	tommy_node hash_node;
};

static void pfx_export_cb(const struct pfx_record *pfx_record, void *data)
{
	struct pfx_export_cb_arg *arg = data;
	tommy_array *roa_array = arg->array;
	tommy_hashlin *roa_hashtable = arg->hashtable;

	tommy_hash_t record_hash = hash_pfx_record(pfx_record);

	if (tommy_hashlin_search(roa_hashtable, &pfx_record_cmp, pfx_record, record_hash) != 0)
		return;

	struct pfx_record_entry *pfx_record_entry = malloc(sizeof(struct pfx_record_entry));

	memcpy(&pfx_record_entry->record, pfx_record, sizeof(struct pfx_record));

	tommy_hashlin_insert(roa_hashtable, &pfx_record_entry->hash_node, pfx_record_entry, record_hash);
	tommy_array_insert(roa_array, pfx_record_entry);
}
