mirror of
https://github.com/espressif/openthread.git
synced 2026-09-02 23:30:07 +00:00
Add CoAP support to thread-cert. (#1063)
This commit is contained in:
committed by
Jonathan Hui
parent
b38faa8fa0
commit
bc8b1a4cd1
@@ -0,0 +1,387 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright (c) 2016, The OpenThread Authors.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# 3. Neither the name of the copyright holder nor the
|
||||
# names of its contributors may be used to endorse or promote products
|
||||
# derived from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
|
||||
import collections
|
||||
import io
|
||||
import struct
|
||||
|
||||
from binascii import hexlify
|
||||
from enum import IntEnum
|
||||
|
||||
|
||||
class CoapMessageType(IntEnum):
|
||||
CON = 0 # Confirmable
|
||||
NON = 1 # Non-confirmable
|
||||
ACK = 2 # Acknowledgement
|
||||
RST = 3 # Reset
|
||||
|
||||
|
||||
class CoapOptionsTypes(IntEnum):
|
||||
IF_MATCH = 1
|
||||
URI_HOST = 3
|
||||
ETAG = 4
|
||||
IF_NOT_MATCH = 5
|
||||
URI_PORT = 7
|
||||
LOCATION_PATH = 8
|
||||
URI_PATH = 11
|
||||
CONTENT_FORMAT = 12
|
||||
MAX_AGE = 14
|
||||
URI_QUERY = 15
|
||||
ACCEPT = 17
|
||||
LOCATION_QUERY = 20
|
||||
PROXY_URI = 35
|
||||
PROXY_SCHEME = 39
|
||||
SIZE1 = 60
|
||||
|
||||
|
||||
class CoapOptionHeader(object):
|
||||
|
||||
""" Class representing CoAP optiona header. """
|
||||
|
||||
def __init__(self, delta, length):
|
||||
self._delta = delta
|
||||
self._length = length
|
||||
|
||||
@property
|
||||
def delta(self):
|
||||
return self._delta
|
||||
|
||||
@property
|
||||
def length(self):
|
||||
return self._length
|
||||
|
||||
@property
|
||||
def is_payload_marker(self):
|
||||
return self.delta == 0xf and self.length == 0xf
|
||||
|
||||
@classmethod
|
||||
def _read_extended_value(cls, data, value):
|
||||
if value == 13:
|
||||
return ord(data.read(1)) + 13
|
||||
elif value == 14:
|
||||
data.read(1)
|
||||
return ord(data.read(1)) + 269
|
||||
else:
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data):
|
||||
initial_byte = ord(data.read(1))
|
||||
|
||||
delta = (initial_byte >> 4) & 0xf
|
||||
length = initial_byte & 0xf
|
||||
|
||||
delta = cls._read_extended_value(data, delta)
|
||||
length = cls._read_extended_value(data, length)
|
||||
|
||||
return cls(delta, length)
|
||||
|
||||
|
||||
class CoapOption(object):
|
||||
|
||||
""" Class representing CoAP option. """
|
||||
|
||||
def __init__(self, _type, value):
|
||||
self._type = _type
|
||||
self._value = value
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return self._type
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self._value
|
||||
|
||||
def __repr__(self):
|
||||
return "CoapOption(type={}, value={})".format(self.type, hexlify(self.value))
|
||||
|
||||
|
||||
class CoapOptionsFactory(object):
|
||||
|
||||
""" Factory that produces CoAP options. """
|
||||
|
||||
def parse(self, data, message_info):
|
||||
options = []
|
||||
|
||||
_type = 0
|
||||
while data.tell() < len(data.getvalue()):
|
||||
option_header = CoapOptionHeader.from_bytes(data)
|
||||
if option_header.is_payload_marker:
|
||||
break
|
||||
|
||||
_type += option_header.delta
|
||||
value = data.read(option_header.length)
|
||||
|
||||
option = CoapOption(_type, value)
|
||||
options.append(option)
|
||||
|
||||
return options
|
||||
|
||||
|
||||
class CoapCode(object):
|
||||
|
||||
""" Class representing CoAP code. """
|
||||
|
||||
def __init__(self, code):
|
||||
self._code = code
|
||||
|
||||
@property
|
||||
def code(self):
|
||||
return self._code
|
||||
|
||||
@property
|
||||
def _class(self):
|
||||
return (self.code >> 5) & 0x7
|
||||
|
||||
@property
|
||||
def detail(self):
|
||||
return self.code & 0x1f
|
||||
|
||||
@classmethod
|
||||
def from_class_and_detail(cls, _class, detail):
|
||||
return cls(((_class & 0x7) << 5) | (detail & 0x1f))
|
||||
|
||||
@classmethod
|
||||
def from_dotted(cls, dotted_str):
|
||||
_class, detail = dotted_str.split(".")
|
||||
return cls.from_class_and_detail(int(_class), int(detail))
|
||||
|
||||
def is_equal_dotted(self, dotted_code):
|
||||
other = self.from_dotted(dotted_code)
|
||||
return self.code == other.code
|
||||
|
||||
@property
|
||||
def dotted(self):
|
||||
return ".".join(["{:01d}".format(self._class), "{:02d}".format(self.detail)])
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, int):
|
||||
return self.code == other
|
||||
|
||||
elif isinstance(other, str):
|
||||
return self.is_equal_dotted(other)
|
||||
|
||||
elif isinstance(other, self.__class__):
|
||||
return self.code == other.code
|
||||
|
||||
else:
|
||||
raise TypeError("Could not compare {} and {}".format(type(self), type(other)))
|
||||
|
||||
def __repr__(self):
|
||||
return self.dotted
|
||||
|
||||
|
||||
class CoapMessage(object):
|
||||
|
||||
""" Class representing CoAP message. """
|
||||
|
||||
def __init__(self, version, _type, code, message_id, token, options, payload, uri_path=None):
|
||||
self._version = version
|
||||
self._type = _type
|
||||
self._code = code
|
||||
self._message_id = message_id
|
||||
self._token = token
|
||||
self._options = options
|
||||
self._payload = payload
|
||||
self._uri_path = uri_path
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
return self._version
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return self._type
|
||||
|
||||
@property
|
||||
def code(self):
|
||||
return self._code
|
||||
|
||||
@property
|
||||
def message_id(self):
|
||||
return self._message_id
|
||||
|
||||
@property
|
||||
def token(self):
|
||||
return self._token
|
||||
|
||||
@property
|
||||
def tkl(self):
|
||||
return len(self._token)
|
||||
|
||||
@property
|
||||
def options(self):
|
||||
return self._options
|
||||
|
||||
@property
|
||||
def payload(self):
|
||||
return self._payload
|
||||
|
||||
@property
|
||||
def uri_path(self):
|
||||
return self._uri_path
|
||||
|
||||
def __repr__(self):
|
||||
options_str = ", ".join([repr(opt) for opt in self.options])
|
||||
return "CoapMessage(version={}, type={}, code={}, message_id={}, token={}, options=[{}], payload={}, uri-path='{}')".format(
|
||||
self.version, CoapMessageType.name[self.type], self.code, self.message_id, hexlify(self.token),
|
||||
options_str, self.payload, self.uri_path)
|
||||
|
||||
|
||||
class CoapMessageProxy(object):
|
||||
|
||||
""" Proxy class of CoAP message.
|
||||
|
||||
The main idea behind this class is to delay parsing payload. Due to architecture of the existing solution
|
||||
it is possible to process confirmation message before a request message. In such case it is not possible
|
||||
to get URI path to get proper payload parser.
|
||||
"""
|
||||
|
||||
def __init__(self, coap_message, message_info, mid_to_uri_path_binder, uri_path_based_payload_factories):
|
||||
self._coap_message = coap_message
|
||||
self._message_info = message_info
|
||||
self._mid_to_uri_path_binder = mid_to_uri_path_binder
|
||||
self._uri_path_based_payload_factories = uri_path_based_payload_factories
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
return self._coap_message.version
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return self._coap_message.type
|
||||
|
||||
@property
|
||||
def code(self):
|
||||
return self._coap_message.code
|
||||
|
||||
@property
|
||||
def message_id(self):
|
||||
return self._coap_message.message_id
|
||||
|
||||
@property
|
||||
def token(self):
|
||||
return self._coap_message.token
|
||||
|
||||
@property
|
||||
def tkl(self):
|
||||
return self._coap_message.tkl
|
||||
|
||||
@property
|
||||
def options(self):
|
||||
return self._coap_message.options
|
||||
|
||||
@property
|
||||
def payload(self):
|
||||
try:
|
||||
binded_uri_path = self._mid_to_uri_path_binder.get_uri_path_for(self.message_id, self.token)
|
||||
|
||||
factory = self._uri_path_based_payload_factories[binded_uri_path]
|
||||
|
||||
return factory.parse(io.BytesIO(self._coap_message.payload), self._message_info)
|
||||
|
||||
except RuntimeError:
|
||||
return self._coap_message.payload
|
||||
|
||||
@property
|
||||
def uri_path(self):
|
||||
return self._coap_message.uri_path
|
||||
|
||||
def __repr__(self):
|
||||
options_str = ", ".join([repr(opt) for opt in self.options])
|
||||
return "CoapMessageProxy(version={}, type={}, code={}, message_id={}, token={}, options=[{}], payload={}, uri-path='{}')".format(
|
||||
self.version, self.type, self.code, self.message_id, hexlify(self.token),
|
||||
options_str, self.payload, self.uri_path)
|
||||
|
||||
|
||||
class CoapMessageIdToUriPathBinder:
|
||||
|
||||
""" Class binds message id and token with URI path. """
|
||||
|
||||
def __init__(self):
|
||||
self._uri_path_binds = collections.defaultdict(collections.defaultdict)
|
||||
|
||||
def add_uri_path_for(self, message_id, token, uri_path):
|
||||
self._uri_path_binds[message_id][hexlify(token)] = uri_path
|
||||
|
||||
def get_uri_path_for(self, message_id, token):
|
||||
try:
|
||||
return self._uri_path_binds[message_id][hexlify(token)]
|
||||
except KeyError:
|
||||
raise RuntimeError("Could not find URI PATH for message_id: {} and token: {}".format(
|
||||
message_id, hexlify(token)))
|
||||
|
||||
|
||||
class CoapMessageFactory(object):
|
||||
|
||||
""" Factory that produces CoAP messages. """
|
||||
|
||||
def __init__(self, options_factory, uri_path_based_payload_factories, message_id_to_uri_path_binder):
|
||||
self._options_factory = options_factory
|
||||
self._uri_path_based_payload_factories = uri_path_based_payload_factories
|
||||
self._mid_to_uri_path_binder = message_id_to_uri_path_binder
|
||||
|
||||
def _uri_path_from(self, options):
|
||||
uri_path_options = []
|
||||
|
||||
for option in options:
|
||||
if option.type == CoapOptionsTypes.URI_PATH:
|
||||
uri_path_options.append(option.value.decode("utf-8"))
|
||||
|
||||
if not uri_path_options:
|
||||
return None
|
||||
|
||||
return "/" + "/".join(uri_path_options)
|
||||
|
||||
def _parse_initial_byte(self, data, message_info):
|
||||
initial_byte = ord(data.read(1))
|
||||
|
||||
version = (initial_byte >> 6) & 0x3
|
||||
_type = CoapMessageType((initial_byte >> 4) & 0x3)
|
||||
token_length = initial_byte & 0xf
|
||||
|
||||
return version, _type, token_length
|
||||
|
||||
def parse(self, data, message_info):
|
||||
version, _type, token_length = self._parse_initial_byte(data, message_info)
|
||||
|
||||
code = CoapCode(ord(data.read(1)))
|
||||
message_id = struct.unpack(">H", data.read(2))[0]
|
||||
token = data.read(token_length)
|
||||
|
||||
options = self._options_factory.parse(data, message_info)
|
||||
|
||||
uri_path = self._uri_path_from(options)
|
||||
if uri_path is not None:
|
||||
self._mid_to_uri_path_binder.add_uri_path_for(message_id, token, uri_path)
|
||||
|
||||
coap_message = CoapMessage(version, _type, code, message_id, token, options, data.read(), uri_path)
|
||||
|
||||
return CoapMessageProxy(coap_message, message_info, self._mid_to_uri_path_binder, self._uri_path_based_payload_factories)
|
||||
Reference in New Issue
Block a user