• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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