/*
 *   This program is 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 2 of the License, 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
 */

/**
 * $Id: dddc3bf4bf939a64939f0c5225caf79b0f5af0fe $
 * @file rlm_unbound.c
 * @brief DNS services via libunbound.
 *
 * @copyright 2013 The FreeRADIUS server project
 * @copyright 2013 Brian S. Julin <bjulin@clarku.edu>
 */
RCSID("$Id: dddc3bf4bf939a64939f0c5225caf79b0f5af0fe $")

#include <freeradius-devel/radiusd.h>
#include <freeradius-devel/modules.h>
#include <freeradius-devel/log.h>
#include <fcntl.h>

#ifdef HAVE_WDOCUMENTATION
DIAG_OFF(documentation)
#endif
#include <unbound.h>
#ifdef HAVE_WDOCUMENTATION
DIAG_ON(documentation)
#endif

typedef struct rlm_unbound_t {
	struct ub_ctx	*ub;   /* This must come first.  Do not move */
	fr_event_list_t	*el; /* This must come second.  Do not move. */

	char const	*name;
	char const	*xlat_a_name;
	char const	*xlat_aaaa_name;
	char const	*xlat_ptr_name;

	uint32_t	timeout;

	char const	*filename;
	char const	*resolvconf;
	char const	*hosts;

	int		log_fd;
	FILE		*log_stream;

	int		log_pipe[2];
	FILE		*log_pipe_stream[2];
	bool		log_pipe_in_use;
} rlm_unbound_t;

/*
 *	A mapping of configuration file names to internal variables.
 */
static const CONF_PARSER module_config[] = {
	{ "filename", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT | PW_TYPE_REQUIRED, rlm_unbound_t, filename), "${modconfdir}/unbound/default.conf"  },
	{ "resolvconf", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, rlm_unbound_t, resolvconf), NULL },
	{ "hosts", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, rlm_unbound_t, hosts), NULL },
	{ "timeout", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_unbound_t, timeout), "3000" },
	CONF_PARSER_TERMINATOR
};

/*
 *	Callback sent to libunbound for xlat functions.  Simply links the
 *	new ub_result via a pointer that has been allocated from the heap.
 *	This pointer has been pre-initialized to a magic value.
 */
static void link_ubres(void* my_arg, int err, struct ub_result* result)
{
	struct ub_result **ubres = (struct ub_result **)my_arg;

	/*
	 *	Note that while result will be NULL on error, we are explicit
	 *	here because that is actually a behavior that is suboptimal
	 *	and only documented in the examples.  It could change.
	 */
	if (err) {
		ERROR("rlm_unbound: %s", ub_strerror(err));
		*ubres = NULL;
	} else {
		*ubres = result;
	}
}

/*
 *	Convert labels as found in a DNS result to a NULL terminated string.
 *
 *	Result is written to memory pointed to by "out" but no result will
 *	be written unless it and its terminating NULL character fit in "left"
 *	bytes.  Returns the number of bytes written excluding the terminating
 *	NULL, or -1 if nothing was written because it would not fit or due
 *	to a violation in the labels format.
 */
static int rrlabels_tostr(char *out, char *rr, size_t left)
{
	int offset = 0;

	/*
	 * TODO: verify that unbound results (will) always use this label
	 * format, and review the specs on this label format for nuances.
	 */

	if (!left) {
		return -1;
	}
	if (left > 253) {
		left = 253; /* DNS length limit */
	}
	/* As a whole this should be "NULL terminated" by the 0-length label */
	if (strnlen(rr, left) > left - 1) {
		return -1;
	}

	/* It will fit, but does it it look well formed? */
	while (1) {
		size_t count;

		count = *((unsigned char *)(rr + offset));
		if (!count) break;

		offset++;
		if (count > 63 || strlen(rr + offset) < count) {
			return -1;
		}
		offset += count;
	}

	/* Data is valid and fits.  Copy it. */
	offset = 0;
	while (1) {
		int count;

		count = *((unsigned char *)(rr));
		if (!count) break;

		if (offset) {
			*(out + offset) = '.';
			offset++;
		}

		rr++;
		memcpy(out + offset, rr, count);
		rr += count;
		offset += count;
	}

	*(out + offset) = '\0';
	return offset;
}

static int ub_common_wait(rlm_unbound_t *inst, REQUEST *request, char const *tag, struct ub_result **ub, int async_id)
{
	useconds_t iv, waited;

	iv = inst->timeout > 64 ? 64000 : inst->timeout * 1000;
	ub_process(inst->ub);

	for (waited = 0; (void*)*ub == (void *)inst; waited += iv, iv *= 2) {

		if (waited + iv > (useconds_t)inst->timeout * 1000) {
			usleep(inst->timeout * 1000 - waited);
			ub_process(inst->ub);
			break;
		}

		usleep(iv);

		/* Check if already handled by event loop */
		if ((void *)*ub != (void *)inst) {
			break;
		}

		/* In case we are running single threaded */
		ub_process(inst->ub);
	}

	if ((void *)*ub == (void *)inst) {
		int res;

		RDEBUG("rlm_unbound (%s): DNS took too long", tag);

		res = ub_cancel(inst->ub, async_id);
		if (res) {
			REDEBUG("rlm_unbound (%s): ub_cancel: %s",
				tag, ub_strerror(res));
		}
		return -1;
	}

	return 0;
}

static int ub_common_fail(REQUEST *request, char const *tag, struct ub_result *ub)
{
	if (ub->bogus) {
		RWDEBUG("rlm_unbound (%s): Bogus DNS response", tag);
		return -1;
	}

	if (ub->nxdomain) {
		RDEBUG("rlm_unbound (%s): NXDOMAIN", tag);
		return -1;
	}

	if (!ub->havedata) {
		RDEBUG("rlm_unbound (%s): empty result", tag);
		return -1;
	}

	return 0;
}

static ssize_t xlat_a(void *instance, REQUEST *request, char const *fmt, char *out, size_t freespace)
{
	rlm_unbound_t *inst = instance;
	struct ub_result **ubres;
	int async_id;

	/* This has to be on the heap, because threads. */
	ubres = talloc(inst, struct ub_result *);

	/* Used and thus impossible value from heap to designate incomplete */
	*ubres = (void *)instance;

	ub_resolve_async(inst->ub, fmt, 1, 1, ubres, link_ubres, &async_id);

	if (ub_common_wait(inst, request, inst->xlat_a_name, ubres, async_id)) {
		goto error0;
	}

	if (*ubres) {
		if (ub_common_fail(request, inst->xlat_a_name, *ubres)) {
			goto error1;
		}

		if (!inet_ntop(AF_INET, (*ubres)->data[0], out, freespace)) {
			goto error1;
		};

		ub_resolve_free(*ubres);
		talloc_free(ubres);
		return strlen(out);
	}

	RWDEBUG("rlm_unbound (%s): no result", inst->xlat_a_name);

 error1:
	ub_resolve_free(*ubres); /* Handles NULL gracefully */

 error0:
	talloc_free(ubres);
	return -1;
}

static ssize_t xlat_aaaa(void *instance, REQUEST *request, char const *fmt, char *out, size_t freespace)
{
	rlm_unbound_t *inst = instance;
	struct ub_result **ubres;
	int async_id;

	/* This has to be on the heap, because threads. */
	ubres = talloc(inst, struct ub_result *);

	/* Used and thus impossible value from heap to designate incomplete */
	*ubres = (void *)instance;

	ub_resolve_async(inst->ub, fmt, 28, 1, ubres, link_ubres, &async_id);

	if (ub_common_wait(inst, request, inst->xlat_aaaa_name, ubres, async_id)) {
		goto error0;
	}

	if (*ubres) {
		if (ub_common_fail(request, inst->xlat_aaaa_name, *ubres)) {
			goto error1;
		}
		if (!inet_ntop(AF_INET6, (*ubres)->data[0], out, freespace)) {
			goto error1;
		};
		ub_resolve_free(*ubres);
		talloc_free(ubres);
		return strlen(out);
	}

	RWDEBUG("rlm_unbound (%s): no result", inst->xlat_aaaa_name);

error1:
	ub_resolve_free(*ubres); /* Handles NULL gracefully */

error0:
	talloc_free(ubres);
	return -1;
}

static ssize_t xlat_ptr(void *instance, REQUEST *request, char const *fmt, char *out, size_t freespace)
{
	rlm_unbound_t *inst = instance;
	struct ub_result **ubres;
	int async_id;

	/* This has to be on the heap, because threads. */
	ubres = talloc(inst, struct ub_result *);

	/* Used and thus impossible value from heap to designate incomplete */
	*ubres = (void *)instance;

	ub_resolve_async(inst->ub, fmt, 12, 1, ubres, link_ubres, &async_id);

	if (ub_common_wait(inst, request, inst->xlat_ptr_name,
			   ubres, async_id)) {
		goto error0;
	}

	if (*ubres) {
		if (ub_common_fail(request, inst->xlat_ptr_name, *ubres)) {
			goto error1;
		}
		if (rrlabels_tostr(out, (*ubres)->data[0], freespace) < 0) {
			goto error1;
		}
		ub_resolve_free(*ubres);
		talloc_free(ubres);
		return strlen(out);
	}

	RWDEBUG("rlm_unbound (%s): no result", inst->xlat_ptr_name);

error1:
	ub_resolve_free(*ubres);  /* Handles NULL gracefully */

error0:
	talloc_free(ubres);
	return -1;
}

/*
 *	Even when run in asyncronous mode, callbacks sent to libunbound still
 *	must be run in an application-side thread (via ub_process.)  This is
 *	probably to keep the API usage consistent across threaded and forked
 *	embedded client modes.  This callback function lets an event loop call
 *	ub_process when the instance's file descriptor becomes ready.
 */
static void ub_fd_handler(UNUSED fr_event_list_t *el, UNUSED int sock, void *ctx)
{
	rlm_unbound_t *inst = ctx;
	int err;

	err = ub_process(inst->ub);
	if (err) {
		ERROR("rlm_unbound (%s) async ub_process: %s",
		      inst->name, ub_strerror(err));
	}
}

#ifndef HAVE_PTHREAD_H

/* If we have to use a pipe to redirect logging, this does the work. */
static void log_spew(UNUSED fr_event_list_t *el, UNUSED int sock, void *ctx)
{
	rlm_unbound_t *inst = ctx;
	char line[1024];

	/*
	 *  This works for pipes from processes, but not from threads
	 *  right now.  The latter is hinky and will require some fancy
	 *  blocking/nonblocking trickery which is not figured out yet,
	 *  since selecting on a pipe from a thread in the same process
	 *  seems to behave differently.  It will likely preclude the use
	 *  of fgets and streams.  Left for now since some unbound logging
	 *  infrastructure is still global across multiple contexts.  Maybe
	 *  we can get unbound folks to provide a ub_ctx_debugout_async that
	 *  takes a function hook instead to just bypass the piping when
	 *  used in threaded mode.
	 */
	while (fgets(line, 1024, inst->log_pipe_stream[0])) {
		DEBUG("rlm_unbound (%s): %s", inst->name, line);
	}
}

#endif

static int mod_bootstrap(CONF_SECTION *conf, void *instance)
{
	rlm_unbound_t *inst = instance;

	inst->name = cf_section_name2(conf);
	if (!inst->name) {
		inst->name = cf_section_name1(conf);
	}

	if (inst->timeout > 10000) {
		cf_log_err_cs(conf, "timeout must be 0 to 10000");
		return -1;
	}

	MEM(inst->xlat_a_name = talloc_typed_asprintf(inst, "%s-a", inst->name));
	MEM(inst->xlat_aaaa_name = talloc_typed_asprintf(inst, "%s-aaaa", inst->name));
	MEM(inst->xlat_ptr_name = talloc_typed_asprintf(inst, "%s-ptr", inst->name));

	if (xlat_register(inst->xlat_a_name, xlat_a, NULL, inst) ||
	    xlat_register(inst->xlat_aaaa_name, xlat_aaaa, NULL, inst) ||
	    xlat_register(inst->xlat_ptr_name, xlat_ptr, NULL, inst)) {
		cf_log_err_cs(conf, "Failed registering xlats");
		return -1;
	}

	return 0;
}

static int mod_instantiate(CONF_SECTION *conf, void *instance)
{
	rlm_unbound_t *inst = instance;
	int res;
	char *optval;

	log_dst_t log_dst;
	int log_level;
	int log_fd = -1;

	inst->el = radius_event_list_corral(EVENT_CORRAL_AUX);
	inst->log_pipe_stream[0] = NULL;
	inst->log_pipe_stream[1] = NULL;
	inst->log_fd = -1;
	inst->log_pipe_in_use = false;

	inst->ub = ub_ctx_create();
	if (!inst->ub) {
		cf_log_err_cs(conf, "ub_ctx_create failed");
		return -1;
	}

#ifdef HAVE_PTHREAD_H
	/*
	 *	Note unbound threads WILL happen with -s option, if it matters.
	 *	We cannot tell from here whether that option is in effect.
	 */
	res = ub_ctx_async(inst->ub, 1);
#else
	/*
	 *	Uses forked subprocesses instead.
	 */
	res = ub_ctx_async(inst->ub, 0);
#endif

	if (res) goto error;

	/*	Glean some default settings to match the main server.	*/
	/*	TODO: debug_level can be changed at runtime. */
	/*	TODO: log until fork when stdout or stderr and !rad_debug_lvl. */
	log_level = 0;

	if (rad_debug_lvl > 0) {
		log_level = rad_debug_lvl;

	} else if (main_config.debug_level > 0) {
		log_level = main_config.debug_level;
	}

	switch (log_level) {
	/* TODO: This will need some tweaking */
	case 0:
	case 1:
		break;

	case 2:
		log_level = 1;
		break;

	case 3:
	case 4:
		log_level = 2; /* mid-to-heavy levels of output */
		break;

	case 5:
	case 6:
	case 7:
	case 8:
		log_level = 3; /* Pretty crazy amounts of output */
		break;

	default:
		log_level = 4; /* Insane amounts of output including crypts */
		break;
	}

	res = ub_ctx_debuglevel(inst->ub, log_level);
	if (res) goto error;

	switch (default_log.dst) {
	case L_DST_STDOUT:
		if (!rad_debug_lvl) {
			log_dst = L_DST_NULL;
			break;
		}
		log_dst = L_DST_STDOUT;
		log_fd = dup(STDOUT_FILENO);
		break;

	case L_DST_STDERR:
		if (!rad_debug_lvl) {
			log_dst = L_DST_NULL;
			break;
		}
		log_dst = L_DST_STDOUT;
		log_fd = dup(STDERR_FILENO);
		break;

	case L_DST_FILES:
		if (main_config.log_file) {
			res = ub_ctx_set_option(inst->ub, "logfile:", main_config.log_file);
			if (res) {
				goto error;
			}
			log_dst = L_DST_FILES;
			break;
		}
		/* FALL-THROUGH */

	case L_DST_NULL:
		log_dst = L_DST_NULL;
		break;

	default:
		log_dst = L_DST_SYSLOG;
		break;
	}

	/* Now load the config file, which can override gleaned settings. */
	{
		res = ub_ctx_config(inst->ub, inst->filename);
		if (res) goto error;
	}

	/*
	 *	Check if the config file tried to use syslog.  Unbound
	 *	does not share syslog gracefully.
	 */
	res = ub_ctx_get_option(inst->ub, "use-syslog", &optval);
	if (res || !optval) goto error;

	if (!strcmp(optval, "yes")) {
		free(optval);

		WARN("rlm_unbound (%s): Overriding syslog settings.", inst->name);
		res = ub_ctx_set_option(inst->ub, "use-syslog:", "no");
		if (res) goto error;

		if (log_dst == L_DST_FILES) {
			/* Reinstate the log file name JIC */
			res = ub_ctx_set_option(inst->ub, "logfile:", main_config.log_file);
			if (res) goto error;
		}

	} else {
		if (optval) free(optval);

		res = ub_ctx_get_option(inst->ub, "logfile", &optval);
		if (res) goto error;

		if (optval && strlen(optval)) {
			log_dst = L_DST_FILES;

		} else if (!rad_debug_lvl) {
			log_dst = L_DST_NULL;
		}

		if (optval) free(optval);
	}

	switch (log_dst) {
	case L_DST_STDOUT:
		/*
		 * We have an fd to log to.  And we've already attempted to
		 * dup it so libunbound doesn't close it on us.
		 */
		if (log_fd == -1) {
			cf_log_err_cs(conf, "Could not dup fd");
			goto error_nores;
		}

		inst->log_stream = fdopen(log_fd, "w");
		if (!inst->log_stream) {
			cf_log_err_cs(conf, "error setting up log stream");
			goto error_nores;
		}

		res = ub_ctx_debugout(inst->ub, inst->log_stream);
		if (res) goto error;
		break;

	case L_DST_FILES:
		/* We gave libunbound a filename.  It is on its own now. */
		break;

	case L_DST_NULL:
		/* We tell libunbound not to log at all. */
		res = ub_ctx_debugout(inst->ub, NULL);
		if (res) goto error;
		break;

	case L_DST_SYSLOG:
#ifdef HAVE_PTHREAD_H
		/*
		 *  Currently this wreaks havoc when running threaded, so just
		 *  turn logging off until that gets figured out.
		 */
		res = ub_ctx_debugout(inst->ub, NULL);
		if (res) goto error;
		break;
#else
		/*
		 *  We need to create a pipe, because libunbound does not
		 *  share syslog nicely.  Or the core added some new logsink.
		 */
		if (pipe(inst->log_pipe)) {
		error_pipe:
			cf_log_err_cs(conf, "Error setting up log pipes");
			goto error_nores;
		}

		if ((fcntl(inst->log_pipe[0], F_SETFL, O_NONBLOCK) < 0) ||
		    (fcntl(inst->log_pipe[0], F_SETFD, FD_CLOEXEC) < 0)) {
			goto error_pipe;
		}

		/* Opaque to us when this can be closed, so we do not. */
		if (fcntl(inst->log_pipe[1], F_SETFL, O_NONBLOCK) < 0) {
			goto error_pipe;
		}

		inst->log_pipe_stream[0] = fdopen(inst->log_pipe[0], "r");
		inst->log_pipe_stream[1] = fdopen(inst->log_pipe[1], "w");

		if (!inst->log_pipe_stream[0] || !inst->log_pipe_stream[1]) {
			if (!inst->log_pipe_stream[1]) {
				close(inst->log_pipe[1]);
			}

			if (!inst->log_pipe_stream[0]) {
				close(inst->log_pipe[0]);
			}
			cf_log_err_cs(conf, "Error setting up log stream");
			goto error_nores;
		}

		res = ub_ctx_debugout(inst->ub, inst->log_pipe_stream[1]);
		if (res) goto error;

		if (!fr_event_fd_insert(inst->el, 0, inst->log_pipe[0], log_spew, inst)) {
			cf_log_err_cs(conf, "could not insert log fd");
			goto error_nores;
		}

		inst->log_pipe_in_use = true;
#endif
	default:
		break;
	}

	/*
	 *	Load resolv.conf if specified
	 */
	if (inst->resolvconf) ub_ctx_resolvconf(inst->ub, inst->resolvconf);

	/*
	 *	Load hosts file if specified
	 */
	if (inst->hosts) ub_ctx_hosts(inst->ub, inst->hosts);

	/*
	 *  Now we need to finalize the context.
	 *
	 *  There's no clean API to just finalize the context made public
	 *  in libunbound.  But we can trick it by trying to delete data
	 *  which as it happens fails quickly and quietly even though the
	 *  data did not exist.
	 */
	ub_ctx_data_remove(inst->ub, "notar33lsite.foo123.nottld A 127.0.0.1");

	inst->log_fd = ub_fd(inst->ub);
	if (inst->log_fd >= 0) {
		if (!fr_event_fd_insert(inst->el, 0, inst->log_fd, ub_fd_handler, inst)) {
			cf_log_err_cs(conf, "could not insert async fd");
			inst->log_fd = -1;
			goto error_nores;
		}

	}

	return 0;

 error:
	cf_log_err_cs(conf, "%s", ub_strerror(res));

 error_nores:
	if (log_fd > -1) close(log_fd);

	return -1;
}

static int mod_detach(UNUSED void *instance)
{
	rlm_unbound_t *inst = instance;

	if (inst->log_fd >= 0) {
		fr_event_fd_delete(inst->el, 0, inst->log_fd);
		if (inst->ub) {
			ub_process(inst->ub);
			/* This can hang/leave zombies currently
			 * see upstream bug #519
			 * ...so expect valgrind to complain with -m
			 */
#if 0
			ub_ctx_delete(inst->ub);
#endif
		}
	}

	if (inst->log_pipe_stream[1]) {
		fclose(inst->log_pipe_stream[1]);
	}

	if (inst->log_pipe_stream[0]) {
		if (inst->log_pipe_in_use) {
			fr_event_fd_delete(inst->el, 0, inst->log_pipe[0]);
		}
		fclose(inst->log_pipe_stream[0]);
	}

	if (inst->log_stream) {
		fclose(inst->log_stream);
	}

	return 0;
}

extern module_t rlm_unbound;
module_t rlm_unbound = {
	.magic		= RLM_MODULE_INIT,
	.name		= "unbound",
	.type		= RLM_TYPE_THREAD_SAFE,
	.inst_size	= sizeof(rlm_unbound_t),
	.config		= module_config,
	.bootstrap	= mod_bootstrap,
	.instantiate	= mod_instantiate,
	.detach		= mod_detach
};
