/*
 * Copyright (C) 2007-2019 Tobias Brunner
 * Copyright (C) 2005-2009 Martin Willi
 * Copyright (C) 2005 Jan Hutter
 * HSR Hochschule fuer Technik Rapperswil
 *
 * 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 <string.h>

#include "peer_cfg.h"

#include <daemon.h>

#include <threading/rwlock.h>
#include <collections/linked_list.h>
#include <utils/identification.h>

ENUM(cert_policy_names, CERT_ALWAYS_SEND, CERT_NEVER_SEND,
	"CERT_ALWAYS_SEND",
	"CERT_SEND_IF_ASKED",
	"CERT_NEVER_SEND",
);

ENUM(unique_policy_names, UNIQUE_NEVER, UNIQUE_KEEP,
	"UNIQUE_NEVER",
	"UNIQUE_NO",
	"UNIQUE_REPLACE",
	"UNIQUE_KEEP",
);

typedef struct private_peer_cfg_t private_peer_cfg_t;

/**
 * Private data of an peer_cfg_t object
 */
struct private_peer_cfg_t {

	/**
	 * Public part
	 */
	peer_cfg_t public;

	/**
	 * Number of references hold by others to this peer_cfg
	 */
	refcount_t refcount;

	/**
	 * Name of the peer_cfg, used to query it
	 */
	char *name;

	/**
	 * IKE config associated to this peer config
	 */
	ike_cfg_t *ike_cfg;

	/**
	 * list of child configs associated to this peer config
	 */
	linked_list_t *child_cfgs;

	/**
	 * lock to access list of child_cfgs
	 */
	rwlock_t *lock;

	/**
	 * should we send a certificate
	 */
	cert_policy_t cert_policy;

	/**
	 * uniqueness of an IKE_SA
	 */
	unique_policy_t unique;

	/**
	 * number of tries after giving up if peer does not respond
	 */
	uint32_t keyingtries;

	/**
	 * enable support for MOBIKE
	 */
	bool use_mobike;

	/**
	 * Use aggressive mode?
	 */
	bool aggressive;

	/**
	 * Use pull or push in mode config?
	 */
	bool pull_mode;

	/**
	 * Time before starting rekeying
	 */
	uint32_t rekey_time;

	/**
	 * Time before starting reauthentication
	 */
	uint32_t reauth_time;

	/**
	 * Time, which specifies the range of a random value subtracted from above.
	 */
	uint32_t jitter_time;

	/**
	 * Delay before deleting a rekeying/reauthenticating SA
	 */
	uint32_t over_time;

	/**
	 * DPD check interval
	 */
	uint32_t dpd;

	/**
	 * DPD timeout interval (used for IKEv1 only)
	 */
	uint32_t dpd_timeout;

	/**
	 * List of virtual IPs (host_t*) to request
	 */
	linked_list_t *vips;

	/**
	 * List of pool names to use for virtual IP lookup
	 */
	linked_list_t *pools;

	/**
	 * local authentication configs (rulesets)
	 */
	linked_list_t *local_auth;

	/**
	 * remote authentication configs (constraints)
	 */
	linked_list_t *remote_auth;

	/**
	 * Optional interface ID to use for inbound CHILD_SA
	 */
	uint32_t if_id_in;

	/**
	 * Optional interface ID to use for outbound CHILD_SA
	 */
	uint32_t if_id_out;

	/**
	 * PPK ID
	 */
	identification_t *ppk_id;

	/**
	 * Whether a PPK is required
	 */
	bool ppk_required;

#ifdef ME
	/**
	 * Is this a mediation connection?
	 */
	bool mediation;

	/**
	 * Name of the mediation connection to mediate through
	 */
	char *mediated_by;

	/**
	 * ID of our peer at the mediation server (= leftid of the peer's conn with
	 * the mediation server)
	 */
	identification_t *peer_id;
#endif /* ME */
};

METHOD(peer_cfg_t, get_name, char*,
	private_peer_cfg_t *this)
{
	return this->name;
}

METHOD(peer_cfg_t, get_ike_version, ike_version_t,
	private_peer_cfg_t *this)
{
	return this->ike_cfg->get_version(this->ike_cfg);
}

METHOD(peer_cfg_t, get_ike_cfg, ike_cfg_t*,
	private_peer_cfg_t *this)
{
	return this->ike_cfg;
}

METHOD(peer_cfg_t, add_child_cfg, void,
	private_peer_cfg_t *this, child_cfg_t *child_cfg)
{
	this->lock->write_lock(this->lock);
	this->child_cfgs->insert_last(this->child_cfgs, child_cfg);
	this->lock->unlock(this->lock);
}

typedef struct {
	enumerator_t public;
	linked_list_t *removed;
	linked_list_t *added;
	enumerator_t *wrapped;
	bool add;
} child_cfgs_replace_enumerator_t;

METHOD(enumerator_t, child_cfgs_replace_enumerate, bool,
	child_cfgs_replace_enumerator_t *this, va_list args)
{
	child_cfg_t *child_cfg, **chd;
	bool *added;

	VA_ARGS_VGET(args, chd, added);

	if (!this->wrapped)
	{
		this->wrapped = this->removed->create_enumerator(this->removed);
	}
	while (TRUE)
	{
		if (this->wrapped->enumerate(this->wrapped, &child_cfg))
		{
			if (chd)
			{
				*chd = child_cfg;
			}
			if (added)
			{
				*added = this->add;
			}
			return TRUE;
		}
		if (this->add)
		{
			break;
		}
		this->wrapped->destroy(this->wrapped);
		this->wrapped = this->added->create_enumerator(this->added);
		this->add = TRUE;
	}
	return FALSE;
}

METHOD(enumerator_t, child_cfgs_replace_enumerator_destroy, void,
	child_cfgs_replace_enumerator_t *this)
{
	DESTROY_IF(this->wrapped);
	this->removed->destroy_offset(this->removed, offsetof(child_cfg_t, destroy));
	this->added->destroy_offset(this->added, offsetof(child_cfg_t, destroy));
	free(this);
}

METHOD(peer_cfg_t, replace_child_cfgs, enumerator_t*,
	private_peer_cfg_t *this, peer_cfg_t *other_pub)
{
	private_peer_cfg_t *other = (private_peer_cfg_t*)other_pub;
	linked_list_t *new_cfgs, *removed, *added;
	enumerator_t *mine, *others;
	child_cfg_t *my_cfg, *other_cfg;
	child_cfgs_replace_enumerator_t *enumerator;
	bool found;

	added = linked_list_create();

	other->lock->read_lock(other->lock);
	new_cfgs = linked_list_create_from_enumerator(
					other->child_cfgs->create_enumerator(other->child_cfgs));
	new_cfgs->invoke_offset(new_cfgs, offsetof(child_cfg_t, get_ref));
	other->lock->unlock(other->lock);

	this->lock->write_lock(this->lock);
	removed = this->child_cfgs;
	this->child_cfgs = new_cfgs;
	others = new_cfgs->create_enumerator(new_cfgs);
	mine = removed->create_enumerator(removed);
	while (others->enumerate(others, &other_cfg))
	{
		found = FALSE;
		while (mine->enumerate(mine, &my_cfg))
		{
			if (my_cfg->equals(my_cfg, other_cfg))
			{
				removed->remove_at(removed, mine);
				my_cfg->destroy(my_cfg);
				found = TRUE;
				break;
			}
		}
		removed->reset_enumerator(removed, mine);
		if (!found)
		{
			added->insert_last(added, other_cfg->get_ref(other_cfg));
		}
	}
	others->destroy(others);
	mine->destroy(mine);
	this->lock->unlock(this->lock);

	INIT(enumerator,
		.public = {
			.enumerate = enumerator_enumerate_default,
			.venumerate = _child_cfgs_replace_enumerate,
			.destroy = _child_cfgs_replace_enumerator_destroy,
		},
		.removed = removed,
		.added = added,
	);
	return &enumerator->public;
}

/**
 * child_cfg enumerator
 */
typedef struct {
	enumerator_t public;
	enumerator_t *wrapped;
	rwlock_t *lock;
} child_cfg_enumerator_t;

METHOD(peer_cfg_t, remove_child_cfg, void,
	private_peer_cfg_t *this, child_cfg_enumerator_t *enumerator)
{
	this->child_cfgs->remove_at(this->child_cfgs, enumerator->wrapped);
}

METHOD(enumerator_t, child_cfg_enumerator_destroy, void,
	child_cfg_enumerator_t *this)
{
	this->lock->unlock(this->lock);
	this->wrapped->destroy(this->wrapped);
	free(this);
}

METHOD(enumerator_t, child_cfg_enumerate, bool,
	child_cfg_enumerator_t *this, va_list args)
{
	child_cfg_t **chd;

	VA_ARGS_VGET(args, chd);
	return this->wrapped->enumerate(this->wrapped, chd);
}

METHOD(peer_cfg_t, create_child_cfg_enumerator, enumerator_t*,
	private_peer_cfg_t *this)
{
	child_cfg_enumerator_t *enumerator;

	INIT(enumerator,
		.public = {
			.enumerate = enumerator_enumerate_default,
			.venumerate = _child_cfg_enumerate,
			.destroy = _child_cfg_enumerator_destroy,
		},
		.lock = this->lock,
		.wrapped = this->child_cfgs->create_enumerator(this->child_cfgs),
	);

	this->lock->read_lock(this->lock);
	return &enumerator->public;
}

/**
 * Check how good a list of TS matches a given child config
 */
static int get_ts_match(child_cfg_t *cfg, bool local,
						linked_list_t *sup_list, linked_list_t *hosts)
{
	linked_list_t *cfg_list;
	enumerator_t *sup_enum, *cfg_enum;
	traffic_selector_t *sup_ts, *cfg_ts, *subset;
	int match = 0, round;

	/* fetch configured TS list, narrowing dynamic TS */
	cfg_list = cfg->get_traffic_selectors(cfg, local, NULL, hosts, TRUE);

	/* use a round counter to rate leading TS with higher priority */
	round = sup_list->get_count(sup_list);

	sup_enum = sup_list->create_enumerator(sup_list);
	while (sup_enum->enumerate(sup_enum, &sup_ts))
	{
		cfg_enum = cfg_list->create_enumerator(cfg_list);
		while (cfg_enum->enumerate(cfg_enum, &cfg_ts))
		{
			if (cfg_ts->equals(cfg_ts, sup_ts))
			{	/* equality is honored better than matches */
				match += round * 5;
			}
			else
			{
				subset = cfg_ts->get_subset(cfg_ts, sup_ts);
				if (subset)
				{
					subset->destroy(subset);
					match += round * 1;
				}
			}
		}
		cfg_enum->destroy(cfg_enum);
		round--;
	}
	sup_enum->destroy(sup_enum);

	cfg_list->destroy_offset(cfg_list, offsetof(traffic_selector_t, destroy));

	return match;
}

METHOD(peer_cfg_t, select_child_cfg, child_cfg_t*,
	private_peer_cfg_t *this, linked_list_t *my_ts, linked_list_t *other_ts,
	linked_list_t *my_hosts, linked_list_t *other_hosts)
{
	child_cfg_t *current, *found = NULL;
	enumerator_t *enumerator;
	int best = 0;

	DBG2(DBG_CFG, "looking for a child config for %#R === %#R", my_ts, other_ts);
	enumerator = create_child_cfg_enumerator(this);
	while (enumerator->enumerate(enumerator, &current))
	{
		int my_prio, other_prio;

		my_prio = get_ts_match(current, TRUE, my_ts, my_hosts);
		other_prio = get_ts_match(current, FALSE, other_ts, other_hosts);

		if (my_prio && other_prio)
		{
			DBG2(DBG_CFG, "  candidate \"%s\" with prio %d+%d",
				 current->get_name(current), my_prio, other_prio);
			if (my_prio + other_prio > best)
			{
				best = my_prio + other_prio;
				DESTROY_IF(found);
				found = current->get_ref(current);
			}
		}
	}
	enumerator->destroy(enumerator);
	if (found)
	{
		DBG2(DBG_CFG, "found matching child config \"%s\" with prio %d",
			 found->get_name(found), best);
	}
	return found;
}

METHOD(peer_cfg_t, get_cert_policy, cert_policy_t,
	private_peer_cfg_t *this)
{
	return this->cert_policy;
}

METHOD(peer_cfg_t, get_unique_policy, unique_policy_t,
	private_peer_cfg_t *this)
{
	return this->unique;
}

METHOD(peer_cfg_t, get_keyingtries, uint32_t,
	private_peer_cfg_t *this)
{
	return this->keyingtries;
}

METHOD(peer_cfg_t, get_rekey_time, uint32_t,
	private_peer_cfg_t *this, bool jitter)
{
	if (this->rekey_time == 0)
	{
		return 0;
	}
	if (this->jitter_time == 0 || !jitter)
	{
		return this->rekey_time;
	}
	return this->rekey_time - (random() % this->jitter_time);
}

METHOD(peer_cfg_t, get_reauth_time, uint32_t,
	private_peer_cfg_t *this, bool jitter)
{
	if (this->reauth_time == 0)
	{
		return 0;
	}
	if (this->jitter_time == 0 || !jitter)
	{
		return this->reauth_time;
	}
	return this->reauth_time - (random() % this->jitter_time);
}

METHOD(peer_cfg_t, get_over_time, uint32_t,
	private_peer_cfg_t *this)
{
	return this->over_time;
}

METHOD(peer_cfg_t, use_mobike, bool,
	private_peer_cfg_t *this)
{
	return this->use_mobike;
}

METHOD(peer_cfg_t, use_aggressive, bool,
	private_peer_cfg_t *this)
{
	return this->aggressive;
}

METHOD(peer_cfg_t, use_pull_mode, bool,
	private_peer_cfg_t *this)
{
	return this->pull_mode;
}

METHOD(peer_cfg_t, get_dpd, uint32_t,
	private_peer_cfg_t *this)
{
	return this->dpd;
}

METHOD(peer_cfg_t, get_dpd_timeout, uint32_t,
	private_peer_cfg_t *this)
{
	return this->dpd_timeout;
}

METHOD(peer_cfg_t, add_virtual_ip, void,
	private_peer_cfg_t *this, host_t *vip)
{
	this->vips->insert_last(this->vips, vip);
}

METHOD(peer_cfg_t, create_virtual_ip_enumerator, enumerator_t*,
	private_peer_cfg_t *this)
{
	return this->vips->create_enumerator(this->vips);
}

METHOD(peer_cfg_t, add_pool, void,
	private_peer_cfg_t *this, char *name)
{
	this->pools->insert_last(this->pools, strdup(name));
}

METHOD(peer_cfg_t, create_pool_enumerator, enumerator_t*,
	private_peer_cfg_t *this)
{
	return this->pools->create_enumerator(this->pools);
}

METHOD(peer_cfg_t, add_auth_cfg, void,
	private_peer_cfg_t *this, auth_cfg_t *cfg, bool local)
{
	if (local)
	{
		this->local_auth->insert_last(this->local_auth, cfg);
	}
	else
	{
		this->remote_auth->insert_last(this->remote_auth, cfg);
	}
}

METHOD(peer_cfg_t, create_auth_cfg_enumerator, enumerator_t*,
	private_peer_cfg_t *this, bool local)
{
	if (local)
	{
		return this->local_auth->create_enumerator(this->local_auth);
	}
	return this->remote_auth->create_enumerator(this->remote_auth);
}

METHOD(peer_cfg_t, get_if_id, uint32_t,
	private_peer_cfg_t *this, bool inbound)
{
	return inbound ? this->if_id_in : this->if_id_out;
}

METHOD(peer_cfg_t, get_ppk_id, identification_t*,
	private_peer_cfg_t *this)
{
	return this->ppk_id;
}

METHOD(peer_cfg_t, ppk_required, bool,
	private_peer_cfg_t *this)
{
	return this->ppk_required;
}

#ifdef ME
METHOD(peer_cfg_t, is_mediation, bool,
	private_peer_cfg_t *this)
{
	return this->mediation;
}

METHOD(peer_cfg_t, get_mediated_by, char*,
	private_peer_cfg_t *this)
{
	return this->mediated_by;
}

METHOD(peer_cfg_t, get_peer_id, identification_t*,
	private_peer_cfg_t *this)
{
	return this->peer_id;
}
#endif /* ME */

/**
 * check auth configs for equality
 */
static bool auth_cfg_equal(private_peer_cfg_t *this, private_peer_cfg_t *other)
{
	enumerator_t *e1, *e2;
	auth_cfg_t *cfg1, *cfg2;
	bool equal = TRUE;

	if (this->local_auth->get_count(this->local_auth) !=
		other->local_auth->get_count(other->local_auth))
	{
		return FALSE;
	}
	if (this->remote_auth->get_count(this->remote_auth) !=
		other->remote_auth->get_count(other->remote_auth))
	{
		return FALSE;
	}

	e1 = this->local_auth->create_enumerator(this->local_auth);
	e2 = other->local_auth->create_enumerator(other->local_auth);
	while (e1->enumerate(e1, &cfg1) && e2->enumerate(e2, &cfg2))
	{
		if (!cfg1->equals(cfg1, cfg2))
		{
			equal = FALSE;
			break;
		}
	}
	e1->destroy(e1);
	e2->destroy(e2);

	if (!equal)
	{
		return FALSE;
	}

	e1 = this->remote_auth->create_enumerator(this->remote_auth);
	e2 = other->remote_auth->create_enumerator(other->remote_auth);
	while (e1->enumerate(e1, &cfg1) && e2->enumerate(e2, &cfg2))
	{
		if (!cfg1->equals(cfg1, cfg2))
		{
			equal = FALSE;
			break;
		}
	}
	e1->destroy(e1);
	e2->destroy(e2);

	return equal;
}

/**
 * Check if two identities are equal, or both are not set
 */
static bool id_equal(identification_t *this, identification_t *other)
{
	return this == other || (this && other && this->equals(this, other));
}

METHOD(peer_cfg_t, equals, bool,
	private_peer_cfg_t *this, private_peer_cfg_t *other)
{
	if (this == other)
	{
		return TRUE;
	}
	if (this->public.equals != other->public.equals)
	{
		return FALSE;
	}
	if (!this->vips->equals_offset(this->vips, other->vips,
								   offsetof(host_t, ip_equals)))
	{
		return FALSE;
	}
	if (!this->pools->equals_function(this->pools, other->pools, (void*)streq))
	{
		return FALSE;
	}
	return (
		get_ike_version(this) == get_ike_version(other) &&
		this->cert_policy == other->cert_policy &&
		this->unique == other->unique &&
		this->keyingtries == other->keyingtries &&
		this->use_mobike == other->use_mobike &&
		this->rekey_time == other->rekey_time &&
		this->reauth_time == other->reauth_time &&
		this->jitter_time == other->jitter_time &&
		this->over_time == other->over_time &&
		this->dpd == other->dpd &&
		this->aggressive == other->aggressive &&
		this->pull_mode == other->pull_mode &&
		auth_cfg_equal(this, other) &&
		this->if_id_in == other->if_id_in &&
		this->if_id_out == other->if_id_out &&
		this->ppk_required == other->ppk_required &&
		id_equal(this->ppk_id, other->ppk_id)
#ifdef ME
		&& this->mediation == other->mediation &&
		streq(this->mediated_by, other->mediated_by) &&
		id_equal(this->peer_id, other->peer_id)
#endif /* ME */
		);
}

METHOD(peer_cfg_t, get_ref, peer_cfg_t*,
	private_peer_cfg_t *this)
{
	ref_get(&this->refcount);
	return &this->public;
}

METHOD(peer_cfg_t, destroy, void,
	private_peer_cfg_t *this)
{
	if (ref_put(&this->refcount))
	{
		this->ike_cfg->destroy(this->ike_cfg);
		this->child_cfgs->destroy_offset(this->child_cfgs,
										offsetof(child_cfg_t, destroy));
		this->local_auth->destroy_offset(this->local_auth,
										offsetof(auth_cfg_t, destroy));
		this->remote_auth->destroy_offset(this->remote_auth,
										offsetof(auth_cfg_t, destroy));
		this->vips->destroy_offset(this->vips, offsetof(host_t, destroy));
		this->pools->destroy_function(this->pools, free);
#ifdef ME
		DESTROY_IF(this->peer_id);
		free(this->mediated_by);
#endif /* ME */
		DESTROY_IF(this->ppk_id);
		this->lock->destroy(this->lock);
		free(this->name);
		free(this);
	}
}

/*
 * Described in header-file
 */
peer_cfg_t *peer_cfg_create(char *name, ike_cfg_t *ike_cfg,
							peer_cfg_create_t *data)
{
	private_peer_cfg_t *this;

	if (data->rekey_time && data->jitter_time > data->rekey_time)
	{
		data->jitter_time = data->rekey_time;
	}
	if (data->reauth_time && data->jitter_time > data->reauth_time)
	{
		data->jitter_time = data->reauth_time;
	}
	if (data->dpd && data->dpd_timeout && data->dpd > data->dpd_timeout)
	{
		data->dpd_timeout = data->dpd;
	}

	INIT(this,
		.public = {
			.get_name = _get_name,
			.get_ike_version = _get_ike_version,
			.get_ike_cfg = _get_ike_cfg,
			.add_child_cfg = _add_child_cfg,
			.remove_child_cfg = (void*)_remove_child_cfg,
			.replace_child_cfgs = _replace_child_cfgs,
			.create_child_cfg_enumerator = _create_child_cfg_enumerator,
			.select_child_cfg = _select_child_cfg,
			.get_cert_policy = _get_cert_policy,
			.get_unique_policy = _get_unique_policy,
			.get_keyingtries = _get_keyingtries,
			.get_rekey_time = _get_rekey_time,
			.get_reauth_time = _get_reauth_time,
			.get_over_time = _get_over_time,
			.use_mobike = _use_mobike,
			.use_aggressive = _use_aggressive,
			.use_pull_mode = _use_pull_mode,
			.get_dpd = _get_dpd,
			.get_dpd_timeout = _get_dpd_timeout,
			.add_virtual_ip = _add_virtual_ip,
			.create_virtual_ip_enumerator = _create_virtual_ip_enumerator,
			.add_pool = _add_pool,
			.create_pool_enumerator = _create_pool_enumerator,
			.add_auth_cfg = _add_auth_cfg,
			.create_auth_cfg_enumerator = _create_auth_cfg_enumerator,
			.get_if_id = _get_if_id,
			.get_ppk_id = _get_ppk_id,
			.ppk_required = _ppk_required,
			.equals = (void*)_equals,
			.get_ref = _get_ref,
			.destroy = _destroy,
#ifdef ME
			.is_mediation = _is_mediation,
			.get_mediated_by = _get_mediated_by,
			.get_peer_id = _get_peer_id,
#endif /* ME */
		},
		.name = strdup(name),
		.ike_cfg = ike_cfg,
		.child_cfgs = linked_list_create(),
		.lock = rwlock_create(RWLOCK_TYPE_DEFAULT),
		.cert_policy = data->cert_policy,
		.unique = data->unique,
		.keyingtries = data->keyingtries,
		.rekey_time = data->rekey_time,
		.reauth_time = data->reauth_time,
		.jitter_time = data->jitter_time,
		.over_time = data->over_time,
		.use_mobike = !data->no_mobike,
		.aggressive = data->aggressive,
		.pull_mode = !data->push_mode,
		.dpd = data->dpd,
		.dpd_timeout = data->dpd_timeout,
		.if_id_in = data->if_id_in,
		.if_id_out = data->if_id_out,
		.ppk_id = data->ppk_id,
		.ppk_required = data->ppk_required,
		.vips = linked_list_create(),
		.pools = linked_list_create(),
		.local_auth = linked_list_create(),
		.remote_auth = linked_list_create(),
		.refcount = 1,
#ifdef ME
		.mediation = data->mediation,
		.mediated_by = strdupnull(data->mediated_by),
		.peer_id = data->peer_id,
#endif /* ME */
	);

	return &this->public;
}
