1 /*
2 * Copyright (C) by Felix Weilbach <felix.weilbach@nextcloud.com>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful, but
10 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12 * for more details.
13 */
14
15 #include "pushnotifications.h"
16 #include "creds/abstractcredentials.h"
17 #include "account.h"
18
19 namespace {
20 static constexpr int MAX_ALLOWED_FAILED_AUTHENTICATION_ATTEMPTS = 3;
21 static constexpr int PING_INTERVAL = 30 * 1000;
22 }
23
24 namespace OCC {
25
26 Q_LOGGING_CATEGORY(lcPushNotifications, "nextcloud.sync.pushnotifications", QtInfoMsg)
27
PushNotifications(Account * account,QObject * parent)28 PushNotifications::PushNotifications(Account *account, QObject *parent)
29 : QObject(parent)
30 , _account(account)
31 , _webSocket(new QWebSocket(QString(), QWebSocketProtocol::VersionLatest, this))
32 {
33 connect(_webSocket, QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error), this, &PushNotifications::onWebSocketError);
34 connect(_webSocket, &QWebSocket::sslErrors, this, &PushNotifications::onWebSocketSslErrors);
35 connect(_webSocket, &QWebSocket::connected, this, &PushNotifications::onWebSocketConnected);
36 connect(_webSocket, &QWebSocket::disconnected, this, &PushNotifications::onWebSocketDisconnected);
37 connect(_webSocket, &QWebSocket::pong, this, &PushNotifications::onWebSocketPongReceived);
38
39 connect(&_pingTimer, &QTimer::timeout, this, &PushNotifications::pingWebSocketServer);
40 _pingTimer.setSingleShot(true);
41 _pingTimer.setInterval(PING_INTERVAL);
42
43 connect(&_pingTimedOutTimer, &QTimer::timeout, this, &PushNotifications::onPingTimedOut);
44 _pingTimedOutTimer.setSingleShot(true);
45 _pingTimedOutTimer.setInterval(PING_INTERVAL);
46 }
47
~PushNotifications()48 PushNotifications::~PushNotifications()
49 {
50 closeWebSocket();
51 }
52
setup()53 void PushNotifications::setup()
54 {
55 qCInfo(lcPushNotifications) << "Setup push notifications";
56 _failedAuthenticationAttemptsCount = 0;
57 reconnectToWebSocket();
58 }
59
reconnectToWebSocket()60 void PushNotifications::reconnectToWebSocket()
61 {
62 closeWebSocket();
63 openWebSocket();
64 }
65
closeWebSocket()66 void PushNotifications::closeWebSocket()
67 {
68 qCInfo(lcPushNotifications) << "Close websocket for account" << _account->url();
69
70 _pingTimer.stop();
71 _pingTimedOutTimer.stop();
72 _isReady = false;
73
74 // Maybe there run some reconnection attempts
75 if (_reconnectTimer) {
76 _reconnectTimer->stop();
77 }
78
79 disconnect(_webSocket, QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error), this, &PushNotifications::onWebSocketError);
80 disconnect(_webSocket, &QWebSocket::sslErrors, this, &PushNotifications::onWebSocketSslErrors);
81
82 _webSocket->close();
83 }
84
onWebSocketConnected()85 void PushNotifications::onWebSocketConnected()
86 {
87 qCInfo(lcPushNotifications) << "Connected to websocket for account" << _account->url();
88
89 connect(_webSocket, &QWebSocket::textMessageReceived, this, &PushNotifications::onWebSocketTextMessageReceived, Qt::UniqueConnection);
90
91 authenticateOnWebSocket();
92 }
93
authenticateOnWebSocket()94 void PushNotifications::authenticateOnWebSocket()
95 {
96 const auto credentials = _account->credentials();
97 const auto username = credentials->user();
98 const auto password = credentials->password();
99
100 // Authenticate
101 _webSocket->sendTextMessage(username);
102 _webSocket->sendTextMessage(password);
103 }
104
onWebSocketDisconnected()105 void PushNotifications::onWebSocketDisconnected()
106 {
107 qCInfo(lcPushNotifications) << "Disconnected from websocket for account" << _account->url();
108 }
109
onWebSocketTextMessageReceived(const QString & message)110 void PushNotifications::onWebSocketTextMessageReceived(const QString &message)
111 {
112 qCInfo(lcPushNotifications) << "Received push notification:" << message;
113
114 if (message == "notify_file") {
115 handleNotifyFile();
116 } else if (message == "notify_activity") {
117 handleNotifyActivity();
118 } else if (message == "notify_notification") {
119 handleNotifyNotification();
120 } else if (message == "authenticated") {
121 handleAuthenticated();
122 } else if (message == "err: Invalid credentials") {
123 handleInvalidCredentials();
124 }
125 }
126
onWebSocketError(QAbstractSocket::SocketError error)127 void PushNotifications::onWebSocketError(QAbstractSocket::SocketError error)
128 {
129 // This error gets thrown in testSetup_maxConnectionAttemptsReached_deletePushNotifications after
130 // the second connection attempt. I have no idea why this happens. Maybe the socket gets not closed correctly?
131 // I think it's fine to ignore this error.
132 if (error == QAbstractSocket::UnfinishedSocketOperationError) {
133 return;
134 }
135
136 qCWarning(lcPushNotifications) << "Websocket error on with account" << _account->url() << error;
137 closeWebSocket();
138 emit connectionLost();
139 }
140
tryReconnectToWebSocket()141 bool PushNotifications::tryReconnectToWebSocket()
142 {
143 ++_failedAuthenticationAttemptsCount;
144 if (_failedAuthenticationAttemptsCount >= MAX_ALLOWED_FAILED_AUTHENTICATION_ATTEMPTS) {
145 qCInfo(lcPushNotifications) << "Max authentication attempts reached";
146 return false;
147 }
148
149 if (!_reconnectTimer) {
150 _reconnectTimer = new QTimer(this);
151 }
152
153 _reconnectTimer->setInterval(_reconnectTimerInterval);
154 _reconnectTimer->setSingleShot(true);
155 connect(_reconnectTimer, &QTimer::timeout, [this]() {
156 reconnectToWebSocket();
157 });
158 _reconnectTimer->start();
159
160 return true;
161 }
162
onWebSocketSslErrors(const QList<QSslError> & errors)163 void PushNotifications::onWebSocketSslErrors(const QList<QSslError> &errors)
164 {
165 qCWarning(lcPushNotifications) << "Websocket ssl errors on with account" << _account->url() << errors;
166 closeWebSocket();
167 emit authenticationFailed();
168 }
169
openWebSocket()170 void PushNotifications::openWebSocket()
171 {
172 // Open websocket
173 const auto capabilities = _account->capabilities();
174 const auto webSocketUrl = capabilities.pushNotificationsWebSocketUrl();
175
176 qCInfo(lcPushNotifications) << "Open connection to websocket on" << webSocketUrl << "for account" << _account->url();
177 connect(_webSocket, QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error), this, &PushNotifications::onWebSocketError);
178 connect(_webSocket, &QWebSocket::sslErrors, this, &PushNotifications::onWebSocketSslErrors);
179 _webSocket->open(webSocketUrl);
180 }
181
setReconnectTimerInterval(uint32_t interval)182 void PushNotifications::setReconnectTimerInterval(uint32_t interval)
183 {
184 _reconnectTimerInterval = interval;
185 }
186
isReady() const187 bool PushNotifications::isReady() const
188 {
189 return _isReady;
190 }
191
handleAuthenticated()192 void PushNotifications::handleAuthenticated()
193 {
194 qCInfo(lcPushNotifications) << "Authenticated successful on websocket";
195 _failedAuthenticationAttemptsCount = 0;
196 _isReady = true;
197 startPingTimer();
198 emit ready();
199
200 // We maybe reconnected to websocket while being offline for a
201 // while. To not miss any notifications that may have happend,
202 // emit all the signals once.
203 emitFilesChanged();
204 emitNotificationsChanged();
205 emitActivitiesChanged();
206 }
207
handleNotifyFile()208 void PushNotifications::handleNotifyFile()
209 {
210 qCInfo(lcPushNotifications) << "Files push notification arrived";
211 emitFilesChanged();
212 }
213
handleInvalidCredentials()214 void PushNotifications::handleInvalidCredentials()
215 {
216 qCInfo(lcPushNotifications) << "Invalid credentials submitted to websocket";
217 if (!tryReconnectToWebSocket()) {
218 closeWebSocket();
219 emit authenticationFailed();
220 }
221 }
222
handleNotifyNotification()223 void PushNotifications::handleNotifyNotification()
224 {
225 qCInfo(lcPushNotifications) << "Push notification arrived";
226 emitNotificationsChanged();
227 }
228
handleNotifyActivity()229 void PushNotifications::handleNotifyActivity()
230 {
231 qCInfo(lcPushNotifications) << "Push activity arrived";
232 emitActivitiesChanged();
233 }
234
onWebSocketPongReceived(quint64,const QByteArray &)235 void PushNotifications::onWebSocketPongReceived(quint64 /*elapsedTime*/, const QByteArray & /*payload*/)
236 {
237 qCDebug(lcPushNotifications) << "Pong received in time";
238 // We are fine with every kind of pong and don't care about the
239 // payload. As long as we receive pongs the server is still alive.
240 _pongReceivedFromWebSocketServer = true;
241 startPingTimer();
242 }
243
startPingTimer()244 void PushNotifications::startPingTimer()
245 {
246 _pingTimedOutTimer.stop();
247 _pingTimer.start();
248 }
249
startPingTimedOutTimer()250 void PushNotifications::startPingTimedOutTimer()
251 {
252 _pingTimedOutTimer.start();
253 }
254
pingWebSocketServer()255 void PushNotifications::pingWebSocketServer()
256 {
257 qCDebug(lcPushNotifications, "Ping websocket server");
258
259 _pongReceivedFromWebSocketServer = false;
260
261 _webSocket->ping({});
262 startPingTimedOutTimer();
263 }
264
onPingTimedOut()265 void PushNotifications::onPingTimedOut()
266 {
267 if (_pongReceivedFromWebSocketServer) {
268 qCDebug(lcPushNotifications) << "Websocket respond with a pong in time.";
269 return;
270 }
271
272 qCInfo(lcPushNotifications) << "Websocket did not respond with a pong in time. Try to reconnect.";
273 // Try again to connect
274 setup();
275 }
276
setPingInterval(int timeoutInterval)277 void PushNotifications::setPingInterval(int timeoutInterval)
278 {
279 _pingTimer.setInterval(timeoutInterval);
280 _pingTimedOutTimer.setInterval(timeoutInterval);
281 }
282
emitFilesChanged()283 void PushNotifications::emitFilesChanged()
284 {
285 emit filesChanged(_account);
286 }
287
emitNotificationsChanged()288 void PushNotifications::emitNotificationsChanged()
289 {
290 emit notificationsChanged(_account);
291 }
292
emitActivitiesChanged()293 void PushNotifications::emitActivitiesChanged()
294 {
295 emit activitiesChanged(_account);
296 }
297 }
298