Revision control
++ // Copyright (C) 2016 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++ // Qt-Security score:critical reason:execute-external-code
++
++ #include "qbluetoothservicediscoveryagent.h"
++ #include "qbluetoothservicediscoveryagent_p.h"
++
++ #include "bluez/bluez5_helper_p.h"
++ #include "bluez/objectmanager_p.h"
++ #include "bluez/adapter1_bluez5_p.h"
++
++ #include <QtCore/QFile>
++ #include <QtCore/QLibraryInfo>
++ #include <QtCore/QLoggingCategory>
++ #include <QtCore/QProcess>
++ #include <QtCore/QScopeGuard>
++
++ #include <QtDBus/QDBusPendingCallWatcher>
++
++ QT_BEGIN_NAMESPACE
++
++ Q_DECLARE_LOGGING_CATEGORY(QT_BT_BLUEZ)
++
++ using namespace QtBluetoothPrivate; // for D-Bus wrappers
++
++ QBluetoothServiceDiscoveryAgentPrivate::QBluetoothServiceDiscoveryAgentPrivate(
++ QBluetoothServiceDiscoveryAgent *qp, const QBluetoothAddress &deviceAdapter)
++ : error(QBluetoothServiceDiscoveryAgent::NoError), m_deviceAdapterAddress(deviceAdapter), state(Inactive),
++ mode(QBluetoothServiceDiscoveryAgent::MinimalDiscovery), singleDevice(false),
++ q_ptr(qp)
++ {
++ initializeBluez5();
++ manager = new OrgFreedesktopDBusObjectManagerInterface(
++ QStringLiteral("org.bluez"), QStringLiteral("/"), QDBusConnection::systemBus());
++ qRegisterMetaType<QBluetoothServiceDiscoveryAgent::Error>();
++ }
++
++ QBluetoothServiceDiscoveryAgentPrivate::~QBluetoothServiceDiscoveryAgentPrivate()
++ {
++ delete manager;
++ }
++
++ void QBluetoothServiceDiscoveryAgentPrivate::start(const QBluetoothAddress &address)
++ {
++ Q_Q(QBluetoothServiceDiscoveryAgent);
++
++ qCDebug(QT_BT_BLUEZ) << "Discovery on: " << address.toString() << "Mode:" << DiscoveryMode();
++
++ if (foundHostAdapterPath.isEmpty()) {
++ // check that we match adapter addresses or use first if it wasn't specified
++
++ bool ok = false;
++ foundHostAdapterPath = findAdapterForAddress(m_deviceAdapterAddress, &ok);
++ if (!ok) {
++ discoveredDevices.clear();
++ error = QBluetoothServiceDiscoveryAgent::InputOutputError;
++ errorString = QBluetoothDeviceDiscoveryAgent::tr("Cannot access adapter during service discovery");
++ emit q->errorOccurred(error);
++ _q_serviceDiscoveryFinished();
++ return;
++ }
++
++ if (foundHostAdapterPath.isEmpty()) {
++ // Cannot find a local adapter
++ // Abort any outstanding discoveries
++ discoveredDevices.clear();
++
++ error = QBluetoothServiceDiscoveryAgent::InvalidBluetoothAdapterError;
++ errorString = QBluetoothServiceDiscoveryAgent::tr("Cannot find local Bluetooth adapter");
++ emit q->errorOccurred(error);
++ _q_serviceDiscoveryFinished();
++
++ return;
++ }
++ }
++
++ // ensure we didn't go offline yet
++ OrgBluezAdapter1Interface adapter(QStringLiteral("org.bluez"),
++ foundHostAdapterPath, QDBusConnection::systemBus());
++ if (!adapter.powered()) {
++ discoveredDevices.clear();
++
++ error = QBluetoothServiceDiscoveryAgent::PoweredOffError;
++ errorString = QBluetoothServiceDiscoveryAgent::tr("Local device is powered off");
++ emit q->errorOccurred(error);
++
++ _q_serviceDiscoveryFinished();
++ return;
++ }
++
++ if (DiscoveryMode() == QBluetoothServiceDiscoveryAgent::MinimalDiscovery) {
++ performMinimalServiceDiscovery(address);
++ } else {
++ runExternalSdpScan(address, QBluetoothAddress(adapter.address()));
++ }
++ }
++
++ /* Bluez 5
++ * src/tools/sdpscanner performs an SDP scan. This is
++ * done out-of-process to avoid license issues. At this stage Bluez uses GPLv2.
++ */
++ void QBluetoothServiceDiscoveryAgentPrivate::runExternalSdpScan(
++ const QBluetoothAddress &remoteAddress, const QBluetoothAddress &localAddress)
++ {
++ Q_Q(QBluetoothServiceDiscoveryAgent);
++
++ if (!sdpScannerProcess) {
++ const QString binPath = QLibraryInfo::paths(QLibraryInfo::LibraryExecutablesPath).value(0);
++ QFileInfo fileInfo(binPath, QStringLiteral("sdpscanner"));
++ if (!fileInfo.exists() || !fileInfo.isExecutable()) {
++ _q_finishSdpScan(QBluetoothServiceDiscoveryAgent::InputOutputError,
++ QBluetoothServiceDiscoveryAgent::tr("Unable to find sdpscanner"),
++ QStringList());
++ qCWarning(QT_BT_BLUEZ) << "Cannot find sdpscanner:"
++ << fileInfo.canonicalFilePath();
++ return;
++ }
++
++ sdpScannerProcess = new QProcess(q);
++ sdpScannerProcess->setReadChannel(QProcess::StandardOutput);
++ if (QT_BT_BLUEZ().isDebugEnabled())
++ sdpScannerProcess->setProcessChannelMode(QProcess::ForwardedErrorChannel);
++ // AXIVION Next Line Qt-Security-QProcessStart: expected behavior
++ sdpScannerProcess->setProgram(fileInfo.canonicalFilePath());
++ q->connect(sdpScannerProcess,
++ QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
++ q, [this](int exitCode, QProcess::ExitStatus status){
++ this->_q_sdpScannerDone(exitCode, status);
++ });
++ }
++
++ QStringList arguments;
++ arguments << remoteAddress.toString() << localAddress.toString();
++
++ // No filter implies PUBLIC_BROWSE_GROUP based SDP scan
++ if (!uuidFilter.isEmpty()) {
++ arguments << QLatin1String("-u"); // cmd line option for list of uuids
++ for (const QBluetoothUuid& uuid : std::as_const(uuidFilter))
++ arguments << uuid.toString();
++ }
++
++ sdpScannerProcess->setArguments(arguments);
++ sdpScannerProcess->start();
++ }
++
++ void QBluetoothServiceDiscoveryAgentPrivate::_q_sdpScannerDone(int exitCode, QProcess::ExitStatus status)
++ {
++ if (status != QProcess::NormalExit || exitCode != 0) {
++ qCWarning(QT_BT_BLUEZ) << "SDP scan failure" << status << exitCode;
++ if (singleDevice) {
++ _q_finishSdpScan(QBluetoothServiceDiscoveryAgent::InputOutputError,
++ QBluetoothServiceDiscoveryAgent::tr("Unable to perform SDP scan"),
++ QStringList());
++ } else {
++ // go to next device
++ _q_finishSdpScan(QBluetoothServiceDiscoveryAgent::NoError, QString(), QStringList());
++ }
++ return;
++ }
++
++ QStringList xmlRecords;
++ const QByteArray utf8Data = QByteArray::fromBase64(sdpScannerProcess->readAllStandardOutput());
++ const QByteArrayView utf8View = utf8Data;
++
++ // split the various xml docs up
++ constexpr auto matcher = qMakeStaticByteArrayMatcher("<?xml");
++ qsizetype next;
++ qsizetype start = matcher.indexIn(utf8View, 0);
++ if (start != -1) {
++ do {
++ next = matcher.indexIn(utf8View, start + 1);
++ if (next != -1)
++ xmlRecords.append(QString::fromUtf8(utf8View.sliced(start, next - start)));
++ else
++ xmlRecords.append(QString::fromUtf8(utf8View.sliced(start)));
++ start = next;
++ } while ( start != -1);
++ }
++
++ _q_finishSdpScan(QBluetoothServiceDiscoveryAgent::NoError, QString(), xmlRecords);
++ }
++
++ void QBluetoothServiceDiscoveryAgentPrivate::_q_finishSdpScan(QBluetoothServiceDiscoveryAgent::Error errorCode,
++ const QString &errorDescription,
++ const QStringList &xmlRecords)
++ {
++ Q_Q(QBluetoothServiceDiscoveryAgent);
++
++ if (errorCode != QBluetoothServiceDiscoveryAgent::NoError) {
++ qCWarning(QT_BT_BLUEZ) << "SDP search failed for"
++ << (!discoveredDevices.isEmpty()
++ ? discoveredDevices.at(0).address().toString()
++ : QStringLiteral("<Unknown>"));
++ // We have an error which we need to indicate and stop further processing
++ discoveredDevices.clear();
++ error = errorCode;
++ errorString = errorDescription;
++ emit q->errorOccurred(error);
++ } else if (!xmlRecords.isEmpty() && discoveryState() != Inactive) {
++ for (const QString &record : xmlRecords) {
++ QBluetoothServiceInfo serviceInfo = parseServiceXml(record);
++
++ //apply uuidFilter
++ if (!uuidFilter.isEmpty()) {
++ bool serviceNameMatched = uuidFilter.contains(serviceInfo.serviceUuid());
++ bool serviceClassMatched = false;
++ const QList<QBluetoothUuid> serviceClassUuids
++ = serviceInfo.serviceClassUuids();
++ for (const QBluetoothUuid &id : serviceClassUuids) {
++ if (uuidFilter.contains(id)) {
++ serviceClassMatched = true;
++ break;
++ }
++ }
++
++ if (!serviceNameMatched && !serviceClassMatched)
++ continue;
++ }
++
++ if (!serviceInfo.isValid())
++ continue;
++
++ // Bluez sdpscanner declares custom uuids into the service class uuid list.
++ // Let's move a potential custom uuid from QBluetoothServiceInfo::serviceClassUuids()
++ // to QBluetoothServiceInfo::serviceUuid(). If there is more than one, just move the first uuid
++ const QList<QBluetoothUuid> serviceClassUuids = serviceInfo.serviceClassUuids();
++ for (const QBluetoothUuid &id : serviceClassUuids) {
++ if (id.minimumSize() == 16) {
++ serviceInfo.setServiceUuid(id);
++ if (serviceInfo.serviceName().isEmpty()) {
++ serviceInfo.setServiceName(
++ QBluetoothServiceDiscoveryAgent::tr("Custom Service"));
++ }
++ QBluetoothServiceInfo::Sequence modSeq =
++ serviceInfo.attribute(QBluetoothServiceInfo::ServiceClassIds).value<QBluetoothServiceInfo::Sequence>();
++ modSeq.removeOne(QVariant::fromValue(id));
++ serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceClassIds, modSeq);
++ break;
++ }
++ }
++
++ if (!isDuplicatedService(serviceInfo)) {
++ discoveredServices.append(serviceInfo);
++ qCDebug(QT_BT_BLUEZ) << "Discovered services" << discoveredDevices.at(0).address().toString()
++ << serviceInfo.serviceName() << serviceInfo.serviceUuid()
++ << ">>>" << serviceInfo.serviceClassUuids();
++ // Use queued connection to allow us finish the service looping; the application
++ // might call stop() when it has detected the service-of-interest.
++ QMetaObject::invokeMethod(q, "serviceDiscovered", Qt::QueuedConnection,
++ Q_ARG(QBluetoothServiceInfo, serviceInfo));
++ }
++ }
++ }
++
++ _q_serviceDiscoveryFinished();
++ }
++
++ void QBluetoothServiceDiscoveryAgentPrivate::stop()
++ {
++ qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO << "Stop called";
++
++ discoveredDevices.clear();
++ setDiscoveryState(Inactive);
++
++ // must happen after discoveredDevices.clear() above to avoid retrigger of next scan
++ // while waitForFinished() is waiting
++ if (sdpScannerProcess) { // Bluez 5
++ if (sdpScannerProcess->state() != QProcess::NotRunning) {
++ sdpScannerProcess->kill();
++ sdpScannerProcess->waitForFinished();
++ }
++ }
++
++ Q_Q(QBluetoothServiceDiscoveryAgent);
++ emit q->canceled();
++ }
++
++ QBluetoothServiceInfo QBluetoothServiceDiscoveryAgentPrivate::parseServiceXml(
++ const QString& xmlRecord)
++ {
++ QXmlStreamReader xml(xmlRecord);
++
++ QBluetoothServiceInfo serviceInfo;
++ serviceInfo.setDevice(discoveredDevices.at(0));
++
++ while (!xml.atEnd()) {
++ xml.readNext();
++
++ if (xml.tokenType() == QXmlStreamReader::StartElement &&
++ xml.name() == QLatin1String("attribute")) {
++ quint16 attributeId =
++ xml.attributes().value(QLatin1String("id")).toUShort(nullptr, 0);
++
++ if (xml.readNextStartElement()) {
++ const QVariant value = readAttributeValue(xml);
++ serviceInfo.setAttribute(attributeId, value);
++ }
++ }
++ }
++
++ return serviceInfo;
++ }
++
++ // Bluez 5
++ void QBluetoothServiceDiscoveryAgentPrivate::performMinimalServiceDiscovery(const QBluetoothAddress &deviceAddress)
++ {
++ if (foundHostAdapterPath.isEmpty()) {
++ _q_serviceDiscoveryFinished();
++ return;
++ }
++
++ Q_Q(QBluetoothServiceDiscoveryAgent);
++
++ QDBusPendingReply<ManagedObjectList> reply = manager->GetManagedObjects();
++ reply.waitForFinished();
++ if (reply.isError()) {
++ if (singleDevice) {
++ error = QBluetoothServiceDiscoveryAgent::InputOutputError;
++ errorString = reply.error().message();
++ emit q->errorOccurred(error);
++ }
++ _q_serviceDiscoveryFinished();
++ return;
++ }
++
++ QStringList uuidStrings;
++
++ ManagedObjectList managedObjectList = reply.value();
++ for (ManagedObjectList::const_iterator it = managedObjectList.constBegin(); it != managedObjectList.constEnd(); ++it) {
++ const InterfaceList &ifaceList = it.value();
++
++ for (InterfaceList::const_iterator jt = ifaceList.constBegin(); jt != ifaceList.constEnd(); ++jt) {
++ const QString &iface = jt.key();
++ const QVariantMap &ifaceValues = jt.value();
++
++ if (iface == QStringLiteral("org.bluez.Device1")) {
++ if (deviceAddress.toString() == ifaceValues.value(QStringLiteral("Address")).toString()) {
++ uuidStrings = ifaceValues.value(QStringLiteral("UUIDs")).toStringList();
++ break;
++ }
++ }
++ }
++ if (!uuidStrings.isEmpty())
++ break;
++ }
++
++ if (uuidStrings.isEmpty() || discoveredDevices.isEmpty()) {
++ qCWarning(QT_BT_BLUEZ) << "No uuids found for" << deviceAddress.toString();
++ // nothing found -> go to next uuid
++ _q_serviceDiscoveryFinished();
++ return;
++ }
++
++ qCDebug(QT_BT_BLUEZ) << "Minimal uuid list for" << deviceAddress.toString() << uuidStrings;
++
++ QBluetoothUuid uuid;
++ for (qsizetype i = 0; i < uuidStrings.size(); ++i) {
++ uuid = QBluetoothUuid(uuidStrings.at(i));
++ if (uuid.isNull())
++ continue;
++
++ //apply uuidFilter
++ if (!uuidFilter.isEmpty() && !uuidFilter.contains(uuid))
++ continue;
++
++ QBluetoothServiceInfo serviceInfo;
++ serviceInfo.setDevice(discoveredDevices.at(0));
++
++ if (uuid.minimumSize() == 16) { // not derived from Bluetooth Base UUID
++ serviceInfo.setServiceUuid(uuid);
++ serviceInfo.setServiceName(QBluetoothServiceDiscoveryAgent::tr("Custom Service"));
++ } else {
++ // set uuid as service class id
++ QBluetoothServiceInfo::Sequence classId;
++ classId << QVariant::fromValue(uuid);
++ serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceClassIds, classId);
++ QBluetoothUuid::ServiceClassUuid clsId
++ = static_cast<QBluetoothUuid::ServiceClassUuid>(uuid.data1 & 0xffff);
++ serviceInfo.setServiceName(QBluetoothUuid::serviceClassToString(clsId));
++ }
++
++ QBluetoothServiceInfo::Sequence protocolDescriptorList;
++ {
++ QBluetoothServiceInfo::Sequence protocol;
++ protocol << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::ProtocolUuid::L2cap));
++ protocolDescriptorList.append(QVariant::fromValue(protocol));
++ }
++ {
++ QBluetoothServiceInfo::Sequence protocol;
++ protocol << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::ProtocolUuid::Att));
++ protocolDescriptorList.append(QVariant::fromValue(protocol));
++ }
++ serviceInfo.setAttribute(QBluetoothServiceInfo::ProtocolDescriptorList, protocolDescriptorList);
++
++ //don't include the service if we already discovered it before
++ if (!isDuplicatedService(serviceInfo)) {
++ discoveredServices << serviceInfo;
++ qCDebug(QT_BT_BLUEZ) << "Discovered services" << discoveredDevices.at(0).address().toString()
++ << serviceInfo.serviceName();
++ emit q->serviceDiscovered(serviceInfo);
++ }
++ }
++
++ _q_serviceDiscoveryFinished();
++ }
++
++ QVariant QBluetoothServiceDiscoveryAgentPrivate::readAttributeValue(QXmlStreamReader &xml,
++ int depth)
++ {
++ auto skippingCurrentElementByDefault = qScopeGuard([&] { xml.skipCurrentElement(); });
++
++ if (depth > kMaxSdpRecursionDepth) {
++ qCWarning(QT_BT_BLUEZ) << "SDP XML attribute recursion depth exceeded";
++ return QVariant();
++ }
++
++ if (xml.name() == QLatin1String("boolean")) {
++ return xml.attributes().value(QLatin1String("value")) == QLatin1String("true");
++ } else if (xml.name() == QLatin1String("uint8")) {
++ quint8 value = xml.attributes().value(QLatin1String("value")).toUShort(nullptr, 0);
++ return value;
++ } else if (xml.name() == QLatin1String("uint16")) {
++ quint16 value = xml.attributes().value(QLatin1String("value")).toUShort(nullptr, 0);
++ return value;
++ } else if (xml.name() == QLatin1String("uint32")) {
++ quint32 value = xml.attributes().value(QLatin1String("value")).toUInt(nullptr, 0);
++ return value;
++ } else if (xml.name() == QLatin1String("uint64")) {
++ quint64 value = xml.attributes().value(QLatin1String("value")).toULongLong(nullptr, 0);
++ return value;
++ } else if (xml.name() == QLatin1String("uuid")) {
++ QBluetoothUuid uuid;
++ const QStringView value = xml.attributes().value(QLatin1String("value"));
++ if (value.startsWith(QLatin1String("0x"))) {
++ if (value.size() == 6) {
++ quint16 v = value.toUShort(nullptr, 0);
++ uuid = QBluetoothUuid(v);
++ } else if (value.size() == 10) {
++ quint32 v = value.toUInt(nullptr, 0);
++ uuid = QBluetoothUuid(v);
++ }
++ } else {
++ uuid = QBluetoothUuid(value.toString());
++ }
++ return QVariant::fromValue(uuid);
++ } else if (xml.name() == QLatin1String("text") || xml.name() == QLatin1String("url")) {
++ const QStringView value = xml.attributes().value(QLatin1String("value"));
++ if (xml.attributes().value(QLatin1String("encoding")) == QLatin1String("hex"))
++ return QString::fromUtf8(QByteArray::fromHex(value.toLatin1()));
++ return value.toString();
++ } else if (xml.name() == QLatin1String("sequence")) {
++ QBluetoothServiceInfo::Sequence sequence;
++
++ skippingCurrentElementByDefault.dismiss(); // we skip several elements here
++
++ while (xml.readNextStartElement()) {
++ QVariant value = readAttributeValue(xml, depth + 1);
++ sequence.append(value);
++ }
++
++ return QVariant::fromValue<QBluetoothServiceInfo::Sequence>(sequence);
++ } else {
++ qCWarning(QT_BT_BLUEZ) << "unknown attribute type"
++ << xml.name()
++ << xml.attributes().value(QLatin1String("value"));
++ return QVariant();
++ }
++ }
++
++ QT_END_NAMESPACE