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 <QtCore/QGlobalStatic>
++ #include <QtCore/QLoggingCategory>
++ #include <QtCore/QMap>
++ #include <QtCore/QVersionNumber>
++ #include <QtNetwork/private/qnet_unix_p.h>
++ #include "bluez5_helper_p.h"
++ #include "bluez_data_p.h"
++ #include "objectmanager_p.h"
++ #include "properties_p.h"
++ #include "adapter1_bluez5_p.h"
++
++ QT_BEGIN_NAMESPACE
++
++ QT_IMPL_METATYPE_EXTERN(InterfaceList)
++ QT_IMPL_METATYPE_EXTERN(ManufacturerDataList)
++ QT_IMPL_METATYPE_EXTERN(ServiceDataList)
++ QT_IMPL_METATYPE_EXTERN(ManagedObjectList)
++
++
++ Q_DECLARE_LOGGING_CATEGORY(QT_BT_BLUEZ)
++
++ using namespace QtBluetoothPrivate; // for D-Bus wrappers
++
++ typedef enum Bluez5TestResultType
++ {
++ BluezVersionUnknown,
++ BluezVersion5,
++ BluezNotAvailable
++ } Bluez5TestResult;
++
++ Q_GLOBAL_STATIC_WITH_ARGS(Bluez5TestResult, bluezVersion, (BluezVersionUnknown))
++ Q_GLOBAL_STATIC_WITH_ARGS(QVersionNumber, bluezDaemonVersion, (QVersionNumber()))
++
++ /*
++ Ensures that the DBus types are registered
++ */
++
++ void initializeBluez5()
++ {
++ if (*bluezVersion() == BluezVersionUnknown) {
++ OrgFreedesktopDBusObjectManagerInterface manager(QStringLiteral("org.bluez"),
++ QStringLiteral("/"),
++ QDBusConnection::systemBus());
++ qDBusRegisterMetaType<InterfaceList>();
++ qDBusRegisterMetaType<ManagedObjectList>();
++ qDBusRegisterMetaType<ManufacturerDataList>();
++ qDBusRegisterMetaType<ServiceDataList>();
++
++ QDBusPendingReply<ManagedObjectList> reply = manager.GetManagedObjects();
++ reply.waitForFinished();
++ if (reply.isError()) {
++ *bluezVersion() = BluezNotAvailable;
++ qWarning() << "Cannot find a compatible running Bluez. "
++ "Please check the Bluez installation. "
++ "QtBluetooth requires at least BlueZ version 5.";
++ } else {
++ *bluezVersion() = BluezVersion5;
++ qCDebug(QT_BT_BLUEZ) << "Bluez 5 detected.";
++ }
++ }
++ }
++
++ /*
++ Checks that the mandatory Bluetooth HCI ioctls are offered
++ by Linux kernel. Returns \c true if the ictls are available; otherwise \c false.
++
++ Mandatory ioctls:
++ - HCIGETCONNLIST
++ - HCIGETDEVINFO
++ - HCIGETDEVLIST
++ */
++ bool mandatoryHciIoctlsAvailable()
++ {
++ // open hci socket
++ int hciSocket = ::qt_safe_socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI);
++ if (hciSocket < 0) {
++ qCWarning(QT_BT_BLUEZ) << "Cannot open HCI socket:" << qt_error_string(errno);
++ return false;
++ }
++
++ // check HCIGETDEVLIST & HCIGETDEVLIST
++ struct hci_dev_req *devRequest = nullptr;
++ struct hci_dev_list_req *devRequestList = nullptr;
++ struct hci_dev_info devInfo;
++ const int devListSize = sizeof(struct hci_dev_list_req)
++ + HCI_MAX_DEV * sizeof(struct hci_dev_req);
++
++ devRequestList = (hci_dev_list_req *) malloc(devListSize);
++ if (!devRequestList) {
++ qt_safe_close(hciSocket);
++ return false; // if we cannot malloc nothing will help anyway
++ }
++
++ QScopedPointer<hci_dev_list_req, QScopedPointerPodDeleter> pDevList(devRequestList);
++ memset(pDevList.data(), 0, devListSize);
++ pDevList->dev_num = HCI_MAX_DEV;
++ devRequest = pDevList->dev_req;
++
++ if (qt_safe_ioctl(hciSocket, HCIGETDEVLIST, devRequestList) < 0) {
++ qt_safe_close(hciSocket);
++ qCWarning(QT_BT_BLUEZ) << "HCI icotl HCIGETDEVLIST:" << qt_error_string(errno);
++ return false;
++ }
++
++ if (devRequestList->dev_num > 0) {
++ devInfo.dev_id = devRequest->dev_id;
++ if (qt_safe_ioctl(hciSocket, HCIGETDEVINFO, &devInfo) < 0) {
++ qt_safe_close(hciSocket);
++ qCWarning(QT_BT_BLUEZ) << "HCI icotl HCIGETDEVINFO:" << qt_error_string(errno);
++ return false;
++ }
++ }
++
++ // check HCIGETCONNLIST
++ const int maxNoOfConnections = 20;
++ hci_conn_list_req *infoList = nullptr;
++ infoList = (hci_conn_list_req *)
++ malloc(sizeof(hci_conn_list_req) + maxNoOfConnections * sizeof(hci_conn_info));
++
++ if (!infoList) {
++ qt_safe_close(hciSocket);
++ return false;
++ }
++
++ QScopedPointer<hci_conn_list_req, QScopedPointerPodDeleter> pInfoList(infoList);
++ pInfoList->conn_num = maxNoOfConnections;
++ pInfoList->dev_id = devInfo.dev_id;
++
++ if (qt_safe_ioctl(hciSocket, HCIGETCONNLIST, (void *) infoList) < 0) {
++ qCWarning(QT_BT_BLUEZ) << "HCI icotl HCIGETCONNLIST:" << qt_error_string(errno);
++ qt_safe_close(hciSocket);
++ return false;
++ }
++
++ qt_safe_close(hciSocket);
++ return true;
++ }
++
++ /*
++ * This function returns the version of bluetoothd in use on the system.
++ * This is required to determine which QLEControllerPrivate implementation
++ * is required. The following version tags are of significance:
++ *
++ * Version < 4.0 -> QLEControllerPrivateCommon
++ * Version < 5.42 -> QLEControllerPrivateBluez
++ * Version >= 5.42 -> QLEControllerPrivateBluezDBus
++ *
++ * This function utilizes a singleton pattern. It always returns a cached
++ * version tag which is determined on first call. This is necessary to
++ * avoid continuesly running the somewhat expensive tests.
++ *
++ * The function must never return a null QVersionNumber.
++ */
++ QVersionNumber bluetoothdVersion()
++ {
++ if (bluezDaemonVersion()->isNull()) {
++ // Register DBus specific meta types (copied from isBluez5())
++ // Not all code paths run through isBluez5() but still need the
++ // registration.
++ qDBusRegisterMetaType<InterfaceList>();
++ qDBusRegisterMetaType<ManagedObjectList>();
++ qDBusRegisterMetaType<ManufacturerDataList>();
++ qDBusRegisterMetaType<ServiceDataList>();
++
++ qCDebug(QT_BT_BLUEZ) << "Detecting bluetoothd version";
++ //Order of matching
++ // 1. Pick whatever the user decides via BLUETOOTH_FORCE_DBUS_LE_VERSION
++ // Set version to below version 5.42 to use custom/old GATT stack implementation
++ const QString version = qEnvironmentVariable("BLUETOOTH_FORCE_DBUS_LE_VERSION");
++ if (!version.isNull()) {
++ const QVersionNumber vn = QVersionNumber::fromString(version);
++ if (!vn.isNull()) {
++ *bluezDaemonVersion() = vn;
++ qCDebug(QT_BT_BLUEZ) << "Forcing Bluez LE API selection:"
++ << bluezDaemonVersion()->toString();
++ }
++ }
++
++ // 2. Find bluetoothd binary and check "bluetoothd --version"
++ if (bluezDaemonVersion()->isNull() && qt_haveLinuxProcfs()) {
++ QDBusConnection session = QDBusConnection::systemBus();
++ qint64 pid = session.interface()->servicePid(QStringLiteral("org.bluez")).value();
++ QByteArray buffer;
++
++ auto determineBinaryVersion = [](const QString &binary) -> QVersionNumber {
++ QProcess process;
++ // AXIVION Next Line Qt-Security-QProcessStart: expected behavior
++ process.start(binary, {QStringLiteral("--version")});
++ process.waitForFinished();
++
++ const QString version = QString::fromLocal8Bit(process.readAll());
++ const QVersionNumber vn = QVersionNumber::fromString(version);
++ if (!vn.isNull())
++ qCDebug(QT_BT_BLUEZ) << "Detected bluetoothd version" << vn;
++ return vn;
++ };
++
++ //try reading /proc/<pid>/exe first -> requires process owner
++ qCDebug(QT_BT_BLUEZ) << "Using /proc/<pid>/exe";
++ const QString procExe = QStringLiteral("/proc/%1/exe").arg(pid);
++ const QVersionNumber vn = determineBinaryVersion(procExe);
++ if (!vn.isNull())
++ *bluezDaemonVersion() = vn;
++
++ if (bluezDaemonVersion()->isNull()) {
++ qCDebug(QT_BT_BLUEZ) << "Using /proc/<pid>/cmdline";
++ //try to reading /proc/<pid>/cmdline (does not require additional process rights)
++ QFile procFile(QStringLiteral("/proc/%1/cmdline").arg(pid));
++ if (procFile.open(QIODevice::ReadOnly|QIODevice::Text)) {
++ buffer = procFile.readAll();
++ procFile.close();
++
++ // cmdline params separated by character \0 -> first is bluetoothd binary
++ const QString binary = QString::fromLocal8Bit(buffer.split('\0').at(0));
++ QFileInfo info(binary);
++ if (info.isExecutable())
++ *bluezDaemonVersion() = determineBinaryVersion(binary);
++ else
++ qCDebug(QT_BT_BLUEZ) << "Cannot determine bluetoothd version via cmdline:"
++ << binary;
++ }
++ }
++ }
++
++ // 3. Fall back to custom ATT backend, if possible?
++ if (bluezDaemonVersion()->isNull()) {
++ // Check mandatory HCI ioctls are available
++ if (mandatoryHciIoctlsAvailable()) {
++ // default 4.0 for now -> implies custom (G)ATT implementation
++ *bluezDaemonVersion() = QVersionNumber(4, 0);
++ }
++ }
++
++ // 4. Ultimate fallback -> enable dummy backend
++ if (bluezDaemonVersion()->isNull()) {
++ // version 3 represents disabled BTLE
++ // bluezDaemonVersion should not be null to avoid repeated version tests
++ *bluezDaemonVersion() = QVersionNumber(3, 0);
++ qCWarning(QT_BT_BLUEZ) << "Cannot determine bluetoothd version and required Bluetooth HCI ioctols";
++ qCWarning(QT_BT_BLUEZ) << "Disabling Qt Bluetooth LE feature";
++ }
++
++ qCDebug(QT_BT_BLUEZ) << "Bluetoothd:" << bluezDaemonVersion()->toString();
++ }
++
++ Q_ASSERT(!bluezDaemonVersion()->isNull());
++ return *bluezDaemonVersion();
++ }
++
++ struct AdapterData
++ {
++ public:
++ AdapterData() : reference(1), wasListeningAlready(false) {}
++
++ int reference;
++ bool wasListeningAlready;
++ OrgFreedesktopDBusPropertiesInterface *propteryListener = nullptr;
++ };
++
++ class QtBluezDiscoveryManagerPrivate
++ {
++ public:
++ QMap<QString, AdapterData *> references;
++ OrgFreedesktopDBusObjectManagerInterface *manager = nullptr;
++ };
++
++ Q_GLOBAL_STATIC(QtBluezDiscoveryManager, discoveryManager)
++
++ /*!
++ \internal
++ \class QtBluezDiscoveryManager
++
++ This class manages the access to "org.bluez.Adapter1::Start/StopDiscovery.
++
++ The flag is a system flag. We want to ensure that the various Qt classes don't turn
++ the flag on and off and thereby get into their way. If some other system component
++ changes the flag (e.g. adapter removed) we notify all Qt classes about the change by emitting
++ \l discoveryInterrupted(QString). Classes should indicate this via an appropriate
++ error message to the user.
++
++ Once the signal was emitted, all existing requests for discovery mode on the same adapter
++ have to be renewed via \l registerDiscoveryInterest(QString).
++ */
++
++ QtBluezDiscoveryManager::QtBluezDiscoveryManager(QObject *parent) :
++ QObject(parent)
++ {
++ qCDebug(QT_BT_BLUEZ) << "Creating QtBluezDiscoveryManager";
++ d = new QtBluezDiscoveryManagerPrivate();
++
++ d->manager = new OrgFreedesktopDBusObjectManagerInterface(
++ QStringLiteral("org.bluez"), QStringLiteral("/"),
++ QDBusConnection::systemBus(), this);
++ connect(d->manager, SIGNAL(InterfacesRemoved(QDBusObjectPath,QStringList)),
++ SLOT(InterfacesRemoved(QDBusObjectPath,QStringList)));
++ }
++
++ QtBluezDiscoveryManager::~QtBluezDiscoveryManager()
++ {
++ qCDebug(QT_BT_BLUEZ) << "Destroying QtBluezDiscoveryManager";
++
++ const QList<QString> adapterPaths = d->references.keys();
++ for (const QString &adapterPath : adapterPaths) {
++ AdapterData *data = d->references.take(adapterPath);
++ delete data->propteryListener;
++
++ // turn discovery off if it wasn't on already
++ if (!data->wasListeningAlready) {
++ OrgBluezAdapter1Interface iface(QStringLiteral("org.bluez"), adapterPath,
++ QDBusConnection::systemBus());
++ iface.StopDiscovery();
++ }
++
++ delete data;
++ }
++
++ delete d;
++ }
++
++ QtBluezDiscoveryManager *QtBluezDiscoveryManager::instance()
++ {
++ return discoveryManager();
++ }
++
++ bool QtBluezDiscoveryManager::registerDiscoveryInterest(const QString &adapterPath)
++ {
++ if (adapterPath.isEmpty())
++ return false;
++
++ // already monitored adapter? -> increase ref count -> done
++ if (d->references.contains(adapterPath)) {
++ d->references[adapterPath]->reference++;
++ return true;
++ }
++
++ AdapterData *data = new AdapterData();
++
++ OrgFreedesktopDBusPropertiesInterface *propIface = new OrgFreedesktopDBusPropertiesInterface(
++ QStringLiteral("org.bluez"), adapterPath, QDBusConnection::systemBus());
++ connect(propIface, &OrgFreedesktopDBusPropertiesInterface::PropertiesChanged,
++ this, &QtBluezDiscoveryManager::PropertiesChanged);
++ data->propteryListener = propIface;
++
++ OrgBluezAdapter1Interface iface(QStringLiteral("org.bluez"), adapterPath,
++ QDBusConnection::systemBus());
++ data->wasListeningAlready = iface.discovering();
++
++ d->references[adapterPath] = data;
++
++ if (!data->wasListeningAlready)
++ iface.StartDiscovery();
++
++ return true;
++ }
++
++ void QtBluezDiscoveryManager::unregisterDiscoveryInterest(const QString &adapterPath)
++ {
++ if (!d->references.contains(adapterPath))
++ return;
++
++ AdapterData *data = d->references[adapterPath];
++ data->reference--;
++
++ if (data->reference > 0) // more than one client requested discovery mode
++ return;
++
++ d->references.remove(adapterPath);
++ if (!data->wasListeningAlready) { // Qt turned discovery mode on, Qt has to turn it off again
++ OrgBluezAdapter1Interface iface(QStringLiteral("org.bluez"), adapterPath,
++ QDBusConnection::systemBus());
++ iface.StopDiscovery();
++ }
++
++ delete data->propteryListener;
++ delete data;
++ }
++
++ //void QtBluezDiscoveryManager::dumpState() const
++ //{
++ // qCDebug(QT_BT_BLUEZ) << "-------------------------";
++ // if (d->references.isEmpty()) {
++ // qCDebug(QT_BT_BLUEZ) << "No running registration";
++ // } else {
++ // const QList<QString> paths = d->references.keys();
++ // for (const QString &path : paths) {
++ // qCDebug(QT_BT_BLUEZ) << path << "->" << d->references[path]->reference;
++ // }
++ // }
++ // qCDebug(QT_BT_BLUEZ) << "-------------------------";
++ //}
++
++ void QtBluezDiscoveryManager::InterfacesRemoved(const QDBusObjectPath &object_path,
++ const QStringList &interfaces)
++ {
++ if (!d->references.contains(object_path.path())
++ || !interfaces.contains(QStringLiteral("org.bluez.Adapter1")))
++ return;
++
++ removeAdapterFromMonitoring(object_path.path());
++ }
++
++ void QtBluezDiscoveryManager::PropertiesChanged(const QString &interface,
++ const QVariantMap &changed_properties,
++ const QStringList &invalidated_properties,
++ const QDBusMessage &)
++ {
++ Q_UNUSED(invalidated_properties);
++
++ OrgFreedesktopDBusPropertiesInterface *propIface =
++ qobject_cast<OrgFreedesktopDBusPropertiesInterface *>(sender());
++
++ if (!propIface)
++ return;
++
++ if (interface == QStringLiteral("org.bluez.Adapter1")
++ && d->references.contains(propIface->path())
++ && changed_properties.contains(QStringLiteral("Discovering"))) {
++ bool isDiscovering = changed_properties.value(QStringLiteral("Discovering")).toBool();
++ if (!isDiscovering) {
++
++ /*
++ Once we stop the Discovering flag will switch a few ms later. This comes through this code
++ path. If a new device discovery is started while we are still
++ waiting for the flag change signal, then the new device discovery will be aborted prematurely.
++ To compensate we check whether there was renewed interest.
++ */
++
++ AdapterData *data = d->references[propIface->path()];
++ if (!data) {
++ removeAdapterFromMonitoring(propIface->path());
++ } else {
++ OrgBluezAdapter1Interface iface(QStringLiteral("org.bluez"), propIface->path(),
++ QDBusConnection::systemBus());
++ iface.StartDiscovery();
++ }
++ }
++ }
++ }
++
++ void QtBluezDiscoveryManager::removeAdapterFromMonitoring(const QString &dbusPath)
++ {
++ // remove adapter from monitoring
++ AdapterData *data = d->references.take(dbusPath);
++ delete data->propteryListener;
++ delete data;
++
++ emit discoveryInterrupted(dbusPath);
++ }
++
++ /*
++ Finds the path for the local adapter with \a wantedAddress or returns an
++ empty string if no local adapter with the given address can be found.
++
++ If \a wantedAddress is \c null it returns the first powered-on adapter, or
++ the first adapter if all of them are powered off, or an empty string if
++ none is available.
++
++ If \a ok is false the lookup was aborted due to a dbus error and this function
++ returns an empty string.
++ */
++ QString findAdapterForAddress(const QBluetoothAddress &wantedAddress, bool *ok = nullptr)
++ {
++ OrgFreedesktopDBusObjectManagerInterface manager(QStringLiteral("org.bluez"),
++ QStringLiteral("/"),
++ QDBusConnection::systemBus());
++
++ QDBusPendingReply<ManagedObjectList> reply = manager.GetManagedObjects();
++ reply.waitForFinished();
++ if (reply.isError()) {
++ if (ok)
++ *ok = false;
++
++ return QString();
++ }
++
++ struct AdapterInfo
++ {
++ QString path;
++ QBluetoothAddress address;
++ bool powered;
++ };
++ QList<AdapterInfo> localAdapters;
++
++ ManagedObjectList managedObjectList = reply.value();
++ for (ManagedObjectList::const_iterator it = managedObjectList.constBegin(); it != managedObjectList.constEnd(); ++it) {
++ const QDBusObjectPath &path = it.key();
++ const InterfaceList &ifaceList = it.value();
++
++ for (InterfaceList::const_iterator jt = ifaceList.constBegin(); jt != ifaceList.constEnd(); ++jt) {
++ const QString &iface = jt.key();
++
++ if (iface == QStringLiteral("org.bluez.Adapter1")) {
++ AdapterInfo info;
++ info.path = path.path();
++ info.address = QBluetoothAddress(ifaceList.value(iface).value(
++ QStringLiteral("Address")).toString());
++ info.powered = ifaceList.value(iface).value(QStringLiteral("Powered")).toBool();
++ if (!info.address.isNull())
++ localAdapters.append(info);
++ break;
++ }
++ }
++ }
++
++ if (ok)
++ *ok = true;
++
++ if (localAdapters.isEmpty())
++ return QString(); // -> no local adapter found
++
++ auto findFirstPowered = [&localAdapters]() {
++ Q_ASSERT(!localAdapters.isEmpty());
++ auto it = std::find_if(localAdapters.cbegin(), localAdapters.cend(),
++ [](const AdapterInfo &info) {
++ return info.powered == true;
++ });
++ if (it != localAdapters.cend())
++ return it->path;
++ // simply return first if none is powered
++ return localAdapters.cbegin()->path;
++ };
++
++ if (wantedAddress.isNull())
++ return findFirstPowered();
++
++ for (const AdapterInfo &info : std::as_const(localAdapters)) {
++ if (info.address == wantedAddress)
++ return info.path; // -> found local adapter with wanted address
++ }
++
++ return QString(); // nothing matching found
++ }
++
++ /*
++
++ Checks the presence of peripheral interface and returns path to the adapter
++
++ */
++
++ QString adapterWithDBusPeripheralInterface(const QBluetoothAddress &localAddress)
++ {
++ initializeBluez5();
++
++ // First find the object path to the desired adapter
++ bool ok = false;
++ const QString hostAdapterPath = findAdapterForAddress(localAddress, &ok);
++ if (!ok || hostAdapterPath.isEmpty())
++ return {};
++
++ // Then check if that adapter provides peripheral dbus interface
++ OrgFreedesktopDBusObjectManagerInterface manager(QStringLiteral("org.bluez"),
++ QStringLiteral("/"),
++ QDBusConnection::systemBus());
++ QDBusPendingReply<ManagedObjectList> reply = manager.GetManagedObjects();
++ reply.waitForFinished();
++ if (reply.isError())
++ return {};
++
++ using namespace Qt::StringLiterals;
++ // For example /org/bluez/hci0 contains org.bluezLEAdvertisingManager1
++ const bool peripheralSupported = reply.value()
++ .value(QDBusObjectPath(hostAdapterPath))
++ .contains("org.bluez.LEAdvertisingManager1"_L1);
++
++ qCDebug(QT_BT_BLUEZ) << "Peripheral role"
++ << (peripheralSupported ? "" : "not")
++ << "supported on" << hostAdapterPath;
++ return peripheralSupported ? hostAdapterPath : QString{};
++ }
++
++ /*
++ Removes every character that cannot be used in QDbusObjectPath
++
++ See QDbusUtil::isValidObjectPath(QString) for more details.
++ */
++ QString sanitizeNameForDBus(const QString &text)
++ {
++ QString appName = text;
++ for (qsizetype i = 0; i < appName.size(); ++i) {
++ ushort us = appName[i].unicode();
++ bool valid = (us >= 'a' && us <= 'z')
++ || (us >= 'A' && us <= 'Z')
++ || (us >= '0' && us <= '9')
++ || (us == '_');
++
++ if (!valid)
++ appName[i] = QLatin1Char('_');
++ }
++
++ return appName;
++ }
++
++ QT_END_NAMESPACE
++
++ #include "moc_bluez5_helper_p.cpp"