/*
* 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 DhcpV6Message.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.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.ArrayList;
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.v6.DhcpV6ClientIdOption;
import com.jagornet.dhcp.option.v6.DhcpV6IaNaOption;
import com.jagornet.dhcp.option.v6.DhcpV6IaPdOption;
import com.jagornet.dhcp.option.v6.DhcpV6IaTaOption;
import com.jagornet.dhcp.option.v6.DhcpV6OptionFactory;
import com.jagornet.dhcp.option.v6.DhcpV6OptionRequestOption;
import com.jagornet.dhcp.option.v6.DhcpV6RelayOption;
import com.jagornet.dhcp.option.v6.DhcpV6ServerIdOption;
import com.jagornet.dhcp.util.DhcpConstants;
import com.jagornet.dhcp.util.Util;
/**
* Title: DhcpV6Message
* Description: Object that represents a DHCPv6 message as defined in
* RFC 3315.
*
* 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
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | msg-type | transaction-id |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | |
* . options .
* . (variable) .
* | |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*
* msg-type Identifies the DHCP message type; the
* available message types are listed in
* section 5.3.
*
* transaction-id The transaction ID for this message exchange.
*
* options Options carried in this message; options are
* described in section 22.
*
* The format of DHCP options is:
* 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
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | option-code | option-len |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | option-opaqueData |
* | (option-len octets) |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*
* option-code An unsigned integer identifying the specific option
* type carried in this option.
*
* option-len An unsigned integer giving the length of the
* option-opaqueData field in this option in octets.
*
* option-opaqueData The opaqueData for the option; the format of this opaqueData
* depends on the definition of the option.
* </pre>
*
* @author A. Gregory Rabil
*/
public class DhcpV6Message implements DhcpMessage
{
private static Logger log = LoggerFactory.getLogger(DhcpV6Message.class);
// true if the message was received on a unicast socket, false otherwise
// note that a DhcpRelayMessage (subclass) may be unicast, but the "inner"
// DhcpMessage will _not_ be unicast, which is the desired behavior
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 messageType = 0; // need a short to hold unsigned byte
protected int transactionId = 0; // we only use low order three bytes
protected Map<Integer, DhcpOption> dhcpOptions = new HashMap<Integer, DhcpOption>();
protected List<DhcpV6IaNaOption> iaNaOptions = new ArrayList<DhcpV6IaNaOption>();
protected List<DhcpV6IaTaOption> iaTaOptions = new ArrayList<DhcpV6IaTaOption>();
protected List<DhcpV6IaPdOption> iaPdOptions = new ArrayList<DhcpV6IaPdOption>();
/**
* 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 DhcpV6Message(InetSocketAddress localAddress, InetSocketAddress remoteAddress)
{
this.localAddress = localAddress;
this.remoteAddress = remoteAddress;
}
/**
* Set the unicast flag for this message.
*
* @param unicast
*/
public void setUnicast(boolean unicast)
{
this.unicast = unicast;
}
/**
* Check if this message was received via unicast.
*
* @return true if unicast message, false otherwise
*/
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)messageType);
buf.put(DhcpTransactionId.encode(transactionId));
buf.put(encodeOptions());
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(1020); // 1024 - 1(msgType) - 3(transId) = 1020 (options)
if (dhcpOptions != null) {
for (DhcpOption option : dhcpOptions.values()) {
buf.put(option.encode());
}
}
if (iaNaOptions != null) {
for (DhcpV6IaNaOption iaNaOption : iaNaOptions) {
buf.put(iaNaOption.encode());
}
}
if (iaTaOptions != null) {
for (DhcpV6IaTaOption iaTaOption : iaTaOptions) {
buf.put(iaTaOption.encode());
}
}
if (iaPdOptions != null) {
for (DhcpV6IaPdOption iaPdOption : iaPdOptions) {
buf.put(iaPdOption.encode());
}
}
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 DhcpV6Message decode(ByteBuffer buf, InetSocketAddress localAddr, InetSocketAddress remoteAddr)
throws IOException
{
DhcpV6Message 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 msgtype = buf.get();
if (log.isDebugEnabled())
log.debug("Message type byte=" + msgtype);
if ( (msgtype >= DhcpConstants.V6MESSAGE_TYPE_SOLICIT) &&
(msgtype <= DhcpConstants.V6MESSAGE_TYPE_INFO_REQUEST) ) {
dhcpMessage = new DhcpV6Message(localAddr, remoteAddr);
}
else if ( (msgtype >= DhcpConstants.V6MESSAGE_TYPE_RELAY_FORW) &&
(msgtype <= DhcpConstants.V6MESSAGE_TYPE_RELAY_REPL) ) {
// note that it doesn't make much sense to be decoding
// a relay-reply message unless we implement a relay
dhcpMessage = new DhcpV6RelayMessage(localAddr, remoteAddr);
}
else {
log.error("Unknown message type: " + msgtype);
}
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 DhcpMessage from: " +
Util.socketAddressAsString(remoteAddress));
if ((buf != null) && buf.hasRemaining()) {
decodeMessageType(buf);
if (buf.hasRemaining()) {
setTransactionId(DhcpTransactionId.decode(buf));
if (log.isDebugEnabled())
log.debug("TransactionId=" + transactionId);
if (buf.hasRemaining()) {
decodeOptions(buf);
}
else {
String errmsg = "Failed to decode options: buffer is empty";
log.error(errmsg);
throw new IOException(errmsg);
}
}
else {
String errmsg = "Failed to decode transaction id: buffer is empty";
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 message type.
*
* @param buf ByteBuffer positioned at the message type in the packet
* @throws IOException
*/
protected void decodeMessageType(ByteBuffer buf) throws IOException
{
if ((buf != null) && buf.hasRemaining()) {
setMessageType(Util.getUnsignedByte(buf));
if (log.isDebugEnabled())
log.debug("MessageType=" + DhcpConstants.getV6MessageString(messageType));
}
else {
String errmsg = "Failed to decode message type: buffer is empty";
log.error(errmsg);
throw new IOException(errmsg);
}
}
/**
* 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()) {
int code = Util.getUnsignedShort(buf);
if (log.isDebugEnabled())
log.debug("Option code=" + code);
DhcpOption option = DhcpV6OptionFactory.getDhcpOption(code);
if (option != null) {
if ((option instanceof DhcpV6RelayOption) &&
(this instanceof DhcpV6RelayMessage)) {
DhcpV6RelayOption relayOption = (DhcpV6RelayOption) option;
relayOption.setRelayMessage((DhcpV6RelayMessage)this);
}
option.decode(buf);
if (option instanceof DhcpV6IaNaOption) {
iaNaOptions.add((DhcpV6IaNaOption)option);
}
else if (option instanceof DhcpV6IaTaOption) {
iaTaOptions.add((DhcpV6IaTaOption)option);
}
else if (option instanceof DhcpV6IaPdOption) {
iaPdOptions.add((DhcpV6IaPdOption)option);
}
else {
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()
{
int len = 4; // msg type (1) + transaction id (3)
len += getOptionsLength();
len += getIaNaOptionsLength();
len += getIaTaOptionsLength();
len += getIaPdOptionsLength();
return len;
}
/**
* Get 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 += 4; // option code (2 bytes) + length (2 bytes)
len += option.getLength();
}
}
return len;
}
/**
* Get the length of the IA_NA options in this DhcpMessage in bytes.
* @return an int containing the total length of all IA_NA options
*/
protected int getIaNaOptionsLength()
{
int len = 0;
if (iaNaOptions != null) {
for (DhcpV6IaNaOption option : iaNaOptions) {
len += 4; // option code (2 bytes) + length (2 bytes)
len += option.getLength();
}
}
return len;
}
/**
* Get the length of the IA_TA options in this DhcpMessage in bytes.
* @return an int containing the total length of all IA_TA options
*/
protected int getIaTaOptionsLength()
{
int len = 0;
if (iaTaOptions != null) {
for (DhcpV6IaTaOption option : iaTaOptions) {
len += 4; // option code (2 bytes) + length (2 bytes)
len += option.getLength();
}
}
return len;
}
/**
* Return the length of the IA_PD options in this DhcpMessage in bytes.
* @return an int containing the total length of all IA_PD options
*/
protected int getIaPdOptionsLength()
{
int len = 0;
if (iaPdOptions != null) {
for (DhcpV6IaPdOption option : iaPdOptions) {
len += 4; // option code (2 bytes) + length (2 bytes)
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 short getMessageType()
{
return messageType;
}
public void setMessageType(short messageType)
{
this.messageType = messageType;
}
public int getTransactionId()
{
return transactionId;
}
public void setTransactionId(int transactionId)
{
this.transactionId = transactionId;
}
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();
}
public List<DhcpV6IaNaOption> getIaNaOptions() {
return iaNaOptions;
}
public void setIaNaOptions(List<DhcpV6IaNaOption> iaNaOptions) {
this.iaNaOptions = iaNaOptions;
}
public void addIaNaOption(DhcpV6IaNaOption iaNaOption) {
if (iaNaOptions == null) {
iaNaOptions = new ArrayList<DhcpV6IaNaOption>();
}
iaNaOptions.add(iaNaOption);
}
public List<DhcpV6IaTaOption> getIaTaOptions() {
return iaTaOptions;
}
public void setIaTaOptions(List<DhcpV6IaTaOption> iaTaOptions) {
this.iaTaOptions = iaTaOptions;
}
public void addIaTaOption(DhcpV6IaTaOption iaTaOption) {
if (iaTaOptions == null) {
iaTaOptions = new ArrayList<DhcpV6IaTaOption>();
}
iaTaOptions.add(iaTaOption);
}
public List<DhcpV6IaPdOption> getIaPdOptions() {
return iaPdOptions;
}
public void setIaPdOptions(List<DhcpV6IaPdOption> iaPdOptions) {
this.iaPdOptions = iaPdOptions;
}
public void addIaPdOption(DhcpV6IaPdOption iaPdOption) {
if (iaPdOptions == null) {
iaPdOptions = new ArrayList<DhcpV6IaPdOption>();
}
iaPdOptions.add(iaPdOption);
}
private DhcpV6ClientIdOption dhcpClientIdOption;
/**
* Convenience method to get ClientID option.
* @return
*/
public DhcpV6ClientIdOption getDhcpClientIdOption() {
if (dhcpClientIdOption == null) {
if (dhcpOptions != null) {
dhcpClientIdOption =
(DhcpV6ClientIdOption) dhcpOptions.get(DhcpConstants.V6OPTION_CLIENTID);
}
}
return dhcpClientIdOption;
}
private DhcpV6ServerIdOption dhcpServerIdOption;
/**
* Convenience method to get ServerID option.
* @return
*/
public DhcpV6ServerIdOption getDhcpServerIdOption() {
if (dhcpServerIdOption == null) {
if (dhcpOptions != null) {
dhcpServerIdOption =
(DhcpV6ServerIdOption) dhcpOptions.get(DhcpConstants.V6OPTION_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) {
DhcpV6OptionRequestOption oro =
(DhcpV6OptionRequestOption) dhcpOptions.get(DhcpConstants.V6OPTION_ORO);
if (oro != null) {
requestedOptionCodes = oro.getUnsignedShortList();
}
}
}
return requestedOptionCodes;
}
public String toString()
{
StringBuilder sb = new StringBuilder(Util.LINE_SEPARATOR);
sb.append(DhcpConstants.getV6MessageString(getMessageType()));
sb.append(" (xactId=");
sb.append(getTransactionId());
sb.append(')');
if ((this.messageType == DhcpConstants.V6MESSAGE_TYPE_ADVERTISE) ||
(this.messageType == DhcpConstants.V6MESSAGE_TYPE_REPLY) ||
(this.messageType == DhcpConstants.V6MESSAGE_TYPE_RECONFIGURE))
sb.append(" to ");
else
sb.append(" from ");
sb.append(Util.socketAddressAsString(remoteAddress));
return sb.toString();
}
public String toStringWithOptions()
{
StringBuilder sb = new StringBuilder(this.toString());
if ((dhcpOptions != null) && !dhcpOptions.isEmpty()) {
sb.append(Util.LINE_SEPARATOR);
sb.append("MSG_DHCPOPTIONS");
for (DhcpOption dhcpOption : dhcpOptions.values()) {
sb.append(dhcpOption.toString());
}
}
if ((iaNaOptions != null) && !iaNaOptions.isEmpty()) {
sb.append(Util.LINE_SEPARATOR);
sb.append("IA_NA_OPTIONS");
for (DhcpV6IaNaOption iaNaOption : iaNaOptions) {
sb.append(iaNaOption.toString());
}
}
if ((iaTaOptions != null) && !iaTaOptions.isEmpty()) {
sb.append(Util.LINE_SEPARATOR);
sb.append("IA_TA_OPTIONS");
for (DhcpV6IaTaOption iaTaOption : iaTaOptions) {
sb.append(iaTaOption.toString());
}
}
if ((iaPdOptions != null) && !iaPdOptions.isEmpty()) {
sb.append(Util.LINE_SEPARATOR);
sb.append("IA_PD_OPTIONS");
for (DhcpV6IaPdOption iaPdOption : iaPdOptions) {
sb.append(iaPdOption.toString());
}
}
return sb.toString();
}
}