accountstate.cpp 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653
  1. /*
  2. * Copyright (C) by Daniel Molkentin <danimo@owncloud.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. #include "accountstate.h"
  15. #include "accountmanager.h"
  16. #include "remotewipe.h"
  17. #include "account.h"
  18. #include "creds/abstractcredentials.h"
  19. #include "creds/httpcredentials.h"
  20. #include "logger.h"
  21. #include "configfile.h"
  22. #include "ocsnavigationappsjob.h"
  23. #include "ocsuserstatusconnector.h"
  24. #include "pushnotifications.h"
  25. #include <QSettings>
  26. #include <QTimer>
  27. #include <QFontMetrics>
  28. #include <QJsonDocument>
  29. #include <QJsonObject>
  30. #include <QJsonArray>
  31. #include <QNetworkRequest>
  32. #include <QBuffer>
  33. #include <cmath>
  34. namespace OCC {
  35. Q_LOGGING_CATEGORY(lcAccountState, "nextcloud.gui.account.state", QtInfoMsg)
  36. AccountState::AccountState(AccountPtr account)
  37. : QObject()
  38. , _account(account)
  39. , _state(AccountState::Disconnected)
  40. , _connectionStatus(ConnectionValidator::Undefined)
  41. , _maintenanceToConnectedDelay(60000 + (qrand() % (4 * 60000))) // 1-5min delay
  42. , _remoteWipe(new RemoteWipe(_account))
  43. , _isDesktopNotificationsAllowed(true)
  44. {
  45. qRegisterMetaType<AccountState *>("AccountState*");
  46. connect(account.data(), &Account::invalidCredentials,
  47. this, &AccountState::slotHandleRemoteWipeCheck);
  48. connect(account.data(), &Account::credentialsFetched,
  49. this, &AccountState::slotCredentialsFetched);
  50. connect(account.data(), &Account::credentialsAsked,
  51. this, &AccountState::slotCredentialsAsked);
  52. connect(account.data(), &Account::pushNotificationsReady,
  53. this, &AccountState::slotPushNotificationsReady);
  54. connect(account.data(), &Account::serverUserStatusChanged, this,
  55. &AccountState::slotServerUserStatusChanged);
  56. connect(this, &AccountState::isConnectedChanged, [=]{
  57. // Get the Apps available on the server if we're now connected.
  58. if (isConnected()) {
  59. fetchNavigationApps();
  60. }
  61. });
  62. connect(&_checkConnectionTimer, &QTimer::timeout, this, &AccountState::slotCheckConnection);
  63. _checkConnectionTimer.setInterval(ConnectionValidator::DefaultCallingIntervalMsec);
  64. _checkConnectionTimer.start();
  65. QTimer::singleShot(0, this, &AccountState::slotCheckConnection);
  66. }
  67. AccountState::~AccountState() = default;
  68. AccountState *AccountState::loadFromSettings(AccountPtr account, QSettings & /*settings*/)
  69. {
  70. auto accountState = new AccountState(account);
  71. return accountState;
  72. }
  73. void AccountState::writeToSettings(QSettings & /*settings*/)
  74. {
  75. }
  76. AccountPtr AccountState::account() const
  77. {
  78. return _account;
  79. }
  80. AccountState::ConnectionStatus AccountState::connectionStatus() const
  81. {
  82. return _connectionStatus;
  83. }
  84. QStringList AccountState::connectionErrors() const
  85. {
  86. return _connectionErrors;
  87. }
  88. AccountState::State AccountState::state() const
  89. {
  90. return _state;
  91. }
  92. void AccountState::setState(State state)
  93. {
  94. if (_state != state) {
  95. qCInfo(lcAccountState) << "AccountState state change: "
  96. << stateString(_state) << "->" << stateString(state);
  97. State oldState = _state;
  98. _state = state;
  99. if (_state == SignedOut) {
  100. _connectionStatus = ConnectionValidator::Undefined;
  101. _connectionErrors.clear();
  102. } else if (oldState == SignedOut && _state == Disconnected) {
  103. // If we stop being voluntarily signed-out, try to connect and
  104. // auth right now!
  105. checkConnectivity();
  106. } else if (_state == ServiceUnavailable) {
  107. // Check if we are actually down for maintenance.
  108. // To do this we must clear the connection validator that just
  109. // produced the 503. It's finished anyway and will delete itself.
  110. _connectionValidator.clear();
  111. checkConnectivity();
  112. }
  113. if (oldState == Connected || _state == Connected) {
  114. emit isConnectedChanged();
  115. }
  116. if (_state == Connected) {
  117. resetRetryCount();
  118. }
  119. }
  120. // might not have changed but the underlying _connectionErrors might have
  121. emit stateChanged(_state);
  122. }
  123. QString AccountState::stateString(State state)
  124. {
  125. switch (state) {
  126. case SignedOut:
  127. return tr("Signed out");
  128. case Disconnected:
  129. return tr("Disconnected");
  130. case Connected:
  131. return tr("Connected");
  132. case ServiceUnavailable:
  133. return tr("Service unavailable");
  134. case MaintenanceMode:
  135. return tr("Maintenance mode");
  136. case NetworkError:
  137. return tr("Network error");
  138. case ConfigurationError:
  139. return tr("Configuration error");
  140. case AskingCredentials:
  141. return tr("Asking Credentials");
  142. }
  143. return tr("Unknown account state");
  144. }
  145. int AccountState::retryCount() const
  146. {
  147. return _retryCount;
  148. }
  149. void AccountState::increaseRetryCount()
  150. {
  151. ++_retryCount;
  152. }
  153. bool AccountState::isSignedOut() const
  154. {
  155. return _state == SignedOut;
  156. }
  157. void AccountState::signOutByUi()
  158. {
  159. account()->credentials()->forgetSensitiveData();
  160. account()->clearCookieJar();
  161. setState(SignedOut);
  162. }
  163. void AccountState::freshConnectionAttempt()
  164. {
  165. if (isConnected())
  166. setState(Disconnected);
  167. checkConnectivity();
  168. }
  169. void AccountState::signIn()
  170. {
  171. if (_state == SignedOut) {
  172. _waitingForNewCredentials = false;
  173. setState(Disconnected);
  174. }
  175. }
  176. bool AccountState::isConnected() const
  177. {
  178. return _state == Connected;
  179. }
  180. void AccountState::tagLastSuccessfullETagRequest(const QDateTime &tp)
  181. {
  182. _timeOfLastETagCheck = tp;
  183. }
  184. QByteArray AccountState::notificationsEtagResponseHeader() const
  185. {
  186. return _notificationsEtagResponseHeader;
  187. }
  188. void AccountState::setNotificationsEtagResponseHeader(const QByteArray &value)
  189. {
  190. _notificationsEtagResponseHeader = value;
  191. }
  192. QByteArray AccountState::navigationAppsEtagResponseHeader() const
  193. {
  194. return _navigationAppsEtagResponseHeader;
  195. }
  196. void AccountState::setNavigationAppsEtagResponseHeader(const QByteArray &value)
  197. {
  198. _navigationAppsEtagResponseHeader = value;
  199. }
  200. bool AccountState::isDesktopNotificationsAllowed() const
  201. {
  202. return _isDesktopNotificationsAllowed;
  203. }
  204. void AccountState::setDesktopNotificationsAllowed(bool isAllowed)
  205. {
  206. if (_isDesktopNotificationsAllowed == isAllowed) {
  207. return;
  208. }
  209. _isDesktopNotificationsAllowed = isAllowed;
  210. emit desktopNotificationsAllowedChanged();
  211. }
  212. AccountState::ConnectionStatus AccountState::lastConnectionStatus() const
  213. {
  214. return _lastConnectionValidatorStatus;
  215. }
  216. void AccountState::trySignIn()
  217. {
  218. if (isSignedOut() && account()) {
  219. account()->resetRejectedCertificates();
  220. signIn();
  221. }
  222. }
  223. void AccountState::systemOnlineConfigurationChanged()
  224. {
  225. QMetaObject::invokeMethod(this, "slotCheckConnection", Qt::QueuedConnection);
  226. }
  227. void AccountState::checkConnectivity()
  228. {
  229. qCInfo(lcAccountState()) << "check connectivity";
  230. if (isSignedOut() || _waitingForNewCredentials) {
  231. return;
  232. }
  233. if (_connectionValidator) {
  234. qCWarning(lcAccountState) << "ConnectionValidator already running, ignoring" << account()->displayName();
  235. return;
  236. }
  237. // If we never fetched credentials, do that now - otherwise connection attempts
  238. // make little sense, we might be missing client certs.
  239. if (!account()->credentials()->wasFetched()) {
  240. _waitingForNewCredentials = true;
  241. account()->credentials()->fetchFromKeychain();
  242. return;
  243. }
  244. // IF the account is connected the connection check can be skipped
  245. // if the last successful etag check job is not so long ago.
  246. const auto polltime = std::chrono::duration_cast<std::chrono::seconds>(ConfigFile().remotePollInterval());
  247. const auto elapsed = _timeOfLastETagCheck.secsTo(QDateTime::currentDateTimeUtc());
  248. if (isConnected() && _timeOfLastETagCheck.isValid()
  249. && elapsed <= polltime.count()) {
  250. qCDebug(lcAccountState) << account()->displayName() << "The last ETag check succeeded within the last " << polltime.count() << "s (" << elapsed << "s). No connection check needed!";
  251. return;
  252. }
  253. auto *conValidator = new ConnectionValidator(AccountStatePtr(this));
  254. _connectionValidator = conValidator;
  255. connect(conValidator, &ConnectionValidator::connectionResult,
  256. this, &AccountState::slotConnectionValidatorResult);
  257. if (isConnected()) {
  258. // Use a small authed propfind as a minimal ping when we're
  259. // already connected.
  260. conValidator->checkAuthentication();
  261. } else {
  262. // Check the server and then the auth.
  263. // Let's try this for all OS and see if it fixes the Qt issues we have on Linux #4720 #3888 #4051
  264. //#ifdef Q_OS_WIN
  265. // There seems to be a bug in Qt on Windows where QNAM sometimes stops
  266. // working correctly after the computer woke up from sleep. See #2895 #2899
  267. // and #2973.
  268. // As an attempted workaround, reset the QNAM regularly if the account is
  269. // disconnected.
  270. account()->resetNetworkAccessManager();
  271. // If we don't reset the ssl config a second CheckServerJob can produce a
  272. // ssl config that does not have a sensible certificate chain.
  273. account()->setSslConfiguration(QSslConfiguration());
  274. //#endif
  275. conValidator->checkServerAndAuth();
  276. }
  277. }
  278. void AccountState::slotConnectionValidatorResult(ConnectionValidator::Status status, const QStringList &errors)
  279. {
  280. const auto updateRetryCount = [this]() {
  281. increaseRetryCount();
  282. qCInfo(lcAccountState()) << "connection retry count" << retryCount();
  283. _lastCheckConnectionTimer.invalidate();
  284. _lastCheckConnectionTimer.start();
  285. };
  286. const auto resetRetryConnection = [this]() {
  287. qCInfo(lcAccountState) << "reset retry count";
  288. resetRetryCount();
  289. _lastCheckConnectionTimer.invalidate();
  290. _lastCheckConnectionTimer.start();
  291. };
  292. if (isSignedOut()) {
  293. qCWarning(lcAccountState) << "Signed out, ignoring" << status << _account->url().toString();
  294. return;
  295. }
  296. _lastConnectionValidatorStatus = status;
  297. // Come online gradually from 503 or maintenance mode
  298. if (status == ConnectionValidator::Connected
  299. && (_connectionStatus == ConnectionValidator::ServiceUnavailable
  300. || _connectionStatus == ConnectionValidator::MaintenanceMode)) {
  301. if (!_timeSinceMaintenanceOver.isValid()) {
  302. qCInfo(lcAccountState) << "AccountState reconnection: delaying for"
  303. << _maintenanceToConnectedDelay << "ms";
  304. _timeSinceMaintenanceOver.start();
  305. QTimer::singleShot(_maintenanceToConnectedDelay + 100, this, &AccountState::checkConnectivity);
  306. return;
  307. } else if (_timeSinceMaintenanceOver.elapsed() < _maintenanceToConnectedDelay) {
  308. qCInfo(lcAccountState) << "AccountState reconnection: only"
  309. << _timeSinceMaintenanceOver.elapsed() << "ms have passed";
  310. return;
  311. }
  312. }
  313. if (_connectionStatus != status) {
  314. qCInfo(lcAccountState) << "AccountState connection status change: "
  315. << _connectionStatus << "->"
  316. << status;
  317. _connectionStatus = status;
  318. emit stateChanged(_state);
  319. }
  320. _connectionErrors = errors;
  321. switch (status) {
  322. case ConnectionValidator::Connected:
  323. if (_state != Connected) {
  324. setState(Connected);
  325. resetRetryConnection();
  326. // Get the Apps available on the server.
  327. fetchNavigationApps();
  328. // Setup push notifications after a successful connection
  329. account()->trySetupPushNotifications();
  330. }
  331. break;
  332. case ConnectionValidator::Undefined:
  333. case ConnectionValidator::NotConfigured:
  334. setState(Disconnected);
  335. updateRetryCount();
  336. break;
  337. case ConnectionValidator::ServerVersionMismatch:
  338. setState(ConfigurationError);
  339. break;
  340. case ConnectionValidator::StatusNotFound:
  341. // This can happen either because the server does not exist
  342. // or because we are having network issues. The latter one is
  343. // much more likely, so keep trying to connect.
  344. setState(NetworkError);
  345. updateRetryCount();
  346. break;
  347. case ConnectionValidator::CredentialsWrong:
  348. case ConnectionValidator::CredentialsNotReady:
  349. handleInvalidCredentials();
  350. break;
  351. case ConnectionValidator::SslError:
  352. setState(SignedOut);
  353. break;
  354. case ConnectionValidator::ServiceUnavailable:
  355. _timeSinceMaintenanceOver.invalidate();
  356. setState(ServiceUnavailable);
  357. break;
  358. case ConnectionValidator::MaintenanceMode:
  359. _timeSinceMaintenanceOver.invalidate();
  360. setState(MaintenanceMode);
  361. break;
  362. case ConnectionValidator::Timeout:
  363. setState(NetworkError);
  364. updateRetryCount();
  365. break;
  366. }
  367. }
  368. void AccountState::slotHandleRemoteWipeCheck()
  369. {
  370. // make sure it changes account state and icons
  371. signOutByUi();
  372. qCInfo(lcAccountState) << "Invalid credentials for" << _account->url().toString()
  373. << "checking for remote wipe request";
  374. _waitingForNewCredentials = false;
  375. setState(SignedOut);
  376. }
  377. void AccountState::handleInvalidCredentials()
  378. {
  379. if (isSignedOut() || _waitingForNewCredentials)
  380. return;
  381. qCInfo(lcAccountState) << "Invalid credentials for" << _account->url().toString()
  382. << "asking user";
  383. _waitingForNewCredentials = true;
  384. setState(AskingCredentials);
  385. if (account()->credentials()->ready()) {
  386. account()->credentials()->invalidateToken();
  387. }
  388. if (auto creds = qobject_cast<HttpCredentials *>(account()->credentials())) {
  389. if (creds->refreshAccessToken())
  390. return;
  391. }
  392. account()->credentials()->askFromUser();
  393. }
  394. void AccountState::slotCredentialsFetched(AbstractCredentials *)
  395. {
  396. // Make a connection attempt, no matter whether the credentials are
  397. // ready or not - we want to check whether we can get an SSL connection
  398. // going before bothering the user for a password.
  399. qCInfo(lcAccountState) << "Fetched credentials for" << _account->url().toString()
  400. << "attempting to connect";
  401. _waitingForNewCredentials = false;
  402. checkConnectivity();
  403. }
  404. void AccountState::slotCredentialsAsked(AbstractCredentials *credentials)
  405. {
  406. qCInfo(lcAccountState) << "Credentials asked for" << _account->url().toString()
  407. << "are they ready?" << credentials->ready();
  408. _waitingForNewCredentials = false;
  409. if (!credentials->ready()) {
  410. // User canceled the connection or did not give a password
  411. setState(SignedOut);
  412. return;
  413. }
  414. if (_connectionValidator) {
  415. // When new credentials become available we always want to restart the
  416. // connection validation, even if it's currently running.
  417. _connectionValidator->deleteLater();
  418. _connectionValidator = nullptr;
  419. }
  420. checkConnectivity();
  421. }
  422. std::unique_ptr<QSettings> AccountState::settings()
  423. {
  424. auto s = ConfigFile::settingsWithGroup(QLatin1String("Accounts"));
  425. s->beginGroup(_account->id());
  426. return s;
  427. }
  428. void AccountState::fetchNavigationApps(){
  429. auto *job = new OcsNavigationAppsJob(_account);
  430. job->addRawHeader("If-None-Match", navigationAppsEtagResponseHeader());
  431. connect(job, &OcsNavigationAppsJob::appsJobFinished, this, &AccountState::slotNavigationAppsFetched);
  432. connect(job, &OcsNavigationAppsJob::etagResponseHeaderReceived, this, &AccountState::slotEtagResponseHeaderReceived);
  433. connect(job, &OcsNavigationAppsJob::ocsError, this, &AccountState::slotOcsError);
  434. job->getNavigationApps();
  435. }
  436. void AccountState::resetRetryCount()
  437. {
  438. _retryCount = 0;
  439. }
  440. void AccountState::slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode){
  441. if(statusCode == 200){
  442. qCDebug(lcAccountState) << "New navigation apps ETag Response Header received " << value;
  443. setNavigationAppsEtagResponseHeader(value);
  444. }
  445. }
  446. void AccountState::slotOcsError(int statusCode, const QString &message)
  447. {
  448. qCDebug(lcAccountState) << "Error " << statusCode << " while fetching new navigation apps: " << message;
  449. }
  450. void AccountState::slotCheckConnection()
  451. {
  452. if (_lastCheckConnectionTimer.isValid()) {
  453. static constexpr auto DefaultCallingIntervalMaxMsec = static_cast<int>(ConnectionValidator::DefaultCallingIntervalMsec) * 8;
  454. const auto minDelay = std::max(retryCount() * ConnectionValidator::DefaultCallingIntervalMsec,
  455. static_cast<int>(ConnectionValidator::DefaultCallingIntervalMsec));
  456. const auto currentDelay = std::min(minDelay, DefaultCallingIntervalMaxMsec);
  457. if (!_lastCheckConnectionTimer.hasExpired(currentDelay - 1)) {
  458. qCInfo(lcAccountState()) << "timer has not expired: do not check now" << _lastCheckConnectionTimer.elapsed() << currentDelay;
  459. return;
  460. }
  461. }
  462. const auto currentState = state();
  463. // Don't check if we're manually signed out or
  464. // when the error is permanent.
  465. const auto pushNotifications = account()->pushNotifications();
  466. const auto pushNotificationsAvailable = (pushNotifications && pushNotifications->isReady());
  467. if (currentState != AccountState::SignedOut && currentState != AccountState::ConfigurationError
  468. && currentState != AccountState::AskingCredentials && !pushNotificationsAvailable) {
  469. checkConnectivity();
  470. } else if (currentState == AccountState::SignedOut && lastConnectionStatus() == AccountState::ConnectionStatus::SslError) {
  471. qCWarning(lcAccountState()) << "Account is signed out due to SSL Handshake error. Going to perform a sign-in attempt...";
  472. trySignIn();
  473. }
  474. }
  475. void AccountState::slotPushNotificationsReady()
  476. {
  477. if (state() != AccountState::State::Connected) {
  478. setState(AccountState::State::Connected);
  479. }
  480. }
  481. void AccountState::slotServerUserStatusChanged()
  482. {
  483. setDesktopNotificationsAllowed(_account->userStatusConnector()->userStatus().state() != UserStatus::OnlineStatus::DoNotDisturb);
  484. }
  485. void AccountState::slotNavigationAppsFetched(const QJsonDocument &reply, int statusCode)
  486. {
  487. if(_account){
  488. if (statusCode == 304) {
  489. qCWarning(lcAccountState) << "Status code " << statusCode << " Not Modified - No new navigation apps.";
  490. } else {
  491. _apps.clear();
  492. if(!reply.isEmpty()){
  493. auto element = reply.object().value("ocs").toObject().value("data");
  494. const auto navLinks = element.toArray();
  495. if(navLinks.size() > 0){
  496. for (const QJsonValue &value : navLinks) {
  497. auto navLink = value.toObject();
  498. auto *app = new AccountApp(navLink.value("name").toString(), QUrl(navLink.value("href").toString()),
  499. navLink.value("id").toString(), QUrl(navLink.value("icon").toString()));
  500. _apps << app;
  501. }
  502. }
  503. }
  504. emit hasFetchedNavigationApps();
  505. }
  506. }
  507. }
  508. AccountAppList AccountState::appList() const
  509. {
  510. return _apps;
  511. }
  512. AccountApp* AccountState::findApp(const QString &appId) const
  513. {
  514. if(!appId.isEmpty()) {
  515. const auto apps = appList();
  516. const auto it = std::find_if(apps.cbegin(), apps.cend(), [appId](const auto &app) {
  517. return app->id() == appId;
  518. });
  519. if (it != apps.cend()) {
  520. return *it;
  521. }
  522. }
  523. return nullptr;
  524. }
  525. /*-------------------------------------------------------------------------------------*/
  526. AccountApp::AccountApp(const QString &name, const QUrl &url,
  527. const QString &id, const QUrl &iconUrl,
  528. QObject *parent)
  529. : QObject(parent)
  530. , _name(name)
  531. , _url(url)
  532. , _id(id)
  533. , _iconUrl(iconUrl)
  534. {
  535. }
  536. QString AccountApp::name() const
  537. {
  538. return _name;
  539. }
  540. QUrl AccountApp::url() const
  541. {
  542. return _url;
  543. }
  544. QString AccountApp::id() const
  545. {
  546. return _id;
  547. }
  548. QUrl AccountApp::iconUrl() const
  549. {
  550. return _iconUrl;
  551. }
  552. /*-------------------------------------------------------------------------------------*/
  553. } // namespace OCC