/*
 * Copyright (C) 2012 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 "eap_radius_dae.h"

#include <radius_message.h>

#include <unistd.h>
#include <errno.h>

#include <daemon.h>
#include <threading/thread.h>
#include <processing/jobs/callback_job.h>
#include <processing/jobs/delete_ike_sa_job.h>

#define RADIUS_DAE_PORT 3799

/* maximum number of cached responses for retransmits (= number of concurrent
 * clients) */
#define DAE_RESPONSES_MAX 32

typedef struct private_eap_radius_dae_t private_eap_radius_dae_t;

/**
 * Private data of an eap_radius_dae_t object.
 */
struct private_eap_radius_dae_t {

	/**
	 * Public eap_radius_dae_t interface.
	 */
	eap_radius_dae_t public;

	/**
	 * RADIUS session state
	 */
	eap_radius_accounting_t *accounting;

	/**
	 * Socket to listen on authorization extension port
	 */
	int fd;

	/**
	 * RADIUS shared secret for DAE exchanges
	 */
	chunk_t secret;

	/**
	 * MD5 hasher
	 */
	hasher_t *hasher;

	/**
	 * HMAC MD5 signer, with secret set
	 */
	signer_t *signer;

	/**
	 * List of responses for retransmission, as entry_t
	 */
	linked_list_t *responses;
};

/**
 * Entry to store responses for retransmit
 */
typedef struct {
	/** stored response */
	radius_message_t *response;
	/** client that sent the request */
	host_t *client;
} entry_t;

/**
 * Clean up an entry
 */
static void entry_destroy(entry_t *entry)
{
	entry->response->destroy(entry->response);
	entry->client->destroy(entry->client);
	free(entry);
}

/**
 * Save/Replace response for retransmission
 */
static void save_retransmit(private_eap_radius_dae_t *this,
							radius_message_t *response, host_t *client)
{
	enumerator_t *enumerator;
	entry_t *entry;
	bool found = FALSE;

	enumerator = this->responses->create_enumerator(this->responses);
	while (enumerator->enumerate(enumerator, &entry))
	{
		if (client->equals(client, entry->client))
		{	/* move recently used entries to the front */
			this->responses->remove_at(this->responses, enumerator);
			this->responses->insert_first(this->responses, entry);
			entry->response->destroy(entry->response);
			entry->response = response;
			found = TRUE;
			break;
		}
	}
	enumerator->destroy(enumerator);

	if (!found)
	{
		INIT(entry,
			.response = response,
			.client = client->clone(client),
		);
		this->responses->insert_first(this->responses, entry);

		/* remove least recently used entry once we reach the maximum */
		if (this->responses->get_count(this->responses) > DAE_RESPONSES_MAX &&
			this->responses->remove_last(this->responses, (void**)&entry))
		{
			entry_destroy(entry);
		}
	}
}

/**
 * Send a RADIUS message to client
 */
static void send_message(private_eap_radius_dae_t *this,
						 radius_message_t *message, host_t *client)
{
	chunk_t data;

	data = message->get_encoding(message);
	if (sendto(this->fd, data.ptr, data.len, 0, client->get_sockaddr(client),
			   *client->get_sockaddr_len(client)) != data.len)
	{
		DBG1(DBG_CFG, "sending RADIUS DAE response failed: %s", strerror(errno));
	}
}

/**
 * Check if we request is a retransmit, retransmit stored response
 */
static bool send_retransmit(private_eap_radius_dae_t *this,
							radius_message_t *request, host_t *client)
{
	enumerator_t *enumerator;
	entry_t *entry;
	bool found = FALSE;

	enumerator = this->responses->create_enumerator(this->responses);
	while (enumerator->enumerate(enumerator, &entry))
	{
		if (client->equals(client, entry->client) &&
			request->get_identifier(request) ==
							entry->response->get_identifier(entry->response))
		{
			DBG1(DBG_CFG, "received retransmit of RADIUS %N, retransmitting %N "
				 "to %H", radius_message_code_names, request->get_code(request),
				 radius_message_code_names,
				 entry->response->get_code(entry->response), client);
			send_message(this, entry->response, client);
			found = TRUE;
			break;
		}
	}
	enumerator->destroy(enumerator);

	return found;
}

/**
 * Send an ACK/NAK response for a request
 */
static void send_response(private_eap_radius_dae_t *this,
						  radius_message_t *request, radius_message_code_t code,
						  host_t *client)
{
	radius_message_t *response;

	response = radius_message_create(code);
	response->set_identifier(response, request->get_identifier(request));
	if (response->sign(response, request->get_authenticator(request),
					   this->secret, this->hasher, this->signer, NULL, FALSE))
	{
		send_message(this, response, client);
		save_retransmit(this, response, client);
	}
	else
	{
		response->destroy(response);
	}
}

/**
 * Add all IKE_SAs matching to user to a list
 */
static void add_matching_ike_sas(linked_list_t *list, identification_t *user)
{
	enumerator_t *enumerator;
	ike_sa_t *ike_sa;
	ike_sa_id_t *id;

	enumerator = charon->ike_sa_manager->create_enumerator(
												charon->ike_sa_manager, FALSE);
	while (enumerator->enumerate(enumerator, &ike_sa))
	{
		if (user->matches(user, ike_sa->get_other_eap_id(ike_sa)))
		{
			id = ike_sa->get_id(ike_sa);
			list->insert_last(list, id->clone(id));
		}
	}
	enumerator->destroy(enumerator);
}

/**
 * Get list of IKE_SAs matching a Disconnect/CoA request
 */
static linked_list_t *get_matching_ike_sas(private_eap_radius_dae_t *this,
									radius_message_t *request, host_t *client)
{
	enumerator_t *enumerator;
	identification_t *user;
	linked_list_t *ids;
	chunk_t data;
	int type;

	ids = linked_list_create();

	enumerator = request->create_enumerator(request);
	while (enumerator->enumerate(enumerator, &type, &data))
	{
		if (type == RAT_USER_NAME && data.len)
		{
			user = identification_create_from_data(data);
			DBG1(DBG_CFG, "received RADIUS DAE %N for %Y from %H",
				 radius_message_code_names, request->get_code(request),
				 user, client);
			add_matching_ike_sas(ids, user);
			user->destroy(user);
		}
	}
	enumerator->destroy(enumerator);

	return ids;
}

/**
 * Process a DAE disconnect request, send response
 */
static void process_disconnect(private_eap_radius_dae_t *this,
							   radius_message_t *request, host_t *client)
{
	enumerator_t *enumerator;
	linked_list_t *ids;
	ike_sa_id_t *id;

	ids = get_matching_ike_sas(this, request, client);

	if (ids->get_count(ids))
	{
		DBG1(DBG_CFG, "closing %d IKE_SA%s matching %N, sending %N",
			 ids->get_count(ids), ids->get_count(ids) > 1 ? "s" : "",
			 radius_message_code_names, RMC_DISCONNECT_REQUEST,
			 radius_message_code_names, RMC_DISCONNECT_ACK);

		enumerator = ids->create_enumerator(ids);
		while (enumerator->enumerate(enumerator, &id))
		{
			lib->processor->queue_job(lib->processor, (job_t*)
									  delete_ike_sa_job_create(id, TRUE));
		}
		enumerator->destroy(enumerator);

		send_response(this, request, RMC_DISCONNECT_ACK, client);
	}
	else
	{
		DBG1(DBG_CFG, "no IKE_SA matches %N, sending %N",
			 radius_message_code_names, RMC_DISCONNECT_REQUEST,
			 radius_message_code_names, RMC_DISCONNECT_NAK);
		send_response(this, request, RMC_DISCONNECT_NAK, client);
	}
	ids->destroy_offset(ids, offsetof(ike_sa_id_t, destroy));
}

/**
 * Apply a new lifetime to an IKE_SA
 */
static void apply_lifetime(private_eap_radius_dae_t *this, ike_sa_id_t *id,
						   uint32_t lifetime)
{
	ike_sa_t *ike_sa;

	ike_sa = charon->ike_sa_manager->checkout(charon->ike_sa_manager, id);
	if (ike_sa)
	{
		if (ike_sa->set_auth_lifetime(ike_sa, lifetime) == DESTROY_ME)
		{
			charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager,
														ike_sa);
		}
		else
		{
			charon->ike_sa_manager->checkin(charon->ike_sa_manager, ike_sa);
		}
	}
}

/**
 * Process a DAE CoA request, send response
 */
static void process_coa(private_eap_radius_dae_t *this,
						radius_message_t *request, host_t *client)
{
	enumerator_t *enumerator;
	linked_list_t *ids;
	ike_sa_id_t *id;
	chunk_t data;
	int type;
	uint32_t lifetime = 0;
	bool lifetime_seen = FALSE;

	ids = get_matching_ike_sas(this, request, client);

	if (ids->get_count(ids))
	{
		enumerator = request->create_enumerator(request);
		while (enumerator->enumerate(enumerator, &type, &data))
		{
			if (type == RAT_SESSION_TIMEOUT && data.len == 4)
			{
				lifetime = untoh32(data.ptr);
				lifetime_seen = TRUE;
				break;
			}
		}
		enumerator->destroy(enumerator);

		if (lifetime_seen)
		{
			DBG1(DBG_CFG, "applying %us lifetime to %d IKE_SA%s matching %N, "
				 "sending %N", lifetime, ids->get_count(ids),
				 ids->get_count(ids) > 1 ? "s" : "",
				 radius_message_code_names, RMC_COA_REQUEST,
				 radius_message_code_names, RMC_COA_ACK);

			enumerator = ids->create_enumerator(ids);
			while (enumerator->enumerate(enumerator, &id))
			{
				apply_lifetime(this, id, lifetime);
			}
			enumerator->destroy(enumerator);
			send_response(this, request, RMC_COA_ACK, client);
		}
		else
		{
			DBG1(DBG_CFG, "no Session-Timeout attribute found in %N, sending %N",
				 radius_message_code_names, RMC_COA_REQUEST,
				 radius_message_code_names, RMC_COA_NAK);
			send_response(this, request, RMC_COA_NAK, client);
		}
	}
	else
	{
		DBG1(DBG_CFG, "no IKE_SA matches %N, sending %N",
			 radius_message_code_names, RMC_COA_REQUEST,
			 radius_message_code_names, RMC_COA_NAK);
		send_response(this, request, RMC_COA_NAK, client);
	}
	ids->destroy_offset(ids, offsetof(ike_sa_id_t, destroy));
}

/**
 * Receive RADIUS DAE requests
 */
CALLBACK(receive, bool,
	private_eap_radius_dae_t *this, int fd, watcher_event_t event)
{
	struct sockaddr_storage addr;
	socklen_t addr_len = sizeof(addr);
	radius_message_t *request;
	char buf[2048];
	ssize_t len;
	host_t *client;

	len = recvfrom(this->fd, buf, sizeof(buf), MSG_DONTWAIT,
				   (struct sockaddr*)&addr, &addr_len);
	if (len > 0)
	{
		request = radius_message_parse(chunk_create(buf, len));
		if (request)
		{
			client = host_create_from_sockaddr((struct sockaddr*)&addr);
			if (client)
			{
				if (!send_retransmit(this, request, client))
				{
					if (request->verify(request, NULL, this->secret,
										this->hasher, this->signer))
					{
						switch (request->get_code(request))
						{
							case RMC_DISCONNECT_REQUEST:
								process_disconnect(this, request, client);
								break;
							case RMC_COA_REQUEST:
								process_coa(this, request, client);
								break;
							default:
								DBG1(DBG_CFG, "ignoring unsupported RADIUS DAE "
									 "%N message from %H",
									 radius_message_code_names,
									 request->get_code(request), client);
							break;
						}
					}
				}
				client->destroy(client);
			}
			request->destroy(request);
		}
		else
		{
			DBG1(DBG_NET, "ignoring invalid RADIUS DAE request");
		}
	}
	else if (errno != EWOULDBLOCK)
	{
		DBG1(DBG_NET, "receiving RADIUS DAE request failed: %s", strerror(errno));
	}
	return TRUE;
}

/**
 * Open DAE socket
 */
static bool open_socket(private_eap_radius_dae_t *this)
{
	host_t *host;

	this->fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if (this->fd == -1)
	{
		DBG1(DBG_CFG, "unable to open RADIUS DAE socket: %s", strerror(errno));
		return FALSE;
	}

	host = host_create_from_string(
				lib->settings->get_str(lib->settings,
							"%s.plugins.eap-radius.dae.listen", "0.0.0.0",
							lib->ns),
				lib->settings->get_int(lib->settings,
							"%s.plugins.eap-radius.dae.port", RADIUS_DAE_PORT,
							lib->ns));
	if (!host)
	{
		DBG1(DBG_CFG, "invalid RADIUS DAE listen address");
		return FALSE;
	}

	if (bind(this->fd, host->get_sockaddr(host),
			 *host->get_sockaddr_len(host)) == -1)
	{
		DBG1(DBG_CFG, "unable to bind RADIUS DAE socket: %s", strerror(errno));
		host->destroy(host);
		return FALSE;
	}
	host->destroy(host);
	return TRUE;
}

METHOD(eap_radius_dae_t, destroy, void,
	private_eap_radius_dae_t *this)
{
	if (this->fd != -1)
	{
		lib->watcher->remove(lib->watcher, this->fd);
		close(this->fd);
	}
	DESTROY_IF(this->signer);
	DESTROY_IF(this->hasher);
	this->responses->destroy_function(this->responses, (void*)entry_destroy);
	free(this);
}

/**
 * See header
 */
eap_radius_dae_t *eap_radius_dae_create(eap_radius_accounting_t *accounting)
{
	private_eap_radius_dae_t *this;

	INIT(this,
		.public = {
			.destroy = _destroy,
		},
		.accounting = accounting,
		.fd = -1,
		.secret = {
			.ptr = lib->settings->get_str(lib->settings,
									"%s.plugins.eap-radius.dae.secret", NULL,
									lib->ns),
		},
		.hasher = lib->crypto->create_hasher(lib->crypto, HASH_MD5),
		.signer = lib->crypto->create_signer(lib->crypto, AUTH_HMAC_MD5_128),
		.responses = linked_list_create(),
	);

	if (!this->hasher || !this->signer)
	{
		destroy(this);
		return NULL;
	}
	if (!this->secret.ptr)
	{
		DBG1(DBG_CFG, "missing RADIUS DAE secret, disabled");
		destroy(this);
		return NULL;
	}
	this->secret.len = strlen(this->secret.ptr);
	if (!this->signer->set_key(this->signer, this->secret) ||
		!open_socket(this))
	{
		destroy(this);
		return NULL;
	}

	lib->watcher->add(lib->watcher, this->fd, WATCHER_READ, receive, this);

	return &this->public;
}
