blob: 019b8e6f9677de05e375987e5ec1f210dab7a930 [file] [log] [blame]
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -07001# 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# -----------------------------------------------------------------------------
Gilles Boccon-Gibod99758e42023-01-20 00:20:50 -080018from __future__ import annotations
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -070019import logging
20import struct
uaelb731f6f2023-02-02 17:36:23 +000021from typing import Dict, List, Type
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -070022
23from . import core
uaeld21da782023-02-23 20:16:33 +000024from .colors import color
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -070025from .core import InvalidStateError
26from .hci import HCI_Object, name_or_number, key_with_value
27
28# -----------------------------------------------------------------------------
29# Logging
30# -----------------------------------------------------------------------------
31logger = logging.getLogger(__name__)
32
33
34# -----------------------------------------------------------------------------
35# Constants
36# -----------------------------------------------------------------------------
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -080037# fmt: off
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -080038# pylint: disable=line-too-long
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -080039
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -070040SDP_CONTINUATION_WATCHDOG = 64 # Maximum number of continuations we're willing to do
41
42SDP_PSM = 0x0001
43
44SDP_ERROR_RESPONSE = 0x01
45SDP_SERVICE_SEARCH_REQUEST = 0x02
46SDP_SERVICE_SEARCH_RESPONSE = 0x03
47SDP_SERVICE_ATTRIBUTE_REQUEST = 0x04
48SDP_SERVICE_ATTRIBUTE_RESPONSE = 0x05
49SDP_SERVICE_SEARCH_ATTRIBUTE_REQUEST = 0x06
50SDP_SERVICE_SEARCH_ATTRIBUTE_RESPONSE = 0x07
51
52SDP_PDU_NAMES = {
53 SDP_ERROR_RESPONSE: 'SDP_ERROR_RESPONSE',
54 SDP_SERVICE_SEARCH_REQUEST: 'SDP_SERVICE_SEARCH_REQUEST',
55 SDP_SERVICE_SEARCH_RESPONSE: 'SDP_SERVICE_SEARCH_RESPONSE',
56 SDP_SERVICE_ATTRIBUTE_REQUEST: 'SDP_SERVICE_ATTRIBUTE_REQUEST',
57 SDP_SERVICE_ATTRIBUTE_RESPONSE: 'SDP_SERVICE_ATTRIBUTE_RESPONSE',
58 SDP_SERVICE_SEARCH_ATTRIBUTE_REQUEST: 'SDP_SERVICE_SEARCH_ATTRIBUTE_REQUEST',
59 SDP_SERVICE_SEARCH_ATTRIBUTE_RESPONSE: 'SDP_SERVICE_SEARCH_ATTRIBUTE_RESPONSE'
60}
61
62SDP_INVALID_SDP_VERSION_ERROR = 0x0001
63SDP_INVALID_SERVICE_RECORD_HANDLE_ERROR = 0x0002
64SDP_INVALID_REQUEST_SYNTAX_ERROR = 0x0003
65SDP_INVALID_PDU_SIZE_ERROR = 0x0004
66SDP_INVALID_CONTINUATION_STATE_ERROR = 0x0005
67SDP_INSUFFICIENT_RESOURCES_TO_SATISFY_REQUEST_ERROR = 0x0006
68
69SDP_ERROR_NAMES = {
70 SDP_INVALID_SDP_VERSION_ERROR: 'SDP_INVALID_SDP_VERSION_ERROR',
71 SDP_INVALID_SERVICE_RECORD_HANDLE_ERROR: 'SDP_INVALID_SERVICE_RECORD_HANDLE_ERROR',
72 SDP_INVALID_REQUEST_SYNTAX_ERROR: 'SDP_INVALID_REQUEST_SYNTAX_ERROR',
73 SDP_INVALID_PDU_SIZE_ERROR: 'SDP_INVALID_PDU_SIZE_ERROR',
74 SDP_INVALID_CONTINUATION_STATE_ERROR: 'SDP_INVALID_CONTINUATION_STATE_ERROR',
75 SDP_INSUFFICIENT_RESOURCES_TO_SATISFY_REQUEST_ERROR: 'SDP_INSUFFICIENT_RESOURCES_TO_SATISFY_REQUEST_ERROR'
76}
77
78SDP_SERVICE_NAME_ATTRIBUTE_ID_OFFSET = 0x0000
79SDP_SERVICE_DESCRIPTION_ATTRIBUTE_ID_OFFSET = 0x0001
80SDP_PROVIDER_NAME_ATTRIBUTE_ID_OFFSET = 0x0002
81
82SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID = 0X0000
83SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID = 0X0001
84SDP_SERVICE_RECORD_STATE_ATTRIBUTE_ID = 0X0002
85SDP_SERVICE_ID_ATTRIBUTE_ID = 0X0003
86SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID = 0X0004
87SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID = 0X0005
88SDP_LANGUAGE_BASE_ATTRIBUTE_ID_LIST_ATTRIBUTE_ID = 0X0006
89SDP_SERVICE_INFO_TIME_TO_LIVE_ATTRIBUTE_ID = 0X0007
90SDP_SERVICE_AVAILABILITY_ATTRIBUTE_ID = 0X0008
91SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID = 0X0009
92SDP_DOCUMENTATION_URL_ATTRIBUTE_ID = 0X000A
93SDP_CLIENT_EXECUTABLE_URL_ATTRIBUTE_ID = 0X000B
94SDP_ICON_URL_ATTRIBUTE_ID = 0X000C
95SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID = 0X000D
96
97SDP_ATTRIBUTE_ID_NAMES = {
98 SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID: 'SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID',
99 SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID: 'SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID',
100 SDP_SERVICE_RECORD_STATE_ATTRIBUTE_ID: 'SDP_SERVICE_RECORD_STATE_ATTRIBUTE_ID',
101 SDP_SERVICE_ID_ATTRIBUTE_ID: 'SDP_SERVICE_ID_ATTRIBUTE_ID',
102 SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID: 'SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID',
103 SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID: 'SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID',
104 SDP_LANGUAGE_BASE_ATTRIBUTE_ID_LIST_ATTRIBUTE_ID: 'SDP_LANGUAGE_BASE_ATTRIBUTE_ID_LIST_ATTRIBUTE_ID',
105 SDP_SERVICE_INFO_TIME_TO_LIVE_ATTRIBUTE_ID: 'SDP_SERVICE_INFO_TIME_TO_LIVE_ATTRIBUTE_ID',
106 SDP_SERVICE_AVAILABILITY_ATTRIBUTE_ID: 'SDP_SERVICE_AVAILABILITY_ATTRIBUTE_ID',
107 SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID: 'SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID',
108 SDP_DOCUMENTATION_URL_ATTRIBUTE_ID: 'SDP_DOCUMENTATION_URL_ATTRIBUTE_ID',
109 SDP_CLIENT_EXECUTABLE_URL_ATTRIBUTE_ID: 'SDP_CLIENT_EXECUTABLE_URL_ATTRIBUTE_ID',
110 SDP_ICON_URL_ATTRIBUTE_ID: 'SDP_ICON_URL_ATTRIBUTE_ID',
111 SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID: 'SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID'
112}
113
114SDP_PUBLIC_BROWSE_ROOT = core.UUID.from_16_bits(0x1002, 'PublicBrowseRoot')
115
116# To be used in searches where an attribute ID list allows a range to be specified
117SDP_ALL_ATTRIBUTES_RANGE = (0x0000FFFF, 4) # Express this as tuple so we can convey the desired encoding size
118
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800119# fmt: on
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800120# pylint: enable=line-too-long
121# pylint: disable=invalid-name
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800122
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700123
124# -----------------------------------------------------------------------------
125class DataElement:
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800126 NIL = 0
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700127 UNSIGNED_INTEGER = 1
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800128 SIGNED_INTEGER = 2
129 UUID = 3
130 TEXT_STRING = 4
131 BOOLEAN = 5
132 SEQUENCE = 6
133 ALTERNATIVE = 7
134 URL = 8
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700135
136 TYPE_NAMES = {
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800137 NIL: 'NIL',
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700138 UNSIGNED_INTEGER: 'UNSIGNED_INTEGER',
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800139 SIGNED_INTEGER: 'SIGNED_INTEGER',
140 UUID: 'UUID',
141 TEXT_STRING: 'TEXT_STRING',
142 BOOLEAN: 'BOOLEAN',
143 SEQUENCE: 'SEQUENCE',
144 ALTERNATIVE: 'ALTERNATIVE',
145 URL: 'URL',
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700146 }
147
148 type_constructors = {
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800149 NIL: lambda x: DataElement(DataElement.NIL, None),
150 UNSIGNED_INTEGER: lambda x, y: DataElement(
151 DataElement.UNSIGNED_INTEGER,
152 DataElement.unsigned_integer_from_bytes(x),
153 value_size=y,
154 ),
155 SIGNED_INTEGER: lambda x, y: DataElement(
156 DataElement.SIGNED_INTEGER,
157 DataElement.signed_integer_from_bytes(x),
158 value_size=y,
159 ),
160 UUID: lambda x: DataElement(
161 DataElement.UUID, core.UUID.from_bytes(bytes(reversed(x)))
162 ),
163 TEXT_STRING: lambda x: DataElement(DataElement.TEXT_STRING, x.decode('utf8')),
164 BOOLEAN: lambda x: DataElement(DataElement.BOOLEAN, x[0] == 1),
165 SEQUENCE: lambda x: DataElement(
166 DataElement.SEQUENCE, DataElement.list_from_bytes(x)
167 ),
168 ALTERNATIVE: lambda x: DataElement(
169 DataElement.ALTERNATIVE, DataElement.list_from_bytes(x)
170 ),
171 URL: lambda x: DataElement(DataElement.URL, x.decode('utf8')),
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700172 }
173
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800174 def __init__(self, element_type, value, value_size=None):
175 self.type = element_type
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800176 self.value = value
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700177 self.value_size = value_size
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800178 # Used as a cache when parsing from bytes so we can emit a byte-for-byte replica
179 self.bytes = None
180 if element_type in (DataElement.UNSIGNED_INTEGER, DataElement.SIGNED_INTEGER):
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700181 if value_size is None:
182 raise ValueError('integer types must have a value size specified')
183
184 @staticmethod
uaelb731f6f2023-02-02 17:36:23 +0000185 def nil() -> DataElement:
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700186 return DataElement(DataElement.NIL, None)
187
188 @staticmethod
uaelb731f6f2023-02-02 17:36:23 +0000189 def unsigned_integer(value: int, value_size: int) -> DataElement:
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700190 return DataElement(DataElement.UNSIGNED_INTEGER, value, value_size)
191
192 @staticmethod
uaelb731f6f2023-02-02 17:36:23 +0000193 def unsigned_integer_8(value: int) -> DataElement:
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700194 return DataElement(DataElement.UNSIGNED_INTEGER, value, value_size=1)
195
196 @staticmethod
uaelb731f6f2023-02-02 17:36:23 +0000197 def unsigned_integer_16(value: int) -> DataElement:
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700198 return DataElement(DataElement.UNSIGNED_INTEGER, value, value_size=2)
199
200 @staticmethod
uaelb731f6f2023-02-02 17:36:23 +0000201 def unsigned_integer_32(value: int) -> DataElement:
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700202 return DataElement(DataElement.UNSIGNED_INTEGER, value, value_size=4)
203
204 @staticmethod
uaelb731f6f2023-02-02 17:36:23 +0000205 def signed_integer(value: int, value_size: int) -> DataElement:
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700206 return DataElement(DataElement.SIGNED_INTEGER, value, value_size)
207
208 @staticmethod
uaelb731f6f2023-02-02 17:36:23 +0000209 def signed_integer_8(value: int) -> DataElement:
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700210 return DataElement(DataElement.SIGNED_INTEGER, value, value_size=1)
211
212 @staticmethod
uaelb731f6f2023-02-02 17:36:23 +0000213 def signed_integer_16(value: int) -> DataElement:
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700214 return DataElement(DataElement.SIGNED_INTEGER, value, value_size=2)
215
216 @staticmethod
uaelb731f6f2023-02-02 17:36:23 +0000217 def signed_integer_32(value: int) -> DataElement:
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700218 return DataElement(DataElement.SIGNED_INTEGER, value, value_size=4)
219
220 @staticmethod
uaelb731f6f2023-02-02 17:36:23 +0000221 def uuid(value: core.UUID) -> DataElement:
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700222 return DataElement(DataElement.UUID, value)
223
224 @staticmethod
uaelb731f6f2023-02-02 17:36:23 +0000225 def text_string(value: str) -> DataElement:
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700226 return DataElement(DataElement.TEXT_STRING, value)
227
228 @staticmethod
uaelb731f6f2023-02-02 17:36:23 +0000229 def boolean(value: bool) -> DataElement:
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700230 return DataElement(DataElement.BOOLEAN, value)
231
232 @staticmethod
uaelb731f6f2023-02-02 17:36:23 +0000233 def sequence(value: List[DataElement]) -> DataElement:
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700234 return DataElement(DataElement.SEQUENCE, value)
235
236 @staticmethod
uaelb731f6f2023-02-02 17:36:23 +0000237 def alternative(value: List[DataElement]) -> DataElement:
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700238 return DataElement(DataElement.ALTERNATIVE, value)
239
240 @staticmethod
uaelb731f6f2023-02-02 17:36:23 +0000241 def url(value: str) -> DataElement:
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700242 return DataElement(DataElement.URL, value)
243
244 @staticmethod
245 def unsigned_integer_from_bytes(data):
246 if len(data) == 1:
247 return data[0]
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800248
249 if len(data) == 2:
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700250 return struct.unpack('>H', data)[0]
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800251
252 if len(data) == 4:
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700253 return struct.unpack('>I', data)[0]
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800254
255 if len(data) == 8:
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700256 return struct.unpack('>Q', data)[0]
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800257
258 raise ValueError(f'invalid integer length {len(data)}')
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700259
260 @staticmethod
261 def signed_integer_from_bytes(data):
262 if len(data) == 1:
263 return struct.unpack('b', data)[0]
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800264
265 if len(data) == 2:
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700266 return struct.unpack('>h', data)[0]
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800267
268 if len(data) == 4:
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700269 return struct.unpack('>i', data)[0]
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800270
271 if len(data) == 8:
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700272 return struct.unpack('>q', data)[0]
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800273
274 raise ValueError(f'invalid integer length {len(data)}')
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700275
276 @staticmethod
277 def list_from_bytes(data):
278 elements = []
279 while data:
280 element = DataElement.from_bytes(data)
281 elements.append(element)
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800282 data = data[len(bytes(element)) :]
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700283 return elements
284
285 @staticmethod
286 def parse_from_bytes(data, offset):
287 element = DataElement.from_bytes(data[offset:])
288 return offset + len(bytes(element)), element
289
290 @staticmethod
291 def from_bytes(data):
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800292 element_type = data[0] >> 3
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800293 size_index = data[0] & 7
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700294 value_offset = 0
295 if size_index == 0:
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800296 if element_type == DataElement.NIL:
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700297 value_size = 0
298 else:
299 value_size = 1
300 elif size_index == 1:
301 value_size = 2
302 elif size_index == 2:
303 value_size = 4
304 elif size_index == 3:
305 value_size = 8
306 elif size_index == 4:
307 value_size = 16
308 elif size_index == 5:
309 value_size = data[1]
310 value_offset = 1
311 elif size_index == 6:
312 value_size = struct.unpack('>H', data[1:3])[0]
313 value_offset = 2
314 else: # size_index == 7
315 value_size = struct.unpack('>I', data[1:5])[0]
316 value_offset = 4
317
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800318 value_data = data[1 + value_offset : 1 + value_offset + value_size]
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800319 constructor = DataElement.type_constructors.get(element_type)
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700320 if constructor:
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800321 if element_type in (
322 DataElement.UNSIGNED_INTEGER,
323 DataElement.SIGNED_INTEGER,
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800324 ):
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700325 result = constructor(value_data, value_size)
326 else:
327 result = constructor(value_data)
328 else:
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800329 result = DataElement(element_type, value_data)
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800330 result.bytes = data[
331 : 1 + value_offset + value_size
332 ] # Keep a copy so we can re-serialize to an exact replica
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700333 return result
334
335 def to_bytes(self):
336 return bytes(self)
337
338 def __bytes__(self):
339 # Return early if we have a cache
340 if self.bytes:
341 return self.bytes
342
343 if self.type == DataElement.NIL:
344 data = b''
345 elif self.type == DataElement.UNSIGNED_INTEGER:
346 if self.value < 0:
347 raise ValueError('UNSIGNED_INTEGER cannot be negative')
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800348
349 if self.value_size == 1:
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700350 data = struct.pack('B', self.value)
351 elif self.value_size == 2:
352 data = struct.pack('>H', self.value)
353 elif self.value_size == 4:
354 data = struct.pack('>I', self.value)
355 elif self.value_size == 8:
356 data = struct.pack('>Q', self.value)
357 else:
358 raise ValueError('invalid value_size')
359 elif self.type == DataElement.SIGNED_INTEGER:
360 if self.value_size == 1:
361 data = struct.pack('b', self.value)
362 elif self.value_size == 2:
363 data = struct.pack('>h', self.value)
364 elif self.value_size == 4:
365 data = struct.pack('>i', self.value)
366 elif self.value_size == 8:
367 data = struct.pack('>q', self.value)
368 else:
369 raise ValueError('invalid value_size')
370 elif self.type == DataElement.UUID:
371 data = bytes(reversed(bytes(self.value)))
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800372 elif self.type in (DataElement.TEXT_STRING, DataElement.URL):
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700373 data = self.value.encode('utf8')
374 elif self.type == DataElement.BOOLEAN:
375 data = bytes([1 if self.value else 0])
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800376 elif self.type in (DataElement.SEQUENCE, DataElement.ALTERNATIVE):
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700377 data = b''.join([bytes(element) for element in self.value])
378 else:
379 data = self.value
380
381 size = len(data)
382 size_bytes = b''
383 if self.type == DataElement.NIL:
384 if size != 0:
385 raise ValueError('NIL must be empty')
386 size_index = 0
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800387 elif self.type in (
388 DataElement.UNSIGNED_INTEGER,
389 DataElement.SIGNED_INTEGER,
390 DataElement.UUID,
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800391 ):
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700392 if size <= 1:
393 size_index = 0
394 elif size == 2:
395 size_index = 1
396 elif size == 4:
397 size_index = 2
398 elif size == 8:
399 size_index = 3
400 elif size == 16:
401 size_index = 4
402 else:
403 raise ValueError('invalid data size')
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800404 elif self.type in (
405 DataElement.TEXT_STRING,
406 DataElement.SEQUENCE,
407 DataElement.ALTERNATIVE,
408 DataElement.URL,
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800409 ):
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700410 if size <= 0xFF:
411 size_index = 5
412 size_bytes = bytes([size])
413 elif size <= 0xFFFF:
414 size_index = 6
415 size_bytes = struct.pack('>H', size)
416 elif size <= 0xFFFFFFFF:
417 size_index = 7
418 size_bytes = struct.pack('>I', size)
419 else:
420 raise ValueError('invalid data size')
421 elif self.type == DataElement.BOOLEAN:
422 if size != 1:
423 raise ValueError('boolean must be 1 byte')
424 size_index = 0
425
426 self.bytes = bytes([self.type << 3 | size_index]) + size_bytes + data
427 return self.bytes
428
429 def to_string(self, pretty=False, indentation=0):
430 prefix = ' ' * indentation
431 type_name = name_or_number(self.TYPE_NAMES, self.type)
432 if self.type == DataElement.NIL:
433 value_string = ''
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800434 elif self.type in (DataElement.SEQUENCE, DataElement.ALTERNATIVE):
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700435 container_separator = '\n' if pretty else ''
436 element_separator = '\n' if pretty else ','
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800437 elements = [
438 element.to_string(pretty, indentation + 1 if pretty else 0)
439 for element in self.value
440 ]
441 value_string = (
442 f'[{container_separator}'
443 f'{element_separator.join(elements)}'
444 f'{container_separator}{prefix}]'
445 )
446 elif self.type in (DataElement.UNSIGNED_INTEGER, DataElement.SIGNED_INTEGER):
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700447 value_string = f'{self.value}#{self.value_size}'
448 elif isinstance(self.value, DataElement):
449 value_string = self.value.to_string(pretty, indentation)
450 else:
451 value_string = str(self.value)
452 return f'{prefix}{type_name}({value_string})'
453
454 def __str__(self):
455 return self.to_string()
456
457
458# -----------------------------------------------------------------------------
459class ServiceAttribute:
uaelb731f6f2023-02-02 17:36:23 +0000460 def __init__(self, attribute_id: int, value: DataElement) -> None:
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800461 self.id = attribute_id
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700462 self.value = value
463
464 @staticmethod
465 def list_from_data_elements(elements):
466 attribute_list = []
467 for i in range(0, len(elements) // 2):
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800468 attribute_id, attribute_value = elements[2 * i : 2 * (i + 1)]
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700469 if attribute_id.type != DataElement.UNSIGNED_INTEGER:
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800470 logger.warning('attribute ID element is not an integer')
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700471 continue
472 attribute_list.append(ServiceAttribute(attribute_id.value, attribute_value))
473
474 return attribute_list
475
476 @staticmethod
477 def find_attribute_in_list(attribute_list, attribute_id):
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800478 return next(
479 (
480 attribute.value
481 for attribute in attribute_list
482 if attribute.id == attribute_id
483 ),
484 None,
485 )
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700486
487 @staticmethod
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800488 def id_name(id_code):
489 return name_or_number(SDP_ATTRIBUTE_ID_NAMES, id_code)
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700490
491 @staticmethod
492 def is_uuid_in_value(uuid, value):
493 # Find if a uuid matches a value, either directly or recursing into sequences
494 if value.type == DataElement.UUID:
495 return value.value == uuid
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800496
497 if value.type == DataElement.SEQUENCE:
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700498 for element in value.value:
499 if ServiceAttribute.is_uuid_in_value(uuid, element):
500 return True
501 return False
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700502
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800503 return False
504
505 def to_string(self, with_colors=False):
506 if with_colors:
507 return (
uaeld21da782023-02-23 20:16:33 +0000508 f'Attribute(id={color(self.id_name(self.id),"magenta")},'
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800509 f'value={self.value})'
510 )
511
512 return f'Attribute(id={self.id_name(self.id)},value={self.value})'
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700513
514 def __str__(self):
515 return self.to_string()
516
517
518# -----------------------------------------------------------------------------
519class SDP_PDU:
520 '''
521 See Bluetooth spec @ Vol 3, Part B - 4.2 PROTOCOL DATA UNIT FORMAT
522 '''
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800523
Gilles Boccon-Gibod99758e42023-01-20 00:20:50 -0800524 sdp_pdu_classes: Dict[int, Type[SDP_PDU]] = {}
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800525 name = None
526 pdu_id = 0
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700527
528 @staticmethod
529 def from_bytes(pdu):
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800530 pdu_id, transaction_id, _parameters_length = struct.unpack_from('>BHH', pdu, 0)
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700531
532 cls = SDP_PDU.sdp_pdu_classes.get(pdu_id)
533 if cls is None:
534 instance = SDP_PDU(pdu)
535 instance.name = SDP_PDU.pdu_name(pdu_id)
536 instance.pdu_id = pdu_id
537 instance.transaction_id = transaction_id
538 return instance
539 self = cls.__new__(cls)
540 SDP_PDU.__init__(self, pdu, transaction_id)
541 if hasattr(self, 'fields'):
542 self.init_from_bytes(pdu, 5)
543 return self
544
545 @staticmethod
546 def parse_service_record_handle_list_preceded_by_count(data, offset):
547 count = struct.unpack_from('>H', data, offset - 2)[0]
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800548 handle_list = [
549 struct.unpack_from('>I', data, offset + x * 4)[0] for x in range(count)
550 ]
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700551 return offset + count * 4, handle_list
552
553 @staticmethod
554 def parse_bytes_preceded_by_length(data, offset):
555 length = struct.unpack_from('>H', data, offset - 2)[0]
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800556 return offset + length, data[offset : offset + length]
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700557
558 @staticmethod
559 def error_name(error_code):
560 return name_or_number(SDP_ERROR_NAMES, error_code)
561
562 @staticmethod
563 def pdu_name(code):
564 return name_or_number(SDP_PDU_NAMES, code)
565
566 @staticmethod
567 def subclass(fields):
568 def inner(cls):
569 name = cls.__name__
570
571 # add a _ character before every uppercase letter, except the SDP_ prefix
572 location = len(name) - 1
573 while location > 4:
574 if not name[location].isupper():
575 location -= 1
576 continue
577 name = name[:location] + '_' + name[location:]
578 location -= 1
579
580 cls.name = name.upper()
581 cls.pdu_id = key_with_value(SDP_PDU_NAMES, cls.name)
582 if cls.pdu_id is None:
583 raise KeyError(f'PDU name {cls.name} not found in SDP_PDU_NAMES')
584 cls.fields = fields
585
586 # Register a factory for this class
587 SDP_PDU.sdp_pdu_classes[cls.pdu_id] = cls
588
589 return cls
590
591 return inner
592
593 def __init__(self, pdu=None, transaction_id=0, **kwargs):
594 if hasattr(self, 'fields') and kwargs:
595 HCI_Object.init_from_fields(self, self.fields, kwargs)
596 if pdu is None:
597 parameters = HCI_Object.dict_to_bytes(kwargs, self.fields)
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800598 pdu = (
599 struct.pack('>BHH', self.pdu_id, transaction_id, len(parameters))
600 + parameters
601 )
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700602 self.pdu = pdu
603 self.transaction_id = transaction_id
604
605 def init_from_bytes(self, pdu, offset):
606 return HCI_Object.init_from_bytes(self, pdu, offset, self.fields)
607
608 def to_bytes(self):
609 return self.pdu
610
611 def __bytes__(self):
612 return self.to_bytes()
613
614 def __str__(self):
615 result = f'{color(self.name, "blue")} [TID={self.transaction_id}]'
616 if fields := getattr(self, 'fields', None):
617 result += ':\n' + HCI_Object.format_fields(self.__dict__, fields, ' ')
618 elif len(self.pdu) > 1:
619 result += f': {self.pdu.hex()}'
620 return result
621
622
623# -----------------------------------------------------------------------------
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800624@SDP_PDU.subclass([('error_code', {'size': 2, 'mapper': SDP_PDU.error_name})])
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700625class SDP_ErrorResponse(SDP_PDU):
626 '''
627 See Bluetooth spec @ Vol 3, Part B - 4.4.1 SDP_ErrorResponse PDU
628 '''
629
630
631# -----------------------------------------------------------------------------
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800632@SDP_PDU.subclass(
633 [
634 ('service_search_pattern', DataElement.parse_from_bytes),
635 ('maximum_service_record_count', '>2'),
636 ('continuation_state', '*'),
637 ]
638)
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700639class SDP_ServiceSearchRequest(SDP_PDU):
640 '''
641 See Bluetooth spec @ Vol 3, Part B - 4.5.1 SDP_ServiceSearchRequest PDU
642 '''
643
644
645# -----------------------------------------------------------------------------
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800646@SDP_PDU.subclass(
647 [
648 ('total_service_record_count', '>2'),
649 ('current_service_record_count', '>2'),
650 (
651 'service_record_handle_list',
652 SDP_PDU.parse_service_record_handle_list_preceded_by_count,
653 ),
654 ('continuation_state', '*'),
655 ]
656)
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700657class SDP_ServiceSearchResponse(SDP_PDU):
658 '''
659 See Bluetooth spec @ Vol 3, Part B - 4.5.2 SDP_ServiceSearchResponse PDU
660 '''
661
662
663# -----------------------------------------------------------------------------
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800664@SDP_PDU.subclass(
665 [
666 ('service_record_handle', '>4'),
667 ('maximum_attribute_byte_count', '>2'),
668 ('attribute_id_list', DataElement.parse_from_bytes),
669 ('continuation_state', '*'),
670 ]
671)
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700672class SDP_ServiceAttributeRequest(SDP_PDU):
673 '''
674 See Bluetooth spec @ Vol 3, Part B - 4.6.1 SDP_ServiceAttributeRequest PDU
675 '''
676
677
678# -----------------------------------------------------------------------------
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800679@SDP_PDU.subclass(
680 [
681 ('attribute_list_byte_count', '>2'),
682 ('attribute_list', SDP_PDU.parse_bytes_preceded_by_length),
683 ('continuation_state', '*'),
684 ]
685)
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700686class SDP_ServiceAttributeResponse(SDP_PDU):
687 '''
688 See Bluetooth spec @ Vol 3, Part B - 4.6.2 SDP_ServiceAttributeResponse PDU
689 '''
690
691
692# -----------------------------------------------------------------------------
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800693@SDP_PDU.subclass(
694 [
695 ('service_search_pattern', DataElement.parse_from_bytes),
696 ('maximum_attribute_byte_count', '>2'),
697 ('attribute_id_list', DataElement.parse_from_bytes),
698 ('continuation_state', '*'),
699 ]
700)
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700701class SDP_ServiceSearchAttributeRequest(SDP_PDU):
702 '''
703 See Bluetooth spec @ Vol 3, Part B - 4.7.1 SDP_ServiceSearchAttributeRequest PDU
704 '''
705
706
707# -----------------------------------------------------------------------------
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800708@SDP_PDU.subclass(
709 [
710 ('attribute_lists_byte_count', '>2'),
711 ('attribute_lists', SDP_PDU.parse_bytes_preceded_by_length),
712 ('continuation_state', '*'),
713 ]
714)
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700715class SDP_ServiceSearchAttributeResponse(SDP_PDU):
716 '''
717 See Bluetooth spec @ Vol 3, Part B - 4.7.2 SDP_ServiceSearchAttributeResponse PDU
718 '''
719
720
721# -----------------------------------------------------------------------------
722class Client:
723 def __init__(self, device):
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800724 self.device = device
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700725 self.pending_request = None
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800726 self.channel = None
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700727
728 async def connect(self, connection):
729 result = await self.device.l2cap_channel_manager.connect(connection, SDP_PSM)
730 self.channel = result
731
732 async def disconnect(self):
733 if self.channel:
734 await self.channel.disconnect()
735 self.channel = None
736
737 async def search_services(self, uuids):
738 if self.pending_request is not None:
739 raise InvalidStateError('request already pending')
740
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800741 service_search_pattern = DataElement.sequence(
742 [DataElement.uuid(uuid) for uuid in uuids]
743 )
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700744
745 # Request and accumulate until there's no more continuation
746 service_record_handle_list = []
747 continuation_state = bytes([0])
748 watchdog = SDP_CONTINUATION_WATCHDOG
749 while watchdog > 0:
750 response_pdu = await self.channel.send_request(
751 SDP_ServiceSearchRequest(
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800752 transaction_id=0, # Transaction ID TODO: pick a real value
753 service_search_pattern=service_search_pattern,
754 maximum_service_record_count=0xFFFF,
755 continuation_state=continuation_state,
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700756 )
757 )
758 response = SDP_PDU.from_bytes(response_pdu)
759 logger.debug(f'<<< Response: {response}')
760 service_record_handle_list += response.service_record_handle_list
761 continuation_state = response.continuation_state
762 if len(continuation_state) == 1 and continuation_state[0] == 0:
763 break
764 logger.debug(f'continuation: {continuation_state.hex()}')
765 watchdog -= 1
766
767 return service_record_handle_list
768
769 async def search_attributes(self, uuids, attribute_ids):
770 if self.pending_request is not None:
771 raise InvalidStateError('request already pending')
772
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800773 service_search_pattern = DataElement.sequence(
774 [DataElement.uuid(uuid) for uuid in uuids]
775 )
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700776 attribute_id_list = DataElement.sequence(
777 [
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800778 DataElement.unsigned_integer(
779 attribute_id[0], value_size=attribute_id[1]
780 )
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800781 if isinstance(attribute_id, tuple)
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700782 else DataElement.unsigned_integer_16(attribute_id)
783 for attribute_id in attribute_ids
784 ]
785 )
786
787 # Request and accumulate until there's no more continuation
788 accumulator = b''
789 continuation_state = bytes([0])
790 watchdog = SDP_CONTINUATION_WATCHDOG
791 while watchdog > 0:
792 response_pdu = await self.channel.send_request(
793 SDP_ServiceSearchAttributeRequest(
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800794 transaction_id=0, # Transaction ID TODO: pick a real value
795 service_search_pattern=service_search_pattern,
796 maximum_attribute_byte_count=0xFFFF,
797 attribute_id_list=attribute_id_list,
798 continuation_state=continuation_state,
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700799 )
800 )
801 response = SDP_PDU.from_bytes(response_pdu)
802 logger.debug(f'<<< Response: {response}')
803 accumulator += response.attribute_lists
804 continuation_state = response.continuation_state
805 if len(continuation_state) == 1 and continuation_state[0] == 0:
806 break
807 logger.debug(f'continuation: {continuation_state.hex()}')
808 watchdog -= 1
809
810 # Parse the result into attribute lists
811 attribute_lists_sequences = DataElement.from_bytes(accumulator)
812 if attribute_lists_sequences.type != DataElement.SEQUENCE:
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800813 logger.warning('unexpected data type')
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700814 return []
815
816 return [
817 ServiceAttribute.list_from_data_elements(sequence.value)
818 for sequence in attribute_lists_sequences.value
819 if sequence.type == DataElement.SEQUENCE
820 ]
821
822 async def get_attributes(self, service_record_handle, attribute_ids):
823 if self.pending_request is not None:
824 raise InvalidStateError('request already pending')
825
826 attribute_id_list = DataElement.sequence(
827 [
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800828 DataElement.unsigned_integer(
829 attribute_id[0], value_size=attribute_id[1]
830 )
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800831 if isinstance(attribute_id, tuple)
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700832 else DataElement.unsigned_integer_16(attribute_id)
833 for attribute_id in attribute_ids
834 ]
835 )
836
837 # Request and accumulate until there's no more continuation
838 accumulator = b''
839 continuation_state = bytes([0])
840 watchdog = SDP_CONTINUATION_WATCHDOG
841 while watchdog > 0:
842 response_pdu = await self.channel.send_request(
843 SDP_ServiceAttributeRequest(
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800844 transaction_id=0, # Transaction ID TODO: pick a real value
845 service_record_handle=service_record_handle,
846 maximum_attribute_byte_count=0xFFFF,
847 attribute_id_list=attribute_id_list,
848 continuation_state=continuation_state,
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700849 )
850 )
851 response = SDP_PDU.from_bytes(response_pdu)
852 logger.debug(f'<<< Response: {response}')
853 accumulator += response.attribute_list
854 continuation_state = response.continuation_state
855 if len(continuation_state) == 1 and continuation_state[0] == 0:
856 break
857 logger.debug(f'continuation: {continuation_state.hex()}')
858 watchdog -= 1
859
860 # Parse the result into a list of attributes
861 attribute_list_sequence = DataElement.from_bytes(accumulator)
862 if attribute_list_sequence.type != DataElement.SEQUENCE:
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800863 logger.warning('unexpected data type')
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700864 return []
865
866 return ServiceAttribute.list_from_data_elements(attribute_list_sequence.value)
867
868
869# -----------------------------------------------------------------------------
870class Server:
871 CONTINUATION_STATE = bytes([0x01, 0x43])
872
873 def __init__(self, device):
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800874 self.device = device
875 self.service_records = {} # Service records maps, by record handle
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800876 self.channel = None
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700877 self.current_response = None
878
879 def register(self, l2cap_channel_manager):
880 l2cap_channel_manager.register_server(SDP_PSM, self.on_connection)
881
882 def send_response(self, response):
883 logger.debug(f'{color(">>> Sending SDP Response", "blue")}: {response}')
884 self.channel.send_pdu(response)
885
886 def match_services(self, search_pattern):
887 # Find the services for which the attributes in the pattern is a subset of the
888 # service's attribute values (NOTE: the value search recurses into sequences)
889 matching_services = {}
890 for handle, service in self.service_records.items():
891 for uuid in search_pattern.value:
892 found = False
893 for attribute in service:
894 if ServiceAttribute.is_uuid_in_value(uuid.value, attribute.value):
895 found = True
896 break
897 if found:
898 matching_services[handle] = service
899 break
900
901 return matching_services
902
903 def on_connection(self, channel):
904 self.channel = channel
905 self.channel.sink = self.on_pdu
906
907 def on_pdu(self, pdu):
908 try:
909 sdp_pdu = SDP_PDU.from_bytes(pdu)
910 except Exception as error:
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800911 logger.warning(color(f'failed to parse SDP Request PDU: {error}', 'red'))
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700912 self.send_response(
913 SDP_ErrorResponse(
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800914 transaction_id=0, error_code=SDP_INVALID_REQUEST_SYNTAX_ERROR
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700915 )
916 )
917
918 logger.debug(f'{color("<<< Received SDP Request", "green")}: {sdp_pdu}')
919
920 # Find the handler method
921 handler_name = f'on_{sdp_pdu.name.lower()}'
922 handler = getattr(self, handler_name, None)
923 if handler:
924 try:
925 handler(sdp_pdu)
926 except Exception as error:
927 logger.warning(f'{color("!!! Exception in handler:", "red")} {error}')
928 self.send_response(
929 SDP_ErrorResponse(
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800930 transaction_id=sdp_pdu.transaction_id,
931 error_code=SDP_INSUFFICIENT_RESOURCES_TO_SATISFY_REQUEST_ERROR,
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700932 )
933 )
934 else:
935 logger.error(color('SDP Request not handled???', 'red'))
936 self.send_response(
937 SDP_ErrorResponse(
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800938 transaction_id=sdp_pdu.transaction_id,
939 error_code=SDP_INVALID_REQUEST_SYNTAX_ERROR,
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700940 )
941 )
942
943 def get_next_response_payload(self, maximum_size):
944 if len(self.current_response) > maximum_size:
945 payload = self.current_response[:maximum_size]
946 continuation_state = Server.CONTINUATION_STATE
947 self.current_response = self.current_response[maximum_size:]
948 else:
949 payload = self.current_response
950 continuation_state = bytes([0])
951 self.current_response = None
952
953 return (payload, continuation_state)
954
955 @staticmethod
956 def get_service_attributes(service, attribute_ids):
957 attributes = []
958 for attribute_id in attribute_ids:
959 if attribute_id.value_size == 4:
960 # Attribute ID range
961 id_range_start = attribute_id.value >> 16
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800962 id_range_end = attribute_id.value & 0xFFFF
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700963 else:
964 id_range_start = attribute_id.value
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800965 id_range_end = attribute_id.value
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700966 attributes += [
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800967 attribute
968 for attribute in service
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700969 if attribute.id >= id_range_start and attribute.id <= id_range_end
970 ]
971
Gilles Boccon-Gibodc2959da2022-12-10 09:29:51 -0800972 # Return the matching attributes, sorted by attribute id
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800973 attributes.sort(key=lambda x: x.id)
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700974 attribute_list = DataElement.sequence([])
975 for attribute in attributes:
976 attribute_list.value.append(DataElement.unsigned_integer_16(attribute.id))
977 attribute_list.value.append(attribute.value)
978
979 return attribute_list
980
981 def on_sdp_service_search_request(self, request):
982 # Check if this is a continuation
983 if len(request.continuation_state) > 1:
984 if not self.current_response:
985 self.send_response(
986 SDP_ErrorResponse(
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -0800987 transaction_id=request.transaction_id,
988 error_code=SDP_INVALID_CONTINUATION_STATE_ERROR,
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -0700989 )
990 )
991 return
992 else:
993 # Cleanup any partial response leftover
994 self.current_response = None
995
996 # Find the matching services
997 matching_services = self.match_services(request.service_search_pattern)
998 service_record_handles = list(matching_services.keys())
999
1000 # Only return up to the maximum requested
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -08001001 service_record_handles_subset = service_record_handles[
1002 : request.maximum_service_record_count
1003 ]
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -07001004
1005 # Serialize to a byte array, and remember the total count
1006 logger.debug(f'Service Record Handles: {service_record_handles}')
1007 self.current_response = (
1008 len(service_record_handles),
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -08001009 service_record_handles_subset,
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -07001010 )
1011
1012 # Respond, keeping any unsent handles for later
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -08001013 service_record_handles = self.current_response[1][
1014 : request.maximum_service_record_count
1015 ]
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -07001016 self.current_response = (
1017 self.current_response[0],
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -08001018 self.current_response[1][request.maximum_service_record_count :],
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -07001019 )
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -08001020 continuation_state = (
1021 Server.CONTINUATION_STATE if self.current_response[1] else bytes([0])
1022 )
1023 service_record_handle_list = b''.join(
1024 [struct.pack('>I', handle) for handle in service_record_handles]
1025 )
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -07001026 self.send_response(
1027 SDP_ServiceSearchResponse(
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -08001028 transaction_id=request.transaction_id,
1029 total_service_record_count=self.current_response[0],
1030 current_service_record_count=len(service_record_handles),
1031 service_record_handle_list=service_record_handle_list,
1032 continuation_state=continuation_state,
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -07001033 )
1034 )
1035
1036 def on_sdp_service_attribute_request(self, request):
1037 # Check if this is a continuation
1038 if len(request.continuation_state) > 1:
1039 if not self.current_response:
1040 self.send_response(
1041 SDP_ErrorResponse(
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -08001042 transaction_id=request.transaction_id,
1043 error_code=SDP_INVALID_CONTINUATION_STATE_ERROR,
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -07001044 )
1045 )
1046 return
1047 else:
1048 # Cleanup any partial response leftover
1049 self.current_response = None
1050
1051 # Check that the service exists
1052 service = self.service_records.get(request.service_record_handle)
1053 if service is None:
1054 self.send_response(
1055 SDP_ErrorResponse(
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -08001056 transaction_id=request.transaction_id,
1057 error_code=SDP_INVALID_SERVICE_RECORD_HANDLE_ERROR,
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -07001058 )
1059 )
1060 return
1061
1062 # Get the attributes for the service
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -08001063 attribute_list = Server.get_service_attributes(
1064 service, request.attribute_id_list.value
1065 )
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -07001066
1067 # Serialize to a byte array
1068 logger.debug(f'Attributes: {attribute_list}')
1069 self.current_response = bytes(attribute_list)
1070
1071 # Respond, keeping any pending chunks for later
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -08001072 attribute_list, continuation_state = self.get_next_response_payload(
1073 request.maximum_attribute_byte_count
1074 )
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -07001075 self.send_response(
1076 SDP_ServiceAttributeResponse(
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -08001077 transaction_id=request.transaction_id,
1078 attribute_list_byte_count=len(attribute_list),
1079 attribute_list=attribute_list,
1080 continuation_state=continuation_state,
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -07001081 )
1082 )
1083
1084 def on_sdp_service_search_attribute_request(self, request):
1085 # Check if this is a continuation
1086 if len(request.continuation_state) > 1:
1087 if not self.current_response:
1088 self.send_response(
1089 SDP_ErrorResponse(
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -08001090 transaction_id=request.transaction_id,
1091 error_code=SDP_INVALID_CONTINUATION_STATE_ERROR,
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -07001092 )
1093 )
1094 else:
1095 # Cleanup any partial response leftover
1096 self.current_response = None
1097
1098 # Find the matching services
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -08001099 matching_services = self.match_services(
1100 request.service_search_pattern
1101 ).values()
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -07001102
1103 # Filter the required attributes
1104 attribute_lists = DataElement.sequence([])
1105 for service in matching_services:
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -08001106 attribute_list = Server.get_service_attributes(
1107 service, request.attribute_id_list.value
1108 )
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -07001109 if attribute_list.value:
1110 attribute_lists.value.append(attribute_list)
1111
1112 # Serialize to a byte array
1113 logger.debug(f'Search response: {attribute_lists}')
1114 self.current_response = bytes(attribute_lists)
1115
1116 # Respond, keeping any pending chunks for later
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -08001117 attribute_lists, continuation_state = self.get_next_response_payload(
1118 request.maximum_attribute_byte_count
1119 )
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -07001120 self.send_response(
1121 SDP_ServiceSearchAttributeResponse(
Gilles Boccon-Gibod135df0d2022-12-10 08:53:51 -08001122 transaction_id=request.transaction_id,
1123 attribute_lists_byte_count=len(attribute_lists),
1124 attribute_lists=attribute_lists,
1125 continuation_state=continuation_state,
Gilles Boccon-Gibod6ac91f72022-05-16 19:42:31 -07001126 )
1127 )