Files
openthread/tests/scripts/thread-cert/message.py
T
Abtin Keshavarzian 71e00eb20f [test] add missing tests in the Makefile.am & update file access mode (#6825)
This commits updates `thread-cert/Makefile.am` adding some existing
tests that were not included in the Makefile when the tests were
added. It also sorts the entries to be in alphabetical order.

This commit also updates the access control mode for all script files
to be executable by all (`chmod 755` or "-rwxr-xr-x").
2021-07-19 14:35:50 -07:00

612 lines
19 KiB
Python
Executable File

#!/usr/bin/env python3
#
# 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 io
import ipaddress
import struct
import coap
import common
import dtls
import ipv6
import mac802154
import mle
from enum import IntEnum
class DropPacketException(Exception):
pass
class MessageType(IntEnum):
MLE = 0
COAP = 1
ICMP = 2
ACK = 3
BEACON = 4
DATA = 5
COMMAND = 6
DTLS = 7
class Message(object):
def __init__(self):
self._type = None
self._channel = None
self._mac_header = None
self._ipv6_packet = None
self._coap = None
self._mle = None
self._icmp = None
self._dtls = None
def _extract_udp_datagram(self, udp_datagram):
if isinstance(udp_datagram.payload, mle.MleMessage):
self._type = MessageType.MLE
self._mle = udp_datagram.payload
elif isinstance(udp_datagram.payload, (coap.CoapMessage, coap.CoapMessageProxy)):
self._type = MessageType.COAP
self._coap = udp_datagram.payload
# DTLS message factory returns a list of messages
elif isinstance(udp_datagram.payload, list):
self._type = MessageType.DTLS
self._dtls = udp_datagram.payload
def _extract_upper_layer_protocol(self, upper_layer_protocol):
if isinstance(upper_layer_protocol, ipv6.ICMPv6):
self._type = MessageType.ICMP
self._icmp = upper_layer_protocol
elif isinstance(upper_layer_protocol, ipv6.UDPDatagram):
self._extract_udp_datagram(upper_layer_protocol)
def try_extract_dtls_messages(self):
"""Extract multiple dtls messages that are sent in a single UDP datagram
"""
if self.type != MessageType.DTLS:
return [self.clone()]
assert isinstance(self.dtls, list)
ret = []
for dtls in self.dtls:
msg = self.clone()
msg._dtls = dtls
ret.append(msg)
return ret
def clone(self):
msg = Message()
msg._type = self.type
msg._channel = self.channel
msg._mac_header = self.mac_header
msg._ipv6_packet = self.ipv6_packet
msg._coap = self.coap
msg._mle = self.mle
msg._icmp = self.icmp
msg._dtls = self.dtls
return msg
@property
def type(self):
return self._type
@type.setter
def type(self, value):
self._type = value
@property
def channel(self):
return self._channel
@channel.setter
def channel(self, value):
self._channel = value
@property
def mac_header(self):
return self._mac_header
@mac_header.setter
def mac_header(self, value):
self._mac_header = value
if self._mac_header.frame_type == mac802154.MacHeader.FrameType.BEACON:
self._type = MessageType.BEACON
elif self._mac_header.frame_type == mac802154.MacHeader.FrameType.ACK:
self._type = MessageType.ACK
elif self._mac_header.frame_type == mac802154.MacHeader.FrameType.DATA:
self._type = MessageType.DATA
elif (self._mac_header.frame_type == mac802154.MacHeader.FrameType.COMMAND):
self._type = MessageType.COMMAND
else:
raise ValueError('Invalid mac frame type %d' % self._mac_header.frame_type)
@property
def ipv6_packet(self):
return self._ipv6_packet
@ipv6_packet.setter
def ipv6_packet(self, value):
self._ipv6_packet = value
self._extract_upper_layer_protocol(value.upper_layer_protocol)
@property
def coap(self):
return self._coap
@property
def mle(self):
return self._mle
@property
def icmp(self):
return self._icmp
@icmp.setter
def icmp(self, value):
self._icmp = value
@property
def dtls(self):
return self._dtls
def get_mle_message_tlv(self, tlv_class_type):
if self.type != MessageType.MLE:
raise ValueError("Invalid message type. Expected MLE message.")
for tlv in self.mle.command.tlvs:
if isinstance(tlv, tlv_class_type):
return tlv
def assertMleMessageIsType(self, command_type):
if self.type != MessageType.MLE:
raise ValueError("Invalid message type. Expected MLE message.")
assert self.mle.command.type == command_type
def assertMleMessageContainsTlv(self, tlv_class_type):
"""To confirm if Mle message contains the TLV type.
Args:
tlv_class_type: tlv's type.
Returns:
mle.Route64: If contains the TLV, return it.
"""
if self.type != MessageType.MLE:
raise ValueError("Invalid message type. Expected MLE message.")
contains_tlv = False
for tlv in self.mle.command.tlvs:
if isinstance(tlv, tlv_class_type):
contains_tlv = True
break
assert contains_tlv
return tlv
def assertAssignedRouterQuantity(self, router_quantity):
"""Confirm if Leader contains the Route64 TLV with router_quantity assigned Router IDs.
Args:
router_quantity: the quantity of router.
"""
tlv = self.assertMleMessageContainsTlv(mle.Route64)
router_id_mask = tlv.router_id_mask
count = 0
for i in range(1, 65):
count += router_id_mask & 1
router_id_mask = router_id_mask >> 1
assert count == router_quantity
def assertMleMessageDoesNotContainTlv(self, tlv_class_type):
if self.type != MessageType.MLE:
raise ValueError("Invalid message type. Expected MLE message.")
contains_tlv = False
for tlv in self.mle.command.tlvs:
if isinstance(tlv, tlv_class_type):
contains_tlv = True
break
assert contains_tlv is False
def assertMleMessageContainsOptionalTlv(self, tlv_class_type):
if self.type != MessageType.MLE:
raise ValueError("Invalid message type. Expected MLE message.")
contains_tlv = False
for tlv in self.mle.command.tlvs:
if isinstance(tlv, tlv_class_type):
contains_tlv = True
break
if contains_tlv:
print("MleMessage contains optional TLV: {}".format(tlv_class_type))
else:
print("MleMessage doesn't contain optional TLV: {}".format(tlv_class_type))
def get_coap_message_tlv(self, tlv_class_type):
if self.type != MessageType.COAP:
raise ValueError("Invalid message type. Expected CoAP message.")
for tlv in self.coap.payload:
if isinstance(tlv, tlv_class_type):
return tlv
def assertCoapMessageContainsTlv(self, tlv_class_type):
if self.type != MessageType.COAP:
raise ValueError("Invalid message type. Expected CoAP message.")
contains_tlv = False
for tlv in self.coap.payload:
if isinstance(tlv, tlv_class_type):
contains_tlv = True
break
assert contains_tlv
def assertCoapMessageDoesNotContainTlv(self, tlv_class_type):
if self.type != MessageType.COAP:
raise ValueError("Invalid message type. Expected COAP message.")
contains_tlv = False
for tlv in self.coap.payload:
if isinstance(tlv, tlv_class_type):
contains_tlv = True
break
assert contains_tlv is False
def assertCoapMessageContainsOptionalTlv(self, tlv_class_type):
if self.type != MessageType.COAP:
raise ValueError("Invalid message type. Expected CoAP message.")
for tlv in self.coap.payload:
if isinstance(tlv, tlv_class_type):
break
print("CoapMessage doesn't contain optional TLV: {}".format(tlv_class_type))
def assertCoapMessageRequestUriPath(self, uri_path):
if self.type != MessageType.COAP:
raise ValueError("Invalid message type. Expected CoAP message.")
assert uri_path == self.coap.uri_path
def assertCoapMessageCode(self, code):
if self.type != MessageType.COAP:
raise ValueError("Invalid message type. Expected CoAP message.")
assert code == self.coap.code
def assertSentToNode(self, node):
sent_to_node = False
dst_addr = self.ipv6_packet.ipv6_header.destination_address
for addr in node.get_addrs():
if dst_addr == ipaddress.ip_address(addr):
sent_to_node = True
if self.mac_header.dest_address.type == common.MacAddressType.SHORT:
mac_address = common.MacAddress.from_rloc16(node.get_addr16())
if self.mac_header.dest_address == mac_address:
sent_to_node = True
elif self.mac_header.dest_address.type == common.MacAddressType.LONG:
mac_address = common.MacAddress.from_eui64(bytearray(node.get_addr64(), encoding="utf-8"))
if self.mac_header.dest_address == mac_address:
sent_to_node = True
assert sent_to_node
def assertSentToDestinationAddress(self, ipv6_address):
assert (self.ipv6_packet.ipv6_header.destination_address == ipaddress.ip_address(ipv6_address))
def assertSentFromSourceAddress(self, ipv6_address):
assert (self.ipv6_packet.ipv6_header.source_address == ipaddress.ip_address(ipv6_address))
def assertSentWithHopLimit(self, hop_limit):
assert self.ipv6_packet.ipv6_header.hop_limit == hop_limit
def isMacAddressTypeLong(self):
return self.mac_header.dest_address.type == common.MacAddressType.LONG
def get_dst_udp_port(self):
assert isinstance(self.ipv6_packet.upper_layer_protocol, ipv6.UDPDatagram)
return self.ipv6_packet.upper_layer_protocol.header.dst_port
def is_data_poll(self):
return self._type == MessageType.COMMAND and \
self._mac_header.command_type == mac802154.MacHeader.CommandIdentifier.DATA_REQUEST
def __repr__(self):
if (self.type == MessageType.DTLS and self.dtls.content_type == dtls.ContentType.HANDSHAKE):
return "Message(type={})".format(str(self.dtls.handshake_type))
return "Message(type={})".format(MessageType(self.type).name)
class MessagesSet(object):
def __init__(self, messages, commissioning_messages=()):
self._messages = messages
self._commissioning_messages = commissioning_messages
@property
def messages(self):
return self._messages
@property
def commissioning_messages(self):
return self._commissioning_messages
def next_data_poll(self):
while True:
message = self.next_message_of(MessageType.COMMAND, False)
if not message:
break
elif message.is_data_poll():
return message
def next_coap_message(self, code, uri_path=None, assert_enabled=True):
message = None
while self.messages:
m = self.messages.pop(0)
if m.type != MessageType.COAP:
continue
if uri_path is not None and m.coap.uri_path != uri_path:
continue
else:
if not m.coap.code.is_equal_dotted(code):
continue
message = m
break
if assert_enabled:
assert (message is not None), "Could not find CoapMessage with code: {}".format(code)
return message
def last_mle_message(self, command_type, assert_enabled=True):
"""Get the last Mle Message with specified type from existing capture.
Args:
command_type: the specified mle type.
assert_enabled: interrupt or not when get the mle.
Returns:
message.Message: the last Mle Message with specified type.
"""
message = None
size = len(self.messages)
for i in range(size - 1, -1, -1):
m = self.messages[i]
if m.type != MessageType.MLE:
continue
# for command_type in command_types:
if m.mle.command.type == command_type:
message = m
break
if assert_enabled:
assert (message is not None), "Could not find MleMessage with type: {}".format(command_type)
return message
def next_mle_message(self, command_type, assert_enabled=True, sent_to_node=None):
message = self.next_mle_message_of_one_of_command_types(command_type)
if assert_enabled:
assert (message is not None), "Could not find MleMessage of the type: {}".format(command_type)
if sent_to_node is not None:
message.assertSentToNode(sent_to_node)
return message
def next_mle_message_of_one_of_command_types(self, *command_types):
message = None
while self.messages:
m = self.messages.pop(0)
if m.type != MessageType.MLE:
continue
command_found = False
for command_type in command_types:
if m.mle.command.type == command_type:
command_found = True
break
if command_found:
message = m
break
return message
def next_message(self, assert_enabled=True):
message = self.messages.pop(0)
if assert_enabled:
assert message is not None, "Could not find next Message"
return message
def next_message_of(self, message_type, assert_enabled=True):
message = None
while self.messages:
m = self.messages.pop(0)
if m.type != message_type:
continue
message = m
break
if assert_enabled:
assert (message is not None), "Could not find Message of the type: {}".format(message_type)
return message
def next_data_message(self):
return self.next_message_of(MessageType.DATA)
def next_command_message(self):
return self.next_message_of(MessageType.COMMAND)
def next_dtls_message(self, content_type, handshake_type=None):
while self.messages:
msg = self.messages.pop(0)
if msg.type != MessageType.DTLS:
continue
if msg.dtls.content_type != content_type:
continue
if (content_type == dtls.ContentType.HANDSHAKE and msg.dtls.handshake_type != handshake_type):
continue
return msg
t = (handshake_type if content_type == dtls.ContentType.HANDSHAKE else content_type)
raise ValueError("Could not find DTLS message of type: {}".format(str(t)))
def contains_icmp_message(self):
for m in self.messages:
if m.type == MessageType.ICMP:
return True
return False
def get_icmp_message(self, icmp_type):
for m in self.messages:
if m.type != MessageType.ICMP:
continue
if m.icmp.header.type == icmp_type:
return m
return None
def contains_mle_message(self, command_type):
for m in self.messages:
if m.type != MessageType.MLE:
continue
if m.mle.command.type == command_type:
return True
return False
def does_not_contain_coap_message(self):
for m in self.messages:
if m.type != MessageType.COAP:
continue
return False
return True
def clone(self):
"""Make a copy of current MessageSet.
"""
return MessagesSet(self.messages[:], self.commissioning_messages[:])
def __repr__(self):
return str(self.messages)
class MessageFactory:
def __init__(self, lowpan_parser):
self._lowpan_parser = lowpan_parser
def _add_device_descriptors(self, message):
for tlv in message.mle.command.tlvs:
if isinstance(tlv, mle.SourceAddress):
mac802154.DeviceDescriptors.add(tlv.address, message.mac_header.src_address)
if isinstance(tlv, mle.Address16):
mac802154.DeviceDescriptors.add(tlv.address, message.mac_header.dest_address)
def _parse_mac_frame(self, data):
mac_frame = mac802154.MacFrame()
mac_frame.parse(data)
return mac_frame
def set_lowpan_context(self, cid, prefix):
self._lowpan_parser.set_lowpan_context(cid, prefix)
def create(self, data):
try:
message = Message()
message.channel = struct.unpack(">B", data.read(1))
# Parse MAC header
mac_frame = self._parse_mac_frame(data)
message.mac_header = mac_frame.header
if message.mac_header.frame_type != mac802154.MacHeader.FrameType.DATA:
return [message]
message_info = common.MessageInfo()
message_info.source_mac_address = message.mac_header.src_address
message_info.destination_mac_address = message.mac_header.dest_address
# Create stream with 6LoWPAN datagram
lowpan_payload = io.BytesIO(mac_frame.payload.data)
ipv6_packet = self._lowpan_parser.parse(lowpan_payload, message_info)
if ipv6_packet is None:
return [message]
message.ipv6_packet = ipv6_packet
if message.type == MessageType.MLE:
self._add_device_descriptors(message)
return message.try_extract_dtls_messages()
except mac802154.KeyIdMode0Exception:
print('Received packet with key_id_mode = 0, cannot be handled in test scripts')
raise DropPacketException