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 | |
15 | #include "accountstate.h" |
16 | #include "accountmanager.h" |
17 | #include "account.h" |
18 | #include "creds/abstractcredentials.h" |
19 | #include "creds/httpcredentials.h" |
20 | #include "logger.h" |
21 | #include "configfile.h" |
22 | |
23 | #include <QSettings> |
24 | #include <QTimer> |
25 | #include <qfontmetrics.h> |
26 | |
27 | namespace OCC { |
28 | |
29 | Q_LOGGING_CATEGORY(lcAccountState, "gui.account.state" , QtInfoMsg) |
30 | |
31 | AccountState::AccountState(AccountPtr account) |
32 | : QObject() |
33 | , _account(account) |
34 | , _state(AccountState::Disconnected) |
35 | , _connectionStatus(ConnectionValidator::Undefined) |
36 | , _waitingForNewCredentials(false) |
37 | , _maintenanceToConnectedDelay(60000 + (qrand() % (4 * 60000))) // 1-5min delay |
38 | { |
39 | qRegisterMetaType<AccountState *>("AccountState*" ); |
40 | |
41 | connect(account.data(), &Account::invalidCredentials, |
42 | this, &AccountState::slotInvalidCredentials); |
43 | connect(account.data(), &Account::credentialsFetched, |
44 | this, &AccountState::slotCredentialsFetched); |
45 | connect(account.data(), &Account::credentialsAsked, |
46 | this, &AccountState::slotCredentialsAsked); |
47 | _timeSinceLastETagCheck.invalidate(); |
48 | } |
49 | |
50 | AccountState::~AccountState() |
51 | { |
52 | } |
53 | |
54 | AccountState *AccountState::loadFromSettings(AccountPtr account, QSettings & /*settings*/) |
55 | { |
56 | auto accountState = new AccountState(account); |
57 | return accountState; |
58 | } |
59 | |
60 | void AccountState::writeToSettings(QSettings & /*settings*/) |
61 | { |
62 | } |
63 | |
64 | AccountPtr AccountState::account() const |
65 | { |
66 | return _account; |
67 | } |
68 | |
69 | AccountState::ConnectionStatus AccountState::connectionStatus() const |
70 | { |
71 | return _connectionStatus; |
72 | } |
73 | |
74 | QStringList AccountState::connectionErrors() const |
75 | { |
76 | return _connectionErrors; |
77 | } |
78 | |
79 | AccountState::State AccountState::state() const |
80 | { |
81 | return _state; |
82 | } |
83 | |
84 | void AccountState::setState(State state) |
85 | { |
86 | if (_state != state) { |
87 | qCInfo(lcAccountState) << "AccountState state change: " |
88 | << stateString(_state) << "->" << stateString(state); |
89 | State oldState = _state; |
90 | _state = state; |
91 | |
92 | if (_state == SignedOut) { |
93 | _connectionStatus = ConnectionValidator::Undefined; |
94 | _connectionErrors.clear(); |
95 | } else if (oldState == SignedOut && _state == Disconnected) { |
96 | // If we stop being voluntarily signed-out, try to connect and |
97 | // auth right now! |
98 | checkConnectivity(); |
99 | } else if (_state == ServiceUnavailable) { |
100 | // Check if we are actually down for maintenance. |
101 | // To do this we must clear the connection validator that just |
102 | // produced the 503. It's finished anyway and will delete itself. |
103 | _connectionValidator.clear(); |
104 | checkConnectivity(); |
105 | } |
106 | if (oldState == Connected || _state == Connected) { |
107 | emit isConnectedChanged(); |
108 | } |
109 | } |
110 | |
111 | // might not have changed but the underlying _connectionErrors might have |
112 | emit stateChanged(_state); |
113 | } |
114 | |
115 | QString AccountState::stateString(State state) |
116 | { |
117 | switch (state) { |
118 | case SignedOut: |
119 | return tr("Signed out" ); |
120 | case Disconnected: |
121 | return tr("Disconnected" ); |
122 | case Connected: |
123 | return tr("Connected" ); |
124 | case ServiceUnavailable: |
125 | return tr("Service unavailable" ); |
126 | case MaintenanceMode: |
127 | return tr("Maintenance mode" ); |
128 | case NetworkError: |
129 | return tr("Network error" ); |
130 | case ConfigurationError: |
131 | return tr("Configuration error" ); |
132 | case AskingCredentials: |
133 | return tr("Asking Credentials" ); |
134 | } |
135 | return tr("Unknown account state" ); |
136 | } |
137 | |
138 | bool AccountState::isSignedOut() const |
139 | { |
140 | return _state == SignedOut; |
141 | } |
142 | |
143 | void AccountState::signOutByUi() |
144 | { |
145 | account()->credentials()->forgetSensitiveData(); |
146 | setState(SignedOut); |
147 | } |
148 | |
149 | void AccountState::freshConnectionAttempt() |
150 | { |
151 | if (isConnected()) |
152 | setState(Disconnected); |
153 | checkConnectivity(); |
154 | } |
155 | |
156 | void AccountState::signIn() |
157 | { |
158 | if (_state == SignedOut) { |
159 | _waitingForNewCredentials = false; |
160 | setState(Disconnected); |
161 | } |
162 | } |
163 | |
164 | bool AccountState::isConnected() const |
165 | { |
166 | return _state == Connected; |
167 | } |
168 | |
169 | void AccountState::tagLastSuccessfullETagRequest() |
170 | { |
171 | _timeSinceLastETagCheck.start(); |
172 | } |
173 | |
174 | void AccountState::checkConnectivity() |
175 | { |
176 | if (isSignedOut() || _waitingForNewCredentials) { |
177 | return; |
178 | } |
179 | |
180 | if (_connectionValidator) { |
181 | qCWarning(lcAccountState) << "ConnectionValidator already running, ignoring" << account()->displayName(); |
182 | return; |
183 | } |
184 | |
185 | // If we never fetched credentials, do that now - otherwise connection attempts |
186 | // make little sense, we might be missing client certs. |
187 | if (!account()->credentials()->wasFetched()) { |
188 | _waitingForNewCredentials = true; |
189 | account()->credentials()->fetchFromKeychain(); |
190 | return; |
191 | } |
192 | |
193 | // IF the account is connected the connection check can be skipped |
194 | // if the last successful etag check job is not so long ago. |
195 | ConfigFile cfg; |
196 | std::chrono::milliseconds polltime = cfg.remotePollInterval(); |
197 | |
198 | if (isConnected() && _timeSinceLastETagCheck.isValid() |
199 | && _timeSinceLastETagCheck.hasExpired(polltime.count())) { |
200 | qCDebug(lcAccountState) << account()->displayName() << "The last ETag check succeeded within the last " << polltime.count() / 1000 << " secs. No connection check needed!" ; |
201 | return; |
202 | } |
203 | |
204 | ConnectionValidator *conValidator = new ConnectionValidator(account()); |
205 | _connectionValidator = conValidator; |
206 | connect(conValidator, &ConnectionValidator::connectionResult, |
207 | this, &AccountState::slotConnectionValidatorResult); |
208 | if (isConnected()) { |
209 | // Use a small authed propfind as a minimal ping when we're |
210 | // already connected. |
211 | conValidator->checkAuthentication(); |
212 | } else { |
213 | // Check the server and then the auth. |
214 | |
215 | // Let's try this for all OS and see if it fixes the Qt issues we have on Linux #4720 #3888 #4051 |
216 | //#ifdef Q_OS_WIN |
217 | // There seems to be a bug in Qt on Windows where QNAM sometimes stops |
218 | // working correctly after the computer woke up from sleep. See #2895 #2899 |
219 | // and #2973. |
220 | // As an attempted workaround, reset the QNAM regularly if the account is |
221 | // disconnected. |
222 | account()->resetNetworkAccessManager(); |
223 | |
224 | // If we don't reset the ssl config a second CheckServerJob can produce a |
225 | // ssl config that does not have a sensible certificate chain. |
226 | account()->setSslConfiguration(QSslConfiguration()); |
227 | //#endif |
228 | conValidator->checkServerAndAuth(); |
229 | } |
230 | } |
231 | |
232 | void AccountState::slotConnectionValidatorResult(ConnectionValidator::Status status, const QStringList &errors) |
233 | { |
234 | if (isSignedOut()) { |
235 | qCWarning(lcAccountState) << "Signed out, ignoring" << status << _account->url().toString(); |
236 | return; |
237 | } |
238 | |
239 | // Come online gradually from 503 or maintenance mode |
240 | if (status == ConnectionValidator::Connected |
241 | && (_connectionStatus == ConnectionValidator::ServiceUnavailable |
242 | || _connectionStatus == ConnectionValidator::MaintenanceMode)) { |
243 | if (!_timeSinceMaintenanceOver.isValid()) { |
244 | qCInfo(lcAccountState) << "AccountState reconnection: delaying for" |
245 | << _maintenanceToConnectedDelay << "ms" ; |
246 | _timeSinceMaintenanceOver.start(); |
247 | QTimer::singleShot(_maintenanceToConnectedDelay + 100, this, &AccountState::checkConnectivity); |
248 | return; |
249 | } else if (_timeSinceMaintenanceOver.elapsed() < _maintenanceToConnectedDelay) { |
250 | qCInfo(lcAccountState) << "AccountState reconnection: only" |
251 | << _timeSinceMaintenanceOver.elapsed() << "ms have passed" ; |
252 | return; |
253 | } |
254 | } |
255 | |
256 | if (_connectionStatus != status) { |
257 | qCInfo(lcAccountState) << "AccountState connection status change: " |
258 | << _connectionStatus << "->" |
259 | << status; |
260 | _connectionStatus = status; |
261 | } |
262 | _connectionErrors = errors; |
263 | |
264 | switch (status) { |
265 | case ConnectionValidator::Connected: |
266 | if (_state != Connected) { |
267 | setState(Connected); |
268 | } |
269 | break; |
270 | case ConnectionValidator::Undefined: |
271 | case ConnectionValidator::NotConfigured: |
272 | setState(Disconnected); |
273 | break; |
274 | case ConnectionValidator::ServerVersionMismatch: |
275 | setState(ConfigurationError); |
276 | break; |
277 | case ConnectionValidator::StatusNotFound: |
278 | // This can happen either because the server does not exist |
279 | // or because we are having network issues. The latter one is |
280 | // much more likely, so keep trying to connect. |
281 | setState(NetworkError); |
282 | break; |
283 | case ConnectionValidator::CredentialsWrong: |
284 | case ConnectionValidator::CredentialsNotReady: |
285 | slotInvalidCredentials(); |
286 | break; |
287 | case ConnectionValidator::SslError: |
288 | setState(SignedOut); |
289 | break; |
290 | case ConnectionValidator::ServiceUnavailable: |
291 | _timeSinceMaintenanceOver.invalidate(); |
292 | setState(ServiceUnavailable); |
293 | break; |
294 | case ConnectionValidator::MaintenanceMode: |
295 | _timeSinceMaintenanceOver.invalidate(); |
296 | setState(MaintenanceMode); |
297 | break; |
298 | case ConnectionValidator::Timeout: |
299 | setState(NetworkError); |
300 | break; |
301 | } |
302 | } |
303 | |
304 | void AccountState::slotInvalidCredentials() |
305 | { |
306 | if (isSignedOut() || _waitingForNewCredentials) |
307 | return; |
308 | |
309 | qCInfo(lcAccountState) << "Invalid credentials for" << _account->url().toString() |
310 | << "asking user" ; |
311 | |
312 | _waitingForNewCredentials = true; |
313 | setState(AskingCredentials); |
314 | |
315 | if (account()->credentials()->ready()) { |
316 | account()->credentials()->invalidateToken(); |
317 | } |
318 | if (auto creds = qobject_cast<HttpCredentials *>(account()->credentials())) { |
319 | if (creds->refreshAccessToken()) |
320 | return; |
321 | } |
322 | account()->credentials()->askFromUser(); |
323 | } |
324 | |
325 | void AccountState::slotCredentialsFetched(AbstractCredentials *) |
326 | { |
327 | // Make a connection attempt, no matter whether the credentials are |
328 | // ready or not - we want to check whether we can get an SSL connection |
329 | // going before bothering the user for a password. |
330 | qCInfo(lcAccountState) << "Fetched credentials for" << _account->url().toString() |
331 | << "attempting to connect" ; |
332 | _waitingForNewCredentials = false; |
333 | checkConnectivity(); |
334 | } |
335 | |
336 | void AccountState::slotCredentialsAsked(AbstractCredentials *credentials) |
337 | { |
338 | qCInfo(lcAccountState) << "Credentials asked for" << _account->url().toString() |
339 | << "are they ready?" << credentials->ready(); |
340 | |
341 | _waitingForNewCredentials = false; |
342 | |
343 | if (!credentials->ready()) { |
344 | // User canceled the connection or did not give a password |
345 | setState(SignedOut); |
346 | return; |
347 | } |
348 | |
349 | if (_connectionValidator) { |
350 | // When new credentials become available we always want to restart the |
351 | // connection validation, even if it's currently running. |
352 | _connectionValidator->deleteLater(); |
353 | _connectionValidator = 0; |
354 | } |
355 | |
356 | checkConnectivity(); |
357 | } |
358 | |
359 | std::unique_ptr<QSettings> AccountState::settings() |
360 | { |
361 | auto s = ConfigFile::settingsWithGroup(QLatin1String("Accounts" )); |
362 | s->beginGroup(_account->id()); |
363 | return s; |
364 | } |
365 | |
366 | } // namespace OCC |
367 | |