1 | // Copyright (C) 2016 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include <QtCore/QByteArray> |
5 | #include <QtCore/QDebug> |
6 | #include <QtCore/QUrl> |
7 | #include <stdio.h> |
8 | #include <string> |
9 | #include <bluetooth/bluetooth.h> |
10 | #include <bluetooth/sdp.h> |
11 | #include <bluetooth/sdp_lib.h> |
12 | |
13 | #define RETURN_SUCCESS 0 |
14 | #define RETURN_USAGE 1 |
15 | #define RETURN_INVALPARAM 2 |
16 | #define RETURN_SDP_ERROR 3 |
17 | |
18 | void usage() |
19 | { |
20 | fprintf(stderr, format: "Usage:\n" ); |
21 | fprintf(stderr, format: "\tsdpscanner <remote bdaddr> <local bdaddr> [Options] ({uuids})\n\n" ); |
22 | fprintf(stderr, format: "Performs an SDP scan on remote device, using the SDP server\n" |
23 | "represented by the local Bluetooth device.\n\n" |
24 | "Options:\n" |
25 | " -p Show scan results in human-readable form\n" |
26 | " -u [list of uuids] List of uuids which should be scanned for.\n" |
27 | " Each uuid must be enclosed in {}.\n" |
28 | " If the list is empty PUBLIC_BROWSE_GROUP scan is used.\n" ); |
29 | } |
30 | |
31 | #define BUFFER_SIZE 1024 |
32 | |
33 | static void parseAttributeValues(sdp_data_t *data, int indentation, QByteArray &xmlOutput) |
34 | { |
35 | if (!data) |
36 | return; |
37 | |
38 | const int length = indentation*2 + 1; |
39 | QByteArray indentString(length, ' '); |
40 | |
41 | char snBuffer[BUFFER_SIZE]; |
42 | |
43 | xmlOutput.append(a: indentString); |
44 | |
45 | // deal with every dtd type |
46 | switch (data->dtd) { |
47 | case SDP_DATA_NIL: |
48 | xmlOutput.append(s: "<nil/>\n" ); |
49 | break; |
50 | case SDP_UINT8: |
51 | qsnprintf(str: snBuffer, BUFFER_SIZE, fmt: "<uint8 value=\"0x%02x\"/>\n" , data->val.uint8); |
52 | xmlOutput.append(s: snBuffer); |
53 | break; |
54 | case SDP_UINT16: |
55 | qsnprintf(str: snBuffer, BUFFER_SIZE, fmt: "<uint16 value=\"0x%04x\"/>\n" , data->val.uint16); |
56 | xmlOutput.append(s: snBuffer); |
57 | break; |
58 | case SDP_UINT32: |
59 | qsnprintf(str: snBuffer, BUFFER_SIZE, fmt: "<uint32 value=\"0x%08x\"/>\n" , data->val.uint32); |
60 | xmlOutput.append(s: snBuffer); |
61 | break; |
62 | case SDP_UINT64: |
63 | qsnprintf(str: snBuffer, BUFFER_SIZE, fmt: "<uint64 value=\"0x%016x\"/>\n" , data->val.uint64); |
64 | xmlOutput.append(s: snBuffer); |
65 | break; |
66 | case SDP_UINT128: |
67 | xmlOutput.append(s: "<uint128 value=\"0x" ); |
68 | for (int i = 0; i < 16; i++) |
69 | ::sprintf(s: &snBuffer[i * 2], format: "%02x" , data->val.uint128.data[i]); |
70 | xmlOutput.append(s: snBuffer); |
71 | xmlOutput.append(s: "\"/>\n" ); |
72 | break; |
73 | case SDP_INT8: |
74 | qsnprintf(str: snBuffer, BUFFER_SIZE, fmt: "<int8 value=\"%d\"/>/n" , data->val.int8); |
75 | xmlOutput.append(s: snBuffer); |
76 | break; |
77 | case SDP_INT16: |
78 | qsnprintf(str: snBuffer, BUFFER_SIZE, fmt: "<int16 value=\"%d\"/>/n" , data->val.int16); |
79 | xmlOutput.append(s: snBuffer); |
80 | break; |
81 | case SDP_INT32: |
82 | qsnprintf(str: snBuffer, BUFFER_SIZE, fmt: "<int32 value=\"%d\"/>/n" , data->val.int32); |
83 | xmlOutput.append(s: snBuffer); |
84 | break; |
85 | case SDP_INT64: |
86 | qsnprintf(str: snBuffer, BUFFER_SIZE, fmt: "<int64 value=\"%d\"/>/n" , data->val.int64); |
87 | xmlOutput.append(s: snBuffer); |
88 | break; |
89 | case SDP_INT128: |
90 | xmlOutput.append(s: "<int128 value=\"0x" ); |
91 | for (int i = 0; i < 16; i++) |
92 | ::sprintf(s: &snBuffer[i * 2], format: "%02x" , data->val.int128.data[i]); |
93 | xmlOutput.append(s: snBuffer); |
94 | xmlOutput.append(s: "\"/>\n" ); |
95 | break; |
96 | case SDP_UUID_UNSPEC: |
97 | break; |
98 | case SDP_UUID16: |
99 | case SDP_UUID32: |
100 | xmlOutput.append(s: "<uuid value=\"0x" ); |
101 | sdp_uuid2strn(uuid: &(data->val.uuid), str: snBuffer, BUFFER_SIZE); |
102 | xmlOutput.append(s: snBuffer); |
103 | xmlOutput.append(s: "\"/>\n" ); |
104 | break; |
105 | case SDP_UUID128: |
106 | xmlOutput.append(s: "<uuid value=\"" ); |
107 | sdp_uuid2strn(uuid: &(data->val.uuid), str: snBuffer, BUFFER_SIZE); |
108 | xmlOutput.append(s: snBuffer); |
109 | xmlOutput.append(s: "\"/>\n" ); |
110 | break; |
111 | case SDP_TEXT_STR_UNSPEC: |
112 | break; |
113 | case SDP_TEXT_STR8: |
114 | case SDP_TEXT_STR16: |
115 | case SDP_TEXT_STR32: |
116 | { |
117 | xmlOutput.append(s: "<text " ); |
118 | QByteArray text = QByteArray::fromRawData(data: data->val.str, size: data->unitSize); |
119 | |
120 | bool hasNonPrintableChar = false; |
121 | for (qsizetype i = 0; i < text.size(); ++i) { |
122 | if (text[i] == '\0') { |
123 | text.resize(size: i); // cut trailing content |
124 | break; |
125 | } else if (!isprint(text[i])) { |
126 | hasNonPrintableChar = true; |
127 | const auto firstNullIdx = text.indexOf(c: '\0'); |
128 | if (firstNullIdx > 0) |
129 | text.resize(size: firstNullIdx); // cut trailing content |
130 | break; |
131 | } |
132 | } |
133 | |
134 | if (hasNonPrintableChar) { |
135 | xmlOutput.append(s: "encoding=\"hex\" value=\"" ); |
136 | xmlOutput.append(a: text.toHex()); |
137 | } else { |
138 | text.replace(before: '&', after: "&" ); |
139 | text.replace(before: '<', after: "<" ); |
140 | text.replace(before: '>', after: ">" ); |
141 | text.replace(before: '"', after: """ ); |
142 | |
143 | xmlOutput.append(s: "value=\"" ); |
144 | xmlOutput.append(a: text); |
145 | } |
146 | |
147 | xmlOutput.append(s: "\"/>\n" ); |
148 | break; |
149 | } |
150 | case SDP_BOOL: |
151 | if (data->val.uint8) |
152 | xmlOutput.append(s: "<boolean value=\"true\"/>\n" ); |
153 | else |
154 | xmlOutput.append(s: "<boolean value=\"false\"/>\n" ); |
155 | break; |
156 | case SDP_SEQ_UNSPEC: |
157 | break; |
158 | case SDP_SEQ8: |
159 | case SDP_SEQ16: |
160 | case SDP_SEQ32: |
161 | xmlOutput.append(s: "<sequence>\n" ); |
162 | parseAttributeValues(data: data->val.dataseq, indentation: indentation + 1, xmlOutput); |
163 | xmlOutput.append(a: indentString); |
164 | xmlOutput.append(s: "</sequence>\n" ); |
165 | break; |
166 | case SDP_ALT_UNSPEC: |
167 | break; |
168 | case SDP_ALT8: |
169 | case SDP_ALT16: |
170 | case SDP_ALT32: |
171 | xmlOutput.append(s: "<alternate>\n" ); |
172 | parseAttributeValues(data: data->val.dataseq, indentation: indentation + 1, xmlOutput); |
173 | xmlOutput.append(a: indentString); |
174 | xmlOutput.append(s: "</alternate>\n" ); |
175 | break; |
176 | case SDP_URL_STR_UNSPEC: |
177 | break; |
178 | case SDP_URL_STR8: |
179 | case SDP_URL_STR16: |
180 | case SDP_URL_STR32: |
181 | { |
182 | xmlOutput.append(s: "<url value=\"" ); |
183 | const QByteArray urlData = |
184 | QByteArray::fromRawData(data: data->val.str, size: qstrnlen(str: data->val.str, maxlen: data->unitSize)); |
185 | const QUrl url = QUrl::fromEncoded(url: urlData); |
186 | // Encoded url %-encodes all of the XML special characters except '&', |
187 | // so we need to do that manually |
188 | xmlOutput.append(a: url.toEncoded().replace(before: '&', after: "&" )); |
189 | xmlOutput.append(s: "\"/>\n" ); |
190 | break; |
191 | } |
192 | default: |
193 | fprintf(stderr, format: "Unknown dtd type\n" ); |
194 | } |
195 | |
196 | parseAttributeValues(data: data->next, indentation, xmlOutput); |
197 | } |
198 | |
199 | static void parseAttribute(void *value, void *) |
200 | { |
201 | sdp_data_t *data = (sdp_data_t *) value; |
202 | QByteArray *xmlOutput = static_cast<QByteArray *>(extraData); |
203 | |
204 | char buffer[BUFFER_SIZE]; |
205 | |
206 | ::qsnprintf(str: buffer, BUFFER_SIZE, fmt: " <attribute id=\"0x%04x\">\n" , data->attrId); |
207 | xmlOutput->append(s: buffer); |
208 | |
209 | parseAttributeValues(data, indentation: 2, xmlOutput&: *xmlOutput); |
210 | |
211 | xmlOutput->append(s: " </attribute>\n" ); |
212 | } |
213 | |
214 | // the resulting xml output is based on the already used xml parser |
215 | QByteArray parseSdpRecord(sdp_record_t *record) |
216 | { |
217 | if (!record || !record->attrlist) |
218 | return QByteArray(); |
219 | |
220 | QByteArray xmlOutput; |
221 | |
222 | xmlOutput.append(s: "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<record>\n" ); |
223 | |
224 | sdp_list_foreach(list: record->attrlist, f: parseAttribute, u: &xmlOutput); |
225 | xmlOutput.append(s: "</record>" ); |
226 | |
227 | return xmlOutput; |
228 | } |
229 | |
230 | |
231 | int main(int argc, char **argv) |
232 | { |
233 | if (argc < 3) { |
234 | usage(); |
235 | return RETURN_USAGE; |
236 | } |
237 | |
238 | fprintf(stderr, format: "SDP for %s %s\n" , argv[1], argv[2]); |
239 | |
240 | bdaddr_t remote; |
241 | bdaddr_t local; |
242 | int result = str2ba(str: argv[1], ba: &remote); |
243 | if (result < 0) { |
244 | fprintf(stderr, format: "Invalid remote address: %s\n" , argv[1]); |
245 | return RETURN_INVALPARAM; |
246 | } |
247 | |
248 | result = str2ba(str: argv[2], ba: &local); |
249 | if (result < 0) { |
250 | fprintf(stderr, format: "Invalid local address: %s\n" , argv[2]); |
251 | return RETURN_INVALPARAM; |
252 | } |
253 | |
254 | bool showHumanReadable = false; |
255 | std::vector<std::string> targetServices; |
256 | |
257 | for (int i = 3; i < argc; i++) { |
258 | if (argv[i][0] != '-') { |
259 | usage(); |
260 | return RETURN_USAGE; |
261 | } |
262 | |
263 | switch (argv[i][1]) |
264 | { |
265 | case 'p': |
266 | showHumanReadable = true; |
267 | break; |
268 | case 'u': |
269 | i++; |
270 | |
271 | for ( ; i < argc && argv[i][0] == '{'; i++) |
272 | targetServices.push_back(x: argv[i]); |
273 | |
274 | i--; // outer loop increments again |
275 | break; |
276 | default: |
277 | fprintf(stderr, format: "Wrong argument: %s\n" , argv[i]); |
278 | usage(); |
279 | return RETURN_USAGE; |
280 | } |
281 | } |
282 | |
283 | std::vector<uuid_t> uuids; |
284 | for (std::vector<std::string>::const_iterator iter = targetServices.cbegin(); |
285 | iter != targetServices.cend(); ++iter) { |
286 | |
287 | uint128_t temp128; |
288 | uint16_t field1, field2, field3, field5; |
289 | uint32_t field0, field4; |
290 | |
291 | fprintf(stderr, format: "Target scan for %s\n" , (*iter).c_str()); |
292 | if (sscanf(s: (*iter).c_str(), format: "{%08x-%04hx-%04hx-%04hx-%08x%04hx}" , &field0, |
293 | &field1, &field2, &field3, &field4, &field5) != 6) { |
294 | fprintf(stderr, format: "Skipping invalid uuid: %s\n" , ((*iter).c_str())); |
295 | continue; |
296 | } |
297 | |
298 | // we need uuid_t conversion based on |
299 | // http://www.spinics.net/lists/linux-bluetooth/msg20356.html |
300 | field0 = htonl(hostlong: field0); |
301 | field4 = htonl(hostlong: field4); |
302 | field1 = htons(hostshort: field1); |
303 | field2 = htons(hostshort: field2); |
304 | field3 = htons(hostshort: field3); |
305 | field5 = htons(hostshort: field5); |
306 | |
307 | uint8_t* temp = (uint8_t*) &temp128; |
308 | memcpy(dest: &temp[0], src: &field0, n: 4); |
309 | memcpy(dest: &temp[4], src: &field1, n: 2); |
310 | memcpy(dest: &temp[6], src: &field2, n: 2); |
311 | memcpy(dest: &temp[8], src: &field3, n: 2); |
312 | memcpy(dest: &temp[10], src: &field4, n: 4); |
313 | memcpy(dest: &temp[14], src: &field5, n: 2); |
314 | |
315 | uuid_t sdpUuid; |
316 | sdp_uuid128_create(uuid: &sdpUuid, data: &temp128); |
317 | uuids.push_back(x: sdpUuid); |
318 | } |
319 | |
320 | sdp_session_t *session = sdp_connect( src: &local, dst: &remote, SDP_RETRY_IF_BUSY); |
321 | if (!session) { |
322 | //try one more time if first time failed |
323 | session = sdp_connect( src: &local, dst: &remote, SDP_RETRY_IF_BUSY); |
324 | } |
325 | |
326 | if (!session) { |
327 | fprintf(stderr, format: "Cannot establish sdp session\n" ); |
328 | return RETURN_SDP_ERROR; |
329 | } |
330 | |
331 | // set the filter for service matches |
332 | if (uuids.empty()) { |
333 | fprintf(stderr, format: "Using PUBLIC_BROWSE_GROUP for SDP search\n" ); |
334 | uuid_t publicBrowseGroupUuid; |
335 | sdp_uuid16_create(uuid: &publicBrowseGroupUuid, PUBLIC_BROWSE_GROUP); |
336 | uuids.push_back(x: publicBrowseGroupUuid); |
337 | } |
338 | |
339 | uint32_t attributeRange = 0x0000ffff; //all attributes |
340 | sdp_list_t *attributes; |
341 | attributes = sdp_list_append(list: nullptr, d: &attributeRange); |
342 | |
343 | sdp_list_t *sdpResults, *sdpIter; |
344 | sdp_list_t *totalResults = nullptr; |
345 | sdp_list_t* serviceFilter; |
346 | |
347 | for (uuid_t &uuid : uuids) { // can't be const, d/t sdp_list_append signature |
348 | serviceFilter = sdp_list_append(list: nullptr, d: &uuid); |
349 | result = sdp_service_search_attr_req(session, search: serviceFilter, |
350 | reqtype: SDP_ATTR_REQ_RANGE, |
351 | attrid_list: attributes, rsp_list: &sdpResults); |
352 | sdp_list_free(list: serviceFilter, f: nullptr); |
353 | if (result != 0) { |
354 | fprintf(stderr, format: "sdp_service_search_attr_req failed\n" ); |
355 | sdp_list_free(list: attributes, f: nullptr); |
356 | sdp_close(session); |
357 | return RETURN_SDP_ERROR; |
358 | } |
359 | |
360 | if (!sdpResults) |
361 | continue; |
362 | |
363 | if (!totalResults) { |
364 | totalResults = sdpResults; |
365 | sdpIter = totalResults; |
366 | } else { |
367 | // attach each new result list to the end of totalResults |
368 | sdpIter->next = sdpResults; |
369 | } |
370 | |
371 | while (sdpIter->next) // skip to end of list |
372 | sdpIter = sdpIter->next; |
373 | } |
374 | sdp_list_free(list: attributes, f: nullptr); |
375 | |
376 | // start XML generation from the front |
377 | sdpResults = totalResults; |
378 | |
379 | QByteArray total; |
380 | while (sdpResults) { |
381 | sdp_record_t *record = (sdp_record_t *) sdpResults->data; |
382 | |
383 | const QByteArray xml = parseSdpRecord(record); |
384 | total += xml; |
385 | |
386 | sdpIter = sdpResults; |
387 | sdpResults = sdpResults->next; |
388 | free(ptr: sdpIter); |
389 | sdp_record_free(rec: record); |
390 | } |
391 | |
392 | if (!total.isEmpty()) { |
393 | if (showHumanReadable) |
394 | printf(format: "%s" , total.constData()); |
395 | else |
396 | printf(format: "%s" , total.toBase64().constData()); |
397 | } |
398 | |
399 | sdp_close(session); |
400 | |
401 | return RETURN_SUCCESS; |
402 | } |
403 | |