/*
 * Copyright (C) 2013-2015 Tobias Brunner
 * Copyright (C) 2008 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 "stroke_control.h"

#include <daemon.h>

#include <processing/jobs/delete_ike_sa_job.h>
#include <processing/jobs/rekey_ike_sa_job.h>
#include <processing/jobs/rekey_child_sa_job.h>

typedef struct private_stroke_control_t private_stroke_control_t;

/**
 * private data of stroke_control
 */
struct private_stroke_control_t {

	/**
	 * public functions
	 */
	stroke_control_t public;

	/**
	 * Timeout for stroke commands, im ms
	 */
	u_int timeout;
};


typedef struct stroke_log_info_t stroke_log_info_t;

/**
 * helper struct to say what and where to log when using controller callback
 */
struct stroke_log_info_t {

	/**
	 * level to log up to
	 */
	level_t level;

	/**
	 * where to write log
	 */
	FILE* out;
};

/**
 * logging to the stroke interface
 */
static bool stroke_log(stroke_log_info_t *info, debug_t group, level_t level,
					   ike_sa_t *ike_sa, char *message)
{
	if (level <= info->level)
	{
		if (fprintf(info->out, "%s", message) < 0 ||
			fprintf(info->out, "\n") < 0 ||
			fflush(info->out) != 0)
		{
			return FALSE;
		}
	}
	return TRUE;
}

/**
 * get the child_cfg with the same name as the 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;
}

/**
 * call the charon controller to initiate the connection
 */
static void charon_initiate(private_stroke_control_t *this, peer_cfg_t *peer_cfg,
							child_cfg_t *child_cfg, stroke_msg_t *msg, FILE *out)
{
	if (msg->output_verbosity < 0)
	{
		charon->controller->initiate(charon->controller, peer_cfg, child_cfg,
									 NULL, NULL, 0, 0, FALSE);
	}
	else
	{
		stroke_log_info_t info = { msg->output_verbosity, out };
		status_t status;

		status = charon->controller->initiate(charon->controller,
							peer_cfg, child_cfg, (controller_cb_t)stroke_log,
							&info, msg->output_verbosity, this->timeout, FALSE);
		switch (status)
		{
			case SUCCESS:
				fprintf(out, "connection '%s' established successfully\n",
						msg->initiate.name);
				break;
			case OUT_OF_RES:
				fprintf(out, "connection '%s' not established after %dms, "
						"detaching\n", msg->initiate.name, this->timeout);
				break;
			default:
			case FAILED:
				fprintf(out, "establishing connection '%s' failed\n",
						msg->initiate.name);
				break;
		}
	}
}

METHOD(stroke_control_t, initiate, void,
	private_stroke_control_t *this, stroke_msg_t *msg, FILE *out)
{
	child_cfg_t *child_cfg = NULL;
	peer_cfg_t *peer_cfg;
	enumerator_t *enumerator;
	bool empty = TRUE;

	peer_cfg = charon->backends->get_peer_cfg_by_name(charon->backends,
													  msg->initiate.name);
	if (peer_cfg)
	{
		child_cfg = get_child_from_peer(peer_cfg, msg->initiate.name);
		if (child_cfg == NULL)
		{
			enumerator = peer_cfg->create_child_cfg_enumerator(peer_cfg);
			while (enumerator->enumerate(enumerator, &child_cfg))
			{
				empty = FALSE;
				charon_initiate(this, peer_cfg->get_ref(peer_cfg),
								child_cfg->get_ref(child_cfg), msg, out);
			}
			enumerator->destroy(enumerator);

			if (empty)
			{
				DBG1(DBG_CFG, "no child config named '%s'", msg->initiate.name);
				fprintf(out, "no child config named '%s'\n", msg->initiate.name);
			}
			peer_cfg->destroy(peer_cfg);
			return;
		}
	}
	else
	{
		enumerator = charon->backends->create_peer_cfg_enumerator(
							charon->backends, NULL, NULL, NULL, NULL, IKE_ANY);
		while (enumerator->enumerate(enumerator, &peer_cfg))
		{
			child_cfg = get_child_from_peer(peer_cfg, msg->initiate.name);
			if (child_cfg)
			{
				peer_cfg->get_ref(peer_cfg);
				break;
			}
		}
		enumerator->destroy(enumerator);

		if (child_cfg == NULL)
		{
			DBG1(DBG_CFG, "no config named '%s'", msg->initiate.name);
			fprintf(out, "no config named '%s'\n", msg->initiate.name);
			return;
		}
	}
	charon_initiate(this, peer_cfg, child_cfg, msg, out);
}

/**
 * Parse a terminate/rekey specifier
 */
static bool parse_specifier(char *string, uint32_t *id,
							char **name, bool *child, bool *all)
{
	int len;
	char *pos = NULL;

	*id = 0;
	*name = NULL;
	*all = FALSE;

	len = strlen(string);
	if (len < 1)
	{
		return FALSE;
	}
	switch (string[len-1])
	{
		case '}':
			*child = TRUE;
			pos = strchr(string, '{');
			break;
		case ']':
			*child = FALSE;
			pos = strchr(string, '[');
			break;
		default:
			*name = string;
			*child = FALSE;
			break;
	}

	if (*name)
	{
		/* is a single name */
	}
	else if (pos == string + len - 2)
	{	/* is name[] or name{} */
		string[len-2] = '\0';
		*name = string;
	}
	else
	{
		if (!pos)
		{
			return FALSE;
		}
		if (*(pos + 1) == '*')
		{	/* is name[*] */
			*all = TRUE;
			*pos = '\0';
			*name = string;
		}
		else
		{	/* is name[123] or name{23} */
			*id = atoi(pos + 1);
			if (*id == 0)
			{
				return FALSE;
			}
		}
	}
	return TRUE;
}

/**
 * Report the result of a terminate() call to console
 */
static void report_terminate_status(private_stroke_control_t *this,
						status_t status, FILE *out, uint32_t id, bool child)
{
	char *prefix, *postfix;

	if (child)
	{
		prefix = "CHILD_SA {";
		postfix = "}";
	}
	else
	{
		prefix = "IKE_SA [";
		postfix = "]";
	}

	switch (status)
	{
		case SUCCESS:
			fprintf(out, "%s%d%s closed successfully\n", prefix, id, postfix);
			break;
		case OUT_OF_RES:
			fprintf(out, "%s%d%s not closed after %dms, detaching\n",
					prefix, id, postfix, this->timeout);
			break;
		default:
		case FAILED:
			fprintf(out, "closing %s%d%s failed\n", prefix, id, postfix);
			break;
	}
}

/**
 * Call the charon controller to terminate a CHILD_SA
 */
static void charon_terminate(private_stroke_control_t *this, uint32_t id,
							 stroke_msg_t *msg, FILE *out, bool child)
{
	if (msg->output_verbosity >= 0)
	{
		stroke_log_info_t info = { msg->output_verbosity, out };
		status_t status;

		if (child)
		{
			status = charon->controller->terminate_child(charon->controller, id,
									(controller_cb_t)stroke_log, &info,
									msg->output_verbosity, this->timeout);
		}
		else
		{
			status = charon->controller->terminate_ike(charon->controller, id,
									FALSE, (controller_cb_t)stroke_log, &info,
									msg->output_verbosity, this->timeout);
		}
		report_terminate_status(this, status, out, id, child);
	}
	else if (child)
	{
		charon->controller->terminate_child(charon->controller, id,
										    NULL, NULL, 0, 0);
	}
	else
	{
		charon->controller->terminate_ike(charon->controller, id, FALSE,
										  NULL, NULL, 0, 0);
	}
}

METHOD(stroke_control_t, terminate, void,
	private_stroke_control_t *this, stroke_msg_t *msg, FILE *out)
{
	char *name;
	uint32_t id;
	bool child, all;
	ike_sa_t *ike_sa;
	enumerator_t *enumerator;
	linked_list_t *ike_list, *child_list;
	uintptr_t del;

	if (!parse_specifier(msg->terminate.name, &id, &name, &child, &all))
	{
		DBG1(DBG_CFG, "error parsing specifier string");
		return;
	}

	if (id)
	{
		return charon_terminate(this, id, msg, out, child);
	}

	ike_list = linked_list_create();
	child_list = linked_list_create();
	enumerator = charon->controller->create_ike_sa_enumerator(
													charon->controller, TRUE);
	while (enumerator->enumerate(enumerator, &ike_sa))
	{
		child_sa_t *child_sa;
		enumerator_t *children;

		if (child)
		{
			children = ike_sa->create_child_sa_enumerator(ike_sa);
			while (children->enumerate(children, (void**)&child_sa))
			{
				if (streq(name, child_sa->get_name(child_sa)))
				{
					child_list->insert_last(child_list,
							(void*)(uintptr_t)child_sa->get_unique_id(child_sa));
					if (!all)
					{
						break;
					}
				}
			}
			children->destroy(children);
			if (child_list->get_count(child_list) && !all)
			{
				break;
			}
		}
		else if (streq(name, ike_sa->get_name(ike_sa)))
		{
			ike_list->insert_last(ike_list,
						(void*)(uintptr_t)ike_sa->get_unique_id(ike_sa));
			if (!all)
			{
				break;
			}
		}
	}
	enumerator->destroy(enumerator);

	enumerator = child_list->create_enumerator(child_list);
	while (enumerator->enumerate(enumerator, &del))
	{
		charon_terminate(this, del, msg, out, TRUE);
	}
	enumerator->destroy(enumerator);

	enumerator = ike_list->create_enumerator(ike_list);
	while (enumerator->enumerate(enumerator, &del))
	{
		charon_terminate(this, del, msg, out, FALSE);
	}
	enumerator->destroy(enumerator);

	if (child_list->get_count(child_list) == 0 &&
		ike_list->get_count(ike_list) == 0)
	{
		DBG1(DBG_CFG, "no %s_SA named '%s' found",
			 child ? "CHILD" : "IKE", name);
	}
	ike_list->destroy(ike_list);
	child_list->destroy(child_list);
}

METHOD(stroke_control_t, rekey, void,
	private_stroke_control_t *this, stroke_msg_t *msg, FILE *out)
{
	char *name;
	uint32_t id;
	bool child, all, finished = FALSE;
	ike_sa_t *ike_sa;
	enumerator_t *enumerator;

	if (!parse_specifier(msg->terminate.name, &id, &name, &child, &all))
	{
		DBG1(DBG_CFG, "error parsing specifier string");
		return;
	}
	enumerator = charon->controller->create_ike_sa_enumerator(
													charon->controller, TRUE);
	while (enumerator->enumerate(enumerator, &ike_sa))
	{
		child_sa_t *child_sa;
		enumerator_t *children;

		if (child)
		{
			children = ike_sa->create_child_sa_enumerator(ike_sa);
			while (children->enumerate(children, (void**)&child_sa))
			{
				if ((name && streq(name, child_sa->get_name(child_sa))) ||
					(id && id == child_sa->get_unique_id(child_sa)))
				{
					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)));
					if (!all)
					{
						finished = TRUE;
						break;
					}
				}
			}
			children->destroy(children);
		}
		else if ((name && streq(name, ike_sa->get_name(ike_sa))) ||
				 (id && 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), FALSE));
			if (!all)
			{
				finished = TRUE;
			}
		}
		if (finished)
		{
			break;
		}
	}
	enumerator->destroy(enumerator);
}

METHOD(stroke_control_t, terminate_srcip, void,
	private_stroke_control_t *this, stroke_msg_t *msg, FILE *out)
{
	enumerator_t *enumerator, *vips;
	ike_sa_t *ike_sa;
	host_t *start = NULL, *end = NULL, *vip;
	chunk_t chunk_start, chunk_end = chunk_empty, chunk;

	if (msg->terminate_srcip.start)
	{
		start = host_create_from_string(msg->terminate_srcip.start, 0);
	}
	if (!start)
	{
		DBG1(DBG_CFG, "invalid start address: %s", msg->terminate_srcip.start);
		return;
	}
	chunk_start = start->get_address(start);
	if (msg->terminate_srcip.end)
	{
		end = host_create_from_string(msg->terminate_srcip.end, 0);
		if (!end)
		{
			DBG1(DBG_CFG, "invalid end address: %s", msg->terminate_srcip.end);
			start->destroy(start);
			return;
		}
		chunk_end = end->get_address(end);
	}

	enumerator = charon->controller->create_ike_sa_enumerator(
													charon->controller, TRUE);
	while (enumerator->enumerate(enumerator, &ike_sa))
	{
		bool match = FALSE;

		vips = ike_sa->create_virtual_ip_enumerator(ike_sa, FALSE);
		while (vips->enumerate(vips, &vip))
		{
			if (!end)
			{
				if (vip->ip_equals(vip, start))
				{
					match = TRUE;
					break;
				}
			}
			else
			{
				chunk = vip->get_address(vip);
				if (chunk.len == chunk_start.len &&
					chunk.len == chunk_end.len &&
					memcmp(chunk.ptr, chunk_start.ptr, chunk.len) >= 0 &&
					memcmp(chunk.ptr, chunk_end.ptr, chunk.len) <= 0)
				{
					match = TRUE;
					break;
				}
			}
		}
		vips->destroy(vips);

		if (match)
		{
			/* schedule delete asynchronously */
			lib->processor->queue_job(lib->processor, (job_t*)
						delete_ike_sa_job_create(ike_sa->get_id(ike_sa), TRUE));
		}
	}
	enumerator->destroy(enumerator);
	start->destroy(start);
	DESTROY_IF(end);
}

METHOD(stroke_control_t, purge_ike, void,
	private_stroke_control_t *this, stroke_msg_t *msg, FILE *out)
{
	enumerator_t *enumerator, *children;
	ike_sa_t *ike_sa;
	child_sa_t *child_sa;
	linked_list_t *list;
	uintptr_t del;

	list = linked_list_create();
	enumerator = charon->controller->create_ike_sa_enumerator(
													charon->controller, TRUE);
	while (enumerator->enumerate(enumerator, &ike_sa))
	{
		children = ike_sa->create_child_sa_enumerator(ike_sa);
		if (!children->enumerate(children, (void**)&child_sa))
		{
			list->insert_last(list,
						(void*)(uintptr_t)ike_sa->get_unique_id(ike_sa));
		}
		children->destroy(children);
	}
	enumerator->destroy(enumerator);

	enumerator = list->create_enumerator(list);
	while (enumerator->enumerate(enumerator, &del))
	{
		charon_terminate(this, del, msg, out, FALSE);
	}
	enumerator->destroy(enumerator);
	list->destroy(list);
}

/**
 * call charon to install a shunt or trap
 */
static void charon_route(peer_cfg_t *peer_cfg, child_cfg_t *child_cfg,
						 char *name, FILE *out)
{
	ipsec_mode_t mode;

	mode = child_cfg->get_mode(child_cfg);
	if (mode == MODE_PASS || mode == MODE_DROP)
	{
		if (charon->shunts->install(charon->shunts,
									peer_cfg->get_name(peer_cfg), child_cfg))
		{
			fprintf(out, "'%s' shunt %N policy installed\n",
					name, ipsec_mode_names, mode);
		}
		else
		{
			fprintf(out, "'%s' shunt %N policy installation failed\n",
					name, ipsec_mode_names, mode);
		}
	}
	else
	{
		if (charon->traps->install(charon->traps, peer_cfg, child_cfg))
		{
			fprintf(out, "'%s' routed\n", name);
		}
		else
		{
			fprintf(out, "routing '%s' failed\n", name);
		}
	}
}

METHOD(stroke_control_t, route, void,
	private_stroke_control_t *this, stroke_msg_t *msg, FILE *out)
{
	child_cfg_t *child_cfg = NULL;
	peer_cfg_t *peer_cfg;
	enumerator_t *enumerator;
	bool empty = TRUE;

	peer_cfg = charon->backends->get_peer_cfg_by_name(charon->backends,
													  msg->route.name);
	if (peer_cfg)
	{
		child_cfg = get_child_from_peer(peer_cfg, msg->route.name);
		if (child_cfg == NULL)
		{
			enumerator = peer_cfg->create_child_cfg_enumerator(peer_cfg);
			while (enumerator->enumerate(enumerator, &child_cfg))
			{
				empty = FALSE;
				charon_route(peer_cfg, child_cfg, child_cfg->get_name(child_cfg),
							 out);
			}
			enumerator->destroy(enumerator);

			if (empty)
			{
				DBG1(DBG_CFG, "no child config named '%s'", msg->route.name);
				fprintf(out, "no child config named '%s'\n", msg->route.name);
			}
			peer_cfg->destroy(peer_cfg);
			return;
		}
	}
	else
	{
		enumerator = charon->backends->create_peer_cfg_enumerator(
							charon->backends, NULL, NULL, NULL, NULL, IKE_ANY);
		while (enumerator->enumerate(enumerator, &peer_cfg))
		{
			child_cfg = get_child_from_peer(peer_cfg, msg->route.name);
			if (child_cfg)
			{
				peer_cfg->get_ref(peer_cfg);
				break;
			}
		}
		enumerator->destroy(enumerator);

		if (child_cfg == NULL)
		{
			DBG1(DBG_CFG, "no config named '%s'", msg->route.name);
			fprintf(out, "no config named '%s'\n", msg->route.name);
			return;
		}
	}
	charon_route(peer_cfg, child_cfg, msg->route.name, out);
	peer_cfg->destroy(peer_cfg);
	child_cfg->destroy(child_cfg);
}

METHOD(stroke_control_t, unroute, void,
	private_stroke_control_t *this, stroke_msg_t *msg, FILE *out)
{
	if (charon->shunts->uninstall(charon->shunts, NULL, msg->unroute.name))
	{
		fprintf(out, "shunt policy '%s' uninstalled\n", msg->unroute.name);
	}
	else if (charon->traps->uninstall(charon->traps, NULL, msg->unroute.name))
	{
		fprintf(out, "trap policy '%s' unrouted\n", msg->unroute.name);
	}
	else
	{
		fprintf(out, "configuration '%s' not found\n", msg->unroute.name);
	}
}

METHOD(stroke_control_t, destroy, void,
	private_stroke_control_t *this)
{
	free(this);
}

/*
 * see header file
 */
stroke_control_t *stroke_control_create()
{
	private_stroke_control_t *this;

	INIT(this,
		.public = {
			.initiate = _initiate,
			.terminate = _terminate,
			.terminate_srcip = _terminate_srcip,
			.rekey = _rekey,
			.purge_ike = _purge_ike,
			.route = _route,
			.unroute = _unroute,
			.destroy = _destroy,
		},
		.timeout = lib->settings->get_int(lib->settings,
									"%s.plugins.stroke.timeout", 0, lib->ns),
	);

	return &this->public;
}
