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