/*
 * Copyright (C) 2015-2017 Tobias Brunner
 * HSR Hochschule fuer Technik Rapperswil
 *
 * Copyright (C) 2014 Martin Willi
 * Copyright (C) 2014 revosec 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 "vici_control.h"
#include "vici_builder.h"

#include <inttypes.h>

#include <daemon.h>
#include <collections/array.h>
#include <processing/jobs/rekey_ike_sa_job.h>
#include <processing/jobs/rekey_child_sa_job.h>
#include <processing/jobs/redirect_job.h>

typedef struct private_vici_control_t private_vici_control_t;

/**
 * Private data of an vici_control_t object.
 */
struct private_vici_control_t {

	/**
	 * Public vici_control_t interface.
	 */
	vici_control_t public;

	/**
	 * Dispatcher
	 */
	vici_dispatcher_t *dispatcher;
};

/**
 * Log callback helper data
 */
typedef struct {
	/** dispatcher to send log messages over */
	vici_dispatcher_t *dispatcher;
	/** connection ID to send messages to */
	u_int id;
	/** loglevel */
	level_t level;
	/** prevent recursive log */
	u_int recursive;
} log_info_t;

/**
 * Log using vici event messages
 */
static bool log_vici(log_info_t *info, debug_t group, level_t level,
					 ike_sa_t *ike_sa, char *text)
{
	if (level <= info->level)
	{
		if (info->recursive++ == 0)
		{
			vici_message_t *message;
			vici_builder_t *builder;

			builder = vici_builder_create();
			builder->add_kv(builder, "group", "%N", debug_names, group);
			builder->add_kv(builder, "level", "%d", level);
			if (ike_sa)
			{
				builder->add_kv(builder, "ikesa-name", "%s",
								ike_sa->get_name(ike_sa));
				builder->add_kv(builder, "ikesa-uniqueid", "%u",
								ike_sa->get_unique_id(ike_sa));
			}
			builder->add_kv(builder, "msg", "%s", text);

			message = builder->finalize(builder);
			if (message)
			{
				info->dispatcher->raise_event(info->dispatcher, "control-log",
											  info->id, message);
			}
		}
		info->recursive--;
	}
	return TRUE;
}

/**
 * Send a (error) reply message
 */
static vici_message_t* send_reply(private_vici_control_t *this, char *fmt, ...)
{
	vici_builder_t *builder;
	va_list args;

	builder = vici_builder_create();
	builder->add_kv(builder, "success", fmt ? "no" : "yes");
	if (fmt)
	{
		va_start(args, fmt);
		builder->vadd_kv(builder, "errmsg", fmt, args);
		va_end(args);
	}
	return builder->finalize(builder);
}

/**
 * Get the child_cfg having name from peer_cfg
 */
static child_cfg_t* get_child_from_peer(peer_cfg_t *peer_cfg, char *name)
{
	child_cfg_t *current, *found = NULL;
	enumerator_t *enumerator;

	enumerator = peer_cfg->create_child_cfg_enumerator(peer_cfg);
	while (enumerator->enumerate(enumerator, &current))
	{
		if (streq(current->get_name(current), name))
		{
			found = current;
			found->get_ref(found);
			break;
		}
	}
	enumerator->destroy(enumerator);
	return found;
}

/**
 * Find a peer/child config from a config name
 */
static child_cfg_t* find_child_cfg(char *name, char *pname, peer_cfg_t **out)
{
	enumerator_t *enumerator;
	peer_cfg_t *peer_cfg;
	child_cfg_t *child_cfg = NULL;

	enumerator = charon->backends->create_peer_cfg_enumerator(
							charon->backends, NULL, NULL, NULL, NULL, IKE_ANY);
	while (enumerator->enumerate(enumerator, &peer_cfg))
	{
		if (pname && !streq(pname, peer_cfg->get_name(peer_cfg)))
		{
			continue;
		}
		if (!name)
		{
			*out = peer_cfg->get_ref(peer_cfg);
			break;
		}
		child_cfg = get_child_from_peer(peer_cfg, name);
		if (child_cfg)
		{
			*out = peer_cfg->get_ref(peer_cfg);
			break;
		}
	}
	enumerator->destroy(enumerator);

	return child_cfg;
}

CALLBACK(initiate, vici_message_t*,
	private_vici_control_t *this, char *name, u_int id, vici_message_t *request)
{
	peer_cfg_t *peer_cfg = NULL;
	child_cfg_t *child_cfg;
	char *child, *ike, *type, *sa;
	int timeout;
	bool limits;
	controller_cb_t log_cb = NULL;
	log_info_t log = {
		.dispatcher = this->dispatcher,
		.id = id,
	};

	child = request->get_str(request, NULL, "child");
	ike = request->get_str(request, NULL, "ike");
	timeout = request->get_int(request, 0, "timeout");
	limits = request->get_bool(request, FALSE, "init-limits");
	log.level = request->get_int(request, 1, "loglevel");

	if (!child && !ike)
	{
		return send_reply(this, "missing configuration name");
	}
	if (timeout >= 0)
	{
		log_cb = (controller_cb_t)log_vici;
	}

	type = child ? "CHILD_SA" : "IKE_SA";
	sa = child ?: ike;

	child_cfg = find_child_cfg(child, ike, &peer_cfg);

	DBG1(DBG_CFG, "vici initiate %s '%s'", type, sa);
	if (!peer_cfg)
	{
		return send_reply(this, "%s config '%s' not found", type, sa);
	}
	switch (charon->controller->initiate(charon->controller, peer_cfg,
									child_cfg, log_cb, &log, timeout, limits))
	{
		case SUCCESS:
			return send_reply(this, NULL);
		case OUT_OF_RES:
			return send_reply(this, "%s '%s' not established after %dms", type,
							  sa, timeout);
		case INVALID_STATE:
			return send_reply(this, "establishing %s '%s' not possible at the "
							  "moment due to limits", type, sa);
		case FAILED:
		default:
			return send_reply(this, "establishing %s '%s' failed", type, sa);
	}
}

CALLBACK(terminate, vici_message_t*,
	private_vici_control_t *this, char *name, u_int id, vici_message_t *request)
{
	enumerator_t *enumerator, *isas, *csas;
	char *child, *ike, *errmsg = NULL;
	u_int child_id, ike_id, current, *del, done = 0;
	bool force;
	int timeout;
	ike_sa_t *ike_sa;
	child_sa_t *child_sa;
	array_t *ids;
	vici_builder_t *builder;
	controller_cb_t log_cb = NULL;
	log_info_t log = {
		.dispatcher = this->dispatcher,
		.id = id,
	};

	child = request->get_str(request, NULL, "child");
	ike = request->get_str(request, NULL, "ike");
	child_id = request->get_int(request, 0, "child-id");
	ike_id = request->get_int(request, 0, "ike-id");
	force = request->get_bool(request, FALSE, "force");
	timeout = request->get_int(request, 0, "timeout");
	log.level = request->get_int(request, 1, "loglevel");

	if (!child && !ike && !ike_id && !child_id)
	{
		return send_reply(this, "missing terminate selector");
	}

	if (ike_id)
	{
		DBG1(DBG_CFG, "vici terminate IKE_SA #%d", ike_id);
	}
	if (child_id)
	{
		DBG1(DBG_CFG, "vici terminate CHILD_SA #%d", child_id);
	}
	if (ike)
	{
		DBG1(DBG_CFG, "vici terminate IKE_SA '%s'", ike);
	}
	if (child)
	{
		DBG1(DBG_CFG, "vici terminate CHILD_SA '%s'", child);
	}

	if (timeout >= 0)
	{
		log_cb = (controller_cb_t)log_vici;
	}

	ids = array_create(sizeof(u_int), 0);

	isas = charon->controller->create_ike_sa_enumerator(charon->controller, TRUE);
	while (isas->enumerate(isas, &ike_sa))
	{
		if (child || child_id)
		{
			if (ike && !streq(ike, ike_sa->get_name(ike_sa)))
			{
				continue;
			}
			if (ike_id && ike_id != ike_sa->get_unique_id(ike_sa))
			{
				continue;
			}
			csas = ike_sa->create_child_sa_enumerator(ike_sa);
			while (csas->enumerate(csas, &child_sa))
			{
				if (child && !streq(child, child_sa->get_name(child_sa)))
				{
					continue;
				}
				if (child_id && child_sa->get_unique_id(child_sa) != child_id)
				{
					continue;
				}
				current = child_sa->get_unique_id(child_sa);
				array_insert(ids, ARRAY_TAIL, &current);
			}
			csas->destroy(csas);
		}
		else if (ike && streq(ike, ike_sa->get_name(ike_sa)))
		{
			current = ike_sa->get_unique_id(ike_sa);
			array_insert(ids, ARRAY_TAIL, &current);
		}
		else if (ike_id && ike_id == ike_sa->get_unique_id(ike_sa))
		{
			array_insert(ids, ARRAY_TAIL, &ike_id);
		}
	}
	isas->destroy(isas);

	enumerator = array_create_enumerator(ids);
	while (enumerator->enumerate(enumerator, &del))
	{
		if (child || child_id)
		{
			if (charon->controller->terminate_child(charon->controller, *del,
											log_cb, &log, timeout) == SUCCESS)
			{
				done++;
			}
		}
		else
		{
			if (charon->controller->terminate_ike(charon->controller, *del, force,
											log_cb, &log, timeout) == SUCCESS)
			{
				done++;
			}
		}
	}
	enumerator->destroy(enumerator);

	builder = vici_builder_create();
	if (array_count(ids) == 0)
	{
		errmsg = "no matching SAs to terminate found";
	}
	else if (done < array_count(ids))
	{
		if (array_count(ids) == 1)
		{
			errmsg = "terminating SA failed";
		}
		else
		{
			errmsg = "not all matching SAs could be terminated";
		}
	}
	builder->add_kv(builder, "success", errmsg ? "no" : "yes");
	builder->add_kv(builder, "matches", "%u", array_count(ids));
	builder->add_kv(builder, "terminated", "%u", done);
	if (errmsg)
	{
		builder->add_kv(builder, "errmsg", "%s", errmsg);
	}
	array_destroy(ids);
	return builder->finalize(builder);
}

CALLBACK(rekey, vici_message_t*,
	private_vici_control_t *this, char *name, u_int id, vici_message_t *request)
{
	enumerator_t *isas, *csas;
	char *child, *ike, *errmsg = NULL;
	u_int child_id, ike_id, found = 0;
	ike_sa_t *ike_sa;
	child_sa_t *child_sa;
	vici_builder_t *builder;
	bool reauth;

	child = request->get_str(request, NULL, "child");
	ike = request->get_str(request, NULL, "ike");
	child_id = request->get_int(request, 0, "child-id");
	ike_id = request->get_int(request, 0, "ike-id");
	reauth = request->get_bool(request, FALSE, "reauth");

	if (!child && !ike && !ike_id && !child_id)
	{
		return send_reply(this, "missing rekey selector");
	}

	if (ike_id)
	{
		DBG1(DBG_CFG, "vici rekey IKE_SA #%d", ike_id);
	}
	if (child_id)
	{
		DBG1(DBG_CFG, "vici rekey CHILD_SA #%d", child_id);
	}
	if (ike)
	{
		DBG1(DBG_CFG, "vici rekey IKE_SA '%s'", ike);
	}
	if (child)
	{
		DBG1(DBG_CFG, "vici rekey CHILD_SA '%s'", child);
	}

	isas = charon->controller->create_ike_sa_enumerator(charon->controller, TRUE);
	while (isas->enumerate(isas, &ike_sa))
	{
		if (child || child_id)
		{
			if (ike && !streq(ike, ike_sa->get_name(ike_sa)))
			{
				continue;
			}
			if (ike_id && ike_id != ike_sa->get_unique_id(ike_sa))
			{
				continue;
			}
			csas = ike_sa->create_child_sa_enumerator(ike_sa);
			while (csas->enumerate(csas, &child_sa))
			{
				if (child && !streq(child, child_sa->get_name(child_sa)))
				{
					continue;
				}
				if (child_id && child_sa->get_unique_id(child_sa) != child_id)
				{
					continue;
				}
				lib->processor->queue_job(lib->processor,
						(job_t*)rekey_child_sa_job_create(
											child_sa->get_protocol(child_sa),
											child_sa->get_spi(child_sa, TRUE),
											ike_sa->get_my_host(ike_sa)));
				found++;
			}
			csas->destroy(csas);
		}
		else if ((ike && streq(ike, ike_sa->get_name(ike_sa))) ||
				 (ike_id && ike_id == ike_sa->get_unique_id(ike_sa)))
		{
			lib->processor->queue_job(lib->processor,
				(job_t*)rekey_ike_sa_job_create(ike_sa->get_id(ike_sa), reauth));
			found++;
		}
	}
	isas->destroy(isas);

	builder = vici_builder_create();
	if (!found)
	{
		errmsg = "no matching SAs to rekey found";
	}
	builder->add_kv(builder, "success", errmsg ? "no" : "yes");
	builder->add_kv(builder, "matches", "%u", found);
	if (errmsg)
	{
		builder->add_kv(builder, "errmsg", "%s", errmsg);
	}
	return builder->finalize(builder);
}

/**
 * Parse a peer-ip specified, which can be a subnet in CIDR notation, a range
 * or a single IP address.
 */
static traffic_selector_t *parse_peer_ip(char *ip)
{
	traffic_selector_t *ts;
	host_t *from, *to;
	ts_type_t type;

	if (host_create_from_range(ip, &from, &to))
	{
		if (to->get_family(to) == AF_INET)
		{
			type = TS_IPV4_ADDR_RANGE;
		}
		else
		{
			type = TS_IPV6_ADDR_RANGE;
		}
		ts = traffic_selector_create_from_bytes(0, type,
												from->get_address(from), 0,
												to->get_address(to), 0xFFFF);
		from->destroy(from);
		to->destroy(to);
		return ts;
	}
	return traffic_selector_create_from_cidr(ip, 0, 0, 0xFFFF);
}

CALLBACK(redirect, vici_message_t*,
	private_vici_control_t *this, char *name, u_int id, vici_message_t *request)
{
	enumerator_t *sas;
	char *ike, *peer_ip, *peer_id, *gw, *errmsg = NULL;
	u_int ike_id, current, found = 0;
	identification_t *gateway, *identity = NULL, *other_id;
	traffic_selector_t *ts = NULL;
	ike_sa_t *ike_sa;
	vici_builder_t *builder;

	ike = request->get_str(request, NULL, "ike");
	ike_id = request->get_int(request, 0, "ike-id");
	peer_ip = request->get_str(request, NULL, "peer-ip");
	peer_id = request->get_str(request, NULL, "peer-id");
	gw = request->get_str(request, NULL, "gateway");

	if (!gw || !(gateway = identification_create_from_string(gw)))
	{
		return send_reply(this, "missing target gateway");
	}
	switch (gateway->get_type(gateway))
	{
		case ID_IPV4_ADDR:
		case ID_IPV6_ADDR:
		case ID_FQDN:
			break;
		default:
			return send_reply(this, "unsupported gateway identity");
	}
	if (peer_ip)
	{
		ts = parse_peer_ip(peer_ip);
		if (!ts)
		{
			return send_reply(this, "invalid peer IP selector");
		}
		DBG1(DBG_CFG, "vici redirect IKE_SAs with src %R to %Y", ts,
			 gateway);
	}
	if (peer_id)
	{
		identity = identification_create_from_string(peer_id);
		if (!identity)
		{
			DESTROY_IF(ts);
			return send_reply(this, "invalid peer identity selector");
		}
		DBG1(DBG_CFG, "vici redirect IKE_SAs with ID '%Y' to %Y", identity,
			 gateway);
	}
	if (ike_id)
	{
		DBG1(DBG_CFG, "vici redirect IKE_SA #%d to '%Y'", ike_id, gateway);
	}
	if (ike)
	{
		DBG1(DBG_CFG, "vici redirect IKE_SA '%s' to '%Y'", ike, gateway);
	}
	if (!peer_ip && !peer_id && !ike && !ike_id)
	{
		return send_reply(this, "missing redirect selector");
	}

	sas = charon->controller->create_ike_sa_enumerator(charon->controller, TRUE);
	while (sas->enumerate(sas, &ike_sa))
	{
		if (ike_sa->get_version(ike_sa) != IKEV2)
		{
			continue;
		}
		current = ike_sa->get_unique_id(ike_sa);
		if (ike_id && ike_id != current)
		{
			continue;
		}
		if (ike && !streq(ike, ike_sa->get_name(ike_sa)))
		{
			continue;
		}
		if (ts && !ts->includes(ts, ike_sa->get_other_host(ike_sa)))
		{
			continue;
		}
		if (identity)
		{
			other_id = ike_sa->get_other_eap_id(ike_sa);
			if (!other_id->matches(other_id, identity))
			{
				continue;
			}
		}
		lib->processor->queue_job(lib->processor,
				(job_t*)redirect_job_create(ike_sa->get_id(ike_sa), gateway));
		found++;
	}
	sas->destroy(sas);

	builder = vici_builder_create();
	if (!found)
	{
		errmsg = "no matching SAs to redirect found";
	}
	builder->add_kv(builder, "success", errmsg ? "no" : "yes");
	builder->add_kv(builder, "matches", "%u", found);
	if (errmsg)
	{
		builder->add_kv(builder, "errmsg", "%s", errmsg);
	}
	gateway->destroy(gateway);
	DESTROY_IF(identity);
	DESTROY_IF(ts);
	return builder->finalize(builder);
}

CALLBACK(install, vici_message_t*,
	private_vici_control_t *this, char *name, u_int id, vici_message_t *request)
{
	child_cfg_t *child_cfg = NULL;
	peer_cfg_t *peer_cfg;
	char *child, *ike;
	bool ok;

	child = request->get_str(request, NULL, "child");
	ike = request->get_str(request, NULL, "ike");
	if (!child)
	{
		return send_reply(this, "missing configuration name");
	}

	DBG1(DBG_CFG, "vici install '%s'", child);

	child_cfg = find_child_cfg(child, ike, &peer_cfg);
	if (!child_cfg)
	{
		return send_reply(this, "configuration name not found");
	}
	switch (child_cfg->get_mode(child_cfg))
	{
		case MODE_PASS:
		case MODE_DROP:
			ok = charon->shunts->install(charon->shunts,
									peer_cfg->get_name(peer_cfg), child_cfg);
			break;
		default:
			ok = charon->traps->install(charon->traps, peer_cfg, child_cfg);
			break;
	}
	peer_cfg->destroy(peer_cfg);
	child_cfg->destroy(child_cfg);

	return send_reply(this, ok ? NULL : "installing policy '%s' failed", child);
}

CALLBACK(uninstall, vici_message_t*,
	private_vici_control_t *this, char *name, u_int id, vici_message_t *request)
{
	char *child, *ike;

	child = request->get_str(request, NULL, "child");
	ike = request->get_str(request, NULL, "ike");
	if (!child)
	{
		return send_reply(this, "missing configuration name");
	}

	DBG1(DBG_CFG, "vici uninstall '%s'", child);

	if (charon->shunts->uninstall(charon->shunts, ike, child))
	{
		return send_reply(this, NULL);
	}
	else if (charon->traps->uninstall(charon->traps, ike, child))
	{
		return send_reply(this, NULL);
	}
	return send_reply(this, "policy '%s' not found", child);
}

CALLBACK(reload_settings, vici_message_t*,
	private_vici_control_t *this, char *name, u_int id, vici_message_t *request)
{
	if (lib->settings->load_files(lib->settings, lib->conf, FALSE))
	{
		charon->load_loggers(charon);
		lib->plugins->reload(lib->plugins, NULL);
		return send_reply(this, NULL);
	}
	return send_reply(this, "reloading '%s' failed", lib->conf);
}

static void manage_command(private_vici_control_t *this,
						   char *name, vici_command_cb_t cb, bool reg)
{
	this->dispatcher->manage_command(this->dispatcher, name,
									 reg ? cb : NULL, this);
}

/**
 * (Un-)register dispatcher functions
 */
static void manage_commands(private_vici_control_t *this, bool reg)
{
	manage_command(this, "initiate", initiate, reg);
	manage_command(this, "terminate", terminate, reg);
	manage_command(this, "rekey", rekey, reg);
	manage_command(this, "redirect", redirect, reg);
	manage_command(this, "install", install, reg);
	manage_command(this, "uninstall", uninstall, reg);
	manage_command(this, "reload-settings", reload_settings, reg);
	this->dispatcher->manage_event(this->dispatcher, "control-log", reg);
}

METHOD(vici_control_t, destroy, void,
	private_vici_control_t *this)
{
	manage_commands(this, FALSE);
	free(this);
}

/**
 * See header
 */
vici_control_t *vici_control_create(vici_dispatcher_t *dispatcher)
{
	private_vici_control_t *this;

	INIT(this,
		.public = {
			.destroy = _destroy,
		},
		.dispatcher = dispatcher,
	);

	manage_commands(this, TRUE);

	return &this->public;
}
