1 /*
2  * Copyright (C) 2015 by Christian Kamm <kamm@incasoftware.de>
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 "proxyauthhandler.h"
16 
17 #include "proxyauthdialog.h"
18 #include "theme.h"
19 #include "configfile.h"
20 #include "account.h"
21 
22 #include <QApplication>
23 
24 #include <qt5keychain/keychain.h>
25 
26 using namespace OCC;
27 using namespace QKeychain;
28 
29 Q_LOGGING_CATEGORY(lcProxy, "nextcloud.gui.credentials.proxy", QtInfoMsg)
30 
instance()31 ProxyAuthHandler *ProxyAuthHandler::instance()
32 {
33     static ProxyAuthHandler inst;
34     return &inst;
35 }
36 
ProxyAuthHandler()37 ProxyAuthHandler::ProxyAuthHandler()
38 {
39     _dialog = new ProxyAuthDialog();
40 
41     _configFile.reset(new ConfigFile);
42     _settings.reset(new QSettings(_configFile->configFile(), QSettings::IniFormat));
43     _settings->beginGroup(QLatin1String("Proxy"));
44     _settings->beginGroup(QLatin1String("Credentials"));
45 }
46 
~ProxyAuthHandler()47 ProxyAuthHandler::~ProxyAuthHandler()
48 {
49     delete _dialog;
50 }
51 
handleProxyAuthenticationRequired(const QNetworkProxy & proxy,QAuthenticator * authenticator)52 void ProxyAuthHandler::handleProxyAuthenticationRequired(
53     const QNetworkProxy &proxy,
54     QAuthenticator *authenticator)
55 {
56     if (!_dialog) {
57         return;
58     }
59 
60     QString key = proxy.hostName() + QLatin1Char(':') + QString::number(proxy.port());
61 
62     // If the proxy server has changed, forget what we know.
63     if (key != _proxy) {
64         _proxy = key;
65         _username.clear();
66         _password.clear();
67         _blocked = false;
68         _gaveCredentialsTo.clear();
69 
70         // If the user explicitly configured the proxy in the
71         // network settings, don't ask about it.
72         if (_configFile->proxyType() == QNetworkProxy::HttpProxy
73             || _configFile->proxyType() == QNetworkProxy::Socks5Proxy) {
74             _blocked = true;
75         }
76     }
77 
78     if (_blocked) {
79         return;
80     }
81 
82     // Find the responsible QNAM if possible.
83     QPointer<QNetworkAccessManager> sending_qnam = nullptr;
84     if (auto account = qobject_cast<Account *>(sender())) {
85         // Since we go into an event loop, it's possible for the account's qnam
86         // to be destroyed before we get back. We can use this to check for its
87         // liveness.
88         sending_qnam = account->sharedNetworkAccessManager().data();
89     }
90     if (!sending_qnam) {
91         qCWarning(lcProxy) << "Could not get the sending QNAM for" << sender();
92     }
93 
94 
95     qCInfo(lcProxy) << "Proxy auth required for" << key << proxy.type();
96 
97     // If we already had a username but auth still failed,
98     // invalidate the old credentials! Unfortunately, authenticator->user()
99     // isn't reliable, so we also invalidate credentials if we previously
100     // gave presumably valid credentials to the same QNAM.
101     bool invalidated = false;
102     if (!_waitingForDialog && !_waitingForKeychain && (!authenticator->user().isEmpty()
103                                                           || (sending_qnam && _gaveCredentialsTo.contains(sending_qnam)))) {
104         qCInfo(lcProxy) << "invalidating old creds" << key;
105         _username.clear();
106         _password.clear();
107         invalidated = true;
108         _gaveCredentialsTo.clear();
109     }
110 
111     if (_username.isEmpty() || _waitingForKeychain) {
112         if (invalidated || !getCredsFromKeychain()) {
113             if (getCredsFromDialog()) {
114                 storeCredsInKeychain();
115             } else {
116                 // dialog was cancelled, never ask for that proxy again
117                 _blocked = true;
118                 return;
119             }
120         }
121     }
122 
123     qCInfo(lcProxy) << "got creds for" << _proxy;
124     authenticator->setUser(_username);
125     authenticator->setPassword(_password);
126     if (sending_qnam) {
127         _gaveCredentialsTo.insert(sending_qnam);
128         connect(sending_qnam, &QObject::destroyed,
129             this, &ProxyAuthHandler::slotSenderDestroyed);
130     }
131 }
132 
slotSenderDestroyed(QObject * obj)133 void ProxyAuthHandler::slotSenderDestroyed(QObject *obj)
134 {
135     _gaveCredentialsTo.remove(obj);
136 }
137 
getCredsFromDialog()138 bool ProxyAuthHandler::getCredsFromDialog()
139 {
140     // Open the credentials dialog
141     if (!_waitingForDialog) {
142         _dialog->reset();
143         _dialog->setProxyAddress(_proxy);
144         _dialog->open();
145     }
146 
147     // This function can be reentered while the dialog is open.
148     // If that's the case, continue processing the dialog until
149     // it's done.
150     if(_dialog) {
151         execAwait(_dialog.data(),
152                   &QDialog::finished,
153                   _waitingForDialog,
154                   QEventLoop::ExcludeSocketNotifiers);
155     }
156 
157     if (_dialog && _dialog->result() == QDialog::Accepted) {
158         qCInfo(lcProxy) << "got creds for" << _proxy << "from dialog";
159         _username = _dialog->username();
160         _password = _dialog->password();
161         return true;
162     }
163     return false;
164 }
165 
166 template<class T, typename PointerToMemberFunction>
execAwait(const T * sender,PointerToMemberFunction signal,int & counter,const QEventLoop::ProcessEventsFlags flags)167 void ProxyAuthHandler::execAwait(const T *sender,
168                                  PointerToMemberFunction signal,
169                                  int &counter,
170                                  const QEventLoop::ProcessEventsFlags flags)
171 {
172     if (!sender) {
173         return;
174     }
175 
176     QEventLoop waitLoop;
177     connect(sender, signal, &waitLoop, &QEventLoop::quit);
178 
179     ++counter;
180     waitLoop.exec(flags);
181     --counter;
182 }
183 
getCredsFromKeychain()184 bool ProxyAuthHandler::getCredsFromKeychain()
185 {
186     if (_waitingForDialog) {
187         return false;
188     }
189 
190     qCDebug(lcProxy) << "trying to load" << _proxy;
191 
192     if (!_waitingForKeychain) {
193         _username = _settings->value(keychainUsernameKey()).toString();
194         if (_username.isEmpty()) {
195             return false;
196         }
197 
198         _readPasswordJob.reset(new ReadPasswordJob(Theme::instance()->appName()));
199         _readPasswordJob->setSettings(_settings.data());
200         _readPasswordJob->setInsecureFallback(false);
201         _readPasswordJob->setKey(keychainPasswordKey());
202         _readPasswordJob->setAutoDelete(false);
203         _readPasswordJob->start();
204     }
205 
206     // While we wait for the password job to be done, this code may be reentered.
207     // This really needs the counter and the flag here, because otherwise we get
208     // bad behavior when we reenter this code after the flag has been switched
209     // but before the while loop has finished.
210     execAwait(_readPasswordJob.data(),
211               &QKeychain::Job::finished,
212               _waitingForKeychain);
213 
214     if (_readPasswordJob->error() == NoError) {
215         qCInfo(lcProxy) << "got creds for" << _proxy << "from keychain";
216         _password = _readPasswordJob->textData();
217         return true;
218     }
219 
220     _username.clear();
221     if (_readPasswordJob->error() != EntryNotFound) {
222         qCWarning(lcProxy) << "ReadPasswordJob failed with" << _readPasswordJob->errorString();
223     }
224     return false;
225 }
226 
storeCredsInKeychain()227 void ProxyAuthHandler::storeCredsInKeychain()
228 {
229     if (_waitingForKeychain) {
230         return;
231     }
232 
233     qCInfo(lcProxy) << "storing" << _proxy;
234 
235     _settings->setValue(keychainUsernameKey(), _username);
236 
237     auto job = new WritePasswordJob(Theme::instance()->appName(), this);
238     job->setSettings(_settings.data());
239     job->setInsecureFallback(false);
240     job->setKey(keychainPasswordKey());
241     job->setTextData(_password);
242     job->setAutoDelete(false);
243     job->start();
244 
245     execAwait(job,
246               &QKeychain::Job::finished,
247               _waitingForKeychain);
248 
249     job->deleteLater();
250     if (job->error() != NoError) {
251         qCWarning(lcProxy) << "WritePasswordJob failed with" << job->errorString();
252     }
253 }
254 
keychainUsernameKey() const255 QString ProxyAuthHandler::keychainUsernameKey() const
256 {
257     return QString::fromLatin1("%1/username").arg(_proxy);
258 }
259 
keychainPasswordKey() const260 QString ProxyAuthHandler::keychainPasswordKey() const
261 {
262     return QString::fromLatin1("%1/password").arg(_proxy);
263 }
264