#include <stdio.h>
#include <string.h>
#include <core.h>
#include "pxe.h"

/* DNS CLASS values we care about */
#define CLASS_IN	1

/* DNS TYPE values we care about */
#define TYPE_A		1
#define TYPE_CNAME	5

/*
 * The DNS header structure
 */
struct dnshdr {
    uint16_t id;
    uint16_t flags;
    /* number of entries in the question section */
    uint16_t qdcount;
    /* number of resource records in the answer section */
    uint16_t ancount;
    /* number of name server resource records in the authority records section*/
    uint16_t nscount;
    /* number of resource records in the additional records section */
    uint16_t arcount;
} __attribute__ ((packed));

/*
 * The DNS query structure
 */
struct dnsquery {
    uint16_t qtype;
    uint16_t qclass;
} __attribute__ ((packed));

/*
 * The DNS Resource recodes structure
 */
struct dnsrr {
    uint16_t type;
    uint16_t class;
    uint32_t ttl;
    uint16_t rdlength;   /* The lenght of this rr data */
    char     rdata[];
} __attribute__ ((packed));


uint32_t dns_server[DNS_MAX_SERVERS] = {0, };


/*
 * Turn a string in _src_ into a DNS "label set" in _dst_; returns the
 * number of dots encountered. On return, *dst is updated.
 */
int dns_mangle(char **dst, const char *p)
{
    char *q = *dst;
    char *count_ptr;
    char c;
    int dots = 0;

    count_ptr = q;
    *q++ = 0;

    while (1) {
        c = *p++;
        if (c == 0 || c == ':' || c == '/')
            break;
        if (c == '.') {
            dots++;
            count_ptr = q;
            *q++ = 0;
            continue;
        }

        *count_ptr += 1;
        *q++ = c;
    }

    if (*count_ptr)
        *q++ = 0;

    /* update the strings */
    *dst = q;
    return dots;
}


/*
 * Compare two sets of DNS labels, in _s1_ and _s2_; the one in _s2_
 * is allowed pointers relative to a packet in buf.
 *
 */
static bool dns_compare(const void *s1, const void *s2, const void *buf)
{
    const uint8_t *q = s1;
    const uint8_t *p = s2;
    unsigned int c0, c1;

    while (1) {
	c0 = p[0];
        if (c0 >= 0xc0) {
	    /* Follow pointer */
	    c1 = p[1];
	    p = (const uint8_t *)buf + ((c0 - 0xc0) << 8) + c1;
	} else if (c0) {
	    c0++;		/* Include the length byte */
	    if (memcmp(q, p, c0))
		return false;
	    q += c0;
	    p += c0;
	} else {
	    return *q == 0;
	}
    }
}

/*
 * Copy a DNS label into a buffer, considering the possibility that we might
 * have to follow pointers relative to "buf".
 * Returns a pointer to the first free byte *after* the terminal null.
 */
static void *dns_copylabel(void *dst, const void *src, const void *buf)
{
    uint8_t *q = dst;
    const uint8_t *p = src;
    unsigned int c0, c1;

    while (1) {
	c0 = p[0];
        if (c0 >= 0xc0) {
	    /* Follow pointer */
	    c1 = p[1];
	    p = (const uint8_t *)buf + ((c0 - 0xc0) << 8) + c1;
	} else if (c0) {
	    c0++;		/* Include the length byte */
	    memcpy(q, p, c0);
	    p += c0;
	    q += c0;
	} else {
	    *q++ = 0;
	    return q;
	}
    }
}

/*
 * Skip past a DNS label set in DS:SI
 */
static char *dns_skiplabel(char *label)
{
    uint8_t c;

    while (1) {
        c = *label++;
        if (c >= 0xc0)
            return ++label; /* pointer is two bytes */
        if (c == 0)
            return label;
        label += c;
    }
}

/*
 * Actual resolver function
 * Points to a null-terminated or :-terminated string in _name_
 * and returns the ip addr in _ip_ if it exists and can be found.
 * If _ip_ = 0 on exit, the lookup failed. _name_ will be updated
 *
 * XXX: probably need some caching here.
 */
uint32_t dns_resolv(const char *name)
{
    static char __lowmem DNSSendBuf[PKTBUF_SIZE];
    static char __lowmem DNSRecvBuf[PKTBUF_SIZE];
    char *p;
    int err;
    int dots;
    int same;
    int rd_len;
    int ques, reps;    /* number of questions and replies */
    uint8_t timeout;
    const uint8_t *timeout_ptr = TimeoutTable;
    uint32_t oldtime;
    uint32_t srv;
    uint32_t *srv_ptr;
    struct dnshdr *hd1 = (struct dnshdr *)DNSSendBuf;
    struct dnshdr *hd2 = (struct dnshdr *)DNSRecvBuf;
    struct dnsquery *query;
    struct dnsrr *rr;
    static __lowmem struct s_PXENV_UDP_WRITE udp_write;
    static __lowmem struct s_PXENV_UDP_READ  udp_read;
    uint16_t local_port;
    uint32_t result = 0;

    /* Make sure we have at least one valid DNS server */
    if (!dns_server[0])
	return 0;

    /* Get a local port number */
    local_port = get_port();

    /* First, fill the DNS header struct */
    hd1->id++;                      /* New query ID */
    hd1->flags   = htons(0x0100);   /* Recursion requested */
    hd1->qdcount = htons(1);        /* One question */
    hd1->ancount = 0;               /* No answers */
    hd1->nscount = 0;               /* No NS */
    hd1->arcount = 0;               /* No AR */

    p = DNSSendBuf + sizeof(struct dnshdr);
    dots = dns_mangle(&p, name);   /* store the CNAME */

    if (!dots) {
        p--; /* Remove final null */
        /* Uncompressed DNS label set so it ends in null */
        strcpy(p, LocalDomain);
    }

    /* Fill the DNS query packet */
    query = (struct dnsquery *)p;
    query->qtype  = htons(TYPE_A);
    query->qclass = htons(CLASS_IN);
    p += sizeof(struct dnsquery);

    /* Now send it to name server */
    timeout_ptr = TimeoutTable;
    timeout = *timeout_ptr++;
    srv_ptr = dns_server;
    while (timeout) {
	srv = *srv_ptr++;
	if (!srv) {
	    srv_ptr = dns_server;
	    srv = *srv_ptr++;
	}

        udp_write.status      = 0;
        udp_write.ip          = srv;
        udp_write.gw          = gateway(srv);
        udp_write.src_port    = local_port;
        udp_write.dst_port    = DNS_PORT;
        udp_write.buffer_size = p - DNSSendBuf;
        udp_write.buffer      = FAR_PTR(DNSSendBuf);
        err = pxe_call(PXENV_UDP_WRITE, &udp_write);
        if (err || udp_write.status)
            continue;

        oldtime = jiffies();
	do {
	    if (jiffies() - oldtime >= timeout)
		goto again;

            udp_read.status      = 0;
            udp_read.src_ip      = srv;
            udp_read.dest_ip     = IPInfo.myip;
            udp_read.s_port      = DNS_PORT;
            udp_read.d_port      = local_port;
            udp_read.buffer_size = PKTBUF_SIZE;
            udp_read.buffer      = FAR_PTR(DNSRecvBuf);
            err = pxe_call(PXENV_UDP_READ, &udp_read);
	} while (err || udp_read.status || hd2->id != hd1->id);

        if ((hd2->flags ^ 0x80) & htons(0xf80f))
            goto badness;

        ques = htons(hd2->qdcount);   /* Questions */
        reps = htons(hd2->ancount);   /* Replies   */
        p = DNSRecvBuf + sizeof(struct dnshdr);
        while (ques--) {
            p = dns_skiplabel(p); /* Skip name */
            p += 4;               /* Skip question trailer */
        }

        /* Parse the replies */
        while (reps--) {
            same = dns_compare(DNSSendBuf + sizeof(struct dnshdr),
			       p, DNSRecvBuf);
            p = dns_skiplabel(p);
            rr = (struct dnsrr *)p;
            rd_len = ntohs(rr->rdlength);
            if (same && ntohs(rr->class) == CLASS_IN) {
		switch (ntohs(rr->type)) {
		case TYPE_A:
		    if (rd_len == 4) {
			result = *(uint32_t *)rr->rdata;
			goto done;
		    }
		    break;
		case TYPE_CNAME:
		    dns_copylabel(DNSSendBuf + sizeof(struct dnshdr),
				  rr->rdata, DNSRecvBuf);
		    /*
		     * We should probably rescan the packet from the top
		     * here, and technically we might have to send a whole
		     * new request here...
		     */
		    break;
		default:
		    break;
		}
	    }

            /* not the one we want, try next */
            p += sizeof(struct dnsrr) + rd_len;
        }

    badness:
        /*
         *
         ; We got back no data from this server.
         ; Unfortunately, for a recursive, non-authoritative
         ; query there is no such thing as an NXDOMAIN reply,
         ; which technically means we can't draw any
         ; conclusions.  However, in practice that means the
         ; domain doesn't exist.  If this turns out to be a
         ; problem, we may want to add code to go through all
         ; the servers before giving up.

         ; If the DNS server wasn't capable of recursion, and
         ; isn't capable of giving us an authoritative reply
         ; (i.e. neither AA or RA set), then at least try a
         ; different setver...
        */
        if (hd2->flags == htons(0x480))
            continue;

        break; /* failed */

    again:
	continue;
    }

done:
    free_port(local_port);	/* Return port number to the free pool */

    return result;
}


/*
 * the one should be called from ASM file
 */
void pxe_dns_resolv(com32sys_t *regs)
{
    const char *name = MK_PTR(regs->ds, regs->esi.w[0]);

    regs->eax.l = dns_resolv(name);
}
