/*
* Copyright Ericsson AB 2011-2014. All Rights Reserved.
*
* The contents of this file are subject to the Lesser GNU Public License,
* (the "License"), either version 2.1 of the License, or
* (at your option) any later version.; you may not use this file except in
* compliance with the License. You should have received a copy of the
* License along with this software. If not, it can be
* retrieved online at https://www.gnu.org/licenses/lgpl.html. Moreover
* it could also be requested from Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
* WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
* EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
* OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND,
* EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
* LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE,
* YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
*
* IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
* WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
* REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR
* DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL
* DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY
* (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED
* INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE
* OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH
* HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
*
*/
package com.ericsson.deviceaccess.coap.basedriver.util;
import static com.ericsson.common.util.BitUtil.getBitsInByteAsByte;
import static com.ericsson.common.util.BitUtil.mergeBytesToShort;
import com.ericsson.common.util.function.FunctionalUtil;
import com.ericsson.deviceaccess.coap.basedriver.api.CoAPException;
import com.ericsson.deviceaccess.coap.basedriver.api.message.CoAPMessage;
import com.ericsson.deviceaccess.coap.basedriver.api.message.CoAPMessage.CoAPMessageType;
import com.ericsson.deviceaccess.coap.basedriver.api.message.CoAPMessageFormat;
import com.ericsson.deviceaccess.coap.basedriver.api.message.CoAPOptionHeader;
import com.ericsson.deviceaccess.coap.basedriver.api.message.CoAPOptionName;
import com.ericsson.deviceaccess.coap.basedriver.api.message.CoAPRequest;
import com.ericsson.deviceaccess.coap.basedriver.api.message.CoAPRequestCode;
import com.ericsson.deviceaccess.coap.basedriver.api.message.CoAPResponse;
import com.ericsson.deviceaccess.coap.basedriver.api.message.CoAPResponseCode;
import java.net.DatagramPacket;
import java.net.InetSocketAddress;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class is responsible for decoding a byte array containing CoAP message
* (either request or response)
*/
public class CoAPMessageReader implements CoAPMessageFormat {
private static final Logger LOGGER = LoggerFactory.getLogger(CoAPMessageReader.class);
// this is the message being decoded
private CoAPMessage message;
private final DatagramPacket packet;
private boolean okOptions;
/**
* Constructor. A datagram containing the data to be decoded is given as
* parameter.
*
* @param packet
*/
public CoAPMessageReader(DatagramPacket packet) {
this.packet = packet;
this.okOptions = true;
}
/**
* This method returns a boolean value indicating if all the options were
* successfully added
*
* @return true, if all options were added. false, otherwise
*/
public boolean validOptions() {
return okOptions;
}
public CoAPMessage decodeStart() throws IncorrectMessageException {
decodeStartPos(packet.getData());
return message;
}
/**
* This method decodes the datagram
*
* @return
* @throws
* com.ericsson.deviceaccess.coap.basedriver.api.message.CoAPMessageFormat.IncorrectMessageException
*/
public CoAPMessage decode() throws IncorrectMessageException {
byte[] bytes = packet.getData();
StringBuilder sb = new StringBuilder(bytes.length * Byte.SIZE);
for (int i = 0; i < Byte.SIZE * bytes.length; i++) {
if (bytes[i / Byte.SIZE] == 0) {
break;
}
sb.append((bytes[i / Byte.SIZE] << i % Byte.SIZE & 0x80) == 0 ? '0' : '1');
}
LOGGER.debug("CoAPMessageReader: Decode message: " + sb);
int position = decodeStartPos(bytes);
position = decodeOptions(bytes, position);
// Only payload left
readPayload(bytes, position);
return message;
}
private int decodeStartPos(byte[] bytes) throws IncorrectMessageException {
int position = 0;
// Get version
// Version should be the first 2 bits (unsigned)
byte versionByte = getBitsInByteAsByte(bytes[position], VERSION_START, VERSION_LENGTH);
int version = (int) versionByte & 0xff;
if (version != 1) {
//MUST be silently ignored //TODO: Handle this
throw new IncorrectMessageException("Wrong version: " + version);
}
// Get type
byte typeByte = getBitsInByteAsByte(bytes[position], TYPE_START, TYPE_LENGTH);
int type = (int) typeByte & 0xff;
// Get token length
byte tokenLengthByte = getBitsInByteAsByte(bytes[position], TOKEN_LENGTH_START, TOKEN_LENGTH_LENGTH);
position++;
int tokenLength = (int) tokenLengthByte & 0xff;
if (tokenLength > 8) {
//MUST NOT be send and MUST be processed as a message format error //TODO: Handle this
throw new IncorrectMessageException("Wrong token length: " + tokenLength);
}
// Get code
byte codeByte = getBitsInByteAsByte(bytes[position], CODE_START, CODE_LENGTH);
position++;
int code = (int) codeByte & 0xff;
// Get message id
// Message ID is a 16-bit unsigned => merge two bytes
byte firstByte = bytes[position];
position++;
byte secondByte = bytes[position];
position++;
short shortInt = mergeBytesToShort(firstByte, secondByte);
// mask to unsigned 16 bit -> int
int messageId = shortInt & 0xFFFF;
//Get token
byte[] token = new byte[tokenLength];
System.arraycopy(bytes, position, token, 0, tokenLength);
position += tokenLength;
CoAPMessageType messageType = CoAPMessageType.getType(type);
if (code == 0) {
// handle empty messages as responses, only acks can be empty?
message = new CoAPResponse(version, messageType, CoAPResponseCode.get(code), messageId, token);
} else if (CoAPResponseCode.isAllowed(code)) {
message = new CoAPResponse(version, messageType, CoAPResponseCode.get(code), messageId, token);
} else if (CoAPRequestCode.isAllowed(code)) {
message = new CoAPRequest(version, messageType, CoAPRequestCode.get(code), messageId, token);
} else {
// TODO exception handling
message = null;
}
if (message != null && packet.getPort() != -1) {
message.setSocketAddress((InetSocketAddress) packet.getSocketAddress());
}
FunctionalUtil.acceptIfCan(CoAPRequest.class, message, m -> {
try {
m.createUriFromRequest(packet.getSocketAddress());
} catch (CoAPException e) {
LOGGER.debug("Creating URI from request failed.", e);
}
});
return position;
}
/**
* This method decodes the options in the given data
*
* @param bytes bytes to decode
* @param optionCount option count (determined from the option count header)
*/
private int decodeOptions(byte[] bytes, int position) throws IncorrectMessageException {
int cur = bytes[position];
position++;
int delta = getBitsInByteAsByte((byte) cur, OPTION_DELTA_START, OPTION_DELTA_LENGTH);
int length = getBitsInByteAsByte((byte) cur, OPTION_LENGTH_START, OPTION_LENGTH_LENGTH);
int optionNumber = 0;
// An Option can be followed by the end of the message, by another Option, or by the Payload Marker and the payload.
while (delta != PAYLOAD_MARKER) {
if (length == 0 && delta == 0) {
return position;
}
//Determine option number
if (delta == ADDITIONAL_DELTA) {
delta += bytes[position];
position++;
} else if (delta == ADDITIONAL_DELTA_2) {
cur = bytes[position];
position++;
delta = ADDITIONAL_DELTA_MAX + ((cur << 8) + bytes[position]);
position++;
}
optionNumber += delta;
CoAPOptionName name = CoAPOptionName.getFromNo(optionNumber);
//Determine length
if (length == ADDITIONAL_LENGTH) {
length += bytes[position];
position++;
} else if (length == ADDITIONAL_LENGTH_2) {
cur = bytes[position];
position++;
length = ADDITIONAL_LENGTH_MAX + ((cur << 8) + bytes[position]);
position++;
}
if (!name.isLegalSize(length)) {
//If the length of an option value in a request is outside the defined range, that option MUST be treated like an unrecognized option
okOptions = false; //TODO: Handle this
}
byte[] value = new byte[length];
System.arraycopy(bytes, position, value, 0, length);
position += length;
boolean okToAdd = message.addOptionHeader(new CoAPOptionHeader(name, value));
// TODO draft-ietf-core-coap-08 specifies behaviour when
// unrecognized options are received in confirmable req/resp.
// So apply to CON messages only
if (!okToAdd && name.isCritical() && message.getMessageType() == CoAPMessageType.CONFIRMABLE) {
this.okOptions = false; //TODO: Handle this
LOGGER.debug("Unrecognized options in a confirmable message");
}
if (position >= bytes.length) {
return position;
}
cur = bytes[position];
position++;
delta = getBitsInByteAsByte((byte) cur, OPTION_DELTA_START, OPTION_DELTA_LENGTH);
length = getBitsInByteAsByte((byte) cur, OPTION_LENGTH_START, OPTION_LENGTH_LENGTH);
}
if (length != PAYLOAD_MARKER) {
// If the field is set to this value but the entire byte is not the payload marker, this MUST be processed as a message format error.
throw new IncorrectMessageException("Payload marker was bad: " + length + " at position " + position); //TODO: Handle this
}
if (position == bytes.length - 1) {
// The presence of a marker followed by a zero-length payload MUST be processed as a message format error.
throw new IncorrectMessageException("Empty payload after payload marker"); //TODO: Handle this
}
return position;
}
/**
* Read the content of the message
*
* @param bytes byte array to decode
*/
private void readPayload(byte[] bytes, int position) {
int bytesLeft = bytes.length - position;
byte[] payload = new byte[bytesLeft];
System.arraycopy(bytes, position, payload, 0, bytesLeft);
message.setPayload(payload);
}
}