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 | |