756 lines
19 KiB
Python
756 lines
19 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
import binascii
|
|
|
|
from coapthon import defines
|
|
from coapthon import utils
|
|
from coapthon.messages.option import Option
|
|
|
|
__author__ = 'Giacomo Tanganelli'
|
|
|
|
|
|
class Message(object):
|
|
"""
|
|
Class to handle the Messages.
|
|
"""
|
|
def __init__(self):
|
|
"""
|
|
Data structure that represent a CoAP message
|
|
"""
|
|
self._type = None
|
|
self._mid = None
|
|
self._token = None
|
|
self._options = []
|
|
self._payload = None
|
|
self._destination = None
|
|
self._source = None
|
|
self._code = None
|
|
self._acknowledged = None
|
|
self._rejected = None
|
|
self._timeouted = None
|
|
self._cancelled = None
|
|
self._duplicated = None
|
|
self._timestamp = None
|
|
self._version = 1
|
|
|
|
@property
|
|
def version(self):
|
|
"""
|
|
Return the CoAP version
|
|
|
|
:return: the version
|
|
"""
|
|
return self._version
|
|
|
|
@version.setter
|
|
def version(self, v):
|
|
"""
|
|
Sets the CoAP version
|
|
|
|
:param v: the version
|
|
:raise AttributeError: if value is not 1
|
|
"""
|
|
if not isinstance(v, int) or v != 1:
|
|
raise AttributeError
|
|
self._version = v
|
|
|
|
@property
|
|
def type(self):
|
|
"""
|
|
Return the type of the message.
|
|
|
|
:return: the type
|
|
"""
|
|
return self._type
|
|
|
|
@type.setter
|
|
def type(self, value):
|
|
"""
|
|
Sets the type of the message.
|
|
|
|
:type value: Types
|
|
:param value: the type
|
|
:raise AttributeError: if value is not a valid type
|
|
"""
|
|
if value not in list(defines.Types.values()):
|
|
raise AttributeError
|
|
self._type = value
|
|
|
|
@property
|
|
def mid(self):
|
|
"""
|
|
Return the mid of the message.
|
|
|
|
:return: the MID
|
|
"""
|
|
return self._mid
|
|
|
|
@mid.setter
|
|
def mid(self, value):
|
|
"""
|
|
Sets the MID of the message.
|
|
|
|
:type value: Integer
|
|
:param value: the MID
|
|
:raise AttributeError: if value is not int or cannot be represented on 16 bits.
|
|
"""
|
|
if not isinstance(value, int) or value > 65536:
|
|
raise AttributeError
|
|
self._mid = value
|
|
|
|
@mid.deleter
|
|
def mid(self):
|
|
"""
|
|
Unset the MID of the message.
|
|
"""
|
|
self._mid = None
|
|
|
|
@property
|
|
def token(self):
|
|
"""
|
|
Get the Token of the message.
|
|
|
|
:return: the Token
|
|
"""
|
|
return self._token
|
|
|
|
@token.setter
|
|
def token(self, value):
|
|
"""
|
|
Set the Token of the message.
|
|
|
|
:type value: Bytes
|
|
:param value: the Token
|
|
:raise AttributeError: if value is longer than 256
|
|
"""
|
|
if value is None:
|
|
self._token = value
|
|
return
|
|
if not isinstance(value, bytes):
|
|
value = bytes(value, 'utf-8')
|
|
|
|
if len(value) > 256:
|
|
raise AttributeError
|
|
self._token = value
|
|
|
|
@token.deleter
|
|
def token(self):
|
|
"""
|
|
Unset the Token of the message.
|
|
"""
|
|
self._token = None
|
|
|
|
@property
|
|
def options(self):
|
|
"""
|
|
Return the options of the CoAP message.
|
|
|
|
:rtype: list
|
|
:return: the options
|
|
"""
|
|
return self._options
|
|
|
|
@options.setter
|
|
def options(self, value):
|
|
"""
|
|
Set the options of the CoAP message.
|
|
|
|
:type value: list
|
|
:param value: list of options
|
|
"""
|
|
if value is None:
|
|
value = []
|
|
assert isinstance(value, list)
|
|
self._options = value
|
|
|
|
@property
|
|
def payload(self):
|
|
"""
|
|
Return the payload.
|
|
|
|
:return: the payload
|
|
"""
|
|
return self._payload
|
|
|
|
@payload.setter
|
|
def payload(self, value):
|
|
"""
|
|
Sets the payload of the message and eventually the Content-Type
|
|
|
|
:param value: the payload
|
|
"""
|
|
if isinstance(value, tuple):
|
|
content_type, payload = value
|
|
self.content_type = content_type
|
|
self._payload = payload
|
|
else:
|
|
self._payload = value
|
|
|
|
@property
|
|
def destination(self):
|
|
"""
|
|
Return the destination of the message.
|
|
|
|
:rtype: tuple
|
|
:return: (ip, port)
|
|
"""
|
|
return self._destination
|
|
|
|
@destination.setter
|
|
def destination(self, value):
|
|
"""
|
|
Set the destination of the message.
|
|
|
|
:type value: tuple
|
|
:param value: (ip, port)
|
|
:raise AttributeError: if value is not a ip and a port.
|
|
"""
|
|
if value is not None and (not isinstance(value, tuple) or len(value)) != 2:
|
|
raise AttributeError
|
|
self._destination = value
|
|
|
|
@property
|
|
def source(self):
|
|
"""
|
|
Return the source of the message.
|
|
|
|
:rtype: tuple
|
|
:return: (ip, port)
|
|
"""
|
|
return self._source
|
|
|
|
@source.setter
|
|
def source(self, value):
|
|
"""
|
|
Set the source of the message.
|
|
|
|
:type value: tuple
|
|
:param value: (ip, port)
|
|
:raise AttributeError: if value is not a ip and a port.
|
|
"""
|
|
if not isinstance(value, tuple) or len(value) != 2:
|
|
raise AttributeError
|
|
self._source = value
|
|
|
|
@property
|
|
def code(self):
|
|
"""
|
|
Return the code of the message.
|
|
|
|
:rtype: Codes
|
|
:return: the code
|
|
"""
|
|
return self._code
|
|
|
|
@code.setter
|
|
def code(self, value):
|
|
"""
|
|
Set the code of the message.
|
|
|
|
:type value: Codes
|
|
:param value: the code
|
|
:raise AttributeError: if value is not a valid code
|
|
"""
|
|
if value not in list(defines.Codes.LIST.keys()) and value is not None:
|
|
raise AttributeError
|
|
self._code = value
|
|
|
|
@property
|
|
def acknowledged(self):
|
|
"""
|
|
Checks if is this message has been acknowledged.
|
|
|
|
:return: True, if is acknowledged
|
|
"""
|
|
return self._acknowledged
|
|
|
|
@acknowledged.setter
|
|
def acknowledged(self, value):
|
|
"""
|
|
Marks this message as acknowledged.
|
|
|
|
:type value: Boolean
|
|
:param value: if acknowledged
|
|
"""
|
|
assert (isinstance(value, bool))
|
|
self._acknowledged = value
|
|
if value:
|
|
self._timeouted = False
|
|
self._rejected = False
|
|
self._cancelled = False
|
|
|
|
@property
|
|
def rejected(self):
|
|
"""
|
|
Checks if this message has been rejected.
|
|
|
|
:return: True, if is rejected
|
|
"""
|
|
return self._rejected
|
|
|
|
@rejected.setter
|
|
def rejected(self, value):
|
|
"""
|
|
Marks this message as rejected.
|
|
|
|
:type value: Boolean
|
|
:param value: if rejected
|
|
"""
|
|
assert (isinstance(value, bool))
|
|
self._rejected = value
|
|
if value:
|
|
self._timeouted = False
|
|
self._acknowledged = False
|
|
self._cancelled = True
|
|
|
|
@property
|
|
def timeouted(self):
|
|
"""
|
|
Checks if this message has timeouted. Confirmable messages in particular
|
|
might timeout.
|
|
|
|
:return: True, if has timeouted
|
|
"""
|
|
return self._timeouted
|
|
|
|
@timeouted.setter
|
|
def timeouted(self, value):
|
|
"""
|
|
Marks this message as timeouted. Confirmable messages in particular might
|
|
timeout.
|
|
|
|
:type value: Boolean
|
|
:param value:
|
|
"""
|
|
assert (isinstance(value, bool))
|
|
self._timeouted = value
|
|
if value:
|
|
self._acknowledged = False
|
|
self._rejected = False
|
|
self._cancelled = True
|
|
|
|
@property
|
|
def duplicated(self):
|
|
"""
|
|
Checks if this message is a duplicate.
|
|
|
|
:return: True, if is a duplicate
|
|
"""
|
|
return self._duplicated
|
|
|
|
@duplicated.setter
|
|
def duplicated(self, value):
|
|
"""
|
|
Marks this message as a duplicate.
|
|
|
|
:type value: Boolean
|
|
:param value: if a duplicate
|
|
"""
|
|
assert (isinstance(value, bool))
|
|
self._duplicated = value
|
|
|
|
@property
|
|
def timestamp(self):
|
|
"""
|
|
Return the timestamp of the message.
|
|
"""
|
|
return self._timestamp
|
|
|
|
@timestamp.setter
|
|
def timestamp(self, value):
|
|
"""
|
|
Set the timestamp of the message.
|
|
|
|
:type value: timestamp
|
|
:param value: the timestamp
|
|
"""
|
|
self._timestamp = value
|
|
|
|
def _already_in(self, option):
|
|
"""
|
|
Check if an option is already in the message.
|
|
|
|
:type option: Option
|
|
:param option: the option to be checked
|
|
:return: True if already present, False otherwise
|
|
"""
|
|
for opt in self._options:
|
|
if option.number == opt.number:
|
|
return True
|
|
return False
|
|
|
|
def add_option(self, option):
|
|
"""
|
|
Add an option to the message.
|
|
|
|
:type option: Option
|
|
:param option: the option
|
|
:raise TypeError: if the option is not repeatable and such option is already present in the message
|
|
"""
|
|
assert isinstance(option, Option)
|
|
repeatable = defines.OptionRegistry.LIST[option.number].repeatable
|
|
if not repeatable:
|
|
ret = self._already_in(option)
|
|
if ret:
|
|
raise TypeError("Option : %s is not repeatable", option.name)
|
|
else:
|
|
self._options.append(option)
|
|
else:
|
|
self._options.append(option)
|
|
|
|
def del_option(self, option):
|
|
"""
|
|
Delete an option from the message
|
|
|
|
:type option: Option
|
|
:param option: the option
|
|
"""
|
|
assert isinstance(option, Option)
|
|
while option in list(self._options):
|
|
self._options.remove(option)
|
|
|
|
def del_option_by_name(self, name):
|
|
"""
|
|
Delete an option from the message by name
|
|
|
|
:type name: String
|
|
:param name: option name
|
|
"""
|
|
for o in list(self._options):
|
|
assert isinstance(o, Option)
|
|
if o.name == name:
|
|
self._options.remove(o)
|
|
|
|
def del_option_by_number(self, number):
|
|
"""
|
|
Delete an option from the message by number
|
|
|
|
:type number: Integer
|
|
:param number: option naumber
|
|
"""
|
|
for o in list(self._options):
|
|
assert isinstance(o, Option)
|
|
if o.number == number:
|
|
self._options.remove(o)
|
|
|
|
@property
|
|
def etag(self):
|
|
"""
|
|
Get the ETag option of the message.
|
|
|
|
:rtype: list
|
|
:return: the ETag values or [] if not specified by the request
|
|
"""
|
|
value = []
|
|
for option in self.options:
|
|
if option.number == defines.OptionRegistry.ETAG.number:
|
|
value.append(option.value)
|
|
return value
|
|
|
|
@etag.setter
|
|
def etag(self, etag):
|
|
"""
|
|
Add an ETag option to the message.
|
|
|
|
:param etag: the etag
|
|
"""
|
|
if not isinstance(etag, list):
|
|
etag = [etag]
|
|
for e in etag:
|
|
option = Option()
|
|
option.number = defines.OptionRegistry.ETAG.number
|
|
if not isinstance(e, bytes):
|
|
e = bytes(e, "utf-8")
|
|
option.value = e
|
|
self.add_option(option)
|
|
|
|
@etag.deleter
|
|
def etag(self):
|
|
"""
|
|
Delete an ETag from a message.
|
|
|
|
"""
|
|
self.del_option_by_number(defines.OptionRegistry.ETAG.number)
|
|
|
|
@property
|
|
def content_type(self):
|
|
"""
|
|
Get the Content-Type option of a response.
|
|
|
|
:return: the Content-Type value or 0 if not specified by the response
|
|
"""
|
|
value = 0
|
|
for option in self.options:
|
|
if option.number == defines.OptionRegistry.CONTENT_TYPE.number:
|
|
value = int(option.value)
|
|
return value
|
|
|
|
@content_type.setter
|
|
def content_type(self, content_type):
|
|
"""
|
|
Set the Content-Type option of a response.
|
|
|
|
:type content_type: int
|
|
:param content_type: the Content-Type
|
|
"""
|
|
option = Option()
|
|
option.number = defines.OptionRegistry.CONTENT_TYPE.number
|
|
option.value = int(content_type)
|
|
self.add_option(option)
|
|
|
|
@content_type.deleter
|
|
def content_type(self):
|
|
"""
|
|
Delete the Content-Type option of a response.
|
|
"""
|
|
|
|
self.del_option_by_number(defines.OptionRegistry.CONTENT_TYPE.number)
|
|
|
|
@property
|
|
def observe(self):
|
|
"""
|
|
Check if the request is an observing request.
|
|
|
|
:return: 0, if the request is an observing request
|
|
"""
|
|
for option in self.options:
|
|
if option.number == defines.OptionRegistry.OBSERVE.number:
|
|
# if option.value is None:
|
|
# return 0
|
|
if option.value is None:
|
|
return 0
|
|
return option.value
|
|
return None
|
|
|
|
@observe.setter
|
|
def observe(self, ob):
|
|
"""
|
|
Add the Observe option.
|
|
|
|
:param ob: observe count
|
|
"""
|
|
option = Option()
|
|
option.number = defines.OptionRegistry.OBSERVE.number
|
|
option.value = ob
|
|
self.del_option_by_number(defines.OptionRegistry.OBSERVE.number)
|
|
self.add_option(option)
|
|
|
|
@observe.deleter
|
|
def observe(self):
|
|
"""
|
|
Delete the Observe option.
|
|
"""
|
|
self.del_option_by_number(defines.OptionRegistry.OBSERVE.number)
|
|
|
|
@property
|
|
def block1(self):
|
|
"""
|
|
Get the Block1 option.
|
|
|
|
:return: the Block1 value
|
|
"""
|
|
value = None
|
|
for option in self.options:
|
|
if option.number == defines.OptionRegistry.BLOCK1.number:
|
|
value = utils.parse_blockwise(option.value)
|
|
return value
|
|
|
|
@block1.setter
|
|
def block1(self, value):
|
|
"""
|
|
Set the Block1 option.
|
|
|
|
:param value: the Block1 value
|
|
"""
|
|
option = Option()
|
|
option.number = defines.OptionRegistry.BLOCK1.number
|
|
num, m, size = value
|
|
if size > 512:
|
|
szx = 6
|
|
elif 256 < size <= 512:
|
|
szx = 5
|
|
elif 128 < size <= 256:
|
|
szx = 4
|
|
elif 64 < size <= 128:
|
|
szx = 3
|
|
elif 32 < size <= 64:
|
|
szx = 2
|
|
elif 16 < size <= 32:
|
|
szx = 1
|
|
else:
|
|
szx = 0
|
|
|
|
value = (num << 4)
|
|
value |= (m << 3)
|
|
value |= szx
|
|
|
|
option.value = value
|
|
self.add_option(option)
|
|
|
|
@block1.deleter
|
|
def block1(self):
|
|
"""
|
|
Delete the Block1 option.
|
|
"""
|
|
self.del_option_by_number(defines.OptionRegistry.BLOCK1.number)
|
|
|
|
@property
|
|
def block2(self):
|
|
"""
|
|
Get the Block2 option.
|
|
|
|
:return: the Block2 value
|
|
"""
|
|
value = None
|
|
for option in self.options:
|
|
if option.number == defines.OptionRegistry.BLOCK2.number:
|
|
value = utils.parse_blockwise(option.value)
|
|
return value
|
|
|
|
@block2.setter
|
|
def block2(self, value):
|
|
"""
|
|
Set the Block2 option.
|
|
|
|
:param value: the Block2 value
|
|
"""
|
|
option = Option()
|
|
option.number = defines.OptionRegistry.BLOCK2.number
|
|
num, m, size = value
|
|
if size > 512:
|
|
szx = 6
|
|
elif 256 < size <= 512:
|
|
szx = 5
|
|
elif 128 < size <= 256:
|
|
szx = 4
|
|
elif 64 < size <= 128:
|
|
szx = 3
|
|
elif 32 < size <= 64:
|
|
szx = 2
|
|
elif 16 < size <= 32:
|
|
szx = 1
|
|
else:
|
|
szx = 0
|
|
|
|
value = (num << 4)
|
|
value |= (m << 3)
|
|
value |= szx
|
|
|
|
option.value = value
|
|
self.add_option(option)
|
|
|
|
@block2.deleter
|
|
def block2(self):
|
|
"""
|
|
Delete the Block2 option.
|
|
"""
|
|
self.del_option_by_number(defines.OptionRegistry.BLOCK2.number)
|
|
|
|
@property
|
|
def size1(self):
|
|
value = None
|
|
for option in self.options:
|
|
if option.number == defines.OptionRegistry.SIZE1.number:
|
|
value = option.value if option.value is not None else 0
|
|
return value
|
|
|
|
@size1.setter
|
|
def size1(self, value):
|
|
option = Option()
|
|
option.number = defines.OptionRegistry.SIZE1.number
|
|
option.value = value
|
|
self.add_option(option)
|
|
|
|
@size1.deleter
|
|
def size1(self):
|
|
self.del_option_by_number(defines.OptionRegistry.SIZE1.number)
|
|
|
|
@property
|
|
def size2(self):
|
|
"""
|
|
Get the Size2 option.
|
|
|
|
:return: the Size2 value
|
|
"""
|
|
value = None
|
|
for option in self.options:
|
|
if option.number == defines.OptionRegistry.SIZE2.number:
|
|
value = option.value
|
|
return value
|
|
|
|
@size2.setter
|
|
def size2(self, value):
|
|
"""
|
|
Set the Size2 option.
|
|
|
|
:param value: the Block2 value
|
|
"""
|
|
option = Option()
|
|
option.number = defines.OptionRegistry.SIZE2.number
|
|
option.value = value
|
|
self.add_option(option)
|
|
|
|
@size2.deleter
|
|
def size2(self):
|
|
"""
|
|
Delete the Size2 option.
|
|
"""
|
|
self.del_option_by_number(defines.OptionRegistry.SIZE2.number)
|
|
|
|
@property
|
|
def line_print(self):
|
|
"""
|
|
Return the message as a one-line string.
|
|
|
|
:return: the string representing the message
|
|
"""
|
|
inv_types = {v: k for k, v in defines.Types.items()}
|
|
|
|
if self._code is None:
|
|
self._code = defines.Codes.EMPTY.number
|
|
|
|
token = binascii.hexlify(self._token).decode("utf-8") if self._token is not None else str(None)
|
|
|
|
msg = "From {source}, To {destination}, {type}-{mid}, {code}-{token}, ["\
|
|
.format(source=self._source, destination=self._destination, type=inv_types[self._type], mid=self._mid,
|
|
code=defines.Codes.LIST[self._code].name, token=token)
|
|
for opt in self._options:
|
|
if 'Block' in opt.name:
|
|
msg += "{name}: {value}, ".format(name=opt.name, value=utils.parse_blockwise(opt.value))
|
|
else:
|
|
msg += "{name}: {value}, ".format(name=opt.name, value=opt.value)
|
|
msg += "]"
|
|
if self.payload is not None:
|
|
if isinstance(self.payload, dict):
|
|
tmp = list(self.payload.values())[0][0:20]
|
|
else:
|
|
tmp = self.payload[0:20]
|
|
msg += " {payload}...{length} bytes".format(payload=tmp, length=len(self.payload))
|
|
else:
|
|
msg += " No payload"
|
|
return msg
|
|
|
|
def __str__(self):
|
|
return self.line_print
|
|
|
|
def pretty_print(self):
|
|
"""
|
|
Return the message as a formatted string.
|
|
|
|
:return: the string representing the message
|
|
"""
|
|
msg = "Source: " + str(self._source) + "\n"
|
|
msg += "Destination: " + str(self._destination) + "\n"
|
|
inv_types = {v: k for k, v in defines.Types.items()}
|
|
msg += "Type: " + str(inv_types[self._type]) + "\n"
|
|
msg += "MID: " + str(self._mid) + "\n"
|
|
if self._code is None:
|
|
self._code = 0
|
|
token = binascii.hexlify(self._token).decode("utf-8") if self._token is not None else str(None)
|
|
msg += "Code: " + str(defines.Codes.LIST[self._code].name) + "\n"
|
|
msg += "Token: " + token + "\n"
|
|
for opt in self._options:
|
|
msg += str(opt)
|
|
msg += "Payload: " + "\n"
|
|
msg += str(self._payload) + "\n"
|
|
return msg
|