| 1 | /* |
| 2 | SPDX-FileCopyrightText: 2008 Nicola Gigante <nicola.gigante@gmail.com> |
| 3 | SPDX-FileCopyrightText: 2009-2010 Dario Freddi <drf@kde.org> |
| 4 | SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org> |
| 5 | |
| 6 | SPDX-License-Identifier: LGPL-2.1-or-later |
| 7 | */ |
| 8 | |
| 9 | #include "DBusHelperProxy.h" |
| 10 | #include "BackendsManager.h" |
| 11 | #include "kauthdebug.h" |
| 12 | #include "kf6authadaptor.h" |
| 13 | |
| 14 | #include <QDBusConnectionInterface> |
| 15 | #include <QDBusMessage> |
| 16 | #include <QDBusMetaType> |
| 17 | #include <QDBusUnixFileDescriptor> |
| 18 | #include <QMap> |
| 19 | #include <QMetaMethod> |
| 20 | #include <QObject> |
| 21 | #include <QTimer> |
| 22 | #include <qplugin.h> |
| 23 | |
| 24 | extern Q_CORE_EXPORT const QtPrivate::QMetaTypeInterface *qMetaTypeGuiHelper; |
| 25 | |
| 26 | namespace KAuth |
| 27 | { |
| 28 | static void debugMessageReceived(int t, const QString &message); |
| 29 | |
| 30 | DBusHelperProxy::DBusHelperProxy() |
| 31 | : responder(nullptr) |
| 32 | , m_stopRequest(false) |
| 33 | , m_busConnection(QDBusConnection::systemBus()) |
| 34 | { |
| 35 | qDBusRegisterMetaType<QMap<QString, QDBusUnixFileDescriptor>>(); |
| 36 | } |
| 37 | |
| 38 | DBusHelperProxy::DBusHelperProxy(const QDBusConnection &busConnection) |
| 39 | : responder(nullptr) |
| 40 | , m_stopRequest(false) |
| 41 | , m_busConnection(busConnection) |
| 42 | { |
| 43 | qDBusRegisterMetaType<QMap<QString, QDBusUnixFileDescriptor>>(); |
| 44 | } |
| 45 | |
| 46 | DBusHelperProxy::~DBusHelperProxy() |
| 47 | { |
| 48 | } |
| 49 | |
| 50 | void DBusHelperProxy::stopAction(const QString &action, const QString &helperID) |
| 51 | { |
| 52 | QDBusMessage message; |
| 53 | message = QDBusMessage::createMethodCall(destination: helperID, path: QLatin1String("/" ), interface: QLatin1String("org.kde.kf6auth" ), method: QLatin1String("stopAction" )); |
| 54 | |
| 55 | QList<QVariant> args; |
| 56 | args << action; |
| 57 | message.setArguments(args); |
| 58 | |
| 59 | m_busConnection.asyncCall(message); |
| 60 | } |
| 61 | |
| 62 | void DBusHelperProxy::executeAction(const QString &action, const QString &helperID, const DetailsMap &details, const QVariantMap &arguments, int timeout) |
| 63 | { |
| 64 | QMap<QString, QDBusUnixFileDescriptor> fds; |
| 65 | QVariantMap nonFds; |
| 66 | for (auto [key, value] : arguments.asKeyValueRange()) { |
| 67 | if (value.metaType() == QMetaType::fromType<QDBusUnixFileDescriptor>()) { |
| 68 | fds.insert(key, value: value.value<QDBusUnixFileDescriptor>()); |
| 69 | } else { |
| 70 | nonFds.insert(key, value); |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | QByteArray blob; |
| 75 | { |
| 76 | QDataStream stream(&blob, QIODevice::WriteOnly); |
| 77 | stream << nonFds; |
| 78 | } |
| 79 | |
| 80 | // on unit tests we won't have a service, but the service will already be running |
| 81 | const auto reply = m_busConnection.interface()->startService(name: helperID); |
| 82 | if (!reply.isValid() && !m_busConnection.interface()->isServiceRegistered(serviceName: helperID)) { |
| 83 | ActionReply errorReply = ActionReply::DBusErrorReply(); |
| 84 | errorReply.setErrorDescription(tr(s: "DBus Backend error: service start %1 failed: %2" ).arg(args: helperID, args: reply.error().message())); |
| 85 | Q_EMIT actionPerformed(action, reply: errorReply); |
| 86 | return; |
| 87 | } |
| 88 | |
| 89 | const bool connected = m_busConnection.connect(service: helperID, |
| 90 | path: QLatin1String("/" ), |
| 91 | interface: QLatin1String("org.kde.kf6auth" ), |
| 92 | name: QLatin1String("remoteSignal" ), |
| 93 | receiver: this, |
| 94 | SLOT(remoteSignalReceived(int, QString, QByteArray))); |
| 95 | |
| 96 | // if already connected reply will be false but we won't have an error or a reason to fail |
| 97 | if (!connected && m_busConnection.lastError().isValid()) { |
| 98 | ActionReply errorReply = ActionReply::DBusErrorReply(); |
| 99 | errorReply.setErrorDescription(tr(s: "DBus Backend error: connection to helper failed. %1\n(application: %2 helper: %3)" ) |
| 100 | .arg(args: m_busConnection.lastError().message(), qApp->applicationName(), args: helperID)); |
| 101 | Q_EMIT actionPerformed(action, reply: errorReply); |
| 102 | return; |
| 103 | } |
| 104 | |
| 105 | QDBusMessage message; |
| 106 | message = QDBusMessage::createMethodCall(destination: helperID, path: QLatin1String("/" ), interface: QLatin1String("org.kde.kf6auth" ), method: QLatin1String("performAction" )); |
| 107 | |
| 108 | QList<QVariant> args; |
| 109 | args << action << BackendsManager::authBackend()->callerID() << BackendsManager::authBackend()->backendDetails(details) << blob << QVariant::fromValue(value: fds); |
| 110 | message.setArguments(args); |
| 111 | |
| 112 | m_actionsInProgress.push_back(t: action); |
| 113 | |
| 114 | QDBusPendingCall pendingCall = m_busConnection.asyncCall(message, timeout); |
| 115 | |
| 116 | auto watcher = new QDBusPendingCallWatcher(pendingCall, this); |
| 117 | |
| 118 | connect(sender: watcher, signal: &QDBusPendingCallWatcher::finished, context: this, slot: [this, action, args, message, watcher, timeout]() mutable { |
| 119 | watcher->deleteLater(); |
| 120 | |
| 121 | QDBusMessage reply = watcher->reply(); |
| 122 | |
| 123 | if (reply.type() == QDBusMessage::ErrorMessage) { |
| 124 | if (watcher->error().type() == QDBusError::InvalidArgs) { |
| 125 | // For backwards compatibility if helper binary was built with older KAuth version. |
| 126 | args.removeAt(i: args.count() - 2); // remove backend details |
| 127 | message.setArguments(args); |
| 128 | reply = m_busConnection.call(message, mode: QDBus::Block, timeout); |
| 129 | if (reply.type() != QDBusMessage::ErrorMessage) { |
| 130 | return; |
| 131 | } |
| 132 | } |
| 133 | ActionReply r = ActionReply::DBusErrorReply(); |
| 134 | r.setErrorDescription(tr(s: "DBus Backend error: could not contact the helper. " |
| 135 | "Connection error: %1. Message error: %2" ) |
| 136 | .arg(args: reply.errorMessage(), args: m_busConnection.lastError().message())); |
| 137 | qCWarning(KAUTH) << reply.errorMessage(); |
| 138 | |
| 139 | Q_EMIT actionPerformed(action, reply: r); |
| 140 | } |
| 141 | }); |
| 142 | } |
| 143 | |
| 144 | bool DBusHelperProxy::initHelper(const QString &name) |
| 145 | { |
| 146 | new Kf6authAdaptor(this); |
| 147 | |
| 148 | if (!m_busConnection.registerService(serviceName: name)) { |
| 149 | qCWarning(KAUTH) << "Error registering helper DBus service" << name << m_busConnection.lastError().message(); |
| 150 | return false; |
| 151 | } |
| 152 | |
| 153 | if (!m_busConnection.registerObject(path: QLatin1String("/" ), object: this)) { |
| 154 | qCWarning(KAUTH) << "Error registering helper DBus object:" << m_busConnection.lastError().message(); |
| 155 | return false; |
| 156 | } |
| 157 | |
| 158 | m_name = name; |
| 159 | |
| 160 | return true; |
| 161 | } |
| 162 | |
| 163 | void DBusHelperProxy::setHelperResponder(QObject *o) |
| 164 | { |
| 165 | responder = o; |
| 166 | } |
| 167 | |
| 168 | void DBusHelperProxy::remoteSignalReceived(int t, const QString &action, QByteArray blob) |
| 169 | { |
| 170 | SignalType type = static_cast<SignalType>(t); |
| 171 | QDataStream stream(&blob, QIODevice::ReadOnly); |
| 172 | |
| 173 | if (type == ActionStarted) { |
| 174 | Q_EMIT actionStarted(action); |
| 175 | } else if (type == ActionPerformed) { |
| 176 | ActionReply reply = ActionReply::deserialize(data: blob); |
| 177 | |
| 178 | m_actionsInProgress.removeOne(t: action); |
| 179 | Q_EMIT actionPerformed(action, reply); |
| 180 | } else if (type == DebugMessage) { |
| 181 | int level; |
| 182 | QString message; |
| 183 | |
| 184 | stream >> level >> message; |
| 185 | |
| 186 | debugMessageReceived(t: level, message); |
| 187 | } else if (type == ProgressStepIndicator) { |
| 188 | int step; |
| 189 | stream >> step; |
| 190 | |
| 191 | Q_EMIT progressStep(action, progress: step); |
| 192 | } else if (type == ProgressStepData) { |
| 193 | QVariantMap data; |
| 194 | stream >> data; |
| 195 | Q_EMIT progressStepData(action, data); |
| 196 | } |
| 197 | } |
| 198 | |
| 199 | void DBusHelperProxy::stopAction(const QString &action) |
| 200 | { |
| 201 | Q_UNUSED(action) |
| 202 | //#warning FIXME: The stop request should be action-specific rather than global |
| 203 | m_stopRequest = true; |
| 204 | } |
| 205 | |
| 206 | bool DBusHelperProxy::hasToStopAction() |
| 207 | { |
| 208 | QEventLoop loop; |
| 209 | loop.processEvents(flags: QEventLoop::AllEvents); |
| 210 | |
| 211 | return m_stopRequest; |
| 212 | } |
| 213 | |
| 214 | bool DBusHelperProxy::isCallerAuthorized(const QString &action, const QByteArray &callerID, const QVariantMap &details) |
| 215 | { |
| 216 | Q_UNUSED(callerID); // this only exists for the benefit of the mac backend. We obtain our callerID from dbus! |
| 217 | return BackendsManager::authBackend()->isCallerAuthorized(action, callerID: message().service().toUtf8(), details); |
| 218 | } |
| 219 | |
| 220 | QByteArray DBusHelperProxy::performAction(const QString &action, |
| 221 | const QByteArray &callerID, |
| 222 | const QVariantMap &details, |
| 223 | QByteArray arguments, |
| 224 | const QMap<QString, QDBusUnixFileDescriptor> &fdArguments) |
| 225 | { |
| 226 | if (!responder) { |
| 227 | return ActionReply::NoResponderReply().serialized(); |
| 228 | } |
| 229 | |
| 230 | if (!m_currentAction.isEmpty()) { |
| 231 | return ActionReply::HelperBusyReply().serialized(); |
| 232 | } |
| 233 | |
| 234 | // Make sure we don't try restoring gui variants, in particular QImage/QPixmap/QIcon are super dangerous |
| 235 | // since they end up calling the image loaders and thus are a vector for crashing → executing code |
| 236 | auto origMetaTypeGuiHelper = qMetaTypeGuiHelper; |
| 237 | qMetaTypeGuiHelper = nullptr; |
| 238 | |
| 239 | QVariantMap args; |
| 240 | QDataStream s(&arguments, QIODevice::ReadOnly); |
| 241 | s >> args; |
| 242 | |
| 243 | for (auto [key, value] : fdArguments.asKeyValueRange()) { |
| 244 | args.insert(key, value: QVariant::fromValue(value)); |
| 245 | } |
| 246 | |
| 247 | qMetaTypeGuiHelper = origMetaTypeGuiHelper; |
| 248 | |
| 249 | m_currentAction = action; |
| 250 | Q_EMIT remoteSignal(type: ActionStarted, action, blob: QByteArray()); |
| 251 | QEventLoop e; |
| 252 | e.processEvents(flags: QEventLoop::AllEvents); |
| 253 | |
| 254 | ActionReply retVal; |
| 255 | |
| 256 | QTimer *timer = responder->property(name: "__KAuth_Helper_Shutdown_Timer" ).value<QTimer *>(); |
| 257 | timer->stop(); |
| 258 | |
| 259 | if (isCallerAuthorized(action, callerID, details)) { |
| 260 | QString slotname = action; |
| 261 | if (slotname.startsWith(s: m_name + QLatin1Char('.'))) { |
| 262 | slotname = slotname.right(n: slotname.length() - m_name.length() - 1); |
| 263 | } |
| 264 | |
| 265 | slotname.replace(before: QLatin1Char('.'), after: QLatin1Char('_')); |
| 266 | |
| 267 | // For legacy reasons we could be dealing with ActionReply types (i.e. |
| 268 | // `using namespace KAuth`). Since Qt type names are verbatim this would |
| 269 | // mismatch a return type that is called 'KAuth::ActionReply' and |
| 270 | // vice versa. This effectively required client code to always 'use' the |
| 271 | // namespace as otherwise we'd not be able to call into it. |
| 272 | // To support both scenarios we now dynamically determine what kind of return type |
| 273 | // we deal with and call Q_RETURN_ARG either with or without namespace. |
| 274 | const auto metaObj = responder->metaObject(); |
| 275 | const QString slotSignature(slotname + QStringLiteral("(QVariantMap)" )); |
| 276 | const QMetaMethod method = metaObj->method(index: metaObj->indexOfMethod(qPrintable(slotSignature))); |
| 277 | if (method.isValid()) { |
| 278 | const auto needle = "KAuth::" ; |
| 279 | bool success = false; |
| 280 | if (strncmp(s1: needle, s2: method.typeName(), n: strlen(s: needle)) == 0) { |
| 281 | success = method.invoke(obj: responder, c: Qt::DirectConnection, Q_RETURN_ARG(KAuth::ActionReply, retVal), Q_ARG(QVariantMap, args)); |
| 282 | } else { |
| 283 | success = method.invoke(obj: responder, c: Qt::DirectConnection, Q_RETURN_ARG(ActionReply, retVal), Q_ARG(QVariantMap, args)); |
| 284 | } |
| 285 | if (!success) { |
| 286 | retVal = ActionReply::NoSuchActionReply(); |
| 287 | } |
| 288 | } else { |
| 289 | retVal = ActionReply::NoSuchActionReply(); |
| 290 | } |
| 291 | } else { |
| 292 | retVal = ActionReply::AuthorizationDeniedReply(); |
| 293 | } |
| 294 | |
| 295 | timer->start(); |
| 296 | |
| 297 | Q_EMIT remoteSignal(type: ActionPerformed, action, blob: retVal.serialized()); |
| 298 | e.processEvents(flags: QEventLoop::AllEvents); |
| 299 | m_currentAction.clear(); |
| 300 | m_stopRequest = false; |
| 301 | |
| 302 | return retVal.serialized(); |
| 303 | } |
| 304 | |
| 305 | void DBusHelperProxy::sendDebugMessage(int level, const char *msg) |
| 306 | { |
| 307 | QByteArray blob; |
| 308 | QDataStream stream(&blob, QIODevice::WriteOnly); |
| 309 | |
| 310 | stream << level << QString::fromLocal8Bit(ba: msg); |
| 311 | |
| 312 | Q_EMIT remoteSignal(type: DebugMessage, action: m_currentAction, blob); |
| 313 | } |
| 314 | |
| 315 | void DBusHelperProxy::sendProgressStep(int step) |
| 316 | { |
| 317 | QByteArray blob; |
| 318 | QDataStream stream(&blob, QIODevice::WriteOnly); |
| 319 | |
| 320 | stream << step; |
| 321 | |
| 322 | Q_EMIT remoteSignal(type: ProgressStepIndicator, action: m_currentAction, blob); |
| 323 | } |
| 324 | |
| 325 | void DBusHelperProxy::sendProgressStepData(const QVariantMap &data) |
| 326 | { |
| 327 | QByteArray blob; |
| 328 | QDataStream stream(&blob, QIODevice::WriteOnly); |
| 329 | |
| 330 | stream << data; |
| 331 | |
| 332 | Q_EMIT remoteSignal(type: ProgressStepData, action: m_currentAction, blob); |
| 333 | } |
| 334 | |
| 335 | void debugMessageReceived(int t, const QString &message) |
| 336 | { |
| 337 | QtMsgType type = static_cast<QtMsgType>(t); |
| 338 | switch (type) { |
| 339 | case QtDebugMsg: |
| 340 | qDebug(msg: "Debug message from helper: %s" , message.toLatin1().data()); |
| 341 | break; |
| 342 | case QtInfoMsg: |
| 343 | qInfo(msg: "Info message from helper: %s" , message.toLatin1().data()); |
| 344 | break; |
| 345 | case QtWarningMsg: |
| 346 | qWarning(msg: "Warning from helper: %s" , message.toLatin1().data()); |
| 347 | break; |
| 348 | case QtCriticalMsg: |
| 349 | qCritical(msg: "Critical warning from helper: %s" , message.toLatin1().data()); |
| 350 | break; |
| 351 | case QtFatalMsg: |
| 352 | qFatal(msg: "Fatal error from helper: %s" , message.toLatin1().data()); |
| 353 | break; |
| 354 | } |
| 355 | } |
| 356 | |
| 357 | int DBusHelperProxy::callerUid() const |
| 358 | { |
| 359 | QDBusConnectionInterface *iface = connection().interface(); |
| 360 | if (!iface) { |
| 361 | return -1; |
| 362 | } |
| 363 | return iface->serviceUid(serviceName: message().service()); |
| 364 | } |
| 365 | |
| 366 | } // namespace KAuth |
| 367 | |
| 368 | #include "moc_DBusHelperProxy.cpp" |
| 369 | |