/*
 * Copyright (C) 2004-2007, 2012  Internet Systems Consortium, Inc. ("ISC")
 * Copyright (C) 2000, 2001  Internet Software Consortium.
 *
 * Permission to use, copy, modify, and/or 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 ISC DISCLAIMS ALL WARRANTIES WITH
 * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS.  IN NO EVENT SHALL ISC 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.
 */

/* $Id$ */

/*! \file */

#include <config.h>

#include <stdlib.h>

#include <isc/netaddr.h>
#include <isc/sockaddr.h>
#include <isc/socket.h>
#include <isc/string.h>		/* Required for HP/UX (and others?) */
#include <isc/util.h>

#include <dns/adb.h>
#include <dns/events.h>
#include <dns/result.h>

#include <named/types.h>
#include <named/lwaddr.h>
#include <named/lwdclient.h>
#include <named/lwresd.h>
#include <named/lwsearch.h>
#include <named/sortlist.h>

#define NEED_V4(c)	((((c)->find_wanted & LWRES_ADDRTYPE_V4) != 0) \
			 && ((c)->v4find == NULL))
#define NEED_V6(c)	((((c)->find_wanted & LWRES_ADDRTYPE_V6) != 0) \
			 && ((c)->v6find == NULL))

static isc_result_t start_find(ns_lwdclient_t *);
static void restart_find(ns_lwdclient_t *);
static void init_gabn(ns_lwdclient_t *);

/*%
 * Destroy any finds.  This can be used to "start over from scratch" and
 * should only be called when events are _not_ being generated by the finds.
 */
static void
cleanup_gabn(ns_lwdclient_t *client) {
	ns_lwdclient_log(50, "cleaning up client %p", client);

	if (client->v6find != NULL) {
		if (client->v6find == client->v4find)
			client->v6find = NULL;
		else
			dns_adb_destroyfind(&client->v6find);
	}
	if (client->v4find != NULL)
		dns_adb_destroyfind(&client->v4find);
}

static void
setup_addresses(ns_lwdclient_t *client, dns_adbfind_t *find, unsigned int at) {
	dns_adbaddrinfo_t *ai;
	lwres_addr_t *addr;
	int af;
	const struct sockaddr *sa;
	isc_result_t result;

	if (at == DNS_ADBFIND_INET)
		af = AF_INET;
	else
		af = AF_INET6;

	ai = ISC_LIST_HEAD(find->list);
	while (ai != NULL && client->gabn.naddrs < LWRES_MAX_ADDRS) {
		sa = &ai->sockaddr.type.sa;
		if (sa->sa_family != af)
			goto next;

		addr = &client->addrs[client->gabn.naddrs];

		result = lwaddr_lwresaddr_fromsockaddr(addr, &ai->sockaddr);
		if (result != ISC_R_SUCCESS)
			goto next;

		ns_lwdclient_log(50, "adding address %p, family %d, length %d",
				 addr->address, addr->family, addr->length);

		client->gabn.naddrs++;
		REQUIRE(!LWRES_LINK_LINKED(addr, link));
		LWRES_LIST_APPEND(client->gabn.addrs, addr, link);

	next:
		ai = ISC_LIST_NEXT(ai, publink);
	}
}

typedef struct {
	isc_netaddr_t address;
	int rank;
} rankedaddress;

static int
addr_compare(const void *av, const void *bv) {
	const rankedaddress *a = (const rankedaddress *) av;
	const rankedaddress *b = (const rankedaddress *) bv;
	return (a->rank - b->rank);
}

static void
sort_addresses(ns_lwdclient_t *client) {
	unsigned int naddrs;
	rankedaddress *addrs;
	isc_netaddr_t remote;
	dns_addressorderfunc_t order;
	const void *arg;
	ns_lwresd_t *lwresd = client->clientmgr->listener->manager;
	unsigned int i;
	isc_result_t result;

	naddrs = client->gabn.naddrs;

	if (naddrs <= 1 || lwresd->view->sortlist == NULL)
		return;

	addrs = isc_mem_get(lwresd->mctx, sizeof(rankedaddress) * naddrs);
	if (addrs == NULL)
		return;

	isc_netaddr_fromsockaddr(&remote, &client->address);
	ns_sortlist_byaddrsetup(lwresd->view->sortlist,
				&remote, &order, &arg);
	if (order == NULL) {
		isc_mem_put(lwresd->mctx, addrs,
			    sizeof(rankedaddress) * naddrs);
		return;
	}
	for (i = 0; i < naddrs; i++) {
		result = lwaddr_netaddr_fromlwresaddr(&addrs[i].address,
						      &client->addrs[i]);
		INSIST(result == ISC_R_SUCCESS);
		addrs[i].rank = (*order)(&addrs[i].address, arg);
	}
	qsort(addrs, naddrs, sizeof(rankedaddress), addr_compare);
	for (i = 0; i < naddrs; i++) {
		result = lwaddr_lwresaddr_fromnetaddr(&client->addrs[i],
						      &addrs[i].address);
		INSIST(result == ISC_R_SUCCESS);
	}

	isc_mem_put(lwresd->mctx, addrs, sizeof(rankedaddress) * naddrs);
}

static void
generate_reply(ns_lwdclient_t *client) {
	isc_result_t result;
	int lwres;
	isc_region_t r;
	lwres_buffer_t lwb;
	ns_lwdclientmgr_t *cm;

	cm = client->clientmgr;
	lwb.base = NULL;

	ns_lwdclient_log(50, "generating gabn reply for client %p", client);

	/*
	 * We must make certain the client->find is not still active.
	 * If it is either the v4 or v6 answer, just set it to NULL and
	 * let the cleanup code destroy it.  Otherwise, destroy it now.
	 */
	if (client->find == client->v4find || client->find == client->v6find)
		client->find = NULL;
	else
		if (client->find != NULL)
			dns_adb_destroyfind(&client->find);

	/*
	 * perhaps there are some here?
	 */
	if (NEED_V6(client) && client->v4find != NULL)
		client->v6find = client->v4find;

	/*
	 * Run through the finds we have and wire them up to the gabn
	 * structure.
	 */
	LWRES_LIST_INIT(client->gabn.addrs);
	if (client->v4find != NULL)
		setup_addresses(client, client->v4find, DNS_ADBFIND_INET);
	if (client->v6find != NULL)
		setup_addresses(client, client->v6find, DNS_ADBFIND_INET6);

	/*
	 * If there are no addresses, try the next element in the search
	 * path, if there are any more.  Otherwise, fall through into
	 * the error handling code below.
	 */
	if (client->gabn.naddrs == 0) {
		do {
			result = ns_lwsearchctx_next(&client->searchctx);
			if (result == ISC_R_SUCCESS) {
				cleanup_gabn(client);
				result = start_find(client);
				if (result == ISC_R_SUCCESS)
					return;
			}
		} while (result == ISC_R_SUCCESS);
	}

	/*
	 * Render the packet.
	 */
	client->pkt.recvlength = LWRES_RECVLENGTH;
	client->pkt.authtype = 0; /* XXXMLG */
	client->pkt.authlength = 0;

	/*
	 * If there are no addresses, return failure.
	 */
	if (client->gabn.naddrs != 0)
		client->pkt.result = LWRES_R_SUCCESS;
	else
		client->pkt.result = LWRES_R_NOTFOUND;

	sort_addresses(client);

	lwres = lwres_gabnresponse_render(cm->lwctx, &client->gabn,
					  &client->pkt, &lwb);
	if (lwres != LWRES_R_SUCCESS)
		goto out;

	r.base = lwb.base;
	r.length = lwb.used;
	client->sendbuf = r.base;
	client->sendlength = r.length;
	result = ns_lwdclient_sendreply(client, &r);
	if (result != ISC_R_SUCCESS)
		goto out;

	NS_LWDCLIENT_SETSEND(client);

	/*
	 * All done!
	 */
	cleanup_gabn(client);

	return;

 out:
	cleanup_gabn(client);

	if (lwb.base != NULL)
		lwres_context_freemem(client->clientmgr->lwctx,
				      lwb.base, lwb.length);

	ns_lwdclient_errorpktsend(client, LWRES_R_FAILURE);
}

/*
 * Take the current real name, move it to an alias slot (if any are
 * open) then put this new name in as the real name for the target.
 *
 * Return success if it can be rendered, otherwise failure.  Note that
 * not having enough alias slots open is NOT a failure.
 */
static isc_result_t
add_alias(ns_lwdclient_t *client) {
	isc_buffer_t b;
	isc_result_t result;
	isc_uint16_t naliases;

	b = client->recv_buffer;

	/*
	 * Render the new name to the buffer.
	 */
	result = dns_name_totext(dns_fixedname_name(&client->target_name),
				 ISC_TRUE, &client->recv_buffer);
	if (result != ISC_R_SUCCESS)
		return (result);

	/*
	 * Are there any open slots?
	 */
	naliases = client->gabn.naliases;
	if (naliases < LWRES_MAX_ALIASES) {
		client->gabn.aliases[naliases] = client->gabn.realname;
		client->gabn.aliaslen[naliases] = client->gabn.realnamelen;
		client->gabn.naliases++;
	}

	/*
	 * Save this name away as the current real name.
	 */
	client->gabn.realname = (char *)(b.base) + b.used;
	client->gabn.realnamelen = client->recv_buffer.used - b.used;

	return (ISC_R_SUCCESS);
}

static isc_result_t
store_realname(ns_lwdclient_t *client) {
	isc_buffer_t b;
	isc_result_t result;
	dns_name_t *tname;

	b = client->recv_buffer;

	tname = dns_fixedname_name(&client->target_name);
	result = ns_lwsearchctx_current(&client->searchctx, tname);
	if (result != ISC_R_SUCCESS)
		return (result);

	/*
	 * Render the new name to the buffer.
	 */
	result = dns_name_totext(tname, ISC_TRUE, &client->recv_buffer);
	if (result != ISC_R_SUCCESS)
		return (result);

	/*
	 * Save this name away as the current real name.
	 */
	client->gabn.realname = (char *) b.base + b.used;
	client->gabn.realnamelen = client->recv_buffer.used - b.used;

	return (ISC_R_SUCCESS);
}

static void
process_gabn_finddone(isc_task_t *task, isc_event_t *ev) {
	ns_lwdclient_t *client = ev->ev_arg;
	isc_eventtype_t evtype;
	isc_boolean_t claimed;

	ns_lwdclient_log(50, "find done for task %p, client %p", task, client);

	evtype = ev->ev_type;
	isc_event_free(&ev);

	/*
	 * No more info to be had?  If so, we have all the good stuff
	 * right now, so we can render things.
	 */
	claimed = ISC_FALSE;
	if (evtype == DNS_EVENT_ADBNOMOREADDRESSES) {
		if (NEED_V4(client)) {
			client->v4find = client->find;
			claimed = ISC_TRUE;
		}
		if (NEED_V6(client)) {
			client->v6find = client->find;
			claimed = ISC_TRUE;
		}
		if (client->find != NULL) {
			if (claimed)
				client->find = NULL;
			else
				dns_adb_destroyfind(&client->find);

		}
		generate_reply(client);
		return;
	}

	/*
	 * We probably don't need this find anymore.  We're either going to
	 * reissue it, or an error occurred.  Either way, we're done with
	 * it.
	 */
	if ((client->find != client->v4find)
	    && (client->find != client->v6find)) {
		dns_adb_destroyfind(&client->find);
	} else {
		client->find = NULL;
	}

	/*
	 * We have some new information we can gather.  Run off and fetch
	 * it.
	 */
	if (evtype == DNS_EVENT_ADBMOREADDRESSES) {
		restart_find(client);
		return;
	}

	/*
	 * An error or other strangeness happened.  Drop this query.
	 */
	cleanup_gabn(client);
	ns_lwdclient_errorpktsend(client, LWRES_R_FAILURE);
}

static void
restart_find(ns_lwdclient_t *client) {
	unsigned int options;
	isc_result_t result;
	isc_boolean_t claimed;

	ns_lwdclient_log(50, "starting find for client %p", client);

	/*
	 * Issue a find for the name contained in the request.  We won't
	 * set the bit that says "anything is good enough" -- we want it
	 * all.
	 */
	options = 0;
	options |= DNS_ADBFIND_WANTEVENT;
	options |= DNS_ADBFIND_RETURNLAME;

	/*
	 * Set the bits up here to mark that we want this address family
	 * and that we do not currently have a find pending.  We will
	 * set that bit again below if it turns out we will get an event.
	 */
	if (NEED_V4(client))
		options |= DNS_ADBFIND_INET;
	if (NEED_V6(client))
		options |= DNS_ADBFIND_INET6;

 find_again:
	INSIST(client->find == NULL);
	result = dns_adb_createfind(client->clientmgr->view->adb,
				    client->clientmgr->task,
				    process_gabn_finddone, client,
				    dns_fixedname_name(&client->target_name),
				    dns_rootname, 0, options, 0,
				    dns_fixedname_name(&client->target_name),
				    client->clientmgr->view->dstport,
				    &client->find);

	/*
	 * Did we get an alias?  If so, save it and re-issue the query.
	 */
	if (result == DNS_R_ALIAS) {
		ns_lwdclient_log(50, "found alias, restarting query");
		dns_adb_destroyfind(&client->find);
		cleanup_gabn(client);
		result = add_alias(client);
		if (result != ISC_R_SUCCESS) {
			ns_lwdclient_log(50,
					 "out of buffer space adding alias");
			ns_lwdclient_errorpktsend(client, LWRES_R_FAILURE);
			return;
		}
		goto find_again;
	}

	ns_lwdclient_log(50, "find returned %d (%s)", result,
			 isc_result_totext(result));

	/*
	 * Did we get an error?
	 */
	if (result != ISC_R_SUCCESS) {
		if (client->find != NULL)
			dns_adb_destroyfind(&client->find);
		cleanup_gabn(client);
		ns_lwdclient_errorpktsend(client, LWRES_R_FAILURE);
		return;
	}

	claimed = ISC_FALSE;

	/*
	 * Did we get our answer to V4 addresses?
	 */
	if (NEED_V4(client)
	    && ((client->find->query_pending & DNS_ADBFIND_INET) == 0)) {
		ns_lwdclient_log(50, "client %p ipv4 satisfied by find %p",
				 client, client->find);
		claimed = ISC_TRUE;
		client->v4find = client->find;
	}

	/*
	 * Did we get our answer to V6 addresses?
	 */
	if (NEED_V6(client)
	    && ((client->find->query_pending & DNS_ADBFIND_INET6) == 0)) {
		ns_lwdclient_log(50, "client %p ipv6 satisfied by find %p",
				 client, client->find);
		claimed = ISC_TRUE;
		client->v6find = client->find;
	}

	/*
	 * If we're going to get an event, set our internal pending flag
	 * and return.  When we get an event back we'll do the right
	 * thing, basically by calling this function again, perhaps with a
	 * new target name.
	 *
	 * If we have both v4 and v6, and we are still getting an event,
	 * we have a programming error, so die hard.
	 */
	if ((client->find->options & DNS_ADBFIND_WANTEVENT) != 0) {
		ns_lwdclient_log(50, "event will be sent");
		INSIST(client->v4find == NULL || client->v6find == NULL);
		return;
	}
	ns_lwdclient_log(50, "no event will be sent");
	if (claimed)
		client->find = NULL;
	else
		dns_adb_destroyfind(&client->find);

	/*
	 * We seem to have everything we asked for, or at least we are
	 * able to respond with things we've learned.
	 */

	generate_reply(client);
}

static isc_result_t
start_find(ns_lwdclient_t *client) {
	isc_result_t result;

	/*
	 * Initialize the real name and alias arrays in the reply we're
	 * going to build up.
	 */
	init_gabn(client);

	result = store_realname(client);
	if (result != ISC_R_SUCCESS)
		return (result);
	restart_find(client);
	return (ISC_R_SUCCESS);

}

static void
init_gabn(ns_lwdclient_t *client) {
	int i;

	/*
	 * Initialize the real name and alias arrays in the reply we're
	 * going to build up.
	 */
	for (i = 0; i < LWRES_MAX_ALIASES; i++) {
		client->aliases[i] = NULL;
		client->aliaslen[i] = 0;
	}
	for (i = 0; i < LWRES_MAX_ADDRS; i++) {
		client->addrs[i].family = 0;
		client->addrs[i].length = 0;
		memset(client->addrs[i].address, 0, LWRES_ADDR_MAXLEN);
		LWRES_LINK_INIT(&client->addrs[i], link);
	}

	client->gabn.naliases = 0;
	client->gabn.naddrs = 0;
	client->gabn.realname = NULL;
	client->gabn.aliases = client->aliases;
	client->gabn.realnamelen = 0;
	client->gabn.aliaslen = client->aliaslen;
	LWRES_LIST_INIT(client->gabn.addrs);
	client->gabn.base = NULL;
	client->gabn.baselen = 0;

	/*
	 * Set up the internal buffer to point to the receive region.
	 */
	isc_buffer_init(&client->recv_buffer, client->buffer, LWRES_RECVLENGTH);
}

/*
 * When we are called, we can be assured that:
 *
 *	client->sockaddr contains the address we need to reply to,
 *
 *	client->pkt contains the packet header data,
 *
 *	the packet "checks out" overall -- any MD5 hashes or crypto
 *	bits have been verified,
 *
 *	"b" points to the remaining data after the packet header
 *	was parsed off.
 *
 *	We are in a the RECVDONE state.
 *
 * From this state we will enter the SEND state if we happen to have
 * everything we need or we need to return an error packet, or to the
 * FINDWAIT state if we need to look things up.
 */
void
ns_lwdclient_processgabn(ns_lwdclient_t *client, lwres_buffer_t *b) {
	isc_result_t result;
	lwres_gabnrequest_t *req;
	ns_lwdclientmgr_t *cm;
	isc_buffer_t namebuf;

	REQUIRE(NS_LWDCLIENT_ISRECVDONE(client));

	cm = client->clientmgr;
	req = NULL;

	result = lwres_gabnrequest_parse(client->clientmgr->lwctx,
					 b, &client->pkt, &req);
	if (result != LWRES_R_SUCCESS)
		goto out;
	if (req->name == NULL)
		goto out;

	isc_buffer_init(&namebuf, req->name, req->namelen);
	isc_buffer_add(&namebuf, req->namelen);

	dns_fixedname_init(&client->target_name);
	dns_fixedname_init(&client->query_name);
	result = dns_name_fromtext(dns_fixedname_name(&client->query_name),
				   &namebuf, NULL, ISC_FALSE, NULL);
	if (result != ISC_R_SUCCESS)
		goto out;
	ns_lwsearchctx_init(&client->searchctx,
			    cm->listener->manager->search,
			    dns_fixedname_name(&client->query_name),
			    cm->listener->manager->ndots);
	ns_lwsearchctx_first(&client->searchctx);

	client->find_wanted = req->addrtypes;
	ns_lwdclient_log(50, "client %p looking for addrtypes %08x",
			 client, client->find_wanted);

	/*
	 * We no longer need to keep this around.
	 */
	lwres_gabnrequest_free(client->clientmgr->lwctx, &req);

	/*
	 * Start the find.
	 */
	result = start_find(client);
	if (result != ISC_R_SUCCESS)
		goto out;

	return;

	/*
	 * We're screwed.  Return an error packet to our caller.
	 */
 out:
	if (req != NULL)
		lwres_gabnrequest_free(client->clientmgr->lwctx, &req);

	ns_lwdclient_errorpktsend(client, LWRES_R_FAILURE);
}
