# Copyright 2021-2022 Google LLC # # 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 # # https://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. # ----------------------------------------------------------------------------- # Imports # ----------------------------------------------------------------------------- import asyncio import logging import struct from colors import color from .core import BT_CENTRAL_ROLE, InvalidStateError, ProtocolError from .hci import (HCI_LE_Connection_Update_Command, HCI_Object, key_with_value, name_or_number) from .utils import EventEmitter # ----------------------------------------------------------------------------- # Logging # ----------------------------------------------------------------------------- logger = logging.getLogger(__name__) # ----------------------------------------------------------------------------- # Constants # ----------------------------------------------------------------------------- L2CAP_SIGNALING_CID = 0x01 L2CAP_LE_SIGNALING_CID = 0x05 L2CAP_MIN_LE_MTU = 23 L2CAP_MIN_BR_EDR_MTU = 48 L2CAP_DEFAULT_MTU = 2048 # Default value for the MTU we are willing to accept # See Bluetooth spec @ Vol 3, Part A - Table 2.1: CID name space on ACL-U, ASB-U, and AMP-U logical links L2CAP_ACL_U_DYNAMIC_CID_RANGE_START = 0x0040 L2CAP_ACL_U_DYNAMIC_CID_RANGE_END = 0xFFFF # See Bluetooth spec @ Vol 3, Part A - Table 2.2: CID name space on LE-U logical link L2CAP_LE_U_DYNAMIC_CID_RANGE_START = 0x0040 L2CAP_LE_U_DYNAMIC_CID_RANGE_START = 0x007F # Frame types L2CAP_COMMAND_REJECT = 0x01 L2CAP_CONNECTION_REQUEST = 0x02 L2CAP_CONNECTION_RESPONSE = 0x03 L2CAP_CONFIGURE_REQUEST = 0x04 L2CAP_CONFIGURE_RESPONSE = 0x05 L2CAP_DISCONNECTION_REQUEST = 0x06 L2CAP_DISCONNECTION_RESPONSE = 0x07 L2CAP_ECHO_REQUEST = 0x08 L2CAP_ECHO_RESPONSE = 0x09 L2CAP_INFORMATION_REQUEST = 0x0A L2CAP_INFORMATION_RESPONSE = 0x0B L2CAP_CREATE_CHANNEL_REQUEST = 0x0C L2CAP_CREATE_CHANNEL_RESPONSE = 0x0D L2CAP_MOVE_CHANNEL_REQUEST = 0x0E L2CAP_MOVE_CHANNEL_RESPONSE = 0x0F L2CAP_MOVE_CHANNEL_CONFIRMATION = 0x10 L2CAP_MOVE_CHANNEL_CONFIRMATION_RESPONSE = 0x11 L2CAP_CONNECTION_PARAMETER_UPDATE_REQUEST = 0x12 L2CAP_CONNECTION_PARAMETER_UPDATE_RESPONSE = 0x13 L2CAP_LE_CREDIT_BASED_CONNECTION_REQUEST = 0x14 L2CAP_LE_CREDIT_BASED_CONNECTION_RESPONSE = 0x15 L2CAP_LE_FLOW_CONTROL_CREDIT = 0x16 L2CAP_CONTROL_FRAME_NAMES = { L2CAP_COMMAND_REJECT: 'L2CAP_COMMAND_REJECT', L2CAP_CONNECTION_REQUEST: 'L2CAP_CONNECTION_REQUEST', L2CAP_CONNECTION_RESPONSE: 'L2CAP_CONNECTION_RESPONSE', L2CAP_CONFIGURE_REQUEST: 'L2CAP_CONFIGURE_REQUEST', L2CAP_CONFIGURE_RESPONSE: 'L2CAP_CONFIGURE_RESPONSE', L2CAP_DISCONNECTION_REQUEST: 'L2CAP_DISCONNECTION_REQUEST', L2CAP_DISCONNECTION_RESPONSE: 'L2CAP_DISCONNECTION_RESPONSE', L2CAP_ECHO_REQUEST: 'L2CAP_ECHO_REQUEST', L2CAP_ECHO_RESPONSE: 'L2CAP_ECHO_RESPONSE', L2CAP_INFORMATION_REQUEST: 'L2CAP_INFORMATION_REQUEST', L2CAP_INFORMATION_RESPONSE: 'L2CAP_INFORMATION_RESPONSE', L2CAP_CREATE_CHANNEL_REQUEST: 'L2CAP_CREATE_CHANNEL_REQUEST', L2CAP_CREATE_CHANNEL_RESPONSE: 'L2CAP_CREATE_CHANNEL_RESPONSE', L2CAP_MOVE_CHANNEL_REQUEST: 'L2CAP_MOVE_CHANNEL_REQUEST', L2CAP_MOVE_CHANNEL_RESPONSE: 'L2CAP_MOVE_CHANNEL_RESPONSE', L2CAP_MOVE_CHANNEL_CONFIRMATION: 'L2CAP_MOVE_CHANNEL_CONFIRMATION', L2CAP_MOVE_CHANNEL_CONFIRMATION_RESPONSE: 'L2CAP_MOVE_CHANNEL_CONFIRMATION_RESPONSE', L2CAP_CONNECTION_PARAMETER_UPDATE_REQUEST: 'L2CAP_CONNECTION_PARAMETER_UPDATE_REQUEST', L2CAP_CONNECTION_PARAMETER_UPDATE_RESPONSE: 'L2CAP_CONNECTION_PARAMETER_UPDATE_RESPONSE', L2CAP_LE_CREDIT_BASED_CONNECTION_REQUEST: 'L2CAP_LE_CREDIT_BASED_CONNECTION_REQUEST', L2CAP_LE_CREDIT_BASED_CONNECTION_RESPONSE: 'L2CAP_LE_CREDIT_BASED_CONNECTION_RESPONSE', L2CAP_LE_FLOW_CONTROL_CREDIT: 'L2CAP_LE_FLOW_CONTROL_CREDIT' } L2CAP_CONNECTION_PARAMETERS_ACCEPTED_RESULT = 0x0000 L2CAP_CONNECTION_PARAMETERS_REJECTED_RESULT = 0x0001 L2CAP_COMMAND_NOT_UNDERSTOOD_REASON = 0x0000 L2CAP_SIGNALING_MTU_EXCEEDED_REASON = 0x0001 L2CAP_INVALID_CID_IN_REQUEST_REASON = 0x0002 L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MTU = 2048 L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MPS = 2048 L2CAP_MAXIMUM_TRANSMISSION_UNIT_CONFIGURATION_OPTION_TYPE = 0x01 L2CAP_MTU_CONFIGURATION_PARAMETER_TYPE = 0x01 # ----------------------------------------------------------------------------- # Classes # ----------------------------------------------------------------------------- class L2CAP_PDU: ''' See Bluetooth spec @ Vol 3, Part A - 3 DATA PACKET FORMAT ''' @staticmethod def from_bytes(data): # Sanity check if len(data) < 4: raise ValueError('not enough data for L2CAP header') _, l2cap_pdu_cid = struct.unpack_from('= 2: type = data[0] length = data[1] value = data[2:2 + length] data = data[2 + length:] options.append((type, value)) return options @staticmethod def encode_configuration_options(options): return b''.join([bytes([option[0], len(option[1])]) + option[1] for option in options]) @staticmethod def subclass(fields): def inner(cls): cls.name = cls.__name__.upper() cls.code = key_with_value(L2CAP_CONTROL_FRAME_NAMES, cls.name) if cls.code is None: raise KeyError(f'Control Frame name {cls.name} not found in L2CAP_CONTROL_FRAME_NAMES') cls.fields = fields # Register a factory for this class L2CAP_Control_Frame.classes[cls.code] = cls return cls return inner def __init__(self, pdu=None, **kwargs): self.identifier = kwargs.get('identifier', 0) if hasattr(self, 'fields') and kwargs: HCI_Object.init_from_fields(self, self.fields, kwargs) if pdu is None: data = HCI_Object.dict_to_bytes(kwargs, self.fields) pdu = bytes([self.code, self.identifier]) + struct.pack(' 1: result += f': {self.pdu.hex()}' return result # ----------------------------------------------------------------------------- @L2CAP_Control_Frame.subclass([ ('reason', {'size': 2, 'mapper': lambda x: L2CAP_Command_Reject.map_reason(x)}), ('data', '*') ]) class L2CAP_Command_Reject(L2CAP_Control_Frame): ''' See Bluetooth spec @ Vol 3, Part A - 4.1 COMMAND REJECT ''' COMMAND_NOT_UNDERSTOOD = 0x0000 SIGNALING_MTU_EXCEEDED = 0x0001 INVALID_CID_IN_REQUEST = 0x0002 REASON_NAMES = { COMMAND_NOT_UNDERSTOOD: 'COMMAND_NOT_UNDERSTOOD', SIGNALING_MTU_EXCEEDED: 'SIGNALING_MTU_EXCEEDED', INVALID_CID_IN_REQUEST: 'INVALID_CID_IN_REQUEST' } @staticmethod def map_reason(reason): return name_or_number(L2CAP_Command_Reject.REASON_NAMES, reason) # ----------------------------------------------------------------------------- @L2CAP_Control_Frame.subclass([ ('psm', 2), ('source_cid', 2) ]) class L2CAP_Connection_Request(L2CAP_Control_Frame): ''' See Bluetooth spec @ Vol 3, Part A - 4.2 CONNECTION REQUEST ''' # ----------------------------------------------------------------------------- @L2CAP_Control_Frame.subclass([ ('destination_cid', 2), ('source_cid', 2), ('result', {'size': 2, 'mapper': lambda x: L2CAP_Connection_Response.result_name(x)}), ('status', 2) ]) class L2CAP_Connection_Response(L2CAP_Control_Frame): ''' See Bluetooth spec @ Vol 3, Part A - 4.3 CONNECTION RESPONSE ''' CONNECTION_SUCCESSFUL = 0x0000 CONNECTION_PENDING = 0x0001 CONNECTION_REFUSED_LE_PSM_NOT_SUPPORTED = 0x0002 CONNECTION_REFUSED_SECURITY_BLOCK = 0x0003 CONNECTION_REFUSED_NO_RESOURCES_AVAILABLE = 0x0004 CONNECTION_REFUSED_INVALID_SOURCE_CID = 0x0006 CONNECTION_REFUSED_SOURCE_CID_ALREADY_ALLOCATED = 0x0007 CONNECTION_REFUSED_UNACCEPTABLE_PARAMETERS = 0x000B CONNECTION_RESULT_NAMES = { CONNECTION_SUCCESSFUL: 'CONNECTION_SUCCESSFUL', CONNECTION_PENDING: 'CONNECTION_PENDING', CONNECTION_REFUSED_LE_PSM_NOT_SUPPORTED: 'CONNECTION_REFUSED_LE_PSM_NOT_SUPPORTED', CONNECTION_REFUSED_SECURITY_BLOCK: 'CONNECTION_REFUSED_SECURITY_BLOCK', CONNECTION_REFUSED_NO_RESOURCES_AVAILABLE: 'CONNECTION_REFUSED_NO_RESOURCES_AVAILABLE', CONNECTION_REFUSED_INVALID_SOURCE_CID: 'CONNECTION_REFUSED_INVALID_SOURCE_CID', CONNECTION_REFUSED_SOURCE_CID_ALREADY_ALLOCATED: 'CONNECTION_REFUSED_SOURCE_CID_ALREADY_ALLOCATED', CONNECTION_REFUSED_UNACCEPTABLE_PARAMETERS: 'CONNECTION_REFUSED_UNACCEPTABLE_PARAMETERS' } @staticmethod def result_name(result): return name_or_number(L2CAP_Connection_Response.CONNECTION_RESULT_NAMES, result) # ----------------------------------------------------------------------------- @L2CAP_Control_Frame.subclass([ ('destination_cid', 2), ('flags', 2), ('options', '*') ]) class L2CAP_Configure_Request(L2CAP_Control_Frame): ''' See Bluetooth spec @ Vol 3, Part A - 4.4 CONFIGURATION REQUEST ''' # ----------------------------------------------------------------------------- @L2CAP_Control_Frame.subclass([ ('source_cid', 2), ('flags', 2), ('result', {'size': 2, 'mapper': lambda x: L2CAP_Configure_Response.map_result(x)}), ('options', '*') ]) class L2CAP_Configure_Response(L2CAP_Control_Frame): ''' See Bluetooth spec @ Vol 3, Part A - 4.5 CONFIGURATION RESPONSE ''' SUCCESS = 0x0000 FAILURE_UNACCEPTABLE_PARAMETERS = 0x0001 FAILURE_REJECTED = 0x0002 FAILURE_UNKNOWN_OPTIONS = 0x0003 PENDING = 0x0004 FAILURE_FLOW_SPEC_REJECTED = 0x0005 RESULT_NAMES = { SUCCESS: 'SUCCESS', FAILURE_UNACCEPTABLE_PARAMETERS: 'FAILURE_UNACCEPTABLE_PARAMETERS', FAILURE_REJECTED: 'FAILURE_REJECTED', FAILURE_UNKNOWN_OPTIONS: 'FAILURE_UNKNOWN_OPTIONS', PENDING: 'PENDING', FAILURE_FLOW_SPEC_REJECTED: 'FAILURE_FLOW_SPEC_REJECTED' } @staticmethod def map_result(result): return name_or_number(L2CAP_Configure_Response.RESULT_NAMES, result) # ----------------------------------------------------------------------------- @L2CAP_Control_Frame.subclass([ ('destination_cid', 2), ('source_cid', 2) ]) class L2CAP_Disconnection_Request(L2CAP_Control_Frame): ''' See Bluetooth spec @ Vol 3, Part A - 4.6 DISCONNECTION REQUEST ''' # ----------------------------------------------------------------------------- @L2CAP_Control_Frame.subclass([ ('destination_cid', 2), ('source_cid', 2) ]) class L2CAP_Disconnection_Response(L2CAP_Control_Frame): ''' See Bluetooth spec @ Vol 3, Part A - 4.7 DISCONNECTION RESPONSE ''' # ----------------------------------------------------------------------------- @L2CAP_Control_Frame.subclass([ ('data', '*') ]) class L2CAP_Echo_Request(L2CAP_Control_Frame): ''' See Bluetooth spec @ Vol 3, Part A - 4.8 ECHO REQUEST ''' # ----------------------------------------------------------------------------- @L2CAP_Control_Frame.subclass([ ('data', '*') ]) class L2CAP_Echo_Response(L2CAP_Control_Frame): ''' See Bluetooth spec @ Vol 3, Part A - 4.9 ECHO RESPONSE ''' # ----------------------------------------------------------------------------- @L2CAP_Control_Frame.subclass([ ('info_type', 2) ]) class L2CAP_Information_Request(L2CAP_Control_Frame): ''' See Bluetooth spec @ Vol 3, Part A - 4.10 INFORMATION REQUEST ''' SUCCESS = 0x00 NOT_SUPPORTED = 0x01 CONNECTIONLESS_MTU = 0x0001 EXTENDED_FEATURES_SUPPORTED = 0x0002 FIXED_CHANNELS_SUPPORTED = 0x0003 # ----------------------------------------------------------------------------- @L2CAP_Control_Frame.subclass([ ('info_type', 2), ('result', 2), ('data', '*') ]) class L2CAP_Information_Response(L2CAP_Control_Frame): ''' See Bluetooth spec @ Vol 3, Part A - 4.11 INFORMATION RESPONSE ''' # ----------------------------------------------------------------------------- @L2CAP_Control_Frame.subclass([ ('interval_min', 2), ('interval_max', 2), ('slave_latency', 2), ('timeout_multiplier', 2) ]) class L2CAP_Connection_Parameter_Update_Request(L2CAP_Control_Frame): ''' See Bluetooth spec @ Vol 3, Part A - 4.20 CONNECTION PARAMETER UPDATE REQUEST ''' # ----------------------------------------------------------------------------- @L2CAP_Control_Frame.subclass([ ('result', 2) ]) class L2CAP_Connection_Parameter_Update_Response(L2CAP_Control_Frame): ''' See Bluetooth spec @ Vol 3, Part A - 4.21 CONNECTION PARAMETER UPDATE RESPONSE ''' # ----------------------------------------------------------------------------- @L2CAP_Control_Frame.subclass([ ('le_psm', 2), ('source_cid', 2), ('mtu', 2), ('mps', 2), ('initial_credits', 2) ]) class L2CAP_LE_Credit_Based_Connection_Request(L2CAP_Control_Frame): ''' See Bluetooth spec @ Vol 3, Part A - 4.22 LE CREDIT BASED CONNECTION REQUEST (CODE 0x14) ''' # ----------------------------------------------------------------------------- @L2CAP_Control_Frame.subclass([ ('destination_cid', 2), ('mtu', 2), ('mps', 2), ('initial_credits', 2), ('result', {'size': 2, 'mapper': lambda x: L2CAP_LE_Credit_Based_Connection_Response.map_result(x)}) ]) class L2CAP_LE_Credit_Based_Connection_Response(L2CAP_Control_Frame): ''' See Bluetooth spec @ Vol 3, Part A - 4.23 LE CREDIT BASED CONNECTION RESPONSE (CODE 0x15) ''' CONNECTION_SUCCESSFUL = 0x0000 CONNECTION_REFUSED_LE_PSM_NOT_SUPPORTED = 0x0002 CONNECTION_REFUSED_NO_RESOURCES_AVAILABLE = 0x0004 CONNECTION_REFUSED_INSUFFICIENT_AUTHENTICATION = 0x0005 CONNECTION_REFUSED_INSUFFICIENT_AUTHORIZATION = 0x0006 CONNECTION_REFUSED_INSUFFICIENT_ENCRYPTION_KEY_SIZE = 0x0007 CONNECTION_REFUSED_INSUFFICIENT_ENCRYPTION = 0x0008 CONNECTION_REFUSED_INVALID_SOURCE_CID = 0x0009 CONNECTION_REFUSED_SOURCE_CID_ALREADY_ALLOCATED = 0x000A CONNECTION_REFUSED_UNACCEPTABLE_PARAMETERS = 0x000B CONNECTION_RESULT_NAMES = { CONNECTION_SUCCESSFUL: 'CONNECTION_SUCCESSFUL', CONNECTION_REFUSED_LE_PSM_NOT_SUPPORTED: 'CONNECTION_REFUSED_LE_PSM_NOT_SUPPORTED', CONNECTION_REFUSED_NO_RESOURCES_AVAILABLE: 'CONNECTION_REFUSED_NO_RESOURCES_AVAILABLE', CONNECTION_REFUSED_INSUFFICIENT_AUTHENTICATION: 'CONNECTION_REFUSED_INSUFFICIENT_AUTHENTICATION', CONNECTION_REFUSED_INSUFFICIENT_AUTHORIZATION: 'CONNECTION_REFUSED_INSUFFICIENT_AUTHORIZATION', CONNECTION_REFUSED_INSUFFICIENT_ENCRYPTION_KEY_SIZE: 'CONNECTION_REFUSED_INSUFFICIENT_ENCRYPTION_KEY_SIZE', CONNECTION_REFUSED_INSUFFICIENT_ENCRYPTION: 'CONNECTION_REFUSED_INSUFFICIENT_ENCRYPTION', CONNECTION_REFUSED_INVALID_SOURCE_CID: 'CONNECTION_REFUSED_INVALID_SOURCE_CID', CONNECTION_REFUSED_SOURCE_CID_ALREADY_ALLOCATED: 'CONNECTION_REFUSED_SOURCE_CID_ALREADY_ALLOCATED', CONNECTION_REFUSED_UNACCEPTABLE_PARAMETERS: 'CONNECTION_REFUSED_UNACCEPTABLE_PARAMETERS' } @staticmethod def map_result(result): return name_or_number(L2CAP_LE_Credit_Based_Connection_Response.CONNECTION_RESULT_NAMES, result) # ----------------------------------------------------------------------------- @L2CAP_Control_Frame.subclass([ ('cid', 2), ('credits', 2) ]) class L2CAP_LE_Flow_Control_Credit(L2CAP_Control_Frame): ''' See Bluetooth spec @ Vol 3, Part A - 4.24 LE FLOW CONTROL CREDIT (CODE 0x16) ''' # ----------------------------------------------------------------------------- class Channel(EventEmitter): # States CLOSED = 0x00 WAIT_CONNECT = 0x01 WAIT_CONNECT_RSP = 0x02 OPEN = 0x03 WAIT_DISCONNECT = 0x04 WAIT_CREATE = 0x05 WAIT_CREATE_RSP = 0x06 WAIT_MOVE = 0x07 WAIT_MOVE_RSP = 0x08 WAIT_MOVE_CONFIRM = 0x09 WAIT_CONFIRM_RSP = 0x0A # CONFIG substates WAIT_CONFIG = 0x10 WAIT_SEND_CONFIG = 0x11 WAIT_CONFIG_REQ_RSP = 0x12 WAIT_CONFIG_RSP = 0x13 WAIT_CONFIG_REQ = 0x14 WAIT_IND_FINAL_RSP = 0x15 WAIT_FINAL_RSP = 0x16 WAIT_CONTROL_IND = 0x17 STATE_NAMES = { CLOSED: 'CLOSED', WAIT_CONNECT: 'WAIT_CONNECT', WAIT_CONNECT_RSP: 'WAIT_CONNECT_RSP', OPEN: 'OPEN', WAIT_DISCONNECT: 'WAIT_DISCONNECT', WAIT_CREATE: 'WAIT_CREATE', WAIT_CREATE_RSP: 'WAIT_CREATE_RSP', WAIT_MOVE: 'WAIT_MOVE', WAIT_MOVE_RSP: 'WAIT_MOVE_RSP', WAIT_MOVE_CONFIRM: 'WAIT_MOVE_CONFIRM', WAIT_CONFIRM_RSP: 'WAIT_CONFIRM_RSP', WAIT_CONFIG: 'WAIT_CONFIG', WAIT_SEND_CONFIG: 'WAIT_SEND_CONFIG', WAIT_CONFIG_REQ_RSP: 'WAIT_CONFIG_REQ_RSP', WAIT_CONFIG_RSP: 'WAIT_CONFIG_RSP', WAIT_CONFIG_REQ: 'WAIT_CONFIG_REQ', WAIT_IND_FINAL_RSP: 'WAIT_IND_FINAL_RSP', WAIT_FINAL_RSP: 'WAIT_FINAL_RSP', WAIT_CONTROL_IND: 'WAIT_CONTROL_IND' } def __init__(self, manager, connection, signaling_cid, psm, source_cid, mtu): super().__init__() self.manager = manager self.connection = connection self.signaling_cid = signaling_cid self.state = Channel.CLOSED self.mtu = mtu self.psm = psm self.source_cid = source_cid self.destination_cid = 0 self.response = None self.connection_result = None self.sink = None def change_state(self, new_state): logger.debug(f'{self} state change -> {color(Channel.STATE_NAMES[new_state], "cyan")}') self.state = new_state def send_pdu(self, pdu): self.manager.send_pdu(self.connection, self.destination_cid, pdu) async def send_request(self, request): # Check that there isn't already a request pending if self.response: raise InvalidStateError('request already pending') if self.state != Channel.OPEN: raise InvalidStateError('channel not open') self.response = asyncio.get_running_loop().create_future() self.send_pdu(request) return await self.response def on_pdu(self, pdu): if self.response: self.response.set_result(pdu) self.response = None elif self.sink: self.sink(pdu) else: logger.warn(color('received pdu without a pending request or sink', 'red')) def send_control_frame(self, frame): self.manager.send_control_frame(self.connection, self.signaling_cid, frame) async def connect(self): if self.state != Channel.CLOSED: raise InvalidStateError('invalid state') self.change_state(Channel.WAIT_CONNECT_RSP) self.send_control_frame( L2CAP_Connection_Request( identifier = self.manager.next_identifier(self.connection), psm = self.psm, source_cid = self.source_cid ) ) # Create a future to wait for the state machine to get to a success or error state self.connection_result = asyncio.get_running_loop().create_future() return await self.connection_result async def disconnect(self): if self.state != Channel.OPEN: raise InvalidStateError('invalid state') self.change_state(Channel.WAIT_DISCONNECT) self.send_control_frame( L2CAP_Disconnection_Request( identifier = self.manager.next_identifier(self.connection), destination_cid = self.destination_cid, source_cid = self.source_cid ) ) # Create a future to wait for the state machine to get to a success or error state self.disconnection_result = asyncio.get_running_loop().create_future() return await self.disconnection_result def send_configure_request(self): options = L2CAP_Control_Frame.encode_configuration_options([( L2CAP_MAXIMUM_TRANSMISSION_UNIT_CONFIGURATION_OPTION_TYPE, struct.pack('{self.destination_cid}, PSM={self.psm}, MTU={self.mtu}, state={Channel.STATE_NAMES[self.state]})' # ----------------------------------------------------------------------------- class ChannelManager: def __init__(self): self.host = None self.channels = {} # Channels, mapped by connection and cid self.identifiers = {} # Incrementing identifier values by connection self.servers = {} # Servers accepting connections, by PSM def find_channel(self, connection_handle, cid): if connection_channels := self.channels.get(connection_handle): return connection_channels.get(cid) @staticmethod def find_free_br_edr_cid(channels): # Pick the smallest valid CID that's not already in the list # (not necessarily the most efficient algorithm, but the list of CID is # very small in practice) for cid in range(L2CAP_ACL_U_DYNAMIC_CID_RANGE_START, L2CAP_ACL_U_DYNAMIC_CID_RANGE_END + 1): if cid not in channels: return cid def next_identifier(self, connection): identifier = (self.identifiers.setdefault(connection.handle, 0) + 1) % 256 self.identifiers[connection.handle] = identifier return identifier def register_server(self, psm, server): self.servers[psm] = server def send_pdu(self, connection, cid, pdu): pdu_str = pdu.hex() if type(pdu) is bytes else str(pdu) logger.debug(f'{color(">>> Sending L2CAP PDU", "blue")} on connection [0x{connection.handle:04X}] (CID={cid}) {connection.peer_address}: {pdu_str}') self.host.send_l2cap_pdu(connection.handle, cid, bytes(pdu)) def on_pdu(self, connection, cid, pdu): if cid == L2CAP_SIGNALING_CID or cid == L2CAP_LE_SIGNALING_CID: # Parse the L2CAP payload into a Control Frame object control_frame = L2CAP_Control_Frame.from_bytes(pdu) self.on_control_frame(connection, cid, control_frame) else: if (channel := self.find_channel(connection.handle, cid)) is None: logger.warn(color(f'channel not found for 0x{connection.handle:04X}:{cid}', 'red')) return channel.on_pdu(pdu) def send_control_frame(self, connection, cid, control_frame): logger.debug(f'{color(">>> Sending L2CAP Signaling Control Frame", "blue")} on connection [0x{connection.handle:04X}] (CID={cid}) {connection.peer_address}:\n{control_frame}') self.host.send_l2cap_pdu(connection.handle, cid, bytes(control_frame)) def on_control_frame(self, connection, cid, control_frame): logger.debug(f'{color("<<< Received L2CAP Signaling Control Frame", "green")} on connection [0x{connection.handle:04X}] (CID={cid}) {connection.peer_address}:\n{control_frame}') # Find the handler method handler_name = f'on_{control_frame.name.lower()}' handler = getattr(self, handler_name, None) if handler: try: handler(connection, cid, control_frame) except Exception as error: logger.warning(f'{color("!!! Exception in handler:", "red")} {error}') self.send_control_frame( connection, cid, L2CAP_Command_Reject( identifier = control_frame.identifier, reason = L2CAP_COMMAND_NOT_UNDERSTOOD_REASON, data = b'' ) ) raise error else: logger.error(color('Channel Manager command not handled???', 'red')) self.send_control_frame( connection, cid, L2CAP_Command_Reject( identifier = control_frame.identifier, reason = L2CAP_COMMAND_NOT_UNDERSTOOD_REASON, data = b'' ) ) def on_l2cap_command_reject(self, connection, cid, packet): logger.warning(f'{color("!!! Command rejected:", "red")} {packet.reason}') pass def on_l2cap_connection_request(self, connection, cid, request): # Check if there's a server for this PSM server = self.servers.get(request.psm) if server: # Find a free CID for this new channel connection_channels = self.channels.setdefault(connection.handle, {}) source_cid = self.find_free_br_edr_cid(connection_channels) if source_cid is None: # Should never happen! self.send_control_frame( connection, cid, L2CAP_Connection_Response( identifier = request.identifier, destination_cid = request.source_cid, source_cid = 0, result = L2CAP_Connection_Response.CONNECTION_REFUSED_NO_RESOURCES_AVAILABLE, status = 0x0000 ) ) return # Create a new channel logger.debug(f'creating server channel with cid={source_cid} for psm {request.psm}') channel = Channel(self, connection, cid, request.psm, source_cid, L2CAP_MIN_BR_EDR_MTU) connection_channels[source_cid] = channel # Notify server(channel) channel.on_connection_request(request) else: logger.warn(f'No server for connection 0x{connection.handle:04X} on PSM {request.psm}') self.send_control_frame( connection, cid, L2CAP_Connection_Response( identifier = request.identifier, destination_cid = request.source_cid, source_cid = 0, result = L2CAP_Connection_Response.CONNECTION_REFUSED_LE_PSM_NOT_SUPPORTED, status = 0x0000 ) ) def on_l2cap_connection_response(self, connection, cid, response): if (channel := self.find_channel(connection.handle, response.source_cid)) is None: logger.warn(color(f'channel {response.source_cid} not found for 0x{connection.handle:04X}:{cid}', 'red')) return channel.on_connection_response(response) def on_l2cap_configure_request(self, connection, cid, request): if (channel := self.find_channel(connection.handle, request.destination_cid)) is None: logger.warn(color(f'channel {request.destination_cid} not found for 0x{connection.handle:04X}:{cid}', 'red')) return channel.on_configure_request(request) def on_l2cap_configure_response(self, connection, cid, response): if (channel := self.find_channel(connection.handle, response.source_cid)) is None: logger.warn(color(f'channel {response.source_cid} not found for 0x{connection.handle:04X}:{cid}', 'red')) return channel.on_configure_response(response) def on_l2cap_disconnection_request(self, connection, cid, request): if (channel := self.find_channel(connection.handle, request.destination_cid)) is None: logger.warn(color(f'channel {request.destination_cid} not found for 0x{connection.handle:04X}:{cid}', 'red')) return channel.on_disconnection_request(request) def on_l2cap_disconnection_response(self, connection, cid, response): if (channel := self.find_channel(connection.handle, response.source_cid)) is None: logger.warn(color(f'channel {response.source_cid} not found for 0x{connection.handle:04X}:{cid}', 'red')) return channel.on_disconnection_response(response) def on_l2cap_echo_request(self, connection, cid, request): logger.debug(f'<<< Echo request: data={request.data.hex()}') self.send_control_frame( connection, cid, L2CAP_Echo_Response( identifier = request.identifier, data = request.data ) ) def on_l2cap_echo_response(self, connection, cid, response): logger.debug(f'<<< Echo response: data={response.data.hex()}') # TODO notify listeners def on_l2cap_information_request(self, connection, cid, request): if request.info_type == L2CAP_Information_Request.CONNECTIONLESS_MTU: result = L2CAP_Information_Request.SUCCESS data = struct.pack('