/*
 *   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.
 *
 *   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.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the Free Software
 *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
 */

/**
 * $Id: ab7a1802c6aec9d359fb5f6034cb284f719179cd $
 *
 * @brief Multi-packet state handling
 * @file main/state.c
 *
 * @ingroup AVP
 *
 * @copyright 2014 The FreeRADIUS server project
 */
RCSID("$Id: ab7a1802c6aec9d359fb5f6034cb284f719179cd $")

#include <freeradius-devel/radiusd.h>
#include <freeradius-devel/state.h>
#include <freeradius-devel/md5.h>
#include <freeradius-devel/rad_assert.h>
#include <freeradius-devel/process.h>

typedef struct state_entry_t {
	uint8_t		state[MD5_DIGEST_LENGTH];

	time_t		cleanup;
	struct state_entry_t *prev;
	struct state_entry_t *next;

	int		tries;
	bool		ours;

	TALLOC_CTX		*ctx;
	VALUE_PAIR		*vps;

	char		*server;
	unsigned int	request_number;
	RADCLIENT	*request_client;
	main_config_t	*request_root;

	void 		*opaque;
	void 		(*free_opaque)(void *opaque);
} state_entry_t;

struct fr_state_t {
	rbtree_t *tree;

	state_entry_t *head, *tail;

#ifdef HAVE_PTHREAD_H
	pthread_mutex_t mutex;
#endif
};

static fr_state_t global_state;

#define STATE_FREE(_x) if (_x != &global_state) talloc_free(_x)

#ifdef HAVE_PTHREAD_H

#define PTHREAD_MUTEX_LOCK pthread_mutex_lock
#define PTHREAD_MUTEX_UNLOCK pthread_mutex_unlock

#else
/*
 *	This is easier than ifdef's throughout the code.
 */
#define PTHREAD_MUTEX_LOCK(_x)
#define PTHREAD_MUTEX_UNLOCK(_x)

#endif

/*
 *	rbtree callback.
 */
static int state_entry_cmp(void const *one, void const *two)
{
	state_entry_t const *a = one;
	state_entry_t const *b = two;

	return memcmp(a->state, b->state, sizeof(a->state));
}

static bool state_entry_link(fr_state_t *state, state_entry_t *entry)
{
	if (!rbtree_insert(state->tree, entry)) {
		return false;
	}

	/*
	 *	Link it to the end of the list, which is implicitely
	 *	ordered by cleanup time.
	 */
	if (!state->head) {
		entry->prev = entry->next = NULL;
		state->head = state->tail = entry;
	} else {
		rad_assert(state->tail != NULL);

		entry->prev = state->tail;
		state->tail->next = entry;

		entry->next = NULL;
		state->tail = entry;
	}

	return true;
}

static void state_entry_unlink(fr_state_t *state, state_entry_t *entry)
{
	state_entry_t *prev, *next;

	prev = entry->prev;
	next = entry->next;

	if (prev) {
		rad_assert(state->head != entry);
		prev->next = next;
	} else if (state->head) {
		rad_assert(state->head == entry);
		state->head = next;
	}

	if (next) {
		rad_assert(state->tail != entry);
		next->prev = prev;
	} else if (state->tail) {
		rad_assert(state->tail == entry);
		state->tail = prev;
	}

	rbtree_deletebydata(state->tree, entry);
}

/*
 *	When an entry is free'd, it's removed from the linked list of
 *	cleanup timers.
 */
static void state_entry_free(fr_state_t *state, state_entry_t *entry)
{
	/*
	 *	If we're deleting the whole tree, don't bother doing
	 *	all of the fixups.
	 */
	if (!state || !state->tree) return;

	state_entry_unlink(state, entry);

	if (entry->opaque) {
		entry->free_opaque(entry->opaque);
	}

#ifdef WITH_VERIFY_PTR
	(void) talloc_get_type_abort(entry, state_entry_t);
#endif
	if (entry->ctx) talloc_free(entry->ctx);

	talloc_free(entry);
}

fr_state_t *fr_state_init(TALLOC_CTX *ctx)
{
	fr_state_t *state;

	if (!ctx) {
		state = &global_state;
		if (state->tree) return state;
	} else {
		state = talloc_zero(ctx, fr_state_t);
		if (!state) return 0;
	}

#ifdef HAVE_PTHREAD_H
	if (pthread_mutex_init(&state->mutex, NULL) != 0) {
		STATE_FREE(state);
		return NULL;
	}
#endif

	state->tree = rbtree_create(NULL, state_entry_cmp, NULL, 0);
	if (!state->tree) {
		STATE_FREE(state);
		return NULL;
	}

	return state;
}

void fr_state_delete(fr_state_t *state)
{
	rbtree_t *my_tree;

	if (!state) return;

	PTHREAD_MUTEX_LOCK(&state->mutex);

	/*
	 *	Tell the talloc callback to NOT delete the entry from
	 *	the tree.  We're deleting the entire tree.
	 */
	my_tree = state->tree;
	state->tree = NULL;

	rbtree_free(my_tree);
	PTHREAD_MUTEX_UNLOCK(&state->mutex);

	STATE_FREE(state);
}

/*
 *	Create a fake request, based on what we know about the
 *	session that has expired, and inject it into the server to
 *	allow final logging or cleaning up.
 */
static REQUEST *fr_state_cleanup_request(state_entry_t *entry)
{
	REQUEST		*request;
	RADIUS_PACKET	*packet = NULL;
	RADIUS_PACKET	*reply_packet = NULL;
	VALUE_PAIR	*vp;

	/*
	 *	Allocate a new fake request with enough to keep
	 *	the rest of the server happy.
	 */
	request = request_alloc(NULL);
	if (unlikely(!request)) return NULL;

	packet = rad_alloc(request, false);
	if (unlikely(!packet)) {
	error:
		TALLOC_FREE(reply_packet);
		TALLOC_FREE(packet);
		TALLOC_FREE(request);
		return NULL;
	}

	reply_packet = rad_alloc(request, false);
	if (unlikely(!reply_packet)) goto error;

	/*
	 *	Move the server from the state entry over to the
	 *	request. Clearing it in the state means this
	 *	function will never be called again.
	 */
	request->server = talloc_steal(request, entry->server);
	entry->server = NULL;

	/*
	 *	Build the fake request with the limited
	 *	information we have from the state.
	 */
	request->packet = packet;
	request->reply = reply_packet;
	request->number = entry->request_number;
	request->client = entry->request_client;
	request->root = entry->request_root;
	request->handle = rad_postauth;

	/*
	 *	Move session-state VPS over, after first freeing the
	 *	separately-parented state_ctx that was allocated along with the
	 *	fake request.
	 */
	talloc_free(request->state_ctx);
	request->state_ctx = entry->ctx;
	request->state = entry->vps;

	entry->ctx = NULL;
	entry->vps = NULL;

	/*
	 *	Set correct Post-Auth-Type section
	 */
	fr_pair_delete_by_num(&request->config, PW_POST_AUTH_TYPE, 0, TAG_ANY);
	vp = pair_make_config("Post-Auth-Type", "Client-Lost", T_OP_SET);
	if (unlikely(!vp)) goto error;

	VERIFY_REQUEST(request);
	return request;
}

/*
 *	Check state for old entries that need to be cleaned up. If
 *	they are old enough then move them from the global state
 *	list to a list of entries to clean up (outside the mutex).
 *	Called with the mutex held.
 */
static state_entry_t *fr_state_cleanup_find(fr_state_t *state)
{
	time_t now = time(NULL);
	state_entry_t *entry, *next;
	state_entry_t *head = NULL, **tail = &head;

	for (entry = state->head; entry != NULL; entry = next) {
		next = entry->next;

		/*
		 *	Unused.  We can delete it, even if now isn't
		 *	the time to clean it up.
		 */
		if (!entry->ctx && !entry->opaque) {
			state_entry_free(state, entry);
			continue;
		}

		/*
		 *	Not yet time to clean it up.
		 */
		if (entry->cleanup > now) {
			continue;
		}

		/*
		 *	We're not running the "client lost" section.
		 *	Just nuke the entry now.
		 */
		if (!main_config.postauth_client_lost) {
			state_entry_free(state, entry);
			continue;
		}

		/*
		 *	Old enough that the request has been removed.
		 *	We can add it to the cleanup list.
		 */
		state_entry_unlink(state, entry);
		entry->prev = entry->next = NULL;
		(*tail) = entry;
		tail = &entry->next;
	}

	return head;
}

/*
 *	Inject all requests in cleanup list for cleanup post-auth
 */
static void fr_state_cleanup(state_entry_t *head)
{
	state_entry_t *entry, *next;

	if (!head) return;

	for (entry = head; entry != NULL; entry = next) {
		REQUEST *request;

		next = entry->next;

		request = fr_state_cleanup_request(entry);
		if (request) {
			RDEBUG2("No response from client, cleaning up expired state");
			RDEBUG2("Restoring &session-state");

			/*
			 *	@todo - print out message
			 *	saying where the handler was
			 *	in the process?  i.e. "sent
			 *	server cert", etc.  This will
			 *	require updating the EAP code
			 *	to put a new attribute into
			 *	the session state list.
			 */

			rdebug_pair_list(L_DBG_LVL_2, request, request->state, "&session-state:");

			request_inject(request);
		}

		if (entry->opaque) {
			entry->free_opaque(entry->opaque);
		}

		if (entry->ctx) talloc_free(entry->ctx);

		talloc_free(entry);
	}
}

static void state_entry_calc(REQUEST *request, state_entry_t *entry, VALUE_PAIR *vp)
{
	/*
	 *	Assume our own State first.  This is where the state
	 *	is the correct size, AND we're not proxying it to an
	 *	external home server.  If we are proxying it to an
	 *	external home server, then that home server creates
	 *	the State attribute, and we don't control it.
	 */
	if (entry->ours ||
	    (vp->vp_length == sizeof(entry->state) &&
	     (!request->proxy || (request->proxy->dst_port == 0)))) {
		memcpy(entry->state, vp->vp_octets, sizeof(entry->state));
		entry->ours = true;

	} else {
		FR_MD5_CTX ctx;

		/*
		 *	We don't control the external State attribute.
		 *	As a result, different home servers _may_
		 *	create the same State attribute.  In order to
		 *	differentiate them, we "mix in" the User-Name,
		 *	which should contain the realm.  And we then
		 *	hope that different home servers in the same
		 *	realm don't create overlapping State
		 *	attributes.
		 */
		fr_md5_init(&ctx);
		fr_md5_update(&ctx, vp->vp_octets, vp->vp_length);

		vp = fr_pair_find_by_num(request->packet->vps, PW_USER_NAME, 0, TAG_ANY);
		if (vp) fr_md5_update(&ctx, vp->vp_octets, vp->vp_length);

		fr_md5_final(entry->state, &ctx);
		fr_md5_destroy(&ctx);
	}
}


/*
 *	Create a new entry.  Called with the mutex held.
 */
static state_entry_t *fr_state_entry_create(fr_state_t *state, REQUEST *request, RADIUS_PACKET *packet, state_entry_t *old)
{
	size_t i;
	uint32_t x;
	time_t now = time(NULL);
	VALUE_PAIR *vp;
	state_entry_t *entry;

	/*
	 *	Limit the size of the cache based on how many requests
	 *	we can handle at the same time.
	 */
	if (rbtree_num_elements(state->tree) >= main_config.max_requests * 2) {
		return NULL;
	}

	/*
	 *	Allocate a new one.
	 */
	entry = talloc_zero(state->tree, state_entry_t);
	if (!entry) return NULL;

	/*
	 *	Limit the lifetime of this entry based on how long the
	 *	server takes to process a request.  Doing it this way
	 *	isn't perfect, but it's reasonable, and it's one less
	 *	thing for an administrator to configure.
	 */
	entry->cleanup = now + main_config.max_request_time * 2;

	/*
	 *	Hacks for EAP, until we convert EAP to using the state API.
	 *
	 *	The EAP module creates it's own State attribute, so we
	 *	want to use that one in preference to one we create.
	 */
	vp = fr_pair_find_by_num(packet->vps, PW_STATE, 0, TAG_ANY);

	/*
	 *	If possible, base the new one off of the old one.
	 */
	if (old) {
		entry->tries = old->tries + 1;
		entry->ours = old->ours;

		/*
		 *	Track State
		 */
		if (!vp && entry->ours) {
			memcpy(entry->state, old->state, sizeof(entry->state));

			entry->state[1] = entry->state[0] ^ entry->tries;
			entry->state[8] = entry->state[2] ^ (((uint32_t) HEXIFY(RADIUSD_VERSION)) & 0xff);
			entry->state[10] = entry->state[2] ^ ((((uint32_t) HEXIFY(RADIUSD_VERSION)) >> 8) & 0xff);
			entry->state[12] = entry->state[2] ^ ((((uint32_t) HEXIFY(RADIUSD_VERSION)) >> 16) & 0xff);
		}

		/*
		 *	The old one isn't used any more, so we can free it.
		 */
		if (!old->opaque) state_entry_free(state, old);

	} else if (!vp) {
		/*
		 *	16 octets of randomness should be enough to
		 *	have a globally unique state.
		 */
		for (i = 0; i < sizeof(entry->state) / sizeof(x); i++) {
			x = fr_rand();
			memcpy(entry->state + (i * 4), &x, sizeof(x));
		}

		entry->ours = true; /* we created it */
	}

	/*
	 *	If EAP created a State, use that.  Otherwise, use the
	 *	one we created above.
	 */
	if (vp) {
		state_entry_calc(request, entry, vp);

	} else {
		vp = fr_pair_afrom_num(packet, PW_STATE, 0);
		fr_pair_value_memcpy(vp, entry->state, sizeof(entry->state));
		fr_pair_add(&packet->vps, vp);
	}

	/*	Make unique for different virtual servers handling same request
	 */
	if (request->server) {
		/*
		 *	Make unique for different virtual servers handling same request
		 */
		if (entry->ours) *((uint32_t *)(&entry->state[4])) ^= fr_hash_string(request->server);

		/*
		 *	Copy server to state in case it's needed for cleanup
		 */
		entry->server = talloc_strdup(entry, request->server);
		entry->request_number = request->number;
		entry->request_client = request->client;
		entry->request_root = request->root;
	}

	if (!state_entry_link(state, entry)) {
		talloc_free(entry);
		return NULL;
	}

	return entry;
}


/*
 *	Find the entry, based on the State attribute.
 */
static state_entry_t *fr_state_find(REQUEST *request, fr_state_t *state, const char *server, RADIUS_PACKET *packet)
{
	VALUE_PAIR *vp;
	state_entry_t *entry, my_entry;

	vp = fr_pair_find_by_num(packet->vps, PW_STATE, 0, TAG_ANY);
	if (!vp) return NULL;

	my_entry.ours = false;
	state_entry_calc(request, &my_entry, vp);

	/*	Make unique for different virtual servers handling same request
	 */
	if (server && my_entry.ours) *((uint32_t *)(&my_entry.state[4])) ^= fr_hash_string(server);

	entry = rbtree_finddata(state->tree, &my_entry);

#ifdef WITH_VERIFY_PTR
	if (entry)  (void) talloc_get_type_abort(entry, state_entry_t);
#endif

	return entry;
}

/*
 *	Called when sending Access-Accept or Access-Reject, so
 *	that all State is discarded.
 */
void fr_state_discard(REQUEST *request, RADIUS_PACKET *original)
{
	state_entry_t *entry;
	fr_state_t *state = &global_state;

	fr_pair_list_free(&request->state);
	request->state = NULL;

	PTHREAD_MUTEX_LOCK(&state->mutex);
	entry = fr_state_find(request, state, request->server, original);
	if (entry) state_entry_free(state, entry);
	PTHREAD_MUTEX_UNLOCK(&state->mutex);
}

/*
 *	Get the VPS from the state.
 */
void fr_state_get_vps(REQUEST *request, RADIUS_PACKET *packet)
{
	state_entry_t *entry;
	fr_state_t *state = &global_state;
	TALLOC_CTX *old_ctx = NULL;

	/*
	 *	No State, don't do anything.
	 */
	if (!fr_pair_find_by_num(request->packet->vps, PW_STATE, 0, TAG_ANY)) {
		RDEBUG3("session-state: No State attribute");
		return;
	}

	rad_assert(request->state == NULL);

	PTHREAD_MUTEX_LOCK(&state->mutex);
	entry = fr_state_find(request, state, request->server, packet);

	/*
	 *	This has to be done in a mutex lock, because talloc
	 *	isn't thread-safe.
	 */
	if (entry) {
		RDEBUG2("Restoring &session-state");

		if (request->state_ctx) old_ctx = request->state_ctx;

		request->state_ctx = entry->ctx;
		request->state = entry->vps;

		entry->ctx = NULL;
		entry->vps = NULL;

		rdebug_pair_list(L_DBG_LVL_2, request, request->state, "&session-state:");

	} else {
		RDEBUG2("session-state: No cached attributes");
	}

	PTHREAD_MUTEX_UNLOCK(&state->mutex);

	/*
	 *	Free this outside of the mutex for less contention.
	 */
	if (old_ctx) talloc_free(old_ctx);

	VERIFY_REQUEST(request);
	return;
}


/*
 *	Put request->state into the State attribute.  Put the State
 *	attribute into the vps list.  Delete the original entry, if it
 *	exists.
 */
bool fr_state_put_vps(REQUEST *request, RADIUS_PACKET *original, RADIUS_PACKET *packet)
{
	state_entry_t *entry, *old;
	fr_state_t *state = &global_state;
	state_entry_t *cleanup_list;

	if (!request->state) {
		size_t i;
		uint32_t x;
		VALUE_PAIR *vp;
		uint8_t buffer[16];

		RDEBUG3("session-state: Nothing to cache");

		if (packet->code != PW_CODE_ACCESS_CHALLENGE) return true;

		vp = fr_pair_find_by_num(packet->vps, PW_STATE, 0, TAG_ANY);
		if (vp) return true;

		/*
		 *
		 */
		for (i = 0; i < sizeof(buffer) / sizeof(x); i++) {
			x = fr_rand();
			memcpy(buffer + (i * 4), &x, sizeof(x));
		}

		vp = fr_pair_afrom_num(packet, PW_STATE, 0);
		fr_pair_value_memcpy(vp, buffer, sizeof(buffer));
		fr_pair_add(&packet->vps, vp);

		return true;
	}

	RDEBUG2("session-state: Saving cached attributes");
	rdebug_pair_list(L_DBG_LVL_1, request, request->state, NULL);

	PTHREAD_MUTEX_LOCK(&state->mutex);

	cleanup_list = fr_state_cleanup_find(state);

	if (original) {
		old = fr_state_find(request, state, request->server, original);
	} else {
		old = NULL;
	}

	/*
	 *	Create a new entry and add it to the list.
	 */
	entry = fr_state_entry_create(state, request, packet, old);
	if (!entry) {
		PTHREAD_MUTEX_UNLOCK(&state->mutex);
		fr_state_cleanup(cleanup_list);
		return false;
	}

	rad_assert(entry->ctx == NULL);
	entry->ctx = request->state_ctx;
	entry->vps = request->state;

	request->state_ctx = NULL;
	request->state = NULL;

	PTHREAD_MUTEX_UNLOCK(&state->mutex);
	fr_state_cleanup(cleanup_list);

	VERIFY_REQUEST(request);
	return true;
}
