/*
 * Blacklist support (currently only DNS Blacklists)
 * (C) Copyright 2015-.. Bram Matthys (Syzop) and the UnrealIRCd team
 *
 * 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 1, 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., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include "unrealircd.h"

ModuleHeader MOD_HEADER
= {
	"blacklist",
	"5.0",
	"Check connecting users against DNS Blacklists",
	"UnrealIRCd Team",
	"unrealircd-6",
};

/* In this module and the config syntax I tried to 'abstract' things
 * a little, so things could later be extended if we ever want
 * to introduce another blacklist type (other than DNSBL).
 * Once that happens, best to re-check/audit the source.
 */

/* The first "quick" recheck: */
static long BLACKLIST_RECHECK_TIME_FIRST = 120;

/* After that, check every <this>: */
static long BLACKLIST_RECHECK_TIME = 900;

#define LastBLCheck(x)	(moddata_local_client(x, blacklistrecheck_md).l)
#define SetLastBLCheck(x, y)	do { moddata_local_client(x, blacklistrecheck_md).l = y; } while(0)

/* Types */

typedef enum {
	DNSBL_RECORD=1, DNSBL_BITMASK=2
} DNSBLType;

typedef struct DNSBL DNSBL;
struct DNSBL {
	char *name;
	DNSBLType type;
	int *reply;
};

typedef union BlacklistBackend BlacklistBackend;
union BlacklistBackend
{
	DNSBL *dns;
};

typedef enum {
	BLACKLIST_BACKEND_DNS = 1
} BlacklistBackendType;

typedef struct Blacklist Blacklist;
struct Blacklist {
	Blacklist *prev, *next;
	char *name;
	BlacklistBackendType backend_type;
	BlacklistBackend *backend;
	BanAction *action;
	long ban_time;
	char *reason;
	SecurityGroup *except;
	int recheck;
};

/* Blacklist user struct. In the c-ares DNS reply callback we need to pass
 * some metadata. We can't use client directly there as the client may
 * be gone already by the time we receive the DNS reply.
 */
typedef struct BLUser BLUser;
struct BLUser {
	Client *client;
	int is_ipv6;
	int refcnt;
	/* The following save_* fields are used by softbans: */
	BanAction *save_action;
	long save_tkltime;
	char *save_opernotice;
	char *save_reason;
	char *save_blacklist;
	char *save_blacklist_dns_name;
	int save_blacklist_dns_reply;
};

/* Global variables */
ModDataInfo *blacklist_md = NULL;
ModDataInfo *blacklistrecheck_md = NULL;
Blacklist *conf_blacklist = NULL;

/* Forward declarations */
int blacklist_config_test(ConfigFile *, ConfigEntry *, int, int *);
int blacklist_config_run(ConfigFile *, ConfigEntry *, int);
int blacklist_set_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
int blacklist_set_config_run(ConfigFile *cf, ConfigEntry *ce, int type);
void blacklist_free_conf(void);
void delete_blacklist_block(Blacklist *e);
void blacklist_md_free(ModData *md);
int blacklist_handshake(Client *client);
int blacklist_ip_change(Client *client, const char *oldip);
int blacklist_quit(Client *client, MessageTag *mtags, const char *comment);
int blacklist_preconnect(Client *client);
void blacklist_resolver_callback(void *arg, int status, int timeouts, struct hostent *he);
int blacklist_start_check(Client *client, int recheck);
int blacklist_dns_request(Client *client, Blacklist *bl);
int blacklist_rehash(void);
int blacklist_rehash_complete(void);
void blacklist_set_handshake_delay(void);
void blacklist_free_bluser_if_able(BLUser *bl);
EVENT(blacklist_recheck);

#define SetBLUser(x, y)	do { moddata_local_client(x, blacklist_md).ptr = y; } while(0)
#define BLUSER(x)	((BLUser *)moddata_local_client(x, blacklist_md).ptr)

MOD_TEST()
{
	HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, blacklist_config_test);
	HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, blacklist_set_config_test);

	CallbackAdd(modinfo->handle, CALLBACKTYPE_BLACKLIST_CHECK, blacklist_start_check);
	return MOD_SUCCESS;
}

/** Called upon module init */
MOD_INIT()
{
	ModDataInfo mreq;

	MARK_AS_OFFICIAL_MODULE(modinfo);
	
	memset(&mreq, 0, sizeof(mreq));
	mreq.name = "blacklist";
	mreq.type = MODDATATYPE_LOCAL_CLIENT;
	mreq.free = blacklist_md_free;
	blacklist_md = ModDataAdd(modinfo->handle, mreq);
	if (!blacklist_md)
	{
		config_error("could not register blacklist moddata");
		return MOD_FAILED;
	}

	memset(&mreq, 0, sizeof(mreq));
	mreq.name = "blacklistrecheck";
	mreq.type = MODDATATYPE_LOCAL_CLIENT;
	blacklistrecheck_md = ModDataAdd(modinfo->handle, mreq);
	if (!blacklistrecheck_md)
	{
		config_error("[blacklist] failed adding moddata for blacklistrecheck. "
		             "Do you perhaps still have third/blacklistrecheck loaded? That module is no longer needed!");
		return MOD_FAILED;
	}

	HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, blacklist_config_run);
	HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, blacklist_set_config_run);
	HookAdd(modinfo->handle, HOOKTYPE_HANDSHAKE, 0, blacklist_handshake);
	HookAdd(modinfo->handle, HOOKTYPE_IP_CHANGE, 0, blacklist_ip_change);
	HookAdd(modinfo->handle, HOOKTYPE_PRE_LOCAL_CONNECT, 0, blacklist_preconnect);
	HookAdd(modinfo->handle, HOOKTYPE_REHASH, 0, blacklist_rehash);
	HookAdd(modinfo->handle, HOOKTYPE_REHASH_COMPLETE, 0, blacklist_rehash_complete);
	HookAdd(modinfo->handle, HOOKTYPE_LOCAL_QUIT, 0, blacklist_quit);

	EventAdd(modinfo->handle, "blacklist_recheck", blacklist_recheck, NULL, 2000, 0);

	RegisterApiCallbackResolverHost(modinfo->handle, "blacklist_resolver_callback", blacklist_resolver_callback);

	return MOD_SUCCESS;
}

/** Called upon module load */
MOD_LOAD()
{
	blacklist_set_handshake_delay();
	return MOD_SUCCESS;
}

/** Called upon unload */
MOD_UNLOAD()
{
	blacklist_free_conf();
	return MOD_SUCCESS;
}

int blacklist_rehash(void)
{
	blacklist_free_conf();
	return 0;
}

int blacklist_rehash_complete(void)
{
	blacklist_set_handshake_delay();
	return 0;
}

void blacklist_set_handshake_delay(void)
{
	if ((iConf.handshake_delay == -1) && conf_blacklist)
	{
		/*
		Too noisy?
		config_status("[blacklist] I'm setting set::handshake-delay to 2 seconds. "
		              "You may wish to set an explicit setting in the configuration file.");
		config_status("See https://www.unrealircd.org/docs/Set_block#set::handshake-delay");
		*/
		iConf.handshake_delay = 2;
	}
}

/** Find blacklist { } block */
Blacklist *blacklist_find_block_by_dns(char *name)
{
	Blacklist *d;
	
	for (d = conf_blacklist; d; d = d->next)
		if ((d->backend_type == BLACKLIST_BACKEND_DNS) && !strcmp(name, d->backend->dns->name))
			return d;

	return NULL;
}

void blacklist_free_conf(void)
{
	Blacklist *d, *d_next;

	for (d = conf_blacklist; d; d = d_next)
	{
		d_next = d->next;
		delete_blacklist_block(d);
	}
	conf_blacklist = NULL;
}

void delete_blacklist_block(Blacklist *e)
{
	if (e->backend_type == BLACKLIST_BACKEND_DNS)
	{
		if (e->backend->dns)
		{
			safe_free(e->backend->dns->name);
			safe_free(e->backend->dns->reply);
			safe_free(e->backend->dns);
		}
	}
	
	safe_free(e->backend);

	safe_free(e->name);
	safe_free(e->reason);
	safe_free_all_ban_actions(e->action);
	free_security_group(e->except);

	safe_free(e);
}

int blacklist_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
{
	ConfigEntry	*cep, *cepp, *ceppp;
	int errors = 0;
	char has_reason = 0, has_ban_time = 0, has_action = 0;
	char has_dns_type = 0, has_dns_reply = 0, has_dns_name = 0, has_recheck = 0;

	if (type != CONFIG_MAIN)
		return 0;
	
	if (!ce)
		return 0;
	
	if (strcmp(ce->name, "blacklist"))
		return 0; /* not interested in non-blacklist stuff.. */
	
	if (!ce->value)
	{
		config_error("%s:%i: blacklist block without name (use: blacklist somename { })",
			ce->file->filename, ce->line_number);
		*errs = 1;
		return -1;
	}

	/* Now actually go parse the blacklist { } block */
	for (cep = ce->items; cep; cep = cep->next)
	{
		if (!strcmp(cep->name, "dns"))
		{
			for (cepp = cep->items; cepp; cepp = cepp->next)
			{
				if (!strcmp(cepp->name, "reply"))
				{
					if (has_dns_reply)
					{
						/* this is an error (not a warning) */
						config_error("%s:%i: blacklist block may contain only one blacklist::dns::reply item. "
									 "You can specify multiple replies by using: reply { 1; 2; 4; };",
									 cepp->file->filename, cepp->line_number);
						errors++;
						continue;
					}
					if (!cepp->value && !cepp->items)
					{
						config_error_blank(cepp->file->filename, cepp->line_number, "blacklist::dns::reply");
						errors++;
						continue;
					}
					has_dns_reply = 1; /* we have a reply. now whether it's actually valid is another story.. */
					if (cepp->value && cepp->items)
					{
						config_error("%s:%i: blacklist::dns::reply must be either using format 'reply 1;' or "
									 "'reply { 1; 2; 4; }; but not both formats at the same time.",
									 cepp->file->filename, cepp->line_number);
						errors++;
						continue;
					}
					if (cepp->value)
					{
						if (atoi(cepp->value) <= 0)
						{
							config_error("%s:%i: blacklist::dns::reply must be >0",
								cepp->file->filename, cepp->line_number);
							errors++;
							continue;
						}
					}
					if (cepp->items)
					{
						for (ceppp = cepp->items; ceppp; ceppp=ceppp->next)
						{
							if (atoi(ceppp->name) <= 0)
							{
								config_error("%s:%i: all items in blacklist::dns::reply must be >0",
									cepp->file->filename, cepp->line_number);
								errors++;
							}
						}
					}
				} else
				if (!cepp->value)
				{
					config_error_empty(cepp->file->filename, cepp->line_number,
						"blacklist::dns", cepp->name);
					errors++;
					continue;
				} else
				if (!strcmp(cepp->name, "name"))
				{
					if (has_dns_name)
					{
						config_warn_duplicate(cepp->file->filename,
							cepp->line_number, "blacklist::dns::name");
					}
					has_dns_name = 1;
				} else
				if (!strcmp(cepp->name, "type"))
				{
					if (has_dns_type)
					{
						config_warn_duplicate(cepp->file->filename,
							cepp->line_number, "blacklist::dns::type");
					}
					has_dns_type = 1;
					if (!strcmp(cepp->value, "record"))
						;
					else if (!strcmp(cepp->value, "bitmask"))
						;
					else
					{
						config_error("%s:%i: unknown blacklist::dns::type '%s', must be either 'record' or 'bitmask'",
							cepp->file->filename, cepp->line_number, cepp->value);
						errors++;
					}
				}
			}
		} else
		if (!strcmp(cep->name, "except"))
		{
			test_match_block(cf, cep, &errors);
		} else
		if (!cep->value)
		{
			config_error_empty(cep->file->filename, cep->line_number,
				"blacklist", cep->name);
			errors++;
			continue;
		}
		else if (!strcmp(cep->name, "action"))
		{
			if (has_action)
			{
				config_warn_duplicate(cep->file->filename,
					cep->line_number, "blacklist::action");
				continue;
			}
			has_action = 1;
			errors += test_ban_action_config(cep);
		}
		else if (!strcmp(cep->name, "ban-time"))
		{
			if (has_ban_time)
			{
				config_warn_duplicate(cep->file->filename,
					cep->line_number, "blacklist::ban-time");
				continue;
			}
			has_ban_time = 1;
		}
		else if (!strcmp(cep->name, "reason"))
		{
			if (has_reason)
			{
				config_warn_duplicate(cep->file->filename,
					cep->line_number, "blacklist::reason");
				continue;
			}
			has_reason = 1;
		}
		else if (!strcmp(cep->name, "recheck"))
		{
			if (has_recheck)
			{
				config_warn_duplicate(cep->file->filename,
					cep->line_number, "blacklist::recheck");
				continue;
			}
			has_recheck = 1;
		}
		else
		{
			config_error_unknown(cep->file->filename, cep->line_number,
				"blacklist", cep->name);
			errors++;
		}
	}

	if (!has_action)
	{
		config_error_missing(ce->file->filename, ce->line_number,
			"blacklist::action");
		errors++;
	}

	if (!has_reason)
	{
		config_error_missing(ce->file->filename, ce->line_number,
			"blacklist::reason");
		errors++;
	}

	if (!has_dns_name)
	{
		config_error_missing(ce->file->filename, ce->line_number,
			"blacklist::dns::name");
		errors++;
	}

	if (!has_dns_type)
	{
		config_error_missing(ce->file->filename, ce->line_number,
			"blacklist::dns::type");
		errors++;
	}

	if (!has_dns_reply)
	{
		config_error_missing(ce->file->filename, ce->line_number,
			"blacklist::dns::reply");
		errors++;
	}

	*errs = errors;
	return errors ? -1 : 1;
}

int blacklist_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
{
	ConfigEntry *cep, *cepp, *ceppp;
	Blacklist *d = NULL;
	
	if (type != CONFIG_MAIN)
		return 0;
	
	if (!ce || !ce->name || strcmp(ce->name, "blacklist"))
		return 0; /* not interested */

	d = safe_alloc(sizeof(Blacklist));
	safe_strdup(d->name, ce->value);
	/* set some defaults */
	d->ban_time = 3600;
	d->recheck = 1;
	
	/* assume dns for now ;) */
	d->backend_type = BLACKLIST_BACKEND_DNS;
	d->backend = safe_alloc(sizeof(BlacklistBackend));
	d->backend->dns = safe_alloc(sizeof(DNSBL));

	for (cep = ce->items; cep; cep = cep->next)
	{
		if (!strcmp(cep->name, "dns"))
		{
			for (cepp = cep->items; cepp; cepp = cepp->next)
			{
				if (!strcmp(cepp->name, "reply"))
				{
					if (cepp->value)
					{
						/* single reply */
						d->backend->dns->reply = safe_alloc(sizeof(int)*2);
						d->backend->dns->reply[0] = atoi(cepp->value);
						d->backend->dns->reply[1] = 0;
					} else
					if (cepp->items)
					{
						/* (potentially) multiple reply values */
						int cnt = 0;
						for (ceppp = cepp->items; ceppp; ceppp = ceppp->next)
						{
							if (ceppp->name)
								cnt++;
						}
						
						if (cnt == 0)
							abort(); /* impossible */
						
						d->backend->dns->reply = safe_alloc(sizeof(int)*(cnt+1));
						
						cnt = 0;
						for (ceppp = cepp->items; ceppp; ceppp = ceppp->next)
						{
							d->backend->dns->reply[cnt++] = atoi(ceppp->name);
						}
						d->backend->dns->reply[cnt] = 0;
					}
				} else
				if (!strcmp(cepp->name, "type"))
				{
					if (!strcmp(cepp->value, "record"))
						d->backend->dns->type = DNSBL_RECORD;
					else if (!strcmp(cepp->value, "bitmask"))
						d->backend->dns->type = DNSBL_BITMASK;
				} else
				if (!strcmp(cepp->name, "name"))
				{
					safe_strdup(d->backend->dns->name, cepp->value);
				}
			}
		}
		else if (!strcmp(cep->name, "action"))
		{
			parse_ban_action_config(cep, &d->action);
		}
		else if (!strcmp(cep->name, "ban-time"))
		{
			d->ban_time = config_checkval(cep->value, CFG_TIME);
		}
		else if (!strcmp(cep->name, "reason"))
		{
			safe_strdup(d->reason, cep->value);
		}
		else if (!strcmp(cep->name, "except"))
		{
			conf_match_block(cf, cep, &d->except);
		}
		else if (!strcmp(cep->name, "recheck"))
		{
			d->recheck = config_checkval(cep->value, CFG_YESNO);
		}
	}

	AddListItem(d, conf_blacklist);
	
	return 0;
}

/** Test the set::blacklist configuration */
int blacklist_set_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
{
	int errors = 0;
	ConfigEntry *cep, *cepp;

	if (type != CONFIG_SET)
		return 0;
	
	/* We are only interrested in set::blacklist.. */
	if (!ce || !ce->name || strcmp(ce->name, "blacklist"))
		return 0;
	
	for (cep = ce->items; cep; cep = cep->next)
	{
		if (!strcmp(cep->name, "recheck-time-first"))
		{
			int v;
			if (!cep->value)
			{
				config_error("%s:%i: set::blacklist::recheck-time-first with no value",
					cep->file->filename, cep->line_number);
				errors++;
				continue;
			}
			if (!strcmp(cep->value, "never"))
			{
				config_error("%s:%i: if you want to disable blacklist rechecks, then you "
				             "should set set::blacklist::recheck-time to 'never' and "
				             "don't set set::blacklist::recheck-time-first.",
				             cep->file->filename, cep->line_number);
				errors++;
				continue;
			}
			v = config_checkval(cep->value, CFG_TIME);
			if (v < 60)
			{
				config_error("%s:%i: set::blacklist::recheck-time-first cannot be less than 60 seconds",
					cep->file->filename, cep->line_number);
				errors++;
			}
		} else
		if (!strcmp(cep->name, "recheck-time"))
		{
			int v;
			if (!cep->value)
			{
				config_error("%s:%i: set::blacklist::recheck-time with no value",
					cep->file->filename, cep->line_number);
				errors++;
				continue;
			}
			if (strcmp(cep->value, "never"))
			{
				v = config_checkval(cep->value, CFG_TIME);
				if (v < 60)
				{
					config_error("%s:%i: set::blacklist::recheck-time cannot be less than 60 seconds",
						cep->file->filename, cep->line_number);
					errors++;
				}
			}
		} else
		{
			config_error("%s:%i: unknown directive set::blacklist::%s",
				cep->file->filename, cep->line_number, cep->name);
			errors++;
			continue;
		}
	}
	
	*errs = errors;
	return errors ? -1 : 1;
}

/* Configure ourselves based on the set::blacklist settings */
int blacklist_set_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
{
	ConfigEntry *cep, *cepp;

	if (type != CONFIG_SET)
		return 0;
	
	/* We are only interrested in set::blacklist.. */
	if (!ce || !ce->name || strcmp(ce->name, "blacklist"))
		return 0;
	
	for (cep = ce->items; cep; cep = cep->next)
	{
		if (!strcmp(cep->name, "recheck-time"))
			BLACKLIST_RECHECK_TIME = config_checkval(cep->value, CFG_TIME);
		if (!strcmp(cep->name, "recheck-time-first"))
			BLACKLIST_RECHECK_TIME_FIRST = config_checkval(cep->value, CFG_TIME);
	}
	return 1;
}

void blacklist_md_free(ModData *md)
{
	BLUser *bl = md->ptr;

	/* Mark bl->client as dead. Free the struct, if able. */
	blacklist_free_bluser_if_able(bl);

	md->ptr = NULL;
}

int blacklist_handshake(Client *client)
{
	blacklist_start_check(client, 0);
	return 0;
}

int blacklist_ip_change(Client *client, const char *oldip)
{
	blacklist_start_check(client, 0);
	return 0;
}

int blacklist_start_check(Client *client, int recheck)
{
	Blacklist *bl;

	if (find_tkl_exception(TKL_BLACKLIST, client))
	{
		/* If the user is exempt from DNSBL checking then:
		 * 1) Don't bother checking DNSBL's
		 * 2) Disable handshake delay for this user, since it serves no purpose.
		 */
		SetNoHandshakeDelay(client);
		return 0;
	}

	if (!BLUSER(client))
	{
		SetBLUser(client, safe_alloc(sizeof(BLUser)));
		BLUSER(client)->client = client;
	}

	for (bl = conf_blacklist; bl; bl = bl->next)
	{
		/* Stop processing if client is (being) killed already */
		if (!BLUSER(client))
			break;

		if (recheck && !bl->recheck)
			continue; /* blacklist::recheck is no */

		/* Check if user is exempt (then don't bother checking) */
		if (user_allowed_by_security_group(client, bl->except))
			continue;

		/* Initiate blacklist requests */
		if (bl->backend_type == BLACKLIST_BACKEND_DNS)
			blacklist_dns_request(client, bl);
	}
	
	return 0;
}

int blacklist_dns_request(Client *client, Blacklist *d)
{
	char buf[256], wbuf[128];
	unsigned int e[8];
	char *ip = GetIP(client);
	
	if (!ip)
		return 0;

	memset(&e, 0, sizeof(e));

	if (strchr(ip, '.'))
	{
		/* IPv4 */
		if (sscanf(ip, "%u.%u.%u.%u", &e[0], &e[1], &e[2], &e[3]) != 4)
			return 0;
	
		snprintf(buf, sizeof(buf), "%u.%u.%u.%u.%s", e[3], e[2], e[1], e[0], d->backend->dns->name);
	} else
	if (strchr(ip, ':'))
	{
		/* IPv6 */
		int i;
		BLUSER(client)->is_ipv6 = 1;
		if (sscanf(ip, "%x:%x:%x:%x:%x:%x:%x:%x",
		    &e[0], &e[1], &e[2], &e[3], &e[4], &e[5], &e[6], &e[7]) != 8)
		{
			return 0;
		}
		*buf = '\0';
		for (i = 7; i >= 0; i--)
		{
			snprintf(wbuf, sizeof(wbuf), "%x.%x.%x.%x.",
				(unsigned int)(e[i] & 0xf),
				(unsigned int)((e[i] >> 4) & 0xf),
				(unsigned int)((e[i] >> 8) & 0xf),
				(unsigned int)((e[i] >> 12) & 0xf));
			strlcat(buf, wbuf, sizeof(buf));
		}
		strlcat(buf, d->backend->dns->name, sizeof(buf));
	}
	else
		return 0; /* unknown IP format */

	BLUSER(client)->refcnt++; /* one (more) blacklist result remaining */
	
	unreal_gethostbyname_api(buf, AF_INET, "blacklist_resolver_callback", BLUSER(client));
	
	return 0;
}

void blacklist_cancel(BLUser *bl)
{
	bl->client = NULL;
}

int blacklist_quit(Client *client, MessageTag *mtags, const char *comment)
{
	if (BLUSER(client))
		blacklist_cancel(BLUSER(client));

	return 0;
}

/** Free the BLUSER() struct, if we are able to do so.
 * This should only be called if the underlying client is dead or dyeing
 * and not earlier.
 * Reasons why we 'are not able' are: refcnt is non-zero, that is:
 * there is still an outstanding resolver request (eg: slow blacklist).
 * In that case, no worries, we will be called again after that request
 * is finished.
 */
void blacklist_free_bluser_if_able(BLUser *bl)
{
	if (bl->client)
		bl->client = NULL;

	if (bl->refcnt > 0)
		return; /* unable, still have DNS requests/replies in-flight */

	safe_free(bl->save_opernotice);
	safe_free(bl->save_reason);
	safe_free(bl->save_blacklist);
	safe_free(bl->save_blacklist_dns_name);
	free_all_ban_actions(bl->save_action);
	safe_free(bl);
}

char *getdnsblname(char *p, Client *client)
{
	int dots = 0;
	int dots_count;

	if (!client)
		return NULL;

	if (BLUSER(client)->is_ipv6)
		dots_count = 32;
	else
		dots_count = 4;

	for (; *p; p++)
	{
		if (*p == '.')
		{
			dots++;
			if (dots == dots_count)
				return p+1;
		}
	}
	return NULL;
}

/* Parse DNS reply.
 * A reply will be an A record in the format x.x.x.<reply>
 */
int blacklist_parse_reply(struct hostent *he, int entry)
{
	char ipbuf[64];
	char *p;

	if ((he->h_addrtype != AF_INET) || (he->h_length != 4))
		return 0;

	*ipbuf = '\0';
	if (!inet_ntop(AF_INET, he->h_addr_list[entry], ipbuf, sizeof(ipbuf)))
		return 0;
	
	p = strrchr(ipbuf, '.');
	if (!p)
		return 0;
	
	return atoi(p+1);
}

/** Take the actual ban action.
 * Called from blacklist_hit() and for immediate bans and
 * from blacklist_preconnect() for softbans that need to be delayed
 * as to give the user the opportunity to do SASL Authentication.
 */
int blacklist_action(Client *client, char *opernotice, BanAction *ban_action, char *ban_reason, long ban_time,
                     char *blacklist, char *blacklist_dns_name, int blacklist_dns_reply)
{
	unreal_log_raw(ULOG_INFO, "blacklist", "BLACKLIST_HIT", client,
	               opernotice,
	               log_data_string("blacklist_name", blacklist),
	               log_data_string("blacklist_dns_name", blacklist_dns_name),
	               log_data_integer("blacklist_dns_reply", blacklist_dns_reply),
	               log_data_string("ban_action", ban_actions_to_string(ban_action)),
	               log_data_string("ban_reason", ban_reason),
	               log_data_integer("ban_time", ban_time));
	return take_action(client, ban_action, ban_reason, ban_time, 0, NULL);
}

void blacklist_hit(Client *client, Blacklist *bl, int reply)
{
	char opernotice[512], banbuf[512], reply_num[5];
	NameValuePrioList *nvp = NULL;
	BLUser *blu = BLUSER(client);

	if (find_tkline_match(client, 1))
		return; /* already klined/glined. Don't send the warning from below. */

	if (IsUser(client))
		snprintf(opernotice, sizeof(opernotice), "[Blacklist] IP %s (%s) matches blacklist %s (%s/reply=%d)",
			GetIP(client), client->name, bl->name, bl->backend->dns->name, reply);
	else
		snprintf(opernotice, sizeof(opernotice), "[Blacklist] IP %s matches blacklist %s (%s/reply=%d)",
			GetIP(client), bl->name, bl->backend->dns->name, reply);

	snprintf(reply_num, sizeof(reply_num), "%d", reply);

	add_nvplist(&nvp, 0, "blacklist", bl->name);
	add_nvplist(&nvp, 0, "dnsname", bl->backend->dns->name);
	add_nvplist(&nvp, 0, "dnsreply", reply_num);
	unreal_expand_string(bl->reason, banbuf, sizeof(banbuf), nvp, 0, client);

	if (only_soft_actions(bl->action) && blu)
	{
		/* This extra 'if' ensures we don't overwrite an existing hit (first hit wins) */
		if (!blu->save_action)
		{
			/* For soft bans, delay the action until later (so user can do SASL auth) */
			blu->save_action = duplicate_ban_actions(bl->action);
			blu->save_tkltime = bl->ban_time;
			safe_strdup(blu->save_opernotice, opernotice);
			safe_strdup(blu->save_reason, banbuf);
			safe_strdup(blu->save_blacklist, bl->name);
			safe_strdup(blu->save_blacklist_dns_name, bl->backend->dns->name);
			blu->save_blacklist_dns_reply = reply;
		}
	} else {
		/* Otherwise, execute the action immediately */
		blacklist_action(client, opernotice, bl->action, banbuf, bl->ban_time, bl->name, bl->backend->dns->name, reply);
	}
}

void blacklist_process_result(Client *client, int status, struct hostent *he)
{
	Blacklist *bl;
	char *domain;
	int reply;
	int i;
	int replycnt;
	
	if ((status != 0) || (he->h_length != 4) || !he->h_name)
		return; /* invalid reply */
	
	domain = getdnsblname(he->h_name, client);
	if (!domain)
		return; /* odd */
	bl = blacklist_find_block_by_dns(domain);
	if (!bl)
		return; /* possibly just rehashed and the blacklist block is gone now */
	
	/* walk through all replies for this record... until we have a hit */
	for (replycnt=0; he->h_addr_list[replycnt]; replycnt++)
	{
		reply = blacklist_parse_reply(he, replycnt);

		for (i = 0; bl->backend->dns->reply[i]; i++)
		{
			if ((bl->backend->dns->reply[i] == -1) ||
				( (bl->backend->dns->type == DNSBL_BITMASK) && (reply & bl->backend->dns->reply[i]) ) ||
				( (bl->backend->dns->type == DNSBL_RECORD) && (bl->backend->dns->reply[i] == reply) ) )
			{
				blacklist_hit(client, bl, reply);
				return;
			}
		}
	}
}

void blacklist_resolver_callback(void *arg, int status, int timeouts, struct hostent *he)
{
	BLUser *blu = (BLUser *)arg;
	Client *client = blu->client;

	blu->refcnt--; /* one less outstanding DNS request remaining */

	/* If we are the last to resolve something and the client is gone
	 * already then free the struct.
	 */
	if ((blu->refcnt == 0) && !client)
		blacklist_free_bluser_if_able(blu);

	blu = NULL;

	if (!client)
		return; /* Client left already */
	/* ^^ note: do not merge this with the other 'if' a few lines up (refcnt!) */

	blacklist_process_result(client, status, he);
}

int blacklist_preconnect(Client *client)
{
	BLUser *blu = BLUSER(client);

	if (!blu || !blu->save_action)
		return HOOK_CONTINUE;

	/* There was a pending softban... has the user authenticated via SASL by now? */
	if (IsLoggedIn(client))
		return HOOK_CONTINUE; /* yup, so the softban does not apply. */

	if (blacklist_action(client, blu->save_opernotice, blu->save_action, blu->save_reason, blu->save_tkltime,
	                     blu->save_blacklist, blu->save_blacklist_dns_name, blu->save_blacklist_dns_reply) > 0)
	{
		return HOOK_DENY;
	}
	return HOOK_CONTINUE; /* exempt */
}

void blacklist_recheck_user(Client *client)
{
	SetLastBLCheck(client, TStime());
	blacklist_start_check(client, 1);
}

EVENT(blacklist_recheck)
{
	Client *client;
	time_t last_check;

	if (BLACKLIST_RECHECK_TIME == 0)
		return;

	list_for_each_entry(client, &lclient_list, lclient_node)
	{
		/* Only check connected users */
		if (!IsUser(client))
			continue;

		last_check = LastBLCheck(client);
		if ((last_check == 0) && (TStime() - client->local->creationtime >= BLACKLIST_RECHECK_TIME_FIRST))
		{
			/* First time: check after 60 seconds already */
			blacklist_recheck_user(client);
		} else /* After that, check every <...> seconds */
		if (last_check && (TStime() - last_check) >= BLACKLIST_RECHECK_TIME)
		{
			blacklist_recheck_user(client);
		}
	}
}
