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 | |
22 | extern Q_CORE_EXPORT const QtPrivate::QMetaTypeInterface *qMetaTypeGuiHelper; |
23 | |
24 | namespace KAuth |
25 | { |
26 | static void debugMessageReceived(int t, const QString &message); |
27 | |
28 | DBusHelperProxy::DBusHelperProxy() |
29 | : responder(nullptr) |
30 | , m_stopRequest(false) |
31 | , m_busConnection(QDBusConnection::systemBus()) |
32 | { |
33 | } |
34 | |
35 | DBusHelperProxy::DBusHelperProxy(const QDBusConnection &busConnection) |
36 | : responder(nullptr) |
37 | , m_stopRequest(false) |
38 | , m_busConnection(busConnection) |
39 | { |
40 | } |
41 | |
42 | DBusHelperProxy::~DBusHelperProxy() |
43 | { |
44 | } |
45 | |
46 | void 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 | |
58 | void 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 | |
130 | bool 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 | |
149 | void DBusHelperProxy::setHelperResponder(QObject *o) |
150 | { |
151 | responder = o; |
152 | } |
153 | |
154 | void 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 | |
185 | void 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 | |
192 | bool DBusHelperProxy::hasToStopAction() |
193 | { |
194 | QEventLoop loop; |
195 | loop.processEvents(flags: QEventLoop::AllEvents); |
196 | |
197 | return m_stopRequest; |
198 | } |
199 | |
200 | bool 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 | |
206 | QByteArray 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 | |
283 | void 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 | |
293 | void 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 | |
303 | void 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 | |
313 | void 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 | |
335 | int 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 | |