/*
* Copyright 2009-2014 Jagornet Technologies, LLC. All Rights Reserved.
*
* This software is the proprietary information of Jagornet Technologies, LLC.
* Use is subject to license terms.
*
*/
/*
* This file DhcpV4Message.java is part of Jagornet DHCP.
*
* Jagornet DHCP 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 3 of the License, or
* (at your option) any later version.
*
* Jagornet DHCP 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 Jagornet DHCP. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.jagornet.dhcp.message;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.jagornet.dhcp.option.base.DhcpOption;
import com.jagornet.dhcp.option.v4.DhcpV4MsgTypeOption;
import com.jagornet.dhcp.option.v4.DhcpV4OptionFactory;
import com.jagornet.dhcp.option.v4.DhcpV4ParamRequestOption;
import com.jagornet.dhcp.option.v4.DhcpV4ServerIdOption;
import com.jagornet.dhcp.util.DhcpConstants;
import com.jagornet.dhcp.util.Util;
/**
* Title: DhcpV4Message
* Description: Object that represents a DHCPv4 message as defined in
* RFC 2131.
*
* The following diagram illustrates the format of DHCP messages sent
* between clients and servers:
*
* <pre>
* 0 1 2 3
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | op (1) | htype (1) | hlen (1) | hops (1) |
* +---------------+---------------+---------------+---------------+
* | xid (4) |
* +-------------------------------+-------------------------------+
* | secs (2) | flags (2) |
* +-------------------------------+-------------------------------+
* | ciaddr (4) |
* +---------------------------------------------------------------+
* | yiaddr (4) |
* +---------------------------------------------------------------+
* | siaddr (4) |
* +---------------------------------------------------------------+
* | giaddr (4) |
* +---------------------------------------------------------------+
* | |
* | chaddr (16) |
* | |
* | |
* +---------------------------------------------------------------+
* | |
* | sname (64) |
* +---------------------------------------------------------------+
* | |
* | file (128) |
* +---------------------------------------------------------------+
* | |
* | options (variable) |
* +---------------------------------------------------------------+
* </pre>
*
* @author A. Gregory Rabil
*/
public class DhcpV4Message implements DhcpMessage
{
private static Logger log = LoggerFactory.getLogger(DhcpV4Message.class);
// true if the message was received on a unicast socket, false otherwise
protected boolean unicast;
// the IP and port on the local host on
// which the message is sent or received
protected InetSocketAddress localAddress;
// the IP and port on the remote host from
// which the message is received or sent
protected InetSocketAddress remoteAddress;
protected short op = 0; // need a short to hold unsigned byte
protected short htype = 0;
protected short hlen = 0;
protected short hops = 0;
protected long transactionId = 0; // need a long to hold unsigned int
protected int secs = 0;
protected int flags = 0;
protected InetAddress ciAddr = DhcpConstants.ZEROADDR_V4;
protected InetAddress yiAddr = DhcpConstants.ZEROADDR_V4;
protected InetAddress siAddr = DhcpConstants.ZEROADDR_V4;
protected InetAddress giAddr = DhcpConstants.ZEROADDR_V4;
protected byte[] chAddr;
protected String sName;
protected String file;
protected static byte[] magicCookie = new byte[] { (byte)99, (byte)130, (byte)83, (byte)99 };
protected Map<Integer, DhcpOption> dhcpOptions = new HashMap<Integer, DhcpOption>();
/**
* Construct a DhcpMessage.
*
* @param localAddress InetSocketAddress on the local host on which
* this message is received or sent
* @param remoteAddress InetSocketAddress on the remote host on which
* this message is sent or received
*/
public DhcpV4Message(InetSocketAddress localAddress, InetSocketAddress remoteAddress)
{
this.localAddress = localAddress;
this.remoteAddress = remoteAddress;
}
public void setUnicast(boolean unicast)
{
this.unicast = unicast;
}
public boolean isUnicast()
{
return unicast;
}
/**
* Encode this DhcpMessage to wire format for sending.
*
* @return a ByteBuffer containing the encoded DhcpMessage
* @throws IOException
*/
public ByteBuffer encode() throws IOException
{
if (log.isDebugEnabled())
log.debug("Encoding DhcpMessage for: " +
Util.socketAddressAsString(remoteAddress));
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.put((byte)op);
buf.put((byte)htype);
buf.put((byte)hlen);
buf.put((byte)hops);
buf.putInt((int)transactionId);
buf.putShort((short)secs);
buf.putShort((short)flags);
if (ciAddr != null) {
buf.put(ciAddr.getAddress());
}
else {
buf.put(DhcpConstants.ZEROADDR_V4.getAddress());
}
if (yiAddr != null) {
buf.put(yiAddr.getAddress());
}
else {
buf.put(DhcpConstants.ZEROADDR_V4.getAddress());
}
if (siAddr != null) {
buf.put(siAddr.getAddress());
}
else {
buf.put(DhcpConstants.ZEROADDR_V4.getAddress());
}
if (giAddr != null) {
buf.put(giAddr.getAddress());
}
else {
buf.put(DhcpConstants.ZEROADDR_V4.getAddress());
}
buf.put(Arrays.copyOf(chAddr, 16)); // pad to 16 bytes for encoded packet
StringBuffer sNameBuf = new StringBuffer();
if (sName != null) {
sNameBuf.append(sName);
}
sNameBuf.setLength(64-sNameBuf.length());
buf.put(sNameBuf.toString().getBytes());
StringBuffer fileBuf = new StringBuffer();
if (file != null) {
fileBuf.append(file);
}
fileBuf.setLength(128-fileBuf.length());
buf.put(fileBuf.toString().getBytes());
buf.put(encodeOptions());
int msglen = buf.position();
if (log.isDebugEnabled())
log.debug("DHCPv4 Message is " + msglen + " bytes");
if (msglen < 300) {
int pad = 300 - msglen;
if (log.isDebugEnabled())
log.debug("Padding with " + pad + " bytes to 300 byte (Bootp) minimum");
buf.put(new byte[pad]);
}
buf.flip();
if (log.isDebugEnabled())
log.debug("DhcpMessage encoded.");
return buf;
}
/**
* Encode the options of this DhcpMessage to wire format for sending.
*
* @return a ByteBuffer containing the encoded options
* @throws IOException
*/
protected ByteBuffer encodeOptions() throws IOException
{
ByteBuffer buf = ByteBuffer.allocate(788); // 788 - 236 = 1020 (options)
if (dhcpOptions != null) {
// magic cookie as per rfc1497
buf.put(magicCookie);
for (DhcpOption option : dhcpOptions.values()) {
buf.put(option.encode());
}
buf.put((byte)DhcpConstants.V4OPTION_EOF); // end option
}
return (ByteBuffer)buf.flip();
}
/**
* Decode a packet received on the wire into a DhcpMessage object.
*
* @param buf ByteBuffer containing the packet to be decoded
* @param localAddr InetSocketAddress on the local host on which
* packet was received
* @param remoteAddr InetSocketAddress on the remote host from which
* the packet was received
* @return a decoded DhcpMessage object, or null if the packet could not be decoded
* @throws IOException
*/
public static DhcpV4Message decode(ByteBuffer buf, InetSocketAddress localAddr, InetSocketAddress remoteAddr)
throws IOException
{
DhcpV4Message dhcpMessage = null;
if ((buf != null) && buf.hasRemaining()) {
if (log.isDebugEnabled()) {
log.debug("Decoding packet:" +
" size=" + buf.limit() +
" localAddr=" + Util.socketAddressAsString(localAddr) +
" remoteAddr=" + Util.socketAddressAsString(remoteAddr));
}
// we'll "peek" at the message type to use for this mini-factory
buf.mark();
byte _op = buf.get();
if (log.isDebugEnabled())
log.debug("op byte=" + _op);
// allow for reply(op=2) messages for use by client
// TODO: see if this is a problem for the server
if ((_op == 1) || (_op == 2)) {
dhcpMessage = new DhcpV4Message(localAddr, remoteAddr);
}
else {
log.error("Unsupported op code: " + _op);
}
if (dhcpMessage != null) {
// reset the buffer to point at the message type byte
// because the message decoder will expect it
buf.reset();
dhcpMessage.decode(buf);
}
}
else {
String errmsg = "Buffer is null or empty";
log.error(errmsg);
throw new IOException(errmsg);
}
return dhcpMessage;
}
/**
* Decode a datagram packet into this DhcpMessage object.
*
* @param buf ByteBuffer containing the packet to be decoded
* @throws IOException
*/
public void decode(ByteBuffer buf) throws IOException
{
if (log.isDebugEnabled())
log.debug("Decoding DhcpV4Message from: " +
Util.socketAddressAsString(remoteAddress));
if ((buf != null) && buf.hasRemaining()) {
// buffer must be at least the size of the fixed
// portion of the DHCPv4 header plus the required
// "magic cookie", and at least the message type option
// and the end option: 244 = 236 + 4 + 3 + 1
if (buf.limit() >= 244) {
op = buf.get();
log.debug("op=" + op);
if (op != 1) {
// TODO
}
htype = buf.get();
if (log.isDebugEnabled())
log.debug("htype=" + htype);
hlen = buf.get();
log.debug("hlen=" + hlen);
hops = buf.get();
if (log.isDebugEnabled())
log.debug("hops=" + hops);
transactionId = buf.getInt();
if (log.isDebugEnabled())
log.debug("xid=" + transactionId);
secs = buf.getShort();
if (log.isDebugEnabled())
log.debug("secs=" + secs);
flags = buf.getShort();
if (log.isDebugEnabled())
log.debug("flags=" + flags);
byte[] ipbuf = new byte[4];
buf.get(ipbuf);
ciAddr = InetAddress.getByAddress(ipbuf);
if (log.isDebugEnabled())
log.debug("ciaddr=" + ciAddr.getHostAddress());
buf.get(ipbuf);
yiAddr = InetAddress.getByAddress(ipbuf);
if (log.isDebugEnabled())
log.debug("yiaddr=" + yiAddr.getHostAddress());
buf.get(ipbuf);
siAddr = InetAddress.getByAddress(ipbuf);
if (log.isDebugEnabled())
log.debug("siaddr=" + siAddr.getHostAddress());
buf.get(ipbuf);
giAddr = InetAddress.getByAddress(ipbuf);
if (log.isDebugEnabled())
log.debug("giaddr=" + giAddr.getHostAddress());
byte[] chbuf = new byte[16];
buf.get(chbuf);
chAddr = Arrays.copyOf(chbuf, hlen); // hlen defines len of chAddr
if (log.isDebugEnabled())
log.debug("chaddr=" + Util.toHexString(chAddr));
byte[] sbuf = new byte[64];
buf.get(sbuf);
sName = new String(sbuf);
if (log.isDebugEnabled())
log.debug("sname=" + sName);
byte[] fbuf = new byte[128];
buf.get(fbuf);
file = new String(fbuf);
if (log.isDebugEnabled())
log.debug("file=" + file);
byte[] cookieBuf = new byte[4];
buf.get(cookieBuf);
if (!Arrays.equals(cookieBuf, magicCookie)) {
String errmsg = "Failed to decode DHCPv4 message: invalid magic cookie";
log.error(errmsg);
throw new IOException(errmsg);
}
decodeOptions(buf);
}
else {
String errmsg = "Failed to decode DHCPv4 message: packet too short";
log.error(errmsg);
throw new IOException(errmsg);
}
}
else {
String errmsg = "Failed to decode message: buffer is empty";
log.error(errmsg);
throw new IOException(errmsg);
}
if (log.isDebugEnabled()) {
log.debug("DhcpMessage decoded.");
}
}
/**
* Decode the options.
* @param buf ByteBuffer positioned at the start of the options in the packet
* @return a Map of DhcpOptions keyed by the option code
* @throws IOException
*/
protected Map<Integer, DhcpOption> decodeOptions(ByteBuffer buf)
throws IOException
{
while (buf.hasRemaining()) {
short code = Util.getUnsignedByte(buf);
if (log.isDebugEnabled())
log.debug("Option code=" + code);
DhcpOption option = DhcpV4OptionFactory.getDhcpOption(code);
if (option != null) {
option.decode(buf);
dhcpOptions.put(option.getCode(), option);
}
else {
break; // no more options, or one is malformed, so we're done
}
}
return dhcpOptions;
}
/**
* Return the length of this DhcpMessage in bytes.
* @return an int containing a length of a least four(4)
*/
public int getLength()
{
// portion of the DHCPv4 header plus the required
// "magic cookie", and at least the message type option
// and the end option: 244 = 236 + 4 + 3 + 1
int len = 244;
len += getOptionsLength();
return len;
}
/**
* Return the length of the options in this DhcpMessage in bytes.
* @return an int containing the total length of all options
*/
protected int getOptionsLength()
{
int len = 0;
if (dhcpOptions != null) {
for (DhcpOption option : dhcpOptions.values()) {
len += 2; // option code (1 byte) + length (1 byte)
len += option.getLength();
}
}
return len;
}
public InetSocketAddress getLocalAddress() {
return localAddress;
}
public void setLocalAddress(InetSocketAddress localAddress) {
this.localAddress = localAddress;
}
public InetSocketAddress getRemoteAddress() {
return remoteAddress;
}
public void setRemoteAddress(InetSocketAddress remoteAddress) {
this.remoteAddress = remoteAddress;
}
public boolean hasOption(int optionCode)
{
if(dhcpOptions.containsKey(optionCode)) {
return true;
}
return false;
}
public DhcpOption getDhcpOption(int optionCode)
{
return dhcpOptions.get(optionCode);
}
public void putDhcpOption(DhcpOption dhcpOption)
{
if(dhcpOption != null) {
dhcpOptions.put(dhcpOption.getCode(), dhcpOption);
}
}
public void putAllDhcpOptions(Map<Integer, DhcpOption> dhcpOptions)
{
this.dhcpOptions.putAll(dhcpOptions);
}
public Map<Integer, DhcpOption> getDhcpOptionMap()
{
return dhcpOptions;
}
public void setDhcpOptionMap(Map<Integer, DhcpOption> dhcpOptions)
{
this.dhcpOptions = dhcpOptions;
}
public Collection<DhcpOption> getDhcpOptions()
{
return dhcpOptions.values();
}
private DhcpV4ServerIdOption dhcpServerIdOption;
/**
* Convenience method to get ServerID option.
* @return
*/
public DhcpV4ServerIdOption getDhcpV4ServerIdOption() {
if (dhcpServerIdOption == null) {
if (dhcpOptions != null) {
dhcpServerIdOption =
(DhcpV4ServerIdOption) dhcpOptions.get(DhcpConstants.V4OPTION_SERVERID);
}
}
return dhcpServerIdOption;
}
private List<Integer> requestedOptionCodes;
/**
* Convenience method to get the requested option codes.
* @return
*/
public List<Integer> getRequestedOptionCodes() {
if (requestedOptionCodes == null) {
if (dhcpOptions != null) {
DhcpV4ParamRequestOption pro =
(DhcpV4ParamRequestOption) dhcpOptions.get(DhcpConstants.V4OPTION_PARAM_REQUEST_LIST);
if (pro != null) {
requestedOptionCodes = new ArrayList<Integer>();
for (short ubyte : pro.getUnsignedByteList()) {
requestedOptionCodes.add((int)ubyte);
}
}
}
}
return requestedOptionCodes;
}
public String toString()
{
StringBuffer sb = new StringBuffer();
sb.append(DhcpConstants.getV4MessageString(getMessageType()));
if (this.getOp() == DhcpConstants.V4_OP_REPLY)
sb.append(" to ");
else
sb.append(" from ");
sb.append(Util.socketAddressAsString(remoteAddress));
sb.append(" (htype=");
sb.append(this.getHtype());
sb.append(", hlen=");
sb.append(this.getHlen());
sb.append(", hops=");
sb.append(this.getHops());
sb.append(", xid=");
sb.append(this.getTransactionId());
sb.append(", secs=");
sb.append(this.getSecs());
sb.append(", flags=");
sb.append(this.getFlags());
sb.append(", ciaddr=");
sb.append(this.getCiAddr().getHostAddress());
sb.append(", yiaddr=");
sb.append(this.getYiAddr().getHostAddress());
sb.append(", siaddr=");
sb.append(this.getSiAddr().getHostAddress());
sb.append(", giaddr=");
sb.append(this.getGiAddr().getHostAddress());
sb.append(", chaddr=");
sb.append(Util.toHexString(this.getChAddr()));
sb.append(')');
return sb.toString();
}
public String toStringWithOptions()
{
StringBuffer sb = new StringBuffer(this.toString());
if ((dhcpOptions != null) && !dhcpOptions.isEmpty()) {
sb.append(Util.LINE_SEPARATOR);
sb.append("dhcpOptions");
for (DhcpOption dhcpOption : dhcpOptions.values()) {
sb.append(dhcpOption.toString());
}
}
return sb.toString();
}
public short getOp() {
return op;
}
public void setOp(short op) {
this.op = op;
}
public short getHtype() {
return htype;
}
public void setHtype(short htype) {
this.htype = htype;
}
public short getHlen() {
return hlen;
}
public void setHlen(short hlen) {
this.hlen = hlen;
}
public short getHops() {
return hops;
}
public void setHops(short hops) {
this.hops = hops;
}
public long getTransactionId() {
return transactionId;
}
public void setTransactionId(long transactionId) {
this.transactionId = transactionId;
}
public int getSecs() {
return secs;
}
public void setSecs(int secs) {
this.secs = secs;
}
public int getFlags() {
return flags;
}
public void setFlags(int flags) {
this.flags = flags;
}
public InetAddress getCiAddr() {
return ciAddr;
}
public void setCiAddr(InetAddress ciAddr) {
this.ciAddr = ciAddr;
}
public InetAddress getYiAddr() {
return yiAddr;
}
public void setYiAddr(InetAddress yiAddr) {
this.yiAddr = yiAddr;
}
public InetAddress getSiAddr() {
return siAddr;
}
public void setSiAddr(InetAddress siAddr) {
this.siAddr = siAddr;
}
public InetAddress getGiAddr() {
return giAddr;
}
public void setGiAddr(InetAddress giAddr) {
this.giAddr = giAddr;
}
public byte[] getChAddr() {
return chAddr;
}
public void setChAddr(byte[] chAddr) {
this.chAddr = chAddr;
}
public String getsName() {
return sName;
}
public void setsName(String sName) {
this.sName = sName;
}
public String getFile() {
return file;
}
public void setFile(String file) {
this.file = file;
}
public void setMessageType(short msgType) {
DhcpV4MsgTypeOption msgTypeOption = new DhcpV4MsgTypeOption();
msgTypeOption.setUnsignedByte(msgType);
putDhcpOption(msgTypeOption);
}
public short getMessageType() {
DhcpV4MsgTypeOption msgType = (DhcpV4MsgTypeOption)
dhcpOptions.get(DhcpConstants.V4OPTION_MESSAGE_TYPE);
if (msgType != null) {
return msgType.getUnsignedByte();
}
return 0;
}
public boolean isBroadcastFlagSet() {
return (this.flags & 0x80) > 0;
}
}