/* vim: set filetype=c:
 *
 **************************************************************************
 *
 * Boot-ROM-Code to load an operating system across a TCP/IP network.
 *
 * Module:  bootp.c86
 * Purpose: Get information for client with BOOTP/DHCP protocol
 * Entries: bootp
 *
 **************************************************************************
 *
 * Copyright (C) 1995-2007 Gero Kuhlmann <gero@gkminix.han.de>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * $Id: bootp.c86,v 1.14 2007/02/01 12:09:20 gkminix Exp $
 *
 **************************************************************************
 *
 * We can print error messages within this module as it gets never called
 * by a PXE function.
 */


#include <common.h86>
#include <romconfig.h86>
#include <i386/string.h86>
#include <i386/conio.h86>
#include <i386/misc.h86>
#include <i386/time.h86>
#include <i386/ctype.h86>
#include <pxe/general.h86>
#include <system/bootrom.h86>
#include "headers/net.h86"
#include "headers/arpa.h86"
#include "headers/flags.h86"
#include "bootp.h86"



/*
 **************************************************************************
 * 
 * Global variables:
 */
static __u32 boot_xid;			/* BOOTP transaction ID	*/
static unsigned short boot_secs;	/* No. seconds since BOOTP started */
static unsigned short boot_timeout;	/* Current timeout counter */
static unsigned char offer_type;	/* Type of last offer packet */



/*
 **************************************************************************
 * 
 * DHCP message types
 */
#define PKT_DHCP_DISCOVER	VEND_DHCP_DISCOVER
#define PKT_DHCP_OFFER		VEND_DHCP_OFFER
#define PKT_DHCP_REQUEST	VEND_DHCP_REQUEST
#define PKT_DHCP_DECLINE	VEND_DHCP_DECLINE
#define PKT_DHCP_ACK		VEND_DHCP_ACK
#define PKT_DHCP_NAK		VEND_DHCP_NAK
#define PKT_DHCP_RELEASE	VEND_DHCP_RELEASE
#define PKT_DHCP_INFORM		VEND_DHCP_INFORM



/*
 **************************************************************************
 * 
 * Type of received packet
 */
#define PKT_TYPE_INVALID	0
#define PKT_TYPE_BOOTP		1
#define PKT_TYPE_DHCP		2
#define PKT_TYPE_NETBOOT	3



/*
 **************************************************************************
 * 
 * Check for packet type
 */
static unsigned char get_pkt_type(short bptype)
{
  register struct bootp *bp = bootp_bufs[cur_bootp_buf];
  register __u8 *cp;

  /* Check that the packet is indeed a BOOTP packet */
  if (
      /* Only accept BOOTP replies */
      bp->bp_op != BOOTP_OP_REPLY ||

      /* Only accept packets that match my transaction ID */
      bp->bp_xid != boot_xid ||

      /* Only accept packets that match my hardware address */
      memcmp(bp->bp_chaddr, myhwaddr, ETH_ALEN) != 0)
	return(PKT_TYPE_INVALID);

  /* Only accept packets with no or valid vendor ID */
  if (bp->bp_vend[0] == '\0') {
	if (bp->bp_file[0] != '\0')
		return(PKT_TYPE_BOOTP);
	return(PKT_TYPE_INVALID);
  }
  if (*((__u32 *)(bp->bp_vend)) != BOOTP_VM_RFC1048)
	return(PKT_TYPE_INVALID);
  (void)get_vend(VEND_RFC_NOP);
  if (get_vend(VEND_RFC_END) == NULL)
	return(PKT_TYPE_INVALID);

  /* Check if this is a correct DHCP message */
  if ((cp = get_vend(VEND_RFC_MSGTYPE)) == NULL)
	return(PKT_TYPE_BOOTP);
  if (*cp != 1 || *(cp + 1) != (__u8)bptype)
	return(PKT_TYPE_INVALID);

  /* Check if this is a netboot packet */
  if (get_vend(VEND_NB_END) != NULL)
	return(PKT_TYPE_NETBOOT);

  /* Everything else means that it's a standard DHCP packet */
  return(PKT_TYPE_DHCP);
}



/*
 **************************************************************************
 * 
 * Fill a BOOTP request buffer.
 */
static void bootp_fill(short bptype, t_ipaddr reqip, t_ipaddr srvip)
{
  register struct bootp *bp = bootp_bufs[cur_bootp_buf];
  register __u8 *cp;
  short i, j;

  /* Fill up the standard fields */
  memset(bp, 0, bootp_sizes[cur_bootp_buf]);
  memcpy(bp->bp_chaddr, myhwaddr, ETH_ALEN);
  bp->bp_op = BOOTP_OP_REQUEST;
  bp->bp_hwtype = mytype;
  bp->bp_hlen = ETH_ALEN;
  bp->bp_xid = boot_xid;
  bp->bp_secs = htons(boot_secs);
  if (bptype == PKT_DHCP_DISCOVER || bptype == PKT_DHCP_REQUEST)
	bp->bp_flags = htons(BOOTP_FLAG_BCAST);
  if (bptype == PKT_DHCP_INFORM)
	bp->bp_ciaddr = myipaddr;

  /*
   * Fill in DHCP options. Note that we get never called in RENEWING,
   * REBINDING or INIT_REBOOT state (according to RFC2131). For sending
   * a DHCP_REQUEST packet the only allowed state here is SELECTING,
   * which immediately follows a DHCP_DISCOVER.
   * Note that the vendor options field is not allowed to be more than
   * 64 bytes in size for a DHCP_DISCOVER message, so that BOOTP servers
   * are able to respond to it.
   */
  cp = bp->bp_vend;
  *((__u32 *)cp) = BOOTP_VM_RFC1048;
  cp += BOOTP_VM_SIZE;

  /* Option 1: DHCP_MSGTYPE */
  *(cp++) = VEND_RFC_MSGTYPE;
  *(cp++) = 0x01;
  *(cp++) = (__u8)(bptype & 0xFF);

  /* Option 2: DHCP_REQIP */
  if (reqip != htonl(IP_ANY)) {
	*(cp++) = VEND_RFC_REQIP;
	*(cp++) = IP_ALEN;
	*((t_ipaddr *)cp) = reqip;
	cp += IP_ALEN;
  }

  /* Option 3: DHCP_PARAMREQ */
  if ((krnflags & KRNFLG_OPTS) == KRNFLG_OPTS) {
	*(cp++) = VEND_RFC_PARAMREQ;
	*(cp++) = 10;
	*(cp++) = VEND_RFC_SUBNET;
	*(cp++) = VEND_RFC_BROADCAST;
	*(cp++) = VEND_RFC_ROUTER;
	*(cp++) = VEND_RFC_CLASS;
	*(cp++) = VEND_RFC_VENDOR;
	*(cp++) = VEND_RFC_BOOTFILE;
	*(cp++) = VEND_RFC_TFTPNAME;
	*(cp++) = VEND_RFC_HNAME;
	*(cp++) = VEND_RFC_ROOTPATH;
	*(cp++) = VEND_RFC_DOMAIN;
  }

  /* Option 4: DHCP_DHCPSIZE */
  *(cp++) = VEND_RFC_DHCPSIZE;
  *(cp++) = 0x02;
  *((__u16 *)cp) = htons(sizeof(struct bootp));
  cp += 2;

  /* Option 5: DHCP_SERVER */
  if (srvip != htonl(IP_ANY)) {
	*(cp++) = VEND_RFC_SERVER;
	*(cp++) = IP_ALEN;
	*((t_ipaddr *)cp) = srvip;
	cp += IP_ALEN;
  }

  /* Option 6: DHCP_CLASS - netboot client class ID and version number */
  *(cp++) = VEND_RFC_CLASS;
  *(cp++) = VEND_NB_STRLEN + 7;
  memcpy(cp, VEND_NB_STRING ":000000" , VEND_NB_STRLEN + 7);
  j = VER_MAJOR * 1000 + VER_MINOR;
  for (i = VEND_NB_STRLEN + 6; j > 0; --i, j /= 10)
	cp[i] = (__u8)(cp[i] + (j % 10));
  cp += VEND_NB_STRLEN + 7;

  /* Option 7: DHCP_CLIENTID */
  if ((krnflags & KRNFLG_NOUID) == 0) {
	*(cp++) = VEND_RFC_CLIENTID;
	*(cp++) = ETH_ALEN + 1;
	*(cp++) = (__u8)(mytype & 0xFF);
	memcpy(cp, myhwaddr, ETH_ALEN);
	cp += ETH_ALEN;
  }

  /* And finally: the end tag */
  *cp = VEND_RFC_END;
}



/*
 **************************************************************************
 * 
 * Send a DHCP DISCOVER packet via UDP.
 *
 */
static short send_discover(void)
{
  register short retval;

  /*
   * Setup the IP interface information. Our own address has to be
   * IP_ANY, so that the IP layer will not discard the incoming packets.
   * Then open a UDP socket, and create a new transaction ID.
   */
  if (boot_xid != 0)
	(void)udp_close();
  myipaddr = htonl(IP_ANY);
  mynetmask = htonl(IP_CLASS_A);
  if ((retval = udp_open(htonl(IP_BROADCAST), htonl(IP_ANY),
				htons(BOOTP_C_PORT),
				htons(BOOTP_S_PORT))) != PXENV_STATUS_SUCCESS)
	return(retval);
  boot_xid = get_ticks() + random(0x7fff);

  /* Create a new BOOTP output buffer */
  init_bootp_buf();
  if (!new_bootp_buf(BOOTP_DISCOVER))
	return(PXENV_STATUS_FAILURE);

  /* Assemble the BOOTP request */
  bootp_fill(PKT_DHCP_DISCOVER, htonl(IP_ANY), htonl(IP_ANY));

  /* Set the size of the BOOTP record */
  (void)set_bootp_size(BOOTP_MIN_SIZE, TRUE);

  /* Send the bootp record as a broadcast message to all servers */
  return(udp_write(MK_FP(getds(), (__u8 *)(bootp_bufs[BOOTP_DISCOVER])),
						bootp_sizes[BOOTP_DISCOVER]));
}



/*
 **************************************************************************
 * 
 * Wait for BOOTP answer or a DHCPOFFER packet
 */
static short wait_offer(void)
{
  register short retval;
  size_t bufsize, oldsize;
  unsigned char pkttype = PKT_TYPE_INVALID;
  unsigned char temptype = PKT_TYPE_INVALID;
  unsigned short grace_timeout = 0;
  __u8 *tempbuf;

  /*
   * Allocate buffer for reply. For BOOTP and DHCP packets we need
   * to have some temporary buffer space. At this point, the BOOTP_ACK
   * buffer should have a size of at least twice the size of a DHCP
   * packet, and a DHCPOFFER or BOOTP packet can't be larger than one
   * DHCP packet, so we can use the remaining parts of the BOOTP_ACK
   * buffer for this temporary buffer space.
   * The following two error conditions should never happen!
   */
  if (!new_bootp_buf(BOOTP_ACK))
	return(PXENV_STATUS_FAILURE);
  tempbuf = (__u8 *)(bootp_bufs[BOOTP_ACK]) + sizeof(struct bootp);

  /*
   * Wait for BOOTP reply packet, or a DHCPOFFER packet. If we
   * receive a netboot DHCPOFFER packet, we can bail out of this
   * loop immediately, as it can't get any better. However, when
   * we receive a BOOTP packet, we wait the full timeout length if
   * there isn't a DHCPOFFER which is just a little late. Even if
   * we get a DHCPOFFER we wait until almost the end of the timeout
   * period because there might a netboot packet still coming in.
   */
  while (TRUE) {
	/* Check for timeout */
	if (boot_timeout == 0 || boot_timeout <= grace_timeout)
		break;
	boot_timeout--;

	/* Read a packet. This will wait for one second */
	bufsize = sizeof(struct bootp);
	retval = udp_read(MK_FP(getds(), (__u8 *)(bootp_bufs[BOOTP_ACK])),
						&bufsize, 1, CHR_ESC, TRUE);

	/* Check for errors */
	pkttype = PKT_TYPE_INVALID;
	if (retval == PXENV_STATUS_ARP_TIMEOUT)
		continue;
	if (retval != PXENV_STATUS_SUCCESS)
		return(retval);
	if (bufsize < BOOTP_MIN_SIZE)
		continue;

	/* Check that we received a valid packet */
	oldsize = bootp_sizes[BOOTP_ACK];
	bootp_sizes[BOOTP_ACK] = sizeof(struct bootp);
	pkttype = get_pkt_type(VEND_DHCP_OFFER);
	bootp_sizes[BOOTP_ACK] = oldsize;
	if (pkttype == PKT_TYPE_INVALID)
		continue;

	/* It can't get any better than a netboot packet */
	if (pkttype == PKT_TYPE_NETBOOT)
		break;

	/* We received a valid packet, so limit any further timeout */
	if (pkttype == PKT_TYPE_DHCP) {
		grace_timeout = DHCP_GRACE_TIMEOUT;
		if (boot_timeout > (DHCP_GRACE_TIMEOUT * 2))
			boot_timeout = DHCP_GRACE_TIMEOUT * 2;
	} else {
		/*
		 * Handle BOOTP timeout:
		 * Do not reset the grace_timeout here for the sake of any
		 * preceding DHCP offer.
		 */
		if (boot_timeout > BOOTP_TIMEOUT)
			boot_timeout = BOOTP_TIMEOUT;
	}

	/* This is a DHCP or BOOTP packet, save if it's better than before */
	if (temptype == PKT_TYPE_INVALID ||
	    (pkttype == PKT_TYPE_DHCP && temptype == PKT_TYPE_BOOTP)) {
		memcpy(tempbuf, (__u8 *)(bootp_bufs[BOOTP_ACK]),
						sizeof(struct bootp));
		temptype = pkttype;
	}
  }

  /* Now prepare the final offer packet by selecting the best offer */
  if (temptype != PKT_TYPE_INVALID &&
      (pkttype == PKT_TYPE_INVALID ||
       pkttype == temptype /* use the first received packet */ ||
       (pkttype == PKT_TYPE_BOOTP && temptype == PKT_TYPE_DHCP))) {
	memcpy((__u8 *)(bootp_bufs[BOOTP_ACK]), tempbuf, sizeof(struct bootp));
	(void)get_vend(VEND_RFC_NOP);
	pkttype = temptype;
  }
  offer_type = pkttype;
  return(pkttype == PKT_TYPE_INVALID ? PXENV_STATUS_DHCP_TIMEOUT :
							PXENV_STATUS_SUCCESS);
}



/*
 **************************************************************************
 * 
 * Send a DHCP REQUEST packet via UDP.
 *
 */
static short send_request(t_ipaddr serverip, t_ipaddr clientip)
{
  register short retval;
  register size_t len;

  /*
   * We need to reopen the UDP connection in order to be able to broadcast
   * another packet.
   */
  (void)udp_close();
  if ((retval = udp_open(htonl(IP_BROADCAST), htonl(IP_ANY),
				htons(BOOTP_C_PORT),
				htons(BOOTP_S_PORT))) != PXENV_STATUS_SUCCESS)
	return(retval);

  /* Assemble the BOOTP request */
  bootp_fill(PKT_DHCP_REQUEST, clientip, serverip);

  /* Determine the length of the packet. Do NOT update any global variables */
  len = set_bootp_size(BOOTP_MIN_SIZE, FALSE);

  /* Send the bootp record as a broadcast message to all servers */
  return(udp_write(MK_FP(getds(), (__u8 *)(bootp_bufs[BOOTP_ACK])), len));
}



/*
 **************************************************************************
 * 
 * Wait for a DHCPACK packet
 */
static short wait_ack(t_ipaddr clientip)
{
  register short retval;
  size_t bufsize;
  unsigned char pkt_type;

  /* Wait for DHCPACK packet */
  while (TRUE) {
	/* Check for timeout */
	if (boot_timeout == 0)
		break;
	boot_timeout--;

	/* Read a packet. This will wait for one second */
	bufsize = bootp_sizes[BOOTP_ACK];
	retval = udp_read(MK_FP(getds(), (__u8 *)(bootp_bufs[BOOTP_ACK])),
						&bufsize, 1, CHR_ESC, TRUE);

	/* Check for errors */
	if (retval == PXENV_STATUS_ARP_TIMEOUT)
		continue;
	if (retval != PXENV_STATUS_SUCCESS)
		return(retval);
	if (bufsize < BOOTP_MIN_SIZE)
		continue;

	/* Check that we received a valid packet */
	pkt_type = get_pkt_type(VEND_DHCP_ACK);
	if (pkt_type == PKT_TYPE_INVALID || pkt_type == PKT_TYPE_BOOTP)
		continue;

	/* Check client IP */
	if (bootp_bufs[BOOTP_ACK]->bp_yiaddr != clientip)
		return(PXENV_STATUS_DHCP_BAD_IP);

	/* We got a suitable DHCPACK packet */
	return(PXENV_STATUS_SUCCESS);
  }
  return(PXENV_STATUS_DHCP_TIMEOUT);
}



/*
 **************************************************************************
 * 
 * Send a DHCPREQUEST and wait for a DHCPACK for the selected offer
 */
static short do_request_ack(void)
{
  register short retval;
  t_ipaddr clientip;
  t_ipaddr serverip;
  __u8 *cp;

  /* Check for correct DHCPOFFER packet */
  if (offer_type == PKT_TYPE_INVALID)
	/* This should never happen */
	return(PXENV_STATUS_FAILURE);
  else if (offer_type == PKT_TYPE_BOOTP)
	/* There is no ACK for BOOTP reply packet */
	return(PXENV_STATUS_SUCCESS);

  /* Determine the client and server IP addresses we want to request. */
  clientip = bootp_bufs[BOOTP_ACK]->bp_yiaddr;
  if ((cp = get_vend(VEND_RFC_SERVER)) != NULL && *cp >= 4)
	serverip = *((t_ipaddr *)(cp + 1));
  else
	serverip = bootp_bufs[BOOTP_ACK]->bp_siaddr;

  /* Send the DHCPREQUEST */
  if ((retval = send_request(serverip, clientip)) != PXENV_STATUS_SUCCESS)
	return(retval);

  /* Wait for a DHCPACK */
  if ((retval = wait_ack(clientip)) != PXENV_STATUS_SUCCESS)
	return(retval);

  return(PXENV_STATUS_SUCCESS);
}



/*
 **************************************************************************
 * 
 * Get a BOOTP record from the server.
 *
 */
static short bootp_get(void)
{
  unsigned short old_timeout;
  short retval, retries;
  char errch;

  /*
   * Now loop until receiving a reply from a server. The retry time is
   * computed as suggested in RFC951.
   */
  boot_secs = 4;
  boot_timeout = BOOTP_TIMEOUT + (unsigned short)random(0x001f);
  retries = BOOTP_RETRIES;
  printf("\n\nDHCP: Sending request (press ESC to abort): ");
  while (TRUE) {
	/* Save current timeout for later */
	old_timeout = boot_timeout;

	/* Send new BOOTP/DHCPDISCOVER request */
	if ((retval = send_discover()) != PXENV_STATUS_SUCCESS)
		break;

	/* Wait for BOOTP answer/DHCP offers */
	if ((retval = wait_offer()) == PXENV_STATUS_SUCCESS &&
	    (retval = do_request_ack()) == PXENV_STATUS_SUCCESS) {
		printf("ok\n");
		break;
	}

	/* Check for error */
	errch = '?';
	if (retval == PXENV_STATUS_FAILURE) {
		/* Something bad happened */
		break;
	} else if (retval == PXENV_STATUS_ARP_CANCELED) {
		/* The user pressed ESC */
		retval = PXENV_STATUS_DHCP_CANCELED;
		break;
	} else if (retval == PXENV_STATUS_DHCP_TIMEOUT) {
		/* We have a timeout, so retry if possible */
		errch = '.';
		if (--retries == 0)
			break;
	}

	/* Increase timeout time and print error character */
	boot_secs += (old_timeout - boot_timeout);
	boot_timeout = (old_timeout << 1) + (unsigned short)random(0x001f);
	if (boot_timeout >= BOOTP_MAX_TIMEOUT)
		boot_timeout = BOOTP_MAX_TIMEOUT;
	printf("%c", errch);
  }

  /* Close UDP connection and return */
  boot_xid = 0UL;
  (void)udp_close();
  return(retval);
}



/*
 **************************************************************************
 * 
 * Handle BOOTP/DHCP protocol.
 */
short bootp(void)
{
  short retval;
  __u8 *cp;

  /* Get BOOTP record from server */
  offer_type = PKT_TYPE_INVALID;
  if ((retval = bootp_get()) != PXENV_STATUS_SUCCESS)
	return(retval);

  /* Check for valid BOOTP record */
  cur_bootp_buf = BOOTP_ACK;
  myipaddr = bootp_bufs[BOOTP_ACK]->bp_yiaddr;
  if (myipaddr == htonl(IP_ANY))
	return(PXENV_STATUS_DHCP_NO_IP_ADDR);
  if (myipaddr == htonl(IP_BROADCAST) ||
      myipaddr == htonl(IP_LOCALHOST) ||
      IN_MULTICAST(ntohl(myipaddr)))
	return(PXENV_STATUS_DHCP_BAD_IP);
  if (bootp_bufs[BOOTP_ACK]->bp_file[0] == '\0' &&
      ((cp = get_vend(VEND_RFC_BOOTFILE)) == NULL || *cp == 0) &&
      ((cp = get_vend(VEND_NB_BOOTFILE)) == NULL || *cp == 0))
	return(PXENV_STATUS_DHCP_NO_FILE);

  /* Setup the network interface with the values received from BOOTP server */
  if ((cp = get_vend(VEND_RFC_SUBNET)) != NULL && *cp >= 4)
	mynetmask = *((t_ipaddr *)(cp + 1));
  else if (IN_CLASS_A(ntohl(myipaddr)))
	mynetmask = htonl(IP_CLASS_A);
  else if (IN_CLASS_B(ntohl(myipaddr)))
	mynetmask = htonl(IP_CLASS_B);
  else if (IN_CLASS_C(ntohl(myipaddr)))
	mynetmask = htonl(IP_CLASS_C);
  else
	mynetmask = htonl(IP_ANY);
  if ((cp = get_vend(VEND_RFC_ROUTER)) != NULL && *cp >= 4)
	mygateway = *((t_ipaddr *)(cp + 1));
  else
	mygateway = htonl(IP_ANY);

#ifndef NOBPEXT
  /* Next try to get the extensions file if there is any specified */
  if (offer_type == PKT_TYPE_BOOTP && (krnflags & KRNFLG_NOBPEXT) == 0 &&
      (cp = get_vend(VEND_RFC_EXTFILE)) != NULL && *cp > 0)
	get_ext(cp);
#endif

  /* Determine the TFTP timeout values */
  if ((cp = get_vend(VEND_NB_CLNTTOUT)) != NULL && *cp == 2)
	clnttout = ntohs(*((__u16 *)(cp + 1)));
  if ((cp = get_vend(VEND_NB_SRVRTOUT)) != NULL && *cp == 2)
	srvrtout = ntohs(*((__u16 *)(cp + 1)));

  /* Finally setup all remaining buffers */
  (void)set_bootp_size(sizeof(struct bootp), TRUE);
  if (!new_bootp_buf(BOOTP_REPLY))
	return(PXENV_STATUS_FAILURE);
  if (bootp_sizes[BOOTP_ACK] > bootp_sizes[BOOTP_REPLY])
	bootp_bufs[BOOTP_REPLY] = bootp_bufs[BOOTP_ACK];
  else
	memcpy((__u8 *)(bootp_bufs[BOOTP_REPLY]),
		(__u8 *)(bootp_bufs[BOOTP_ACK]), bootp_sizes[BOOTP_ACK]);
  (void)set_bootp_size(sizeof(struct bootp), FALSE);
  return(PXENV_STATUS_SUCCESS);
}

