| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the QtBluetooth module of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:LGPL$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU Lesser General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU Lesser |
| ** General Public License version 3 as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
| ** packaging of this file. Please review the following information to |
| ** ensure the GNU Lesser General Public License version 3 requirements |
| ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 2.0 or (at your option) the GNU General |
| ** Public license version 3 or any later version approved by the KDE Free |
| ** Qt Foundation. The licenses are as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
| ** https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include <QtCore/QByteArray> |
| #include <QtCore/QDebug> |
| #include <stdio.h> |
| #include <string> |
| #include <bluetooth/bluetooth.h> |
| #include <bluetooth/sdp.h> |
| #include <bluetooth/sdp_lib.h> |
| |
| #define RETURN_SUCCESS 0 |
| #define RETURN_USAGE 1 |
| #define RETURN_INVALPARAM 2 |
| #define RETURN_SDP_ERROR 3 |
| |
| void usage() |
| { |
| fprintf(stderr, "Usage:\n"); |
| fprintf(stderr, "\tsdpscanner <remote bdaddr> <local bdaddr> [Options] ({uuids})\n\n"); |
| fprintf(stderr, "Performs an SDP scan on remote device, using the SDP server\n" |
| "represented by the local Bluetooth device.\n\n" |
| "Options:\n" |
| " -p Show scan results in human-readable form\n" |
| " -u [list of uuids] List of uuids which should be scanned for.\n" |
| " Each uuid must be enclosed in {}.\n" |
| " If the list is empty PUBLIC_BROWSE_GROUP scan is used.\n"); |
| } |
| |
| #define BUFFER_SIZE 1024 |
| |
| static void parseAttributeValues(sdp_data_t *data, int indentation, QByteArray &xmlOutput) |
| { |
| if (!data) |
| return; |
| |
| const int length = indentation*2 + 1; |
| QByteArray indentString(length, ' '); |
| |
| char snBuffer[BUFFER_SIZE]; |
| |
| xmlOutput.append(indentString); |
| |
| // deal with every dtd type |
| switch (data->dtd) { |
| case SDP_DATA_NIL: |
| xmlOutput.append("<nil/>\n"); |
| break; |
| case SDP_UINT8: |
| qsnprintf(snBuffer, BUFFER_SIZE, "<uint8 value=\"0x%02x\"/>\n", data->val.uint8); |
| xmlOutput.append(snBuffer); |
| break; |
| case SDP_UINT16: |
| qsnprintf(snBuffer, BUFFER_SIZE, "<uint16 value=\"0x%04x\"/>\n", data->val.uint16); |
| xmlOutput.append(snBuffer); |
| break; |
| case SDP_UINT32: |
| qsnprintf(snBuffer, BUFFER_SIZE, "<uint32 value=\"0x%08x\"/>\n", data->val.uint32); |
| xmlOutput.append(snBuffer); |
| break; |
| case SDP_UINT64: |
| qsnprintf(snBuffer, BUFFER_SIZE, "<uint64 value=\"0x%016x\"/>\n", data->val.uint64); |
| xmlOutput.append(snBuffer); |
| break; |
| case SDP_UINT128: |
| xmlOutput.append("<uint128 value=\"0x"); |
| for (int i = 0; i < 16; i++) |
| ::sprintf(&snBuffer[i * 2], "%02x", data->val.uint128.data[i]); |
| xmlOutput.append(snBuffer); |
| xmlOutput.append("\"/>\n"); |
| break; |
| case SDP_INT8: |
| qsnprintf(snBuffer, BUFFER_SIZE, "<int8 value=\"%d\"/>/n", data->val.int8); |
| xmlOutput.append(snBuffer); |
| break; |
| case SDP_INT16: |
| qsnprintf(snBuffer, BUFFER_SIZE, "<int16 value=\"%d\"/>/n", data->val.int16); |
| xmlOutput.append(snBuffer); |
| break; |
| case SDP_INT32: |
| qsnprintf(snBuffer, BUFFER_SIZE, "<int32 value=\"%d\"/>/n", data->val.int32); |
| xmlOutput.append(snBuffer); |
| break; |
| case SDP_INT64: |
| qsnprintf(snBuffer, BUFFER_SIZE, "<int64 value=\"%d\"/>/n", data->val.int64); |
| xmlOutput.append(snBuffer); |
| break; |
| case SDP_INT128: |
| xmlOutput.append("<int128 value=\"0x"); |
| for (int i = 0; i < 16; i++) |
| ::sprintf(&snBuffer[i * 2], "%02x", data->val.int128.data[i]); |
| xmlOutput.append(snBuffer); |
| xmlOutput.append("\"/>\n"); |
| break; |
| case SDP_UUID_UNSPEC: |
| break; |
| case SDP_UUID16: |
| case SDP_UUID32: |
| xmlOutput.append("<uuid value=\"0x"); |
| sdp_uuid2strn(&(data->val.uuid), snBuffer, BUFFER_SIZE); |
| xmlOutput.append(snBuffer); |
| xmlOutput.append("\"/>\n"); |
| break; |
| case SDP_UUID128: |
| xmlOutput.append("<uuid value=\""); |
| sdp_uuid2strn(&(data->val.uuid), snBuffer, BUFFER_SIZE); |
| xmlOutput.append(snBuffer); |
| xmlOutput.append("\"/>\n"); |
| break; |
| case SDP_TEXT_STR_UNSPEC: |
| break; |
| case SDP_TEXT_STR8: |
| case SDP_TEXT_STR16: |
| case SDP_TEXT_STR32: |
| { |
| xmlOutput.append("<text "); |
| QByteArray text = QByteArray::fromRawData(data->val.str, data->unitSize); |
| |
| bool hasNonPrintableChar = false; |
| for (int i = 0; i < text.count(); i++) { |
| if (text[i] == '\0') { |
| text.resize(i); // cut trailing content |
| break; |
| } else if (!isprint(text[i])) { |
| hasNonPrintableChar = true; |
| text.resize(text.indexOf('\0')); // cut trailing content |
| break; |
| } |
| } |
| |
| if (hasNonPrintableChar) { |
| xmlOutput.append("encoding=\"hex\" value=\""); |
| xmlOutput.append(text.toHex()); |
| } else { |
| text.replace('&', "&"); |
| text.replace('<', "<"); |
| text.replace('>', ">"); |
| text.replace('"', """); |
| |
| xmlOutput.append("value=\""); |
| xmlOutput.append(text); |
| } |
| |
| xmlOutput.append("\"/>\n"); |
| break; |
| } |
| case SDP_BOOL: |
| if (data->val.uint8) |
| xmlOutput.append("<boolean value=\"true\"/>\n"); |
| else |
| xmlOutput.append("<boolean value=\"false\"/>\n"); |
| break; |
| case SDP_SEQ_UNSPEC: |
| break; |
| case SDP_SEQ8: |
| case SDP_SEQ16: |
| case SDP_SEQ32: |
| xmlOutput.append("<sequence>\n"); |
| parseAttributeValues(data->val.dataseq, indentation + 1, xmlOutput); |
| xmlOutput.append(indentString); |
| xmlOutput.append("</sequence>\n"); |
| break; |
| case SDP_ALT_UNSPEC: |
| break; |
| case SDP_ALT8: |
| case SDP_ALT16: |
| case SDP_ALT32: |
| xmlOutput.append("<alternate>\n"); |
| parseAttributeValues(data->val.dataseq, indentation + 1, xmlOutput); |
| xmlOutput.append(indentString); |
| xmlOutput.append("</alternate>\n"); |
| break; |
| case SDP_URL_STR_UNSPEC: |
| break; |
| case SDP_URL_STR8: |
| case SDP_URL_STR16: |
| case SDP_URL_STR32: |
| strncpy(snBuffer, data->val.str, data->unitSize - 1); |
| xmlOutput.append("<url value=\""); |
| xmlOutput.append(snBuffer); |
| xmlOutput.append("\"/>\n"); |
| break; |
| default: |
| fprintf(stderr, "Unknown dtd type\n"); |
| } |
| |
| parseAttributeValues(data->next, indentation, xmlOutput); |
| } |
| |
| static void parseAttribute(void *value, void *extraData) |
| { |
| sdp_data_t *data = (sdp_data_t *) value; |
| QByteArray *xmlOutput = static_cast<QByteArray *>(extraData); |
| |
| char buffer[BUFFER_SIZE]; |
| |
| ::qsnprintf(buffer, BUFFER_SIZE, " <attribute id=\"0x%04x\">\n", data->attrId); |
| xmlOutput->append(buffer); |
| |
| parseAttributeValues(data, 2, *xmlOutput); |
| |
| xmlOutput->append(" </attribute>\n"); |
| } |
| |
| // the resulting xml output is based on the already used xml parser |
| QByteArray parseSdpRecord(sdp_record_t *record) |
| { |
| if (!record || !record->attrlist) |
| return QByteArray(); |
| |
| QByteArray xmlOutput; |
| |
| xmlOutput.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<record>\n"); |
| |
| sdp_list_foreach(record->attrlist, parseAttribute, &xmlOutput); |
| xmlOutput.append("</record>"); |
| |
| return xmlOutput; |
| } |
| |
| |
| int main(int argc, char **argv) |
| { |
| if (argc < 3) { |
| usage(); |
| return RETURN_USAGE; |
| } |
| |
| fprintf(stderr, "SDP for %s %s\n", argv[1], argv[2]); |
| |
| bdaddr_t remote; |
| bdaddr_t local; |
| int result = str2ba(argv[1], &remote); |
| if (result < 0) { |
| fprintf(stderr, "Invalid remote address: %s\n", argv[1]); |
| return RETURN_INVALPARAM; |
| } |
| |
| result = str2ba(argv[2], &local); |
| if (result < 0) { |
| fprintf(stderr, "Invalid local address: %s\n", argv[2]); |
| return RETURN_INVALPARAM; |
| } |
| |
| bool showHumanReadable = false; |
| std::vector<std::string> targetServices; |
| |
| for (int i = 3; i < argc; i++) { |
| if (argv[i][0] != '-') { |
| usage(); |
| return RETURN_USAGE; |
| } |
| |
| switch (argv[i][1]) |
| { |
| case 'p': |
| showHumanReadable = true; |
| break; |
| case 'u': |
| i++; |
| |
| for ( ; i < argc && argv[i][0] == '{'; i++) |
| targetServices.push_back(argv[i]); |
| |
| i--; // outer loop increments again |
| break; |
| default: |
| fprintf(stderr, "Wrong argument: %s\n", argv[i]); |
| usage(); |
| return RETURN_USAGE; |
| } |
| } |
| |
| std::vector<uuid_t> uuids; |
| for (std::vector<std::string>::const_iterator iter = targetServices.cbegin(); |
| iter != targetServices.cend(); ++iter) { |
| |
| uint128_t temp128; |
| uint16_t field1, field2, field3, field5; |
| uint32_t field0, field4; |
| |
| fprintf(stderr, "Target scan for %s\n", (*iter).c_str()); |
| if (sscanf((*iter).c_str(), "{%08x-%04hx-%04hx-%04hx-%08x%04hx}", &field0, |
| &field1, &field2, &field3, &field4, &field5) != 6) { |
| fprintf(stderr, "Skipping invalid uuid: %s\n", ((*iter).c_str())); |
| continue; |
| } |
| |
| // we need uuid_t conversion based on |
| // http://www.spinics.net/lists/linux-bluetooth/msg20356.html |
| field0 = htonl(field0); |
| field4 = htonl(field4); |
| field1 = htons(field1); |
| field2 = htons(field2); |
| field3 = htons(field3); |
| field5 = htons(field5); |
| |
| uint8_t* temp = (uint8_t*) &temp128; |
| memcpy(&temp[0], &field0, 4); |
| memcpy(&temp[4], &field1, 2); |
| memcpy(&temp[6], &field2, 2); |
| memcpy(&temp[8], &field3, 2); |
| memcpy(&temp[10], &field4, 4); |
| memcpy(&temp[14], &field5, 2); |
| |
| uuid_t sdpUuid; |
| sdp_uuid128_create(&sdpUuid, &temp128); |
| uuids.push_back(sdpUuid); |
| } |
| |
| sdp_session_t *session = sdp_connect( &local, &remote, SDP_RETRY_IF_BUSY); |
| if (!session) { |
| //try one more time if first time failed |
| session = sdp_connect( &local, &remote, SDP_RETRY_IF_BUSY); |
| } |
| |
| if (!session) { |
| fprintf(stderr, "Cannot establish sdp session\n"); |
| return RETURN_SDP_ERROR; |
| } |
| |
| // set the filter for service matches |
| if (uuids.empty()) { |
| fprintf(stderr, "Using PUBLIC_BROWSE_GROUP for SDP search\n"); |
| uuid_t publicBrowseGroupUuid; |
| sdp_uuid16_create(&publicBrowseGroupUuid, PUBLIC_BROWSE_GROUP); |
| uuids.push_back(publicBrowseGroupUuid); |
| } |
| |
| uint32_t attributeRange = 0x0000ffff; //all attributes |
| sdp_list_t *attributes; |
| attributes = sdp_list_append(nullptr, &attributeRange); |
| |
| sdp_list_t *sdpResults, *sdpIter; |
| sdp_list_t *totalResults = nullptr; |
| sdp_list_t* serviceFilter; |
| |
| for (uint i = 0; i < uuids.size(); ++i) { |
| serviceFilter = sdp_list_append(nullptr, &uuids[i]); |
| result = sdp_service_search_attr_req(session, serviceFilter, |
| SDP_ATTR_REQ_RANGE, |
| attributes, &sdpResults); |
| sdp_list_free(serviceFilter, nullptr); |
| if (result != 0) { |
| fprintf(stderr, "sdp_service_search_attr_req failed\n"); |
| sdp_list_free(attributes, nullptr); |
| sdp_close(session); |
| return RETURN_SDP_ERROR; |
| } |
| |
| if (!sdpResults) |
| continue; |
| |
| if (!totalResults) { |
| totalResults = sdpResults; |
| sdpIter = totalResults; |
| } else { |
| // attach each new result list to the end of totalResults |
| sdpIter->next = sdpResults; |
| } |
| |
| while (sdpIter->next) // skip to end of list |
| sdpIter = sdpIter->next; |
| } |
| sdp_list_free(attributes, nullptr); |
| |
| // start XML generation from the front |
| sdpResults = totalResults; |
| |
| QByteArray total; |
| while (sdpResults) { |
| sdp_record_t *record = (sdp_record_t *) sdpResults->data; |
| |
| const QByteArray xml = parseSdpRecord(record); |
| total += xml; |
| |
| sdpIter = sdpResults; |
| sdpResults = sdpResults->next; |
| free(sdpIter); |
| sdp_record_free(record); |
| } |
| |
| if (!total.isEmpty()) { |
| if (showHumanReadable) |
| printf("%s", total.constData()); |
| else |
| printf("%s", total.toBase64().constData()); |
| } |
| |
| sdp_close(session); |
| |
| return RETURN_SUCCESS; |
| } |