/* Copyright [2011] [University of Rostock] * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *****************************************************************************/ /* WS4D Java CoAP Implementation * (c) 2011 WS4D.org * * written by Sebastian Unger */ package org.ws4d.coap.messages; import java.nio.ByteBuffer; import java.util.Collections; import java.util.Iterator; import java.util.Random; import java.util.Vector; import org.apache.log4j.Logger; import org.ws4d.coap.connection.BasicCoapChannelManager; import org.ws4d.coap.interfaces.CoapChannel; import org.ws4d.coap.interfaces.CoapMessage; /** * @author Christian Lerche <christian.lerche@uni-rostock.de> */ public abstract class AbstractCoapMessage implements CoapMessage { /* use the logger of the channel manager */ private final static Logger logger = Logger.getLogger(BasicCoapChannelManager.class); protected static final int HEADER_LENGTH = 4; /* Header */ protected int version; protected CoapPacketType packetType; protected int messageCodeValue; //protected int optionCount; protected int messageId; /* Options */ protected CoapHeaderOptions options = new CoapHeaderOptions(); /* Payload */ protected byte[] payload = null; protected int payloadLength = 0; /* corresponding channel */ CoapChannel channel = null; /* Retransmission State */ int timeout = 0; int retransmissionCounter = 0; protected void serialize(byte[] bytes, int length, int offset){ /* check length to avoid buffer overflow exceptions */ this.version = 1; this.packetType = (CoapPacketType.getPacketType((bytes[offset + 0] & 0x30) >> 4)); int optionCount = bytes[offset + 0] & 0x0F; this.messageCodeValue = (bytes[offset + 1] & 0xFF); this.messageId = ((bytes[offset + 2] << 8) & 0xFF00) + (bytes[offset + 3] & 0xFF); /* serialize options */ this.options = new CoapHeaderOptions(bytes, offset + HEADER_LENGTH, optionCount); /* get and check payload length */ payloadLength = length - HEADER_LENGTH - options.getDeserializedLength(); if (payloadLength < 0){ throw new IllegalStateException("Invaldid CoAP Message (payload length negative)"); } /* copy payload */ int payloadOffset = offset + HEADER_LENGTH + options.getDeserializedLength(); payload = new byte[payloadLength]; for (int i = 0; i < payloadLength; i++){ payload[i] = bytes[i + payloadOffset]; } } /* TODO: this function should be in another class */ public static CoapMessage parseMessage(byte[] bytes, int length){ return parseMessage(bytes, length, 0); } public static CoapMessage parseMessage(byte[] bytes, int length, int offset){ /* we "peek" the header to determine the kind of message * TODO: duplicate Code */ int messageCodeValue = (bytes[offset + 1] & 0xFF); if (messageCodeValue == 0){ return new CoapEmptyMessage(bytes, length, offset); } else if (messageCodeValue >= 0 && messageCodeValue <= 31 ){ return new BasicCoapRequest(bytes, length, offset); } else if (messageCodeValue >= 64 && messageCodeValue <= 191){ return new BasicCoapResponse(bytes, length, offset); } else { throw new IllegalArgumentException("unknown CoAP message"); } } public int getVersion() { return version; } public int getMessageCodeValue() { return messageCodeValue; } public CoapPacketType getPacketType() { return packetType; } public byte[] getPayload() { return payload; } public int getPayloadLength() { return payloadLength; } public int getMessageID() { return messageId; } public void setMessageID(int messageId) { this.messageId = messageId; } public byte[] serialize() { /* TODO improve memory allocation */ /* serialize header options first to get the length*/ int optionsLength = 0; byte[] optionsArray = null; if (options != null) { optionsArray = this.options.serialize(); optionsLength = this.options.getSerializedLength(); } /* allocate memory for the complete packet */ int length = HEADER_LENGTH + optionsLength + payloadLength; byte[] serializedPacket = new byte[length]; /* serialize header */ serializedPacket[0] = (byte) ((this.version & 0x03) << 6); serializedPacket[0] |= (byte) ((this.packetType.getValue() & 0x03) << 4); serializedPacket[0] |= (byte) (options.getOptionCount() & 0x0F); serializedPacket[1] = (byte) (this.getMessageCodeValue() & 0xFF); serializedPacket[2] = (byte) ((this.messageId >> 8) & 0xFF); serializedPacket[3] = (byte) (this.messageId & 0xFF); /* copy serialized options to the final array */ int offset = HEADER_LENGTH; if (options != null) { for (int i = 0; i < optionsLength; i++) serializedPacket[i + offset] = optionsArray[i]; } /* copy payload to the final array */ offset = HEADER_LENGTH + optionsLength; for (int i = 0; i < this.payloadLength; i++) { serializedPacket[i + offset] = payload[i]; } return serializedPacket; } public void setPayload(byte[] payload) { this.payload = payload; if (payload!=null) this.payloadLength = payload.length; else this.payloadLength = 0; } public void setPayload(char[] payload) { this.payload = new byte[payload.length]; for (int i = 0; i < payload.length; i++) { this.payload[i] = (byte) payload[i]; } this.payloadLength = payload.length; } public void setPayload(String payload) { setPayload(payload.toCharArray()); } public void setContentType(CoapMediaType mediaType){ CoapHeaderOption option = options.getOption(CoapHeaderOptionType.Content_Type); if (option != null){ /* content Type MUST only exists once */ throw new IllegalStateException("added content option twice"); } if ( mediaType == CoapMediaType.UNKNOWN){ throw new IllegalStateException("unknown content type"); } /* convert value */ byte[] data = long2CoapUint(mediaType.getValue()); /* no need to check result, mediaType is safe */ /* add option to Coap Header*/ options.addOption(new CoapHeaderOption(CoapHeaderOptionType.Content_Type, data)); } public CoapMediaType getContentType(){ CoapHeaderOption option = options.getOption(CoapHeaderOptionType.Content_Type); if (option == null){ /* not content type TODO: return UNKNOWN ?*/ return null; } /* no need to check length, CoapMediaType parse function will do*/ int mediaTypeCode = (int) coapUint2Long(options.getOption(CoapHeaderOptionType.Content_Type).getOptionData()); return CoapMediaType.parse(mediaTypeCode); } public byte[] getToken(){ CoapHeaderOption option = options.getOption(CoapHeaderOptionType.Token); if (option == null){ return null; } return option.getOptionData(); } protected void setToken(byte[] token){ if (token == null){ return; } if (token.length < 1 || token.length > 8){ throw new IllegalArgumentException("Invalid Token Length"); } options.addOption(CoapHeaderOptionType.Token, token); } public CoapBlockOption getBlock1(){ CoapHeaderOption option = options.getOption(CoapHeaderOptionType.Block1); if (option == null){ return null; } CoapBlockOption blockOpt = new CoapBlockOption(option.getOptionData()); return blockOpt; } public void setBlock1(CoapBlockOption blockOption){ CoapHeaderOption option = options.getOption(CoapHeaderOptionType.Block1); if (option != null){ //option already exists options.removeOption(CoapHeaderOptionType.Block1); } options.addOption(CoapHeaderOptionType.Block1, blockOption.getBytes()); } public CoapBlockOption getBlock2(){ CoapHeaderOption option = options.getOption(CoapHeaderOptionType.Block2); if (option == null){ return null; } CoapBlockOption blockOpt = new CoapBlockOption(option.getOptionData()); return blockOpt; } public void setBlock2(CoapBlockOption blockOption){ CoapHeaderOption option = options.getOption(CoapHeaderOptionType.Block2); if (option != null){ //option already exists options.removeOption(CoapHeaderOptionType.Block2); } options.addOption(CoapHeaderOptionType.Block2, blockOption.getBytes()); } public Integer getObserveOption() { CoapHeaderOption option = options.getOption(CoapHeaderOptionType.Observe); if (option == null){ return null; } byte[] data = option.getOptionData(); if (data.length < 0 || data.length > 2){ logger.warn("invalid observe option length, return null"); return null; } return (int) AbstractCoapMessage.coapUint2Long(data); } public void setObserveOption(int sequenceNumber) { CoapHeaderOption option = options.getOption(CoapHeaderOptionType.Observe); if (option != null){ options.removeOption(CoapHeaderOptionType.Observe); } byte[] data = long2CoapUint(sequenceNumber); if (data.length < 0 || data.length > 2){ throw new IllegalArgumentException("invalid observe option length"); } options.addOption(CoapHeaderOptionType.Observe, data); } public void copyHeaderOptions(AbstractCoapMessage origin){ options.removeAll(); options.copyFrom(origin.options); } public void removeOption(CoapHeaderOptionType optionType){ options.removeOption(optionType); } public CoapChannel getChannel() { return channel; } public void setChannel(CoapChannel channel) { this.channel = channel; } public int getTimeout() { if (timeout == 0) { Random random = new Random(); timeout = RESPONSE_TIMEOUT_MS + random.nextInt((int) (RESPONSE_TIMEOUT_MS * RESPONSE_RANDOM_FACTOR) - RESPONSE_TIMEOUT_MS); } return timeout; } public boolean maxRetransReached() { if (retransmissionCounter < MAX_RETRANSMIT) { return false; } return true; } public void incRetransCounterAndTimeout() { /*TODO: Rename*/ retransmissionCounter += 1; timeout *= 2; } public boolean isReliable() { if (packetType == CoapPacketType.NON){ return false; } return true; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((channel == null) ? 0 : channel.hashCode()); result = prime * result + getMessageID(); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; AbstractCoapMessage other = (AbstractCoapMessage) obj; if (channel == null) { if (other.channel != null) return false; } else if (!channel.equals(other.channel)) return false; if (getMessageID() != other.getMessageID()) return false; return true; } protected static long coapUint2Long(byte[] data){ /* avoid buffer overflow */ if(data.length > 8){ return -1; } /* fill with leading zeros */ byte[] tmp = new byte[8]; for (int i = 0; i < data.length; i++) { tmp[i + 8 - data.length] = data[i]; } /* convert to long */ ByteBuffer buf = ByteBuffer.wrap(tmp); /* byte buffer contains 8 bytes */ return buf.getLong(); } protected static byte[] long2CoapUint(long value){ /* only unsigned values supported */ if (value < 0){ return null; } /* a zero length value implies zero */ if (value == 0){ return new byte[0]; } /* convert long to byte array with a fixed length of 8 byte*/ ByteBuffer buf = ByteBuffer.allocate(8); buf.putLong(value); byte[] tmp = buf.array(); /* remove leading zeros */ int leadingZeros = 0; for (int i = 0; i < tmp.length; i++) { if (tmp[i] == 0){ leadingZeros = i+1; } else { break; } } /* copy to byte array without leading zeros */ byte[] result = new byte[8 - leadingZeros]; for (int i = 0; i < result.length; i++) { result[i] = tmp[i + leadingZeros]; } return result; } public enum CoapHeaderOptionType { UNKNOWN(-1), Content_Type (1), Max_Age (2), Proxy_Uri(3), Etag (4), Uri_Host (5), Location_Path (6), Uri_Port (7), Location_Query (8), Uri_Path (9), Observe (10), Token (11), Accept (12), If_Match (13), Uri_Query (15), If_None_Match (21), Block1 (19), Block2 (17); int value; CoapHeaderOptionType(int optionValue){ value = optionValue; } public static CoapHeaderOptionType parse(int optionTypeValue){ switch(optionTypeValue){ case 1: return Content_Type; case 2: return Max_Age; case 3: return Proxy_Uri; case 4: return Etag; case 5: return Uri_Host; case 6: return Location_Path; case 7: return Uri_Port; case 8: return Location_Query; case 9: return Uri_Path; case 10: return Observe; case 11:return Token; case 12:return Accept; case 13:return If_Match; case 15:return Uri_Query; case 21:return If_None_Match; case 19:return Block1; case 17:return Block2; default: return UNKNOWN; } } public int getValue(){ return value; } /* TODO: implement validity checks */ /*TODO: implement isCritical(int optionTypeValue), isElective()*/ } protected class CoapHeaderOption implements Comparable<CoapHeaderOption> { CoapHeaderOptionType optionType; int optionTypeValue; /* integer representation of optionType*/ byte[] optionData; int shortLength; int longLength; int deserializedLength; static final int MAX_LENGTH = 270; public int getDeserializedLength() { return deserializedLength; } public CoapHeaderOption(CoapHeaderOptionType optionType, byte[] value) { if (optionType == CoapHeaderOptionType.UNKNOWN){ /*TODO: implement check if it is a critical option */ throw new IllegalStateException("Unknown header option"); } if (value == null){ throw new IllegalArgumentException("Header option value MUST NOT be null"); } this.optionTypeValue = optionType.getValue(); this.optionData = value; if (value.length < 15) { shortLength = value.length; longLength = 0; } else { shortLength = 15; longLength = value.length - shortLength; } } public CoapHeaderOption(byte[] bytes, int offset, int lastOptionNumber){ int headerLength; /* parse option type */ optionTypeValue = ((bytes[offset] & 0xF0) >> 4) + lastOptionNumber; optionType = CoapHeaderOptionType.parse(optionTypeValue); if (optionType == CoapHeaderOptionType.UNKNOWN){ if (optionTypeValue % 14 == 0){ /* no-op: no operation for deltas > 14 */ } else { /*TODO: implement check if it is a critical option */ throw new IllegalArgumentException("Unknown header option"); } } /* parse length */ if ((bytes[offset] & 0x0F) < 15) { shortLength = bytes[offset] & 0x0F; longLength = 0; headerLength = 1; } else { shortLength = 15; longLength = bytes[offset + 1]; headerLength = 2; /* additional length byte */ } /* copy value */ optionData = new byte[shortLength + longLength]; for (int i = 0; i < shortLength + longLength; i++){ optionData[i] = bytes[i + headerLength + offset]; } deserializedLength += headerLength + shortLength + longLength; } public int compareTo(CoapHeaderOption option) { /* compare function for sorting * TODO: check what happens in case of equal option values * IMPORTANT: order must be the same for e.g., URI path*/ if (this.optionTypeValue != option.optionTypeValue) return this.optionTypeValue < option.optionTypeValue ? -1 : 1; else return 0; } public boolean hasLongLength(){ if (shortLength == 15){ return true; } else return false; } public int getLongLength() { return longLength; } public int getShortLength() { return shortLength; } public int getOptionTypeValue() { return optionTypeValue; } public byte[] getOptionData() { return optionData; } public int getSerializeLength(){ if (hasLongLength()){ return optionData.length + 2; } else { return optionData.length + 1; } } @Override public String toString() { char[] printableOptionValue = new char[optionData.length]; for (int i = 0; i < optionData.length; i++) printableOptionValue[i] = (char) optionData[i]; return "Option Number: " + " (" + optionTypeValue + ")" + ", Option Value: " + String.copyValueOf(printableOptionValue); } public CoapHeaderOptionType getOptionType() { return optionType; } } protected class CoapHeaderOptions implements Iterable<CoapHeaderOption>{ private Vector<CoapHeaderOption> headerOptions = new Vector<CoapHeaderOption>(); private int deserializedLength; private int serializedLength = 0; public CoapHeaderOptions(byte[] bytes, int option_count){ this(bytes, option_count, option_count); } public CoapHeaderOptions(byte[] bytes, int offset, int optionCount){ /* note: we only receive deltas and never concrete numbers */ /* TODO: check integrity */ deserializedLength = 0; int lastOptionNumber = 0; int optionOffset = offset; for (int i = 0; i < optionCount; i++) { CoapHeaderOption option = new CoapHeaderOption(bytes, optionOffset, lastOptionNumber); lastOptionNumber = option.getOptionTypeValue(); deserializedLength += option.getDeserializedLength(); optionOffset += option.getDeserializedLength(); addOption(option); } } public CoapHeaderOptions() { /* creates empty header options */ } public CoapHeaderOption getOption(int optionNumber) { for (CoapHeaderOption headerOption : headerOptions) { if (headerOption.getOptionTypeValue() == optionNumber) { return headerOption; } } return null; } public CoapHeaderOption getOption(CoapHeaderOptionType optionType) { for (CoapHeaderOption headerOption : headerOptions) { if (headerOption.getOptionType() == optionType) { return headerOption; } } return null; } public boolean optionExists(CoapHeaderOptionType optionType) { CoapHeaderOption option = getOption(optionType); if (option == null){ return false; } else return true; } public void addOption(CoapHeaderOption option) { headerOptions.add(option); /*TODO: only sort when options are serialized*/ Collections.sort(headerOptions); } public void addOption(CoapHeaderOptionType optionType, byte[] value){ addOption(new CoapHeaderOption(optionType, value)); } public void removeOption(CoapHeaderOptionType optionType){ CoapHeaderOption headerOption; // get elements of Vector /* note: iterating over and changing a vector at the same time is not allowed */ int i = 0; while (i < headerOptions.size()){ headerOption = headerOptions.get(i); if (headerOption.getOptionType() == optionType) { headerOptions.remove(i); } else { /* only increase when no element was removed*/ i++; } } Collections.sort(headerOptions); } public void removeAll(){ headerOptions.clear(); } public void copyFrom(CoapHeaderOptions origin){ for (CoapHeaderOption option : origin) { addOption(option); } } public int getOptionCount() { return headerOptions.size(); } public byte[] serialize() { /* options are serialized here to be more efficient (only one byte array necessary)*/ int length = 0; /* calculate the overall length first */ for (CoapHeaderOption option : headerOptions) { length += option.getSerializeLength(); } byte[] data = new byte[length]; int arrayIndex = 0; int lastOptionNumber = 0; /* let's keep track of this */ for (CoapHeaderOption headerOption : headerOptions) { /* TODO: move the serialization implementation to CoapHeaderOption */ int optionDelta = headerOption.getOptionTypeValue() - lastOptionNumber; lastOptionNumber = headerOption.getOptionTypeValue(); // set length(s) data[arrayIndex++] = (byte) (((optionDelta & 0x0F) << 4) | (headerOption.getShortLength() & 0x0F)); if (headerOption.hasLongLength()) { data[arrayIndex++] = (byte) (headerOption.getLongLength() & 0xFF); } // copy option value byte[] value = headerOption.getOptionData(); for (int i = 0; i < value.length; i++) { data[arrayIndex++] = value[i]; } } serializedLength = length; return data; } public int getDeserializedLength(){ return deserializedLength; } public int getSerializedLength() { return serializedLength; } public Iterator<CoapHeaderOption> iterator() { return headerOptions.iterator(); } @Override public String toString() { String result = "\tOptions:\n"; for (CoapHeaderOption option : headerOptions) { result += "\t\t" + option.toString() + "\n"; } return result; } } }