| 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 | |