#include <u.h>
#include <libc.h>
#include <ip.h>
#include "dns.h"

typedef struct Scan	Scan;
struct Scan
{
	uchar	*base;		/* input buffer */
	uchar	*p;		/* current position */
	uchar	*ep;		/* byte after the end */

	char	*err;
	char	errbuf[256];	/* hold a formatted error sometimes */
	int	rcode;		/* outgoing response codes (reply flags) */
	int	stop;		/* flag: stop processing */
	int	trunc;		/* flag: input truncated */
};

static int
errneg(RR *rp, Scan *sp, int actual)
{
	snprint(sp->errbuf, sizeof sp->errbuf, "negative len %d: %R",
		actual, rp);
	sp->err = sp->errbuf;
	return 0;
}

static int
errtoolong(RR *rp, Scan *sp, int remain, int need, char *where)
{
	char *p, *ep;
	char ptype[64];

	p =  sp->errbuf;
	ep = sp->errbuf + sizeof sp->errbuf - 1;
	if (where)
		p = seprint(p, ep, "%s: ", where);
	if (rp)
		p = seprint(p, ep, "type %s RR: ",
			rrname(rp->type, ptype, sizeof ptype));
	p = seprint(p, ep, "%d bytes needed; %d remain", need, remain);
	if (rp)
		p = seprint(p, ep, ": %R", rp);
	/*
	 * hack to cope with servers that don't set Ftrunc when they should:
	 * if the (udp) packet is full-sized, if must be truncated because
	 * it is incomplete.  otherwise, it's just garbled.
	 */
	if (sp->ep - sp->base >= Maxpayload) {
		sp->trunc = 1;
		seprint(p, ep, " (truncated)");
	}
	if (debug && rp)
		dnslog("malformed rr: %R", rp);
	sp->err = sp->errbuf;
	return 0;
}

/*
 *  get a ushort/ulong
 */
static ushort
gchar(RR *rp, Scan *sp)
{
	ushort x;

	if(sp->err)
		return 0;
	if(sp->ep - sp->p < 1)
		return errtoolong(rp, sp, sp->ep - sp->p, 1, "gchar");
	x = sp->p[0];
	sp->p += 1;
	return x;
}
static ushort
gshort(RR *rp, Scan *sp)
{
	ushort x;

	if(sp->err)
		return 0;
	if(sp->ep - sp->p < 2)
		return errtoolong(rp, sp, sp->ep - sp->p, 2, "gshort");
	x = sp->p[0]<<8 | sp->p[1];
	sp->p += 2;
	return x;
}
static ulong
glong(RR *rp, Scan *sp)
{
	ulong x;

	if(sp->err)
		return 0;
	if(sp->ep - sp->p < 4)
		return errtoolong(rp, sp, sp->ep - sp->p, 4, "glong");
	x = sp->p[0]<<24 | sp->p[1]<<16 | sp->p[2]<<8 | sp->p[3];
	sp->p += 4;
	return x;
}

/*
 *  get an ip address
 */
static DN*
gv4addr(RR *rp, Scan *sp)
{
	char addr[32];

	if(sp->err)
		return 0;
	if(sp->ep - sp->p < 4)
		return (DN*)errtoolong(rp, sp, sp->ep - sp->p, 4, "gv4addr");
	snprint(addr, sizeof addr, "%V", sp->p);
	sp->p += 4;

	return dnlookup(addr, Cin, 1);
}
static DN*
gv6addr(RR *rp, Scan *sp)
{
	char addr[64];

	if(sp->err)
		return 0;
	if(sp->ep - sp->p < IPaddrlen)
		return (DN*)errtoolong(rp, sp, sp->ep - sp->p, IPaddrlen,
			"gv6addr");
	snprint(addr, sizeof addr, "%I", sp->p);
	sp->p += IPaddrlen;

	return dnlookup(addr, Cin, 1);
}

/*
 *  get a string.  make it an internal symbol.
 */
static DN*
gsym(RR *rp, Scan *sp)
{
	int n;
	char sym[Strlen+1];

	if(sp->err)
		return 0;
	n = 0;
	if (sp->p < sp->ep)
		n = *(sp->p++);
	if(sp->ep - sp->p < n)
		return (DN*)errtoolong(rp, sp, sp->ep - sp->p, n, "gsym");

	if(n > Strlen){
		sp->err = "illegal string (symbol)";
		return 0;
	}
	strncpy(sym, (char*)sp->p, n);
	sym[n] = 0;
	if (strlen(sym) != n)
		sp->err = "symbol shorter than declared length";
	sp->p += n;

	return dnlookup(sym, Csym, 1);
}

/*
 *  get a string.  don't make it an internal symbol.
 */
static Txt*
gstr(RR *rp, Scan *sp)
{
	int n;
	char sym[Strlen+1];
	Txt *t;

	if(sp->err)
		return 0;
	n = 0;
	if (sp->p < sp->ep)
		n = *(sp->p++);
	if(sp->ep - sp->p < n)
		return (Txt*)errtoolong(rp, sp, sp->ep - sp->p, n, "gstr");

	if(n > Strlen){
		sp->err = "illegal string";
		return 0;
	}
	strncpy(sym, (char*)sp->p, n);
	sym[n] = 0;
	if (strlen(sym) != n)
		sp->err = "string shorter than declared length";
	sp->p += n;

	t = emalloc(sizeof(*t));
	t->next = nil;
	t->p = estrdup(sym);
	return t;
}

/*
 *  get a sequence of bytes
 */
static int
gbytes(RR *rp, Scan *sp, uchar **p, int n)
{
	*p = nil;			/* i think this is a good idea */
	if(sp->err)
		return 0;
	if(n < 0)
		return errneg(rp, sp, n);
	if(sp->ep - sp->p < n)
		return errtoolong(rp, sp, sp->ep - sp->p, n, "gbytes");
	*p = emalloc(n);
	memmove(*p, sp->p, n);
	sp->p += n;

	return n;
}

/*
 *  get a domain name.  'to' must point to a buffer at least Domlen+1 long.
 */
static char*
gname(char *to, RR *rp, Scan *sp)
{
	int len, off, pointer, n;
	char *tostart, *toend;
	uchar *p;

	tostart = to;
	if(sp->err || sp->stop)
		goto err;
	pointer = 0;
	p = sp->p;
	if (p == nil) {
		dnslog("gname: %R: nil sp->p", rp);
		goto err;
	}
	toend = to + Domlen;
	for(len = 0; *p && p < sp->ep; len += (pointer? 0: n+1)) {
		n = 0;
		switch (*p & 0300) {
		case 0:			/* normal label */
			if (p < sp->ep)
				n = *p++ & 077;		/* pick up length */
			if(len + n < Domlen - 1){
				if(n > toend - to){
					errtoolong(rp, sp, toend - to, n,
						"name too long");
					goto err;
				}
				memmove(to, p, n);
				to += n;
			}
			p += n;
			if(*p){
				if(to >= toend){
					errtoolong(rp, sp, toend - to, 2,
				     "more name components but no bytes left");
					goto err;
				}
				*to++ = '.';
			}
			break;
		case 0100:		/* edns extended label type, rfc 6891 */
			/*
			 * treat it like an EOF for now; it seems to be at
			 * the end of a long tcp reply.
			 */
			dnslog("edns label; first byte 0%o = '%c'", *p, *p);
			sp->stop = 1;
			goto err;
		case 0200:		/* reserved */
			sp->err = "reserved-use label present";
			goto err;
		case 0300:		/* pointer to other spot in message */
			if(pointer++ > 10){
				sp->err = "pointer loop";
				goto err;
			}
			off = (p[0] & 077)<<8 | p[1];
			p = sp->base + off;
			if(p >= sp->ep){
				sp->err = "bad pointer";
				goto err;
			}
			n = 0;
			break;
		}
	}
	*to = 0;
	if(pointer)
		sp->p += len + 2;	/* + 2 for pointer */
	else
		sp->p += len + 1;	/* + 1 for the null domain */
	return tostart;
err:
	*tostart = 0;
	return tostart;
}

/*
 * ms windows 2000 seems to get the bytes backward in the type field
 * of ptr records, so return a format error as feedback.
 */
static ushort
mstypehack(Scan *sp, ushort type, char *where)
{
	if ((uchar)type == 0 && (type>>8) != 0) {
		USED(where);
//		dnslog("%s: byte-swapped type field in ptr rr from win2k",
//			where);
		if (sp->rcode == Rok)
			sp->rcode = Rformat;
		type >>= 8;
	}
	return type;
}

#define NAME(x)		gname(x, rp, sp)
#define SYMBOL(x)	((x) = gsym(rp, sp))
#define STRING(x)	((x) = gstr(rp, sp))
#define USHORT(x)	((x) = gshort(rp, sp))
#define ULONG(x)	((x) = glong(rp, sp))
#define UCHAR(x)	((x) = gchar(rp, sp))
#define V4ADDR(x)	((x) = gv4addr(rp, sp))
#define V6ADDR(x)	((x) = gv6addr(rp, sp))
#define BYTES(x, y)	((y) = gbytes(rp, sp, &(x), len - (sp->p - data)))

/*
 *  convert the next RR from a message
 */
static RR*
convM2RR(Scan *sp, char *what)
{
	int type, class, len, left;
	char *dn;
	char dname[Domlen+1];
	uchar *data;
	RR *rp;
	Txt *t, **l;

retry:
	rp = nil;
	NAME(dname);
	USHORT(type);
	USHORT(class);

	type = mstypehack(sp, type, "convM2RR");
	rp = rralloc(type);
	rp->owner = dnlookup(dname, class, 1);
	rp->type = type;

	ULONG(rp->ttl);
	rp->ttl += now;
	USHORT(len);			/* length of data following */
	data = sp->p;
	assert(data != nil);
	left = sp->ep - sp->p;

	/*
	 * ms windows generates a lot of badly-formatted hints.
	 * hints are only advisory, so don't log complaints about them.
	 * it also generates answers in which p overshoots ep by exactly
	 * one byte; this seems to be harmless, so don't log them either.
	 */
	if (len > left &&
	   !(strcmp(what, "hints") == 0 ||
	     sp->p == sp->ep + 1 && strcmp(what, "answers") == 0))
		errtoolong(rp, sp, left, len, "convM2RR");
	if(sp->err || sp->rcode || sp->stop){
		rrfree(rp);
		return nil;
	}
	/* even if we don't log an error message, truncate length to fit data */
	if (len > left)
		len = left;

	switch(type){
	default:
		/* unknown type, just ignore it */
		sp->p = data + len;
		rrfree(rp);
		goto retry;
	case Thinfo:
		SYMBOL(rp->cpu);
		SYMBOL(rp->os);
		break;
	case Tcname:
	case Tmb:
	case Tmd:
	case Tmf:
	case Tns:
		rp->host = dnlookup(NAME(dname), Cin, 1);
		break;
	case Tmg:
	case Tmr:
		rp->mb  = dnlookup(NAME(dname), Cin, 1);
		break;
	case Tminfo:
		rp->rmb = dnlookup(NAME(dname), Cin, 1);
		rp->mb  = dnlookup(NAME(dname), Cin, 1);
		break;
	case Tmx:
		USHORT(rp->pref);
		dn = NAME(dname);
		rp->host = dnlookup(dn, Cin, 1);
		if(strchr((char *)rp->host, '\n') != nil) {
			dnslog("newline in mx text for %s", dn);
			sp->trunc = 1;		/* try again via tcp */
		}
		break;
	case Ta:
		V4ADDR(rp->ip);
		break;
	case Taaaa:
		V6ADDR(rp->ip);
		break;
	case Tptr:
		rp->ptr = dnlookup(NAME(dname), Cin, 1);
		break;
	case Tsoa:
		rp->host = dnlookup(NAME(dname), Cin, 1);
		rp->rmb  = dnlookup(NAME(dname), Cin, 1);
		ULONG(rp->soa->serial);
		ULONG(rp->soa->refresh);
		ULONG(rp->soa->retry);
		ULONG(rp->soa->expire);
		ULONG(rp->soa->minttl);
		break;
	case Tsrv:
		USHORT(rp->srv->pri);
		USHORT(rp->srv->weight);
		USHORT(rp->port);
		/*
		 * rfc2782 sez no name compression but to be
		 * backward-compatible with rfc2052, we try to expand the name. 
		 * if the length is under 64 bytes, either interpretation is
		 * fine; if it's longer, we'll assume it's compressed,
		 * as recommended by rfc3597.
		 */
		rp->host = dnlookup(NAME(dname), Cin, 1);
		break;
	case Ttxt:
		l = &rp->txt;
		*l = nil;
		while(sp->p - data < len){
			STRING(t);
			*l = t;
			l = &t->next;
		}
		break;
	case Tnull:
		BYTES(rp->null->data, rp->null->dlen);
		break;
	case Trp:
		rp->rmb = dnlookup(NAME(dname), Cin, 1);
		rp->rp  = dnlookup(NAME(dname), Cin, 1);
		break;
	case Tkey:
		USHORT(rp->key->flags);
		UCHAR(rp->key->proto);
		UCHAR(rp->key->alg);
		BYTES(rp->key->data, rp->key->dlen);
		break;
	case Tsig:
		USHORT(rp->sig->type);
		UCHAR(rp->sig->alg);
		UCHAR(rp->sig->labels);
		ULONG(rp->sig->ttl);
		ULONG(rp->sig->exp);
		ULONG(rp->sig->incep);
		USHORT(rp->sig->tag);
		rp->sig->signer = dnlookup(NAME(dname), Cin, 1);
		BYTES(rp->sig->data, rp->sig->dlen);
		break;
	case Tcert:
		USHORT(rp->cert->type);
		USHORT(rp->cert->tag);
		UCHAR(rp->cert->alg);
		BYTES(rp->cert->data, rp->cert->dlen);
		break;
	}
	if(sp->p - data != len) {
		char ptype[64];

		/*
		 * ms windows 2000 generates cname queries for reverse lookups
		 * with this particular error.  don't bother logging it.
		 *
		 * server: input error: bad cname RR len (actual 2 != len 0):
		 * 235.9.104.135.in-addr.arpa cname
		 *	235.9.104.135.in-addr.arpa from 135.104.9.235
		 */
		if (type == Tcname && sp->p - data == 2 && len == 0)
			return rp;
		if (len > sp->p - data){
			dnslog("bad %s RR len (%d bytes nominal, %lud actual): %R",
				rrname(type, ptype, sizeof ptype), len,
				sp->p - data, rp);
			rrfree(rp);
			rp = nil;
		}
	}
	// if(rp) dnslog("convM2RR: got %R", rp);
	return rp;
}

/*
 *  convert the next question from a message
 */
static RR*
convM2Q(Scan *sp)
{
	char dname[Domlen+1];
	int type, class;
	RR *rp;

	rp = nil;
	NAME(dname);
	USHORT(type);
	USHORT(class);
	if(sp->err || sp->rcode || sp->stop)
		return nil;

	type = mstypehack(sp, type, "convM2Q");
	rp = rralloc(type);
	rp->owner = dnlookup(dname, class, 1);

	return rp;
}

static RR*
rrloop(Scan *sp, char *what, int count, int quest)
{
	int i;
	RR *first, *rp, **l;

	if(sp->err || sp->rcode || sp->stop)
		return nil;
	l = &first;
	first = nil;
	for(i = 0; i < count; i++){
		rp = quest? convM2Q(sp): convM2RR(sp, what);
		if(rp == nil)
			break;
		setmalloctag(rp, getcallerpc(&sp));
		/*
		 * it might be better to ignore the bad rr, possibly break out,
		 * but return the previous rrs, if any.  that way our callers
		 * would know that they had got a response, however ill-formed.
		 */
		if(sp->err || sp->rcode || sp->stop){
			rrfree(rp);
			break;
		}
		*l = rp;
		l = &rp->next;
	}
//	if(first)
//		setmalloctag(first, getcallerpc(&sp));
	return first;
}

/*
 *  convert the next DNS from a message stream.
 *  if there are formatting errors or the like during parsing of the message,
 *  set *codep to the outgoing response code (e.g., Rformat), which will
 *  abort processing and reply immediately with the outgoing response code.
 *
 *  ideally would note if len == Maxpayload && query was via UDP, for errtoolong.
 */
char*
convM2DNS(uchar *buf, int len, DNSmsg *m, int *codep)
{
	char *err = nil;
	RR *rp = nil;
	Scan scan;
	Scan *sp;

	assert(len >= 0);
	assert(buf != nil);
	sp = &scan;
	memset(sp, 0, sizeof *sp);
	sp->base = sp->p = buf;
	sp->ep = buf + len;
	sp->err = nil;
	sp->errbuf[0] = '\0';
	sp->rcode = Rok;

	memset(m, 0, sizeof *m);
	USHORT(m->id);
	USHORT(m->flags);
	USHORT(m->qdcount);
	USHORT(m->ancount);
	USHORT(m->nscount);
	USHORT(m->arcount);

	m->qd = rrloop(sp, "questions",	m->qdcount, 1);
	m->an = rrloop(sp, "answers",	m->ancount, 0);
	m->ns = rrloop(sp, "nameservers",m->nscount, 0);
	if (sp->stop)
		sp->err = nil;
	if (sp->err)
		err = strdup(sp->err);		/* live with bad ar's */
	m->ar = rrloop(sp, "hints",	m->arcount, 0);
	if (sp->trunc)
		m->flags |= Ftrunc;
	if (sp->stop)
		sp->rcode = Rok;
	if (codep)
		*codep = sp->rcode;
	return err;
}
