// $Id: DHCPPacketDecoder.java,v 1.13 2004/11/03 16:17:05 dphull Exp $

import java.util.HashMap;

public final class DHCPPacketDecoder
{
	private DHCPFingerprint dhcpFingerprint;

  public DHCPPacketDecoder(String fingerprintsFile)
  {
		dhcpFingerprint = DHCPFingerprintLoader.load(fingerprintsFile);
  }

  public DHCPClientInformation decode(byte[] rawPacket)
  {
		int messageType;
		
		if ((rawPacket == null) || (rawPacket.length < 48))
    {
			DHCPClientInformation dci = new DHCPClientInformation();
			dci.setMessageType("Badly formed packet");
			return dci;
    }
		
		messageType = getMessageType(rawPacket);
		if (messageType == 1 || messageType == 3) 
		{
			HashMap dhcpOptions = decodeOptions(rawPacket);

	    DHCPClientInformation dci = new DHCPClientInformation();

	    dci.setVendorClassID(decodeVendorClassID(dhcpOptions));
		  dci.setRequestedIP(decodeRequestedIP(dhcpOptions));
			dci.setSystemName(decodeSystemName(dhcpOptions));
	    dci.setMessageType(decodeMessageType(dhcpOptions));
		  dci.setMacAddress(decodeMAC(rawPacket));
			dci.setOperatingSystem(decodeOS(dhcpOptions));
                
	    byte[] rawIP = new byte[4];
		  System.arraycopy(rawPacket, 12, rawIP, 0, 4);
			dci.setIpAddress(decodeIP(rawIP));

			return dci;
		} 
		else 
		{
			DHCPClientInformation dci = new DHCPClientInformation();
			dci.setMessageType("IGNORE");
			return dci;
		}
	}

		private int getMessageType(byte[] rawPacket) 
		{
			final int optionsStart = 240;
			int index = 0;
			 
			while (optionsStart + index < rawPacket.length) 
			{
				if (rawPacket[optionsStart + index] == 0) 
				{
					index++;
					continue;
				} 
				else if (rawPacket[optionsStart + index] == 53) 
				{
					return rawPacket[optionsStart + index + 2];
				}
				else if (rawPacket[optionsStart + index] == 255) 
				{
					return 0;
				} 
				else
				{
					index += rawPacket[optionsStart + index++];
				}
			}
			return 0;			
		}


    public static String decodeIP(byte[] rawIP)
    {
        if(rawIP == null)
        {
          return "null";
        }

        String theIP = "";
        int octet;

        for (int i = 0; i < 4; i++)
        {
            octet = rawIP[i];

            if (octet < 0)
            {
                octet += 256;
            }

            theIP += octet;

            if (i < 3)
            {
                theIP += ".";
            }
        }
        
        if(theIP.equals("0.0.0.0"))
        {
          return null;
        }

        return (theIP);
    }

    public static String decodeRequestedIP(HashMap dhcpOptions)
    {
      return decodeIP((byte[])dhcpOptions.get("50"));
    }

    public String decodeVendorClassID(HashMap dhcpOptions)
    {
      return extractString((byte[]) dhcpOptions.get("60"));
    }

    public static String decodeSystemName(HashMap dhcpOptions)
    {
      return extractString((byte[]) dhcpOptions.get("12"));
    }

    private static String extractString(byte[] raw)
    {
      if(raw != null)
      {
        StringBuffer sb = new StringBuffer(raw.length);
        for(int i=0;i<raw.length;i++)
        {
          sb.append((char)(raw[i]));
        }
        return sb.toString();
      }
      else
      {
        return null;
      }
    }

    public static String decodeMessageType(HashMap dhcpOptions)
    {
        byte[] messageType = (byte[]) dhcpOptions.get("53");

        if (messageType == null)
        {
            return "NULL";
        }
        else
        {
            switch ((int)messageType[0])
            {
            case 1:
                return "DISCOVER";
            case 3:
                return "REQUEST";
            default:
                return "UNKNOWN";
            }
        }
    }

    private String decodeMAC(byte[] rawPacket)
    {
        byte[] macAddress = new byte[6]; // not all 16
        System.arraycopy(rawPacket, 28, macAddress, 0, 6);

        // Convert the byte array to string form
        StringBuffer macBuffer = new StringBuffer(17);
        int octet;

        for (int i = 0; i < 6; i++)
        {
            octet = macAddress[i];

            if (octet < 0)
            {
                octet += 256;
            }

            if (octet < 16)
            {
                macBuffer.append("0");
            }

            macBuffer.append(Integer.toString(octet, 16));

            if (i < 5)
            {
                macBuffer.append(":");
            }
        }

        return macBuffer.toString();
    }

    private String decodeOS(HashMap dhcpOptions)
    {
        byte[] dhcpRequestedOptions = (byte[]) dhcpOptions.get("55");

        String os = dhcpFingerprint.get(dhcpRequestedOptions);

        if (os != null)
        {
            return os;
        }
        else
        {
            if (dhcpRequestedOptions == null)
            {
                return "NONE";
            }
            else
            {
                StringBuffer sb = new StringBuffer(54);
                sb.append("{ ");

                for (int i = 0; i < dhcpRequestedOptions.length; i++)
                {
                    sb.append(dhcpRequestedOptions[i]);

                    if ((i + 1) != dhcpRequestedOptions.length)
                    {
                        sb.append(", ");
                    }
                }

                sb.append(" }");

                return sb.toString();
            }
        }
    }
		
		private HashMap decodeOptions(byte[] rawPacket)
    {
			final int optionsStart = 240;
      boolean found = false;
			int optionsLength = 0;

      while ((optionsStart + optionsLength < rawPacket.length) && !found)
      {
				if (rawPacket[optionsStart + optionsLength++] == (byte) 255)
					{
						found = true;
          }
      }

      if (found)
      {
				byte[] options = new byte[optionsLength];
				byte code;
				byte lengthOctet;
				byte[] value;
				int optionsPosition = 0;
				HashMap dhcpOptions = new HashMap();
				boolean done = false;

				System.arraycopy(rawPacket, optionsStart, options, 0, options.length);
					
				while (optionsPosition < options.length)
				{
					code = options[optionsPosition++];
					if ((code != 0) && (code != (byte) 255)) 
					{
						lengthOctet = options[optionsPosition++];
						if (lengthOctet + optionsPosition < options.length) 
						{
							if (lengthOctet > 0)
							{
								value = new byte[(int) lengthOctet];							
						    for (int j = 0; j < (int) lengthOctet; j++)
							  {
									value[j] = options[optionsPosition++];
								}	
								dhcpOptions.put(Byte.toString(code), value);
							}
							else 
							{
								System.out.println("WARNING: Negative length octet (" + lengthOctet + ") found.");
							}
						} 
						else
						{
							System.out.println("WARNING: Option " + code + " starts at " + optionsPosition + " and has a length of " + lengthOctet + ".");
							System.out.println("WARNING: This exceeds the total length of the options array which is " + options.length + ".");
						}
					}
				}	
				return dhcpOptions;
      }
      else
      {
				return null;
      }
		}
}
