1 | /* |
2 | * Copyright (C) by Klaas Freitag <freitag@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 <QJsonDocument> |
16 | #include <QJsonObject> |
17 | #include <QLoggingCategory> |
18 | #include <QNetworkReply> |
19 | #include <QNetworkProxyFactory> |
20 | #include <QXmlStreamReader> |
21 | |
22 | #include "connectionvalidator.h" |
23 | #include "account.h" |
24 | #include "networkjobs.h" |
25 | #include "clientproxy.h" |
26 | #include <creds/abstractcredentials.h> |
27 | |
28 | namespace OCC { |
29 | |
30 | Q_LOGGING_CATEGORY(lcConnectionValidator, "sync.connectionvalidator" , QtInfoMsg) |
31 | |
32 | // Make sure the timeout for this job is less than how often we get called |
33 | // This makes sure we get tried often enough without "ConnectionValidator already running" |
34 | static qint64 timeoutToUseMsec = qMax(1000, ConnectionValidator::DefaultCallingIntervalMsec - 5 * 1000); |
35 | |
36 | ConnectionValidator::ConnectionValidator(AccountPtr account, QObject *parent) |
37 | : QObject(parent) |
38 | , _account(account) |
39 | , _isCheckingServerAndAuth(false) |
40 | { |
41 | } |
42 | |
43 | void ConnectionValidator::checkServerAndAuth() |
44 | { |
45 | if (!_account) { |
46 | _errors << tr("No ownCloud account configured" ); |
47 | reportResult(NotConfigured); |
48 | return; |
49 | } |
50 | qCDebug(lcConnectionValidator) << "Checking server and authentication" ; |
51 | |
52 | _isCheckingServerAndAuth = true; |
53 | |
54 | // Lookup system proxy in a thread https://github.com/owncloud/client/issues/2993 |
55 | if (ClientProxy::isUsingSystemDefault()) { |
56 | qCDebug(lcConnectionValidator) << "Trying to look up system proxy" ; |
57 | ClientProxy::lookupSystemProxyAsync(_account->url(), |
58 | this, SLOT(systemProxyLookupDone(QNetworkProxy))); |
59 | } else { |
60 | // We want to reset the QNAM proxy so that the global proxy settings are used (via ClientProxy settings) |
61 | _account->networkAccessManager()->setProxy(QNetworkProxy(QNetworkProxy::DefaultProxy)); |
62 | // use a queued invocation so we're as asynchronous as with the other code path |
63 | QMetaObject::invokeMethod(this, "slotCheckServerAndAuth" , Qt::QueuedConnection); |
64 | } |
65 | } |
66 | |
67 | void ConnectionValidator::systemProxyLookupDone(const QNetworkProxy &proxy) |
68 | { |
69 | if (!_account) { |
70 | qCWarning(lcConnectionValidator) << "Bailing out, Account had been deleted" ; |
71 | return; |
72 | } |
73 | |
74 | if (proxy.type() != QNetworkProxy::NoProxy) { |
75 | qCInfo(lcConnectionValidator) << "Setting QNAM proxy to be system proxy" << printQNetworkProxy(proxy); |
76 | } else { |
77 | qCInfo(lcConnectionValidator) << "No system proxy set by OS" ; |
78 | } |
79 | _account->networkAccessManager()->setProxy(proxy); |
80 | |
81 | slotCheckServerAndAuth(); |
82 | } |
83 | |
84 | // The actual check |
85 | void ConnectionValidator::slotCheckServerAndAuth() |
86 | { |
87 | CheckServerJob *checkJob = new CheckServerJob(_account, this); |
88 | checkJob->setTimeout(timeoutToUseMsec); |
89 | checkJob->setIgnoreCredentialFailure(true); |
90 | connect(checkJob, &CheckServerJob::instanceFound, this, &ConnectionValidator::slotStatusFound); |
91 | connect(checkJob, &CheckServerJob::instanceNotFound, this, &ConnectionValidator::slotNoStatusFound); |
92 | connect(checkJob, &CheckServerJob::timeout, this, &ConnectionValidator::slotJobTimeout); |
93 | checkJob->start(); |
94 | } |
95 | |
96 | void ConnectionValidator::slotStatusFound(const QUrl &url, const QJsonObject &info) |
97 | { |
98 | // Newer servers don't disclose any version in status.php anymore |
99 | // https://github.com/owncloud/core/pull/27473/files |
100 | // so this string can be empty. |
101 | QString serverVersion = CheckServerJob::version(info); |
102 | |
103 | // status.php was found. |
104 | qCInfo(lcConnectionValidator) << "** Application: ownCloud found: " |
105 | << url << " with version " |
106 | << CheckServerJob::versionString(info) |
107 | << "(" << serverVersion << ")" ; |
108 | |
109 | // Update server url in case of redirection |
110 | if (_account->url() != url) { |
111 | qCInfo(lcConnectionValidator()) << "status.php was redirected to" << url.toString(); |
112 | _account->setUrl(url); |
113 | _account->wantsAccountSaved(_account.data()); |
114 | } |
115 | |
116 | if (!serverVersion.isEmpty() && !setAndCheckServerVersion(serverVersion)) { |
117 | return; |
118 | } |
119 | |
120 | // Check for maintenance mode: Servers send "true", so go through QVariant |
121 | // to parse it correctly. |
122 | if (info["maintenance" ].toVariant().toBool()) { |
123 | reportResult(MaintenanceMode); |
124 | return; |
125 | } |
126 | |
127 | // now check the authentication |
128 | QTimer::singleShot(0, this, &ConnectionValidator::checkAuthentication); |
129 | } |
130 | |
131 | // status.php could not be loaded (network or server issue!). |
132 | void ConnectionValidator::slotNoStatusFound(QNetworkReply *reply) |
133 | { |
134 | auto job = qobject_cast<CheckServerJob *>(sender()); |
135 | qCWarning(lcConnectionValidator) << reply->error() << job->errorString() << reply->peek(1024); |
136 | if (reply->error() == QNetworkReply::SslHandshakeFailedError) { |
137 | reportResult(SslError); |
138 | return; |
139 | } |
140 | |
141 | if (!_account->credentials()->stillValid(reply)) { |
142 | // Note: Why would this happen on a status.php request? |
143 | _errors.append(tr("Authentication error: Either username or password are wrong." )); |
144 | } else { |
145 | //_errors.append(tr("Unable to connect to %1").arg(_account->url().toString())); |
146 | _errors.append(job->errorString()); |
147 | } |
148 | reportResult(StatusNotFound); |
149 | } |
150 | |
151 | void ConnectionValidator::slotJobTimeout(const QUrl &url) |
152 | { |
153 | Q_UNUSED(url); |
154 | //_errors.append(tr("Unable to connect to %1").arg(url.toString())); |
155 | _errors.append(tr("timeout" )); |
156 | reportResult(Timeout); |
157 | } |
158 | |
159 | |
160 | void ConnectionValidator::checkAuthentication() |
161 | { |
162 | AbstractCredentials *creds = _account->credentials(); |
163 | |
164 | if (!creds->ready()) { |
165 | reportResult(CredentialsNotReady); |
166 | return; |
167 | } |
168 | |
169 | // simply GET the webdav root, will fail if credentials are wrong. |
170 | // continue in slotAuthCheck here :-) |
171 | qCDebug(lcConnectionValidator) << "# Check whether authenticated propfind works." ; |
172 | PropfindJob *job = new PropfindJob(_account, "/" , this); |
173 | job->setTimeout(timeoutToUseMsec); |
174 | job->setProperties(QList<QByteArray>() << "getlastmodified" ); |
175 | connect(job, &PropfindJob::result, this, &ConnectionValidator::slotAuthSuccess); |
176 | connect(job, &PropfindJob::finishedWithError, this, &ConnectionValidator::slotAuthFailed); |
177 | job->start(); |
178 | } |
179 | |
180 | void ConnectionValidator::slotAuthFailed(QNetworkReply *reply) |
181 | { |
182 | auto job = qobject_cast<PropfindJob *>(sender()); |
183 | Status stat = Timeout; |
184 | |
185 | if (reply->error() == QNetworkReply::SslHandshakeFailedError) { |
186 | _errors << job->errorStringParsingBody(); |
187 | stat = SslError; |
188 | |
189 | } else if (reply->error() == QNetworkReply::AuthenticationRequiredError |
190 | || !_account->credentials()->stillValid(reply)) { |
191 | qCWarning(lcConnectionValidator) << "******** Password is wrong!" << reply->error() << job->errorString(); |
192 | _errors << tr("The provided credentials are not correct" ); |
193 | stat = CredentialsWrong; |
194 | |
195 | } else if (reply->error() != QNetworkReply::NoError) { |
196 | _errors << job->errorStringParsingBody(); |
197 | |
198 | const int httpStatus = |
199 | reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); |
200 | if (httpStatus == 503) { |
201 | _errors.clear(); |
202 | stat = ServiceUnavailable; |
203 | } |
204 | } |
205 | |
206 | reportResult(stat); |
207 | } |
208 | |
209 | void ConnectionValidator::slotAuthSuccess() |
210 | { |
211 | _errors.clear(); |
212 | if (!_isCheckingServerAndAuth) { |
213 | reportResult(Connected); |
214 | return; |
215 | } |
216 | checkServerCapabilities(); |
217 | } |
218 | |
219 | void ConnectionValidator::checkServerCapabilities() |
220 | { |
221 | // The main flow now needs the capabilities |
222 | JsonApiJob *job = new JsonApiJob(_account, QLatin1String("ocs/v1.php/cloud/capabilities" ), this); |
223 | job->setTimeout(timeoutToUseMsec); |
224 | QObject::connect(job, &JsonApiJob::jsonReceived, this, &ConnectionValidator::slotCapabilitiesRecieved); |
225 | job->start(); |
226 | |
227 | // And we'll retrieve the ocs config in parallel |
228 | // note that 'this' might be destroyed before the job finishes, so intentionally not parented |
229 | auto configJob = new JsonApiJob(_account, QLatin1String("ocs/v1.php/config" )); |
230 | configJob->setTimeout(timeoutToUseMsec); |
231 | auto account = _account; // capturing account by value will make it live long enough |
232 | QObject::connect(configJob, &JsonApiJob::jsonReceived, _account.data(), |
233 | [=](const QJsonDocument &json) { |
234 | ocsConfigReceived(json, account); |
235 | }); |
236 | configJob->start(); |
237 | } |
238 | |
239 | void ConnectionValidator::slotCapabilitiesRecieved(const QJsonDocument &json) |
240 | { |
241 | auto caps = json.object().value("ocs" ).toObject().value("data" ).toObject().value("capabilities" ).toObject(); |
242 | qCInfo(lcConnectionValidator) << "Server capabilities" << caps; |
243 | _account->setCapabilities(caps.toVariantMap()); |
244 | |
245 | // New servers also report the version in the capabilities |
246 | QString serverVersion = caps["core" ].toObject()["status" ].toObject()["version" ].toString(); |
247 | if (!serverVersion.isEmpty() && !setAndCheckServerVersion(serverVersion)) { |
248 | return; |
249 | } |
250 | |
251 | fetchUser(); |
252 | } |
253 | |
254 | void ConnectionValidator::ocsConfigReceived(const QJsonDocument &json, AccountPtr account) |
255 | { |
256 | QString host = json.object().value("ocs" ).toObject().value("data" ).toObject().value("host" ).toString(); |
257 | if (host.isEmpty()) { |
258 | qCWarning(lcConnectionValidator) << "Could not extract 'host' from ocs config reply" ; |
259 | return; |
260 | } |
261 | qCInfo(lcConnectionValidator) << "Determined user-visible host to be" << host; |
262 | account->setUserVisibleHost(host); |
263 | } |
264 | |
265 | void ConnectionValidator::fetchUser() |
266 | { |
267 | JsonApiJob *job = new JsonApiJob(_account, QLatin1String("ocs/v1.php/cloud/user" ), this); |
268 | job->setTimeout(timeoutToUseMsec); |
269 | QObject::connect(job, &JsonApiJob::jsonReceived, this, &ConnectionValidator::slotUserFetched); |
270 | job->start(); |
271 | } |
272 | |
273 | bool ConnectionValidator::setAndCheckServerVersion(const QString &version) |
274 | { |
275 | qCInfo(lcConnectionValidator) << _account->url() << "has server version" << version; |
276 | _account->setServerVersion(version); |
277 | |
278 | // We cannot deal with servers < 7.0.0 |
279 | if (_account->serverVersionInt() |
280 | && _account->serverVersionInt() < Account::makeServerVersion(7, 0, 0)) { |
281 | _errors.append(tr("The configured server for this client is too old" )); |
282 | _errors.append(tr("Please update to the latest server and restart the client." )); |
283 | reportResult(ServerVersionMismatch); |
284 | return false; |
285 | } |
286 | // We attempt to work with servers >= 7.0.0 but warn users. |
287 | // Check usages of Account::serverVersionUnsupported() for details. |
288 | |
289 | #if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0) |
290 | // Record that the server supports HTTP/2 |
291 | // Actual decision if we should use HTTP/2 is done in AccessManager::createRequest |
292 | if (auto job = qobject_cast<AbstractNetworkJob *>(sender())) { |
293 | if (auto reply = job->reply()) { |
294 | _account->setHttp2Supported( |
295 | reply->attribute(QNetworkRequest::HTTP2WasUsedAttribute).toBool()); |
296 | } |
297 | } |
298 | #endif |
299 | return true; |
300 | } |
301 | |
302 | void ConnectionValidator::slotUserFetched(const QJsonDocument &json) |
303 | { |
304 | QString user = json.object().value("ocs" ).toObject().value("data" ).toObject().value("id" ).toString(); |
305 | if (!user.isEmpty()) { |
306 | _account->setDavUser(user); |
307 | } |
308 | QString displayName = json.object().value("ocs" ).toObject().value("data" ).toObject().value("display-name" ).toString(); |
309 | if (!displayName.isEmpty()) { |
310 | _account->setDavDisplayName(displayName); |
311 | } |
312 | #ifndef TOKEN_AUTH_ONLY |
313 | AvatarJob *job = new AvatarJob(_account, _account->davUser(), 128, this); |
314 | job->setTimeout(20 * 1000); |
315 | QObject::connect(job, &AvatarJob::avatarPixmap, this, &ConnectionValidator::slotAvatarImage); |
316 | job->start(); |
317 | #else |
318 | reportResult(Connected); |
319 | #endif |
320 | } |
321 | |
322 | #ifndef TOKEN_AUTH_ONLY |
323 | void ConnectionValidator::slotAvatarImage(const QImage &img) |
324 | { |
325 | _account->setAvatar(img); |
326 | reportResult(Connected); |
327 | } |
328 | #endif |
329 | |
330 | void ConnectionValidator::reportResult(Status status) |
331 | { |
332 | emit connectionResult(status, _errors); |
333 | deleteLater(); |
334 | } |
335 | |
336 | } // namespace OCC |
337 | |