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