mirror of https://github.com/nextcloud/desktop
298 lines
9.0 KiB
C++
298 lines
9.0 KiB
C++
/*
|
|
* Copyright (C) by Felix Weilbach <felix.weilbach@nextcloud.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
* for more details.
|
|
*/
|
|
|
|
#include "pushnotifications.h"
|
|
#include "creds/abstractcredentials.h"
|
|
#include "account.h"
|
|
|
|
namespace {
|
|
static constexpr int MAX_ALLOWED_FAILED_AUTHENTICATION_ATTEMPTS = 3;
|
|
static constexpr int PING_INTERVAL = 30 * 1000;
|
|
}
|
|
|
|
namespace OCC {
|
|
|
|
Q_LOGGING_CATEGORY(lcPushNotifications, "nextcloud.sync.pushnotifications", QtInfoMsg)
|
|
|
|
PushNotifications::PushNotifications(Account *account, QObject *parent)
|
|
: QObject(parent)
|
|
, _account(account)
|
|
, _webSocket(new QWebSocket(QString(), QWebSocketProtocol::VersionLatest, this))
|
|
{
|
|
connect(_webSocket, QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error), this, &PushNotifications::onWebSocketError);
|
|
connect(_webSocket, &QWebSocket::sslErrors, this, &PushNotifications::onWebSocketSslErrors);
|
|
connect(_webSocket, &QWebSocket::connected, this, &PushNotifications::onWebSocketConnected);
|
|
connect(_webSocket, &QWebSocket::disconnected, this, &PushNotifications::onWebSocketDisconnected);
|
|
connect(_webSocket, &QWebSocket::pong, this, &PushNotifications::onWebSocketPongReceived);
|
|
|
|
connect(&_pingTimer, &QTimer::timeout, this, &PushNotifications::pingWebSocketServer);
|
|
_pingTimer.setSingleShot(true);
|
|
_pingTimer.setInterval(PING_INTERVAL);
|
|
|
|
connect(&_pingTimedOutTimer, &QTimer::timeout, this, &PushNotifications::onPingTimedOut);
|
|
_pingTimedOutTimer.setSingleShot(true);
|
|
_pingTimedOutTimer.setInterval(PING_INTERVAL);
|
|
}
|
|
|
|
PushNotifications::~PushNotifications()
|
|
{
|
|
closeWebSocket();
|
|
}
|
|
|
|
void PushNotifications::setup()
|
|
{
|
|
qCInfo(lcPushNotifications) << "Setup push notifications";
|
|
_failedAuthenticationAttemptsCount = 0;
|
|
reconnectToWebSocket();
|
|
}
|
|
|
|
void PushNotifications::reconnectToWebSocket()
|
|
{
|
|
closeWebSocket();
|
|
openWebSocket();
|
|
}
|
|
|
|
void PushNotifications::closeWebSocket()
|
|
{
|
|
qCInfo(lcPushNotifications) << "Close websocket for account" << _account->url();
|
|
|
|
_pingTimer.stop();
|
|
_pingTimedOutTimer.stop();
|
|
_isReady = false;
|
|
|
|
// Maybe there run some reconnection attempts
|
|
if (_reconnectTimer) {
|
|
_reconnectTimer->stop();
|
|
}
|
|
|
|
disconnect(_webSocket, QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error), this, &PushNotifications::onWebSocketError);
|
|
disconnect(_webSocket, &QWebSocket::sslErrors, this, &PushNotifications::onWebSocketSslErrors);
|
|
|
|
_webSocket->close();
|
|
}
|
|
|
|
void PushNotifications::onWebSocketConnected()
|
|
{
|
|
qCInfo(lcPushNotifications) << "Connected to websocket for account" << _account->url();
|
|
|
|
connect(_webSocket, &QWebSocket::textMessageReceived, this, &PushNotifications::onWebSocketTextMessageReceived, Qt::UniqueConnection);
|
|
|
|
authenticateOnWebSocket();
|
|
}
|
|
|
|
void PushNotifications::authenticateOnWebSocket()
|
|
{
|
|
const auto credentials = _account->credentials();
|
|
const auto username = credentials->user();
|
|
const auto password = credentials->password();
|
|
|
|
// Authenticate
|
|
_webSocket->sendTextMessage(username);
|
|
_webSocket->sendTextMessage(password);
|
|
}
|
|
|
|
void PushNotifications::onWebSocketDisconnected()
|
|
{
|
|
qCInfo(lcPushNotifications) << "Disconnected from websocket for account" << _account->url();
|
|
}
|
|
|
|
void PushNotifications::onWebSocketTextMessageReceived(const QString &message)
|
|
{
|
|
qCInfo(lcPushNotifications) << "Received push notification:" << message;
|
|
|
|
if (message == "notify_file") {
|
|
handleNotifyFile();
|
|
} else if (message == "notify_activity") {
|
|
handleNotifyActivity();
|
|
} else if (message == "notify_notification") {
|
|
handleNotifyNotification();
|
|
} else if (message == "authenticated") {
|
|
handleAuthenticated();
|
|
} else if (message == "err: Invalid credentials") {
|
|
handleInvalidCredentials();
|
|
}
|
|
}
|
|
|
|
void PushNotifications::onWebSocketError(QAbstractSocket::SocketError error)
|
|
{
|
|
// This error gets thrown in testSetup_maxConnectionAttemptsReached_deletePushNotifications after
|
|
// the second connection attempt. I have no idea why this happens. Maybe the socket gets not closed correctly?
|
|
// I think it's fine to ignore this error.
|
|
if (error == QAbstractSocket::UnfinishedSocketOperationError) {
|
|
return;
|
|
}
|
|
|
|
qCWarning(lcPushNotifications) << "Websocket error on with account" << _account->url() << error;
|
|
closeWebSocket();
|
|
emit connectionLost();
|
|
}
|
|
|
|
bool PushNotifications::tryReconnectToWebSocket()
|
|
{
|
|
++_failedAuthenticationAttemptsCount;
|
|
if (_failedAuthenticationAttemptsCount >= MAX_ALLOWED_FAILED_AUTHENTICATION_ATTEMPTS) {
|
|
qCInfo(lcPushNotifications) << "Max authentication attempts reached";
|
|
return false;
|
|
}
|
|
|
|
if (!_reconnectTimer) {
|
|
_reconnectTimer = new QTimer(this);
|
|
}
|
|
|
|
_reconnectTimer->setInterval(_reconnectTimerInterval);
|
|
_reconnectTimer->setSingleShot(true);
|
|
connect(_reconnectTimer, &QTimer::timeout, [this]() {
|
|
reconnectToWebSocket();
|
|
});
|
|
_reconnectTimer->start();
|
|
|
|
return true;
|
|
}
|
|
|
|
void PushNotifications::onWebSocketSslErrors(const QList<QSslError> &errors)
|
|
{
|
|
qCWarning(lcPushNotifications) << "Websocket ssl errors on with account" << _account->url() << errors;
|
|
closeWebSocket();
|
|
emit authenticationFailed();
|
|
}
|
|
|
|
void PushNotifications::openWebSocket()
|
|
{
|
|
// Open websocket
|
|
const auto capabilities = _account->capabilities();
|
|
const auto webSocketUrl = capabilities.pushNotificationsWebSocketUrl();
|
|
|
|
qCInfo(lcPushNotifications) << "Open connection to websocket on" << webSocketUrl << "for account" << _account->url();
|
|
connect(_webSocket, QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error), this, &PushNotifications::onWebSocketError);
|
|
connect(_webSocket, &QWebSocket::sslErrors, this, &PushNotifications::onWebSocketSslErrors);
|
|
_webSocket->open(webSocketUrl);
|
|
}
|
|
|
|
void PushNotifications::setReconnectTimerInterval(uint32_t interval)
|
|
{
|
|
_reconnectTimerInterval = interval;
|
|
}
|
|
|
|
bool PushNotifications::isReady() const
|
|
{
|
|
return _isReady;
|
|
}
|
|
|
|
void PushNotifications::handleAuthenticated()
|
|
{
|
|
qCInfo(lcPushNotifications) << "Authenticated successful on websocket";
|
|
_failedAuthenticationAttemptsCount = 0;
|
|
_isReady = true;
|
|
startPingTimer();
|
|
emit ready();
|
|
|
|
// We maybe reconnected to websocket while being offline for a
|
|
// while. To not miss any notifications that may have happened,
|
|
// emit all the signals once.
|
|
emitFilesChanged();
|
|
emitNotificationsChanged();
|
|
emitActivitiesChanged();
|
|
}
|
|
|
|
void PushNotifications::handleNotifyFile()
|
|
{
|
|
qCInfo(lcPushNotifications) << "Files push notification arrived";
|
|
emitFilesChanged();
|
|
}
|
|
|
|
void PushNotifications::handleInvalidCredentials()
|
|
{
|
|
qCInfo(lcPushNotifications) << "Invalid credentials submitted to websocket";
|
|
if (!tryReconnectToWebSocket()) {
|
|
closeWebSocket();
|
|
emit authenticationFailed();
|
|
}
|
|
}
|
|
|
|
void PushNotifications::handleNotifyNotification()
|
|
{
|
|
qCInfo(lcPushNotifications) << "Push notification arrived";
|
|
emitNotificationsChanged();
|
|
}
|
|
|
|
void PushNotifications::handleNotifyActivity()
|
|
{
|
|
qCInfo(lcPushNotifications) << "Push activity arrived";
|
|
emitActivitiesChanged();
|
|
}
|
|
|
|
void PushNotifications::onWebSocketPongReceived(quint64 /*elapsedTime*/, const QByteArray & /*payload*/)
|
|
{
|
|
qCDebug(lcPushNotifications) << "Pong received in time";
|
|
// We are fine with every kind of pong and don't care about the
|
|
// payload. As long as we receive pongs the server is still alive.
|
|
_pongReceivedFromWebSocketServer = true;
|
|
startPingTimer();
|
|
}
|
|
|
|
void PushNotifications::startPingTimer()
|
|
{
|
|
_pingTimedOutTimer.stop();
|
|
_pingTimer.start();
|
|
}
|
|
|
|
void PushNotifications::startPingTimedOutTimer()
|
|
{
|
|
_pingTimedOutTimer.start();
|
|
}
|
|
|
|
void PushNotifications::pingWebSocketServer()
|
|
{
|
|
qCDebug(lcPushNotifications, "Ping websocket server");
|
|
|
|
_pongReceivedFromWebSocketServer = false;
|
|
|
|
_webSocket->ping({});
|
|
startPingTimedOutTimer();
|
|
}
|
|
|
|
void PushNotifications::onPingTimedOut()
|
|
{
|
|
if (_pongReceivedFromWebSocketServer) {
|
|
qCDebug(lcPushNotifications) << "Websocket respond with a pong in time.";
|
|
return;
|
|
}
|
|
|
|
qCInfo(lcPushNotifications) << "Websocket did not respond with a pong in time. Try to reconnect.";
|
|
// Try again to connect
|
|
setup();
|
|
}
|
|
|
|
void PushNotifications::setPingInterval(int timeoutInterval)
|
|
{
|
|
_pingTimer.setInterval(timeoutInterval);
|
|
_pingTimedOutTimer.setInterval(timeoutInterval);
|
|
}
|
|
|
|
void PushNotifications::emitFilesChanged()
|
|
{
|
|
emit filesChanged(_account);
|
|
}
|
|
|
|
void PushNotifications::emitNotificationsChanged()
|
|
{
|
|
emit notificationsChanged(_account);
|
|
}
|
|
|
|
void PushNotifications::emitActivitiesChanged()
|
|
{
|
|
emit activitiesChanged(_account);
|
|
}
|
|
}
|