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 process.start(binary, {QStringLiteral("--version")});
192 process.waitForFinished();
193
194 const QString version = QString::fromLocal8Bit(process.readAll());
195 const QVersionNumber vn = QVersionNumber::fromString(version);
196 if (!vn.isNull())
197 qCDebug(QT_BT_BLUEZ) << "Detected bluetoothd version" << vn;
198 return vn;
199 };
200
201 //try reading /proc/<pid>/exe first -> requires process owner
202 qCDebug(QT_BT_BLUEZ) << "Using /proc/<pid>/exe";
203 const QString procExe = QStringLiteral("/proc/%1/exe").arg(pid);
204 const QVersionNumber vn = determineBinaryVersion(procExe);
205 if (!vn.isNull())
206 *bluezDaemonVersion() = vn;
207
208 if (bluezDaemonVersion()->isNull()) {
209 qCDebug(QT_BT_BLUEZ) << "Using /proc/<pid>/cmdline";
210 //try to reading /proc/<pid>/cmdline (does not require additional process rights)
211 QFile procFile(QStringLiteral("/proc/%1/cmdline").arg(pid));
212 if (procFile.open(QIODevice::ReadOnly|QIODevice::Text)) {
213 buffer = procFile.readAll();
214 procFile.close();
215
216 // cmdline params separated by character \0 -> first is bluetoothd binary
217 const QString binary = QString::fromLocal8Bit(buffer.split('\0').at(0));
218 QFileInfo info(binary);
219 if (info.isExecutable())
220 *bluezDaemonVersion() = determineBinaryVersion(binary);
221 else
222 qCDebug(QT_BT_BLUEZ) << "Cannot determine bluetoothd version via cmdline:"
223 << binary;
224 }
225 }
226 }
227
228 // 3. Fall back to custom ATT backend, if possible?
229 if (bluezDaemonVersion()->isNull()) {
230 // Check mandatory HCI ioctls are available
231 if (mandatoryHciIoctlsAvailable()) {
232 // default 4.0 for now -> implies custom (G)ATT implementation
233 *bluezDaemonVersion() = QVersionNumber(4, 0);
234 }
235 }
236
237 // 4. Ultimate fallback -> enable dummy backend
238 if (bluezDaemonVersion()->isNull()) {
239 // version 3 represents disabled BTLE
240 // bluezDaemonVersion should not be null to avoid repeated version tests
241 *bluezDaemonVersion() = QVersionNumber(3, 0);
242 qCWarning(QT_BT_BLUEZ) << "Cannot determine bluetoothd version and required Bluetooth HCI ioctols";
243 qCWarning(QT_BT_BLUEZ) << "Disabling Qt Bluetooth LE feature";
244 }
245
246 qCDebug(QT_BT_BLUEZ) << "Bluetoothd:" << bluezDaemonVersion()->toString();
247 }
248
249 Q_ASSERT(!bluezDaemonVersion()->isNull());
250 return *bluezDaemonVersion();
251}
252
253struct AdapterData
254{
255public:
256 AdapterData() : reference(1), wasListeningAlready(false) {}
257
258 int reference;
259 bool wasListeningAlready;
260 OrgFreedesktopDBusPropertiesInterface *propteryListener = nullptr;
261};
262
269
270Q_GLOBAL_STATIC(QtBluezDiscoveryManager, discoveryManager)
271
272/*!
273 \internal
274 \class QtBluezDiscoveryManager
275
276 This class manages the access to "org.bluez.Adapter1::Start/StopDiscovery.
277
278 The flag is a system flag. We want to ensure that the various Qt classes don't turn
279 the flag on and off and thereby get into their way. If some other system component
280 changes the flag (e.g. adapter removed) we notify all Qt classes about the change by emitting
281 \l discoveryInterrupted(QString). Classes should indicate this via an appropriate
282 error message to the user.
283
284 Once the signal was emitted, all existing requests for discovery mode on the same adapter
285 have to be renewed via \l registerDiscoveryInterest(QString).
286*/
287
290{
291 qCDebug(QT_BT_BLUEZ) << "Creating QtBluezDiscoveryManager";
292 d = new QtBluezDiscoveryManagerPrivate();
293
294 d->manager = new OrgFreedesktopDBusObjectManagerInterface(
295 QStringLiteral("org.bluez"), QStringLiteral("/"),
296 QDBusConnection::systemBus(), this);
297 connect(d->manager, SIGNAL(InterfacesRemoved(QDBusObjectPath,QStringList)),
298 SLOT(InterfacesRemoved(QDBusObjectPath,QStringList)));
299}
300
301QtBluezDiscoveryManager::~QtBluezDiscoveryManager()
302{
303 qCDebug(QT_BT_BLUEZ) << "Destroying QtBluezDiscoveryManager";
304
305 const QList<QString> adapterPaths = d->references.keys();
306 for (const QString &adapterPath : adapterPaths) {
307 AdapterData *data = d->references.take(adapterPath);
308 delete data->propteryListener;
309
310 // turn discovery off if it wasn't on already
311 if (!data->wasListeningAlready) {
312 OrgBluezAdapter1Interface iface(QStringLiteral("org.bluez"), adapterPath,
313 QDBusConnection::systemBus());
314 iface.StopDiscovery();
315 }
316
317 delete data;
318 }
319
320 delete d;
321}
322
324{
325 return discoveryManager();
326}
327
328bool QtBluezDiscoveryManager::registerDiscoveryInterest(const QString &adapterPath)
329{
330 if (adapterPath.isEmpty())
331 return false;
332
333 // already monitored adapter? -> increase ref count -> done
334 if (d->references.contains(adapterPath)) {
335 d->references[adapterPath]->reference++;
336 return true;
337 }
338
339 AdapterData *data = new AdapterData();
340
341 OrgFreedesktopDBusPropertiesInterface *propIface = new OrgFreedesktopDBusPropertiesInterface(
342 QStringLiteral("org.bluez"), adapterPath, QDBusConnection::systemBus());
343 connect(propIface, &OrgFreedesktopDBusPropertiesInterface::PropertiesChanged,
344 this, &QtBluezDiscoveryManager::PropertiesChanged);
345 data->propteryListener = propIface;
346
347 OrgBluezAdapter1Interface iface(QStringLiteral("org.bluez"), adapterPath,
348 QDBusConnection::systemBus());
349 data->wasListeningAlready = iface.discovering();
350
351 d->references[adapterPath] = data;
352
353 if (!data->wasListeningAlready)
354 iface.StartDiscovery();
355
356 return true;
357}
358
360{
361 if (!d->references.contains(adapterPath))
362 return;
363
364 AdapterData *data = d->references[adapterPath];
365 data->reference--;
366
367 if (data->reference > 0) // more than one client requested discovery mode
368 return;
369
370 d->references.remove(adapterPath);
371 if (!data->wasListeningAlready) { // Qt turned discovery mode on, Qt has to turn it off again
372 OrgBluezAdapter1Interface iface(QStringLiteral("org.bluez"), adapterPath,
373 QDBusConnection::systemBus());
374 iface.StopDiscovery();
375 }
376
377 delete data->propteryListener;
378 delete data;
379}
380
381//void QtBluezDiscoveryManager::dumpState() const
382//{
383// qCDebug(QT_BT_BLUEZ) << "-------------------------";
384// if (d->references.isEmpty()) {
385// qCDebug(QT_BT_BLUEZ) << "No running registration";
386// } else {
387// const QList<QString> paths = d->references.keys();
388// for (const QString &path : paths) {
389// qCDebug(QT_BT_BLUEZ) << path << "->" << d->references[path]->reference;
390// }
391// }
392// qCDebug(QT_BT_BLUEZ) << "-------------------------";
393//}
394
395void QtBluezDiscoveryManager::InterfacesRemoved(const QDBusObjectPath &object_path,
396 const QStringList &interfaces)
397{
398 if (!d->references.contains(object_path.path())
399 || !interfaces.contains(QStringLiteral("org.bluez.Adapter1")))
400 return;
401
402 removeAdapterFromMonitoring(object_path.path());
403}
404
405void QtBluezDiscoveryManager::PropertiesChanged(const QString &interface,
406 const QVariantMap &changed_properties,
407 const QStringList &invalidated_properties,
408 const QDBusMessage &)
409{
410 Q_UNUSED(invalidated_properties);
411
412 OrgFreedesktopDBusPropertiesInterface *propIface =
413 qobject_cast<OrgFreedesktopDBusPropertiesInterface *>(sender());
414
415 if (!propIface)
416 return;
417
418 if (interface == QStringLiteral("org.bluez.Adapter1")
419 && d->references.contains(propIface->path())
420 && changed_properties.contains(QStringLiteral("Discovering"))) {
421 bool isDiscovering = changed_properties.value(QStringLiteral("Discovering")).toBool();
422 if (!isDiscovering) {
423
424 /*
425 Once we stop the Discovering flag will switch a few ms later. This comes through this code
426 path. If a new device discovery is started while we are still
427 waiting for the flag change signal, then the new device discovery will be aborted prematurely.
428 To compensate we check whether there was renewed interest.
429 */
430
431 AdapterData *data = d->references[propIface->path()];
432 if (!data) {
433 removeAdapterFromMonitoring(propIface->path());
434 } else {
435 OrgBluezAdapter1Interface iface(QStringLiteral("org.bluez"), propIface->path(),
436 QDBusConnection::systemBus());
437 iface.StartDiscovery();
438 }
439 }
440 }
441}
442
443void QtBluezDiscoveryManager::removeAdapterFromMonitoring(const QString &dbusPath)
444{
445 // remove adapter from monitoring
446 AdapterData *data = d->references.take(dbusPath);
447 delete data->propteryListener;
448 delete data;
449
450 emit discoveryInterrupted(dbusPath);
451}
452
453/*
454 Finds the path for the local adapter with \a wantedAddress or returns an
455 empty string if no local adapter with the given address can be found.
456
457 If \a wantedAddress is \c null it returns the first powered-on adapter, or
458 the first adapter if all of them are powered off, or an empty string if
459 none is available.
460
461 If \a ok is false the lookup was aborted due to a dbus error and this function
462 returns an empty string.
463 */
464QString findAdapterForAddress(const QBluetoothAddress &wantedAddress, bool *ok = nullptr)
465{
466 OrgFreedesktopDBusObjectManagerInterface manager(QStringLiteral("org.bluez"),
467 QStringLiteral("/"),
468 QDBusConnection::systemBus());
469
470 QDBusPendingReply<ManagedObjectList> reply = manager.GetManagedObjects();
471 reply.waitForFinished();
472 if (reply.isError()) {
473 if (ok)
474 *ok = false;
475
476 return QString();
477 }
478
479 struct AdapterInfo
480 {
481 QString path;
482 QBluetoothAddress address;
483 bool powered;
484 };
485 QList<AdapterInfo> localAdapters;
486
487 ManagedObjectList managedObjectList = reply.value();
488 for (ManagedObjectList::const_iterator it = managedObjectList.constBegin(); it != managedObjectList.constEnd(); ++it) {
489 const QDBusObjectPath &path = it.key();
490 const InterfaceList &ifaceList = it.value();
491
492 for (InterfaceList::const_iterator jt = ifaceList.constBegin(); jt != ifaceList.constEnd(); ++jt) {
493 const QString &iface = jt.key();
494
495 if (iface == QStringLiteral("org.bluez.Adapter1")) {
496 AdapterInfo info;
497 info.path = path.path();
498 info.address = QBluetoothAddress(ifaceList.value(iface).value(
499 QStringLiteral("Address")).toString());
500 info.powered = ifaceList.value(iface).value(QStringLiteral("Powered")).toBool();
501 if (!info.address.isNull())
502 localAdapters.append(info);
503 break;
504 }
505 }
506 }
507
508 if (ok)
509 *ok = true;
510
511 if (localAdapters.isEmpty())
512 return QString(); // -> no local adapter found
513
514 auto findFirstPowered = [&localAdapters]() {
515 Q_ASSERT(!localAdapters.isEmpty());
516 auto it = std::find_if(localAdapters.cbegin(), localAdapters.cend(),
517 [](const AdapterInfo &info) {
518 return info.powered == true;
519 });
520 if (it != localAdapters.cend())
521 return it->path;
522 // simply return first if none is powered
523 return localAdapters.cbegin()->path;
524 };
525
526 if (wantedAddress.isNull())
527 return findFirstPowered();
528
529 for (const AdapterInfo &info : std::as_const(localAdapters)) {
530 if (info.address == wantedAddress)
531 return info.path; // -> found local adapter with wanted address
532 }
533
534 return QString(); // nothing matching found
535}
536
537/*
538
539 Checks the presence of peripheral interface and returns path to the adapter
540
541*/
542
543QString adapterWithDBusPeripheralInterface(const QBluetoothAddress &localAddress)
544{
545 initializeBluez5();
546
547 // First find the object path to the desired adapter
548 bool ok = false;
549 const QString hostAdapterPath = findAdapterForAddress(localAddress, &ok);
550 if (!ok || hostAdapterPath.isEmpty())
551 return {};
552
553 // Then check if that adapter provides peripheral dbus interface
554 OrgFreedesktopDBusObjectManagerInterface manager(QStringLiteral("org.bluez"),
555 QStringLiteral("/"),
556 QDBusConnection::systemBus());
557 QDBusPendingReply<ManagedObjectList> reply = manager.GetManagedObjects();
558 reply.waitForFinished();
559 if (reply.isError())
560 return {};
561
562 using namespace Qt::StringLiterals;
563 // For example /org/bluez/hci0 contains org.bluezLEAdvertisingManager1
564 const bool peripheralSupported = reply.value()
565 .value(QDBusObjectPath(hostAdapterPath))
566 .contains("org.bluez.LEAdvertisingManager1"_L1);
567
568 qCDebug(QT_BT_BLUEZ) << "Peripheral role"
569 << (peripheralSupported ? "" : "not")
570 << "supported on" << hostAdapterPath;
571 return peripheralSupported ? hostAdapterPath : QString{};
572}
573
574/*
575 Removes every character that cannot be used in QDbusObjectPath
576
577 See QDbusUtil::isValidObjectPath(QString) for more details.
578 */
579QString sanitizeNameForDBus(const QString &text)
580{
581 QString appName = text;
582 for (qsizetype i = 0; i < appName.size(); ++i) {
583 ushort us = appName[i].unicode();
584 bool valid = (us >= 'a' && us <= 'z')
585 || (us >= 'A' && us <= 'Z')
586 || (us >= '0' && us <= '9')
587 || (us == '_');
588
589 if (!valid)
590 appName[i] = QLatin1Char('_');
591 }
592
593 return appName;
594}
595
596QT_END_NAMESPACE
597
598#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:73
\inmodule QtCore
Definition qobject.h:105
QMap< QString, AdapterData * > references
OrgFreedesktopDBusObjectManagerInterface * manager
static QtBluezDiscoveryManager * instance()
bool registerDiscoveryInterest(const QString &adapterPath)
void unregisterDiscoveryInterest(const QString &adapterPath)