/*
 *
 *  Wireless daemon for Linux
 *
 *  Copyright (C) 2019  Intel Corporation. All rights reserved.
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library 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
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <ell/ell.h>

#include "src/blacklist.h"
#include "src/util.h"
#include "src/iwd.h"
#include "src/module.h"

/*
 * The current timeout is multiplied by this value after an entry is blacklisted
 * more than once.
 */
#define BLACKLIST_DEFAULT_MULTIPLIER	30

/* Initial timeout for a new blacklist entry */
#define BLACKLIST_DEFAULT_TIMEOUT	60

/* The maximum amount of time a BSS can be blacklisted for */
#define BLACKLIST_DEFAULT_MAX_TIMEOUT	86400

static uint64_t blacklist_multiplier;
static uint64_t blacklist_initial_timeout;
static uint64_t blacklist_roam_initial_timeout;
static uint64_t blacklist_max_timeout;

struct blacklist_entry {
	uint8_t addr[6];
	uint64_t added_time;
	uint64_t expire_time;
	enum blacklist_reason reason;
};

struct blacklist_search {
	const uint8_t *addr;
	enum blacklist_reason reason;
};

static struct l_queue *blacklist;

static uint64_t get_reason_timeout(enum blacklist_reason reason)
{
	switch (reason) {
	case BLACKLIST_REASON_CONNECT_FAILED:
		return blacklist_initial_timeout;
	case BLACKLIST_REASON_ROAM_REQUESTED:
		return blacklist_roam_initial_timeout;
	default:
		l_warn("Unhandled blacklist reason: %u", reason);
		return 0;
	}
}

static bool check_if_expired(void *data, void *user_data)
{
	struct blacklist_entry *entry = data;
	uint64_t now = l_get_u64(user_data);

	if (l_time_after(now, entry->expire_time)) {
		l_debug("Removing entry "MAC" on prune", MAC_STR(entry->addr));
		l_free(entry);
		return true;
	}

	return false;
}

static void blacklist_prune(void)
{
	uint64_t now = l_time_now();

	l_queue_foreach_remove(blacklist, check_if_expired, &now);
}

static bool match_addr(const void *a, const void *b)
{
	const struct blacklist_entry *entry = a;
	const uint8_t *addr = b;

	if (!memcmp(entry->addr, addr, 6))
		return true;

	return false;
}

static bool match_addr_and_reason(const void *a, const void *b)
{
	const struct blacklist_entry *entry = a;
	const struct blacklist_search *search = b;

	if (entry->reason != search->reason)
		return false;

	if (!memcmp(entry->addr, search->addr, 6))
		return true;

	return false;
}

void blacklist_add_bss(const uint8_t *addr, enum blacklist_reason reason)
{
	struct blacklist_entry *entry;
	uint64_t timeout;

	blacklist_prune();

	timeout = get_reason_timeout(reason);
	if (!timeout)
		return;

	entry = l_queue_find(blacklist, match_addr, addr);

	if (entry) {
		uint64_t offset;

		if (reason < entry->reason) {
			l_debug("Promoting "MAC" blacklist to reason %u",
					MAC_STR(addr), reason);
			/* Reset this to the new timeout and reason */
			entry->reason = reason;
			entry->added_time = l_time_now();
			entry->expire_time = l_time_offset(entry->added_time,
								timeout);
			return;
		} else if (reason > entry->reason) {
			l_debug("Ignoring blacklist extension of "MAC", "
				"current blacklist status is more severe!",
				MAC_STR(addr));
			return;
		}

		offset = l_time_diff(entry->added_time, entry->expire_time);

		offset *= blacklist_multiplier;

		if (offset > blacklist_max_timeout)
			offset = blacklist_max_timeout;

		entry->expire_time = l_time_offset(entry->added_time, offset);

		return;
	}

	entry = l_new(struct blacklist_entry, 1);

	entry->added_time = l_time_now();
	entry->expire_time = l_time_offset(entry->added_time, timeout);
	entry->reason = reason;
	memcpy(entry->addr, addr, 6);

	l_queue_push_tail(blacklist, entry);
}

bool blacklist_contains_bss(const uint8_t *addr, enum blacklist_reason reason)
{
	struct blacklist_search search = {
		.addr = addr,
		.reason = reason
	};

	blacklist_prune();

	return l_queue_find(blacklist, match_addr_and_reason, &search) != NULL;
}

void blacklist_remove_bss(const uint8_t *addr, enum blacklist_reason reason)
{
	struct blacklist_entry *entry;
	struct blacklist_search search = {
		.addr = addr,
		.reason = reason
	};

	blacklist_prune();

	entry = l_queue_remove_if(blacklist, match_addr_and_reason, &search);

	if (!entry)
		return;

	l_free(entry);
}

static int blacklist_init(void)
{
	const struct l_settings *config = iwd_get_config();

	if (!l_settings_get_uint64(config, "Blacklist", "InitialTimeout",
					&blacklist_initial_timeout))
		blacklist_initial_timeout = BLACKLIST_DEFAULT_TIMEOUT;

	/* For easier user configuration the timeout values are in seconds */
	blacklist_initial_timeout *= L_USEC_PER_SEC;

	if (!l_settings_get_uint64(config, "Blacklist",
					"InitialRoamRequestedTimeout",
					&blacklist_roam_initial_timeout))
		blacklist_roam_initial_timeout = BLACKLIST_DEFAULT_TIMEOUT;

	/* For easier user configuration the timeout values are in seconds */
	blacklist_roam_initial_timeout *= L_USEC_PER_SEC;

	if (!l_settings_get_uint64(config, "Blacklist",
					"Multiplier",
					&blacklist_multiplier))
		blacklist_multiplier = BLACKLIST_DEFAULT_MULTIPLIER;

	if (blacklist_multiplier == 0) {
		l_warn("[Blacklist].Multiplier cannot be zero, setting to 1");
		blacklist_multiplier = 1;
	}

	if (!l_settings_get_uint64(config, "Blacklist",
					"MaximumTimeout",
					&blacklist_max_timeout))
		blacklist_max_timeout = BLACKLIST_DEFAULT_MAX_TIMEOUT;

	blacklist_max_timeout *= L_USEC_PER_SEC;

	if (blacklist_initial_timeout > blacklist_max_timeout)
		l_warn("[Blacklist].InitialTimeout exceeded "
			"[Blacklist].MaximumTimeout!");

	if (!blacklist_initial_timeout)
		l_debug("initial timeout was zero, blacklist will be disabled");

	blacklist = l_queue_new();

	return 0;
}

static void blacklist_exit(void)
{
	l_queue_destroy(blacklist, l_free);
}

IWD_MODULE(blacklist, blacklist_init, blacklist_exit)
