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