/*
 * BOOTP packet protocol handling.
 */
#include <sys/types.h>
#include <sys/uio.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <netinet/in.h>

#include "ipconfig.h"
#include "netdev.h"
#include "bootp_packet.h"
#include "bootp_proto.h"
#include "packet.h"

static uint8_t bootp_options[312] = {
	[  0] = 99, 130, 83, 99,/* RFC1048 magic cookie */
	[  4] = 1, 4,		/*   4-  9 subnet mask */
	[ 10] = 3, 4,		/*  10- 15 default gateway */
	[ 16] = 5, 8,		/*  16- 25 nameserver */
	[ 26] = 12, 32,		/*  26- 59 host name */
	[ 60] = 40, 32,		/*  60- 95 nis domain name */
	[ 96] = 17, 40,		/*  96-137 boot path */
	[138] = 57, 2, 1, 150,	/* 138-141 extension buffer */
	[142] = 255,		/* end of list */
};

/*
 * Send a plain bootp request packet with options
 */
int bootp_send_request(struct netdev *dev)
{
	struct bootp_hdr bootp;
	struct iovec iov[] = {
		/* [0] = ip + udp headers */
		[1] = {&bootp, sizeof(bootp)},
		[2] = {bootp_options, 312}
	};

	memset(&bootp, 0, sizeof(struct bootp_hdr));

	bootp.op	= BOOTP_REQUEST, bootp.htype = dev->hwtype;
	bootp.hlen	= dev->hwlen;
	bootp.xid	= dev->bootp.xid;
	bootp.ciaddr	= dev->ip_addr;
	bootp.secs	= htons(time(NULL) - dev->open_time);
	memcpy(bootp.chaddr, dev->hwaddr, 16);

	dprintf("-> bootp xid 0x%08x secs 0x%08x ",
		bootp.xid, ntohs(bootp.secs));

	return packet_send(dev, iov, 2);
}

/*
 * DESCRIPTION
 *  bootp_ext119_decode() decodes Domain Search Option data.
 *  The decoded string is separated with ' '.
 *  For example, it is either "foo.bar.baz. bar.baz.", "foo.bar.", or "foo.".
 *
 * ARGUMENTS
 *  const uint8_t *ext
 *   *ext is a pointer to a DHCP Domain Search Option data. *ext does not
 *   include a tag(code) octet and a length octet in DHCP options.
 *   For example, if *ext is {3, 'f', 'o', 'o', 0}, this function returns
 *   a pointer to a "foo." string.
 *
 *  int16_t ext_size
 *   ext_size is the memory size of *ext. For example,
 *   if *ext is {3, 'f', 'o', 'o', 0}, ext_size must be 5.
 *
 *  uint8_t *tmp
 *   *tmp is a pointer to a temporary memory space for decoding.
 *   The memory size must be equal to or more than ext_size.
 *   'memset(tmp, 0, sizeof(tmp));' is not required, but values in *tmp
 *   are changed in decoding process.
 *
 * RETURN VALUE
 *  if OK, a pointer to a decoded string malloc-ed
 *  else , NULL
 *
 * SEE ALSO RFC3397
 */
static char *bootp_ext119_decode(const void *ext, int16_t ext_size, void *tmp)
{
	uint8_t *u8ext;
	int_fast32_t i;
	int_fast32_t decoded_size;
	int_fast8_t currentdomain_is_singledot;

	/* only for validating *ext */
	uint8_t *is_pointee;
	int_fast32_t is_pointee_size;

	/* only for structing a decoded string */
	char *decoded_str;
	int_fast32_t dst_i;

	if (ext == NULL || ext_size <= 0 || tmp == NULL)
		return NULL;

	u8ext = (uint8_t *)ext;
	is_pointee = tmp;
	memset(is_pointee, 0, (size_t)ext_size);
	is_pointee_size = 0;

	/*
	 * validate the format of *ext and
	 * calculate the memory size for a decoded string
	 */
	i = 0;
	decoded_size = 0;
	currentdomain_is_singledot = 1;
	while (1) {
		if (i >= ext_size)
			return NULL;

		if (u8ext[i] == 0) {
			/* Zero-ending */
			if (currentdomain_is_singledot)
				decoded_size++; /* for '.' */
			decoded_size++; /* for ' ' or '\0' */
			currentdomain_is_singledot = 1;
			i++;
			if (i == ext_size)
				break;
			is_pointee_size = i;
		} else if (u8ext[i] < 0x40) {
			/* Label(sub-domain string) */
			int j;

			/* loosely validate characters for domain names */
			if (i + u8ext[i] >= ext_size)
				return NULL;
			for (j = i + 1; j <= i + u8ext[i]; j++)
				if (!(u8ext[j] == '-' ||
				     ('0' <= u8ext[j] && u8ext[j] <= '9') ||
				     ('A' <= u8ext[j] && u8ext[j] <= 'Z') ||
				     ('a' <= u8ext[j] && u8ext[j] <= 'z')))
					return NULL;

			is_pointee[i] = 1;
			decoded_size += u8ext[i] + 1; /* for Label + '.' */
			currentdomain_is_singledot = 0;
			i += u8ext[i] + 1;
		} else if (u8ext[i] < 0xc0)
			return NULL;

		else {
			/* Compression-pointer (to a prior Label) */
			int_fast32_t p;

			if (i + 1 >= ext_size)
				return NULL;

			p = ((0x3f & u8ext[i]) << 8) + u8ext[i + 1];
			if (!(p < is_pointee_size && is_pointee[p]))
				return NULL;

			while (1) {
				/* u8ext[p] was validated */
				if (u8ext[p] == 0) {
					/* Zero-ending */
					decoded_size++;
					break;
				} else if (u8ext[p] < 0x40) {
					/* Label(sub-domain string) */
					decoded_size += u8ext[p] + 1;
					p += u8ext[p] + 1;
				} else {
					/* Compression-pointer */
					p = ((0x3f & u8ext[p]) << 8)
						+ u8ext[p + 1];
				}
			}

			currentdomain_is_singledot = 1;
			i += 2;
			if (i == ext_size)
				break;
			is_pointee_size = i;
		}
	}


	/*
	 * construct a decoded string
	 */
	decoded_str = malloc(decoded_size);
	if (decoded_str == NULL)
		return NULL;

	i = 0;
	dst_i = 0;
	currentdomain_is_singledot = 1;
	while (1) {
		if (u8ext[i] == 0) {
			/* Zero-ending */
			if (currentdomain_is_singledot) {
				if (dst_i != 0)
					dst_i++;
				decoded_str[dst_i] = '.';
			}
			dst_i++;
			decoded_str[dst_i] = ' ';

			currentdomain_is_singledot = 1;
			i++;
			if (i == ext_size)
				break;
		} else if (u8ext[i] < 0x40) {
			/* Label(sub-domain string) */
			if (dst_i != 0)
				dst_i++;
			memcpy(&decoded_str[dst_i], &u8ext[i + 1],
			       (size_t)u8ext[i]);
			dst_i += u8ext[i];
			decoded_str[dst_i] = '.';

			currentdomain_is_singledot = 0;
			i += u8ext[i] + 1;
		} else {
			/* Compression-pointer (to a prior Label) */
			int_fast32_t p;

			p = ((0x3f & u8ext[i]) << 8) + u8ext[i + 1];
			while (1) {
				if (u8ext[p] == 0) {
					/* Zero-ending */
					decoded_str[dst_i++] = '.';
					decoded_str[dst_i] = ' ';
					break;
				} else if (u8ext[p] < 0x40) {
					/* Label(sub-domain string) */
					dst_i++;
					memcpy(&decoded_str[dst_i],
					       &u8ext[p + 1],
					       (size_t)u8ext[p]);
					dst_i += u8ext[p];
					decoded_str[dst_i] = '.';

					p += u8ext[p] + 1;
				} else {
					/* Compression-pointer */
					p = ((0x3f & u8ext[p]) << 8)
						+ u8ext[p + 1];
				}
			}

			currentdomain_is_singledot = 1;
			i += 2;
			if (i == ext_size)
				break;
		}
	}
	decoded_str[dst_i] = '\0';
#ifdef DEBUG
	if (dst_i + 1 != decoded_size) {
		dprintf("bug:%s():bottom: malloc(%ld), write(%ld)\n",
			__func__, (long)decoded_size, (long)(dst_i + 1));
		exit(1);
	}
#endif
	return decoded_str;
}

/*
 * Parse a bootp reply packet
 */
int bootp_parse(struct netdev *dev, struct bootp_hdr *hdr,
		uint8_t *exts, int extlen)
{
	uint8_t ext119_buf[BOOTP_EXTS_SIZE];
	int16_t ext119_len = 0;

	dev->bootp.gateway	= hdr->giaddr;
	dev->ip_addr		= hdr->yiaddr;
	dev->ip_server		= hdr->siaddr;
	dev->ip_netmask		= INADDR_ANY;
	dev->ip_broadcast	= INADDR_ANY;
	dev->ip_gateway		= hdr->giaddr;
	dev->ip_nameserver[0]	= INADDR_ANY;
	dev->ip_nameserver[1]	= INADDR_ANY;
	dev->hostname[0]	= '\0';
	dev->nisdomainname[0]	= '\0';
	dev->bootpath[0]	= '\0';
	memcpy(&dev->filename, &hdr->boot_file, FNLEN);

	if (extlen >= 4 && exts[0] == 99 && exts[1] == 130 &&
	    exts[2] == 83 && exts[3] == 99) {
		uint8_t *ext;

		for (ext = exts + 4; ext - exts < extlen;) {
			int len;
			uint8_t opt = *ext++;

			if (opt == 0)
				continue;
			else if (opt == 255)
				break;

			if (ext - exts >= extlen)
				break;
			len = *ext++;

			if (ext - exts + len > extlen)
				break;
			switch (opt) {
			case 1:	/* subnet mask */
				if (len == 4)
					memcpy(&dev->ip_netmask, ext, 4);
				break;
			case 3:	/* default gateway */
				if (len >= 4)
					memcpy(&dev->ip_gateway, ext, 4);
				break;
			case 6:	/* DNS server */
				if (len >= 4)
					memcpy(&dev->ip_nameserver, ext,
					       len >= 8 ? 8 : 4);
				break;
			case 12:	/* host name */
				if (len > sizeof(dev->hostname) - 1)
					len = sizeof(dev->hostname) - 1;
				memcpy(&dev->hostname, ext, len);
				dev->hostname[len] = '\0';
				break;
			case 15:	/* domain name */
				if (len > sizeof(dev->dnsdomainname) - 1)
					len = sizeof(dev->dnsdomainname) - 1;
				memcpy(&dev->dnsdomainname, ext, len);
				dev->dnsdomainname[len] = '\0';
				break;
			case 17:	/* root path */
				if (len > sizeof(dev->bootpath) - 1)
					len = sizeof(dev->bootpath) - 1;
				memcpy(&dev->bootpath, ext, len);
				dev->bootpath[len] = '\0';
				break;
			case 26:	/* interface MTU */
				if (len == 2)
					dev->mtu = (ext[0] << 8) + ext[1];
				break;
			case 28:	/* broadcast addr */
				if (len == 4)
					memcpy(&dev->ip_broadcast, ext, 4);
				break;
			case 40:	/* NIS domain name */
				if (len > sizeof(dev->nisdomainname) - 1)
					len = sizeof(dev->nisdomainname) - 1;
				memcpy(&dev->nisdomainname, ext, len);
				dev->nisdomainname[len] = '\0';
				break;
			case 54:	/* server identifier */
				if (len == 4 && !dev->ip_server)
					memcpy(&dev->ip_server, ext, 4);
				break;
			case 119:	/* Domain Search Option */
				if (ext119_len >= 0 &&
				    ext119_len + len <= sizeof(ext119_buf)) {
					memcpy(ext119_buf + ext119_len,
					       ext, len);
					ext119_len += len;
				} else
					ext119_len = -1;

				break;
			}

			ext += len;
		}
	}
	if (ext119_len > 0) {
		char *ret;
		uint8_t ext119_tmp[BOOTP_EXTS_SIZE];

		ret = bootp_ext119_decode(ext119_buf, ext119_len, ext119_tmp);
		if (ret != NULL) {
			if (dev->domainsearch != NULL)
				free(dev->domainsearch);
			dev->domainsearch = ret;
		}
	}

	/*
	 * Got packet.
	 */
	return 1;
}

/*
 * Receive a bootp reply and parse packet
 * Returns:
 *-1 = Error in packet_recv, try again later
 * 0 = Unexpected packet, discarded
 * 1 = Correctly received and parsed packet
 */
int bootp_recv_reply(struct netdev *dev)
{
	struct bootp_hdr bootp;
	uint8_t bootp_options[BOOTP_EXTS_SIZE];
	struct iovec iov[] = {
		/* [0] = ip + udp headers */
		[1] = {&bootp, sizeof(struct bootp_hdr)},
		[2] = {bootp_options, sizeof(bootp_options)}
	};
	int ret;

	ret = packet_recv(dev, iov, 3);
	if (ret <= 0)
		return ret;

	if (ret < sizeof(struct bootp_hdr) ||
	    bootp.op != BOOTP_REPLY ||	/* RFC951 7.5 */
	    bootp.xid != dev->bootp.xid ||
	    memcmp(bootp.chaddr, dev->hwaddr, 16))
		return 0;

	ret -= sizeof(struct bootp_hdr);

	return bootp_parse(dev, &bootp, bootp_options, ret);
}

/*
 * Initialise interface for bootp.
 */
int bootp_init_if(struct netdev *dev)
{
	short flags;

	/*
	 * Get the device flags
	 */
	if (netdev_getflags(dev, &flags))
		return -1;

	/*
	 * We can't do DHCP nor BOOTP if this device
	 * doesn't support broadcast.
	 */
	if (dev->mtu < 364 || (flags & IFF_BROADCAST) == 0) {
		dev->caps &= ~(CAP_BOOTP | CAP_DHCP);
		return 0;
	}

	/*
	 * Get a random XID
	 */
	dev->bootp.xid = (uint32_t) lrand48();
	dev->open_time = time(NULL);

	return 0;
}
