/*
 * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * See the COPYRIGHT file distributed with this work for additional
 * information regarding copyright ownership.
 */

/*! \file */

#include <config.h>

#include <stdbool.h>

#include <isc/util.h>

#include <isc/mem.h>
#include <isc/once.h>
#include <isc/string.h>

#include <dns/acl.h>
#include <dns/geoip.h>

#include <isc/thread.h>
#include <math.h>
#ifndef WIN32
#include <netinet/in.h>
#else
#ifndef _WINSOCKAPI_
#define _WINSOCKAPI_   /* Prevent inclusion of winsock.h in windows.h */
#endif
#include <winsock2.h>
#endif	/* WIN32 */
#include <dns/log.h>

#ifdef HAVE_GEOIP
#include <GeoIP.h>
#include <GeoIPCity.h>

/*
 * This structure preserves state from the previous GeoIP lookup,
 * so that successive lookups for the same data from the same IP
 * address will not require repeated calls into the GeoIP library
 * to look up data in the database. This should improve performance
 * somewhat.
 *
 * For lookups in the City and Region databases, we preserve pointers
 * to the GeoIPRecord and GeoIPregion structures; these will need to be
 * freed by GeoIPRecord_delete() and GeoIPRegion_delete().
 *
 * for lookups in ISP, AS, Org and Domain we prserve a pointer to
 * the returned name; these must be freed by free().
 *
 * For lookups in Country we preserve a pointer to the text of
 * the country code, name, etc (we use a different pointer for this
 * than for the names returned by Org, ISP, etc, because those need
 * to be freed but country lookups do not).
 *
 * For lookups in Netspeed we preserve the returned ID.
 *
 * XXX: Currently this mechanism is only used for IPv4 lookups; the
 * family and addr6 fields are to be used IPv6 is added.
 */
typedef struct geoip_state {
	uint16_t subtype;
	unsigned int family;
	uint32_t ipnum;
	geoipv6_t ipnum6;
	uint8_t scope;
	GeoIPRecord *record;
	GeoIPRegion *region;
	const char *text;
	char *name;
	int id;
	isc_mem_t *mctx;
} geoip_state_t;

#ifdef ISC_PLATFORM_USETHREADS
static isc_mutex_t key_mutex;
static bool state_key_initialized = false;
static isc_thread_key_t state_key;
static isc_once_t mutex_once = ISC_ONCE_INIT;
static isc_mem_t *state_mctx = NULL;

static void
key_mutex_init(void) {
	RUNTIME_CHECK(isc_mutex_init(&key_mutex) == ISC_R_SUCCESS);
}

static void
free_state(void *arg) {
	geoip_state_t *state = arg;
	if (state != NULL && state->record != NULL)
		GeoIPRecord_delete(state->record);
	if (state != NULL)
		isc_mem_putanddetach(&state->mctx,
				     state, sizeof(geoip_state_t));
	isc_thread_key_setspecific(state_key, NULL);
}

static isc_result_t
state_key_init(void) {
	isc_result_t result;

	result = isc_once_do(&mutex_once, key_mutex_init);
	if (result != ISC_R_SUCCESS)
		return (result);

	if (!state_key_initialized) {
		LOCK(&key_mutex);
		if (!state_key_initialized) {
			int ret;

			if (state_mctx == NULL)
				result = isc_mem_create2(0, 0, &state_mctx, 0);
			if (result != ISC_R_SUCCESS)
				goto unlock;
			isc_mem_setname(state_mctx, "geoip_state", NULL);
			isc_mem_setdestroycheck(state_mctx, false);

			ret = isc_thread_key_create(&state_key, free_state);
			if (ret == 0)
				state_key_initialized = true;
			else
				result = ISC_R_FAILURE;
		}
 unlock:
		UNLOCK(&key_mutex);
	}

	return (result);
}
#else
static geoip_state_t saved_state;
#endif

static void
clean_state(geoip_state_t *state) {
	if (state == NULL)
		return;

	if (state->record != NULL) {
		GeoIPRecord_delete(state->record);
		state->record = NULL;
	}
	if (state->region != NULL) {
		GeoIPRegion_delete(state->region);
		state->region = NULL;
	}
	if (state->name != NULL) {
		free (state->name);
		state->name = NULL;
	}
	state->ipnum = 0;
	state->text = NULL;
	state->id = 0;
}

static isc_result_t
set_state(unsigned int family, uint32_t ipnum, const geoipv6_t *ipnum6,
	  uint8_t scope, dns_geoip_subtype_t subtype, GeoIPRecord *record,
	  GeoIPRegion *region, char *name, const char *text, int id)
{
	geoip_state_t *state = NULL;
#ifdef ISC_PLATFORM_USETHREADS
	isc_result_t result;

	result = state_key_init();
	if (result != ISC_R_SUCCESS)
		return (result);

	state = (geoip_state_t *) isc_thread_key_getspecific(state_key);
	if (state == NULL) {
		state = (geoip_state_t *) isc_mem_get(state_mctx,
						      sizeof(geoip_state_t));
		if (state == NULL)
			return (ISC_R_NOMEMORY);
		memset(state, 0, sizeof(*state));

		result = isc_thread_key_setspecific(state_key, state);
		if (result != ISC_R_SUCCESS) {
			isc_mem_put(state_mctx, state, sizeof(geoip_state_t));
			return (result);
		}

		isc_mem_attach(state_mctx, &state->mctx);
	} else
		clean_state(state);
#else
	state = &saved_state;
	clean_state(state);
#endif

	if (family == AF_INET) {
		state->ipnum = ipnum;
	} else {
		INSIST(ipnum6 != NULL);
		state->ipnum6 = *ipnum6;
	}

	state->family = family;
	state->subtype = subtype;
	state->scope = scope;
	state->record = record;
	state->region = region;
	state->name = name;
	state->text = text;
	state->id = id;

	return (ISC_R_SUCCESS);
}

static geoip_state_t *
get_state_for(unsigned int family, uint32_t ipnum,
	      const geoipv6_t *ipnum6)
{
	geoip_state_t *state;

#ifdef ISC_PLATFORM_USETHREADS
	isc_result_t result;

	result = state_key_init();
	if (result != ISC_R_SUCCESS)
		return (NULL);

	state = (geoip_state_t *) isc_thread_key_getspecific(state_key);
	if (state == NULL)
		return (NULL);
#else
	state = &saved_state;
#endif

	if (state->family == family &&
	    ((state->family == AF_INET && state->ipnum == ipnum) ||
	     (state->family == AF_INET6 && ipnum6 != NULL &&
	      memcmp(state->ipnum6.s6_addr, ipnum6->s6_addr, 16) == 0)))
		return (state);

	return (NULL);
}

/*
 * Country lookups are performed if the previous lookup was from a
 * different IP address than the current, or was for a search of a
 * different subtype.
 */
static const char *
country_lookup(GeoIP *db, dns_geoip_subtype_t subtype,
	       unsigned int family,
	       uint32_t ipnum, const geoipv6_t *ipnum6,
	       uint8_t *scope)
{
	geoip_state_t *prev_state = NULL;
	const char *text = NULL;
	GeoIPLookup gl;

	REQUIRE(db != NULL);

#ifndef HAVE_GEOIP_V6
	/* no IPv6 support? give up now */
	if (family == AF_INET6)
		return (NULL);
#endif

	prev_state = get_state_for(family, ipnum, ipnum6);
	if (prev_state != NULL && prev_state->subtype == subtype) {
		text = prev_state->text;
		if (scope != NULL)
			*scope = prev_state->scope;
	}

	if (text == NULL) {
		switch (subtype) {
		case dns_geoip_country_code:
			if (family == AF_INET)
				text = GeoIP_country_code_by_ipnum_gl(db,
								 ipnum, &gl);
#ifdef HAVE_GEOIP_V6
			else
				text = GeoIP_country_code_by_ipnum_v6_gl(db,
								 *ipnum6, &gl);
#endif
			break;
		case dns_geoip_country_code3:
			if (family == AF_INET)
				text = GeoIP_country_code3_by_ipnum_gl(db,
								 ipnum, &gl);
#ifdef HAVE_GEOIP_V6
			else
				text = GeoIP_country_code3_by_ipnum_v6_gl(db,
								 *ipnum6, &gl);
#endif
			break;
		case dns_geoip_country_name:
			if (family == AF_INET)
				text = GeoIP_country_name_by_ipnum_gl(db,
								 ipnum, &gl);
#ifdef HAVE_GEOIP_V6
			else
				text = GeoIP_country_name_by_ipnum_v6_gl(db,
								 *ipnum6, &gl);
#endif
			break;
		default:
			INSIST(0);
			ISC_UNREACHABLE();
		}

		if (text == NULL)
			return (NULL);

		if (scope != NULL)
			*scope = gl.netmask;

		set_state(family, ipnum, ipnum6, gl.netmask, subtype,
			  NULL, NULL, NULL, text, 0);
	}

	return (text);
}

static char *
city_string(GeoIPRecord *record, dns_geoip_subtype_t subtype, int *maxlen) {
	const char *s;
	char *deconst;

	REQUIRE(record != NULL);
	REQUIRE(maxlen != NULL);

	/* Set '*maxlen' to the maximum length of this subtype, if any */
	switch (subtype) {
	case dns_geoip_city_countrycode:
	case dns_geoip_city_region:
	case dns_geoip_city_continentcode:
		*maxlen = 2;
		break;

	case dns_geoip_city_countrycode3:
		*maxlen = 3;
		break;

	default:
		/* No fixed length; just use strcasecmp() for comparison */
		*maxlen = 255;
	}

	switch (subtype) {
	case dns_geoip_city_countrycode:
		return (record->country_code);
	case dns_geoip_city_countrycode3:
		return (record->country_code3);
	case dns_geoip_city_countryname:
		return (record->country_name);
	case dns_geoip_city_region:
		return (record->region);
	case dns_geoip_city_regionname:
		s = GeoIP_region_name_by_code(record->country_code,
					      record->region);
		DE_CONST(s, deconst);
		return (deconst);
	case dns_geoip_city_name:
		return (record->city);
	case dns_geoip_city_postalcode:
		return (record->postal_code);
	case dns_geoip_city_continentcode:
		return (record->continent_code);
	case dns_geoip_city_timezonecode:
		s = GeoIP_time_zone_by_country_and_region(record->country_code,
							  record->region);
		DE_CONST(s, deconst);
		return (deconst);
	default:
		INSIST(0);
		ISC_UNREACHABLE();
	}
}

static bool
is_city(dns_geoip_subtype_t subtype) {
	switch (subtype) {
	case dns_geoip_city_countrycode:
	case dns_geoip_city_countrycode3:
	case dns_geoip_city_countryname:
	case dns_geoip_city_region:
	case dns_geoip_city_regionname:
	case dns_geoip_city_name:
	case dns_geoip_city_postalcode:
	case dns_geoip_city_continentcode:
	case dns_geoip_city_timezonecode:
	case dns_geoip_city_metrocode:
	case dns_geoip_city_areacode:
		return (true);
	default:
		return (false);
	}
}

/*
 * GeoIPRecord lookups are performed if the previous lookup was
 * from a different IP address than the current, or was for a search
 * outside the City database.
 */
static GeoIPRecord *
city_lookup(GeoIP *db, dns_geoip_subtype_t subtype,
	    unsigned int family, uint32_t ipnum,
	    const geoipv6_t *ipnum6,
	    uint8_t *scope)
{
	GeoIPRecord *record = NULL;
	geoip_state_t *prev_state = NULL;

	REQUIRE(db != NULL);

#ifndef HAVE_GEOIP_V6
	/* no IPv6 support? give up now */
	if (family == AF_INET6)
		return (NULL);
#endif

	prev_state = get_state_for(family, ipnum, ipnum6);
	if (prev_state != NULL && is_city(prev_state->subtype)) {
		record = prev_state->record;
		if (scope != NULL)
			*scope = record->netmask;
	}

	if (record == NULL) {
		if (family == AF_INET)
			record = GeoIP_record_by_ipnum(db, ipnum);
#ifdef HAVE_GEOIP_V6
		else
			record = GeoIP_record_by_ipnum_v6(db, *ipnum6);
#endif
		if (record == NULL)
			return (NULL);

		if (scope != NULL)
			*scope = record->netmask;

		set_state(family, ipnum, ipnum6, record->netmask, subtype,
			  record, NULL, NULL, NULL, 0);
	}

	return (record);
}

static char * region_string(GeoIPRegion *region, dns_geoip_subtype_t subtype, int *maxlen) {
	const char *s;
	char *deconst;

	REQUIRE(region != NULL);
	REQUIRE(maxlen != NULL);

	switch (subtype) {
	case dns_geoip_region_countrycode:
		*maxlen = 2;
		return (region->country_code);
	case dns_geoip_region_code:
		*maxlen = 2;
		return (region->region);
	case dns_geoip_region_name:
		*maxlen = 255;
		s = GeoIP_region_name_by_code(region->country_code,
					      region->region);
		DE_CONST(s, deconst);
		return (deconst);
	default:
		INSIST(0);
		ISC_UNREACHABLE();
	}
}

static bool
is_region(dns_geoip_subtype_t subtype) {
	switch (subtype) {
	case dns_geoip_region_countrycode:
	case dns_geoip_region_code:
		return (true);
	default:
		return (false);
	}
}

/*
 * GeoIPRegion lookups are performed if the previous lookup was
 * from a different IP address than the current, or was for a search
 * outside the Region database.
 */
static GeoIPRegion *
region_lookup(GeoIP *db, dns_geoip_subtype_t subtype,
	      uint32_t ipnum, uint8_t *scope)
{
	GeoIPRegion *region = NULL;
	geoip_state_t *prev_state = NULL;
	GeoIPLookup gl;

	REQUIRE(db != NULL);

	prev_state = get_state_for(AF_INET, ipnum, NULL);
	if (prev_state != NULL && is_region(prev_state->subtype)) {
		region = prev_state->region;
		if (scope != NULL)
			*scope = prev_state->scope;
	}

	if (region == NULL) {
		region = GeoIP_region_by_ipnum_gl(db, ipnum, &gl);
		if (region == NULL)
			return (NULL);

		if (scope != NULL)
			*scope = gl.netmask;

		set_state(AF_INET, ipnum, NULL, gl.netmask,
			  subtype, NULL, region, NULL, NULL, 0);
	}

	return (region);
}

/*
 * ISP, Organization, AS Number and Domain lookups are performed if
 * the previous lookup was from a different IP address than the current,
 * or was for a search of a different subtype.
 */
static char *
name_lookup(GeoIP *db, dns_geoip_subtype_t subtype,
	    uint32_t ipnum, uint8_t *scope)
{
	char *name = NULL;
	geoip_state_t *prev_state = NULL;
	GeoIPLookup gl;

	REQUIRE(db != NULL);

	prev_state = get_state_for(AF_INET, ipnum, NULL);
	if (prev_state != NULL && prev_state->subtype == subtype) {
		name = prev_state->name;
		if (scope != NULL)
			*scope = prev_state->scope;
	}

	if (name == NULL) {
		name = GeoIP_name_by_ipnum_gl(db, ipnum, &gl);
		if (name == NULL)
			return (NULL);

		if (scope != NULL)
			*scope = gl.netmask;

		set_state(AF_INET, ipnum, NULL, gl.netmask,
			  subtype, NULL, NULL, name, NULL, 0);
	}

	return (name);
}

/*
 * Netspeed lookups are performed if the previous lookup was from a
 * different IP address than the current, or was for a search of a
 * different subtype.
 */
static int
netspeed_lookup(GeoIP *db, dns_geoip_subtype_t subtype,
		uint32_t ipnum, uint8_t *scope)
{
	geoip_state_t *prev_state = NULL;
	bool found = false;
	GeoIPLookup gl;
	int id = -1;

	REQUIRE(db != NULL);

	prev_state = get_state_for(AF_INET, ipnum, NULL);
	if (prev_state != NULL && prev_state->subtype == subtype) {
		id = prev_state->id;
		if (scope != NULL)
			*scope = prev_state->scope;
		found = true;
	}

	if (!found) {
		id = GeoIP_id_by_ipnum_gl(db, ipnum, &gl);
		if (id == 0)
			return (0);

		if (scope != NULL)
			*scope = gl.netmask;

		set_state(AF_INET, ipnum, NULL, gl.netmask,
			  subtype, NULL, NULL, NULL, NULL, id);
	}

	return (id);
}
#endif /* HAVE_GEOIP */

#define DB46(addr, geoip, name) \
	((addr->family == AF_INET) ? (geoip->name##_v4) : (geoip->name##_v6))

#ifdef HAVE_GEOIP
/*
 * Find the best database to answer a generic subtype
 */
static dns_geoip_subtype_t
fix_subtype(const isc_netaddr_t *reqaddr, const dns_geoip_databases_t *geoip,
	    dns_geoip_subtype_t subtype)
{
	dns_geoip_subtype_t ret = subtype;

	switch (subtype) {
	case dns_geoip_countrycode:
		if (DB46(reqaddr, geoip, city) != NULL)
			ret = dns_geoip_city_countrycode;
		else if (reqaddr->family == AF_INET && geoip->region != NULL)
			ret = dns_geoip_region_countrycode;
		else if (DB46(reqaddr, geoip, country) != NULL)
			ret = dns_geoip_country_code;
		break;
	case dns_geoip_countrycode3:
		if (DB46(reqaddr, geoip, city) != NULL)
			ret = dns_geoip_city_countrycode3;
		else if (DB46(reqaddr, geoip, country) != NULL)
			ret = dns_geoip_country_code3;
		break;
	case dns_geoip_countryname:
		if (DB46(reqaddr, geoip, city) != NULL)
			ret = dns_geoip_city_countryname;
		else if (DB46(reqaddr, geoip, country) != NULL)
			ret = dns_geoip_country_name;
		break;
	case dns_geoip_region:
		if (DB46(reqaddr, geoip, city) != NULL)
			ret = dns_geoip_city_region;
		else if (reqaddr->family == AF_INET && geoip->region != NULL)
			ret = dns_geoip_region_code;
		break;
	case dns_geoip_regionname:
		if (DB46(reqaddr, geoip, city) != NULL)
			ret = dns_geoip_city_regionname;
		else if (reqaddr->family == AF_INET && geoip->region != NULL)
			ret = dns_geoip_region_name;
		break;
	default:
		break;
	}

	return (ret);
}
#endif /* HAVE_GEOIP */

bool
dns_geoip_match(const isc_netaddr_t *reqaddr, uint8_t *scope,
		const dns_geoip_databases_t *geoip,
		const dns_geoip_elem_t *elt)
{
#ifndef HAVE_GEOIP
	UNUSED(reqaddr);
	UNUSED(geoip);
	UNUSED(elt);

	return (false);
#else
	GeoIP *db;
	GeoIPRecord *record;
	GeoIPRegion *region;
	dns_geoip_subtype_t subtype;
	uint32_t ipnum = 0;
	int maxlen = 0, id, family;
	const char *cs;
	char *s;
#ifdef HAVE_GEOIP_V6
	const geoipv6_t *ipnum6 = NULL;
#else
	const void *ipnum6 = NULL;
#endif

	INSIST(geoip != NULL);

	family = reqaddr->family;
	switch (family) {
	case AF_INET:
		ipnum = ntohl(reqaddr->type.in.s_addr);
		break;
	case AF_INET6:
#ifdef HAVE_GEOIP_V6
		ipnum6 = &reqaddr->type.in6;
		break;
#else
		return (false);
#endif
	default:
		return (false);
	}

	subtype = fix_subtype(reqaddr, geoip, elt->subtype);

	switch (subtype) {
	case dns_geoip_country_code:
		maxlen = 2;
		goto getcountry;

	case dns_geoip_country_code3:
		maxlen = 3;
		goto getcountry;

	case dns_geoip_country_name:
		maxlen = 255;
 getcountry:
		db = DB46(reqaddr, geoip, country);
		if (db == NULL)
			return (false);

		INSIST(elt->as_string != NULL);

		cs = country_lookup(db, subtype, family, ipnum, ipnum6, scope);
		if (cs != NULL && strncasecmp(elt->as_string, cs, maxlen) == 0)
			return (true);
		break;

	case dns_geoip_city_countrycode:
	case dns_geoip_city_countrycode3:
	case dns_geoip_city_countryname:
	case dns_geoip_city_region:
	case dns_geoip_city_regionname:
	case dns_geoip_city_name:
	case dns_geoip_city_postalcode:
	case dns_geoip_city_continentcode:
	case dns_geoip_city_timezonecode:
		INSIST(elt->as_string != NULL);

		db = DB46(reqaddr, geoip, city);
		if (db == NULL)
			return (false);

		record = city_lookup(db, subtype, family,
				     ipnum, ipnum6, scope);
		if (record == NULL)
			break;

		s = city_string(record, subtype, &maxlen);
		INSIST(maxlen != 0);
		if (s != NULL && strncasecmp(elt->as_string, s, maxlen) == 0)
			return (true);
		break;

	case dns_geoip_city_metrocode:
		db = DB46(reqaddr, geoip, city);
		if (db == NULL)
			return (false);

		record = city_lookup(db, subtype, family,
				     ipnum, ipnum6, scope);
		if (record == NULL)
			break;

		if (elt->as_int == record->metro_code)
			return (true);
		break;

	case dns_geoip_city_areacode:
		db = DB46(reqaddr, geoip, city);
		if (db == NULL)
			return (false);

		record = city_lookup(db, subtype, family,
				     ipnum, ipnum6, scope);
		if (record == NULL)
			break;

		if (elt->as_int == record->area_code)
			return (true);
		break;

	case dns_geoip_region_countrycode:
	case dns_geoip_region_code:
	case dns_geoip_region_name:
	case dns_geoip_region:
		if (geoip->region == NULL)
			return (false);

		INSIST(elt->as_string != NULL);

		/* Region DB is not supported for IPv6 */
		if (family == AF_INET6)
			return (false);

		region = region_lookup(geoip->region, subtype, ipnum, scope);
		if (region == NULL)
			break;

		s = region_string(region, subtype, &maxlen);
		INSIST(maxlen != 0);
		if (s != NULL && strncasecmp(elt->as_string, s, maxlen) == 0)
			return (true);
		break;

	case dns_geoip_isp_name:
		db = geoip->isp;
		goto getname;

	case dns_geoip_org_name:
		db = geoip->org;
		goto getname;

	case dns_geoip_as_asnum:
		db = geoip->as;
		goto getname;

	case dns_geoip_domain_name:
		db = geoip->domain;

 getname:
		if (db == NULL)
			return (false);

		INSIST(elt->as_string != NULL);
		/* ISP, Org, AS, and Domain are not supported for IPv6 */
		if (family == AF_INET6)
			return (false);

		s = name_lookup(db, subtype, ipnum, scope);
		if (s != NULL) {
			size_t l;
			if (strcasecmp(elt->as_string, s) == 0)
				return (true);
			if (subtype != dns_geoip_as_asnum)
				break;
			/*
			 * Just check if the ASNNNN value matches.
			 */
			l = strlen(elt->as_string);
			if (l > 0U && strchr(elt->as_string, ' ') == NULL &&
			    strncasecmp(elt->as_string, s, l) == 0 &&
			    s[l] == ' ')
				return (true);
		}
		break;

	case dns_geoip_netspeed_id:
		INSIST(geoip->netspeed != NULL);

		/* Netspeed DB is not supported for IPv6 */
		if (family == AF_INET6)
			return (false);

		id = netspeed_lookup(geoip->netspeed, subtype, ipnum, scope);
		if (id == elt->as_int)
			return (true);
		break;

	case dns_geoip_countrycode:
	case dns_geoip_countrycode3:
	case dns_geoip_countryname:
	case dns_geoip_regionname:
		/*
		 * If these were not remapped by fix_subtype(),
		 * the database was unavailable. Always return false.
		 */
		break;

	default:
		INSIST(0);
		ISC_UNREACHABLE();
	}

	return (false);
#endif
}

void
dns_geoip_shutdown(void) {
#ifdef HAVE_GEOIP
	GeoIP_cleanup();
#ifdef ISC_PLATFORM_USETHREADS
	if (state_mctx != NULL)
		isc_mem_detach(&state_mctx);
#endif
#else
	return;
#endif
}
