/* Copyright (c) Dovecot authors, see top-level COPYING file */

#include "lib.h"
#include "ioloop.h"
#include "net.h"
#include "crc32.h"
#include "master-service.h"
#include "anvil-client.h"
#include "auth-request.h"
#include "auth-penalty.h"

#include <stdio.h>

/* We don't want IPv6 hosts being able to flood our penalty
   tracking with tons of different IPs. */
#define PENALTY_IPV6_MASK_BITS 48

struct auth_penalty_request {
	struct auth_request *auth_request;
	struct anvil_client *client;
	struct event *event;
	auth_penalty_callback_t *callback;
};

struct auth_penalty {
	struct anvil_client *client;

	bool disabled:1;
};

struct auth_penalty *auth_penalty_init(const char *path)
{
	struct auth_penalty *penalty;

	penalty = i_new(struct auth_penalty, 1);
	penalty->client = anvil_client_init(path, NULL,
					    ANVIL_CLIENT_FLAG_HIDE_ENOENT);
	if (anvil_client_connect(penalty->client, TRUE) < 0)
		penalty->disabled = TRUE;
	else {
		anvil_client_cmd(penalty->client, t_strdup_printf(
			"PENALTY-SET-EXPIRE-SECS\t%u", AUTH_PENALTY_TIMEOUT));
	}
	return penalty;
}

void auth_penalty_deinit(struct auth_penalty **_penalty)
{
	struct auth_penalty *penalty = *_penalty;

	*_penalty = NULL;
	anvil_client_deinit(&penalty->client);
	i_free(penalty);
}

unsigned int auth_penalty_to_secs(unsigned int penalty)
{
	unsigned int i, secs = AUTH_PENALTY_INIT_SECS;

	for (i = 0; i < penalty; i++)
		secs *= 2;
	return secs < AUTH_PENALTY_MAX_SECS ? secs : AUTH_PENALTY_MAX_SECS;
}

static void
auth_penalty_anvil_callback(const struct anvil_reply *reply,
			    struct auth_penalty_request *request)
{
	unsigned int penalty = 0;
	unsigned long last_penalty = 0;
	unsigned int secs, drop_penalty;

	if (reply->error != NULL) {
		/* internal failure. */
		e_error(request->event,
			"Auth penalty lookup failed: %s", reply->error);
		if (!anvil_client_is_connected(request->client)) {
			/* we probably didn't have permissions to reconnect
			   back to anvil. need to restart ourself. */
			e_warning(request->event,
				"Anvil not connected - restarting auth process");
			master_service_stop(master_service);
		}
	} else if (sscanf(reply->reply, "%u %lu", &penalty, &last_penalty) != 2) {
		e_error(request->event,
			"Invalid PENALTY-GET reply: %s", reply->reply);
	} else {
		if ((time_t)last_penalty > ioloop_time) {
			/* time moved backwards? */
			last_penalty = ioloop_time;
		}

		/* update penalty. */
		drop_penalty = AUTH_PENALTY_MAX_PENALTY;
		while (penalty > 0) {
			secs = auth_penalty_to_secs(drop_penalty);
			if (ioloop_time - last_penalty < secs)
				break;
			drop_penalty--;
			penalty--;
		}
		secs = penalty == 0 ? 0 : auth_penalty_to_secs(penalty);
		e_debug(request->event, "Delaying response for %u seconds", secs);
	}

	request->callback(penalty, request->auth_request);
	auth_request_unref(&request->auth_request);
	event_unref(&request->event);
	i_free(request);
}

static const char *
auth_penalty_get_ident(struct auth_request *auth_request)
{
	struct ip_addr ip;

	ip = auth_request->fields.remote_ip;
	if (IPADDR_IS_V6(&ip)) {
		memset(ip.u.ip6.s6_addr + PENALTY_IPV6_MASK_BITS/CHAR_BIT, 0,
		       sizeof(ip.u.ip6.s6_addr) -
		       PENALTY_IPV6_MASK_BITS/CHAR_BIT);
	}
	return net_ip2addr(&ip);
}

void auth_penalty_lookup(struct auth_penalty *penalty,
			 struct auth_request *auth_request,
			 auth_penalty_callback_t *callback)
{
	struct auth_penalty_request *request;
	const char *ident;

	ident = auth_penalty_get_ident(auth_request);
	if (penalty->disabled || ident == NULL ||
	    auth_request->fields.no_penalty) {
		callback(0, auth_request);
		return;
	}

	request = i_new(struct auth_penalty_request, 1);
	request->auth_request = auth_request;
	request->event = event_create(auth_request->event);
	event_set_append_log_prefix(request->event,
		t_strdup_printf("penalty(%s): ", ident));
	request->client = penalty->client;
	request->callback = callback;
	auth_request_ref(auth_request);

	T_BEGIN {
		anvil_client_query(penalty->client,
				   t_strdup_printf("PENALTY-GET\t%s", ident),
				   ANVIL_DEFAULT_LOOKUP_TIMEOUT_MSECS,
				   auth_penalty_anvil_callback, request);
	} T_END;
}

static unsigned int
get_userpass_checksum(struct auth_request *auth_request)
{
	return auth_request->mech_password == NULL ? 0 :
		crc32_str_more(crc32_str(auth_request->mech_password),
			       auth_request->fields.user);
}

void auth_penalty_update(struct auth_penalty *penalty,
			 struct auth_request *auth_request, unsigned int value)
{
	const char *ident;

	ident = auth_penalty_get_ident(auth_request);
	if (penalty->disabled || ident == NULL ||
	    auth_request->fields.no_penalty)
		return;

	if (value > AUTH_PENALTY_MAX_PENALTY) {
		/* even if the actual value doesn't change, the last_change
		   timestamp does. */
		value = AUTH_PENALTY_MAX_PENALTY;
	}
	T_BEGIN {
		const char *cmd;
		unsigned int checksum;

		checksum = value == 0 ? 0 : get_userpass_checksum(auth_request);
		cmd = t_strdup_printf("PENALTY-INC\t%s\t%u\t%u",
				      ident, checksum, value);
		anvil_client_cmd(penalty->client, cmd);
	} T_END;
}
