/*
 * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
 *
 * SPDX-License-Identifier: MPL-2.0
 *
 * 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/.
 *
 * See the COPYRIGHT file distributed with this work for additional
 * information regarding copyright ownership.
 */

/*! \file resconf.c */

/**
 * Module for parsing resolv.conf files (largely derived from lwconfig.c).
 *
 *    irs_resconf_load() opens the file filename and parses it to initialize
 *    the configuration structure.
 *
 * \section lwconfig_return Return Values
 *
 *    irs_resconf_load() returns #IRS_R_SUCCESS if it successfully read and
 *    parsed filename. It returns a non-0 error code if filename could not be
 *    opened or contained incorrect resolver statements.
 *
 * \section lwconfig_see See Also
 *
 *    stdio(3), \link resolver resolver \endlink
 *
 * \section files Files
 *
 *    /etc/resolv.conf
 */

#include <ctype.h>
#include <errno.h>
#include <inttypes.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>

#include <isc/magic.h>
#include <isc/mem.h>
#include <isc/netaddr.h>
#include <isc/sockaddr.h>
#include <isc/util.h>

#include <irs/resconf.h>

#define IRS_RESCONF_MAGIC    ISC_MAGIC('R', 'E', 'S', 'c')
#define IRS_RESCONF_VALID(c) ISC_MAGIC_VALID(c, IRS_RESCONF_MAGIC)

/*!
 * protocol constants
 */

#if !defined(NS_INADDRSZ)
#define NS_INADDRSZ 4
#endif /* if !defined(NS_INADDRSZ) */

#if !defined(NS_IN6ADDRSZ)
#define NS_IN6ADDRSZ 16
#endif /* if !defined(NS_IN6ADDRSZ) */

/*!
 * resolv.conf parameters
 */

#define RESCONFMAXNAMESERVERS 3U   /*%< max 3 "nameserver" entries */
#define RESCONFMAXSEARCH      8U   /*%< max 8 domains in "search" entry */
#define RESCONFMAXLINELEN     256U /*%< max size of a line */
#define RESCONFMAXSORTLIST    10U  /*%< max 10 */

#define CHECK(op)                            \
	do {                                 \
		result = (op);               \
		if (result != ISC_R_SUCCESS) \
			goto cleanup;        \
	} while (0)

/*!
 * configuration data structure
 */

struct irs_resconf {
	/*
	 * The configuration data is a thread-specific object, and does not
	 * need to be locked.
	 */
	unsigned int magic;
	isc_mem_t *mctx;

	isc_sockaddrlist_t nameservers;
	unsigned int numns; /*%< number of configured servers
			     * */

	char *domainname;
	char *search[RESCONFMAXSEARCH];
	uint8_t searchnxt; /*%< index for next free slot
			    * */

	irs_resconf_searchlist_t searchlist;

	struct {
		isc_netaddr_t addr;
		/*% mask has a non-zero 'family' if set */
		isc_netaddr_t mask;
	} sortlist[RESCONFMAXSORTLIST];
	uint8_t sortlistnxt;

	/*%< non-zero if 'options debug' set */
	uint8_t resdebug;
	/*%< set to n in 'options ndots:n' */
	uint8_t ndots;
	/*%< set to n in 'options attempts:n' */
	uint8_t attempts;
	/*%< set to n in 'options timeout:n' */
	uint8_t timeout;
};

static isc_result_t
resconf_parsenameserver(irs_resconf_t *conf, FILE *fp);
static isc_result_t
resconf_parsedomain(irs_resconf_t *conf, FILE *fp);
static isc_result_t
resconf_parsesearch(irs_resconf_t *conf, FILE *fp);
static isc_result_t
resconf_parsesortlist(irs_resconf_t *conf, FILE *fp);
static isc_result_t
resconf_parseoption(irs_resconf_t *ctx, FILE *fp);

/*!
 * Eat characters from FP until EOL or EOF. Returns EOF or '\n'
 */
static int
eatline(FILE *fp) {
	int ch;

	ch = fgetc(fp);
	while (ch != '\n' && ch != EOF) {
		ch = fgetc(fp);
	}

	return (ch);
}

/*!
 * Eats white space up to next newline or non-whitespace character (of
 * EOF). Returns the last character read. Comments are considered white
 * space.
 */
static int
eatwhite(FILE *fp) {
	int ch;

	ch = fgetc(fp);
	while (ch != '\n' && ch != EOF && isspace((unsigned char)ch)) {
		ch = fgetc(fp);
	}

	if (ch == ';' || ch == '#') {
		ch = eatline(fp);
	}

	return (ch);
}

/*!
 * Skip over any leading whitespace and then read in the next sequence of
 * non-whitespace characters. In this context newline is not considered
 * whitespace. Returns EOF on end-of-file, or the character
 * that caused the reading to stop.
 */
static int
getword(FILE *fp, char *buffer, size_t size) {
	char *p = NULL;
	int ch;

	REQUIRE(buffer != NULL);
	REQUIRE(size > 0U);

	p = buffer;
	*p = '\0';

	ch = eatwhite(fp);

	if (ch == EOF) {
		return (EOF);
	}

	do {
		*p = '\0';

		if (ch == EOF || isspace((unsigned char)ch)) {
			break;
		} else if ((size_t)(p - buffer) == size - 1) {
			return (EOF); /* Not enough space. */
		}

		*p++ = (char)ch;
		ch = fgetc(fp);
	} while (1);

	return (ch);
}

static isc_result_t
add_server(isc_mem_t *mctx, const char *address_str,
	   isc_sockaddrlist_t *nameservers) {
	int error;
	isc_sockaddr_t *address = NULL;
	struct addrinfo hints, *res;
	isc_result_t result = ISC_R_SUCCESS;

	res = NULL;
	memset(&hints, 0, sizeof(hints));
	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_DGRAM;
	hints.ai_protocol = IPPROTO_UDP;
	hints.ai_flags = AI_NUMERICHOST;
	error = getaddrinfo(address_str, "53", &hints, &res);
	if (error != 0) {
		return (ISC_R_BADADDRESSFORM);
	}

	/* XXX: special case: treat all-0 IPv4 address as loopback */
	if (res->ai_family == AF_INET) {
		struct in_addr *v4;
		unsigned char zeroaddress[] = { 0, 0, 0, 0 };
		unsigned char loopaddress[] = { 127, 0, 0, 1 };

		v4 = &((struct sockaddr_in *)res->ai_addr)->sin_addr;
		if (memcmp(v4, zeroaddress, 4) == 0) {
			memmove(v4, loopaddress, 4);
		}
	}

	address = isc_mem_get(mctx, sizeof(*address));
	if (res->ai_addrlen > sizeof(address->type)) {
		isc_mem_put(mctx, address, sizeof(*address));
		result = ISC_R_RANGE;
		goto cleanup;
	}
	address->length = (unsigned int)res->ai_addrlen;
	memmove(&address->type.ss, res->ai_addr, res->ai_addrlen);
	ISC_LINK_INIT(address, link);
	ISC_LIST_APPEND(*nameservers, address, link);

cleanup:
	freeaddrinfo(res);

	return (result);
}

static isc_result_t
create_addr(const char *buffer, isc_netaddr_t *addr, int convert_zero) {
	struct in_addr v4;
	struct in6_addr v6;

	if (inet_pton(AF_INET, buffer, &v4) == 1) {
		if (convert_zero) {
			unsigned char zeroaddress[] = { 0, 0, 0, 0 };
			unsigned char loopaddress[] = { 127, 0, 0, 1 };
			if (memcmp(&v4, zeroaddress, 4) == 0) {
				memmove(&v4, loopaddress, 4);
			}
		}
		addr->family = AF_INET;
		memmove(&addr->type.in, &v4, NS_INADDRSZ);
		addr->zone = 0;
	} else if (inet_pton(AF_INET6, buffer, &v6) == 1) {
		addr->family = AF_INET6;
		memmove(&addr->type.in6, &v6, NS_IN6ADDRSZ);
		addr->zone = 0;
	} else {
		return (ISC_R_BADADDRESSFORM); /* Unrecognised format. */
	}

	return (ISC_R_SUCCESS);
}

static isc_result_t
resconf_parsenameserver(irs_resconf_t *conf, FILE *fp) {
	char word[RESCONFMAXLINELEN];
	int cp;
	isc_result_t result;

	if (conf->numns == RESCONFMAXNAMESERVERS) {
		return (ISC_R_SUCCESS);
	}

	cp = getword(fp, word, sizeof(word));
	if (strlen(word) == 0U) {
		return (ISC_R_UNEXPECTEDEND); /* Nothing on line. */
	} else if (cp == ' ' || cp == '\t') {
		cp = eatwhite(fp);
	}

	if (cp != EOF && cp != '\n') {
		return (ISC_R_UNEXPECTEDTOKEN); /* Extra junk on line. */
	}

	result = add_server(conf->mctx, word, &conf->nameservers);
	if (result != ISC_R_SUCCESS) {
		return (result);
	}
	conf->numns++;

	return (ISC_R_SUCCESS);
}

static isc_result_t
resconf_parsedomain(irs_resconf_t *conf, FILE *fp) {
	char word[RESCONFMAXLINELEN];
	int res;
	unsigned int i;

	res = getword(fp, word, sizeof(word));
	if (strlen(word) == 0U) {
		return (ISC_R_UNEXPECTEDEND); /* Nothing else on line. */
	} else if (res == ' ' || res == '\t') {
		res = eatwhite(fp);
	}

	if (res != EOF && res != '\n') {
		return (ISC_R_UNEXPECTEDTOKEN); /* Extra junk on line. */
	}

	if (conf->domainname != NULL) {
		isc_mem_free(conf->mctx, conf->domainname);
	}

	/*
	 * Search and domain are mutually exclusive.
	 */
	for (i = 0; i < RESCONFMAXSEARCH; i++) {
		if (conf->search[i] != NULL) {
			isc_mem_free(conf->mctx, conf->search[i]);
			conf->search[i] = NULL;
		}
	}
	conf->searchnxt = 0;

	conf->domainname = isc_mem_strdup(conf->mctx, word);

	return (ISC_R_SUCCESS);
}

static isc_result_t
resconf_parsesearch(irs_resconf_t *conf, FILE *fp) {
	int delim;
	unsigned int idx;
	char word[RESCONFMAXLINELEN];

	if (conf->domainname != NULL) {
		/*
		 * Search and domain are mutually exclusive.
		 */
		isc_mem_free(conf->mctx, conf->domainname);
		conf->domainname = NULL;
	}

	/*
	 * Remove any previous search definitions.
	 */
	for (idx = 0; idx < RESCONFMAXSEARCH; idx++) {
		if (conf->search[idx] != NULL) {
			isc_mem_free(conf->mctx, conf->search[idx]);
			conf->search[idx] = NULL;
		}
	}
	conf->searchnxt = 0;

	delim = getword(fp, word, sizeof(word));
	if (strlen(word) == 0U) {
		return (ISC_R_UNEXPECTEDEND); /* Nothing else on line. */
	}

	idx = 0;
	while (strlen(word) > 0U) {
		if (conf->searchnxt == RESCONFMAXSEARCH) {
			goto ignore; /* Too many domains. */
		}

		INSIST(idx < sizeof(conf->search) / sizeof(conf->search[0]));
		conf->search[idx] = isc_mem_strdup(conf->mctx, word);
		idx++;
		conf->searchnxt++;

	ignore:
		if (delim == EOF || delim == '\n') {
			break;
		} else {
			delim = getword(fp, word, sizeof(word));
		}
	}

	return (ISC_R_SUCCESS);
}

static isc_result_t
resconf_parsesortlist(irs_resconf_t *conf, FILE *fp) {
	int delim, res;
	unsigned int idx;
	char word[RESCONFMAXLINELEN];
	char *p;

	delim = getword(fp, word, sizeof(word));
	if (strlen(word) == 0U) {
		return (ISC_R_UNEXPECTEDEND); /* Empty line after keyword. */
	}

	while (strlen(word) > 0U) {
		if (conf->sortlistnxt == RESCONFMAXSORTLIST) {
			return (ISC_R_QUOTA); /* Too many values. */
		}

		p = strchr(word, '/');
		if (p != NULL) {
			*p++ = '\0';
		}

		idx = conf->sortlistnxt;
		INSIST(idx <
		       sizeof(conf->sortlist) / sizeof(conf->sortlist[0]));
		res = create_addr(word, &conf->sortlist[idx].addr, 1);
		if (res != ISC_R_SUCCESS) {
			return (res);
		}

		if (p != NULL) {
			res = create_addr(p, &conf->sortlist[idx].mask, 0);
			if (res != ISC_R_SUCCESS) {
				return (res);
			}
		} else {
			/*
			 * Make up a mask. (XXX: is this correct?)
			 */
			conf->sortlist[idx].mask = conf->sortlist[idx].addr;
			memset(&conf->sortlist[idx].mask.type, 0xff,
			       sizeof(conf->sortlist[idx].mask.type));
		}

		conf->sortlistnxt++;

		if (delim == EOF || delim == '\n') {
			break;
		} else {
			delim = getword(fp, word, sizeof(word));
		}
	}

	return (ISC_R_SUCCESS);
}

static isc_result_t
resconf_optionnumber(const char *word, uint8_t *number) {
	char *p;
	long n;

	n = strtol(word, &p, 10);
	if (*p != '\0') { /* Bad string. */
		return (ISC_R_UNEXPECTEDTOKEN);
	}
	if (n < 0 || n > 0xff) { /* Out of range. */
		return (ISC_R_RANGE);
	}
	*number = n;
	return (ISC_R_SUCCESS);
}

static isc_result_t
resconf_parseoption(irs_resconf_t *conf, FILE *fp) {
	int delim;
	isc_result_t result = ISC_R_SUCCESS;
	char word[RESCONFMAXLINELEN];

	delim = getword(fp, word, sizeof(word));
	if (strlen(word) == 0U) {
		return (ISC_R_UNEXPECTEDEND); /* Empty line after keyword. */
	}

	while (strlen(word) > 0U) {
		if (strcmp("debug", word) == 0) {
			conf->resdebug = 1;
		} else if (strncmp("ndots:", word, 6) == 0) {
			CHECK(resconf_optionnumber(word + 6, &conf->ndots));
		} else if (strncmp("attempts:", word, 9) == 0) {
			CHECK(resconf_optionnumber(word + 9, &conf->attempts));
		} else if (strncmp("timeout:", word, 8) == 0) {
			CHECK(resconf_optionnumber(word + 8, &conf->timeout));
		}

		if (delim == EOF || delim == '\n') {
			break;
		} else {
			delim = getword(fp, word, sizeof(word));
		}
	}

cleanup:
	return (result);
}

static isc_result_t
add_search(irs_resconf_t *conf, char *domain) {
	irs_resconf_search_t *entry;

	entry = isc_mem_get(conf->mctx, sizeof(*entry));

	entry->domain = domain;
	ISC_LINK_INIT(entry, link);
	ISC_LIST_APPEND(conf->searchlist, entry, link);

	return (ISC_R_SUCCESS);
}

/*% parses a file and fills in the data structure. */
isc_result_t
irs_resconf_load(isc_mem_t *mctx, const char *filename, irs_resconf_t **confp) {
	FILE *fp = NULL;
	char word[256];
	isc_result_t rval, ret = ISC_R_SUCCESS;
	irs_resconf_t *conf;
	unsigned int i;
	int stopchar;

	REQUIRE(mctx != NULL);
	REQUIRE(filename != NULL);
	REQUIRE(strlen(filename) > 0U);
	REQUIRE(confp != NULL && *confp == NULL);

	conf = isc_mem_get(mctx, sizeof(*conf));

	conf->mctx = mctx;
	ISC_LIST_INIT(conf->nameservers);
	ISC_LIST_INIT(conf->searchlist);
	conf->numns = 0;
	conf->domainname = NULL;
	conf->searchnxt = 0;
	conf->sortlistnxt = 0;
	conf->resdebug = 0;
	conf->ndots = 1;
	conf->attempts = 3;
	conf->timeout = 0;
	for (i = 0; i < RESCONFMAXSEARCH; i++) {
		conf->search[i] = NULL;
	}

	errno = 0;
	if ((fp = fopen(filename, "r")) != NULL) {
		do {
			stopchar = getword(fp, word, sizeof(word));
			if (stopchar == EOF) {
				rval = ISC_R_SUCCESS;
				POST(rval);
				break;
			}

			if (strlen(word) == 0U) {
				rval = ISC_R_SUCCESS;
			} else if (strcmp(word, "nameserver") == 0) {
				rval = resconf_parsenameserver(conf, fp);
			} else if (strcmp(word, "domain") == 0) {
				rval = resconf_parsedomain(conf, fp);
			} else if (strcmp(word, "search") == 0) {
				rval = resconf_parsesearch(conf, fp);
			} else if (strcmp(word, "sortlist") == 0) {
				rval = resconf_parsesortlist(conf, fp);
			} else if (strcmp(word, "options") == 0) {
				rval = resconf_parseoption(conf, fp);
			} else {
				/* unrecognised word. Ignore entire line */
				rval = ISC_R_SUCCESS;
				stopchar = eatline(fp);
				if (stopchar == EOF) {
					break;
				}
			}
			if (ret == ISC_R_SUCCESS && rval != ISC_R_SUCCESS) {
				ret = rval;
			}
		} while (1);

		fclose(fp);
	} else {
		switch (errno) {
		case ENOENT:
			break;
		default:
			isc_mem_put(mctx, conf, sizeof(*conf));
			return (ISC_R_INVALIDFILE);
		}
	}

	if (ret != ISC_R_SUCCESS) {
		goto error;
	}

	/*
	 * Construct unified search list from domain or configured
	 * search list
	 */
	if (conf->domainname != NULL) {
		ret = add_search(conf, conf->domainname);
	} else if (conf->searchnxt > 0) {
		for (i = 0; i < conf->searchnxt; i++) {
			ret = add_search(conf, conf->search[i]);
			if (ret != ISC_R_SUCCESS) {
				break;
			}
		}
	}

	/* If we don't find a nameserver fall back to localhost */
	if (conf->numns == 0U) {
		INSIST(ISC_LIST_EMPTY(conf->nameservers));

		/* XXX: should we catch errors? */
		(void)add_server(conf->mctx, "::1", &conf->nameservers);
		(void)add_server(conf->mctx, "127.0.0.1", &conf->nameservers);
	}

error:
	conf->magic = IRS_RESCONF_MAGIC;

	if (ret != ISC_R_SUCCESS) {
		irs_resconf_destroy(&conf);
	} else {
		if (fp == NULL) {
			ret = ISC_R_FILENOTFOUND;
		}
		*confp = conf;
	}

	return (ret);
}

void
irs_resconf_destroy(irs_resconf_t **confp) {
	irs_resconf_t *conf;
	isc_sockaddr_t *address;
	irs_resconf_search_t *searchentry;
	unsigned int i;

	REQUIRE(confp != NULL);
	conf = *confp;
	*confp = NULL;
	REQUIRE(IRS_RESCONF_VALID(conf));

	while ((searchentry = ISC_LIST_HEAD(conf->searchlist)) != NULL) {
		ISC_LIST_UNLINK(conf->searchlist, searchentry, link);
		isc_mem_put(conf->mctx, searchentry, sizeof(*searchentry));
	}

	while ((address = ISC_LIST_HEAD(conf->nameservers)) != NULL) {
		ISC_LIST_UNLINK(conf->nameservers, address, link);
		isc_mem_put(conf->mctx, address, sizeof(*address));
	}

	if (conf->domainname != NULL) {
		isc_mem_free(conf->mctx, conf->domainname);
	}

	for (i = 0; i < RESCONFMAXSEARCH; i++) {
		if (conf->search[i] != NULL) {
			isc_mem_free(conf->mctx, conf->search[i]);
		}
	}

	isc_mem_put(conf->mctx, conf, sizeof(*conf));
}

isc_sockaddrlist_t *
irs_resconf_getnameservers(irs_resconf_t *conf) {
	REQUIRE(IRS_RESCONF_VALID(conf));

	return (&conf->nameservers);
}

irs_resconf_searchlist_t *
irs_resconf_getsearchlist(irs_resconf_t *conf) {
	REQUIRE(IRS_RESCONF_VALID(conf));

	return (&conf->searchlist);
}

unsigned int
irs_resconf_getndots(irs_resconf_t *conf) {
	REQUIRE(IRS_RESCONF_VALID(conf));

	return ((unsigned int)conf->ndots);
}

unsigned int
irs_resconf_getattempts(irs_resconf_t *conf) {
	REQUIRE(IRS_RESCONF_VALID(conf));

	return ((unsigned int)conf->attempts);
}

unsigned int
irs_resconf_gettimeout(irs_resconf_t *conf) {
	REQUIRE(IRS_RESCONF_VALID(conf));

	return ((unsigned int)conf->timeout);
}
