/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */

/* The idea behind checksums is that the same username+password doesn't
   increase the penalty, because it's most likely a user with a misconfigured
   account. */

#include "lib.h"
#include "ioloop.h"
#include "hash.h"
#include "str.h"
#include "strescape.h"
#include "llist.h"
#include "ostream.h"
#include "penalty.h"

#include <time.h>

#define PENALTY_DEFAULT_EXPIRE_SECS (60*60)
#define CHECKSUM_VALUE_COUNT 2
#define CHECKSUM_VALUE_PTR_COUNT 10

#define LAST_UPDATE_BITS 15

struct penalty_rec {
	/* ordered by last_update */
	struct penalty_rec *prev, *next;

	char *ident;
	unsigned int last_penalty;

	unsigned int penalty:16;
	unsigned int last_update:LAST_UPDATE_BITS; /* last_penalty + n */
	bool checksum_is_pointer:1;
	/* we use value up to two different checksums.
	   after that switch to pointer. */
	union {
		unsigned int value[CHECKSUM_VALUE_COUNT];
		unsigned int *value_ptr;
	} checksum;
};

struct penalty {
	/* ident => penalty_rec */
	HASH_TABLE(char *, struct penalty_rec *) hash;
	struct penalty_rec *oldest, *newest;

	unsigned int expire_secs;
	struct timeout *to;
};

struct penalty *penalty_init(void)
{
	struct penalty *penalty;

	penalty = i_new(struct penalty, 1);
	hash_table_create(&penalty->hash, default_pool, 0, str_hash, strcmp);
	penalty->expire_secs = PENALTY_DEFAULT_EXPIRE_SECS;
	return penalty;
}

static void penalty_rec_free(struct penalty *penalty, struct penalty_rec *rec)
{
	DLLIST2_REMOVE(&penalty->oldest, &penalty->newest, rec);
	if (rec->checksum_is_pointer)
		i_free(rec->checksum.value_ptr);
	i_free(rec->ident);
	i_free(rec);
}

void penalty_deinit(struct penalty **_penalty)
{
	struct penalty *penalty = *_penalty;

	*_penalty = NULL;

	while (penalty->oldest != NULL)
		penalty_rec_free(penalty, penalty->oldest);
	hash_table_destroy(&penalty->hash);

	timeout_remove(&penalty->to);
	i_free(penalty);
}

void penalty_set_expire_secs(struct penalty *penalty, unsigned int expire_secs)
{
	penalty->expire_secs = expire_secs;
}

static bool
penalty_bump_checksum(struct penalty_rec *rec, unsigned int checksum)
{
	unsigned int *checksums;
	unsigned int i, count;

	if (!rec->checksum_is_pointer) {
		checksums = rec->checksum.value;
		count = CHECKSUM_VALUE_COUNT;
	} else {
		checksums = rec->checksum.value_ptr;
		count = CHECKSUM_VALUE_PTR_COUNT;
	}

	for (i = 0; i < count; i++) {
		if (checksums[i] == checksum) {
			if (i > 0) {
				memmove(checksums + 1, checksums,
					sizeof(checksums[0]) * i);
				checksums[0] = checksum;
			}
			return TRUE;
		}
	}
	return FALSE;
}

static void penalty_add_checksum(struct penalty_rec *rec, unsigned int checksum)
{
	unsigned int *checksums;

	i_assert(checksum != 0);

	if (!rec->checksum_is_pointer) {
		if (rec->checksum.value[CHECKSUM_VALUE_COUNT-1] == 0) {
			memmove(rec->checksum.value + 1, rec->checksum.value,
				sizeof(rec->checksum.value[0]) *
				(CHECKSUM_VALUE_COUNT-1));
			rec->checksum.value[0] = checksum;
			return;
		}

		/* switch to using a pointer */
		checksums = i_new(unsigned int, CHECKSUM_VALUE_PTR_COUNT);
		memcpy(checksums, rec->checksum.value,
		       sizeof(checksums[0]) * CHECKSUM_VALUE_COUNT);
		rec->checksum.value_ptr = checksums;
		rec->checksum_is_pointer = TRUE;
	}

	memmove(rec->checksum.value_ptr + 1, rec->checksum.value_ptr,
		sizeof(rec->checksum.value_ptr[0]) *
		(CHECKSUM_VALUE_PTR_COUNT-1));
	rec->checksum.value_ptr[0] = checksum;
}

unsigned int penalty_get(struct penalty *penalty, const char *ident,
			 time_t *last_penalty_r)
{
	struct penalty_rec *rec;

	rec = hash_table_lookup(penalty->hash, ident);
	if (rec == NULL) {
		*last_penalty_r = 0;
		return 0;
	}

	*last_penalty_r = rec->last_penalty;
	return rec->penalty;
}

static void penalty_timeout(struct penalty *penalty)
{
	struct penalty_rec *rec;
	time_t rec_last_update, expire_time;
	unsigned int diff;

	timeout_remove(&penalty->to);

	expire_time = ioloop_time - penalty->expire_secs;
	while (penalty->oldest != NULL) {
		rec = penalty->oldest;

		rec_last_update = rec->last_penalty + rec->last_update;
		if (rec_last_update > expire_time) {
			diff = rec_last_update - expire_time;
			penalty->to = timeout_add(diff * 1000,
						  penalty_timeout, penalty);
			break;
		}
		hash_table_remove(penalty->hash, rec->ident);
		penalty_rec_free(penalty, rec);
	}
}

void penalty_inc(struct penalty *penalty, const char *ident,
		 unsigned int checksum, unsigned int value)
{
	struct penalty_rec *rec;
	time_t diff;

	i_assert(value > 0 || checksum == 0);
	i_assert(value <= INT_MAX);

	rec = hash_table_lookup(penalty->hash, ident);
	if (rec == NULL) {
		rec = i_new(struct penalty_rec, 1);
		rec->ident = i_strdup(ident);
		hash_table_insert(penalty->hash, rec->ident, rec);
	} else {
		DLLIST2_REMOVE(&penalty->oldest, &penalty->newest, rec);
	}

	if (checksum == 0) {
		rec->penalty = value;
		rec->last_penalty = time_to_uint32(ioloop_time);
	} else {
		if (penalty_bump_checksum(rec, checksum))
			rec->penalty = value - 1;
		else {
			penalty_add_checksum(rec, checksum);
			rec->penalty = value;
			rec->last_penalty = time_to_uint32(ioloop_time);
		}
	}

	diff = ioloop_time - rec->last_penalty;
	if (diff >= (1 << LAST_UPDATE_BITS)) {
		rec->last_update = (1 << LAST_UPDATE_BITS) - 1;
		rec->last_penalty = ioloop_time - rec->last_update;
	} else {
		rec->last_update = time_to_uint32(diff);
	}

	DLLIST2_APPEND(&penalty->oldest, &penalty->newest, rec);

	if (penalty->to == NULL) {
		penalty->to = timeout_add(penalty->expire_secs * 1000,
					  penalty_timeout, penalty);
	}
}

bool penalty_has_checksum(struct penalty *penalty, const char *ident,
			  unsigned int checksum)
{
	struct penalty_rec *rec;
	const unsigned int *checksums;
	unsigned int i, count;

	rec = hash_table_lookup(penalty->hash, ident);
	if (rec == NULL)
		return FALSE;

	if (!rec->checksum_is_pointer) {
		checksums = rec->checksum.value;
		count = CHECKSUM_VALUE_COUNT;
	} else {
		checksums = rec->checksum.value_ptr;
		count = CHECKSUM_VALUE_PTR_COUNT;
	}

	for (i = 0; i < count; i++) {
		if (checksums[i] == checksum)
			return TRUE;
	}
	return FALSE;
}

void penalty_dump(struct penalty *penalty, struct ostream *output)
{
	const struct penalty_rec *rec;
	string_t *str = t_str_new(256);

	for (rec = penalty->oldest; rec != NULL; rec = rec->next) {
		str_truncate(str, 0);
		str_append_tabescaped(str, rec->ident);
		str_printfa(str, "\t%u\t%u\t%u\n",
			    rec->penalty, rec->last_penalty,
			    rec->last_penalty + rec->last_update);
		if (o_stream_send(output, str_data(str), str_len(str)) < 0)
			break;
	}
	o_stream_nsend(output, "\n", 1);
}
