Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
bluez5_helper.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:critical reason:execute-external-code
4
5#include <QtCore/QGlobalStatic>
6#include <QtCore/QLoggingCategory>
7#include <QtCore/QMap>
8#include <QtCore/QVersionNumber>
9#include <QtNetwork/private/qnet_unix_p.h>
11#include "bluez_data_p.h"
13#include "properties_p.h"
15
16QT_BEGIN_NAMESPACE
17
18QT_IMPL_METATYPE_EXTERN(InterfaceList)
19QT_IMPL_METATYPE_EXTERN(ManufacturerDataList)
20QT_IMPL_METATYPE_EXTERN(ServiceDataList)
21QT_IMPL_METATYPE_EXTERN(ManagedObjectList)
22
23
24Q_DECLARE_LOGGING_CATEGORY(QT_BT_BLUEZ)
25
26using namespace QtBluetoothPrivate; // for D-Bus wrappers
27
34
35Q_GLOBAL_STATIC_WITH_ARGS(Bluez5TestResult, bluezVersion, (BluezVersionUnknown))
36Q_GLOBAL_STATIC_WITH_ARGS(QVersionNumber, bluezDaemonVersion, (QVersionNumber()))
37
38/*
39 Ensures that the DBus types are registered
40 */
41
42void initializeBluez5()
43{
44 if (*bluezVersion() == BluezVersionUnknown) {
45 OrgFreedesktopDBusObjectManagerInterface manager(QStringLiteral("org.bluez"),
46 QStringLiteral("/"),
47 QDBusConnection::systemBus());
48 qDBusRegisterMetaType<InterfaceList>();
49 qDBusRegisterMetaType<ManagedObjectList>();
50 qDBusRegisterMetaType<ManufacturerDataList>();
51 qDBusRegisterMetaType<ServiceDataList>();
52
53 QDBusPendingReply<ManagedObjectList> reply = manager.GetManagedObjects();
54 reply.waitForFinished();
55 if (reply.isError()) {
56 *bluezVersion() = BluezNotAvailable;
57 qWarning() << "Cannot find a compatible running Bluez. "
58 "Please check the Bluez installation. "
59 "QtBluetooth requires at least BlueZ version 5.";
60 } else {
61 *bluezVersion() = BluezVersion5;
62 qCDebug(QT_BT_BLUEZ) << "Bluez 5 detected.";
63 }
64 }
65}
66
67/*
68 Checks that the mandatory Bluetooth HCI ioctls are offered
69 by Linux kernel. Returns \c true if the ictls are available; otherwise \c false.
70
71 Mandatory ioctls:
72 - HCIGETCONNLIST
73 - HCIGETDEVINFO
74 - HCIGETDEVLIST
75 */
76bool mandatoryHciIoctlsAvailable()
77{
78 // open hci socket
79 int hciSocket = ::qt_safe_socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI);
80 if (hciSocket < 0) {
81 qCWarning(QT_BT_BLUEZ) << "Cannot open HCI socket:" << qt_error_string(errno);
82 return false;
83 }
84
85 // check HCIGETDEVLIST & HCIGETDEVLIST
86 struct hci_dev_req *devRequest = nullptr;
87 struct hci_dev_list_req *devRequestList = nullptr;
88 struct hci_dev_info devInfo;
89 const int devListSize = sizeof(struct hci_dev_list_req)
90 + HCI_MAX_DEV * sizeof(struct hci_dev_req);
91
92 devRequestList = (hci_dev_list_req *) malloc(devListSize);
93 if (!devRequestList) {
94 qt_safe_close(hciSocket);
95 return false; // if we cannot malloc nothing will help anyway
96 }
97
98 QScopedPointer<hci_dev_list_req, QScopedPointerPodDeleter> pDevList(devRequestList);
99 memset(pDevList.data(), 0, devListSize);
100 pDevList->dev_num = HCI_MAX_DEV;
101 devRequest = pDevList->dev_req;
102
103 if (qt_safe_ioctl(hciSocket, HCIGETDEVLIST, devRequestList) < 0) {
104 qt_safe_close(hciSocket);
105 qCWarning(QT_BT_BLUEZ) << "HCI icotl HCIGETDEVLIST:" << qt_error_string(errno);
106 return false;
107 }
108
109 if (devRequestList->dev_num > 0) {
110 devInfo.dev_id = devRequest->dev_id;
111 if (qt_safe_ioctl(hciSocket, HCIGETDEVINFO, &devInfo) < 0) {
112 qt_safe_close(hciSocket);
113 qCWarning(QT_BT_BLUEZ) << "HCI icotl HCIGETDEVINFO:" << qt_error_string(errno);
114 return false;
115 }
116 }
117
118 // check HCIGETCONNLIST
119 const int maxNoOfConnections = 20;
120 hci_conn_list_req *infoList = nullptr;
121 infoList = (hci_conn_list_req *)
122 malloc(sizeof(hci_conn_list_req) + maxNoOfConnections * sizeof(hci_conn_info));
123
124 if (!infoList) {
125 qt_safe_close(hciSocket);
126 return false;
127 }
128
129 QScopedPointer<hci_conn_list_req, QScopedPointerPodDeleter> pInfoList(infoList);
130 pInfoList->conn_num = maxNoOfConnections;
131 pInfoList->dev_id = devInfo.dev_id;
132
133 if (qt_safe_ioctl(hciSocket, HCIGETCONNLIST, (void *) infoList) < 0) {
134 qCWarning(QT_BT_BLUEZ) << "HCI icotl HCIGETCONNLIST:" << qt_error_string(errno);
135 qt_safe_close(hciSocket);
136 return false;
137 }
138
139 qt_safe_close(hciSocket);
140 return true;
141}
142
143/*
144 * This function returns the version of bluetoothd in use on the system.
145 * This is required to determine which QLEControllerPrivate implementation
146 * is required. The following version tags are of significance:
147 *
148 * Version < 4.0 -> QLEControllerPrivateCommon
149 * Version < 5.42 -> QLEControllerPrivateBluez
150 * Version >= 5.42 -> QLEControllerPrivateBluezDBus
151 *
152 * This function utilizes a singleton pattern. It always returns a cached
153 * version tag which is determined on first call. This is necessary to
154 * avoid continuesly running the somewhat expensive tests.
155 *
156 * The function must never return a null QVersionNumber.
157 */
158QVersionNumber bluetoothdVersion()
159{
160 if (bluezDaemonVersion()->isNull()) {
161 // Register DBus specific meta types (copied from isBluez5())
162 // Not all code paths run through isBluez5() but still need the
163 // registration.
164 qDBusRegisterMetaType<InterfaceList>();
165 qDBusRegisterMetaType<ManagedObjectList>();
166 qDBusRegisterMetaType<ManufacturerDataList>();
167 qDBusRegisterMetaType<ServiceDataList>();
168
169 qCDebug(QT_BT_BLUEZ) << "Detecting bluetoothd version";
170 //Order of matching
171 // 1. Pick whatever the user decides via BLUETOOTH_FORCE_DBUS_LE_VERSION
172 // Set version to below version 5.42 to use custom/old GATT stack implementation
173 const QString version = qEnvironmentVariable("BLUETOOTH_FORCE_DBUS_LE_VERSION");
174 if (!version.isNull()) {
175 const QVersionNumber vn = QVersionNumber::fromString(version);
176 if (!vn.isNull()) {
177 *bluezDaemonVersion() = vn;
178 qCDebug(QT_BT_BLUEZ) << "Forcing Bluez LE API selection:"
179 << bluezDaemonVersion()->toString();
180 }
181 }
182
183 // 2. Find bluetoothd binary and check "bluetoothd --version"
184 if (bluezDaemonVersion()->isNull() && qt_haveLinuxProcfs()) {
185 QDBusConnection session = QDBusConnection::systemBus();
186 qint64 pid = session.interface()->servicePid(QStringLiteral("org.bluez")).value();
187 QByteArray buffer;
188
189 auto determineBinaryVersion = [](const QString &binary) -> QVersionNumber {
190 QProcess process;
191 // AXIVION Next Line Qt-Security-QProcessStart: expected behavior
192 process.start(binary, {QStringLiteral("--version")});
193 process.waitForFinished();
194
195 const QString version = QString::fromLocal8Bit(process.readAll());
196 const QVersionNumber vn = QVersionNumber::fromString(version);
197 if (!vn.isNull())
198 qCDebug(QT_BT_BLUEZ) << "Detected bluetoothd version" << vn;
199 return vn;
200 };
201
202 //try reading /proc/<pid>/exe first -> requires process owner
203 qCDebug(QT_BT_BLUEZ) << "Using /proc/<pid>/exe";
204 const QString procExe = QStringLiteral("/proc/%1/exe").arg(pid);
205 const QVersionNumber vn = determineBinaryVersion(procExe);
206 if (!vn.isNull())
207 *bluezDaemonVersion() = vn;
208
209 if (bluezDaemonVersion()->isNull()) {
210 qCDebug(QT_BT_BLUEZ) << "Using /proc/<pid>/cmdline";
211 //try to reading /proc/<pid>/cmdline (does not require additional process rights)
212 QFile procFile(QStringLiteral("/proc/%1/cmdline").arg(pid));
213 if (procFile.open(QIODevice::ReadOnly|QIODevice::Text)) {
214 buffer = procFile.readAll();
215 procFile.close();
216
217 // cmdline params separated by character \0 -> first is bluetoothd binary
218 const QString binary = QString::fromLocal8Bit(buffer.split('\0').at(0));
219 QFileInfo info(binary);
220 if (info.isExecutable())
221 *bluezDaemonVersion() = determineBinaryVersion(binary);
222 else
223 qCDebug(QT_BT_BLUEZ) << "Cannot determine bluetoothd version via cmdline:"
224 << binary;
225 }
226 }
227 }
228
229 // 3. Fall back to custom ATT backend, if possible?
230 if (bluezDaemonVersion()->isNull()) {
231 // Check mandatory HCI ioctls are available
232 if (mandatoryHciIoctlsAvailable()) {
233 // default 4.0 for now -> implies custom (G)ATT implementation
234 *bluezDaemonVersion() = QVersionNumber(4, 0);
235 }
236 }
237
238 // 4. Ultimate fallback -> enable dummy backend
239 if (bluezDaemonVersion()->isNull()) {
240 // version 3 represents disabled BTLE
241 // bluezDaemonVersion should not be null to avoid repeated version tests
242 *bluezDaemonVersion() = QVersionNumber(3, 0);
243 qCWarning(QT_BT_BLUEZ) << "Cannot determine bluetoothd version and required Bluetooth HCI ioctols";
244 qCWarning(QT_BT_BLUEZ) << "Disabling Qt Bluetooth LE feature";
245 }
246
247 qCDebug(QT_BT_BLUEZ) << "Bluetoothd:" << bluezDaemonVersion()->toString();
248 }
249
250 Q_ASSERT(!bluezDaemonVersion()->isNull());
251 return *bluezDaemonVersion();
252}
253
254struct AdapterData
255{
256public:
257 AdapterData() : reference(1), wasListeningAlready(false) {}
258
259 int reference;
260 bool wasListeningAlready;
261 OrgFreedesktopDBusPropertiesInterface *propteryListener = nullptr;
262};
263
270
271Q_GLOBAL_STATIC(QtBluezDiscoveryManager, discoveryManager)
272
273/*!
274 \internal
275 \class QtBluezDiscoveryManager
276
277 This class manages the access to "org.bluez.Adapter1::Start/StopDiscovery.
278
279 The flag is a system flag. We want to ensure that the various Qt classes don't turn
280 the flag on and off and thereby get into their way. If some other system component
281 changes the flag (e.g. adapter removed) we notify all Qt classes about the change by emitting
282 \l discoveryInterrupted(QString). Classes should indicate this via an appropriate
283 error message to the user.
284
285 Once the signal was emitted, all existing requests for discovery mode on the same adapter
286 have to be renewed via \l registerDiscoveryInterest(QString).
287*/
288
291{
292 qCDebug(QT_BT_BLUEZ) << "Creating QtBluezDiscoveryManager";
294
295 d->manager = new OrgFreedesktopDBusObjectManagerInterface(
296 QStringLiteral("org.bluez"), QStringLiteral("/"),
297 QDBusConnection::systemBus(), this);
298 connect(d->manager, SIGNAL(InterfacesRemoved(QDBusObjectPath,QStringList)),
299 SLOT(InterfacesRemoved(QDBusObjectPath,QStringList)));
300}
301
303{
304 qCDebug(QT_BT_BLUEZ) << "Destroying QtBluezDiscoveryManager";
305
306 const QList<QString> adapterPaths = d->references.keys();
307 for (const QString &adapterPath : adapterPaths) {
308 AdapterData *data = d->references.take(adapterPath);
309 delete data->propteryListener;
310
311 // turn discovery off if it wasn't on already
312 if (!data->wasListeningAlready) {
313 OrgBluezAdapter1Interface iface(QStringLiteral("org.bluez"), adapterPath,
314 QDBusConnection::systemBus());
315 iface.StopDiscovery();
316 }
317
318 delete data;
319 }
320
321 delete d;
322}
323
325{
326 return discoveryManager();
327}
328
329bool QtBluezDiscoveryManager::registerDiscoveryInterest(const QString &adapterPath)
330{
331 if (adapterPath.isEmpty())
332 return false;
333
334 // already monitored adapter? -> increase ref count -> done
335 if (d->references.contains(adapterPath)) {
336 d->references[adapterPath]->reference++;
337 return true;
338 }
339
340 AdapterData *data = new AdapterData();
341
342 OrgFreedesktopDBusPropertiesInterface *propIface = new OrgFreedesktopDBusPropertiesInterface(
343 QStringLiteral("org.bluez"), adapterPath, QDBusConnection::systemBus());
344 connect(propIface, &OrgFreedesktopDBusPropertiesInterface::PropertiesChanged,
345 this, &QtBluezDiscoveryManager::PropertiesChanged);
346 data->propteryListener = propIface;
347
348 OrgBluezAdapter1Interface iface(QStringLiteral("org.bluez"), adapterPath,
349 QDBusConnection::systemBus());
350 data->wasListeningAlready = iface.discovering();
351
352 d->references[adapterPath] = data;
353
354 if (!data->wasListeningAlready)
355 iface.StartDiscovery();
356
357 return true;
358}
359
361{
362 if (!d->references.contains(adapterPath))
363 return;
364
365 AdapterData *data = d->references[adapterPath];
366 data->reference--;
367
368 if (data->reference > 0) // more than one client requested discovery mode
369 return;
370
371 d->references.remove(adapterPath);
372 if (!data->wasListeningAlready) { // Qt turned discovery mode on, Qt has to turn it off again
373 OrgBluezAdapter1Interface iface(QStringLiteral("org.bluez"), adapterPath,
374 QDBusConnection::systemBus());
375 iface.StopDiscovery();
376 }
377
378 delete data->propteryListener;
379 delete data;
380}
381
382//void QtBluezDiscoveryManager::dumpState() const
383//{
384// qCDebug(QT_BT_BLUEZ) << "-------------------------";
385// if (d->references.isEmpty()) {
386// qCDebug(QT_BT_BLUEZ) << "No running registration";
387// } else {
388// const QList<QString> paths = d->references.keys();
389// for (const QString &path : paths) {
390// qCDebug(QT_BT_BLUEZ) << path << "->" << d->references[path]->reference;
391// }
392// }
393// qCDebug(QT_BT_BLUEZ) << "-------------------------";
394//}
395
396void QtBluezDiscoveryManager::InterfacesRemoved(const QDBusObjectPath &object_path,
397 const QStringList &interfaces)
398{
399 if (!d->references.contains(object_path.path())
400 || !interfaces.contains(QStringLiteral("org.bluez.Adapter1")))
401 return;
402
403 removeAdapterFromMonitoring(object_path.path());
404}
405
406void QtBluezDiscoveryManager::PropertiesChanged(const QString &interface,
407 const QVariantMap &changed_properties,
408 const QStringList &invalidated_properties,
409 const QDBusMessage &)
410{
411 Q_UNUSED(invalidated_properties);
412
413 OrgFreedesktopDBusPropertiesInterface *propIface =
414 qobject_cast<OrgFreedesktopDBusPropertiesInterface *>(sender());
415
416 if (!propIface)
417 return;
418
419 if (interface == QStringLiteral("org.bluez.Adapter1")
420 && d->references.contains(propIface->path())
421 && changed_properties.contains(QStringLiteral("Discovering"))) {
422 bool isDiscovering = changed_properties.value(QStringLiteral("Discovering")).toBool();
423 if (!isDiscovering) {
424
425 /*
426 Once we stop the Discovering flag will switch a few ms later. This comes through this code
427 path. If a new device discovery is started while we are still
428 waiting for the flag change signal, then the new device discovery will be aborted prematurely.
429 To compensate we check whether there was renewed interest.
430 */
431
432 AdapterData *data = d->references[propIface->path()];
433 if (!data) {
434 removeAdapterFromMonitoring(propIface->path());
435 } else {
436 OrgBluezAdapter1Interface iface(QStringLiteral("org.bluez"), propIface->path(),
437 QDBusConnection::systemBus());
438 iface.StartDiscovery();
439 }
440 }
441 }
442}
443
444void QtBluezDiscoveryManager::removeAdapterFromMonitoring(const QString &dbusPath)
445{
446 // remove adapter from monitoring
447 AdapterData *data = d->references.take(dbusPath);
448 delete data->propteryListener;
449 delete data;
450
451 emit discoveryInterrupted(dbusPath);
452}
453
454/*
455 Finds the path for the local adapter with \a wantedAddress or returns an
456 empty string if no local adapter with the given address can be found.
457
458 If \a wantedAddress is \c null it returns the first powered-on adapter, or
459 the first adapter if all of them are powered off, or an empty string if
460 none is available.
461
462 If \a ok is false the lookup was aborted due to a dbus error and this function
463 returns an empty string.
464 */
465QString findAdapterForAddress(const QBluetoothAddress &wantedAddress, bool *ok = nullptr)
466{
467 OrgFreedesktopDBusObjectManagerInterface manager(QStringLiteral("org.bluez"),
468 QStringLiteral("/"),
469 QDBusConnection::systemBus());
470
471 QDBusPendingReply<ManagedObjectList> reply = manager.GetManagedObjects();
472 reply.waitForFinished();
473 if (reply.isError()) {
474 if (ok)
475 *ok = false;
476
477 return QString();
478 }
479
480 struct AdapterInfo
481 {
482 QString path;
483 QBluetoothAddress address;
484 bool powered;
485 };
486 QList<AdapterInfo> localAdapters;
487
488 ManagedObjectList managedObjectList = reply.value();
489 for (ManagedObjectList::const_iterator it = managedObjectList.constBegin(); it != managedObjectList.constEnd(); ++it) {
490 const QDBusObjectPath &path = it.key();
491 const InterfaceList &ifaceList = it.value();
492
493 for (InterfaceList::const_iterator jt = ifaceList.constBegin(); jt != ifaceList.constEnd(); ++jt) {
494 const QString &iface = jt.key();
495
496 if (iface == QStringLiteral("org.bluez.Adapter1")) {
497 AdapterInfo info;
498 info.path = path.path();
499 info.address = QBluetoothAddress(ifaceList.value(iface).value(
500 QStringLiteral("Address")).toString());
501 info.powered = ifaceList.value(iface).value(QStringLiteral("Powered")).toBool();
502 if (!info.address.isNull())
503 localAdapters.append(info);
504 break;
505 }
506 }
507 }
508
509 if (ok)
510 *ok = true;
511
512 if (localAdapters.isEmpty())
513 return QString(); // -> no local adapter found
514
515 auto findFirstPowered = [&localAdapters]() {
516 Q_ASSERT(!localAdapters.isEmpty());
517 auto it = std::find_if(localAdapters.cbegin(), localAdapters.cend(),
518 [](const AdapterInfo &info) {
519 return info.powered == true;
520 });
521 if (it != localAdapters.cend())
522 return it->path;
523 // simply return first if none is powered
524 return localAdapters.cbegin()->path;
525 };
526
527 if (wantedAddress.isNull())
528 return findFirstPowered();
529
530 for (const AdapterInfo &info : std::as_const(localAdapters)) {
531 if (info.address == wantedAddress)
532 return info.path; // -> found local adapter with wanted address
533 }
534
535 return QString(); // nothing matching found
536}
537
538/*
539
540 Checks the presence of peripheral interface and returns path to the adapter
541
542*/
543
544QString adapterWithDBusPeripheralInterface(const QBluetoothAddress &localAddress)
545{
546 initializeBluez5();
547
548 // First find the object path to the desired adapter
549 bool ok = false;
550 const QString hostAdapterPath = findAdapterForAddress(localAddress, &ok);
551 if (!ok || hostAdapterPath.isEmpty())
552 return {};
553
554 // Then check if that adapter provides peripheral dbus interface
555 OrgFreedesktopDBusObjectManagerInterface manager(QStringLiteral("org.bluez"),
556 QStringLiteral("/"),
557 QDBusConnection::systemBus());
558 QDBusPendingReply<ManagedObjectList> reply = manager.GetManagedObjects();
559 reply.waitForFinished();
560 if (reply.isError())
561 return {};
562
563 using namespace Qt::StringLiterals;
564 // For example /org/bluez/hci0 contains org.bluezLEAdvertisingManager1
565 const bool peripheralSupported = reply.value()
566 .value(QDBusObjectPath(hostAdapterPath))
567 .contains("org.bluez.LEAdvertisingManager1"_L1);
568
569 qCDebug(QT_BT_BLUEZ) << "Peripheral role"
570 << (peripheralSupported ? "" : "not")
571 << "supported on" << hostAdapterPath;
572 return peripheralSupported ? hostAdapterPath : QString{};
573}
574
575/*
576 Removes every character that cannot be used in QDbusObjectPath
577
578 See QDbusUtil::isValidObjectPath(QString) for more details.
579 */
580QString sanitizeNameForDBus(const QString &text)
581{
582 QString appName = text;
583 for (qsizetype i = 0; i < appName.size(); ++i) {
584 ushort us = appName[i].unicode();
585 bool valid = (us >= 'a' && us <= 'z')
586 || (us >= 'A' && us <= 'Z')
587 || (us >= '0' && us <= '9')
588 || (us == '_');
589
590 if (!valid)
591 appName[i] = QLatin1Char('_');
592 }
593
594 return appName;
595}
596
597QT_END_NAMESPACE
598
599#include "moc_bluez5_helper_p.cpp"
QString adapterWithDBusPeripheralInterface(const QBluetoothAddress &localAddress)
QString sanitizeNameForDBus(const QString &text)
QString findAdapterForAddress(const QBluetoothAddress &wantedAddress, bool *ok=nullptr)
Bluez5TestResultType
@ BluezVersion5
@ BluezNotAvailable
@ BluezVersionUnknown
QMap< QDBusObjectPath, InterfaceList > ManagedObjectList
QMap< QString, QVariantMap > InterfaceList
#define HCIGETDEVLIST
#define HCIGETCONNLIST
#define HCI_MAX_DEV
#define BTPROTO_HCI
#define HCIGETDEVINFO
QObject * parent
Definition qobject.h:74
\inmodule QtCore
Definition qobject.h:106
QMap< QString, AdapterData * > references
OrgFreedesktopDBusObjectManagerInterface * manager
static QtBluezDiscoveryManager * instance()
bool registerDiscoveryInterest(const QString &adapterPath)
void unregisterDiscoveryInterest(const QString &adapterPath)
connect(manager, &QNetworkAccessManager::finished, this, &MyClass::replyFinished)