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
24extern Q_CORE_EXPORT const QtPrivate::QMetaTypeInterface *qMetaTypeGuiHelper;
25
26namespace KAuth
27{
28static void debugMessageReceived(int t, const QString &message);
29
30DBusHelperProxy::DBusHelperProxy()
31 : responder(nullptr)
32 , m_stopRequest(false)
33 , m_busConnection(QDBusConnection::systemBus())
34{
35 qDBusRegisterMetaType<QMap<QString, QDBusUnixFileDescriptor>>();
36}
37
38DBusHelperProxy::DBusHelperProxy(const QDBusConnection &busConnection)
39 : responder(nullptr)
40 , m_stopRequest(false)
41 , m_busConnection(busConnection)
42{
43 qDBusRegisterMetaType<QMap<QString, QDBusUnixFileDescriptor>>();
44}
45
46DBusHelperProxy::~DBusHelperProxy()
47{
48}
49
50void 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
62void 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
144bool 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
163void DBusHelperProxy::setHelperResponder(QObject *o)
164{
165 responder = o;
166}
167
168void 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
199void 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
206bool DBusHelperProxy::hasToStopAction()
207{
208 QEventLoop loop;
209 loop.processEvents(flags: QEventLoop::AllEvents);
210
211 return m_stopRequest;
212}
213
214bool 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
220QByteArray 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
305void 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
315void 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
325void 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
335void 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
357int 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

source code of kauth/src/backends/dbus/DBusHelperProxy.cpp