/*
 * Copyright (C) 2018 Tobias Brunner
 * Copyright (C) 2007-2009 Martin Willi
 *
 * Copyright (C) secunet Security Networks AG
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2 of the License, or (at your
 * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 */

#include "backend_manager.h"

#include <sys/types.h>

#include <daemon.h>
#include <collections/linked_list.h>
#include <threading/rwlock.h>


typedef struct private_backend_manager_t private_backend_manager_t;

/**
 * Private data of an backend_manager_t object.
 */
struct private_backend_manager_t {

	/**
	 * Public part of backend_manager_t object.
	 */
	backend_manager_t public;

	/**
	 * list of registered backends
	 */
	linked_list_t *backends;

	/**
	 * rwlock for backends
	 */
	rwlock_t *lock;
};

/**
 * match of an ike_cfg
 */
typedef enum ike_cfg_match_t {
	/* doesn't match at all */
	MATCH_NONE		= 0x00,
	/* match for a %any host. For both hosts, hence skip 0x02 */
	MATCH_ANY		= 0x01,
	/* IKE version matches exactly (config is not for any version) */
	MATCH_VERSION	= 0x04,
	/* local identity matches */
	MATCH_ME		= 0x08,
	/* remote identity matches */
	MATCH_OTHER		= 0x10,
} ike_cfg_match_t;

/**
 * data to pass nested IKE enumerator
 */
typedef struct {
	private_backend_manager_t *this;
	host_t *me;
	host_t *other;
} ike_data_t;

/**
 * inner enumerator constructor for IKE cfgs
 */
static enumerator_t *ike_enum_create(backend_t *backend, ike_data_t *data)
{
	return backend->create_ike_cfg_enumerator(backend, data->me, data->other);
}

/**
 * get a match of a candidate ike_cfg for two hosts
 */
static ike_cfg_match_t get_ike_match(ike_cfg_t *cand, host_t *me, host_t *other,
									 ike_version_t version)
{
	ike_cfg_match_t match = MATCH_NONE;
	int quality;

	if (cand->get_version(cand) != IKE_ANY &&
		version != cand->get_version(cand))
	{
		return MATCH_NONE;
	}

	if (me)
	{
		quality = cand->match_me(cand, me);
		if (!quality)
		{
			return MATCH_NONE;
		}
		match += quality * MATCH_ME;
	}
	else
	{
		match += MATCH_ANY;
	}

	if (other)
	{
		quality = cand->match_other(cand, other);
		if (!quality)
		{
			return MATCH_NONE;
		}
		match += quality * MATCH_OTHER;
	}
	else
	{
		match += MATCH_ANY;
	}

	if (match != MATCH_NONE &&
		cand->get_version(cand) != IKE_ANY)
	{	/* if we have a match, improve it if candidate version specified */
		match += MATCH_VERSION;
	}
	return match;
}

/**
 * list element to help sorting
 */
typedef struct {
	ike_cfg_match_t match;
	ike_cfg_t *cfg;
} ike_match_entry_t;

CALLBACK(ike_enum_filter, bool,
	linked_list_t *configs, enumerator_t *orig, va_list args)
{
	ike_match_entry_t *entry;
	ike_cfg_t **out;

	VA_ARGS_VGET(args, out);

	if (orig->enumerate(orig, &entry))
	{
		*out = entry->cfg;
		return TRUE;
	}
	return FALSE;
}

CALLBACK(ike_match_entry_list_destroy, void,
	linked_list_t *configs)
{
	ike_match_entry_t *entry;

	while (configs->remove_last(configs, (void**)&entry) == SUCCESS)
	{
		entry->cfg->destroy(entry->cfg);
		free(entry);
	}
	configs->destroy(configs);
}

/**
 * Insert entry into match-sorted list
 */
static void insert_sorted_ike(ike_match_entry_t *entry, linked_list_t *list)
{
	enumerator_t *enumerator;
	ike_match_entry_t *current;

	enumerator = list->create_enumerator(list);
	while (enumerator->enumerate(enumerator, &current))
	{
		if (entry->match > current->match)
		{
			break;
		}
	}
	list->insert_before(list, enumerator, entry);
	enumerator->destroy(enumerator);
}

/**
 * Create a sorted list of all matching IKE configs
 */
static linked_list_t *get_matching_ike_cfgs(private_backend_manager_t *this,
											host_t *me, host_t *other,
											ike_version_t version)
{
	ike_cfg_t *current;
	enumerator_t *enumerator;
	ike_data_t *data;
	linked_list_t *configs;
	ike_cfg_match_t match;
	ike_match_entry_t *entry;

	INIT(data,
		.this = this,
		.me = me,
		.other = other,
	);

	configs = linked_list_create();

	this->lock->read_lock(this->lock);
	enumerator = enumerator_create_nested(
						this->backends->create_enumerator(this->backends),
						(void*)ike_enum_create, data, (void*)free);

	while (enumerator->enumerate(enumerator, &current))
	{
#if DEBUG_LEVEL >= 2
		char *my_addr = current->get_my_addr(current);
		char *other_addr = current->get_other_addr(current);
#endif
		match = get_ike_match(current, me, other, version);
		DBG3(DBG_CFG, "ike config match: %d (%s...%s %N)", match, my_addr,
			 other_addr, ike_version_names, current->get_version(current));

		if (match)
		{
			DBG2(DBG_CFG, "  candidate: %s...%s, prio %d",
				 my_addr, other_addr, match);

			INIT(entry,
				.match = match,
				.cfg = current->get_ref(current),
			);
			insert_sorted_ike(entry, configs);
		}
	}
	enumerator->destroy(enumerator);
	this->lock->unlock(this->lock);

	return configs;
}

METHOD(backend_manager_t, get_ike_cfg, ike_cfg_t*,
	private_backend_manager_t *this, host_t *me, host_t *other,
	ike_version_t version)
{
	linked_list_t *configs;
	ike_match_entry_t *entry;
	ike_cfg_t *found = NULL;

	DBG2(DBG_CFG, "looking for an %N config for %H...%H", ike_version_names,
		 version, me, other);

	configs = get_matching_ike_cfgs(this, me, other, version);
	if (configs->get_first(configs, (void**)&entry) == SUCCESS)
	{
		found = entry->cfg->get_ref(entry->cfg);
		DBG2(DBG_CFG, "found matching ike config: %s...%s with prio %d",
			 found->get_my_addr(found), found->get_other_addr(found),
			 entry->match);
	}
	ike_match_entry_list_destroy(configs);

	return found;
}

METHOD(backend_manager_t, create_ike_cfg_enumerator, enumerator_t*,
	private_backend_manager_t *this, host_t *me, host_t *other,
	ike_version_t version)
{
	linked_list_t *configs;

	DBG2(DBG_CFG, "looking for %N configs for %H...%H", ike_version_names,
		 version, me, other);

	configs = get_matching_ike_cfgs(this, me, other, version);

	return enumerator_create_filter(configs->create_enumerator(configs),
									ike_enum_filter, configs,
									ike_match_entry_list_destroy);
}

/**
 * Get the best ID match in one of the configs auth_cfg
 */
static id_match_t get_peer_match(identification_t *id,
								 peer_cfg_t *cfg, bool local)
{
	enumerator_t *enumerator;
	auth_cfg_t *auth;
	identification_t *candidate;
	id_match_t match = ID_MATCH_NONE;
	char *where DBG_UNUSED = local ? "local" : "remote";

	if (!id)
	{
		DBG3(DBG_CFG, "  %s id match: %d (%N)",
			 where, ID_MATCH_ANY, id_type_names, ID_ANY);
		return ID_MATCH_ANY;
	}

	/* compare first auth config only */
	enumerator = cfg->create_auth_cfg_enumerator(cfg, local);
	if (enumerator->enumerate(enumerator, &auth))
	{
		candidate = auth->get(auth, AUTH_RULE_IDENTITY);
		if (candidate)
		{
			match = id->matches(id, candidate);
			/* match vice-versa, as the proposed IDr might be ANY */
			if (!match)
			{
				match = candidate->matches(candidate, id);
			}
		}
		else
		{
			match = ID_MATCH_ANY;
		}
	}
	enumerator->destroy(enumerator);

#if DEBUG_LEVEL >= 3
	chunk_t data = id->get_encoding(id);
	DBG3(DBG_CFG, "  %s id match: %d (%N: %#B)",
		 where, match, id_type_names, id->get_type(id), &data);
#endif
	return match;
}

/**
 * data to pass nested peer enumerator
 */
typedef struct {
	rwlock_t *lock;
	identification_t *me;
	identification_t *other;
} peer_data_t;

/**
 * list element to help sorting
 */
typedef struct {
	id_match_t match_peer;
	ike_cfg_match_t match_ike;
	peer_cfg_t *cfg;
} match_entry_t;

/**
 * inner enumerator constructor for peer cfgs
 */
static enumerator_t *peer_enum_create(backend_t *backend, peer_data_t *data)
{
	return backend->create_peer_cfg_enumerator(backend, data->me, data->other);
}

/**
 * unlock/cleanup peer enumerator
 */
static void peer_enum_destroy(peer_data_t *data)
{
	data->lock->unlock(data->lock);
	free(data);
}

CALLBACK(peer_enum_filter, bool,
	linked_list_t *configs, enumerator_t *orig, va_list args)
{
	match_entry_t *entry;
	peer_cfg_t **out;

	VA_ARGS_VGET(args, out);

	if (orig->enumerate(orig, &entry))
	{
		*out = entry->cfg;
		return TRUE;
	}
	return FALSE;
}

CALLBACK(peer_enum_filter_destroy, void,
	linked_list_t *configs)
{
	match_entry_t *entry;

	while (configs->remove_last(configs, (void**)&entry) == SUCCESS)
	{
		entry->cfg->destroy(entry->cfg);
		free(entry);
	}
	configs->destroy(configs);
}

/**
 * Insert entry into match-sorted list
 */
static void insert_sorted(match_entry_t *entry, linked_list_t *list)
{
	enumerator_t *enumerator;
	match_entry_t *current;

	enumerator = list->create_enumerator(list);
	while (enumerator->enumerate(enumerator, &current))
	{
		if ((entry->match_ike > current->match_ike &&
			 entry->match_peer >= current->match_peer) ||
			(entry->match_ike >= current->match_ike &&
			  entry->match_peer > current->match_peer))
		{
			break;
		}
	}
	list->insert_before(list, enumerator, entry);
	enumerator->destroy(enumerator);
}

METHOD(backend_manager_t, create_peer_cfg_enumerator, enumerator_t*,
	private_backend_manager_t *this, host_t *me, host_t *other,
	identification_t *my_id, identification_t *other_id, ike_version_t version)
{
	enumerator_t *enumerator;
	peer_data_t *data;
	peer_cfg_t *cfg;
	linked_list_t *configs;

	INIT(data,
		.lock = this->lock,
		.me = my_id,
		.other = other_id,
	);

	/* create a sorted list with all matches */
	this->lock->read_lock(this->lock);
	enumerator = enumerator_create_nested(
					this->backends->create_enumerator(this->backends),
					(void*)peer_enum_create, data, (void*)peer_enum_destroy);

	if (!me && !other && !my_id && !other_id)
	{	/* shortcut if we are doing a "listall" */
		return enumerator;
	}

	configs = linked_list_create();
	while (enumerator->enumerate(enumerator, &cfg))
	{
		ike_cfg_t *ike_cfg = cfg->get_ike_cfg(cfg);
		ike_cfg_match_t match_ike;
		id_match_t match_peer_me, match_peer_other;
		match_entry_t *entry;

		match_ike = get_ike_match(ike_cfg, me, other, version);
		DBG3(DBG_CFG, "peer config \"%s\", ike match: %d (%s...%s %N)",
			 cfg->get_name(cfg), match_ike, ike_cfg->get_my_addr(ike_cfg),
			 ike_cfg->get_other_addr(ike_cfg), ike_version_names,
			 ike_cfg->get_version(ike_cfg));

		if (!match_ike)
		{
			continue;
		}

		match_peer_me = get_peer_match(my_id, cfg, TRUE);
		if (!match_peer_me)
		{
			continue;
		}
		match_peer_other = get_peer_match(other_id, cfg, FALSE);

		if (match_peer_other)
		{
			DBG2(DBG_CFG, "  candidate \"%s\", match: %d/%d/%d (me/other/ike)",
				 cfg->get_name(cfg), match_peer_me, match_peer_other, match_ike);
			INIT(entry,
				.match_peer = match_peer_me + match_peer_other,
				.match_ike = match_ike,
				.cfg = cfg->get_ref(cfg),
			);
			insert_sorted(entry, configs);
		}
	}
	enumerator->destroy(enumerator);

	return enumerator_create_filter(configs->create_enumerator(configs),
									peer_enum_filter, configs,
									peer_enum_filter_destroy);
}

METHOD(backend_manager_t, get_peer_cfg_by_name, peer_cfg_t*,
	private_backend_manager_t *this, char *name)
{
	backend_t *backend;
	peer_cfg_t *config = NULL;
	enumerator_t *enumerator;

	this->lock->read_lock(this->lock);
	enumerator = this->backends->create_enumerator(this->backends);
	while (config == NULL && enumerator->enumerate(enumerator, (void**)&backend))
	{
		config = backend->get_peer_cfg_by_name(backend, name);
	}
	enumerator->destroy(enumerator);
	this->lock->unlock(this->lock);
	return config;
}

METHOD(backend_manager_t, remove_backend, void,
	private_backend_manager_t *this, backend_t *backend)
{
	this->lock->write_lock(this->lock);
	this->backends->remove(this->backends, backend, NULL);
	this->lock->unlock(this->lock);
}

METHOD(backend_manager_t, add_backend, void,
	private_backend_manager_t *this, backend_t *backend)
{
	this->lock->write_lock(this->lock);
	this->backends->insert_last(this->backends, backend);
	this->lock->unlock(this->lock);
}

METHOD(backend_manager_t, destroy, void,
	private_backend_manager_t *this)
{
	this->backends->destroy(this->backends);
	this->lock->destroy(this->lock);
	free(this);
}

/*
 * Described in header
 */
backend_manager_t *backend_manager_create()
{
	private_backend_manager_t *this;

	INIT(this,
		.public = {
			.get_ike_cfg = _get_ike_cfg,
			.create_ike_cfg_enumerator = _create_ike_cfg_enumerator,
			.get_peer_cfg_by_name = _get_peer_cfg_by_name,
			.create_peer_cfg_enumerator = _create_peer_cfg_enumerator,
			.add_backend = _add_backend,
			.remove_backend = _remove_backend,
			.destroy = _destroy,
		},
		.backends = linked_list_create(),
		.lock = rwlock_create(RWLOCK_TYPE_DEFAULT),
	);

	return &this->public;
}
