/*
 * Copyright (C) 2002 Stichting NLnet, Netherlands, stichting@nlnet.nl.
 * Copyright (C) 2012 Vadim Goncharov, Russia, vadim_nuclight@mail.ru.
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the
 * above copyright notice and this permission notice appear in all
 * copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND STICHTING NLNET
 * DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
 * STICHTING NLNET BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
 * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
 * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
 * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE
 * USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 * The development of Dynamically Loadable Zones (DLZ) for Bind 9 was
 * conceived and contributed by Rob Butler.
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the
 * above copyright notice and this permission notice appear in all
 * copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND ROB BUTLER
 * DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
 * ROB BUTLER BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
 * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
 * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
 * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE
 * USE OR PERFORMANCE OF THIS SOFTWARE.
 */

/*
 * Copyright (C) 1999-2001, 2013, 2016  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/.
 */

/*
 * This provides the externally loadable wildcard DLZ module.
 */

#include <inttypes.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include <stdlib.h>

#include <dlz_minimal.h>
#include <dlz_list.h>
#include <dlz_dbi.h>

#include <ctype.h>

#define DE_CONST(konst, var) \
	do { \
		union { const void *k; void *v; } _u; \
		_u.k = konst; \
		var = _u.v; \
	} while (0)

/* fnmatch() return values. */
#define	FNM_NOMATCH	1	/* Match failed. */

/* fnmatch() flags. */
#define	FNM_NOESCAPE	0x01	/* Disable backslash escaping. */
#define	FNM_PATHNAME	0x02	/* Slash must be matched by slash. */
#define	FNM_PERIOD	0x04	/* Period must be matched by period. */
#define	FNM_LEADING_DIR	0x08	/* Ignore /<tail> after Imatch. */
#define	FNM_CASEFOLD	0x10	/* Case insensitive search. */
#define	FNM_IGNORECASE	FNM_CASEFOLD
#define	FNM_FILE_NAME	FNM_PATHNAME

/*
 * Our data structures.
 */

typedef struct named_rr nrr_t;
typedef DLZ_LIST(nrr_t) rr_list_t;

typedef struct config_data {
	char		*zone_pattern;
	char		*axfr_pattern;
	rr_list_t	rrs_list;
	char		*zone;
	char		*record;
	char		*client;

	/* Helper functions from the dlz_dlopen driver */
	log_t *log;
	dns_sdlz_putrr_t *putrr;
	dns_sdlz_putnamedrr_t *putnamedrr;
	dns_dlz_writeablezone_t *writeable_zone;
} config_data_t;

struct named_rr {
	char		*name;
	char		*type;
	int		ttl;
	query_list_t	*data;
	DLZ_LINK(nrr_t)	link;
};

/*
 * Forward references
 */
static int
rangematch(const char *, char, int, char **);

static int
fnmatch(const char *pattern, const char *string, int flags);

static void
b9_add_helper(struct config_data *cd, const char *helper_name, void *ptr);

static const char *
shortest_match(const char *pattern, const char *string);

isc_result_t
dlz_allnodes(const char *zone, void *dbdata, dns_sdlzallnodes_t *allnodes) {
	config_data_t *cd = (config_data_t *) dbdata;
	isc_result_t result;
	char *querystring = NULL;
	nrr_t *nrec;
	int i = 0;

	DE_CONST(zone, cd->zone);

	/* Write info message to log */
	cd->log(ISC_LOG_DEBUG(1),
		"dlz_wildcard allnodes called for zone '%s'", zone);

	result = ISC_R_FAILURE;

	nrec = DLZ_LIST_HEAD(cd->rrs_list);
	while (nrec != NULL) {
		cd->record = nrec->name;

		querystring = build_querystring(nrec->data);

		if (querystring == NULL) {
			result = ISC_R_NOMEMORY;
			goto done;
		}

		cd->log(ISC_LOG_DEBUG(2),
			"dlz_wildcard allnodes entry num %d: calling "
			"putnamedrr(name=%s type=%s ttl=%d qs=%s)",
			i++, nrec->name, nrec->type, nrec->ttl, querystring);

		result = cd->putnamedrr(allnodes, nrec->name, nrec->type,
					nrec->ttl, querystring);
		if (result != ISC_R_SUCCESS)
			goto done;

		nrec = DLZ_LIST_NEXT(nrec, link);
	}

done:
	cd->zone = NULL;

	if (querystring != NULL)
		free(querystring);

	return (result);
}

isc_result_t
dlz_allowzonexfr(void *dbdata, const char *name, const char *client) {
	config_data_t *cd = (config_data_t *) dbdata;

	UNUSED(name);

	/* Write info message to log */
	cd->log(ISC_LOG_DEBUG(1),
		"dlz_wildcard allowzonexfr called for client '%s'", client);

	if (fnmatch(cd->axfr_pattern, client, FNM_CASEFOLD) == 0)
		return (ISC_R_SUCCESS);
	else
		return (ISC_R_NOTFOUND);
}

#if DLZ_DLOPEN_VERSION < 3
isc_result_t
dlz_findzonedb(void *dbdata, const char *name)
#else
isc_result_t
dlz_findzonedb(void *dbdata, const char *name,
	       dns_clientinfomethods_t *methods,
	       dns_clientinfo_t *clientinfo)
#endif
{
	config_data_t *cd = (config_data_t *) dbdata;
	const char *p;

#if DLZ_DLOPEN_VERSION >= 3
	UNUSED(methods);
	UNUSED(clientinfo);
#endif

	p = shortest_match(cd->zone_pattern, name);
	if (p == NULL)
		return (ISC_R_NOTFOUND);

	/* Write info message to log */
	cd->log(ISC_LOG_DEBUG(1),
		"dlz_wildcard findzonedb matched '%s'", p);

	return (ISC_R_SUCCESS);
}

#if DLZ_DLOPEN_VERSION == 1
isc_result_t
dlz_lookup(const char *zone, const char *name,
	   void *dbdata, dns_sdlzlookup_t *lookup)
#else
isc_result_t
dlz_lookup(const char *zone, const char *name,
	   void *dbdata, dns_sdlzlookup_t *lookup,
	   dns_clientinfomethods_t *methods,
	   dns_clientinfo_t *clientinfo)
#endif
{
	isc_result_t result;
	config_data_t *cd = (config_data_t *) dbdata;
	char *querystring = NULL;
	const char *p;
	char *namebuf;
	nrr_t *nrec;
	bool origin = true;

#if DLZ_DLOPEN_VERSION >= 2
	UNUSED(methods);
	UNUSED(clientinfo);
#endif

	p = shortest_match(cd->zone_pattern, zone);
	if (p == NULL)
		return (ISC_R_NOTFOUND);

	DE_CONST(name, cd->record);
	DE_CONST(p, cd->zone);

	if ((p != zone) && (strcmp(name, "@") == 0 || strcmp(name, zone) == 0))
	{
		size_t len = p - zone;
		namebuf = malloc(len);
		if (namebuf == NULL)
			return (ISC_R_NOMEMORY);
		strncpy(namebuf, zone, len - 1);
		namebuf[len - 1] = '\0';
		cd->record = namebuf;
		origin = false;
	} else if (p == zone)
		cd->record = "@";

	/* Write info message to log */
	cd->log(ISC_LOG_DEBUG(1),
		"dlz_wildcard_dynamic: lookup for '%s' in '%s': "
		"trying '%s' in '%s'",
		name, zone, cd->record, cd->zone);

	result = ISC_R_NOTFOUND;
	nrec = DLZ_LIST_HEAD(cd->rrs_list);
	while (nrec != NULL) {
		nrr_t *next = DLZ_LIST_NEXT(nrec, link);
		if (strcmp(cd->record, nrec->name) == 0) {
			/* We handle authority data in dlz_authority() */
			if (strcmp(nrec->type, "SOA") == 0 ||
			    strcmp(nrec->type, "NS") == 0)
			{
				nrec = next;
				continue;
			}

			querystring = build_querystring(nrec->data);
			if (querystring == NULL) {
				result = ISC_R_NOMEMORY;
				goto done;
			}

			result = cd->putrr(lookup, nrec->type,
					   nrec->ttl, querystring);
			if (result != ISC_R_SUCCESS)
				goto done;

			result = ISC_R_SUCCESS;

			free(querystring);
			querystring = NULL;
		}
		nrec = next;
	}

done:
	cd->zone = NULL;
	cd->record = NULL;

	if (querystring != NULL)
		free(querystring);

	return (result);
}

isc_result_t
dlz_authority(const char *zone, void *dbdata, dns_sdlzlookup_t *lookup) {
	isc_result_t result;
	config_data_t *cd = (config_data_t *) dbdata;
	char *querystring = NULL;
	nrr_t *nrec;
	const char *p, *name = "@";

	p = shortest_match(cd->zone_pattern, zone);
	if (p == NULL)
		return (ISC_R_NOTFOUND);

	DE_CONST(p, cd->zone);

	/* Write info message to log */
	cd->log(ISC_LOG_DEBUG(1),
		"dlz_wildcard_dynamic: authority for '%s'", zone);

	result = ISC_R_NOTFOUND;
	nrec = DLZ_LIST_HEAD(cd->rrs_list);
	while (nrec != NULL) {
		bool origin;
		if (strcmp("@", nrec->name) == 0) {
			isc_result_t presult;

			querystring = build_querystring(nrec->data);
			if (querystring == NULL) {
				result = ISC_R_NOMEMORY;
				goto done;
			}

			presult = cd->putrr(lookup, nrec->type,
					   nrec->ttl, querystring);
			if (presult != ISC_R_SUCCESS) {
				result = presult;
				goto done;
			}

			result = ISC_R_SUCCESS;

			free(querystring);
			querystring = NULL;
		}
		nrec = DLZ_LIST_NEXT(nrec, link);
	}

done:
	cd->zone = NULL;

	if (querystring != NULL)
		free(querystring);

	return (result);
}

static void
destroy_rrlist(config_data_t *cd) {
	nrr_t *trec, *nrec;

	nrec = DLZ_LIST_HEAD(cd->rrs_list);

	while (nrec != NULL) {
		trec = nrec;

		destroy_querylist(&trec->data);

		if (trec->name != NULL)
			free(trec->name);
		if (trec->type != NULL)
			free(trec->type);
		trec->name = trec->type = NULL;

		/* Get the next record, before we destroy this one. */
		nrec = DLZ_LIST_NEXT(nrec, link);

		free(trec);
	}
}

isc_result_t
dlz_create(const char *dlzname, unsigned int argc, char *argv[],
	   void **dbdata, ...)
{
	config_data_t *cd;
	char *endp;
	int i, def_ttl;
	nrr_t *trec = NULL;
	isc_result_t result;
	const char *helper_name;
	va_list ap;

	if (argc < 8 || argc % 4 != 0)
		return (ISC_R_FAILURE);

	cd = calloc(1, sizeof(config_data_t));
	if (cd == NULL)
		return (ISC_R_NOMEMORY);
	memset(cd, 0, sizeof(config_data_t));

	/* Fill in the helper functions */
	va_start(ap, dbdata);
	while ((helper_name = va_arg(ap, const char*)) != NULL)
		b9_add_helper(cd, helper_name, va_arg(ap, void*));
	va_end(ap);

	/*
	 * Write info message to log
	 */
	cd->log(ISC_LOG_INFO,
		"Loading '%s' using DLZ_wildcard driver. "
		"Zone: %s, AXFR allowed for: %s, $TTL: %s",
		dlzname, argv[1], argv[2], argv[3]);

	/* initialize the records list here to simplify cleanup */
	DLZ_LIST_INIT(cd->rrs_list);

	cd->zone_pattern = strdup(argv[1]);
	cd->axfr_pattern = strdup(argv[2]);
	if (cd->zone_pattern == NULL || cd->axfr_pattern == NULL) {
		result = ISC_R_NOMEMORY;
		goto cleanup;
	}

	def_ttl = strtol(argv[3], &endp, 10);
	if (*endp != '\0' || def_ttl < 0) {
		def_ttl = 3600;
		cd->log(ISC_LOG_ERROR, "default TTL invalid, using 3600");
	}

	for (i = 4; i < argc; i += 4) {
		result = ISC_R_NOMEMORY;

		trec = malloc(sizeof(nrr_t));
		if (trec == NULL)
			goto full_cleanup;

		memset(trec, 0, sizeof(nrr_t));

		/* Initialize the record link */
		DLZ_LINK_INIT(trec, link);
		/* Append the record to the list */
		DLZ_LIST_APPEND(cd->rrs_list, trec, link);

		trec->name = strdup(argv[i]);
		if (trec->name == NULL)
			goto full_cleanup;

		trec->type = strdup(argv[i + 2]);
		if (trec->type == NULL)
			goto full_cleanup;

		trec->ttl = strtol(argv[i + 1], &endp, 10);
		if (argv[i + 1][0] == '\0' || *endp != '\0' || trec->ttl < 0)
			trec->ttl = def_ttl;

		result = build_querylist(argv[i + 3], &cd->zone,
					 &cd->record, &cd->client,
					 &trec->data, 0, cd->log);
		/* If unsuccessful, log err msg and cleanup */
		if (result != ISC_R_SUCCESS) {
			cd->log(ISC_LOG_ERROR,
				"Could not build RR data list at argv[%d]",
				i + 3);
			goto full_cleanup;
		}
	}

	*dbdata = cd;

	return (ISC_R_SUCCESS);

full_cleanup:
	destroy_rrlist(cd);

cleanup:
	if (cd->zone_pattern != NULL)
		free(cd->zone_pattern);
	if (cd->axfr_pattern != NULL)
		free(cd->axfr_pattern);
	free(cd);

	return (result);
}

void
dlz_destroy(void *dbdata) {
	config_data_t *cd = (config_data_t *) dbdata;

	/*
	 * Write debugging message to log
	 */
	cd->log(ISC_LOG_DEBUG(2), "Unloading DLZ_wildcard driver.");

	destroy_rrlist(cd);

	free(cd->zone_pattern);
	free(cd->axfr_pattern);
	free(cd);
}


/*
 * Return the version of the API
 */
int
dlz_version(unsigned int *flags) {
	UNUSED(flags);
	/* XXX: ok to set DNS_SDLZFLAG_THREADSAFE here? */
	return (DLZ_DLOPEN_VERSION);
}

/*
 * Register a helper function from the bind9 dlz_dlopen driver
 */
static void
b9_add_helper(struct config_data *cd, const char *helper_name, void *ptr) {
	if (strcmp(helper_name, "log") == 0)
		cd->log = (log_t *)ptr;
	if (strcmp(helper_name, "putrr") == 0)
		cd->putrr = (dns_sdlz_putrr_t *)ptr;
	if (strcmp(helper_name, "putnamedrr") == 0)
		cd->putnamedrr = (dns_sdlz_putnamedrr_t *)ptr;
	if (strcmp(helper_name, "writeable_zone") == 0)
		cd->writeable_zone = (dns_dlz_writeablezone_t *)ptr;
}

static const char *
shortest_match(const char *pattern, const char *string) {
	const char *p = string;
	if (pattern == NULL || p == NULL || *p == '\0')
		return (NULL);

	p += strlen(p);
	while (p-- > string) {
		if (*p == '.') {
			if (fnmatch(pattern, p + 1, FNM_CASEFOLD) == 0)
				return (p + 1);
		}
	}
	if (fnmatch(pattern, string, FNM_CASEFOLD) == 0)
		return (string);

	return (NULL);
}

/*
 * The helper functions stolen from the FreeBSD kernel (sys/libkern/fnmatch.c).
 *
 * Why don't we use fnmatch(3) from libc? Because it is not thread-safe, and
 * it is not thread-safe because it supports multibyte characters. But here,
 * in BIND, we want to be thread-safe and don't need multibyte - DNS names are
 * always ASCII.
 */
#define	EOS	'\0'

#define RANGE_MATCH     1
#define RANGE_NOMATCH   0
#define RANGE_ERROR     (-1)

static int
fnmatch(const char *pattern, const char *string, int flags) {
	const char *stringstart;
	char *newp;
	char c, test;

	for (stringstart = string;;)
		switch (c = *pattern++) {
		case EOS:
			if ((flags & FNM_LEADING_DIR) && *string == '/')
				return (0);
			return (*string == EOS ? 0 : FNM_NOMATCH);
		case '?':
			if (*string == EOS)
				return (FNM_NOMATCH);
			if (*string == '/' && (flags & FNM_PATHNAME))
				return (FNM_NOMATCH);
			if (*string == '.' && (flags & FNM_PERIOD) &&
			    (string == stringstart ||
			    ((flags & FNM_PATHNAME) && *(string - 1) == '/')))
				return (FNM_NOMATCH);
			++string;
			break;
		case '*':
			c = *pattern;
			/* Collapse multiple stars. */
			while (c == '*')
				c = *++pattern;

			if (*string == '.' && (flags & FNM_PERIOD) &&
			    (string == stringstart ||
			    ((flags & FNM_PATHNAME) && *(string - 1) == '/')))
				return (FNM_NOMATCH);

			/* Optimize for pattern with * at end or before /. */
			if (c == EOS)
				if (flags & FNM_PATHNAME)
					return ((flags & FNM_LEADING_DIR) ||
					    index(string, '/') == NULL ?
					    0 : FNM_NOMATCH);
				else
					return (0);
			else if (c == '/' && flags & FNM_PATHNAME) {
				if ((string = index(string, '/')) == NULL)
					return (FNM_NOMATCH);
				break;
			}

			/* General case, use recursion. */
			while ((test = *string) != EOS) {
				if (!fnmatch(pattern, string,
					     flags & ~FNM_PERIOD))
					return (0);
				if (test == '/' && flags & FNM_PATHNAME)
					break;
				++string;
			}
			return (FNM_NOMATCH);
		case '[':
			if (*string == EOS)
				return (FNM_NOMATCH);
			if (*string == '/' && (flags & FNM_PATHNAME))
				return (FNM_NOMATCH);
			if (*string == '.' && (flags & FNM_PERIOD) &&
			    (string == stringstart ||
			    ((flags & FNM_PATHNAME) && *(string - 1) == '/')))
				return (FNM_NOMATCH);

			switch (rangematch(pattern, *string, flags, &newp)) {
			case RANGE_ERROR:
				goto norm;
			case RANGE_MATCH:
				pattern = newp;
				break;
			case RANGE_NOMATCH:
				return (FNM_NOMATCH);
			}
			++string;
			break;
		case '\\':
			if (!(flags & FNM_NOESCAPE)) {
				if ((c = *pattern++) == EOS) {
					c = '\\';
					--pattern;
				}
			}
			/* FALLTHROUGH */
		default:
		norm:
			if (c == *string)
				;
			else if ((flags & FNM_CASEFOLD) &&
				 (tolower((unsigned char)c) ==
				  tolower((unsigned char)*string)))
				;
			else
				return (FNM_NOMATCH);
			string++;
			break;
		}
	/* NOTREACHED */
}

static int
rangematch(const char *pattern, char test, int flags, char **newp) {
	int negate, ok;
	char c, c2;

	/*
	 * A bracket expression starting with an unquoted circumflex
	 * character produces unspecified results (IEEE 1003.2-1992,
	 * 3.13.2).  This implementation treats it like '!', for
	 * consistency with the regular expression syntax.
	 * J.T. Conklin (conklin@ngai.kaleida.com)
	 */
	if ( (negate = (*pattern == '!' || *pattern == '^')) )
		++pattern;

	if (flags & FNM_CASEFOLD)
		test = tolower((unsigned char)test);

	/*
	 * A right bracket shall lose its special meaning and represent
	 * itself in a bracket expression if it occurs first in the list.
	 * -- POSIX.2 2.8.3.2
	 */
	ok = 0;
	c = *pattern++;
	do {
		if (c == '\\' && !(flags & FNM_NOESCAPE))
			c = *pattern++;
		if (c == EOS)
			return (RANGE_ERROR);

		if (c == '/' && (flags & FNM_PATHNAME))
			return (RANGE_NOMATCH);

		if (flags & FNM_CASEFOLD)
			c = tolower((unsigned char)c);

		if (*pattern == '-'
		    && (c2 = *(pattern+1)) != EOS && c2 != ']') {
			pattern += 2;
			if (c2 == '\\' && !(flags & FNM_NOESCAPE))
				c2 = *pattern++;
			if (c2 == EOS)
				return (RANGE_ERROR);

			if (flags & FNM_CASEFOLD)
				c2 = tolower((unsigned char)c2);

			if (c <= test && test <= c2)
				ok = 1;
		} else if (c == test)
			ok = 1;
	} while ((c = *pattern++) != ']');

	*newp = (char *)(uintptr_t)pattern;
	return (ok == negate ? RANGE_NOMATCH : RANGE_MATCH);
}
