1# Copyright 2021-2022 Google LLC 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15# ----------------------------------------------------------------------------- 16# Imports 17# ----------------------------------------------------------------------------- 18from __future__ import annotations 19import logging 20import struct 21from typing import Dict, List, Type, Optional, Tuple, Union, NewType, TYPE_CHECKING 22from typing_extensions import Self 23 24from . import core, l2cap 25from .colors import color 26from .core import InvalidStateError 27from .hci import HCI_Object, name_or_number, key_with_value 28 29if TYPE_CHECKING: 30 from .device import Device, Connection 31 32# ----------------------------------------------------------------------------- 33# Logging 34# ----------------------------------------------------------------------------- 35logger = logging.getLogger(__name__) 36 37 38# ----------------------------------------------------------------------------- 39# Constants 40# ----------------------------------------------------------------------------- 41# fmt: off 42# pylint: disable=line-too-long 43 44SDP_CONTINUATION_WATCHDOG = 64 # Maximum number of continuations we're willing to do 45 46SDP_PSM = 0x0001 47 48SDP_ERROR_RESPONSE = 0x01 49SDP_SERVICE_SEARCH_REQUEST = 0x02 50SDP_SERVICE_SEARCH_RESPONSE = 0x03 51SDP_SERVICE_ATTRIBUTE_REQUEST = 0x04 52SDP_SERVICE_ATTRIBUTE_RESPONSE = 0x05 53SDP_SERVICE_SEARCH_ATTRIBUTE_REQUEST = 0x06 54SDP_SERVICE_SEARCH_ATTRIBUTE_RESPONSE = 0x07 55 56SDP_PDU_NAMES = { 57 SDP_ERROR_RESPONSE: 'SDP_ERROR_RESPONSE', 58 SDP_SERVICE_SEARCH_REQUEST: 'SDP_SERVICE_SEARCH_REQUEST', 59 SDP_SERVICE_SEARCH_RESPONSE: 'SDP_SERVICE_SEARCH_RESPONSE', 60 SDP_SERVICE_ATTRIBUTE_REQUEST: 'SDP_SERVICE_ATTRIBUTE_REQUEST', 61 SDP_SERVICE_ATTRIBUTE_RESPONSE: 'SDP_SERVICE_ATTRIBUTE_RESPONSE', 62 SDP_SERVICE_SEARCH_ATTRIBUTE_REQUEST: 'SDP_SERVICE_SEARCH_ATTRIBUTE_REQUEST', 63 SDP_SERVICE_SEARCH_ATTRIBUTE_RESPONSE: 'SDP_SERVICE_SEARCH_ATTRIBUTE_RESPONSE' 64} 65 66SDP_INVALID_SDP_VERSION_ERROR = 0x0001 67SDP_INVALID_SERVICE_RECORD_HANDLE_ERROR = 0x0002 68SDP_INVALID_REQUEST_SYNTAX_ERROR = 0x0003 69SDP_INVALID_PDU_SIZE_ERROR = 0x0004 70SDP_INVALID_CONTINUATION_STATE_ERROR = 0x0005 71SDP_INSUFFICIENT_RESOURCES_TO_SATISFY_REQUEST_ERROR = 0x0006 72 73SDP_ERROR_NAMES = { 74 SDP_INVALID_SDP_VERSION_ERROR: 'SDP_INVALID_SDP_VERSION_ERROR', 75 SDP_INVALID_SERVICE_RECORD_HANDLE_ERROR: 'SDP_INVALID_SERVICE_RECORD_HANDLE_ERROR', 76 SDP_INVALID_REQUEST_SYNTAX_ERROR: 'SDP_INVALID_REQUEST_SYNTAX_ERROR', 77 SDP_INVALID_PDU_SIZE_ERROR: 'SDP_INVALID_PDU_SIZE_ERROR', 78 SDP_INVALID_CONTINUATION_STATE_ERROR: 'SDP_INVALID_CONTINUATION_STATE_ERROR', 79 SDP_INSUFFICIENT_RESOURCES_TO_SATISFY_REQUEST_ERROR: 'SDP_INSUFFICIENT_RESOURCES_TO_SATISFY_REQUEST_ERROR' 80} 81 82SDP_SERVICE_NAME_ATTRIBUTE_ID_OFFSET = 0x0000 83SDP_SERVICE_DESCRIPTION_ATTRIBUTE_ID_OFFSET = 0x0001 84SDP_PROVIDER_NAME_ATTRIBUTE_ID_OFFSET = 0x0002 85 86SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID = 0X0000 87SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID = 0X0001 88SDP_SERVICE_RECORD_STATE_ATTRIBUTE_ID = 0X0002 89SDP_SERVICE_ID_ATTRIBUTE_ID = 0X0003 90SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID = 0X0004 91SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID = 0X0005 92SDP_LANGUAGE_BASE_ATTRIBUTE_ID_LIST_ATTRIBUTE_ID = 0X0006 93SDP_SERVICE_INFO_TIME_TO_LIVE_ATTRIBUTE_ID = 0X0007 94SDP_SERVICE_AVAILABILITY_ATTRIBUTE_ID = 0X0008 95SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID = 0X0009 96SDP_DOCUMENTATION_URL_ATTRIBUTE_ID = 0X000A 97SDP_CLIENT_EXECUTABLE_URL_ATTRIBUTE_ID = 0X000B 98SDP_ICON_URL_ATTRIBUTE_ID = 0X000C 99SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID = 0X000D 100 101 102# Profile-specific Attribute Identifiers (cf. Assigned Numbers for Service Discovery) 103# used by AVRCP, HFP and A2DP 104SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID = 0x0311 105 106SDP_ATTRIBUTE_ID_NAMES = { 107 SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID: 'SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID', 108 SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID: 'SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID', 109 SDP_SERVICE_RECORD_STATE_ATTRIBUTE_ID: 'SDP_SERVICE_RECORD_STATE_ATTRIBUTE_ID', 110 SDP_SERVICE_ID_ATTRIBUTE_ID: 'SDP_SERVICE_ID_ATTRIBUTE_ID', 111 SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID: 'SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID', 112 SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID: 'SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID', 113 SDP_LANGUAGE_BASE_ATTRIBUTE_ID_LIST_ATTRIBUTE_ID: 'SDP_LANGUAGE_BASE_ATTRIBUTE_ID_LIST_ATTRIBUTE_ID', 114 SDP_SERVICE_INFO_TIME_TO_LIVE_ATTRIBUTE_ID: 'SDP_SERVICE_INFO_TIME_TO_LIVE_ATTRIBUTE_ID', 115 SDP_SERVICE_AVAILABILITY_ATTRIBUTE_ID: 'SDP_SERVICE_AVAILABILITY_ATTRIBUTE_ID', 116 SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID: 'SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID', 117 SDP_DOCUMENTATION_URL_ATTRIBUTE_ID: 'SDP_DOCUMENTATION_URL_ATTRIBUTE_ID', 118 SDP_CLIENT_EXECUTABLE_URL_ATTRIBUTE_ID: 'SDP_CLIENT_EXECUTABLE_URL_ATTRIBUTE_ID', 119 SDP_ICON_URL_ATTRIBUTE_ID: 'SDP_ICON_URL_ATTRIBUTE_ID', 120 SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID: 'SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID', 121 SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID: 'SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID', 122} 123 124SDP_PUBLIC_BROWSE_ROOT = core.UUID.from_16_bits(0x1002, 'PublicBrowseRoot') 125 126# To be used in searches where an attribute ID list allows a range to be specified 127SDP_ALL_ATTRIBUTES_RANGE = (0x0000FFFF, 4) # Express this as tuple so we can convey the desired encoding size 128 129# fmt: on 130# pylint: enable=line-too-long 131# pylint: disable=invalid-name 132 133 134# ----------------------------------------------------------------------------- 135class DataElement: 136 NIL = 0 137 UNSIGNED_INTEGER = 1 138 SIGNED_INTEGER = 2 139 UUID = 3 140 TEXT_STRING = 4 141 BOOLEAN = 5 142 SEQUENCE = 6 143 ALTERNATIVE = 7 144 URL = 8 145 146 TYPE_NAMES = { 147 NIL: 'NIL', 148 UNSIGNED_INTEGER: 'UNSIGNED_INTEGER', 149 SIGNED_INTEGER: 'SIGNED_INTEGER', 150 UUID: 'UUID', 151 TEXT_STRING: 'TEXT_STRING', 152 BOOLEAN: 'BOOLEAN', 153 SEQUENCE: 'SEQUENCE', 154 ALTERNATIVE: 'ALTERNATIVE', 155 URL: 'URL', 156 } 157 158 type_constructors = { 159 NIL: lambda x: DataElement(DataElement.NIL, None), 160 UNSIGNED_INTEGER: lambda x, y: DataElement( 161 DataElement.UNSIGNED_INTEGER, 162 DataElement.unsigned_integer_from_bytes(x), 163 value_size=y, 164 ), 165 SIGNED_INTEGER: lambda x, y: DataElement( 166 DataElement.SIGNED_INTEGER, 167 DataElement.signed_integer_from_bytes(x), 168 value_size=y, 169 ), 170 UUID: lambda x: DataElement( 171 DataElement.UUID, core.UUID.from_bytes(bytes(reversed(x))) 172 ), 173 TEXT_STRING: lambda x: DataElement(DataElement.TEXT_STRING, x), 174 BOOLEAN: lambda x: DataElement(DataElement.BOOLEAN, x[0] == 1), 175 SEQUENCE: lambda x: DataElement( 176 DataElement.SEQUENCE, DataElement.list_from_bytes(x) 177 ), 178 ALTERNATIVE: lambda x: DataElement( 179 DataElement.ALTERNATIVE, DataElement.list_from_bytes(x) 180 ), 181 URL: lambda x: DataElement(DataElement.URL, x.decode('utf8')), 182 } 183 184 def __init__(self, element_type, value, value_size=None): 185 self.type = element_type 186 self.value = value 187 self.value_size = value_size 188 # Used as a cache when parsing from bytes so we can emit a byte-for-byte replica 189 self.bytes = None 190 if element_type in (DataElement.UNSIGNED_INTEGER, DataElement.SIGNED_INTEGER): 191 if value_size is None: 192 raise ValueError('integer types must have a value size specified') 193 194 @staticmethod 195 def nil() -> DataElement: 196 return DataElement(DataElement.NIL, None) 197 198 @staticmethod 199 def unsigned_integer(value: int, value_size: int) -> DataElement: 200 return DataElement(DataElement.UNSIGNED_INTEGER, value, value_size) 201 202 @staticmethod 203 def unsigned_integer_8(value: int) -> DataElement: 204 return DataElement(DataElement.UNSIGNED_INTEGER, value, value_size=1) 205 206 @staticmethod 207 def unsigned_integer_16(value: int) -> DataElement: 208 return DataElement(DataElement.UNSIGNED_INTEGER, value, value_size=2) 209 210 @staticmethod 211 def unsigned_integer_32(value: int) -> DataElement: 212 return DataElement(DataElement.UNSIGNED_INTEGER, value, value_size=4) 213 214 @staticmethod 215 def signed_integer(value: int, value_size: int) -> DataElement: 216 return DataElement(DataElement.SIGNED_INTEGER, value, value_size) 217 218 @staticmethod 219 def signed_integer_8(value: int) -> DataElement: 220 return DataElement(DataElement.SIGNED_INTEGER, value, value_size=1) 221 222 @staticmethod 223 def signed_integer_16(value: int) -> DataElement: 224 return DataElement(DataElement.SIGNED_INTEGER, value, value_size=2) 225 226 @staticmethod 227 def signed_integer_32(value: int) -> DataElement: 228 return DataElement(DataElement.SIGNED_INTEGER, value, value_size=4) 229 230 @staticmethod 231 def uuid(value: core.UUID) -> DataElement: 232 return DataElement(DataElement.UUID, value) 233 234 @staticmethod 235 def text_string(value: bytes) -> DataElement: 236 return DataElement(DataElement.TEXT_STRING, value) 237 238 @staticmethod 239 def boolean(value: bool) -> DataElement: 240 return DataElement(DataElement.BOOLEAN, value) 241 242 @staticmethod 243 def sequence(value: List[DataElement]) -> DataElement: 244 return DataElement(DataElement.SEQUENCE, value) 245 246 @staticmethod 247 def alternative(value: List[DataElement]) -> DataElement: 248 return DataElement(DataElement.ALTERNATIVE, value) 249 250 @staticmethod 251 def url(value: str) -> DataElement: 252 return DataElement(DataElement.URL, value) 253 254 @staticmethod 255 def unsigned_integer_from_bytes(data): 256 if len(data) == 1: 257 return data[0] 258 259 if len(data) == 2: 260 return struct.unpack('>H', data)[0] 261 262 if len(data) == 4: 263 return struct.unpack('>I', data)[0] 264 265 if len(data) == 8: 266 return struct.unpack('>Q', data)[0] 267 268 raise ValueError(f'invalid integer length {len(data)}') 269 270 @staticmethod 271 def signed_integer_from_bytes(data): 272 if len(data) == 1: 273 return struct.unpack('b', data)[0] 274 275 if len(data) == 2: 276 return struct.unpack('>h', data)[0] 277 278 if len(data) == 4: 279 return struct.unpack('>i', data)[0] 280 281 if len(data) == 8: 282 return struct.unpack('>q', data)[0] 283 284 raise ValueError(f'invalid integer length {len(data)}') 285 286 @staticmethod 287 def list_from_bytes(data): 288 elements = [] 289 while data: 290 element = DataElement.from_bytes(data) 291 elements.append(element) 292 data = data[len(bytes(element)) :] 293 return elements 294 295 @staticmethod 296 def parse_from_bytes(data, offset): 297 element = DataElement.from_bytes(data[offset:]) 298 return offset + len(bytes(element)), element 299 300 @staticmethod 301 def from_bytes(data): 302 element_type = data[0] >> 3 303 size_index = data[0] & 7 304 value_offset = 0 305 if size_index == 0: 306 if element_type == DataElement.NIL: 307 value_size = 0 308 else: 309 value_size = 1 310 elif size_index == 1: 311 value_size = 2 312 elif size_index == 2: 313 value_size = 4 314 elif size_index == 3: 315 value_size = 8 316 elif size_index == 4: 317 value_size = 16 318 elif size_index == 5: 319 value_size = data[1] 320 value_offset = 1 321 elif size_index == 6: 322 value_size = struct.unpack('>H', data[1:3])[0] 323 value_offset = 2 324 else: # size_index == 7 325 value_size = struct.unpack('>I', data[1:5])[0] 326 value_offset = 4 327 328 value_data = data[1 + value_offset : 1 + value_offset + value_size] 329 constructor = DataElement.type_constructors.get(element_type) 330 if constructor: 331 if element_type in ( 332 DataElement.UNSIGNED_INTEGER, 333 DataElement.SIGNED_INTEGER, 334 ): 335 result = constructor(value_data, value_size) 336 else: 337 result = constructor(value_data) 338 else: 339 result = DataElement(element_type, value_data) 340 result.bytes = data[ 341 : 1 + value_offset + value_size 342 ] # Keep a copy so we can re-serialize to an exact replica 343 return result 344 345 def to_bytes(self): 346 return bytes(self) 347 348 def __bytes__(self): 349 # Return early if we have a cache 350 if self.bytes: 351 return self.bytes 352 353 if self.type == DataElement.NIL: 354 data = b'' 355 elif self.type == DataElement.UNSIGNED_INTEGER: 356 if self.value < 0: 357 raise ValueError('UNSIGNED_INTEGER cannot be negative') 358 359 if self.value_size == 1: 360 data = struct.pack('B', self.value) 361 elif self.value_size == 2: 362 data = struct.pack('>H', self.value) 363 elif self.value_size == 4: 364 data = struct.pack('>I', self.value) 365 elif self.value_size == 8: 366 data = struct.pack('>Q', self.value) 367 else: 368 raise ValueError('invalid value_size') 369 elif self.type == DataElement.SIGNED_INTEGER: 370 if self.value_size == 1: 371 data = struct.pack('b', self.value) 372 elif self.value_size == 2: 373 data = struct.pack('>h', self.value) 374 elif self.value_size == 4: 375 data = struct.pack('>i', self.value) 376 elif self.value_size == 8: 377 data = struct.pack('>q', self.value) 378 else: 379 raise ValueError('invalid value_size') 380 elif self.type == DataElement.UUID: 381 data = bytes(reversed(bytes(self.value))) 382 elif self.type == DataElement.URL: 383 data = self.value.encode('utf8') 384 elif self.type == DataElement.BOOLEAN: 385 data = bytes([1 if self.value else 0]) 386 elif self.type in (DataElement.SEQUENCE, DataElement.ALTERNATIVE): 387 data = b''.join([bytes(element) for element in self.value]) 388 else: 389 data = self.value 390 391 size = len(data) 392 size_bytes = b'' 393 if self.type == DataElement.NIL: 394 if size != 0: 395 raise ValueError('NIL must be empty') 396 size_index = 0 397 elif self.type in ( 398 DataElement.UNSIGNED_INTEGER, 399 DataElement.SIGNED_INTEGER, 400 DataElement.UUID, 401 ): 402 if size <= 1: 403 size_index = 0 404 elif size == 2: 405 size_index = 1 406 elif size == 4: 407 size_index = 2 408 elif size == 8: 409 size_index = 3 410 elif size == 16: 411 size_index = 4 412 else: 413 raise ValueError('invalid data size') 414 elif self.type in ( 415 DataElement.TEXT_STRING, 416 DataElement.SEQUENCE, 417 DataElement.ALTERNATIVE, 418 DataElement.URL, 419 ): 420 if size <= 0xFF: 421 size_index = 5 422 size_bytes = bytes([size]) 423 elif size <= 0xFFFF: 424 size_index = 6 425 size_bytes = struct.pack('>H', size) 426 elif size <= 0xFFFFFFFF: 427 size_index = 7 428 size_bytes = struct.pack('>I', size) 429 else: 430 raise ValueError('invalid data size') 431 elif self.type == DataElement.BOOLEAN: 432 if size != 1: 433 raise ValueError('boolean must be 1 byte') 434 size_index = 0 435 436 self.bytes = bytes([self.type << 3 | size_index]) + size_bytes + data 437 return self.bytes 438 439 def to_string(self, pretty=False, indentation=0): 440 prefix = ' ' * indentation 441 type_name = name_or_number(self.TYPE_NAMES, self.type) 442 if self.type == DataElement.NIL: 443 value_string = '' 444 elif self.type in (DataElement.SEQUENCE, DataElement.ALTERNATIVE): 445 container_separator = '\n' if pretty else '' 446 element_separator = '\n' if pretty else ',' 447 elements = [ 448 element.to_string(pretty, indentation + 1 if pretty else 0) 449 for element in self.value 450 ] 451 value_string = ( 452 f'[{container_separator}' 453 f'{element_separator.join(elements)}' 454 f'{container_separator}{prefix}]' 455 ) 456 elif self.type in (DataElement.UNSIGNED_INTEGER, DataElement.SIGNED_INTEGER): 457 value_string = f'{self.value}#{self.value_size}' 458 elif isinstance(self.value, DataElement): 459 value_string = self.value.to_string(pretty, indentation) 460 else: 461 value_string = str(self.value) 462 return f'{prefix}{type_name}({value_string})' 463 464 def __str__(self): 465 return self.to_string() 466 467 468# ----------------------------------------------------------------------------- 469class ServiceAttribute: 470 def __init__(self, attribute_id: int, value: DataElement) -> None: 471 self.id = attribute_id 472 self.value = value 473 474 @staticmethod 475 def list_from_data_elements(elements: List[DataElement]) -> List[ServiceAttribute]: 476 attribute_list = [] 477 for i in range(0, len(elements) // 2): 478 attribute_id, attribute_value = elements[2 * i : 2 * (i + 1)] 479 if attribute_id.type != DataElement.UNSIGNED_INTEGER: 480 logger.warning('attribute ID element is not an integer') 481 continue 482 attribute_list.append(ServiceAttribute(attribute_id.value, attribute_value)) 483 484 return attribute_list 485 486 @staticmethod 487 def find_attribute_in_list( 488 attribute_list: List[ServiceAttribute], attribute_id: int 489 ) -> Optional[DataElement]: 490 return next( 491 ( 492 attribute.value 493 for attribute in attribute_list 494 if attribute.id == attribute_id 495 ), 496 None, 497 ) 498 499 @staticmethod 500 def id_name(id_code): 501 return name_or_number(SDP_ATTRIBUTE_ID_NAMES, id_code) 502 503 @staticmethod 504 def is_uuid_in_value(uuid: core.UUID, value: DataElement) -> bool: 505 # Find if a uuid matches a value, either directly or recursing into sequences 506 if value.type == DataElement.UUID: 507 return value.value == uuid 508 509 if value.type == DataElement.SEQUENCE: 510 for element in value.value: 511 if ServiceAttribute.is_uuid_in_value(uuid, element): 512 return True 513 return False 514 515 return False 516 517 def to_string(self, with_colors=False): 518 if with_colors: 519 return ( 520 f'Attribute(id={color(self.id_name(self.id),"magenta")},' 521 f'value={self.value})' 522 ) 523 524 return f'Attribute(id={self.id_name(self.id)},value={self.value})' 525 526 def __str__(self): 527 return self.to_string() 528 529 530# ----------------------------------------------------------------------------- 531class SDP_PDU: 532 ''' 533 See Bluetooth spec @ Vol 3, Part B - 4.2 PROTOCOL DATA UNIT FORMAT 534 ''' 535 536 sdp_pdu_classes: Dict[int, Type[SDP_PDU]] = {} 537 name = None 538 pdu_id = 0 539 540 @staticmethod 541 def from_bytes(pdu): 542 pdu_id, transaction_id, _parameters_length = struct.unpack_from('>BHH', pdu, 0) 543 544 cls = SDP_PDU.sdp_pdu_classes.get(pdu_id) 545 if cls is None: 546 instance = SDP_PDU(pdu) 547 instance.name = SDP_PDU.pdu_name(pdu_id) 548 instance.pdu_id = pdu_id 549 instance.transaction_id = transaction_id 550 return instance 551 self = cls.__new__(cls) 552 SDP_PDU.__init__(self, pdu, transaction_id) 553 if hasattr(self, 'fields'): 554 self.init_from_bytes(pdu, 5) 555 return self 556 557 @staticmethod 558 def parse_service_record_handle_list_preceded_by_count( 559 data: bytes, offset: int 560 ) -> Tuple[int, List[int]]: 561 count = struct.unpack_from('>H', data, offset - 2)[0] 562 handle_list = [ 563 struct.unpack_from('>I', data, offset + x * 4)[0] for x in range(count) 564 ] 565 return offset + count * 4, handle_list 566 567 @staticmethod 568 def parse_bytes_preceded_by_length(data, offset): 569 length = struct.unpack_from('>H', data, offset - 2)[0] 570 return offset + length, data[offset : offset + length] 571 572 @staticmethod 573 def error_name(error_code): 574 return name_or_number(SDP_ERROR_NAMES, error_code) 575 576 @staticmethod 577 def pdu_name(code): 578 return name_or_number(SDP_PDU_NAMES, code) 579 580 @staticmethod 581 def subclass(fields): 582 def inner(cls): 583 name = cls.__name__ 584 585 # add a _ character before every uppercase letter, except the SDP_ prefix 586 location = len(name) - 1 587 while location > 4: 588 if not name[location].isupper(): 589 location -= 1 590 continue 591 name = name[:location] + '_' + name[location:] 592 location -= 1 593 594 cls.name = name.upper() 595 cls.pdu_id = key_with_value(SDP_PDU_NAMES, cls.name) 596 if cls.pdu_id is None: 597 raise KeyError(f'PDU name {cls.name} not found in SDP_PDU_NAMES') 598 cls.fields = fields 599 600 # Register a factory for this class 601 SDP_PDU.sdp_pdu_classes[cls.pdu_id] = cls 602 603 return cls 604 605 return inner 606 607 def __init__(self, pdu=None, transaction_id=0, **kwargs): 608 if hasattr(self, 'fields') and kwargs: 609 HCI_Object.init_from_fields(self, self.fields, kwargs) 610 if pdu is None: 611 parameters = HCI_Object.dict_to_bytes(kwargs, self.fields) 612 pdu = ( 613 struct.pack('>BHH', self.pdu_id, transaction_id, len(parameters)) 614 + parameters 615 ) 616 self.pdu = pdu 617 self.transaction_id = transaction_id 618 619 def init_from_bytes(self, pdu, offset): 620 return HCI_Object.init_from_bytes(self, pdu, offset, self.fields) 621 622 def to_bytes(self): 623 return self.pdu 624 625 def __bytes__(self): 626 return self.to_bytes() 627 628 def __str__(self): 629 result = f'{color(self.name, "blue")} [TID={self.transaction_id}]' 630 if fields := getattr(self, 'fields', None): 631 result += ':\n' + HCI_Object.format_fields(self.__dict__, fields, ' ') 632 elif len(self.pdu) > 1: 633 result += f': {self.pdu.hex()}' 634 return result 635 636 637# ----------------------------------------------------------------------------- 638@SDP_PDU.subclass([('error_code', {'size': 2, 'mapper': SDP_PDU.error_name})]) 639class SDP_ErrorResponse(SDP_PDU): 640 ''' 641 See Bluetooth spec @ Vol 3, Part B - 4.4.1 SDP_ErrorResponse PDU 642 ''' 643 644 645# ----------------------------------------------------------------------------- 646@SDP_PDU.subclass( 647 [ 648 ('service_search_pattern', DataElement.parse_from_bytes), 649 ('maximum_service_record_count', '>2'), 650 ('continuation_state', '*'), 651 ] 652) 653class SDP_ServiceSearchRequest(SDP_PDU): 654 ''' 655 See Bluetooth spec @ Vol 3, Part B - 4.5.1 SDP_ServiceSearchRequest PDU 656 ''' 657 658 service_search_pattern: DataElement 659 maximum_service_record_count: int 660 continuation_state: bytes 661 662 663# ----------------------------------------------------------------------------- 664@SDP_PDU.subclass( 665 [ 666 ('total_service_record_count', '>2'), 667 ('current_service_record_count', '>2'), 668 ( 669 'service_record_handle_list', 670 SDP_PDU.parse_service_record_handle_list_preceded_by_count, 671 ), 672 ('continuation_state', '*'), 673 ] 674) 675class SDP_ServiceSearchResponse(SDP_PDU): 676 ''' 677 See Bluetooth spec @ Vol 3, Part B - 4.5.2 SDP_ServiceSearchResponse PDU 678 ''' 679 680 service_record_handle_list: List[int] 681 total_service_record_count: int 682 current_service_record_count: int 683 continuation_state: bytes 684 685 686# ----------------------------------------------------------------------------- 687@SDP_PDU.subclass( 688 [ 689 ('service_record_handle', '>4'), 690 ('maximum_attribute_byte_count', '>2'), 691 ('attribute_id_list', DataElement.parse_from_bytes), 692 ('continuation_state', '*'), 693 ] 694) 695class SDP_ServiceAttributeRequest(SDP_PDU): 696 ''' 697 See Bluetooth spec @ Vol 3, Part B - 4.6.1 SDP_ServiceAttributeRequest PDU 698 ''' 699 700 service_record_handle: int 701 maximum_attribute_byte_count: int 702 attribute_id_list: DataElement 703 continuation_state: bytes 704 705 706# ----------------------------------------------------------------------------- 707@SDP_PDU.subclass( 708 [ 709 ('attribute_list_byte_count', '>2'), 710 ('attribute_list', SDP_PDU.parse_bytes_preceded_by_length), 711 ('continuation_state', '*'), 712 ] 713) 714class SDP_ServiceAttributeResponse(SDP_PDU): 715 ''' 716 See Bluetooth spec @ Vol 3, Part B - 4.6.2 SDP_ServiceAttributeResponse PDU 717 ''' 718 719 attribute_list_byte_count: int 720 attribute_list: bytes 721 continuation_state: bytes 722 723 724# ----------------------------------------------------------------------------- 725@SDP_PDU.subclass( 726 [ 727 ('service_search_pattern', DataElement.parse_from_bytes), 728 ('maximum_attribute_byte_count', '>2'), 729 ('attribute_id_list', DataElement.parse_from_bytes), 730 ('continuation_state', '*'), 731 ] 732) 733class SDP_ServiceSearchAttributeRequest(SDP_PDU): 734 ''' 735 See Bluetooth spec @ Vol 3, Part B - 4.7.1 SDP_ServiceSearchAttributeRequest PDU 736 ''' 737 738 service_search_pattern: DataElement 739 maximum_attribute_byte_count: int 740 attribute_id_list: DataElement 741 continuation_state: bytes 742 743 744# ----------------------------------------------------------------------------- 745@SDP_PDU.subclass( 746 [ 747 ('attribute_lists_byte_count', '>2'), 748 ('attribute_lists', SDP_PDU.parse_bytes_preceded_by_length), 749 ('continuation_state', '*'), 750 ] 751) 752class SDP_ServiceSearchAttributeResponse(SDP_PDU): 753 ''' 754 See Bluetooth spec @ Vol 3, Part B - 4.7.2 SDP_ServiceSearchAttributeResponse PDU 755 ''' 756 757 attribute_list_byte_count: int 758 attribute_list: bytes 759 continuation_state: bytes 760 761 762# ----------------------------------------------------------------------------- 763class Client: 764 channel: Optional[l2cap.ClassicChannel] 765 766 def __init__(self, connection: Connection) -> None: 767 self.connection = connection 768 self.pending_request = None 769 self.channel = None 770 771 async def connect(self) -> None: 772 self.channel = await self.connection.create_l2cap_channel( 773 spec=l2cap.ClassicChannelSpec(SDP_PSM) 774 ) 775 776 async def disconnect(self) -> None: 777 if self.channel: 778 await self.channel.disconnect() 779 self.channel = None 780 781 async def search_services(self, uuids: List[core.UUID]) -> List[int]: 782 if self.pending_request is not None: 783 raise InvalidStateError('request already pending') 784 if self.channel is None: 785 raise InvalidStateError('L2CAP not connected') 786 787 service_search_pattern = DataElement.sequence( 788 [DataElement.uuid(uuid) for uuid in uuids] 789 ) 790 791 # Request and accumulate until there's no more continuation 792 service_record_handle_list = [] 793 continuation_state = bytes([0]) 794 watchdog = SDP_CONTINUATION_WATCHDOG 795 while watchdog > 0: 796 response_pdu = await self.channel.send_request( 797 SDP_ServiceSearchRequest( 798 transaction_id=0, # Transaction ID TODO: pick a real value 799 service_search_pattern=service_search_pattern, 800 maximum_service_record_count=0xFFFF, 801 continuation_state=continuation_state, 802 ) 803 ) 804 response = SDP_PDU.from_bytes(response_pdu) 805 logger.debug(f'<<< Response: {response}') 806 service_record_handle_list += response.service_record_handle_list 807 continuation_state = response.continuation_state 808 if len(continuation_state) == 1 and continuation_state[0] == 0: 809 break 810 logger.debug(f'continuation: {continuation_state.hex()}') 811 watchdog -= 1 812 813 return service_record_handle_list 814 815 async def search_attributes( 816 self, uuids: List[core.UUID], attribute_ids: List[Union[int, Tuple[int, int]]] 817 ) -> List[List[ServiceAttribute]]: 818 if self.pending_request is not None: 819 raise InvalidStateError('request already pending') 820 if self.channel is None: 821 raise InvalidStateError('L2CAP not connected') 822 823 service_search_pattern = DataElement.sequence( 824 [DataElement.uuid(uuid) for uuid in uuids] 825 ) 826 attribute_id_list = DataElement.sequence( 827 [ 828 ( 829 DataElement.unsigned_integer( 830 attribute_id[0], value_size=attribute_id[1] 831 ) 832 if isinstance(attribute_id, tuple) 833 else DataElement.unsigned_integer_16(attribute_id) 834 ) 835 for attribute_id in attribute_ids 836 ] 837 ) 838 839 # Request and accumulate until there's no more continuation 840 accumulator = b'' 841 continuation_state = bytes([0]) 842 watchdog = SDP_CONTINUATION_WATCHDOG 843 while watchdog > 0: 844 response_pdu = await self.channel.send_request( 845 SDP_ServiceSearchAttributeRequest( 846 transaction_id=0, # Transaction ID TODO: pick a real value 847 service_search_pattern=service_search_pattern, 848 maximum_attribute_byte_count=0xFFFF, 849 attribute_id_list=attribute_id_list, 850 continuation_state=continuation_state, 851 ) 852 ) 853 response = SDP_PDU.from_bytes(response_pdu) 854 logger.debug(f'<<< Response: {response}') 855 accumulator += response.attribute_lists 856 continuation_state = response.continuation_state 857 if len(continuation_state) == 1 and continuation_state[0] == 0: 858 break 859 logger.debug(f'continuation: {continuation_state.hex()}') 860 watchdog -= 1 861 862 # Parse the result into attribute lists 863 attribute_lists_sequences = DataElement.from_bytes(accumulator) 864 if attribute_lists_sequences.type != DataElement.SEQUENCE: 865 logger.warning('unexpected data type') 866 return [] 867 868 return [ 869 ServiceAttribute.list_from_data_elements(sequence.value) 870 for sequence in attribute_lists_sequences.value 871 if sequence.type == DataElement.SEQUENCE 872 ] 873 874 async def get_attributes( 875 self, 876 service_record_handle: int, 877 attribute_ids: List[Union[int, Tuple[int, int]]], 878 ) -> List[ServiceAttribute]: 879 if self.pending_request is not None: 880 raise InvalidStateError('request already pending') 881 if self.channel is None: 882 raise InvalidStateError('L2CAP not connected') 883 884 attribute_id_list = DataElement.sequence( 885 [ 886 ( 887 DataElement.unsigned_integer( 888 attribute_id[0], value_size=attribute_id[1] 889 ) 890 if isinstance(attribute_id, tuple) 891 else DataElement.unsigned_integer_16(attribute_id) 892 ) 893 for attribute_id in attribute_ids 894 ] 895 ) 896 897 # Request and accumulate until there's no more continuation 898 accumulator = b'' 899 continuation_state = bytes([0]) 900 watchdog = SDP_CONTINUATION_WATCHDOG 901 while watchdog > 0: 902 response_pdu = await self.channel.send_request( 903 SDP_ServiceAttributeRequest( 904 transaction_id=0, # Transaction ID TODO: pick a real value 905 service_record_handle=service_record_handle, 906 maximum_attribute_byte_count=0xFFFF, 907 attribute_id_list=attribute_id_list, 908 continuation_state=continuation_state, 909 ) 910 ) 911 response = SDP_PDU.from_bytes(response_pdu) 912 logger.debug(f'<<< Response: {response}') 913 accumulator += response.attribute_list 914 continuation_state = response.continuation_state 915 if len(continuation_state) == 1 and continuation_state[0] == 0: 916 break 917 logger.debug(f'continuation: {continuation_state.hex()}') 918 watchdog -= 1 919 920 # Parse the result into a list of attributes 921 attribute_list_sequence = DataElement.from_bytes(accumulator) 922 if attribute_list_sequence.type != DataElement.SEQUENCE: 923 logger.warning('unexpected data type') 924 return [] 925 926 return ServiceAttribute.list_from_data_elements(attribute_list_sequence.value) 927 928 async def __aenter__(self) -> Self: 929 await self.connect() 930 return self 931 932 async def __aexit__(self, *args) -> None: 933 await self.disconnect() 934 935 936# ----------------------------------------------------------------------------- 937class Server: 938 CONTINUATION_STATE = bytes([0x01, 0x43]) 939 channel: Optional[l2cap.ClassicChannel] 940 Service = NewType('Service', List[ServiceAttribute]) 941 service_records: Dict[int, Service] 942 current_response: Union[None, bytes, Tuple[int, List[int]]] 943 944 def __init__(self, device: Device) -> None: 945 self.device = device 946 self.service_records = {} # Service records maps, by record handle 947 self.channel = None 948 self.current_response = None 949 950 def register(self, l2cap_channel_manager: l2cap.ChannelManager) -> None: 951 l2cap_channel_manager.create_classic_server( 952 spec=l2cap.ClassicChannelSpec(psm=SDP_PSM), handler=self.on_connection 953 ) 954 955 def send_response(self, response): 956 logger.debug(f'{color(">>> Sending SDP Response", "blue")}: {response}') 957 self.channel.send_pdu(response) 958 959 def match_services(self, search_pattern: DataElement) -> Dict[int, Service]: 960 # Find the services for which the attributes in the pattern is a subset of the 961 # service's attribute values (NOTE: the value search recurses into sequences) 962 matching_services = {} 963 for handle, service in self.service_records.items(): 964 for uuid in search_pattern.value: 965 found = False 966 for attribute in service: 967 if ServiceAttribute.is_uuid_in_value(uuid.value, attribute.value): 968 found = True 969 break 970 if found: 971 matching_services[handle] = service 972 break 973 974 return matching_services 975 976 def on_connection(self, channel): 977 self.channel = channel 978 self.channel.sink = self.on_pdu 979 980 def on_pdu(self, pdu): 981 try: 982 sdp_pdu = SDP_PDU.from_bytes(pdu) 983 except Exception as error: 984 logger.warning(color(f'failed to parse SDP Request PDU: {error}', 'red')) 985 self.send_response( 986 SDP_ErrorResponse( 987 transaction_id=0, error_code=SDP_INVALID_REQUEST_SYNTAX_ERROR 988 ) 989 ) 990 991 logger.debug(f'{color("<<< Received SDP Request", "green")}: {sdp_pdu}') 992 993 # Find the handler method 994 handler_name = f'on_{sdp_pdu.name.lower()}' 995 handler = getattr(self, handler_name, None) 996 if handler: 997 try: 998 handler(sdp_pdu) 999 except Exception as error: 1000 logger.exception(f'{color("!!! Exception in handler:", "red")} {error}') 1001 self.send_response( 1002 SDP_ErrorResponse( 1003 transaction_id=sdp_pdu.transaction_id, 1004 error_code=SDP_INSUFFICIENT_RESOURCES_TO_SATISFY_REQUEST_ERROR, 1005 ) 1006 ) 1007 else: 1008 logger.error(color('SDP Request not handled???', 'red')) 1009 self.send_response( 1010 SDP_ErrorResponse( 1011 transaction_id=sdp_pdu.transaction_id, 1012 error_code=SDP_INVALID_REQUEST_SYNTAX_ERROR, 1013 ) 1014 ) 1015 1016 def get_next_response_payload(self, maximum_size): 1017 if len(self.current_response) > maximum_size: 1018 payload = self.current_response[:maximum_size] 1019 continuation_state = Server.CONTINUATION_STATE 1020 self.current_response = self.current_response[maximum_size:] 1021 else: 1022 payload = self.current_response 1023 continuation_state = bytes([0]) 1024 self.current_response = None 1025 1026 return (payload, continuation_state) 1027 1028 @staticmethod 1029 def get_service_attributes( 1030 service: Service, attribute_ids: List[DataElement] 1031 ) -> DataElement: 1032 attributes = [] 1033 for attribute_id in attribute_ids: 1034 if attribute_id.value_size == 4: 1035 # Attribute ID range 1036 id_range_start = attribute_id.value >> 16 1037 id_range_end = attribute_id.value & 0xFFFF 1038 else: 1039 id_range_start = attribute_id.value 1040 id_range_end = attribute_id.value 1041 attributes += [ 1042 attribute 1043 for attribute in service 1044 if attribute.id >= id_range_start and attribute.id <= id_range_end 1045 ] 1046 1047 # Return the matching attributes, sorted by attribute id 1048 attributes.sort(key=lambda x: x.id) 1049 attribute_list = DataElement.sequence([]) 1050 for attribute in attributes: 1051 attribute_list.value.append(DataElement.unsigned_integer_16(attribute.id)) 1052 attribute_list.value.append(attribute.value) 1053 1054 return attribute_list 1055 1056 def on_sdp_service_search_request(self, request: SDP_ServiceSearchRequest) -> None: 1057 # Check if this is a continuation 1058 if len(request.continuation_state) > 1: 1059 if self.current_response is None: 1060 self.send_response( 1061 SDP_ErrorResponse( 1062 transaction_id=request.transaction_id, 1063 error_code=SDP_INVALID_CONTINUATION_STATE_ERROR, 1064 ) 1065 ) 1066 return 1067 else: 1068 # Cleanup any partial response leftover 1069 self.current_response = None 1070 1071 # Find the matching services 1072 matching_services = self.match_services(request.service_search_pattern) 1073 service_record_handles = list(matching_services.keys()) 1074 1075 # Only return up to the maximum requested 1076 service_record_handles_subset = service_record_handles[ 1077 : request.maximum_service_record_count 1078 ] 1079 1080 # Serialize to a byte array, and remember the total count 1081 logger.debug(f'Service Record Handles: {service_record_handles}') 1082 self.current_response = ( 1083 len(service_record_handles), 1084 service_record_handles_subset, 1085 ) 1086 1087 # Respond, keeping any unsent handles for later 1088 assert isinstance(self.current_response, tuple) 1089 service_record_handles = self.current_response[1][ 1090 : request.maximum_service_record_count 1091 ] 1092 self.current_response = ( 1093 self.current_response[0], 1094 self.current_response[1][request.maximum_service_record_count :], 1095 ) 1096 continuation_state = ( 1097 Server.CONTINUATION_STATE if self.current_response[1] else bytes([0]) 1098 ) 1099 service_record_handle_list = b''.join( 1100 [struct.pack('>I', handle) for handle in service_record_handles] 1101 ) 1102 self.send_response( 1103 SDP_ServiceSearchResponse( 1104 transaction_id=request.transaction_id, 1105 total_service_record_count=self.current_response[0], 1106 current_service_record_count=len(service_record_handles), 1107 service_record_handle_list=service_record_handle_list, 1108 continuation_state=continuation_state, 1109 ) 1110 ) 1111 1112 def on_sdp_service_attribute_request( 1113 self, request: SDP_ServiceAttributeRequest 1114 ) -> None: 1115 # Check if this is a continuation 1116 if len(request.continuation_state) > 1: 1117 if self.current_response is None: 1118 self.send_response( 1119 SDP_ErrorResponse( 1120 transaction_id=request.transaction_id, 1121 error_code=SDP_INVALID_CONTINUATION_STATE_ERROR, 1122 ) 1123 ) 1124 return 1125 else: 1126 # Cleanup any partial response leftover 1127 self.current_response = None 1128 1129 # Check that the service exists 1130 service = self.service_records.get(request.service_record_handle) 1131 if service is None: 1132 self.send_response( 1133 SDP_ErrorResponse( 1134 transaction_id=request.transaction_id, 1135 error_code=SDP_INVALID_SERVICE_RECORD_HANDLE_ERROR, 1136 ) 1137 ) 1138 return 1139 1140 # Get the attributes for the service 1141 attribute_list = Server.get_service_attributes( 1142 service, request.attribute_id_list.value 1143 ) 1144 1145 # Serialize to a byte array 1146 logger.debug(f'Attributes: {attribute_list}') 1147 self.current_response = bytes(attribute_list) 1148 1149 # Respond, keeping any pending chunks for later 1150 attribute_list_response, continuation_state = self.get_next_response_payload( 1151 request.maximum_attribute_byte_count 1152 ) 1153 self.send_response( 1154 SDP_ServiceAttributeResponse( 1155 transaction_id=request.transaction_id, 1156 attribute_list_byte_count=len(attribute_list_response), 1157 attribute_list=attribute_list, 1158 continuation_state=continuation_state, 1159 ) 1160 ) 1161 1162 def on_sdp_service_search_attribute_request( 1163 self, request: SDP_ServiceSearchAttributeRequest 1164 ) -> None: 1165 # Check if this is a continuation 1166 if len(request.continuation_state) > 1: 1167 if self.current_response is None: 1168 self.send_response( 1169 SDP_ErrorResponse( 1170 transaction_id=request.transaction_id, 1171 error_code=SDP_INVALID_CONTINUATION_STATE_ERROR, 1172 ) 1173 ) 1174 else: 1175 # Cleanup any partial response leftover 1176 self.current_response = None 1177 1178 # Find the matching services 1179 matching_services = self.match_services( 1180 request.service_search_pattern 1181 ).values() 1182 1183 # Filter the required attributes 1184 attribute_lists = DataElement.sequence([]) 1185 for service in matching_services: 1186 attribute_list = Server.get_service_attributes( 1187 service, request.attribute_id_list.value 1188 ) 1189 if attribute_list.value: 1190 attribute_lists.value.append(attribute_list) 1191 1192 # Serialize to a byte array 1193 logger.debug(f'Search response: {attribute_lists}') 1194 self.current_response = bytes(attribute_lists) 1195 1196 # Respond, keeping any pending chunks for later 1197 attribute_lists_response, continuation_state = self.get_next_response_payload( 1198 request.maximum_attribute_byte_count 1199 ) 1200 self.send_response( 1201 SDP_ServiceSearchAttributeResponse( 1202 transaction_id=request.transaction_id, 1203 attribute_lists_byte_count=len(attribute_lists_response), 1204 attribute_lists=attribute_lists, 1205 continuation_state=continuation_state, 1206 ) 1207 ) 1208