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