1/*
2 SPDX-FileCopyrightText: 2009 Pino Toscano <pino@kde.org>
3 SPDX-FileCopyrightText: 2009-2012 Lukáš Tinkl <ltinkl@redhat.com>
4
5 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
6*/
7
8#include "udisksstorageaccess.h"
9#include "udisks2.h"
10#include "udisks_debug.h"
11
12#include <QDBusConnection>
13#include <QDBusInterface>
14#include <QDBusMetaType>
15#include <QDir>
16#include <QGuiApplication>
17#include <QWindow>
18
19#include <config-solid.h>
20#if HAVE_LIBMOUNT
21#include <libmount/libmount.h>
22#endif
23
24struct AvailableAnswer {
25 bool checkResult;
26 QString binaryName;
27};
28Q_DECLARE_METATYPE(AvailableAnswer)
29
30QDBusArgument &operator<<(QDBusArgument &argument, const AvailableAnswer &answer)
31{
32 argument.beginStructure();
33 argument << answer.checkResult << answer.binaryName;
34 argument.endStructure();
35 return argument;
36}
37
38const QDBusArgument &operator>>(const QDBusArgument &argument, AvailableAnswer &answer)
39{
40 argument.beginStructure();
41 argument >> answer.checkResult >> answer.binaryName;
42 argument.endStructure();
43 return argument;
44}
45
46using namespace Solid::Backends::UDisks2;
47
48StorageAccess::StorageAccess(Device *device)
49 : DeviceInterface(device)
50 , m_setupInProgress(false)
51 , m_teardownInProgress(false)
52 , m_repairInProgress(false)
53 , m_passphraseRequested(false)
54{
55 qDBusRegisterMetaType<AvailableAnswer>();
56
57 connect(sender: device, SIGNAL(changed()), receiver: this, SLOT(checkAccessibility()));
58 updateCache();
59
60 // Delay connecting to DBus signals to avoid the related time penalty
61 // in hot paths such as predicate matching
62 QTimer::singleShot(msec: 0, receiver: this, SLOT(connectDBusSignals()));
63}
64
65StorageAccess::~StorageAccess()
66{
67}
68
69void StorageAccess::connectDBusSignals()
70{
71 m_device->registerAction(actionName: "setup", dest: this, SLOT(slotSetupRequested()), SLOT(slotSetupDone(int, QString)));
72
73 m_device->registerAction(actionName: "teardown", dest: this, SLOT(slotTeardownRequested()), SLOT(slotTeardownDone(int, QString)));
74
75 m_device->registerAction(actionName: "repair", dest: this, SLOT(slotRepairRequested()), SLOT(slotRepairDone(int, QString)));
76}
77
78bool StorageAccess::isLuksDevice() const
79{
80 return m_device->isEncryptedContainer(); // encrypted device
81}
82
83bool StorageAccess::isAccessible() const
84{
85 if (isLuksDevice()) { // check if the cleartext slave is mounted
86 const QString path = clearTextPath();
87 // qDebug() << Q_FUNC_INFO << "CLEARTEXT device path: " << path;
88 if (path.isEmpty() || path == "/") {
89 return false;
90 }
91 Device holderDevice(path);
92 return holderDevice.isMounted();
93 }
94
95 return m_device->isMounted();
96}
97
98bool StorageAccess::isEncrypted() const
99{
100 // FIXME We should also check if physical device is encrypted
101 // FIXME Gocryptfs is not supported
102 return isLuksDevice() || m_device->isEncryptedCleartext();
103}
104
105bool StorageAccess::canCheck() const
106{
107 const auto idType = m_device->prop(key: "IdType").toString();
108 auto c = QDBusConnection::systemBus();
109 auto msg = QDBusMessage::createMethodCall(UD2_DBUS_SERVICE, UD2_DBUS_PATH_MANAGER, interface: "org.freedesktop.UDisks2.Manager", method: "CanCheck");
110 msg << idType;
111 QDBusReply<AvailableAnswer> r = c.call(message: msg);
112 if (!r.isValid()) {
113 qCDebug(UDISKS2) << Q_FUNC_INFO << dbusPath() << idType << "DBus error, code" << r.error().type();
114 return false;
115 }
116
117 const bool ret = r.value().checkResult;
118 qCDebug(UDISKS2) << Q_FUNC_INFO << dbusPath() << idType << ret << r.value().binaryName;
119 return ret;
120}
121
122bool StorageAccess::check()
123{
124 if (m_setupInProgress || m_teardownInProgress) {
125 return false;
126 }
127
128 const auto path = dbusPath();
129 auto c = QDBusConnection::systemBus();
130 auto msg = QDBusMessage::createMethodCall(UD2_DBUS_SERVICE, path, UD2_DBUS_INTERFACE_FILESYSTEM, method: "Check");
131 QVariantMap options;
132 msg << options;
133 QDBusReply<bool> r = c.call(message: msg);
134 bool ret = r.isValid() && r.value();
135 qCDebug(UDISKS2) << Q_FUNC_INFO << path << ret;
136 return ret;
137}
138
139bool StorageAccess::canRepair() const
140{
141 const auto idType = m_device->prop(key: "IdType").toString();
142 auto c = QDBusConnection::systemBus();
143 auto msg = QDBusMessage::createMethodCall(UD2_DBUS_SERVICE, UD2_DBUS_PATH_MANAGER, interface: "org.freedesktop.UDisks2.Manager", method: "CanRepair");
144 msg << idType;
145 QDBusReply<AvailableAnswer> r = c.call(message: msg);
146 if (!r.isValid()) {
147 qCDebug(UDISKS2) << Q_FUNC_INFO << dbusPath() << idType << "DBus error, code" << r.error().type();
148 return false;
149 }
150
151 const bool ret = r.value().checkResult;
152 qCDebug(UDISKS2) << Q_FUNC_INFO << dbusPath() << idType << ret << r.value().binaryName;
153 return ret;
154}
155
156bool StorageAccess::repair()
157{
158 if (m_teardownInProgress || m_setupInProgress || m_repairInProgress) {
159 return false;
160 }
161 m_repairInProgress = true;
162 m_device->broadcastActionRequested(actionName: "repair");
163
164 const auto path = dbusPath();
165 auto c = QDBusConnection::systemBus();
166 auto msg = QDBusMessage::createMethodCall(UD2_DBUS_SERVICE, path, UD2_DBUS_INTERFACE_FILESYSTEM, method: "Repair");
167 QVariantMap options;
168 msg << options;
169
170 qCDebug(UDISKS2) << Q_FUNC_INFO << path;
171 return c.callWithCallback(message: msg, receiver: this, SLOT(slotDBusReply(QDBusMessage)), SLOT(slotDBusError(QDBusError)));
172}
173
174static QString baseMountPoint(const QByteArray &dev)
175{
176 QString mountPoint;
177
178#if HAVE_LIBMOUNT
179 // UDisks "MountPoints" property contains multiple paths, this happens with
180 // devices with bind mounts; try finding the "base" mount point
181 if (struct libmnt_table *table = mnt_new_table()) {
182 // This parses "/etc/mtab" if present or "/proc/self/mountinfo" by default
183 if (mnt_table_parse_mtab(tb: table, filename: "/proc/self/mountinfo") == 0) {
184 // BACKWARD because the fs's we're interested in, /dev/sdXY, are typically at the end
185 struct libmnt_iter *itr = mnt_new_iter(direction: MNT_ITER_BACKWARD);
186 struct libmnt_fs *fs;
187
188 const QByteArray devicePath = dev.endsWith(c: '\x00') ? dev.chopped(len: 1) : dev;
189
190 while (mnt_table_next_fs(tb: table, itr, fs: &fs) == 0) {
191 if (mnt_fs_get_srcpath(fs) == devicePath //
192 && (qstrcmp(str1: mnt_fs_get_root(fs), str2: "/") == 0) // Base mount point will have "/" as root fs
193 ) {
194 mountPoint = QFile::decodeName(localFileName: mnt_fs_get_target(fs));
195 break;
196 }
197 }
198
199 mnt_free_iter(itr);
200 }
201
202 mnt_free_table(tb: table);
203 }
204#else
205 Q_UNUSED(dev);
206#endif
207
208 return mountPoint;
209}
210
211QString StorageAccess::filePath() const
212{
213 if (isLuksDevice()) { // encrypted (and unlocked) device
214 const QString path = clearTextPath();
215 if (path.isEmpty() || path == "/") {
216 return QString();
217 }
218 Device holderDevice(path);
219 const auto mntPoints = qdbus_cast<QByteArrayList>(v: holderDevice.prop(key: "MountPoints"));
220 if (!mntPoints.isEmpty()) {
221 QByteArray first = mntPoints.first();
222 if (first.endsWith(c: '\x00')) {
223 first.chop(n: 1);
224 }
225 return QFile::decodeName(localFileName: first); // FIXME Solid doesn't support multiple mount points
226 } else {
227 return QString();
228 }
229 }
230
231 const auto mntPoints = qdbus_cast<QByteArrayList>(v: m_device->prop(key: "MountPoints"));
232 if (mntPoints.isEmpty()) {
233 return {};
234 }
235
236 QByteArray first = mntPoints.first();
237 if (first.endsWith(c: '\x00')) {
238 first.chop(n: 1);
239 }
240 const QString potentialMountPoint = QFile::decodeName(localFileName: first);
241
242 if (mntPoints.size() == 1) {
243 return potentialMountPoint;
244 }
245
246 // Device has bind mounts?
247 const QString basePoint = baseMountPoint(dev: m_device->prop(key: "Device").toByteArray());
248
249 return !basePoint.isEmpty() ? basePoint : potentialMountPoint;
250}
251
252bool StorageAccess::isIgnored() const
253{
254 if (m_device->prop(key: "HintIgnore").toBool()) {
255 return true;
256 }
257
258 const QStringList mountOptions = m_device->prop(key: "UserspaceMountOptions").toStringList();
259 if (mountOptions.contains(str: QLatin1String("x-gdu.hide"))) {
260 return true;
261 }
262
263 const QString path = filePath();
264
265 const bool inUserPath = (path.startsWith(s: QLatin1String("/media/")) //
266 || path.startsWith(s: QLatin1String("/run/media/")) //
267 || path.startsWith(s: QDir::homePath()));
268 return !inUserPath;
269}
270
271bool StorageAccess::setup()
272{
273 if (m_teardownInProgress || m_setupInProgress || m_repairInProgress) {
274 return false;
275 }
276 m_setupInProgress = true;
277 m_device->broadcastActionRequested(actionName: "setup");
278
279 if (m_device->isEncryptedContainer() && clearTextPath().isEmpty()) {
280 return requestPassphrase();
281 } else {
282 return mount();
283 }
284}
285
286bool StorageAccess::teardown()
287{
288 if (m_teardownInProgress || m_setupInProgress || m_repairInProgress) {
289 return false;
290 }
291 m_teardownInProgress = true;
292 m_device->broadcastActionRequested(actionName: "teardown");
293
294 return unmount();
295}
296
297void StorageAccess::updateCache()
298{
299 m_isAccessible = isAccessible();
300}
301
302void StorageAccess::checkAccessibility()
303{
304 const bool old_isAccessible = m_isAccessible;
305 updateCache();
306
307 if (old_isAccessible != m_isAccessible) {
308 Q_EMIT accessibilityChanged(accessible: m_isAccessible, udi: m_device->udi());
309 }
310}
311
312void StorageAccess::slotDBusReply(const QDBusMessage & /*reply*/)
313{
314 if (m_setupInProgress) {
315 if (isLuksDevice() && !isAccessible()) { // unlocked device, now mount it
316 mount();
317 } else { // Don't broadcast setupDone unless the setup is really done. (Fix kde#271156)
318 m_setupInProgress = false;
319 m_device->invalidateCache();
320 m_device->broadcastActionDone(actionName: "setup");
321
322 checkAccessibility();
323 }
324 } else if (m_teardownInProgress) { // FIXME
325 const QString ctPath = clearTextPath();
326 qCDebug(UDISKS2) << "Successfully unmounted " << m_device->udi();
327 if (isLuksDevice() && !ctPath.isEmpty() && ctPath != "/") { // unlocked device, lock it
328 callCryptoTeardown();
329 } else if (!ctPath.isEmpty() && ctPath != "/") {
330 callCryptoTeardown(actOnParent: true); // Lock encrypted parent
331 } else {
332 // try to "eject" (aka safely remove) from the (parent) drive, e.g. SD card from a reader
333 QString drivePath = m_device->drivePath();
334 if (!drivePath.isEmpty() || drivePath != "/") {
335 Device drive(drivePath);
336 QDBusConnection c = QDBusConnection::systemBus();
337
338 if (drive.prop(key: "MediaRemovable").toBool() //
339 && drive.prop(key: "MediaAvailable").toBool() //
340 && !m_device->isOpticalDisc()) { // optical drives have their Eject method
341 QDBusMessage msg = QDBusMessage::createMethodCall(UD2_DBUS_SERVICE, path: drivePath, UD2_DBUS_INTERFACE_DRIVE, method: "Eject");
342 msg << QVariantMap(); // options, unused now
343 c.call(message: msg, mode: QDBus::NoBlock);
344 } else if (drive.prop(key: "CanPowerOff").toBool() //
345 && !m_device->isOpticalDisc()) { // avoid disconnecting optical drives from the bus
346 qCDebug(UDISKS2) << "Drive can power off:" << drivePath;
347 QDBusMessage msg = QDBusMessage::createMethodCall(UD2_DBUS_SERVICE, path: drivePath, UD2_DBUS_INTERFACE_DRIVE, method: "PowerOff");
348 msg << QVariantMap(); // options, unused now
349 c.call(message: msg, mode: QDBus::NoBlock);
350 }
351 }
352
353 m_teardownInProgress = false;
354 m_device->invalidateCache();
355 m_device->broadcastActionDone(actionName: "teardown");
356
357 checkAccessibility();
358 }
359 } else if (m_repairInProgress) {
360 qCDebug(UDISKS2) << "Successfully repaired " << m_device->udi();
361 m_repairInProgress = false;
362 m_device->broadcastActionDone(actionName: "repair");
363 }
364}
365
366void StorageAccess::slotDBusError(const QDBusError &error)
367{
368 // qDebug() << Q_FUNC_INFO << "DBUS ERROR:" << error.name() << error.message();
369
370 if (m_setupInProgress) {
371 m_setupInProgress = false;
372 m_device->broadcastActionDone(actionName: "setup", //
373 error: m_device->errorToSolidError(error: error.name()),
374 errorString: m_device->errorToString(error: error.name()) + ": " + error.message());
375
376 checkAccessibility();
377 } else if (m_teardownInProgress) {
378 m_teardownInProgress = false;
379 m_device->broadcastActionDone(actionName: "teardown", //
380 error: m_device->errorToSolidError(error: error.name()),
381 errorString: m_device->errorToString(error: error.name()) + ": " + error.message());
382 checkAccessibility();
383 } else if (m_repairInProgress) {
384 m_repairInProgress = false;
385 m_device->broadcastActionDone(actionName: "repair", error: m_device->errorToSolidError(error: error.name()), errorString: m_device->errorToString(error: error.name()) + ": " + error.message());
386 }
387}
388
389void StorageAccess::slotSetupRequested()
390{
391 m_setupInProgress = true;
392 // qDebug() << "SETUP REQUESTED:" << m_device->udi();
393 Q_EMIT setupRequested(m_device->udi());
394}
395
396void StorageAccess::slotSetupDone(int error, const QString &errorString)
397{
398 m_setupInProgress = false;
399 // qDebug() << "SETUP DONE:" << m_device->udi();
400 checkAccessibility();
401 Q_EMIT setupDone(error: static_cast<Solid::ErrorType>(error), errorData: errorString, udi: m_device->udi());
402}
403
404void StorageAccess::slotTeardownRequested()
405{
406 m_teardownInProgress = true;
407 Q_EMIT teardownRequested(udi: m_device->udi());
408}
409
410void StorageAccess::slotTeardownDone(int error, const QString &errorString)
411{
412 m_teardownInProgress = false;
413 checkAccessibility();
414 Q_EMIT teardownDone(error: static_cast<Solid::ErrorType>(error), errorData: errorString, udi: m_device->udi());
415}
416
417void StorageAccess::slotRepairRequested()
418{
419 m_repairInProgress = true;
420 Q_EMIT repairRequested(udi: m_device->udi());
421}
422
423void StorageAccess::slotRepairDone(int error, const QString &errorString)
424{
425 m_repairInProgress = false;
426 Q_EMIT repairDone(error: static_cast<Solid::ErrorType>(error), errorData: errorString, udi: m_device->udi());
427}
428
429bool StorageAccess::mount()
430{
431 const auto path = dbusPath();
432
433 QDBusConnection c = QDBusConnection::systemBus();
434 QDBusMessage msg = QDBusMessage::createMethodCall(UD2_DBUS_SERVICE, path, UD2_DBUS_INTERFACE_FILESYSTEM, method: "Mount");
435 QVariantMap options;
436
437 if (m_device->prop(key: "IdType").toString() == "vfat") {
438 options.insert(key: "options", value: "flush");
439 }
440
441 msg << options;
442
443 return c.callWithCallback(message: msg, receiver: this, SLOT(slotDBusReply(QDBusMessage)), SLOT(slotDBusError(QDBusError)));
444}
445
446bool StorageAccess::unmount()
447{
448 const auto path = dbusPath();
449
450 QDBusConnection c = QDBusConnection::systemBus();
451 QDBusMessage msg = QDBusMessage::createMethodCall(UD2_DBUS_SERVICE, path, UD2_DBUS_INTERFACE_FILESYSTEM, method: "Unmount");
452
453 msg << QVariantMap(); // options, unused now
454
455 qCDebug(UDISKS2) << "Initiating unmount of " << path;
456 return c.callWithCallback(message: msg, receiver: this, SLOT(slotDBusReply(QDBusMessage)), SLOT(slotDBusError(QDBusError)), timeout: s_unmountTimeout);
457}
458
459QString StorageAccess::generateReturnObjectPath()
460{
461 static QAtomicInt number = 1;
462
463 return "/org/kde/solid/UDisks2StorageAccess_" + QString::number(number++);
464}
465
466QString StorageAccess::clearTextPath() const
467{
468 const QString path = m_device->prop(key: "CleartextDevice").value<QDBusObjectPath>().path();
469 if (path != QLatin1String("/")) {
470 return path;
471 }
472 return QString();
473}
474
475QString StorageAccess::dbusPath() const
476{
477 QString path = m_device->udi();
478 if (isLuksDevice()) { // mount options for the cleartext volume
479 const QString ctPath = clearTextPath();
480 if (!ctPath.isEmpty()) {
481 path = ctPath;
482 }
483 }
484 return path;
485}
486
487bool StorageAccess::requestPassphrase()
488{
489 QString udi = m_device->udi();
490 QString returnService = QDBusConnection::sessionBus().baseService();
491 m_lastReturnObject = generateReturnObjectPath();
492
493 QDBusConnection::sessionBus().registerObject(path: m_lastReturnObject, object: this, options: QDBusConnection::ExportScriptableSlots);
494
495 // TODO: this only works on X11, Wayland doesn't have global window ids.
496 // Passing ids to other processes doesn't make any sense
497 auto activeWindow = QGuiApplication::focusWindow();
498 uint wId = 0;
499 if (activeWindow != nullptr) {
500 wId = (uint)activeWindow->winId();
501 }
502
503 QString appId = QCoreApplication::applicationName();
504
505 const auto plasmaVersionMajor = qEnvironmentVariable(varName: "KDE_SESSION_VERSION", defaultValue: "6");
506
507 // TODO KF6: remove hard dep on Plasma here which provides the SolidUiServer kded plugin
508 QDBusInterface soliduiserver(QStringLiteral("org.kde.kded") + plasmaVersionMajor, "/modules/soliduiserver", "org.kde.SolidUiServer");
509 QDBusReply<void> reply = soliduiserver.call(method: "showPassphraseDialog", args&: udi, args&: returnService, args&: m_lastReturnObject, args&: wId, args&: appId);
510 m_passphraseRequested = reply.isValid();
511 if (!m_passphraseRequested) {
512 qCWarning(UDISKS2) << "Failed to call the SolidUiServer, D-Bus said:" << reply.error();
513 }
514
515 return m_passphraseRequested;
516}
517
518void StorageAccess::passphraseReply(const QString &passphrase)
519{
520 if (m_passphraseRequested) {
521 QDBusConnection::sessionBus().unregisterObject(path: m_lastReturnObject);
522 m_passphraseRequested = false;
523 if (!passphrase.isEmpty()) {
524 callCryptoSetup(passphrase);
525 } else {
526 m_setupInProgress = false;
527 m_device->broadcastActionDone(actionName: "setup", error: Solid::UserCanceled);
528 }
529 }
530}
531
532void StorageAccess::callCryptoSetup(const QString &passphrase)
533{
534 QDBusConnection c = QDBusConnection::systemBus();
535 QDBusMessage msg = QDBusMessage::createMethodCall(UD2_DBUS_SERVICE, path: m_device->udi(), UD2_DBUS_INTERFACE_ENCRYPTED, method: "Unlock");
536
537 msg << passphrase;
538 msg << QVariantMap(); // options, unused now
539
540 c.callWithCallback(message: msg, receiver: this, SLOT(slotDBusReply(QDBusMessage)), SLOT(slotDBusError(QDBusError)));
541}
542
543bool StorageAccess::callCryptoTeardown(bool actOnParent)
544{
545 QDBusConnection c = QDBusConnection::systemBus();
546 QDBusMessage msg = QDBusMessage::createMethodCall(UD2_DBUS_SERVICE,
547 path: actOnParent ? (m_device->prop(key: "CryptoBackingDevice").value<QDBusObjectPath>().path()) : m_device->udi(),
548 UD2_DBUS_INTERFACE_ENCRYPTED,
549 method: "Lock");
550 msg << QVariantMap(); // options, unused now
551
552 return c.callWithCallback(message: msg, receiver: this, SLOT(slotDBusReply(QDBusMessage)), SLOT(slotDBusError(QDBusError)));
553}
554
555#include "moc_udisksstorageaccess.cpp"
556

source code of solid/src/solid/devices/backends/udisks2/udisksstorageaccess.cpp