/* flaneth - flash and ethernet
   Copyright (C) 2007-2012 Stefan Schuermans <stefan@schuermans.info>
   Copyleft: GNU public license V2 - http://www.gnu.org/copyleft/gpl.html
   a BlinkenArea project - http://www.blinkenarea.org/ */

#include <string.h>

#include "config.h"
#include "checksum.h"
#include "debug.h"
#include "dhcp.h"
#include "ethernet.h"
#include "ip.h"
#include "macros.h"
#include "nethelp.h"
#include "random.h"
#include "udp.h"

// configuration
static const unsigned char DhcpRetrySecsMax = 10;       // avarage timeout
                                                        // after which to
                                                        // retry DHCP action
static const unsigned char DhcpRetriesMax = 5;  // maximum number of retries
                                                // before aborting DHCP
                                                // action
static const unsigned long DhcpLeaseRestMin = 300;      // when to ask to
                                                        // extend lease
static const unsigned char DhcpLeaseRenewFraction = 8;  // to renw lease when 
                                                        // only this fraction 
                                                        // of lease is left

// DHCP tick counter to get full seconds
static unsigned char DhcpTicks = 0;

// time of current DHCP action
static unsigned char DhcpInProgress = 0;        // if DHCP action is in
                                                // progress
static unsigned char DhcpRetries = 0;   // number of retries left
static unsigned char DhcpRetrySecs = 0; // rest of time for current retry

// current XId
static unsigned char DhcpHaveXId = 0;   // if an XId exists at the moment
static unsigned long DhcpXId = 0;       // the current XId data
static unsigned int DhcpXIdSecs = 0;    // time of XId

// DHCP server and lease time
static unsigned char DhcpActive = 0;    // if own IP is leased from DHCP
static unsigned char DhcpServer[4];     // IP address of DHCP server own IP
                                        // is leased from
static unsigned long DhcpLeaseTime = 0; // total lease time
static unsigned long DhcpLeaseRest = 0; // rest of lease time

// send a DHCP packet
// pData must point to a struct DhcpPacket with IpHdr.Dest, DhcpHdr.XId
// DhcpHdr.Secs, DhcpHdr.Flags and DhcpHdr.YIAddr already initialized
static void DhcpSend(unsigned char *pData, unsigned short Length)       // (extern)
{
  struct DhcpPacket *pDhcpPack;

  // packet too short
  if (Length < sizeof(struct DhcpPacket))
    return;

  // convert pointer to DHCP packet
  // (this saves us from always casting pData)
  pDhcpPack = (struct DhcpPacket *)pData;

  debug_dhcp_printf("send xid=%08lX len=%d",
                    ntohl(pDhcpPack->DhcpHdr.XId), Length);

  // we are DHCP client
  // - source port must be 68
  // - destination port must be 67
  pDhcpPack->UdpHdr.SrcPort = htons(68);
  pDhcpPack->UdpHdr.DestPort = htons(67);
  // set up DHCP header fields as client
  pDhcpPack->DhcpHdr.Op = 1;
  pDhcpPack->DhcpHdr.HType = 1;
  pDhcpPack->DhcpHdr.HLen = 6;
  pDhcpPack->DhcpHdr.HOps = 0;
  memset(pDhcpPack->DhcpHdr.CIAddr, 0, 4);
  memset(pDhcpPack->DhcpHdr.SIAddr, 0, 4);
  memset(pDhcpPack->DhcpHdr.GIAddr, 0, 4);
  mac_cpy(pDhcpPack->DhcpHdr.CHAddr, ConfigMac);
  memset(pDhcpPack->DhcpHdr.CHAddr + 6, 0, 10);
  memset(pDhcpPack->DhcpHdr.SName, 0, 64);
  memset(pDhcpPack->DhcpHdr.File, 0, 128);
  pDhcpPack->DhcpHdr.MCookie = htonl(0x63825363);

  // send DHCP packet
  UdpSend(pData, Length);
}

// send a DHCP discover
static void DhcpDiscover(void)
{
  struct {
    struct DhcpPacket DhcpPack;
    unsigned char OptType[3];
    unsigned char OptEnd[2];
  } DhcpDiscover;

  debug_dhcp_printf("discover");

  ip_cpy(DhcpDiscover.DhcpPack.IpHdr.Dest, "\xFF\xFF\xFF\xFF"); // broadcast
  DhcpDiscover.DhcpPack.DhcpHdr.XId = htonl(DhcpXId);
  DhcpDiscover.DhcpPack.DhcpHdr.Secs = htons(DhcpXIdSecs);      // use time
                                                                // of XId
  DhcpDiscover.DhcpPack.DhcpHdr.Flags = htons(0x8000);  // broadcast
  memset(DhcpDiscover.DhcpPack.DhcpHdr.YIAddr, 0, 4);
  DhcpDiscover.OptType[0] = 0x35;       // DHCP discover
  DhcpDiscover.OptType[1] = 0x01;
  DhcpDiscover.OptType[2] = 0x01;
  DhcpDiscover.OptEnd[0] = 0xFF;        // end of options
  DhcpDiscover.OptEnd[1] = 0x00;

  DhcpSend((unsigned char *)&DhcpDiscover, sizeof(DhcpDiscover));
}

// send a DHCP request
static void DhcpRequest(void)
{
  struct {
    struct DhcpPacket DhcpPack;
    unsigned char OptType[3];
    unsigned char OptReq[6];
    unsigned char OptServer[6];
    unsigned char OptEnd[2];
  } DhcpRequest;

  debug_dhcp_printf("request ip=%d.%d.%d.%d",
                    ConfigIp[0], ConfigIp[1], ConfigIp[2], ConfigIp[3]);

  ip_cpy(DhcpRequest.DhcpPack.IpHdr.Dest, DhcpServer);  // send to DHCP
                                                        // server
  DhcpRequest.DhcpPack.DhcpHdr.XId = htonl(DhcpXId);
  DhcpRequest.DhcpPack.DhcpHdr.Secs = htons(DhcpXIdSecs);       // use time
                                                                // of XId
  DhcpRequest.DhcpPack.DhcpHdr.Flags = htons(0x0000);
  ip_cpy(DhcpRequest.DhcpPack.DhcpHdr.YIAddr, ConfigIp);
  DhcpRequest.OptType[0] = 0x35;        // DHCP request
  DhcpRequest.OptType[1] = 0x01;
  DhcpRequest.OptType[2] = 0x03;
  DhcpRequest.OptReq[0] = 0x32; // requested IP
  DhcpRequest.OptReq[1] = 0x04;
  ip_cpy(&DhcpRequest.OptReq[2], ConfigIp);
  DhcpRequest.OptServer[0] = 0x36;      // DHCP server
  DhcpRequest.OptServer[1] = 0x04;
  ip_cpy(&DhcpRequest.OptServer[2], DhcpServer);
  DhcpRequest.OptEnd[0] = 0xFF; // end of options
  DhcpRequest.OptEnd[1] = 0x00;

  DhcpSend((unsigned char *)&DhcpRequest, sizeof(DhcpRequest));
}

// abort DHCP action
static void DhcpAbort(void)
{
  // abort DHCP action
  DhcpInProgress = 0;
  DhcpRetries = 0;
  DhcpRetrySecs = 0;

  // forget XId
  DhcpHaveXId = 0;
  DhcpXId = 0;
  DhcpXIdSecs = 0;

  // give up IP address if it was leased
  if (DhcpActive) {
    ip_cpy(ConfigIp, "\0\0\0\0");
    ip_cpy(ConfigMask, "\0\0\0\0");
    ip_cpy(ConfigGw, "\0\0\0\0");
    DhcpActive = 0;
    DhcpLeaseTime = 0;
    DhcpLeaseRest = 0;
  }
}

// retry DHCP action
static void DhcpRetry(void)
{
  // DHCP lease not active ---> re-send DHCP discover
  if (!DhcpActive) {

    // get time for next try
    RandomGetData((unsigned char *)&DhcpRetrySecs, sizeof(DhcpRetrySecs));
    DhcpRetrySecs = DhcpRetrySecs % DhcpRetrySecsMax + DhcpRetrySecsMax / 2;
    // re-send DHCP discover
    DhcpDiscover();

  }
  // DHCP lease active and lease time almost over ---> re-send DHCP request
  if (DhcpActive && (DhcpLeaseRest < DhcpLeaseRestMin ||
                     DhcpLeaseRest <
                     DhcpLeaseTime / DhcpLeaseRenewFraction)) {

    // get time for next try
    RandomGetData((unsigned char *)&DhcpRetrySecs, sizeof(DhcpRetrySecs));
    DhcpRetrySecs = DhcpRetrySecs % DhcpRetrySecsMax + DhcpRetrySecsMax / 2;
    // re-send DHCP request
    DhcpRequest();

  }
}

// start new DHCP action
static void DhcpStart(void)
{
  // DHCP lease not active ---> DHCP discover
  if (!DhcpActive) {

    // DHCP operation starts
    DhcpInProgress = 1;
    DhcpRetries = DhcpRetriesMax;
    RandomGetData((unsigned char *)&DhcpRetrySecs, sizeof(DhcpRetrySecs));
    DhcpRetrySecs = DhcpRetrySecs % DhcpRetrySecsMax + DhcpRetrySecsMax / 2;
    // get new XId
    RandomGetData((unsigned char *)&DhcpXId, sizeof(DhcpXId));
    DhcpXIdSecs++;
    DhcpHaveXId = 1;
    // send DHCP discover
    DhcpDiscover();

  }
  // DHCP lease active and lease time almost over ---> DHCP request
  if (DhcpActive && (DhcpLeaseRest < DhcpLeaseRestMin ||
                     DhcpLeaseRest <
                     DhcpLeaseTime / DhcpLeaseRenewFraction)) {

    // DHCP operation starts
    DhcpInProgress = 1;
    DhcpRetries = DhcpRetriesMax;
    RandomGetData((unsigned char *)&DhcpRetrySecs, sizeof(DhcpRetrySecs));
    DhcpRetrySecs = DhcpRetrySecs % DhcpRetrySecsMax + DhcpRetrySecsMax / 2;
    // get new XId if none available
    if (!DhcpHaveXId) {
      RandomGetData((unsigned char *)&DhcpXId, sizeof(DhcpXId));
      DhcpXIdSecs++;
      DhcpHaveXId = 1;
    }
    // send DHCP request
    DhcpRequest();

  }
}

// tick procedure - every 1000ms
static void DhcpTick1000(void)
{
  // DHCP operation in progress
  if (DhcpInProgress) {

    // count down time of current try of current action
    DhcpRetrySecs--;
    if (DhcpRetrySecs <= 0) {

      // count down retries
      DhcpRetries--;

      // no more retries left ---> abort, retry otherwise
      if (DhcpRetries <= 0)
        DhcpAbort();
      else
        DhcpRetry();

    }

  }
  // increase time of XId
  DhcpXIdSecs++;

  // dcrease remaining lease time
  DhcpLeaseRest--;
  // lease timed out ---> abort DHCP (will also invalidate IP)
  if (DhcpLeaseRest <= 0) {
    debug_dhcp_printf("lease expired");
    DhcpAbort();
  }
}

// tick procedure - call every 200ms
void DhcpTick200(void)  // (extern)
{
  // get 1 second interval
  DhcpTicks++;
  if (DhcpTicks >= 5) {
    DhcpTicks = 0;
    DhcpTick1000();
  }
  // no DHCP operation in progress
  if (!DhcpInProgress) {
    // DHCP lease active or no IP address ---> DHCP allowed (i.e. no static
    // IP)
    if (DhcpActive || ip_eq(ConfigIp, "\0\0\0\0")) {
      // start new DHCP action
      DhcpStart();
    }
  }
}

// process a received DHCP offer
static void DhcpOffer(unsigned char addr[4], unsigned char mask[4],
                      unsigned char gateway[4], unsigned long time,
                      unsigned char server[4])
{
  debug_dhcp_printf("offer addr=%d.%d.%d.%d mask=%d.%d.%d.%d gw=%d.%d.%d.%d",
                    addr[0], addr[1], addr[2], addr[3],
                    mask[0], mask[1], mask[2], mask[3],
                    gateway[0], gateway[1], gateway[2], gateway[3]);
  debug_dhcp_printf("offer time=%ld server=%d.%d.%d.%d",
                    time, server[0], server[1], server[2], server[3]);

  // use this IP address, mask and gateway
  ip_cpy(ConfigIp, addr);
  ip_cpy(ConfigMask, mask);
  ip_cpy(ConfigGw, gateway);

  // store DHCP server and lease time
  DhcpActive = 1;
  ip_cpy(DhcpServer, server);
  DhcpLeaseTime = time;
  DhcpLeaseRest = DhcpLeaseRestMin;     // schedule DHCP request

  // DHCP action finished
  DhcpInProgress = 0;
  DhcpRetries = 0;
  DhcpRetrySecs = 0;
}

// process a received DHCP ack
static void DhcpAck(unsigned char addr[4], unsigned char mask[4],
                    unsigned char gateway[4], unsigned long time,
                    unsigned char server[4])
{
  debug_dhcp_printf("ack addr=%d.%d.%d.%d mask=%d.%d.%d.%d gw=%d.%d.%d.%d",
                    addr[0], addr[1], addr[2], addr[3],
                    mask[0], mask[1], mask[2], mask[3],
                    gateway[0], gateway[1], gateway[2], gateway[3]);
  debug_dhcp_printf("ack time=%ld server=%d.%d.%d.%d",
                    time, server[0], server[1], server[2], server[3]);

  // check if IP address, mask and gateway match
  if (!ip_eq(ConfigIp, addr) ||
      !ip_eq(ConfigMask, mask) || !ip_eq(ConfigGw, gateway)) {
    // mismatch ---> something is wrong, abort
    DhcpAbort();
    return;
  }
  // store DHCP server and lease time
  DhcpActive = 1;
  ip_cpy(DhcpServer, server);
  DhcpLeaseTime = time;
  DhcpLeaseRest = time;

  // DHCP action finished
  DhcpInProgress = 0;
  DhcpRetries = 0;
  DhcpRetrySecs = 0;

  // forget XId
  DhcpHaveXId = 0;
  DhcpXId = 0;
  DhcpXIdSecs = 0;
}

// process a received DHCP packet
void DhcpRecv(unsigned char *pData, unsigned short Length)      // (extern)
{
  struct DhcpPacket *pDhcpPack;
  unsigned char *opt_ptr, tag, len;
  unsigned short opt_len;
  unsigned char type;
  unsigned char have_mask, mask[4], have_gateway, gateway[4];
  unsigned char have_time, have_server, server[4];
  unsigned long time;

  // packet too short
  if (Length < sizeof(struct DhcpPacket))
    return;

  // convert pointer to UDP packet
  // (this saves us from always casting pData)
  pDhcpPack = (struct DhcpPacket *)pData;

  // we are DHCP client
  // - source port must be 67
  // - destination port must be 68
  if (pDhcpPack->UdpHdr.SrcPort != htons(67) ||
      pDhcpPack->UdpHdr.DestPort != htons(68))
    return;
  // check DHCP fields
  if (pDhcpPack->DhcpHdr.Op != 2 ||
      pDhcpPack->DhcpHdr.HType != 1 ||
      pDhcpPack->DhcpHdr.HLen != 6 ||
      pDhcpPack->DhcpHdr.HOps != 0 ||
      pDhcpPack->DhcpHdr.MCookie != htonl(0x63825363))
    return;

  debug_dhcp_printf("recv xid=%08lX len=%d",
                    ntohl(pDhcpPack->DhcpHdr.XId), Length);

  // check XId
  if (!DhcpHaveXId || ntohl(pDhcpPack->DhcpHdr.XId) != DhcpXId)
    return;

  // get options
  opt_ptr = pData + sizeof(struct DhcpPacket);
  opt_len = Length - sizeof(struct DhcpPacket);

  // parse options
  type = 0;
  have_mask = 0;
  have_gateway = 0;
  time = 0;
  have_time = 0;
  have_server = 0;
  while (opt_len > 2) {

    // get tag and length
    tag = *opt_ptr++;
    len = *opt_ptr++;
    if (opt_len < 2 + (unsigned short)len)
      break;

    // get option
    switch (tag) {

      // type
    case 0x35:
      if (len >= 1)
        type = opt_ptr[0];
      break;

      // subnet mask
    case 0x01:
      if (len >= 4)
        memcpy(mask, opt_ptr, 4);
      have_mask = 1;
      break;

      // gateway
    case 0x03:
      if (len >= 4) {
        memcpy(gateway, opt_ptr, 4);
        have_gateway = 1;
      }
      break;

      // lease time
    case 0x33:
      if (len >= 4) {
        time = ntohl(*(unsigned long *)opt_ptr);
        have_time = 1;
      }
      break;

      // DHCP server
    case 0x36:
      if (len >= 4) {
        memcpy(server, opt_ptr, 4);
        have_server = 1;
      }
      break;

    }

    // skip option
    opt_ptr += len;
    opt_len -= len;
  }

  debug_dhcp_printf("type=%d%s%s%s%s", type,
                    have_mask ? " mask" : "",
                    have_gateway ? " gateway" : "",
                    have_time ? " time" : "", have_server ? " server" : "");

  // fill in server from source address if not present
  if (!have_server) {
    ip_cpy(server, pDhcpPack->IpHdr.Src);
    have_server = 1;
  }
  // process DHCP response
  switch (type) {

    // DHCP offer
  case 2:
    if (have_mask && have_gateway && have_time && have_server)
      DhcpOffer(pDhcpPack->DhcpHdr.YIAddr, mask, gateway, time, server);
    break;

    // DHCP ack
  case 5:
    if (have_mask && have_gateway && have_time && have_server)
      DhcpAck(pDhcpPack->DhcpHdr.YIAddr, mask, gateway, time, server);
    break;

  }
}
