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
28namespace OCC {
29
30Q_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"
34static qint64 timeoutToUseMsec = qMax(1000, ConnectionValidator::DefaultCallingIntervalMsec - 5 * 1000);
35
36ConnectionValidator::ConnectionValidator(AccountPtr account, QObject *parent)
37 : QObject(parent)
38 , _account(account)
39 , _isCheckingServerAndAuth(false)
40{
41}
42
43void 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
67void 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
85void 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
96void 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!).
132void 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
151void 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
160void 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
180void 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
209void ConnectionValidator::slotAuthSuccess()
210{
211 _errors.clear();
212 if (!_isCheckingServerAndAuth) {
213 reportResult(Connected);
214 return;
215 }
216 checkServerCapabilities();
217}
218
219void 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
239void 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
254void 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
265void 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
273bool 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
302void 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
323void ConnectionValidator::slotAvatarImage(const QImage &img)
324{
325 _account->setAvatar(img);
326 reportResult(Connected);
327}
328#endif
329
330void ConnectionValidator::reportResult(Status status)
331{
332 emit connectionResult(status, _errors);
333 deleteLater();
334}
335
336} // namespace OCC
337