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

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