/*
 * dnstcp - serve dns via tcp
 */
#include <u.h>
#include <lib9.h>
#include <ip.h>
#include "dns.h"

Cfg cfg;

char	*caller = "";
char	*dbfile;
int	debug;
uint8_t	ipaddr[IPaddrlen];	/* my ip address */
char	*logfile = "dns";
int	maxage = 60*60;
char	mntpt[Maxpath];
int	needrefresh;
uint32_t	now;
int64_t	nowns;
int	testing;
int	traceactivity;
char	*zonerefreshprogram;

static int	readmsg(int, uint8_t*, int);
static void	reply(int, DNSmsg*, Request*);
static void	dnzone(DNSmsg*, DNSmsg*, Request*);
static void	getcaller(char*);
static void	refreshmain(char*);

void
usage(void)
{
	fprint(2, "usage: %s [-rR] [-f ndb-file] [-x netmtpt] [conndir]\n", argv0);
	exits("usage");
}

void
main(int argc, char *argv[])
{
	int len, rcode;
	char tname[32];
	char *err, *ext = "";
	uint8_t buf[64*1024], callip[IPaddrlen];
	DNSmsg reqmsg, repmsg;
	Request req;

	alarm(2*60*1000);
	cfg.cachedb = 1;
	ARGBEGIN{
	case 'd':
		debug++;
		break;
	case 'f':
		dbfile = EARGF(usage());
		break;
	case 'r':
		cfg.resolver = 1;
		break;
	case 'R':
		norecursion = 1;
		break;
	case 'x':
		ext = EARGF(usage());
		break;
	default:
		usage();
		break;
	}ARGEND

	if(argc > 0)
		getcaller(argv[0]);

	cfg.inside = 1;
	dninit();

	if(*ext == '/')
		snprint(mntpt, sizeof mntpt, "%s", ext);
	else
		snprint(mntpt, sizeof mntpt, "/net%s", ext);

	if(myipaddr(ipaddr, mntpt) < 0)
		sysfatal("can't read my ip address");
	dnslog("dnstcp call from %s to %I", caller, ipaddr);
	memset(callip, 0, sizeof callip);
	parseip(callip, caller);

	db2cache(1);

	memset(&req, 0, sizeof req);
	setjmp(req.mret);
	req.isslave = 0;
	procsetname("main loop");

	/* loop on requests */
	for(;; putactivity(0)){
		now = time(nil);
		memset(&repmsg, 0, sizeof repmsg);
		len = readmsg(0, buf, sizeof buf);
		if(len <= 0)
			break;

		getactivity(&req, 0);
		req.aborttime = timems() + S2MS(15*Min);
		rcode = 0;
		memset(&reqmsg, 0, sizeof reqmsg);
		err = convM2DNS(buf, len, &reqmsg, &rcode);
		if(err){
			dnslog("server: input error: %s from %s", err, caller);
			free(err);
			break;
		}
		if (rcode == 0)
			if(reqmsg.qdcount < 1){
				dnslog("server: no questions from %s", caller);
				break;
			} else if(reqmsg.flags & Fresp){
				dnslog("server: reply not request from %s",
					caller);
				break;
			} else if((reqmsg.flags & Omask) != Oquery){
				dnslog("server: op %d from %s",
					reqmsg.flags & Omask, caller);
				break;
			}

		if(reqmsg.qd == nil){
			dnslog("server: no question RR from %s", caller);
			break;
		}

		if(debug)
			dnslog("[%d] %d: serve (%s) %d %s %s",
				getpid(), req.id, caller,
				reqmsg.id, reqmsg.qd->owner->name,
				rrname(reqmsg.qd->type, tname, sizeof tname));

		/* loop through each question */
		while(reqmsg.qd)
			if(reqmsg.qd->type == Taxfr)
				dnzone(&reqmsg, &repmsg, &req);
			else {
				dnserver(&reqmsg, &repmsg, &req, callip, rcode);
				reply(1, &repmsg, &req);
				rrfreelist(repmsg.qd);
				rrfreelist(repmsg.an);
				rrfreelist(repmsg.ns);
				rrfreelist(repmsg.ar);
			}
		rrfreelist(reqmsg.qd);		/* qd will be nil */
		rrfreelist(reqmsg.an);
		rrfreelist(reqmsg.ns);
		rrfreelist(reqmsg.ar);

		if(req.isslave){
			putactivity(0);
			_exits(0);
		}
	}
	refreshmain(mntpt);
}

static int
readmsg(int fd, uint8_t *buf, int max)
{
	int n;
	uint8_t x[2];

	if(readn(fd, x, 2) != 2)
		return -1;
	n = x[0]<<8 | x[1];
	if(n > max)
		return -1;
	if(readn(fd, buf, n) != n)
		return -1;
	return n;
}

static void
reply(int fd, DNSmsg *rep, Request *req)
{
	int len, rv;
	char tname[32];
	uint8_t buf[64*1024];
	RR *rp;

	if(debug){
		dnslog("%d: reply (%s) %s %s %ux",
			req->id, caller,
			rep->qd->owner->name,
			rrname(rep->qd->type, tname, sizeof tname),
			rep->flags);
		for(rp = rep->an; rp; rp = rp->next)
			dnslog("an %R", rp);
		for(rp = rep->ns; rp; rp = rp->next)
			dnslog("ns %R", rp);
		for(rp = rep->ar; rp; rp = rp->next)
			dnslog("ar %R", rp);
	}


	len = convDNS2M(rep, buf+2, sizeof(buf) - 2);
	buf[0] = len>>8;
	buf[1] = len;
	rv = write(fd, buf, len+2);
	if(rv != len+2){
		dnslog("[%d] sending reply: %d instead of %d", getpid(), rv,
			len+2);
		exits(0);
	}
}

/*
 *  Hash table for domain names.  The hash is based only on the
 *  first element of the domain name.
 */
extern DN	*ht[HTLEN];

static int
numelem(char *name)
{
	int i;

	i = 1;
	for(; *name; name++)
		if(*name == '.')
			i++;
	return i;
}

int
inzone(DN *dp, char *name, int namelen, int depth)
{
	int n;

	if(dp->name == nil)
		return 0;
	if(numelem(dp->name) != depth)
		return 0;
	n = strlen(dp->name);
	if(n < namelen)
		return 0;
	if(cistrcmp(name, dp->name + n - namelen) != 0)
		return 0;
	if(n > namelen && dp->name[n - namelen - 1] != '.')
		return 0;
	return 1;
}

static void
dnzone(DNSmsg *reqp, DNSmsg *repp, Request *req)
{
	DN *dp, *ndp;
	RR r, *rp;
	int h, depth, found, nlen;

	memset(repp, 0, sizeof(*repp));
	repp->id = reqp->id;
	repp->qd = reqp->qd;
	reqp->qd = reqp->qd->next;
	repp->qd->next = 0;
	repp->flags = Fauth | Fresp | Oquery;
	if(!norecursion)
		repp->flags |= Fcanrec;
	dp = repp->qd->owner;

	/* send the soa */
	repp->an = rrlookup(dp, Tsoa, NOneg);
	reply(1, repp, req);
	if(repp->an == 0)
		goto out;
	rrfreelist(repp->an);
	repp->an = nil;

	nlen = strlen(dp->name);

	/* construct a breadth-first search of the name space (hard with a hash) */
	repp->an = &r;
	for(depth = numelem(dp->name); ; depth++){
		found = 0;
		for(h = 0; h < HTLEN; h++)
			for(ndp = ht[h]; ndp; ndp = ndp->next)
				if(inzone(ndp, dp->name, nlen, depth)){
					for(rp = ndp->rr; rp; rp = rp->next){
						/*
						 * there shouldn't be negatives,
						 * but just in case.
						 * don't send any soa's,
						 * ns's are enough.
						 */
						if (rp->negative ||
						    rp->type == Tsoa)
							continue;
						r = *rp;
						r.next = 0;
						reply(1, repp, req);
					}
					found = 1;
				}
		if(!found)
			break;
	}

	/* resend the soa */
	repp->an = rrlookup(dp, Tsoa, NOneg);
	reply(1, repp, req);
	rrfreelist(repp->an);
	repp->an = nil;
out:
	rrfree(repp->qd);
	repp->qd = nil;
}

static void
getcaller(char *dir)
{
	int fd, n;
	static char remote[128];

	snprint(remote, sizeof(remote), "%s/remote", dir);
	fd = open(remote, OREAD);
	if(fd < 0)
		return;
	n = read(fd, remote, sizeof remote - 1);
	close(fd);
	if(n <= 0)
		return;
	if(remote[n-1] == '\n')
		n--;
	remote[n] = 0;
	caller = remote;
}

static void
refreshmain(char *net)
{
	int fd;
	char file[128];

	snprint(file, sizeof(file), "%s/dns", net);
	if(debug)
		dnslog("refreshing %s", file);
	fd = open(file, ORDWR);
	if(fd < 0)
		dnslog("can't refresh %s", file);
	else {
		fprint(fd, "refresh");
		close(fd);
	}
}

/*
 *  the following varies between dnsdebug and dns
 */
void
logreply(int id, uint8_t *addr, DNSmsg *mp)
{
	RR *rp;

	dnslog("%d: rcvd %I flags:%s%s%s%s%s", id, addr,
		mp->flags & Fauth? " auth": "",
		mp->flags & Ftrunc? " trunc": "",
		mp->flags & Frecurse? " rd": "",
		mp->flags & Fcanrec? " ra": "",
		(mp->flags & (Fauth|Rmask)) == (Fauth|Rname)? " nx": "");
	for(rp = mp->qd; rp != nil; rp = rp->next)
		dnslog("%d: rcvd %I qd %s", id, addr, rp->owner->name);
	for(rp = mp->an; rp != nil; rp = rp->next)
		dnslog("%d: rcvd %I an %R", id, addr, rp);
	for(rp = mp->ns; rp != nil; rp = rp->next)
		dnslog("%d: rcvd %I ns %R", id, addr, rp);
	for(rp = mp->ar; rp != nil; rp = rp->next)
		dnslog("%d: rcvd %I ar %R", id, addr, rp);
}

void
logsend(int id, int subid, uint8_t *addr, char *sname, char *rname, int type)
{
	char buf[12];

	dnslog("%d.%d: sending to %I/%s %s %s",
		id, subid, addr, sname, rname, rrname(type, buf, sizeof buf));
}

RR*
getdnsservers(int class)
{
	return dnsservers(class);
}
