/*
 * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
 *
 * SPDX-License-Identifier: MPL-2.0 and 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 https://mozilla.org/MPL/2.0/.
 */

/*
 * Copyright (C) Stichting NLnet, Netherlands, stichting@nlnet.nl.
 * Copyright (C) Vadim Goncharov, Russia, vadim_nuclight@mail.ru.
 *
 * 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 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.
 */

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

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

#include <dlz_dbi.h>
#include <dlz_list.h>
#include <dlz_minimal.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  /* if DLZ_DLOPEN_VERSION < 3 */
isc_result_t
dlz_findzonedb(void *dbdata, const char *name, dns_clientinfomethods_t *methods,
	       dns_clientinfo_t *clientinfo)
#endif /* if DLZ_DLOPEN_VERSION < 3 */
{
	config_data_t *cd = (config_data_t *)dbdata;
	const char *p;

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

	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  /* if DLZ_DLOPEN_VERSION == 1 */
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 /* if DLZ_DLOPEN_VERSION == 1 */
{
	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 /* if DLZ_DLOPEN_VERSION >= 2 */

	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;
		}
	}
	UNREACHABLE();
}

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);
}
