/**********************************************************************
   Mark Butcher    Bsc (Hons) MPhil MIET

   M.J.Butcher Consulting
   Birchstrasse 20f,    CH-5406, Rütihof
   Switzerland

   www.uTasker.com    Skype: M_J_Butcher

   ---------------------------------------------------------------------
   File:        dhcp.c
   Project:     Single Chip Embedded Internet
   ---------------------------------------------------------------------
   Copyright (C) M.J.Butcher Consulting 2004..2010
   *********************************************************************

   10.06.2007   Quieten GNU compiler by initialising a variable          {1}
   22.06.2008   Allow operation without random number generator support  {2}
   14.10.2009   Allow overload options to be tolerated, but not handled  {3}
   22.03.2010   Verify the MAC address in offers to ensure accepting only own values {4}
   03.06.2010   Correct MAX_TIMER_DELAY value                            {5}
   04.06.2010   Check for invalid renewal and rebind timers from server and force realistic values {6}
   24.10.2010   Add relay agent support                                  {7}

*/        


/*
   The 'xid' field is used by the client to match incoming DHCP messages
   with pending requests.  A DHCP client MUST choose 'xid's in such a
   way as to minimize the chance of using an 'xid' identical to one used
   by another client. For example, a client may choose a different,
   random initial 'xid' each time the client is rebooted, and
   subsequently use sequential 'xid's until the next reboot.  Selecting
   a new 'xid' for each retransmission is an implementation decision.  A
   client may choose to reuse the same 'xid' or select a new 'xid' for
   each retransmitted message.

Read more: http://www.faqs.org/rfcs/rfc2131.html#ixzz0j8gXkSXK
*/

    
#include "config.h"


#ifdef USE_DHCP

#define ALLOW_OVERLOAD_OPTIONS                                           // {3}
#define VERIFY_TIMERS                                                    // {6}
#define SUPPORT_RELAY_AGENT                                              // {7}

/* =================================================================== */
/*                 local function prototype declarations               */
/* =================================================================== */

static void          fnStateEventDHCP(unsigned char ucEvent);
static int           fnDHCPListner(USOCKET socket, unsigned char uc, unsigned char *ucIP, unsigned short us, unsigned char *data, unsigned short us2);
static void          fnRandomise(DELAY_LIMIT DHCPTimeout, unsigned char ucTimerEvent);
static unsigned char fnStartDHCPTimer(unsigned char ucTimerEvent);
static void          fnSendDHCP(unsigned char ucDHCP_message);


/* =================================================================== */
/*                          local definitions                          */
/* =================================================================== */


#define OWN_TASK                 TASK_DHCP


/*
 --------                               -------
|        | +-------------------------->|       |<-------------------+
| INIT-  | |     +-------------------->| INIT  |                    |
| REBOOT |DHCPNAK/         +---------->|       |<---+               |
|        |Restart|         |            -------     |               |
 --------  |  DHCPNAK/     |               |                        |
    |      Discard offer   |      -/Send DHCPDISCOVER               |
-/Send DHCPREQUEST         |               |                        |
    |      |     |      DHCPACK            v        |               |
 -----------     |   (not accept.)/   -----------   |               |
|           |    |  Send DHCPDECLINE |           |                  |
| REBOOTING |    |         |         | SELECTING |<----+            |
|           |    |        /          |           |     |DHCPOFFER/  |
 -----------     |       /            -----------   |  |Collect     |
    |            |      /                  |   |       |  replies   |
DHCPACK/         |     /  +----------------+   +-------+            |
Record lease, set|    |   v   Select offer/                         |
timers T1, T2   ------------  send DHCPREQUEST      |               |
    |   +----->|            |             DHCPNAK, Lease expired/   |
    |   |      | REQUESTING |                  Halt network         |
    DHCPOFFER/ |            |                       |               |
    Discard     ------------                        |               |
    |   |        |        |                   -----------           |
    |   +--------+     DHCPACK/              |           |          |
    |              Record lease, set    -----| REBINDING |          |
    |                timers T1, T2     /     |           |          |
    |                     |        DHCPACK/   -----------           |
    |                     v     Record lease, set   ^               |
    +----------------> -------      /timers T1,T2   |               |
               +----->|       |<---+                |               |
               |      | BOUND |<---+                |               |
  DHCPOFFER, DHCPACK, |       |    |            T2 expires/   DHCPNAK/
   DHCPNAK/Discard     -------     |             Broadcast  Halt network
               |       | |         |            DHCPREQUEST         |
               +-------+ |        DHCPACK/          |               |
                    T1 expires/   Record lease, set |               |
                 Send DHCPREQUEST timers T1, T2     |               |
                 to leasing server |                |               |
                         |   ----------             |               |
                         |  |          |------------+               |
                         +->| RENEWING |                            |
                            |          |----------------------------+
                             ----------

          Figure 5:  State-transition diagram for DHCP clients [RFC 2131]
*/

// Events
//
#define E_START_DHCP            1
#define E_DISCOVER_TIMEOUT      2
#define E_REQUEST_TIMEOUT       3
#define E_DHCP_COLLISION        4
#define E_DHCP_BIND_NOW         5
#define E_ACK_RECEIVED          6
#define E_REJECT_RECEIVED       7
#define E_RENEW_NOW             8
#define E_START_RENEWAL         9
#define E_START_REBIND          10
#define E_REBIND_NOW            11


// DHCP messages
//
#define DHCP_DISCOVER           1
#define DHCP_OFFER              2
#define DHCP_REQUEST            3
#define DHCP_DECLINE            4
#define DHCP_ACK                5
#define DHCP_NAK                6
#define DHCP_RELEASE            7
#define DHCP_INFORM             8

// DHCP options
//
#define DHCP_OPT_PAD            0
#define DHCP_OPT_SUBNET_MASK    1
#define DHCP_OPT_TIME_OFFSET    2
#define DHCP_OPT_ROUTER         3
#define DHCP_OPT_TIME_SERVER    4
#define DHCP_OPT_NAME_SERVER    5
#define DHCP_OPT_DNS_SERVER     6
#define DHCP_OPT_HOST_NAME      12
#define DHCP_OPT_REQUESTED_IP   50
#define DHCP_OPT_LEASE_TIME     51
#define DHCP_OPT_OVERLOAD       52
#define DHCP_OPT_MSG_TYPE       53
#define DHCP_OPT_SERV_IDENT     54
#define DHCP_OPT_PARAM_REQUEST  55
#define DHCP_OPT_MAX_MSG_SIZE   57
#define DHCP_OPT_T1_VALUE       58
#define DHCP_OPT_T2_VALUE       59
#define DHCP_OPT_POP3_SERVER    70
#define DHCP_OPT_END            255


#define BOOT_REQUEST            1
#define BOOT_REPLY              2


#define HW_ETHERNET_10M         1

#define BROADCAST_FLAG          0x8000

#define DHCL_OPTIONS            (BROADCAST_FLAG)

#define CHADD_LENGTH            16
#define SNAME_LENGTH            64
#define FILE_LENGTH             128

#define XID_LENGTH              4
#define XID_OFFSET              4

#define MAGIC_COOKIE_LENGTH     4

#define DHCP_BUFFER             (300)                                    // adequate for the largest DHCP message we send

/* =================================================================== */
/*                             constants                               */
/* =================================================================== */

static const unsigned char cDHCP_op_param_req[] = {
    DHCP_OPT_PARAM_REQUEST,
#ifdef USE_DNS
    7,                                                                   // number of parameters being requested (inc. DNS)
#else
    6,                                                                   // number of parameters being requested (without DNS)
#endif
    DHCP_OPT_SUBNET_MASK, DHCP_OPT_ROUTER,
#ifdef USE_DNS
    DHCP_OPT_DNS_SERVER,                                                 // only request DNS when we need it
#endif
    DHCP_OPT_HOST_NAME,
    DHCP_OPT_LEASE_TIME, DHCP_OPT_T1_VALUE, DHCP_OPT_T2_VALUE
};

static const unsigned char cRequestHeader[] = {
    BOOT_REQUEST,                                                        // operation is boot request
    HW_ETHERNET_10M,                                                     // htype = ethernet
    MAC_LENGTH,                                                          // hlen
    0x00,                                                                // hops is 0 for clients
    0x12, 0x34, 0x56, 0x78,                                              // XID (random number)
    0x00, 0x00,                                                          // seconds since our boot    
    (unsigned char)(DHCL_OPTIONS>>8),                                    // options
    (unsigned char)(DHCL_OPTIONS)
};

static const unsigned char ucMagicCookie_and_message[] = {
    99, 130, 83, 99,                                                     // magic cookie
    DHCP_OPT_MSG_TYPE, 1                                                 // DHCP message and length
};


/* =================================================================== */
/*                       local structure definitions                   */
/* =================================================================== */

typedef struct stUDP_DHCP_MESSAGE
{     
    UDP_HEADER       tUDP_Header;                                         // reserve header space
    unsigned char    ucUDP_Message[DHCP_BUFFER];
} UDP_DHCP_MESSAGE;


/* =================================================================== */
/*                     global variable definitions                     */
/* =================================================================== */

static unsigned char ucDHCP_state = DHCP_INIT;                           // DHCP not yet active
static USOCKET       DHCPSocketNr = -1;                                  // UDP socket
static unsigned char ucDHCP_IP[IPV4_LENGTH];                             // requested IP
static unsigned char ucDHCP_SERVER_IP[IPV4_LENGTH];                      // server's IP
static DELAY_LIMIT   DHCPTimeout;

static unsigned long ulLeaseTime;                                        // lease time variables
static unsigned long ulRenewalTime;
static unsigned long ulRebindingTime;

static unsigned char ucResendAfterArp = 0;

static UTASK_TASK    MasterTask = 0;                                     // task who started the DHCP process

#ifdef RANDOM_NUMBER_GENERATOR 
    static unsigned char ucXid[XID_LENGTH];                              // DHCP transation ID for checking negotiation match
#endif

  
// DHCP task
//
extern void fnDHCP(TTASKTABLE *ptrTaskTable)  
{
    QUEUE_HANDLE PortIDInternal = ptrTaskTable->TaskID;                  // queue ID for task input 
    unsigned char ucInputMessage[SMALL_QUEUE];                           // reserve space for receiving messages 

    if ( fnRead( PortIDInternal, ucInputMessage, HEADER_LENGTH )) {      // check input queue
        if ( ucInputMessage[ MSG_SOURCE_TASK ] == TASK_ARP) {
            // Note that we receive ARP messages only on our attempt to send a test message to a node with our allocated IP address.
            // Since DHCP uses broadcast messages until this point there can be no ARP errors
            fnRead( PortIDInternal, ucInputMessage, ucInputMessage[MSG_CONTENT_LENGTH]); // read the contents
            if (ARP_RESOLUTION_SUCCESS == ucInputMessage[ 0 ]) {
                if (ucResendAfterArp) {
                    fnSendDHCP(ucResendAfterArp);
                }
                else {
                    fnStateEventDHCP(E_DHCP_COLLISION);                  // this is bad news. We have received IP data from DHCP server but found that someone is already using the address. We have to try again
                }
            }
            else if (ARP_RESOLUTION_FAILED == ucInputMessage[ 0 ]) {
                // we have probed to ensure than no one else has the IP address which we have received
                // the probing failed which means we can use it - inform application...
                fnStateEventDHCP(E_DHCP_BIND_NOW);
            }
            ucResendAfterArp = 0;
        }
        else {
            fnStateEventDHCP(ucInputMessage[ MSG_TIMER_EVENT ]);           // timer event
        }
    }
}

// Call this function to start DHCP
//
extern int fnStartDHCP(UTASK_TASK Task)
{
    if ((DHCPSocketNr >= 0) || ((DHCPSocketNr = fnGetUDP_socket(TOS_MINIMISE_DELAY, fnDHCPListner, (UDP_OPT_SEND_CS | UDP_OPT_CHECK_CS))) >= 0)) {
        fnBindSocket(DHCPSocketNr, DHCP_CLIENT_PORT);
        MasterTask = (Task & ~FORCE_INIT);
        if (!(Task & FORCE_INIT) && (uMemcmp(&network.ucOurIP[0], cucNullMACIP, IPV4_LENGTH))) {   // if we have a non-zero IP address we will try to re-obtain it
            uMemcpy(ucDHCP_IP, &network.ucOurIP[0], IPV4_LENGTH);        // copy our IP address to the DHCP preferred address
            uMemset(&network.ucOurIP[0], 0, IPV4_LENGTH);                // remove the local IP since it may only be used after being validated
            ucDHCP_state = DHCP_STATE_INIT_REBOOT;                       // we already have a previous value - we will try to obtain it again
        }
        else {
            ucDHCP_state = DHCP_STATE_INIT;                              // we have none so we must start fresh
        }
        fnRandomise((4*SEC), E_START_DHCP);                              // perform DHCP state/event after short delay                    
        return 0;                                                        // OK
    }    
    return NO_UDP_SOCKET_FOR_DHCP;                                       // error
}

// Call to stop DHCP and return our preferred IP address
//
extern void fnStopDHCP(void)
{
    uTaskerStopTimer(OWN_TASK);                                          // stop all DHCP activity
    ucDHCP_state = DHCP_INIT;
    uMemcpy(&network.ucOurIP[0], ucDHCP_IP, IPV4_LENGTH);                // return the preferred IP address to the global value so that we can continue if we want
#ifdef _WINDOWS
    fnUpdateIPConfig();                                                  // update display in simulator
#endif
}

// Function allowing an option with IP address to be added to a DHCP message
//
static unsigned char *fnAddIPoption(unsigned char ucHDCP_option, unsigned char *ucIP, unsigned char *ucBuf)
{    
    *ucBuf++ = ucHDCP_option;                                            // DHCP message type
    *ucBuf++ = IPV4_LENGTH;                                              // length
    uMemcpy(ucBuf, ucIP, IPV4_LENGTH);                                   // add the IP address
    ucBuf += IPV4_LENGTH;
    return (ucBuf);
}

// Generate and send a DHCP message
//
static void fnSendDHCP(unsigned char ucDHCP_message)
{
    UDP_DHCP_MESSAGE tUDP_Message;                                       // message space
    unsigned char *ucBuf = tUDP_Message.ucUDP_Message;                   // insert to message content

    uMemset(ucBuf, 0, DHCP_BUFFER);                                      // ensure message contents cleared

    uMemcpy(ucBuf, cRequestHeader, sizeof(cRequestHeader));              // build the message (with default XID)
#ifdef RANDOM_NUMBER_GENERATOR 
    ucBuf += XID_OFFSET;
    ucXid[0] = (unsigned char)fnRandom();
    ucXid[1] = (unsigned char)fnRandom();
    ucXid[2] = (unsigned char)fnRandom();
    ucXid[3] = (unsigned char)fnRandom();
    uMemcpy(ucBuf, ucXid, XID_LENGTH);                                   // insert our XID
    ucBuf += (sizeof(cRequestHeader) - XID_LENGTH);
#else
    ucBuf += sizeof(cRequestHeader);
#endif
    
    if (ucDHCP_state & (DHCP_STATE_BOUND | DHCP_STATE_RENEWING | DHCP_STATE_REBINDING)) { // ciaddr is only sent when in BOUND, RENEW or REBINDING state
        uMemcpy(ucBuf, &network.ucOurIP[0], IPV4_LENGTH);                // insert our IP address
    }
    ucBuf += (4 * IPV4_LENGTH);                                          // leave yiaddr, siaddr and giaddr at zero
    
    uMemcpy(ucBuf, &network.ucOurMAC[0], MAC_LENGTH);                    // chaddr
    ucBuf += CHADD_LENGTH + SNAME_LENGTH + FILE_LENGTH;                  // leave sname and file blank
    
    uMemcpy(ucBuf, ucMagicCookie_and_message, sizeof(ucMagicCookie_and_message));
    ucBuf += sizeof(ucMagicCookie_and_message);                          // options field. First magic cookie and DHCP message type
    *ucBuf++ = ucDHCP_message;                                           // the message type we are building
        
    switch (ucDHCP_message) {                                            // next options depend on what state we're in and message type
        case DHCP_REQUEST:
        case DHCP_DECLINE:                                               // sent only from REQUESTING state
            if (!(ucDHCP_state & (DHCP_STATE_RENEWING | DHCP_STATE_REBINDING))) {  // not allowed in renewing or rebinding
                ucBuf = fnAddIPoption(DHCP_OPT_REQUESTED_IP, ucDHCP_IP, ucBuf);
            }    

            if (!(ucDHCP_state & (DHCP_STATE_INIT_REBOOT | DHCP_STATE_REBOOTING | DHCP_STATE_RENEWING | DHCP_STATE_REBINDING))) {
                ucBuf = fnAddIPoption(DHCP_OPT_SERV_IDENT, ucDHCP_SERVER_IP, ucBuf); // server identification
            }    
            if (ucDHCP_message == DHCP_DECLINE) {                        // decline doesn't send a parameter list
                 break;
            }
            // fall through intentional
        case DHCP_DISCOVER:        
            uMemcpy(ucBuf, cDHCP_op_param_req, sizeof(cDHCP_op_param_req)); // request parameter list (also for DHCP request)
            ucBuf += sizeof(cDHCP_op_param_req);
            break;            

        default:
            break;
    }
    *ucBuf = DHCP_OPT_END;                                               // end option list
        
    if (ucDHCP_state & (DHCP_STATE_BOUND | DHCP_STATE_RENEWING)) {       // send unicast - message is fixed length with padding
        if (NO_ARP_ENTRY == fnSendUDP(DHCPSocketNr, ucDHCP_SERVER_IP, DHCP_SERVER_PORT, (unsigned char *)&tUDP_Message.tUDP_Header, DHCP_BUFFER, OWN_TASK)) {
            ucResendAfterArp = ucDHCP_message;                            // we no longer have the address of the DHCP server in our ARP cache so we must repeat after the address has been resolved
        }
    }
    else {                                                               // or broadcast
        fnSendUDP(DHCPSocketNr, (unsigned char *)cucBroadcast, DHCP_SERVER_PORT, (unsigned char *)&tUDP_Message.tUDP_Header, DHCP_BUFFER, OWN_TASK);
    }
}

// Start an ARP enquiry to an IP address and wait for either ARP resolution success or failure
//
static void fnCheckIPAddress(void)
{
    uTaskerStopTimer(OWN_TASK);                                          // stop timer until we receive the result from the ARP test
    uMemcpy(&network.ucOurIP[0], ucDHCP_IP, IPV4_LENGTH);                // temporarily accept IP address as own
    fnDeleteArp();                                                       // ensure our ARP table is empty
    fnGetIP_ARP(&network.ucOurIP[0], OWN_TASK, -1);                      // cause a ping of our own address and wait for the result
}

// This function handles the DHCP state event machine
//
static void fnStateEventDHCP(unsigned char ucEvent)
{    
    switch (ucDHCP_state) {
        case DHCP_STATE_INIT_REBOOT:                                     // try to obtain a preferred address which we already know
            ucDHCP_state = DHCP_STATE_REBOOTING;                         // move to rebooting state
            DHCPTimeout = (DELAY_LIMIT)(4*SEC);                          // first timeout is 4s
            // fall through intentional
        case DHCP_STATE_REBOOTING:                                       // timeout while waiting for answer
            if (E_ACK_RECEIVED == ucEvent) {                             // ACK received, we first test the new IP address
                fnCheckIPAddress();                                      // check whether the IP address is really free and bind if OK
                break;
            }
            // fall through intentional
        case DHCP_STATE_REQUESTING:
            if ((E_REJECT_RECEIVED == ucEvent) || (E_DHCP_COLLISION == ucEvent)) {
                if (E_DHCP_COLLISION == ucEvent) {
                    fnSendDHCP(DHCP_DECLINE);                            // we must send a decline to the server in this case
                    fnInterruptMessage(MasterTask, DHCP_COLLISION);      // inform the application of the event
                }
                ucDHCP_state = DHCP_STATE_INIT;                          // NACK received. reset
                uMemset(&network.ucOurIP[0], 0, IPV4_LENGTH);            // clear our known IP address and restart from INIT
                fnRandomise((11*SEC), E_START_DHCP);                     // start between 10..12s
                break;
            }
            else if (E_DHCP_BIND_NOW == ucEvent) {                       // we have checked that a newly received IP is really free so we use it
#ifdef VERIFY_TIMERS                                                     // {6}
                if (ulLeaseTime <= (ulRebindingTime + 10)) {             // invalid or unrealistic rebinding time
                    ulRebindingTime = ((ulLeaseTime/4)*3);               // set a rebinding time of 3/4 the lease time
                }                
                if (ulRenewalTime >= (ulRebindingTime - 10)) {           // invalid or unrealistic renewal time
                    ulRenewalTime = ((ulRebindingTime/3)*2);             // set a renewal time of 2/3 the rebinding time (typically 1/2 the lease time)
                }
#endif
                ucDHCP_state = DHCP_STATE_BOUND;
                ulLeaseTime -= ulRebindingTime;                          // difference between lease expiracy and T2
                ulRebindingTime -= ulRenewalTime;                        // difference between T2 and T1
                fnStartDHCPTimer(E_START_RENEWAL);                       // start the renewal timer (T1)
                fnInterruptMessage(MasterTask, DHCP_SUCCESSFUL);         // we inform the master task that we have details to use the TCP/IP
#ifdef _WINDOWS
                fnUpdateIPConfig();                                      // update display in simulator
#endif
                break;
            }
            fnSendDHCP(DHCP_REQUEST);                                    // send the request message
            fnRandomise(DHCPTimeout, E_REQUEST_TIMEOUT);
            DHCPTimeout *= 2;                                            // exponential backoff for timeout
            if (DHCPTimeout > (DELAY_LIMIT)(64*SEC)) {                   // once the timeout gets too long we shorten and start again
                DHCPTimeout = (DELAY_LIMIT)(4*SEC);
                fnInterruptMessage(MasterTask, DHCP_MISSING_SERVER);     // inform the application that we are probably missing a server
            }
            break;

        case DHCP_STATE_INIT:                                            // try to obtain fresh parameters
            ucDHCP_state = DHCP_STATE_SELECTING;                         // move to selecting state
            DHCPTimeout = (DELAY_LIMIT)(4*SEC);                          // first timeout is 4s
            // fall through intentional
        case DHCP_STATE_SELECTING:                                       // timeout while waiting for offer
            fnSendDHCP(DHCP_DISCOVER);                                   // send the discover message
            fnRandomise(DHCPTimeout, E_DISCOVER_TIMEOUT);
            DHCPTimeout *= 2;                                            // exponential backoff for timeout
            if (DHCPTimeout > (DELAY_LIMIT)(64*SEC)) {                   // once the timeout gets too long we shorten and start again
                DHCPTimeout = (DELAY_LIMIT)(4*SEC);                      // go back to minimum retransmission
                fnInterruptMessage(MasterTask, DHCP_MISSING_SERVER);     // inform the application that we are probably missing a server
            }
            break;

        case DHCP_STATE_BOUND:
            if (fnStartDHCPTimer(ucEvent) == 0) {
                break;                                                   // intermediate timer - don't handle yet
            }

            if (E_RENEW_NOW == ucEvent) {
                ucDHCP_state = DHCP_STATE_RENEWING;                      // T1 expired, start renewing process
                fnSendDHCP(DHCP_REQUEST);
                ulRebindingTime /= 2;
                ulRenewalTime = ulRebindingTime;                         // the retransmission timer is set to half of the remaining
                // we don't check 60s minimum time here, only on subsequent repetitions
                fnStartDHCPTimer(E_START_RENEWAL);                       // use as retransmission timer
#ifdef _WINDOWS
                fnUpdateIPConfig();                                      // update display in simulator
#endif
            }
            break;

        case DHCP_STATE_RENEWING:
            if (E_RENEW_NOW == ucEvent) {                                // timeout
                if (!fnStartDHCPTimer(ucEvent)) break;                   // intermediate timer - don't handle yet

                ulRebindingTime /= 2;
                ulRenewalTime = ulRebindingTime;                         // the retransmission timer is set to half of the remaining
                if (ulRenewalTime < 60) {                                // we are not allowed to use retransmission times less that 60s so give up
                    ucDHCP_state = DHCP_STATE_REBINDING;                 // we don't check 60s minimum time here, only on subsequent repetitions
                    ulLeaseTime /= 2;                                    // set timeout to half the remaining time to lease end
                    ulRenewalTime = ulLeaseTime;
                    fnStartDHCPTimer(E_START_REBIND);                    // use as retransmission timer
                }
                else {
                    fnStartDHCPTimer(E_START_RENEWAL);                   // use as retransmission timer
                }
                fnSendDHCP(DHCP_REQUEST);
#ifdef _WINDOWS
                fnUpdateIPConfig();                                      // Update display in simulator
#endif
            }
            else if (E_ACK_RECEIVED == ucEvent) {                        // lease renewal successful
                ucDHCP_state = DHCP_STATE_BOUND;                         // go back to bound state
                ulLeaseTime -= ulRebindingTime;                          // difference between leaste expiracy and T2
                ulRebindingTime -= ulRenewalTime;                        // difference between T2 and T1
                fnStartDHCPTimer(E_START_RENEWAL);                       // start the renewal timer (T1)
            }
            break;

        case DHCP_STATE_REBINDING:
            if (E_REBIND_NOW == ucEvent) {                               // timeout in rebinding
                if (!fnStartDHCPTimer(ucEvent)) break;                   // intermediate timer - don't handle yet

                ulLeaseTime /= 2;                                        // set timeout to half the remaining time to lease end
                ulRenewalTime = ulLeaseTime;
                if (ulRenewalTime < 60) {                                // we are not allowed to use retransmission times less that 60s so give up
                    ucDHCP_state = DHCP_STATE_INIT;                      // NACK received. reset
                    uMemset(&network.ucOurIP[0], 0, IPV4_LENGTH);        // clear our known IP address and restart from INIT
                    fnRandomise((11*SEC), E_START_DHCP);                 // retry after 10..12s
                    fnInterruptMessage(MasterTask, DHCP_LEASE_TERMINATED);
                    break;
                }
                fnStartDHCPTimer(E_START_REBIND);                        // use as retransmission timer
                fnSendDHCP(DHCP_REQUEST);
            }
            else if (E_ACK_RECEIVED == ucEvent) {
                ucDHCP_state = DHCP_STATE_REQUESTING;                    // go to this state and wait for result of ARP test before going to bound state or else restarting
                fnCheckIPAddress();                                      // check whether the IP address is really free and bind if OK
            }
            break;
    }
}



// Read a DHCP option field and return value of last bytes (max 4, can be less)
//
static unsigned long fnReadDHCP_long(unsigned char **data)
{
    unsigned long ulRet = 0;
    unsigned char ucMsgLength = **data;
    unsigned char *ptr = (*data + 1);

    *data += ucMsgLength + 1;                                            // modify calling pointer

    while (ucMsgLength--) {                                              // collect data, resulting in the last 4 bytes as unsigned long
        ulRet <<= 8;
        ulRet |= *ptr++;
    }                                                                       
    return ulRet;
}

// Read a DHCP option field and return an IP address
//
static void fnReadDHCP_IP(unsigned char **data, unsigned char *ucIP)
{
    unsigned char ucMsgLength = **data;
    unsigned char *ptr = (*data + ucMsgLength - IPV4_LENGTH + 1);

    *data += ucMsgLength + 1;                                            // modify calling pointer

    uMemcpy(ucIP, ptr, IPV4_LENGTH);                                     // copy the IP address from the last 4 bytes of field
}

// Process a DHCP reception frame
//
static int fnDoRxDHCP(unsigned char *data, unsigned char *usOfferedIP, unsigned short usLength)
{
    int iOfferValid = 0;
    unsigned char ucMsgTyp = 0;                                          // {1}

    switch (ucDHCP_state) {
        case DHCP_STATE_SELECTING:                                       // we are waiting for an offer from a DHCP server
            while (*data != DHCP_OPT_END) {
                switch (*data++) {
                    case DHCP_OPT_PAD:                                   // skip pads
                        break;

                    case DHCP_OPT_MSG_TYPE:
                        data++;
                        ucMsgTyp = *data++;                              // save message type
                        break;

                    case DHCP_OPT_SERV_IDENT:
                        data++;
                        uMemcpy(ucDHCP_SERVER_IP, data, IPV4_LENGTH);    // read server identifier
                        data += IPV4_LENGTH;
                        iOfferValid = 1;                                 // mark that we have saved the offering server IP
                        break;

                    case DHCP_OPT_LEASE_TIME:                            // lease time received
                        ulLeaseTime = fnReadDHCP_long(&data);            // get the lease time in seconds
                        break;

#if !defined ALLOW_OVERLOAD_OPTIONS                                      // {3}
                    case DHCP_OPT_OVERLOAD:
                        return NO_SUPPORT_FOR_DHCP_OVERLOAD;             // we don't process overload messages
#endif

                    case DHCP_OPT_SUBNET_MASK:
                        fnReadDHCP_IP(&data, &network.ucNetMask[0]);
                        break;

                    case DHCP_OPT_ROUTER:
                        fnReadDHCP_IP(&data, &network.ucDefGW[0]);
                        break;
#ifdef USE_DNS
                    case DHCP_OPT_DNS_SERVER:
                        fnReadDHCP_IP(&data, &network.ucDNS_server[0]);
                        break;
#endif

                    default:                                             // non-supported options are skipped
                        fnReadDHCP_long(&data);
                        break;
                }
            }

            if ((!iOfferValid) || (ucMsgTyp != DHCP_OFFER)) {
                return NO_VALID_DHCP_MSG;                                // info not found - quit
            }

            ucDHCP_state = DHCP_STATE_REQUESTING;                        // move to requesting state
            uMemcpy(ucDHCP_IP, usOfferedIP, IPV4_LENGTH);                // save the offered IP
            
            fnSendDHCP(DHCP_REQUEST);                                    // send the request
            DHCPTimeout = (DELAY_LIMIT)(4*SEC);                          // first timeout is 4s
            fnRandomise(DHCPTimeout, E_REQUEST_TIMEOUT);
            break;

        case DHCP_STATE_REQUESTING:            
            while (*data != DHCP_OPT_END) {
                switch (*data++) {
                    case DHCP_OPT_SUBNET_MASK:
                        fnReadDHCP_IP(&data, &network.ucNetMask[0]);
                        break;

                    case DHCP_OPT_ROUTER:
                        fnReadDHCP_IP(&data, &network.ucDefGW[0]);
                        break;

                    case DHCP_OPT_SERV_IDENT:
                        data++;
                        uMemcpy(ucDHCP_SERVER_IP, data, IPV4_LENGTH);    // read server identifier
                        data += IPV4_LENGTH;
                        iOfferValid = 1;
                        break;

                    case DHCP_OPT_LEASE_TIME:                            // lease time received
                        ulLeaseTime = fnReadDHCP_long(&data);
                        break;

#ifdef USE_DNS
                    case DHCP_OPT_DNS_SERVER:
                        fnReadDHCP_IP(&data, &network.ucDNS_server[0]);
                        break;
#endif

                    case DHCP_OPT_MSG_TYPE:
                        data++;
                        if (*data++ == DHCP_ACK){
                            // probe to be sure that not already assigned to another local hardware - if the
                            // ARP check times out we are really bound..
                            fnCheckIPAddress();
                        }
                        else {
                            return (INVALID_DHCP_PACKET);                // not DHCP packet...
                        }    
                        break;

                    case DHCP_OPT_T1_VALUE:                              // renewal time value in s
                        ulRenewalTime = fnReadDHCP_long(&data);          // value in s
                        break;

                    case DHCP_OPT_T2_VALUE:                              // rebinding time value
                        ulRebindingTime = fnReadDHCP_long(&data);        // value in s
                        break;

#if !defined ALLOW_OVERLOAD_OPTIONS                                      // {3}
                    case DHCP_OPT_OVERLOAD:
                        return NO_SUPPORT_FOR_DHCP_OVERLOAD;             // we don't process overload messages
#endif

                    case DHCP_OPT_PAD:                                   // skip pads
                        break;

                    case DHCP_OPT_HOST_NAME:                             // we don't save this
                    default:
                        fnReadDHCP_long(&data);
                        break;
                }
            }
            break;

        case DHCP_STATE_RENEWING:
        case DHCP_STATE_REBINDING:
        case DHCP_STATE_REBOOTING:
            while (*data != DHCP_OPT_END) {                              // process all options until end is encountered
                switch (*data++) {
                    case DHCP_OPT_PAD:                                   // pad so ignore
                        break;

                    case DHCP_OPT_LEASE_TIME:                            // lease time received 
                        ulLeaseTime = fnReadDHCP_long(&data);            // get the IP address lease time in seconds
                        break;                                

                    case DHCP_OPT_MSG_TYPE:
                        data++;                                          // jump length which is always 1
                        if (*data == DHCP_NAK) {
                            fnStateEventDHCP(E_REJECT_RECEIVED); 
                            return (DHCP_REBOOT_REJECTED);
                        }

                        if (*data == DHCP_ACK) {
                            iOfferValid = 1;
                        }
                        data++;
                        break;

                    case DHCP_OPT_T1_VALUE:                              // renewal time value in s
                        ulRenewalTime = fnReadDHCP_long(&data);          // value in s
                        break;

                    case DHCP_OPT_T2_VALUE:                              // rebinding time value
                        ulRebindingTime = fnReadDHCP_long(&data);        // value in s
                        break;

                    default:                                             // unsupported, read the options but ignore them
                        fnReadDHCP_long(&data);
                        break;
                }
            }
            if (iOfferValid) {
                fnStateEventDHCP(E_ACK_RECEIVED);
            }
            break;

        default:
            return NO_OPTIONS_ALLOWED_IN_DHCP_STATE;                     // don't process any options in other states
    }
    return 0;
}


// The UDP listner function for DHCP port
//
static int fnDHCPListner(USOCKET SocketNr, unsigned char ucEvent, unsigned char *ucIP, unsigned short usPortNr, unsigned char *data, unsigned short usLength)
{
    unsigned char usOfferedIP[IPV4_LENGTH];
#ifdef SUPPORT_RELAY_AGENT
  //unsigned char usNextServerIP[IPV4_LENGTH];
    unsigned char usRelayAgentIP[IPV4_LENGTH];
#endif

    if (SocketNr != DHCPSocketNr) {
        return NOT_DHCP_SOCKET;                                          // not our socket so ignore
    }
    
    switch (ucEvent) {
        case UDP_EVENT_PORT_UNREACHABLE:                                 // we have sent a UDP frame to a device with no socket open on the relevant port
           break;

        case UDP_EVENT_RXDATA:                                           // reception frame on our port
            if ((usLength < (236 - UDP_HLEN)) || (*data++ != BOOT_REPLY)) {
                return INVALID_DHCP_PACKET;                              // bad length or not boot reply
            }
                
            data += 3;                                                   // skip htype, hlen, hops
    
#ifndef ETHEREAL            
    #ifdef RANDOM_NUMBER_GENERATOR 
            if (uMemcmp(data, ucXid, XID_LENGTH)) {
                return FOREIGN_DHCP_PACKET;                              // XID not as expected
            }
    #else
            if (uMemcmp(data, &cRequestHeader[XID_OFFSET], XID_LENGTH)) {
                return FOREIGN_DHCP_PACKET;                              // XID not as expected
            }
    #endif
#endif                
            data += (8 + XID_LENGTH);                                    // skip secs, flags, ciaddr

            uMemcpy(usOfferedIP, data, IPV4_LENGTH);                     // get offered address
#ifdef SUPPORT_RELAY_AGENT
          //uMemcpy(usNextServerIP, (data + IPV4_LENGTH), IPV4_LENGTH);  // get next server IP address
            uMemcpy(usRelayAgentIP, (data + (2 * IPV4_LENGTH)), IPV4_LENGTH); // get relay agent IP address
#endif
            data += (IPV4_LENGTH * 3);                                   // move past yiaddr, skip siaddr, giaddr

            if (uMemcmp(data, &network.ucOurMAC[0], MAC_LENGTH)) {       // {4}
                return FOREIGN_DHCP_PACKET;                              // chaddr not as expected (offer is to another client)
            }
            
            data += (16 + 64 + 128);                                     // skip chaddr, sname, file and get all info from the options field                
                                                                         // check magic cookie
            if (uMemcmp(data, ucMagicCookie_and_message, MAGIC_COOKIE_LENGTH)) {
                return BAD_MAGIC_COOKIE;                                 // magic cookie is bad - quit
            }
            data += MAGIC_COOKIE_LENGTH;

            if (ucDHCP_state == DHCP_STATE_REQUESTING) {                 // we do this check here simply to avoid having to pass parameters
#ifdef SUPPORT_RELAY_AGENT
                if ((uMemcmp(usOfferedIP, ucDHCP_IP, IPV4_LENGTH) != 0) || ((uMemcmp(ucDHCP_SERVER_IP, ucIP, IPV4_LENGTH) != 0) && (uMemcmp(usRelayAgentIP, ucIP, IPV4_LENGTH) != 0))) {
                    return FOREIGN_DHCP_PACKET;                          // server or requested IP not same
                }
#else
                if ((uMemcmp(ucDHCP_SERVER_IP, ucIP, IPV4_LENGTH)) || (uMemcmp(usOfferedIP, ucDHCP_IP, IPV4_LENGTH))) {
                    return FOREIGN_DHCP_PACKET;                          // server or requested IP not same
                }
#endif
            }
                                                                         // the initial checks were successful - now we do state specific stuff
            return (fnDoRxDHCP(data, usOfferedIP, (unsigned short)(usLength + UDP_HLEN - (3 + 8 + XID_LENGTH + IPV4_LENGTH + 216))));
    }
    return 0;
}

// This routine handles the renewal timers. It is special because it allows the handling of possibly very long
// delays with limited maximum timer length
//
static unsigned char fnStartDHCPTimer(unsigned char ucTimerEvent)
{
    DELAY_LIMIT ThisDelay;
    unsigned char ucNewEvent;
    #define MAX_TIMER_DELAY (((DELAY_LIMIT)0xffffffff)/(SEC)/*TICK_RESOLUTION*/) // {5}

    switch (ucTimerEvent) {
    case E_START_RENEWAL:
        if (0xffffffff == ulRenewalTime) {
            return 0;                                                    // if we have received an infinite lease time we do not start the timer
        }
        ucNewEvent = E_RENEW_NOW;
        break;

    case E_START_REBIND:
        ucNewEvent = E_REBIND_NOW;
        break;

    default:
        ucNewEvent = ucTimerEvent;
        break;
    }
    if (!ulRenewalTime) {
        return ucTimerEvent;                                             // the timer has completely timed out
    }
    else if (ulRenewalTime >= (MAX_TIMER_DELAY)) {                       // note that the T1,T2 times are in seconds and we have to convert
        ThisDelay = (DELAY_LIMIT)0xffffffff;
        ulRenewalTime -= (MAX_TIMER_DELAY);
    }
    else {
        ThisDelay = (DELAY_LIMIT)(((DELAY_LIMIT)ulRenewalTime)*SEC);
        ulRenewalTime = 0;
    }

    uTaskerMonoTimer( OWN_TASK, ThisDelay, ucNewEvent );
    return 0;
}


// This routine randomises the value to +/- 1 second of passed time value
//
static void fnRandomise(DELAY_LIMIT DHCPTimeout, unsigned char ucTimerEvent)
{
#ifdef RANDOM_NUMBER_GENERATOR                                           // {2}
    DELAY_LIMIT random_sec = (fnRandom() / (0xffff/(4*TICK_RESOLUTION)));
    if (random_sec > (DELAY_LIMIT)(1*SEC)) {
        DHCPTimeout -= ((DELAY_LIMIT)(2*SEC) - random_sec);              // decrease of 0..1s
    }
    else {
        DHCPTimeout += random_sec;                                       // increase of 0..1s   
    }
#else                                                                    // simply change time +/- 0.5s each time
    static unsigned char random_sec = 0;
    if (random_sec != 0) {
        DHCPTimeout += (DELAY_LIMIT)(0.5*SEC);
    }
    else {
        DHCPTimeout -= (DELAY_LIMIT)(0.5*SEC);
    }
    random_sec ^= 1;
#endif
    uTaskerMonoTimer( OWN_TASK, DHCPTimeout, ucTimerEvent );                               
}

#ifdef _WINDOWS
extern unsigned char fnGetDHCP_State(void)
{
    return ucDHCP_state;
}
#endif

#endif


