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 "account.h"
16 #include "cookiejar.h"
17 #include "networkjobs.h"
18 #include "accessmanager.h"
19 #include "creds/abstractcredentials.h"
20 #include "capabilities.h"
21 #include "theme.h"
22 #include "common/asserts.h"
23
24 #include <QSettings>
25 #include <QLoggingCategory>
26 #include <QMutex>
27 #include <QNetworkReply>
28 #include <QNetworkAccessManager>
29 #include <QSslSocket>
30 #include <QNetworkCookieJar>
31 #include <QFileInfo>
32 #include <QDir>
33 #include <QSslKey>
34 #include <QAuthenticator>
35 #include <QStandardPaths>
36
37 namespace OCC {
38
39 Q_LOGGING_CATEGORY(lcAccount, "sync.account", QtInfoMsg)
40
Account(QObject * parent)41 Account::Account(QObject *parent)
42 : QObject(parent)
43 , _capabilities(QVariantMap())
44 , _davPath(Theme::instance()->webDavPath())
45 {
46 qRegisterMetaType<AccountPtr>("AccountPtr");
47 }
48
create()49 AccountPtr Account::create()
50 {
51 AccountPtr acc = AccountPtr(new Account);
52 acc->setSharedThis(acc);
53 return acc;
54 }
55
~Account()56 Account::~Account()
57 {
58 }
59
davPath() const60 QString Account::davPath() const
61 {
62 if (capabilities().chunkingNg()) {
63 // The chunking-ng means the server prefer to use the new webdav URL
64 return QLatin1String("/remote.php/dav/files/") + davUser() + QLatin1Char('/');
65 }
66
67 // make sure to have a trailing slash
68 if (!_davPath.endsWith(QLatin1Char('/'))) {
69 QString dp(_davPath);
70 dp.append(QLatin1Char('/'));
71 return dp;
72 }
73 return _davPath;
74 }
75
setSharedThis(AccountPtr sharedThis)76 void Account::setSharedThis(AccountPtr sharedThis)
77 {
78 _sharedThis = sharedThis.toWeakRef();
79 }
80
sharedFromThis()81 AccountPtr Account::sharedFromThis()
82 {
83 return _sharedThis.toStrongRef();
84 }
85
davUser() const86 QString Account::davUser() const
87 {
88 return _davUser.isEmpty() ? _credentials->user() : _davUser;
89 }
90
setDavUser(const QString & newDavUser)91 void Account::setDavUser(const QString &newDavUser)
92 {
93 if (_davUser == newDavUser)
94 return;
95 _davUser = newDavUser;
96 emit wantsAccountSaved(this);
97 }
98
99 #ifndef TOKEN_AUTH_ONLY
avatar() const100 QPixmap Account::avatar() const
101 {
102 return _avatarImg;
103 }
setAvatar(const QPixmap & img)104 void Account::setAvatar(const QPixmap &img)
105 {
106 _avatarImg = img;
107 emit accountChangedAvatar();
108 }
109 #endif
110
displayName() const111 QString Account::displayName() const
112 {
113 QString user = davDisplayName();
114 if (user.isEmpty())
115 user = davUser();
116 QString host = _url.host();
117 const int port = url().port();
118 if (port > 0 && port != 80 && port != 443) {
119 host += QStringLiteral(":%1").arg(QString::number(port));
120 }
121 return tr("%1@%2").arg(user, host);
122 }
123
davDisplayName() const124 QString Account::davDisplayName() const
125 {
126 return _displayName;
127 }
128
setDavDisplayName(const QString & newDisplayName)129 void Account::setDavDisplayName(const QString &newDisplayName)
130 {
131 _displayName = newDisplayName;
132 emit accountChangedDisplayName();
133 }
134
id() const135 QString Account::id() const
136 {
137 return _id;
138 }
139
credentials() const140 AbstractCredentials *Account::credentials() const
141 {
142 return _credentials.data();
143 }
144
setCredentials(AbstractCredentials * cred)145 void Account::setCredentials(AbstractCredentials *cred)
146 {
147 // set active credential manager
148 QNetworkCookieJar *jar = nullptr;
149 if (_am) {
150 jar = _am->cookieJar();
151 jar->setParent(nullptr);
152
153 _am = QSharedPointer<QNetworkAccessManager>();
154 }
155
156 // The order for these two is important! Reading the credential's
157 // settings accesses the account as well as account->_credentials,
158 _credentials.reset(cred);
159 cred->setAccount(this);
160
161 // Note: This way the QNAM can outlive the Account and Credentials.
162 // This is necessary to avoid issues with the QNAM being deleted while
163 // processing slotHandleSslErrors().
164 _am = QSharedPointer<QNetworkAccessManager>(_credentials->createQNAM(), &QObject::deleteLater);
165
166 if (jar) {
167 _am->setCookieJar(jar);
168 }
169 connect(_am.data(), SIGNAL(sslErrors(QNetworkReply *, QList<QSslError>)),
170 SLOT(slotHandleSslErrors(QNetworkReply *, QList<QSslError>)));
171 connect(_am.data(), &QNetworkAccessManager::proxyAuthenticationRequired,
172 this, &Account::proxyAuthenticationRequired);
173 connect(_credentials.data(), &AbstractCredentials::fetched,
174 this, &Account::slotCredentialsFetched);
175 connect(_credentials.data(), &AbstractCredentials::asked,
176 this, &Account::slotCredentialsAsked);
177 }
178
davUrl() const179 QUrl Account::davUrl() const
180 {
181 return Utility::concatUrlPath(url(), davPath());
182 }
183
deprecatedPrivateLinkUrl(const QByteArray & numericFileId) const184 QUrl Account::deprecatedPrivateLinkUrl(const QByteArray &numericFileId) const
185 {
186 return Utility::concatUrlPath(_userVisibleUrl,
187 QStringLiteral("/index.php/f/") + QString::fromLatin1(QUrl::toPercentEncoding(QString::fromLatin1(numericFileId))));
188 }
189
190 /**
191 * clear all cookies. (Session cookies or not)
192 */
clearCookieJar()193 void Account::clearCookieJar()
194 {
195 auto jar = qobject_cast<CookieJar *>(_am->cookieJar());
196 OC_ASSERT(jar);
197 jar->setAllCookies(QList<QNetworkCookie>());
198 emit wantsAccountSaved(this);
199 }
200
201 /*! This shares our official cookie jar (containing all the tasty
202 authentication cookies) with another QNAM while making sure
203 of not losing its ownership. */
lendCookieJarTo(QNetworkAccessManager * guest)204 void Account::lendCookieJarTo(QNetworkAccessManager *guest)
205 {
206 auto jar = _am->cookieJar();
207 auto oldParent = jar->parent();
208 guest->setCookieJar(jar); // takes ownership of our precious cookie jar
209 jar->setParent(oldParent); // takes it back
210 }
211
cookieJarPath()212 QString Account::cookieJarPath()
213 {
214 return QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation) + QStringLiteral("/cookies") + id() + QStringLiteral(".db");
215 }
216
resetNetworkAccessManager()217 void Account::resetNetworkAccessManager()
218 {
219 if (!_credentials || !_am) {
220 return;
221 }
222
223 qCDebug(lcAccount) << "Resetting QNAM";
224 QNetworkCookieJar *jar = _am->cookieJar();
225
226 // Use a QSharedPointer to allow locking the life of the QNAM on the stack.
227 // Make it call deleteLater to make sure that we can return to any QNAM stack frames safely.
228 _am = QSharedPointer<QNetworkAccessManager>(_credentials->createQNAM(), &QObject::deleteLater);
229
230 _am->setCookieJar(jar); // takes ownership of the old cookie jar
231 connect(_am.data(), SIGNAL(sslErrors(QNetworkReply *, QList<QSslError>)),
232 SLOT(slotHandleSslErrors(QNetworkReply *, QList<QSslError>)));
233 connect(_am.data(), &QNetworkAccessManager::proxyAuthenticationRequired,
234 this, &Account::proxyAuthenticationRequired);
235 }
236
networkAccessManager()237 QNetworkAccessManager *Account::networkAccessManager()
238 {
239 return _am.data();
240 }
241
sharedNetworkAccessManager()242 QSharedPointer<QNetworkAccessManager> Account::sharedNetworkAccessManager()
243 {
244 return _am;
245 }
246
sendRawRequest(const QByteArray & verb,const QUrl & url,QNetworkRequest req,QIODevice * data)247 QNetworkReply *Account::sendRawRequest(const QByteArray &verb, const QUrl &url, QNetworkRequest req, QIODevice *data)
248 {
249 req.setUrl(url);
250 req.setSslConfiguration(this->getOrCreateSslConfig());
251 if (verb == "HEAD" && !data) {
252 return _am->head(req);
253 } else if (verb == "GET" && !data) {
254 return _am->get(req);
255 } else if (verb == "POST") {
256 return _am->post(req, data);
257 } else if (verb == "PUT") {
258 return _am->put(req, data);
259 } else if (verb == "DELETE" && !data) {
260 return _am->deleteResource(req);
261 }
262 return _am->sendCustomRequest(req, verb, data);
263 }
264
sendRequest(const QByteArray & verb,const QUrl & url,QNetworkRequest req,QIODevice * data)265 SimpleNetworkJob *Account::sendRequest(const QByteArray &verb, const QUrl &url, QNetworkRequest req, QIODevice *data)
266 {
267 auto job = new SimpleNetworkJob(sharedFromThis());
268 job->startRequest(verb, url, req, data);
269 return job;
270 }
271
setSslConfiguration(const QSslConfiguration & config)272 void Account::setSslConfiguration(const QSslConfiguration &config)
273 {
274 _sslConfiguration = config;
275 }
276
getOrCreateSslConfig()277 QSslConfiguration Account::getOrCreateSslConfig()
278 {
279 if (!_sslConfiguration.isNull()) {
280 // Will be set by CheckServerJob::finished()
281 // We need to use a central shared config to get SSL session tickets
282 return _sslConfiguration;
283 }
284
285 // if setting the client certificate fails, you will probably get an error similar to this:
286 // "An internal error number 1060 happened. SSL handshake failed, client certificate was requested: SSL error: sslv3 alert handshake failure"
287 QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration();
288
289 // Try hard to re-use session for different requests
290 sslConfig.setSslOption(QSsl::SslOptionDisableSessionTickets, false);
291 sslConfig.setSslOption(QSsl::SslOptionDisableSessionSharing, false);
292 sslConfig.setSslOption(QSsl::SslOptionDisableSessionPersistence, false);
293
294 return sslConfig;
295 }
296
setApprovedCerts(const QList<QSslCertificate> certs)297 void Account::setApprovedCerts(const QList<QSslCertificate> certs)
298 {
299 _approvedCerts = certs;
300 QSslSocket::addDefaultCaCertificates(certs);
301 }
302
addApprovedCerts(const QList<QSslCertificate> certs)303 void Account::addApprovedCerts(const QList<QSslCertificate> certs)
304 {
305 _approvedCerts += certs;
306 }
307
resetRejectedCertificates()308 void Account::resetRejectedCertificates()
309 {
310 _rejectedCertificates.clear();
311 }
312
setSslErrorHandler(AbstractSslErrorHandler * handler)313 void Account::setSslErrorHandler(AbstractSslErrorHandler *handler)
314 {
315 _sslErrorHandler.reset(handler);
316 }
317
setUrl(const QUrl & url)318 void Account::setUrl(const QUrl &url)
319 {
320 _url = url;
321 _userVisibleUrl = url;
322 }
323
setUserVisibleHost(const QString & host)324 void Account::setUserVisibleHost(const QString &host)
325 {
326 _userVisibleUrl.setHost(host);
327 }
328
credentialSetting(const QString & key) const329 QVariant Account::credentialSetting(const QString &key) const
330 {
331 if (_credentials) {
332 QString prefix = _credentials->authType();
333 QVariant value = _settingsMap.value(prefix + QLatin1Char('_') + key);
334 if (value.isNull()) {
335 value = _settingsMap.value(key);
336 }
337 return value;
338 }
339 return QVariant();
340 }
341
setCredentialSetting(const QString & key,const QVariant & value)342 void Account::setCredentialSetting(const QString &key, const QVariant &value)
343 {
344 if (_credentials) {
345 QString prefix = _credentials->authType();
346 _settingsMap.insert(prefix + QLatin1Char('_') + key, value);
347 }
348 }
349
slotHandleSslErrors(QNetworkReply * reply,QList<QSslError> errors)350 void Account::slotHandleSslErrors(QNetworkReply *reply, QList<QSslError> errors)
351 {
352 NetworkJobTimeoutPauser pauser(reply);
353 QString out;
354 QDebug(&out) << "SSL-Errors happened for url " << reply->url().toString();
355 foreach (const QSslError &error, errors) {
356 QDebug(&out) << "\tError in " << error.certificate() << ":"
357 << error.errorString() << "(" << error.error() << ")"
358 << "\n";
359 }
360
361 bool allPreviouslyRejected = true;
362 foreach (const QSslError &error, errors) {
363 if (!_rejectedCertificates.contains(error.certificate())) {
364 allPreviouslyRejected = false;
365 }
366 }
367
368 // If all certs have previously been rejected by the user, don't ask again.
369 if (allPreviouslyRejected) {
370 qCInfo(lcAccount) << out << "Certs not trusted by user decision, returning.";
371 return;
372 }
373
374 QList<QSslCertificate> approvedCerts;
375 if (_sslErrorHandler.isNull()) {
376 qCWarning(lcAccount) << out << "called without valid SSL error handler for account" << url();
377 return;
378 }
379
380 // SslDialogErrorHandler::handleErrors will run an event loop that might execute
381 // the deleteLater() of the QNAM before we have the chance of unwinding our stack.
382 // Keep a ref here on our stackframe to make sure that it doesn't get deleted before
383 // handleErrors returns.
384 QSharedPointer<QNetworkAccessManager> qnamLock = _am;
385 QPointer<QObject> guard = reply;
386
387 if (_sslErrorHandler->handleErrors(errors, reply->sslConfiguration(), &approvedCerts, sharedFromThis())) {
388 if (!guard)
389 return;
390
391 if (!approvedCerts.isEmpty()) {
392 QSslSocket::addDefaultCaCertificates(approvedCerts);
393 addApprovedCerts(approvedCerts);
394 emit wantsAccountSaved(this);
395
396 // all ssl certs are known and accepted. We can ignore the problems right away.
397 qCInfo(lcAccount) << out << "Certs are known and trusted! This is not an actual error.";
398 }
399
400 // Warning: Do *not* use ignoreSslErrors() (without args) here:
401 // it permanently ignores all SSL errors for this host, even
402 // certificate changes.
403 reply->ignoreSslErrors(errors);
404 } else {
405 if (!guard)
406 return;
407
408 // Mark all involved certificates as rejected, so we don't ask the user again.
409 foreach (const QSslError &error, errors) {
410 if (!_rejectedCertificates.contains(error.certificate())) {
411 _rejectedCertificates.append(error.certificate());
412 }
413 }
414
415 // Not calling ignoreSslErrors will make the SSL handshake fail.
416 return;
417 }
418 }
419
slotCredentialsFetched()420 void Account::slotCredentialsFetched()
421 {
422 emit credentialsFetched(_credentials.data());
423 }
424
slotCredentialsAsked()425 void Account::slotCredentialsAsked()
426 {
427 emit credentialsAsked(_credentials.data());
428 }
429
handleInvalidCredentials()430 void Account::handleInvalidCredentials()
431 {
432 emit invalidCredentials();
433 }
434
clearQNAMCache()435 void Account::clearQNAMCache()
436 {
437 _am->clearAccessCache();
438 }
439
capabilities() const440 const Capabilities &Account::capabilities() const
441 {
442 return _capabilities;
443 }
444
setCapabilities(const QVariantMap & caps)445 void Account::setCapabilities(const QVariantMap &caps)
446 {
447 _capabilities = Capabilities(caps);
448 }
449
serverVersion() const450 QString Account::serverVersion() const
451 {
452 return _serverVersion;
453 }
454
serverVersionInt() const455 int Account::serverVersionInt() const
456 {
457 // FIXME: Use Qt 5.5 QVersionNumber
458 auto components = serverVersion().split(QLatin1Char('.'));
459 return makeServerVersion(components.value(0).toInt(),
460 components.value(1).toInt(),
461 components.value(2).toInt());
462 }
463
makeServerVersion(int majorVersion,int minorVersion,int patchVersion)464 int Account::makeServerVersion(int majorVersion, int minorVersion, int patchVersion)
465 {
466 return (majorVersion << 16) + (minorVersion << 8) + patchVersion;
467 }
468
serverVersionUnsupported() const469 bool Account::serverVersionUnsupported() const
470 {
471 if (serverVersionInt() == 0) {
472 // not detected yet, assume it is fine.
473 return false;
474 }
475 // Older version which is not "end of life" according to https://github.com/owncloud/core/wiki/Maintenance-and-Release-Schedule
476 return serverVersionInt() < makeServerVersion(10, 0, 0) || serverVersion().endsWith(QLatin1String("Nextcloud"));
477 }
478
setServerVersion(const QString & version)479 void Account::setServerVersion(const QString &version)
480 {
481 if (version == _serverVersion) {
482 return;
483 }
484
485 auto oldServerVersion = _serverVersion;
486 _serverVersion = version;
487 emit serverVersionChanged(this, oldServerVersion, version);
488 }
489
490 } // namespace OCC
491