/* * Copyright (C) 2015 by Christian Kamm * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * for more details. */ #include "proxyauthhandler.h" #include "proxyauthdialog.h" #include "theme.h" #include "configfile.h" #include "account.h" #include #include using namespace OCC; using namespace QKeychain; Q_LOGGING_CATEGORY(lcProxy, "nextcloud.gui.credentials.proxy", QtInfoMsg) ProxyAuthHandler *ProxyAuthHandler::instance() { static ProxyAuthHandler inst; return &inst; } ProxyAuthHandler::ProxyAuthHandler() { _dialog = new ProxyAuthDialog(); _configFile.reset(new ConfigFile); _settings.reset(new QSettings(_configFile->configFile(), QSettings::IniFormat)); _settings->beginGroup(QLatin1String("Proxy")); _settings->beginGroup(QLatin1String("Credentials")); } ProxyAuthHandler::~ProxyAuthHandler() { delete _dialog; } void ProxyAuthHandler::handleProxyAuthenticationRequired( const QNetworkProxy &proxy, QAuthenticator *authenticator) { if (!_dialog) { return; } QString key = proxy.hostName() + QLatin1Char(':') + QString::number(proxy.port()); // If the proxy server has changed, forget what we know. if (key != _proxy) { _proxy = key; _username.clear(); _password.clear(); _blocked = false; _gaveCredentialsTo.clear(); // If the user explicitly configured the proxy in the // network settings, don't ask about it. if (_configFile->proxyType() == QNetworkProxy::HttpProxy || _configFile->proxyType() == QNetworkProxy::Socks5Proxy) { _blocked = true; } } if (_blocked) { return; } // Find the responsible QNAM if possible. QNetworkAccessManager *sending_qnam = nullptr; QWeakPointer qnam_alive; if (auto account = qobject_cast(sender())) { // Since we go into an event loop, it's possible for the account's qnam // to be destroyed before we get back. We can use this to check for its // liveness. qnam_alive = account->sharedNetworkAccessManager(); sending_qnam = qnam_alive.data(); } if (!sending_qnam) { qCWarning(lcProxy) << "Could not get the sending QNAM for" << sender(); } qCInfo(lcProxy) << "Proxy auth required for" << key << proxy.type(); // If we already had a username but auth still failed, // invalidate the old credentials! Unfortunately, authenticator->user() // isn't reliable, so we also invalidate credentials if we previously // gave presumably valid credentials to the same QNAM. bool invalidated = false; if (!_waitingForDialog && !_waitingForKeychain && (!authenticator->user().isEmpty() || (sending_qnam && _gaveCredentialsTo.contains(sending_qnam)))) { qCInfo(lcProxy) << "invalidating old creds" << key; _username.clear(); _password.clear(); invalidated = true; _gaveCredentialsTo.clear(); } if (_username.isEmpty() || _waitingForKeychain) { if (invalidated || !getCredsFromKeychain()) { if (getCredsFromDialog()) { storeCredsInKeychain(); } else { // dialog was cancelled, never ask for that proxy again _blocked = true; return; } } } qCInfo(lcProxy) << "got creds for" << _proxy; authenticator->setUser(_username); authenticator->setPassword(_password); sending_qnam = qnam_alive.data(); if (sending_qnam) { _gaveCredentialsTo.insert(sending_qnam); connect(sending_qnam, &QObject::destroyed, this, &ProxyAuthHandler::slotSenderDestroyed); } } void ProxyAuthHandler::slotSenderDestroyed(QObject *obj) { _gaveCredentialsTo.remove(obj); } bool ProxyAuthHandler::getCredsFromDialog() { // Open the credentials dialog if (!_waitingForDialog) { _dialog->reset(); _dialog->setProxyAddress(_proxy); _dialog->open(); } // This function can be reentered while the dialog is open. // If that's the case, continue processing the dialog until // it's done. if(_dialog) { execAwait(_dialog.data(), &QDialog::finished, _waitingForDialog, QEventLoop::ExcludeSocketNotifiers); } if (_dialog && _dialog->result() == QDialog::Accepted) { qCInfo(lcProxy) << "got creds for" << _proxy << "from dialog"; _username = _dialog->username(); _password = _dialog->password(); return true; } return false; } template void ProxyAuthHandler::execAwait(const T *sender, PointerToMemberFunction signal, int &counter, const QEventLoop::ProcessEventsFlags flags) { if (!sender) { return; } QEventLoop waitLoop; connect(sender, signal, &waitLoop, &QEventLoop::quit); ++counter; waitLoop.exec(flags); --counter; } bool ProxyAuthHandler::getCredsFromKeychain() { if (_waitingForDialog) { return false; } qCDebug(lcProxy) << "trying to load" << _proxy; if (!_waitingForKeychain) { _username = _settings->value(keychainUsernameKey()).toString(); if (_username.isEmpty()) { return false; } _readPasswordJob.reset(new ReadPasswordJob(Theme::instance()->appName())); _readPasswordJob->setSettings(_settings.data()); _readPasswordJob->setInsecureFallback(false); _readPasswordJob->setKey(keychainPasswordKey()); _readPasswordJob->setAutoDelete(false); _readPasswordJob->start(); } // While we wait for the password job to be done, this code may be reentered. // This really needs the counter and the flag here, because otherwise we get // bad behavior when we reenter this code after the flag has been switched // but before the while loop has finished. execAwait(_readPasswordJob.data(), &QKeychain::Job::finished, _waitingForKeychain); if (_readPasswordJob->error() == NoError) { qCInfo(lcProxy) << "got creds for" << _proxy << "from keychain"; _password = _readPasswordJob->textData(); return true; } _username.clear(); if (_readPasswordJob->error() != EntryNotFound) { qCWarning(lcProxy) << "ReadPasswordJob failed with" << _readPasswordJob->errorString(); } return false; } void ProxyAuthHandler::storeCredsInKeychain() { if (_waitingForKeychain) { return; } qCInfo(lcProxy) << "storing" << _proxy; _settings->setValue(keychainUsernameKey(), _username); auto job = new WritePasswordJob(Theme::instance()->appName(), this); job->setSettings(_settings.data()); job->setInsecureFallback(false); job->setKey(keychainPasswordKey()); job->setTextData(_password); job->setAutoDelete(false); job->start(); execAwait(job, &QKeychain::Job::finished, _waitingForKeychain); job->deleteLater(); if (job->error() != NoError) { qCWarning(lcProxy) << "WritePasswordJob failed with" << job->errorString(); } } QString ProxyAuthHandler::keychainUsernameKey() const { return QString::fromLatin1("%1/username").arg(_proxy); } QString ProxyAuthHandler::keychainPasswordKey() const { return QString::fromLatin1("%1/password").arg(_proxy); }