KUnifiedPush

connector.cpp
1/*
2 SPDX-FileCopyrightText: 2022 Volker Krause <vkrause@kde.org>
3 SPDX-License-Identifier: LGPL-2.0-or-later
4*/
5
6#include "connector.h"
7#include "connector_p.h"
8#include "logging.h"
9
10#include "../shared/connectorutils_p.h"
11#include "../shared/unifiedpush-constants.h"
12
13#include <QFile>
14#include <QSettings>
15#include <QStandardPaths>
16#include <QUuid>
17
18using namespace KUnifiedPush;
19
20ConnectorPrivate::ConnectorPrivate(Connector *qq)
21 : QObject(qq)
22 , q(qq)
23{
24 init();
25}
26
27ConnectorPrivate::~ConnectorPrivate()
28{
29 deinit();
30}
31
32void ConnectorPrivate::Message(const QString &token, const QByteArray &message, const QString &messageIdentifier)
33{
34 qCDebug(Log) << token << message << messageIdentifier << m_contentEnc.hasKeys();
35 if (token != m_token) {
36 qCWarning(Log) << "Got message for a different token??";
37 return;
38 }
39 if (m_contentEnc.hasKeys()) {
40 const auto decrypted = m_contentEnc.decrypt(message);
41 qCDebug(Log) << token << decrypted;
42 if (!decrypted.isEmpty()) {
43 Q_EMIT q->messageReceived(decrypted);
44 return;
45 }
46 }
47
48 Q_EMIT q->messageReceived(message);
49}
50
51QVariantMap ConnectorPrivate::Message(const QVariantMap &args)
52{
53 const auto token = args.value(UP_ARG_TOKEN).toString();
54 const auto message = args.value(UP_ARG_MESSAGE).toByteArray();
55 const auto id = args.value(UP_ARG_MESSAGE_IDENTIFIER).toString();
56 Message(token, message, id);
57
58 QVariantMap r;
59 if (!id.isEmpty()) {
60 r.insert(UP_ARG_MESSAGE_IDENTIFIER, id);
61 }
62 return r;
63}
64
65void ConnectorPrivate::NewEndpoint(const QString &token, const QString &endpoint)
66{
67 qCDebug(Log) << token << endpoint;
68 if (token != m_token) {
69 qCWarning(Log) << "Got new endpoint for a different token??";
70 return;
71 }
72
73 // ### Gotify workaround...
74 QString actuallyWorkingEndpoint(endpoint);
75 actuallyWorkingEndpoint.replace(QLatin1String("/UP?"), QLatin1String("/message?"));
76
77 if (m_endpoint != actuallyWorkingEndpoint) {
78 m_endpoint = actuallyWorkingEndpoint;
79 Q_EMIT q->endpointChanged(m_endpoint);
80 }
81 storeState();
82 setState(Connector::Registered);
83}
84
85QVariantMap ConnectorPrivate::NewEndpoint(const QVariantMap &args)
86{
87 const auto token = args.value(UP_ARG_TOKEN).toString();
88 const auto endpoint = args.value(UP_ARG_ENDPOINT).toString();
89 NewEndpoint(token, endpoint);
90 return {};
91}
92
93void ConnectorPrivate::Unregistered(const QString &token)
94{
95 qCDebug(Log) << token;
96
97 // confirmation of our unregistration request
98 if (token.isEmpty() || (token == m_token && m_currentCommand == Command::Unregister)) {
99 m_token.clear();
100 m_endpoint.clear();
101 Q_EMIT q->endpointChanged(m_endpoint);
102 const auto res = QFile::remove(stateFile());
103 qCDebug(Log) << "Removing" << stateFile() << res;
104 setState(Connector::Unregistered);
105 }
106
107 // we got unregistered by the distributor
108 else if (token == m_token) {
109 m_endpoint.clear();
110 Q_EMIT q->endpointChanged(m_endpoint);
111 setState(Connector::Unregistered);
112 storeState();
113 }
114
115 if (m_currentCommand == Command::Unregister) {
116 m_currentCommand = Command::None;
117 }
118 processNextCommand();
119}
120
121QVariantMap ConnectorPrivate::Unregistered(const QVariantMap &args)
122{
123 const auto token = args.value(UP_ARG_TOKEN).toString();
124 Unregistered(token);
125 return {};
126}
127
128QString ConnectorPrivate::stateFile() const
129{
130 return QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QLatin1String("/kunifiedpush-") + m_serviceName;
131}
132
133void ConnectorPrivate::loadState()
134{
135 QSettings settings(stateFile(), QSettings::IniFormat);
136 settings.beginGroup("Client");
137 m_token = settings.value("Token").toString();
138 m_endpoint = settings.value("Endpoint").toString();
139 m_description = settings.value("Description").toString();
140 m_vapidRequired = settings.value("VapidRequired", false).toBool();
141 m_vapidPublicKey = settings.value("VapidPublicKey").toString();
142
143 const auto pubKey = settings.value("ContentEncryptionPublicKey").toByteArray();
144 const auto privKey = settings.value("ContentEncryptionPrivateKey").toByteArray();
145 const auto authSec = settings.value("contentEncryptionAuthSecret").toByteArray();
146 if (!pubKey.isEmpty() && !privKey.isEmpty() && !authSec.isEmpty()) {
147 m_contentEnc = ContentEncryption(pubKey, privKey, authSec);
148 }
149}
150
151void ConnectorPrivate::storeState() const
152{
153 QSettings settings(stateFile(), QSettings::IniFormat);
154 settings.beginGroup("Client");
155 settings.setValue("Token", m_token);
156 settings.setValue("Endpoint", m_endpoint);
157 settings.setValue("Description", m_description);
158 settings.setValue("VapidRequired", m_vapidRequired);
159 settings.setValue("VapidPublicKey", m_vapidPublicKey);
160
161 if (m_contentEnc.hasKeys()) {
162 settings.setValue("ContentEncryptionPublicKey", m_contentEnc.publicKey());
163 settings.setValue("ContentEncryptionPrivateKey", m_contentEnc.privateKey());
164 settings.setValue("contentEncryptionAuthSecret", m_contentEnc.authSecret());
165 }
166}
167
168void ConnectorPrivate::setDistributor(const QString &distServiceName)
169{
170 if (distServiceName.isEmpty()) {
171 qCWarning(Log) << "No UnifiedPush distributor found.";
172 setState(Connector::NoDistributor);
173 return;
174 }
175
176 doSetDistributor(distServiceName);
177 qCDebug(Log) << "Selected distributor" << distServiceName;
178 setState(Connector::Unregistered);
179
180 if (!m_token.isEmpty()) { // re-register if we have been registered before
181 q->registerClient(m_description);
182 }
183}
184
185void ConnectorPrivate::registrationFailed(const QString &token, const QString &reason)
186{
187 qCDebug(Log) << token << reason;
188 if (token != m_token) {
189 return;
190 }
191
192 // TODO error code/error message
193 setState(Connector::Error);
194}
195
196void ConnectorPrivate::setState(Connector::State state)
197{
198 qCDebug(Log) << state;
199 if (m_state == state) {
200 return;
201 }
202
203 m_state = state;
204 Q_EMIT q->stateChanged(m_state);
205}
206
207void ConnectorPrivate::addCommand(ConnectorPrivate::Command cmd)
208{
209 // ignore new commands that are already in the queue or cancel each other out
210 if (!m_commandQueue.empty()) {
211 if (m_commandQueue.back() == cmd) {
212 return;
213 }
214 if ((m_commandQueue.back() == Command::Register && cmd == Command::Unregister) || (m_commandQueue.back() == Command::Unregister && cmd == Command::Register)) {
215 m_commandQueue.pop_back();
216 return;
217 }
218 } else if (m_currentCommand == cmd) {
219 return;
220 }
221
222 m_commandQueue.push_back(cmd);
223 processNextCommand();
224}
225
226bool ConnectorPrivate::isNextCommandReady() const
227{
228 assert(!m_commandQueue.empty());
229
230 if (m_commandQueue.front() == Command::Register) {
231 return !m_vapidRequired || !m_vapidPublicKey.isEmpty();
232 }
233 return true;
234}
235
236void ConnectorPrivate::processNextCommand()
237{
238 if (m_currentCommand != Command::None || !hasDistributor() || m_commandQueue.empty() || !isNextCommandReady()) {
239 return;
240 }
241
242 m_currentCommand = m_commandQueue.front();
243 m_commandQueue.pop_front();
244
245 switch (m_currentCommand) {
246 case Command::None:
247 break;
248 case Command::Register:
249 {
250 if (m_state == Connector::Registered) {
251 m_currentCommand = Command::None;
252 break;
253 }
254 setState(Connector::Registering);
255 if (m_token.isEmpty()) {
256 m_token = QUuid::createUuid().toString();
257 }
258 qCDebug(Log) << "Registering";
259 doRegister();
260 break;
261 }
263 if (m_state == Connector::Unregistered) {
264 m_currentCommand = Command::None;
265 break;
266 }
267 qCDebug(Log) << "Unregistering";
268 doUnregister();
269 break;
270 }
271
272 processNextCommand();
273}
274
275void ConnectorPrivate::ensureKeys()
276{
277 if (m_contentEnc.hasKeys()) {
278 return;
279 }
280 m_contentEnc = ContentEncryption::generateKeys();
281 storeState();
282}
283
284
286 : QObject(parent)
287 , d(new ConnectorPrivate(this))
288{
289 d->m_serviceName = serviceName;
290 if (d->m_serviceName.isEmpty()) {
291 qCWarning(Log) << "empty D-Bus service name!";
292 return;
293 }
294
295 d->loadState();
296 d->setDistributor(ConnectorUtils::selectDistributor());
297}
298
299Connector::~Connector() = default;
300
301QString Connector::endpoint() const
302{
303 return d->m_endpoint;
304}
305
306void Connector::registerClient(const QString &description)
307{
308 qCDebug(Log) << d->m_state;
309 d->m_description = description;
310 d->addCommand(ConnectorPrivate::Command::Register);
311}
312
314{
315 qCDebug(Log) << d->m_state;
316 d->addCommand(ConnectorPrivate::Command::Unregister);
317}
318
319Connector::State Connector::state() const
320{
321 return d->m_state;
322}
323
324QString Connector::vapidPublicKey() const
325{
326 return d->m_vapidPublicKey;
327}
328
329void Connector::setVapidPublicKey(const QString &vapidPublicKey)
330{
331 if (d->m_vapidPublicKey == vapidPublicKey) {
332 return;
333 }
334
335 d->m_vapidPublicKey = vapidPublicKey;
337
338 // if the VAPID key changed after we had previously registered we need to re-register
339 if (!d->m_token.isEmpty()) {
340 d->addCommand(ConnectorPrivate::Command::Unregister);
341 d->addCommand(ConnectorPrivate::Command::None); // no-op as a barrier to prevent the other two command from being merged
342 d->addCommand(ConnectorPrivate::Command::Register);
343 } else {
344 d->processNextCommand();
345 }
346}
347
348bool Connector::vapidPublicKeyRequired() const
349{
350 return d->m_vapidRequired;
351}
352
354{
355 if (d->m_vapidRequired == vapidRequired) {
356 return;
357 }
358
359 d->m_vapidRequired = vapidRequired;
361 d->processNextCommand();
362}
363
365{
366 d->ensureKeys();
367 return d->m_contentEnc.publicKey();
368}
369
371{
372 d->ensureKeys();
373 return d->m_contentEnc.authSecret();
374}
375
376#include "moc_connector.cpp"
377#include "moc_connector_p.cpp"
@ Unregister
unregistration requested by client
Definition command.h:22
Client connector to UnifiedPush.
Definition connector.h:23
Connector(const QString &serviceName, QObject *parent=nullptr)
Create a new connector instance.
void setVapidPublicKeyRequired(bool vapidRequired)
Sets whether a Voluntary Application Server Identification (VAPID) public key is required before regi...
void vapidPublicKeyChanged()
Emitted when the VAPID public key changed.
QByteArray contentEncryptionPublicKey() const
Content encryption user agent public key.
void unregisterClient()
Unregister this client.
QByteArray contentEncryptionAuthSecret() const
Content encryption authentication secret.
State
Connector state.
Definition connector.h:60
@ Error
Any other error condition.
Definition connector.h:65
@ NoDistributor
Connector cannot find a UnifiedPush distributor to register at.
Definition connector.h:64
@ Registering
Connector is registering with the push provider.
Definition connector.h:62
@ Registered
Connector is registered and thus operational.
Definition connector.h:63
@ Unregistered
Connector is not yet registered, or explicitly unregistered.
Definition connector.h:61
void vapidPublicKeyRequiredChanged()
Emitted when the VAPID public key required property changed.
void registerClient(const QString &description)
Register this client.
void setVapidPublicKey(const QString &vapidPublicKey)
Sets the Voluntary Application Server Identification (VAPID) public key of the corresponding applicat...
A received push notification message.
Definition message.h:15
Client-side integration with UnifiedPush.
Definition connector.h:14
QCA_EXPORT void init()
QCA_EXPORT void deinit()
bool remove()
QObject(QObject *parent)
Q_EMITQ_EMIT
QObject * parent() const const
QString writableLocation(StandardLocation type)
bool isEmpty() const const
QUuid createUuid()
QString toString(StringFormat mode) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Apr 25 2025 12:05:39 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.