/*
 * Copyright (C) 2013 Tobias Brunner
 * Hochschule fuer Technik Rapperswil
 *
 * Copyright (C) 2012 Martin Willi
 * Copyright (C) 2012 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 "unity_handler.h"

#include <daemon.h>
#include <threading/mutex.h>
#include <collections/linked_list.h>
#include <processing/jobs/callback_job.h>

typedef struct private_unity_handler_t private_unity_handler_t;

/**
 * Private data of an unity_handler_t object.
 */
struct private_unity_handler_t {

	/**
	 * Public unity_handler_t interface.
	 */
	unity_handler_t public;

	/**
	 * List of subnets to include, as entry_t
	 */
	linked_list_t *include;

	/**
	 * Mutex for concurrent access to lists
	 */
	mutex_t *mutex;
};

/**
 * Traffic selector entry for networks to include under a given IKE_SA
 */
typedef struct {
	/** associated IKE_SA COOKIEs */
	ike_sa_id_t *id;
	/** traffic selector to include/exclude */
	traffic_selector_t *ts;
} entry_t;

/**
 * Clean up an entry
 */
static void entry_destroy(entry_t *this)
{
	this->id->destroy(this->id);
	this->ts->destroy(this->ts);
	free(this);
}

/**
 * Create a traffic selector from a unity subnet definition
 */
static traffic_selector_t *create_ts(chunk_t subnet)
{
	chunk_t net, mask;
	int i;

	net = chunk_create(subnet.ptr, 4);
	mask = chunk_clonea(chunk_create(subnet.ptr + 4, 4));
	for (i = 0; i < net.len; i++)
	{
		mask.ptr[i] = (mask.ptr[i] ^ 0xFF) | net.ptr[i];
	}
	return traffic_selector_create_from_bytes(0, TS_IPV4_ADDR_RANGE,
											  net, 0, mask, 65535);
}

/**
 * Parse a unity attribute and extract all subnets as traffic selectors
 */
static linked_list_t *parse_subnets(chunk_t data)
{
	linked_list_t *list = NULL;
	traffic_selector_t *ts;

	while (data.len >= 8)
	{	/* the padding is optional */
		ts = create_ts(data);
		if (ts)
		{
			if (!list)
			{
				list = linked_list_create();
			}
			list->insert_last(list, ts);
		}
		/* skip address, mask and 6 bytes of padding */
		data = chunk_skip(data, 14);
	}
	return list;
}

/**
 * Store a list of subnets to include in tunnels under this IKE_SA
 */
static bool add_include(private_unity_handler_t *this, chunk_t data)
{
	traffic_selector_t *ts;
	linked_list_t *list;
	ike_sa_t *ike_sa;
	entry_t *entry;

	ike_sa = charon->bus->get_sa(charon->bus);
	if (!ike_sa)
	{
		return FALSE;
	}
	list = parse_subnets(data);
	if (!list)
	{
		return FALSE;
	}
	while (list->remove_first(list, (void**)&ts) == SUCCESS)
	{
		INIT(entry,
			.id = ike_sa->get_id(ike_sa),
			.ts = ts,
		);
		entry->id = entry->id->clone(entry->id);

		this->mutex->lock(this->mutex);
		this->include->insert_last(this->include, entry);
		this->mutex->unlock(this->mutex);
	}
	list->destroy(list);
	return TRUE;
}

/**
 * Remove a list of subnets from the inclusion list for this IKE_SA
 */
static bool remove_include(private_unity_handler_t *this, chunk_t data)
{
	enumerator_t *enumerator;
	traffic_selector_t *ts;
	linked_list_t *list;
	ike_sa_t *ike_sa;
	entry_t *entry;

	ike_sa = charon->bus->get_sa(charon->bus);
	if (!ike_sa)
	{
		return FALSE;
	}
	list = parse_subnets(data);
	if (!list)
	{
		return FALSE;
	}

	this->mutex->lock(this->mutex);
	while (list->remove_first(list, (void**)&ts) == SUCCESS)
	{
		enumerator = this->include->create_enumerator(this->include);
		while (enumerator->enumerate(enumerator, &entry))
		{
			if (entry->id->equals(entry->id, ike_sa->get_id(ike_sa)) &&
				ts->equals(ts, entry->ts))
			{
				this->include->remove_at(this->include, enumerator);
				entry_destroy(entry);
				break;
			}
		}
		enumerator->destroy(enumerator);
		ts->destroy(ts);
	}
	this->mutex->unlock(this->mutex);
	list->destroy(list);
	return TRUE;
}

/**
 * Create a unique shunt name for a bypass policy
 */
static void create_shunt_name(ike_sa_t *ike_sa, traffic_selector_t *ts,
							  char *buf, size_t len)
{
	snprintf(buf, len, "Unity (%s[%u]: %R)", ike_sa->get_name(ike_sa),
			 ike_sa->get_unique_id(ike_sa), ts);
}

/**
 * Install entry as a shunt policy
 */
static job_requeue_t add_exclude_async(entry_t *entry)
{
	enumerator_t *enumerator;
	child_cfg_t *child_cfg;
	child_cfg_create_t child = {
		.mode = MODE_PASS,
	};
	ike_sa_t *ike_sa;
	char name[128];
	host_t *host;

	ike_sa = charon->ike_sa_manager->checkout(charon->ike_sa_manager, entry->id);
	if (ike_sa)
	{
		create_shunt_name(ike_sa, entry->ts, name, sizeof(name));

		child_cfg = child_cfg_create(name, &child);
		child_cfg->add_traffic_selector(child_cfg, FALSE,
										entry->ts->clone(entry->ts));
		host = ike_sa->get_my_host(ike_sa);
		child_cfg->add_traffic_selector(child_cfg, TRUE,
				traffic_selector_create_from_subnet(host->clone(host),
													32, 0, 0, 65535));
		enumerator = ike_sa->create_virtual_ip_enumerator(ike_sa, TRUE);
		while (enumerator->enumerate(enumerator, &host))
		{
			child_cfg->add_traffic_selector(child_cfg, TRUE,
				traffic_selector_create_from_subnet(host->clone(host),
													32, 0, 0, 65535));
		}
		enumerator->destroy(enumerator);
		charon->ike_sa_manager->checkin(charon->ike_sa_manager, ike_sa);

		charon->shunts->install(charon->shunts, "unity", child_cfg);
		child_cfg->destroy(child_cfg);

		DBG1(DBG_IKE, "installed %N bypass policy for %R",
			 configuration_attribute_type_names, UNITY_LOCAL_LAN, entry->ts);
	}
	return JOB_REQUEUE_NONE;
}

/**
 * Add a bypass policy for a given subnet
 */
static bool add_exclude(private_unity_handler_t *this, chunk_t data)
{
	traffic_selector_t *ts;
	linked_list_t *list;
	ike_sa_t *ike_sa;
	entry_t *entry;

	ike_sa = charon->bus->get_sa(charon->bus);
	if (!ike_sa)
	{
		return FALSE;
	}
	list = parse_subnets(data);
	if (!list)
	{
		return FALSE;
	}

	while (list->remove_first(list, (void**)&ts) == SUCCESS)
	{
		INIT(entry,
			.id = ike_sa->get_id(ike_sa),
			.ts = ts,
		);
		entry->id = entry->id->clone(entry->id);

		/* we can't install the shunt policy yet, as we don't know the virtual IP.
		 * Defer installation using an async callback. */
		lib->processor->queue_job(lib->processor, (job_t*)
							callback_job_create((void*)add_exclude_async, entry,
												(void*)entry_destroy, NULL));
	}
	list->destroy(list);
	return TRUE;
}

/**
 * Remove a bypass policy for a given subnet
 */
static bool remove_exclude(private_unity_handler_t *this, chunk_t data)
{
	traffic_selector_t *ts;
	linked_list_t *list;
	ike_sa_t *ike_sa;
	char name[128];
	bool success = TRUE;

	ike_sa = charon->bus->get_sa(charon->bus);
	if (!ike_sa)
	{
		return FALSE;
	}
	list = parse_subnets(data);
	if (!list)
	{
		return FALSE;
	}
	while (list->remove_first(list, (void**)&ts) == SUCCESS)
	{
		create_shunt_name(ike_sa, ts, name, sizeof(name));
		DBG1(DBG_IKE, "uninstalling %N bypass policy for %R",
			 configuration_attribute_type_names, UNITY_LOCAL_LAN, ts);
		ts->destroy(ts);
		success = charon->shunts->uninstall(charon->shunts, "unity",
											name) && success;
	}
	list->destroy(list);
	return success;
}

METHOD(attribute_handler_t, handle, bool,
	private_unity_handler_t *this, ike_sa_t *ike_sa,
	configuration_attribute_type_t type, chunk_t data)
{
	switch (type)
	{
		case UNITY_SPLIT_INCLUDE:
			return add_include(this, data);
		case UNITY_LOCAL_LAN:
			return add_exclude(this, data);
		default:
			return FALSE;
	}
}

METHOD(attribute_handler_t, release, void,
	private_unity_handler_t *this, ike_sa_t *ike_sa,
	configuration_attribute_type_t type, chunk_t data)
{
	switch (type)
	{
		case UNITY_SPLIT_INCLUDE:
			remove_include(this, data);
			break;
		case UNITY_LOCAL_LAN:
			remove_exclude(this, data);
			break;
		default:
			break;
	}
}

/**
 * Configuration attributes to request
 */
static configuration_attribute_type_t attributes[] = {
	UNITY_SPLIT_INCLUDE,
	UNITY_LOCAL_LAN,
};

/**
 * Attribute enumerator implementation
 */
typedef struct {
	/** implements enumerator_t */
	enumerator_t public;
	/** position in attributes[] */
	int i;
} attribute_enumerator_t;

METHOD(enumerator_t, enumerate_attributes, bool,
	attribute_enumerator_t *this, va_list args)
{
	configuration_attribute_type_t *type;
	chunk_t *data;

	VA_ARGS_VGET(args, type, data);
	if (this->i < countof(attributes))
	{
		*type = attributes[this->i++];
		*data = chunk_empty;
		return TRUE;
	}
	return FALSE;
}

METHOD(attribute_handler_t, create_attribute_enumerator, enumerator_t *,
	unity_handler_t *this, ike_sa_t *ike_sa, linked_list_t *vips)
{
	attribute_enumerator_t *enumerator;

	ike_sa = charon->bus->get_sa(charon->bus);
	if (!ike_sa || ike_sa->get_version(ike_sa) != IKEV1 ||
		!ike_sa->supports_extension(ike_sa, EXT_CISCO_UNITY))
	{
		return enumerator_create_empty();
	}
	INIT(enumerator,
		.public = {
			.enumerate = enumerator_enumerate_default,
			.venumerate = _enumerate_attributes,
			.destroy = (void*)free,
		},
	);
	return &enumerator->public;
}

typedef struct {
	/** mutex to unlock */
	mutex_t *mutex;
	/** IKE_SA ID to filter for */
	ike_sa_id_t *id;
} include_filter_t;

CALLBACK(include_filter, bool,
	include_filter_t *data, enumerator_t *orig, va_list args)
{
	entry_t *entry;
	traffic_selector_t **ts;

	VA_ARGS_VGET(args, ts);

	while (orig->enumerate(orig, &entry))
	{
		if (data->id->equals(data->id, entry->id))
		{
			*ts = entry->ts;
			return TRUE;
		}
	}
	return FALSE;
}

CALLBACK(destroy_filter, void,
	include_filter_t *data)
{
	data->mutex->unlock(data->mutex);
	free(data);
}

METHOD(unity_handler_t, create_include_enumerator, enumerator_t*,
	private_unity_handler_t *this, ike_sa_id_t *id)
{
	include_filter_t *data;

	INIT(data,
		.mutex = this->mutex,
		.id = id,
	);
	data->mutex->lock(data->mutex);
	return enumerator_create_filter(
					this->include->create_enumerator(this->include),
					include_filter, data, destroy_filter);
}

METHOD(unity_handler_t, destroy, void,
	private_unity_handler_t *this)
{
	this->include->destroy(this->include);
	this->mutex->destroy(this->mutex);
	free(this);
}

/**
 * See header
 */
unity_handler_t *unity_handler_create()
{
	private_unity_handler_t *this;

	INIT(this,
		.public = {
			.handler = {
				.handle = _handle,
				.release = _release,
				.create_attribute_enumerator = _create_attribute_enumerator,
			},
			.create_include_enumerator = _create_include_enumerator,
			.destroy = _destroy,
		},
		.include = linked_list_create(),
		.mutex = mutex_create(MUTEX_TYPE_DEFAULT),
	);

	return &this->public;
}
