/*
 * Copyright (C) 2012 Tobias Brunner
 * 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 "ipsec.h"
#include "ipsec_processor.h"

#include <utils/debug.h>
#include <library.h>
#include <threading/rwlock.h>
#include <collections/blocking_queue.h>
#include <processing/jobs/callback_job.h>

typedef struct private_ipsec_processor_t private_ipsec_processor_t;

/**
 * Private additions to ipsec_processor_t.
 */
struct private_ipsec_processor_t {

	/**
	 * Public members
	 */
	ipsec_processor_t public;

	/**
	 * Queue for inbound packets (esp_packet_t*)
	 */
	blocking_queue_t *inbound_queue;

	/**
	 * Queue for outbound packets (ip_packet_t*)
	 */
	blocking_queue_t *outbound_queue;

	/**
	 * Registered inbound callback
	 */
	struct {
		ipsec_inbound_cb_t cb;
		void *data;
	} inbound;

	/**
	 * Registered outbound callback
	 */
	struct {
		ipsec_outbound_cb_t cb;
		void *data;
	} outbound;

	/**
	 * Lock used to synchronize access to the callbacks
	 */
	rwlock_t *lock;
};

/**
 * Deliver an inbound IP packet to the registered listener
 */
static void deliver_inbound(private_ipsec_processor_t *this,
							esp_packet_t *packet)
{
	this->lock->read_lock(this->lock);
	if (this->inbound.cb)
	{
		this->inbound.cb(this->inbound.data, packet->extract_payload(packet));
	}
	else
	{
		DBG2(DBG_ESP, "no inbound callback registered, dropping packet");
	}
	packet->destroy(packet);
	this->lock->unlock(this->lock);
}

/**
 * Processes inbound packets
 */
static job_requeue_t process_inbound(private_ipsec_processor_t *this)
{
	esp_packet_t *packet;
	ip_packet_t *ip_packet;
	ipsec_sa_t *sa;
	uint8_t next_header;
	uint32_t spi, reqid;

	packet = (esp_packet_t*)this->inbound_queue->dequeue(this->inbound_queue);

	if (!packet->parse_header(packet, &spi))
	{
		packet->destroy(packet);
		return JOB_REQUEUE_DIRECT;
	}

	sa = ipsec->sas->checkout_by_spi(ipsec->sas, spi,
									 packet->get_destination(packet));
	if (!sa)
	{
		DBG2(DBG_ESP, "inbound ESP packet does not belong to an installed SA");
		packet->destroy(packet);
		return JOB_REQUEUE_DIRECT;
	}

	if (!sa->is_inbound(sa))
	{
		DBG1(DBG_ESP, "error: IPsec SA is not inbound");
		packet->destroy(packet);
		ipsec->sas->checkin(ipsec->sas, sa);
		return JOB_REQUEUE_DIRECT;
	}

	if (packet->decrypt(packet, sa->get_esp_context(sa)) != SUCCESS)
	{
		ipsec->sas->checkin(ipsec->sas, sa);
		packet->destroy(packet);
		return JOB_REQUEUE_DIRECT;
	}
	ip_packet = packet->get_payload(packet);
	sa->update_usestats(sa, ip_packet->get_encoding(ip_packet).len);
	reqid = sa->get_reqid(sa);
	ipsec->sas->checkin(ipsec->sas, sa);

	next_header = packet->get_next_header(packet);
	switch (next_header)
	{
		case IPPROTO_IPIP:
		case IPPROTO_IPV6:
		{
			ipsec_policy_t *policy;

			policy = ipsec->policies->find_by_packet(ipsec->policies,
													 ip_packet, TRUE, reqid);
			if (policy)
			{
				deliver_inbound(this, packet);
				policy->destroy(policy);
				break;
			}
			DBG1(DBG_ESP, "discarding inbound IP packet %#H == %#H [%hhu] due "
				 "to policy", ip_packet->get_source(ip_packet),
				 ip_packet->get_destination(ip_packet),
				 ip_packet->get_next_header(ip_packet));
			/* no matching policy found, fall-through */
		}
		case IPPROTO_NONE:
			/* discard dummy packets */
			/* fall-through */
		default:
			packet->destroy(packet);
			break;
	}
	return JOB_REQUEUE_DIRECT;
}

/**
 * Send an ESP packet using the registered outbound callback
 */
static void send_outbound(private_ipsec_processor_t *this,
						  esp_packet_t *packet)
{
	this->lock->read_lock(this->lock);
	if (this->outbound.cb)
	{
		this->outbound.cb(this->outbound.data, packet);
	}
	else
	{
		DBG2(DBG_ESP, "no outbound callback registered, dropping packet");
		packet->destroy(packet);
	}
	this->lock->unlock(this->lock);
}

/**
 * Processes outbound packets
 */
static job_requeue_t process_outbound(private_ipsec_processor_t *this)
{
	ipsec_policy_t *policy;
	esp_packet_t *esp_packet;
	ip_packet_t *packet;
	ipsec_sa_t *sa;
	host_t *src, *dst;

	packet = (ip_packet_t*)this->outbound_queue->dequeue(this->outbound_queue);

	policy = ipsec->policies->find_by_packet(ipsec->policies, packet, FALSE, 0);
	if (!policy)
	{
		DBG2(DBG_ESP, "no matching outbound IPsec policy for %#H == %#H [%hhu]",
			 packet->get_source(packet), packet->get_destination(packet),
			 packet->get_next_header(packet));
		packet->destroy(packet);
		return JOB_REQUEUE_DIRECT;
	}

	sa = ipsec->sas->checkout_by_reqid(ipsec->sas, policy->get_reqid(policy),
									   FALSE);
	if (!sa)
	{	/* TODO-IPSEC: send an acquire to upper layer */
		DBG1(DBG_ESP, "could not find an outbound IPsec SA for reqid {%u}, "
			 "dropping packet", policy->get_reqid(policy));
		packet->destroy(packet);
		policy->destroy(policy);
		return JOB_REQUEUE_DIRECT;
	}
	src = sa->get_source(sa);
	dst = sa->get_destination(sa);
	esp_packet = esp_packet_create_from_payload(src->clone(src),
												dst->clone(dst), packet);
	if (esp_packet->encrypt(esp_packet, sa->get_esp_context(sa),
							sa->get_spi(sa)) != SUCCESS)
	{
		ipsec->sas->checkin(ipsec->sas, sa);
		esp_packet->destroy(esp_packet);
		policy->destroy(policy);
		return JOB_REQUEUE_DIRECT;
	}
	sa->update_usestats(sa, packet->get_encoding(packet).len);
	ipsec->sas->checkin(ipsec->sas, sa);
	policy->destroy(policy);
	send_outbound(this, esp_packet);
	return JOB_REQUEUE_DIRECT;
}

METHOD(ipsec_processor_t, queue_inbound, void,
	private_ipsec_processor_t *this, esp_packet_t *packet)
{
	this->inbound_queue->enqueue(this->inbound_queue, packet);
}

METHOD(ipsec_processor_t, queue_outbound, void,
	private_ipsec_processor_t *this, ip_packet_t *packet)
{
	this->outbound_queue->enqueue(this->outbound_queue, packet);
}

METHOD(ipsec_processor_t, register_inbound, void,
	private_ipsec_processor_t *this, ipsec_inbound_cb_t cb, void *data)
{
	this->lock->write_lock(this->lock);
	this->inbound.cb = cb;
	this->inbound.data = data;
	this->lock->unlock(this->lock);
}

METHOD(ipsec_processor_t, unregister_inbound, void,
	private_ipsec_processor_t *this, ipsec_inbound_cb_t cb)
{
	this->lock->write_lock(this->lock);
	if (this->inbound.cb == cb)
	{
		this->inbound.cb = NULL;
	}
	this->lock->unlock(this->lock);
}

METHOD(ipsec_processor_t, register_outbound, void,
	private_ipsec_processor_t *this, ipsec_outbound_cb_t cb, void *data)
{
	this->lock->write_lock(this->lock);
	this->outbound.cb = cb;
	this->outbound.data = data;
	this->lock->unlock(this->lock);
}

METHOD(ipsec_processor_t, unregister_outbound, void,
	private_ipsec_processor_t *this, ipsec_outbound_cb_t cb)
{
	this->lock->write_lock(this->lock);
	if (this->outbound.cb == cb)
	{
		this->outbound.cb = NULL;
	}
	this->lock->unlock(this->lock);
}

METHOD(ipsec_processor_t, destroy, void,
	private_ipsec_processor_t *this)
{
	this->inbound_queue->destroy_offset(this->inbound_queue,
										offsetof(esp_packet_t, destroy));
	this->outbound_queue->destroy_offset(this->outbound_queue,
										 offsetof(ip_packet_t, destroy));
	this->lock->destroy(this->lock);
	free(this);
}

/**
 * Described in header.
 */
ipsec_processor_t *ipsec_processor_create()
{
	private_ipsec_processor_t *this;

	INIT(this,
		.public = {
			.queue_inbound = _queue_inbound,
			.queue_outbound = _queue_outbound,
			.register_inbound = _register_inbound,
			.unregister_inbound = _unregister_inbound,
			.register_outbound = _register_outbound,
			.unregister_outbound = _unregister_outbound,
			.destroy = _destroy,
		},
		.inbound_queue = blocking_queue_create(),
		.outbound_queue = blocking_queue_create(),
		.lock = rwlock_create(RWLOCK_TYPE_DEFAULT),
	);

	lib->processor->queue_job(lib->processor,
		(job_t*)callback_job_create((callback_job_cb_t)process_inbound, this,
									NULL, (callback_job_cancel_t)return_false));
	lib->processor->queue_job(lib->processor,
		(job_t*)callback_job_create((callback_job_cb_t)process_outbound, this,
									NULL, (callback_job_cancel_t)return_false));
	return &this->public;
}
