/* This probably doesn't work on anything other then Ethernet! It may work
   on PLIP as well, but ARCnet and Token Ring are unlikely at best. */

#include <errno.h>
#include <net/if.h>
#include <net/route.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

#include "bootpc.h"
#include "net.h"

#define NUM_RETRIES	3

#ifdef STANDALONE
#define _(foo) ((foo))
#include <stdarg.h>
#else
#include "log.h"
#include "intl.h"
#endif

#ifdef STANDALONE
void logMessage(char * mess, ...) {
    va_list args;

    va_start(args, mess);
    vprintf(mess, args);
    va_end(args);
    puts("");
}
#endif

typedef int bp_int32;
typedef short bp_int16;

#define BOOTP_OPTION_NETMASK		1
#define BOOTP_OPTION_GATEWAY		3
#define BOOTP_OPTION_DNS		6
#define BOOTP_OPTION_HOSTNAME		12
#define BOOTP_OPTION_BOOTFILE		13
#define BOOTP_OPTION_DOMAIN		15
#define BOOTP_OPTION_BROADCAST		28

#define DHCP_OPTION_REQADDR		50
#define DHCP_OPTION_LEASE		51
#define DHCP_OPTION_OVERLOAD		52
#define DHCP_OPTION_TYPE		53
#define DHCP_OPTION_SERVER		54
#define DHCP_OPTION_T1			58

#define BOOTP_CLIENT_PORT	68
#define BOOTP_SERVER_PORT	67

#define BOOTP_OPCODE_REQUEST	1
#define BOOTP_OPCODE_REPLY	2

#define DHCP_TYPE_DISCOVER	1
#define DHCP_TYPE_OFFER		2
#define DHCP_TYPE_REQUEST	3
#define DHCP_TYPE_DECLINE	4
#define DHCP_TYPE_ACK		5
#define DHCP_TYPE_NAK		6
#define DHCP_TYPE_RELEASE	7
#define DHCP_TYPE_INFORM	8

#define BOOTP_VENDOR_LENGTH	64
#define DHCP_VENDOR_LENGTH	340

struct bootpRequest {
    char opcode;
    char hw;
    char hwlength;
    char hopcount;
    bp_int32 id;
    bp_int16 secs;
    bp_int16 flags;
    bp_int32 ciaddr, yiaddr, server_ip, bootp_gw_ip;
    char hwaddr[16];
    char servername[64];
    char bootfile[128];
    char vendor[DHCP_VENDOR_LENGTH];
} ;

static const char vendCookie[] = { 99, 130, 83, 99, 255 };

static char * perrorstr(char * msg);
static char * setupInterface(char * device, int s);
static void parseReply(struct bootpRequest * breq, struct intfInfo * intf, 
		       struct netInfo * net);
static char * prepareRequest(struct bootpRequest * breq,
			     int sock, char * device);
static void initVendorCodes(struct bootpRequest * breq);
static char * handleTransaction(int s, int timeout, struct bootpRequest * breq,
			        struct bootpRequest * bresp, 
			        struct sockaddr_in * serverAddr,
			        struct sockaddr_in * respondant,
				int dhcpType);
static int dhcpMessageType(struct bootpRequest * response);

static char * setupInterface(char * device, int s) {
    struct sockaddr_in * addrp;
    struct ifreq req;
    struct rtentry route;
    int true = 1;

    addrp = (struct sockaddr_in *) &req.ifr_addr;

    strcpy(req.ifr_name, device);
    addrp->sin_family = AF_INET;
    addrp->sin_port = 0;
    memset(&addrp->sin_addr, 0, sizeof(addrp->sin_addr));
    memset(&route, 0, sizeof(route));
    memcpy(&route.rt_dst, addrp, sizeof(route.rt_dst));

    if (ioctl(s, SIOCSIFADDR, &req))
	return perrorstr("SIOCSIFADDR");

    if (ioctl(s, SIOCSIFNETMASK, &req))
	return perrorstr("SIOCSIFNETMASK");

    /* the broadcast address is 255.255.255.255 */
    memset(&addrp->sin_addr,255 , sizeof(addrp->sin_addr));
    if (ioctl(s, SIOCSIFBRDADDR, &req))
	return perrorstr("SIOCSIFBRDADDR");


    req.ifr_flags = IFF_UP | IFF_BROADCAST | IFF_RUNNING;
    if (ioctl(s, SIOCSIFFLAGS, &req))
	return perrorstr("SIOCSIFFLAGS");

    route.rt_dev = device;
    route.rt_flags = RTF_UP;

    if (ioctl(s, SIOCADDRT, &route)) 
	return perrorstr("SIOCSADDRT");

    if (setsockopt(s, SOL_SOCKET, SO_BROADCAST, &true, 1)) {
	close(s);
	return perrorstr("setsockopt");
    }

    return NULL;
}

static int dhcpMessageType(struct bootpRequest * response) {
    unsigned char * chptr;
    unsigned char option, length;
   
    chptr = response->vendor;

    chptr += 4;
    while (*chptr != 0xFF) {
	option = *chptr++;
	if (!option) continue;
	length = *chptr++;
	if (option == DHCP_OPTION_TYPE)
	    return *chptr;

	chptr += length;
    }

    return -1;
}

void setMissingIpInfo(struct intfInfo * intf) {
    bp_int32 ipNum = *((bp_int32 *) &intf->ip);
    bp_int32 nmNum = *((bp_int32 *) &intf->netmask);
    bp_int32 ipRealNum = ntohl(ipNum);

    if (!(intf->set & INTFINFO_HAS_NETMASK)) {
	if (((ipRealNum & 0xFF000000) >> 24) <= 127)
	    nmNum = 0xFF000000;
	else if (((ipRealNum & 0xFF000000) >> 24) <= 191)
	    nmNum = 0xFFFF0000;
	else 
	    nmNum = 0xFFFFFF00;
	*((bp_int32 *) &intf->netmask) = nmNum = htonl(nmNum);
    }

    if (!(intf->set & INTFINFO_HAS_BROADCAST))
	*((bp_int32 *) &intf->broadcast) = (ipNum & nmNum) | ~(nmNum);

    if (!(intf->set & INTFINFO_HAS_NETWORK))
	*((bp_int32 *) &intf->network) = ipNum & nmNum;

    intf->set |= INTFINFO_HAS_BROADCAST | INTFINFO_HAS_NETWORK | 
		 INTFINFO_HAS_NETMASK;
}

static void parseReply(struct bootpRequest * breq, struct intfInfo * intf, 
		       struct netInfo * net) {
    unsigned int i;
    unsigned char * chptr;
    unsigned char option, length;

    i = ~(INTFINFO_HAS_IP | INTFINFO_HAS_NETMASK | INTFINFO_HAS_NETWORK |
	  INTFINFO_HAS_BROADCAST);
    intf->set &= i;
    intf->manuallySet &= i;

    if (strlen(breq->bootfile)) {
	intf->bootFile = strdup(breq->bootfile);
	intf->set |= INTFINFO_HAS_BOOTFILE;
    } else {
	intf->set &= ~INTFINFO_HAS_BOOTFILE;
    }

    if (breq->server_ip) {
	intf->set |= INTFINFO_HAS_BOOTSERVER;
	memcpy(&intf->bootServer, &breq->server_ip, sizeof(breq->server_ip));
    }

    memcpy(&intf->ip, &breq->yiaddr, 4);
    intf->set |= INTFINFO_HAS_IP;

    chptr = breq->vendor;
    chptr += 4;
    while (*chptr != 0xFF) {
	option = *chptr++;
	if (!option) continue;
	length = *chptr++;

	switch (option) {
	    case BOOTP_OPTION_DNS:
		net->numDns = 0;
		for (i = 0; i < length; i += 4)
		    memcpy(&net->dnsServers[net->numDns++], chptr + i, 4);
		net->set |= NETINFO_HAS_DNS;
		break;

	    case BOOTP_OPTION_NETMASK:
		memcpy(&intf->netmask, chptr, 4);
		intf->set |= INTFINFO_HAS_NETMASK;
		break;

	    case BOOTP_OPTION_DOMAIN:
		net->domain = malloc(length + 1);
		memcpy(net->domain, chptr, length);
		net->domain[length] = '\0';
		net->set |= NETINFO_HAS_DOMAIN;
		break;

	    case BOOTP_OPTION_BROADCAST:
		memcpy(&intf->broadcast, chptr, 4);
		intf->set |= INTFINFO_HAS_BROADCAST;
		break;

	    case BOOTP_OPTION_GATEWAY:
		memcpy(&net->gateway, chptr, 4);
		net->set |= NETINFO_HAS_GATEWAY;
		break;

	    case BOOTP_OPTION_HOSTNAME:
		net->hostname = malloc(length + 1);
		memcpy(net->hostname, chptr, length);
		net->hostname[length] = '\0';
		net->set |= NETINFO_HAS_HOSTNAME;
		break;

	    case BOOTP_OPTION_BOOTFILE:
		/* we ignore this right now */
		break;

	    case DHCP_OPTION_OVERLOAD:
		/* FIXME: we should pay attention to this */
		logMessage("dhcp overload option is currently ignored!");
		break;
	}

	chptr += length;
    }

    setMissingIpInfo(intf);
}

static char * perrorstr(char * msg) {
    static char * err = NULL;
    static int errsize = 0;
    static int newsize;

    newsize = strlen(msg) + strlen(strerror(errno)) + 3;
    if (!errsize) {
	errsize = newsize;
	err = malloc(errsize);
    } else if (errsize < newsize) {
	free(err);
	errsize = newsize;
	err = malloc(errsize);
    } 
 
    sprintf(err, "%s: %s", msg, strerror(errno));

    return err;
}

static void initVendorCodes(struct bootpRequest * breq) {
    memcpy(breq->vendor, vendCookie, sizeof(vendCookie));
}

static char * prepareRequest(struct bootpRequest * breq,
			     int sock, char * device) {
    struct ifreq req;
    int i;

    memset(breq, 0, sizeof(*breq));

    breq->opcode = BOOTP_OPCODE_REQUEST;

    strcpy(req.ifr_name, device);
    if (ioctl(sock, SIOCGIFHWADDR, &req))
	return perrorstr("SIOCSIFHWADDR");

    breq->hw = 1; 		/* ethernet */
    breq->hwlength = IFHWADDRLEN;	
    memcpy(breq->hwaddr, req.ifr_hwaddr.sa_data, IFHWADDRLEN);

    /* we should use something random here, but I don't want to start using
       stuff from the math library */
    breq->id = time(NULL);
    for (i = 0; i < IFHWADDRLEN; i++)
	breq->id ^= breq->hwaddr[i] << (8 * (i % 4));

    breq->hopcount = 0;
    breq->secs = 0;

    initVendorCodes(breq);

    return NULL;
}

static char * handleTransaction(int s, int timeout, struct bootpRequest * breq,
			        struct bootpRequest * bresp, 
			        struct sockaddr_in * serverAddr,
				struct sockaddr_in * respondant,
				int dhcpType) {
    struct timeval tv;
    fd_set readfs;
    int i;
    struct sockaddr_in tmpAddress;
    int gotit = 0;
    int tries = NUM_RETRIES;
    int nextTimeout = 4;

    while (!gotit && tries--) {
	i = sizeof(*breq);
	if (dhcpType == -1)
	    i -= (DHCP_VENDOR_LENGTH - BOOTP_VENDOR_LENGTH);

	if (sendto(s, breq, i, 0, (struct sockaddr *) serverAddr, 
		   sizeof(*serverAddr)) != i) {
	    close(s);
	    return perrorstr("sendto");
	}

	tv.tv_usec = 0;
 	tv.tv_sec = nextTimeout;
	if (tv.tv_sec > timeout) tv.tv_sec = timeout;
	timeout -= tv.tv_sec;
	nextTimeout *= 2;
	switch (time(NULL) & 4) {
	    case 0:	nextTimeout--; break;
	    case 1:	nextTimeout++; break;
	}

	FD_ZERO(&readfs);
	FD_SET(s, &readfs);
	switch ((select(s + 1, &readfs, NULL, NULL, &tv))) {
	  case 0:
	    break;

	  case 1:
	    i = sizeof(tmpAddress);
	    if (recvfrom(s, bresp, sizeof(*bresp), 0, 
		     (struct sockaddr *) &tmpAddress, &i) < 0)
		return perrorstr("recvfrom");

	    /* sanity checks */
	    if (bresp->id != breq->id) continue;
	    if (bresp->opcode != BOOTP_OPCODE_REPLY) continue;
	    if (bresp->hwlength != breq->hwlength) continue;
	    if (memcmp(bresp->hwaddr, breq->hwaddr, bresp->hwlength)) continue;
	    if (dhcpMessageType(bresp) != dhcpType) continue;
	    if (memcmp(bresp->vendor, vendCookie, 4)) continue;
	    if (respondant) *respondant = tmpAddress;
	    gotit = 1;

	    break;

	  default:
	    return perrorstr("select");
	}
    }

    if (!gotit)
    if (dhcpType == -1)
	return _("No BOOTP reply received");
    else
	return _("No DHCP reply received");

    return NULL;
}

static void addVendorCode(struct bootpRequest * breq, unsigned char option,
			  unsigned char length, void * data) {
    unsigned char * chptr;
    int theOption, theLength;

    chptr = breq->vendor;
    chptr += 4;
    while (*chptr != 0xFF && *chptr != option) {
	theOption = *chptr++;
	if (!theOption) continue;
	theLength = *chptr++;
	chptr += theLength;
    }

    *chptr++ = option;
    *chptr++ = length;
    memcpy(chptr, data, length);
    chptr[length] = 0xff;
}

static int getVendorCode(struct bootpRequest * bresp, unsigned char option,
			  void * data) {
    unsigned char * chptr;
    unsigned int length, theOption;

    chptr = bresp->vendor;
    chptr += 4;
    while (*chptr != 0xFF && *chptr != option) {
	theOption = *chptr++;
	if (!theOption) continue;
	length = *chptr++;
	chptr += length;
    }

    if (*chptr++ == 0xff) return 1;

    length = *chptr++;
    memcpy(data, chptr, length);

    return 0;
}

static void hareyCarey(int signo) {
    logMessage("DHCP FAILED TO RENEW LEASE -- KILLING INSTALL");
    kill(getppid(), SIGFPE);
}

/* This is somewhat broken. We try only to renew the lease. If we fail,
   we don't try to completely rebind. This doesn't follow the DHCP spec,
   but for the install it should be a reasonable compromise. */
static void dhcpDaemon(int s, struct bootpRequest * origResult, char * device) {
    unsigned int t1, lease;		/* offsets from "now" */
    time_t now;
    struct bootpRequest breq, bresp;
    unsigned char messageType;
    struct sockaddr_in serverAddr;
    char * chptr;
    int renewLease = 1;

    while (renewLease) {
	now = time(NULL);
	
	if (getVendorCode(origResult, DHCP_OPTION_LEASE, &lease))
	    renewLease = 0;

	if (getVendorCode(origResult, DHCP_OPTION_T1, &t1)) {
	    /* this is equiv to t1 = 0.875 * lease, which is mandated by RFC */
	    t1 = (7 * lease) >> 3;
	}

	t1 = 120;
	/* if this goes off, we have a problem! */
	signal(SIGALRM, hareyCarey);
	alarm((now + lease) - time(NULL));

	/* go to sleep until we're needed again */
	sleep((now + t1) - time(NULL));

	if ((chptr = prepareRequest(&breq, s, device))) {
	    close(s);
	    while (1);	/* problem */
	}

	messageType = DHCP_TYPE_REQUEST;
	addVendorCode(&breq, DHCP_OPTION_TYPE, 1, &messageType);
	breq.ciaddr = origResult->yiaddr;

	serverAddr.sin_family = AF_INET;
	serverAddr.sin_port = htons(BOOTP_SERVER_PORT);	/* bootp server */
	getVendorCode(origResult, DHCP_OPTION_SERVER, &serverAddr.sin_addr);
     
	if ((chptr = handleTransaction(s, 60, &breq, &bresp, &serverAddr,
				       NULL, DHCP_TYPE_ACK))) {
	    renewLease = 0;
	} else {
	    *origResult = bresp;
	}
    }

    #ifdef STANDALONE
	printf("dhcp renewel failed\n");
    #else
	logMessage("DHCP FAILED TO RENEW LEASE -- KILLING INSTALL");
	kill(getppid(), SIGFPE);
    #endif
}

char * doDhcp(int timeout, struct intfInfo * intf, struct netInfo * net,
		int shouldDaemonize) {
    int s, i;
    struct sockaddr_in serverAddr;
    struct sockaddr_in clientAddr;
    struct sockaddr_in broadcastAddr;
    struct bootpRequest breq, bresp;
    unsigned char * chptr;
    unsigned char messageType;
    unsigned int lease;

    s = socket(AF_INET, SOCK_DGRAM, 0);
    if (s < 0) {
	return perrorstr("socket");
    }

    if ((chptr = setupInterface(intf->device, s))) {
	close(s);
	return chptr;
    }

    if ((chptr = prepareRequest(&breq, s, intf->device))) {
	close(s);
	return chptr;
    }

    messageType = DHCP_TYPE_DISCOVER;
    addVendorCode(&breq, DHCP_OPTION_TYPE, 1, &messageType);

    memset(&clientAddr.sin_addr, 0, sizeof(&clientAddr.sin_addr));
    clientAddr.sin_family = AF_INET;
    clientAddr.sin_port = htons(BOOTP_CLIENT_PORT);	/* bootp client */

    if (bind(s, (struct sockaddr *) &clientAddr, sizeof(clientAddr))) {
	return perrorstr("bind");
    }

    broadcastAddr.sin_family = AF_INET;
    broadcastAddr.sin_port = htons(BOOTP_SERVER_PORT);	/* bootp server */
    memset(&broadcastAddr.sin_addr, 0xff, 
	   sizeof(broadcastAddr.sin_addr));  /* broadcast */

    if ((chptr = handleTransaction(s, timeout, &breq, &bresp, &broadcastAddr,
				   NULL, DHCP_TYPE_OFFER))) {
	close(s);
	return chptr;
    }

    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(BOOTP_SERVER_PORT);	/* bootp server */
    if (getVendorCode(&bresp, DHCP_OPTION_SERVER, &serverAddr.sin_addr)) {
	close(s);
	return "DHCPOFFER didn't include server address";
    }

    initVendorCodes(&breq);
    messageType = DHCP_TYPE_REQUEST;
    addVendorCode(&breq, DHCP_OPTION_TYPE, 1, &messageType);
    addVendorCode(&breq, DHCP_OPTION_SERVER, 4, &serverAddr.sin_addr);
    addVendorCode(&breq, DHCP_OPTION_REQADDR, 4, &bresp.yiaddr);

    /* request a lease of 1 hour */
    i = htonl(60 * 60);
    addVendorCode(&breq, DHCP_OPTION_LEASE, 4, &i);
    
    if ((chptr = handleTransaction(s, timeout, &breq, &bresp, &broadcastAddr,
				   NULL, DHCP_TYPE_ACK))) {
	close(s);
	return chptr;
    }

    if (getVendorCode(&bresp, DHCP_OPTION_LEASE, &lease))
	return "failed to get lease time\n";
    lease = ntohl(lease);

    if (lease && lease != 0xffffffff && shouldDaemonize) {
#ifndef STANDALONE
	if (!fork())
#endif
	    dhcpDaemon(s, &bresp, intf->device);
    }

    close(s);

    parseReply(&bresp, intf, net);

    if (!(intf->set & INTFINFO_HAS_BOOTSERVER)) {
	intf->bootServer = serverAddr.sin_addr;
	intf->set |= INTFINFO_HAS_BOOTSERVER;
    }

    return NULL;
}

char * doBootp(int timeout, struct intfInfo * intf, struct netInfo * net) {
    int s;
    struct sockaddr_in clientAddr;
    struct sockaddr_in broadcastAddr;
    struct sockaddr_in serverAddr;
    struct bootpRequest breq, bresp;
    unsigned char * chptr;

    s = socket(AF_INET, SOCK_DGRAM, 0);
    if (s < 0) {
	return perrorstr("socket");
    }

    if ((chptr = setupInterface(intf->device, s))) {
	close(s);
	return chptr;
    }

    if ((chptr = prepareRequest(&breq, s, intf->device))) {
	close(s);
	return chptr;
    }

    memset(&clientAddr.sin_addr, 0, sizeof(&clientAddr.sin_addr));
    clientAddr.sin_family = AF_INET;
    clientAddr.sin_port = htons(BOOTP_CLIENT_PORT);	/* bootp client */

    if (bind(s, (struct sockaddr *) &clientAddr, sizeof(clientAddr))) {
	return perrorstr("bind");
    }

    broadcastAddr.sin_family = AF_INET;
    broadcastAddr.sin_port = htons(BOOTP_SERVER_PORT);	/* bootp server */
    memset(&broadcastAddr.sin_addr, 0xff, 
	   sizeof(broadcastAddr.sin_addr));  /* broadcast */

    if ((chptr = handleTransaction(s, timeout, &breq, &bresp, &broadcastAddr,
				   &serverAddr, -1))) {
	close(s);
	return chptr;
    }

    close(s);

    parseReply(&bresp, intf, net);

    if (!(intf->set & INTFINFO_HAS_BOOTSERVER)) {
	intf->bootServer = serverAddr.sin_addr;
	intf->set |= INTFINFO_HAS_BOOTSERVER;
    }

    return NULL;
}

#ifdef STANDALONE
void main (void) {
    char * msg;
    struct intfInfo intf;
    struct netInfo net;

    memset(&intf, 0, sizeof(intf));
    memset(&net, 0, sizeof(net));
    strcpy(intf.device, "eth0");

#ifndef USEBOOTP
    msg = doDhcp(30, &intf, &net);
#else
    msg = doBootp(30, &intf, &net);
#endif

    if (msg) {
	 puts(msg);
	 exit(1);
    }

    printf("got address %s\n", inet_ntoa(intf.ip));
    if (intf.set & INTFINFO_HAS_NETMASK)
	printf("got netmask %s\n", inet_ntoa(intf.netmask));
    if (net.set & NETINFO_HAS_DOMAIN)
	printf("got domain %s\n", net.domain);
}
#endif