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