1 /* ============================================================
2 * QuiteRSS is a open-source cross-platform RSS/Atom news feeds reader
3 * Copyright (C) 2011-2020 QuiteRSS Team <quiterssteam@gmail.com>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 * ============================================================ */
18 #include "networkmanager.h"
19
20 #include "mainapplication.h"
21 #include "common.h"
22 #include "settings.h"
23 #include "authenticationdialog.h"
24 #include "adblockmanager.h"
25 #include "webpage.h"
26 #include "sslerrordialog.h"
27 #include "cabundleupdater.h"
28
29 #include <QNetworkReply>
30 #include <QSslSocket>
31 #include <QDebug>
32
fileNameForCert(const QSslCertificate & cert)33 static QString fileNameForCert(const QSslCertificate &cert)
34 {
35 QString certFileName = SslErrorDialog::certificateItemText(cert);
36 certFileName.remove(QLatin1Char(' '));
37 certFileName.append(QLatin1String(".crt"));
38 certFileName = Common::filterCharsFromFilename(certFileName);
39
40 while (certFileName.startsWith(QLatin1Char('.'))) {
41 certFileName = certFileName.mid(1);
42 }
43
44 return certFileName;
45 }
46
NetworkManager(bool isThread,QObject * parent)47 NetworkManager::NetworkManager(bool isThread, QObject* parent)
48 : QNetworkAccessManager(parent)
49 , ignoreAllWarnings_(false)
50 , adblockManager_(0)
51 {
52 setCookieJar(mainApp->cookieJar());
53 // CookieJar is shared between NetworkManagers
54 mainApp->cookieJar()->setParent(0);
55
56 #ifndef QT_NO_NETWORKPROXY
57 qRegisterMetaType<QNetworkProxy>("QNetworkProxy");
58 qRegisterMetaType<QList<QSslError> >("QList<QSslError>");
59 #endif
60
61 connect(this, SIGNAL(sslErrors(QNetworkReply*, QList<QSslError>)),
62 this, SLOT(slotSslError(QNetworkReply*, QList<QSslError>)));
63
64 if (isThread) {
65 connect(this, SIGNAL(authenticationRequired(QNetworkReply*,QAuthenticator*)),
66 mainApp->networkManager(), SLOT(slotAuthentication(QNetworkReply*,QAuthenticator*)),
67 Qt::BlockingQueuedConnection);
68 connect(this, SIGNAL(proxyAuthenticationRequired(QNetworkProxy,QAuthenticator*)),
69 mainApp->networkManager(), SLOT(slotProxyAuthentication(QNetworkProxy,QAuthenticator*)),
70 Qt::BlockingQueuedConnection);
71
72 } else {
73 connect(this, SIGNAL(authenticationRequired(QNetworkReply*,QAuthenticator*)),
74 SLOT(slotAuthentication(QNetworkReply*,QAuthenticator*)));
75 connect(this, SIGNAL(proxyAuthenticationRequired(QNetworkProxy,QAuthenticator*)),
76 SLOT(slotProxyAuthentication(QNetworkProxy,QAuthenticator*)));
77
78 loadSettings();
79 }
80 }
81
~NetworkManager()82 NetworkManager::~NetworkManager()
83 {
84 }
85
loadSettings()86 void NetworkManager::loadSettings()
87 {
88 #if defined(Q_OS_WIN) || defined(Q_OS_OS2)
89 QString certDir = mainApp->dataDir() + "/certificates";
90 QString bundlePath = certDir + "/ca-bundle.crt";
91 QString bundleVersionPath = certDir + "/bundle_version";
92
93 if (!QDir(certDir).exists()) {
94 QDir dir;
95 dir.mkdir(certDir);
96 }
97
98 if (!QFile::exists(bundlePath)) {
99 QFile(":data/ca-bundle.crt").copy(bundlePath);
100 QFile(bundlePath).setPermissions(QFile::ReadUser | QFile::WriteUser);
101
102 QFile(":data/bundle_version").copy(bundleVersionPath);
103 QFile(bundleVersionPath).setPermissions(QFile::ReadUser | QFile::WriteUser);
104 }
105
106 QSslSocket::setDefaultCaCertificates(QSslCertificate::fromPath(bundlePath));
107 #else
108 QSslSocket::setDefaultCaCertificates(QSslSocket::systemCaCertificates());
109 #endif
110
111 loadCertificates();
112 }
113
loadCertificates()114 void NetworkManager::loadCertificates()
115 {
116 Settings settings;
117 settings.beginGroup("SSL-Configuration");
118 certPaths_ = settings.value("CACertPaths", QStringList()).toStringList();
119 ignoreAllWarnings_ = settings.value("IgnoreAllSSLWarnings", false).toBool();
120 settings.endGroup();
121
122 // CA Certificates
123 caCerts_ = QSslSocket::defaultCaCertificates();
124
125 foreach (const QString &path, certPaths_) {
126 #ifdef Q_OS_WIN
127 // Used from Qt 4.7.4 qsslcertificate.cpp and modified because QSslCertificate::fromPath
128 // is kind of a bugged on Windows, it does work only with full path to cert file
129 QDirIterator it(path, QDir::Files, QDirIterator::FollowSymlinks | QDirIterator::Subdirectories);
130 while (it.hasNext()) {
131 QString filePath = it.next();
132 if (!filePath.endsWith(QLatin1String(".crt"))) {
133 continue;
134 }
135
136 QFile file(filePath);
137 if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
138 caCerts_ += QSslCertificate::fromData(file.readAll(), QSsl::Pem);
139 }
140 }
141 #else
142 caCerts_ += QSslCertificate::fromPath(path + "/*.crt", QSsl::Pem, QRegExp::Wildcard);
143 #endif
144 }
145 // Local Certificates
146 #ifdef Q_OS_WIN
147 QDirIterator it_(mainApp->dataDir() + "/certificates", QDir::Files, QDirIterator::FollowSymlinks | QDirIterator::Subdirectories);
148 while (it_.hasNext()) {
149 QString filePath = it_.next();
150 if (!filePath.endsWith(QLatin1String(".crt"))) {
151 continue;
152 }
153
154 QFile file(filePath);
155 if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
156 localCerts_ += QSslCertificate::fromData(file.readAll(), QSsl::Pem);
157 }
158 }
159 #else
160 localCerts_ = QSslCertificate::fromPath(mainApp->dataDir() + "/certificates/*.crt", QSsl::Pem, QRegExp::Wildcard);
161 #endif
162
163 QSslSocket::setDefaultCaCertificates(caCerts_ + localCerts_);
164
165 #if defined(Q_OS_WIN) || defined(Q_OS_OS2)
166 new CaBundleUpdater(this, this);
167 #endif
168 }
169
170 /** @brief Request authentification
171 *---------------------------------------------------------------------------*/
slotAuthentication(QNetworkReply * reply,QAuthenticator * auth)172 void NetworkManager::slotAuthentication(QNetworkReply *reply, QAuthenticator *auth)
173 {
174 AuthenticationDialog *authenticationDialog =
175 new AuthenticationDialog(reply->url(), auth);
176
177 if (!authenticationDialog->save_->isChecked())
178 authenticationDialog->exec();
179
180 delete authenticationDialog;
181 }
182 /** @brief Request proxy authentification
183 *---------------------------------------------------------------------------*/
slotProxyAuthentication(const QNetworkProxy & proxy,QAuthenticator * auth)184 void NetworkManager::slotProxyAuthentication(const QNetworkProxy &proxy, QAuthenticator *auth)
185 {
186 AuthenticationDialog *authenticationDialog =
187 new AuthenticationDialog(proxy.hostName(), auth);
188
189 if (!authenticationDialog->save_->isChecked())
190 authenticationDialog->exec();
191
192 delete authenticationDialog;
193 }
194
qHash(const QSslCertificate & cert)195 static inline uint qHash(const QSslCertificate &cert)
196 {
197 return qHash(cert.toPem());
198 }
199
slotSslError(QNetworkReply * reply,QList<QSslError> errors)200 void NetworkManager::slotSslError(QNetworkReply *reply, QList<QSslError> errors)
201 {
202 if (ignoreAllWarnings_ || reply->property("downloadReply").toBool() ||
203 (mainApp->networkManager() != this)) {
204 reply->ignoreSslErrors(errors);
205 return;
206 }
207
208 QHash<QSslCertificate, QStringList> errorHash;
209 foreach (const QSslError &error, errors) {
210 // Weird behavior on Windows
211 if (error.error() == QSslError::NoError) {
212 continue;
213 }
214
215 QSslCertificate cert = error.certificate();
216
217 if (errorHash.contains(cert)) {
218 errorHash[cert].append(error.errorString());
219 }
220 else {
221 errorHash.insert(cert, QStringList(error.errorString()));
222 }
223 }
224
225 // User already rejected those certs
226 if (containsRejectedCerts(errorHash.keys())) {
227 return;
228 }
229
230 QString title = tr("SSL Certificate Error!");
231 QString text1 = QString(tr("The \"%1\" server has the following errors in the SSL certificate:")).
232 arg(reply->url().host());
233
234 QString certs;
235
236 QHash<QSslCertificate, QStringList>::const_iterator i = errorHash.constBegin();
237 while (i != errorHash.constEnd()) {
238 const QSslCertificate cert = i.key();
239 const QStringList errors = i.value();
240
241 if (localCerts_.contains(cert) || tempAllowedCerts_.contains(cert) || errors.isEmpty()) {
242 ++i;
243 continue;
244 }
245
246 certs += "<ul><li>";
247 certs += tr("<b>Organization: </b>") +
248 SslErrorDialog::clearCertSpecialSymbols(cert.subjectInfo(QSslCertificate::Organization));
249 certs += "</li><li>";
250 certs += tr("<b>Domain Name: </b>") +
251 SslErrorDialog::clearCertSpecialSymbols(cert.subjectInfo(QSslCertificate::CommonName));
252 certs += "</li><li>";
253 certs += tr("<b>Expiration Date: </b>") +
254 cert.expiryDate().toString("hh:mm:ss dddd d. MMMM yyyy");
255 certs += "</li></ul>";
256
257 certs += "<ul>";
258 foreach (const QString &error, errors) {
259 certs += "<li>";
260 certs += tr("<b>Error: </b>") + error;
261 certs += "</li>";
262 }
263 certs += "</ul>";
264
265 ++i;
266 }
267
268 QString text2 = tr("Would you like to make an exception for this certificate?");
269 QString message = QString("<b>%1</b><p>%2</p>%3<p>%4</p><br>").arg(title, text1, certs, text2);
270
271 if (!certs.isEmpty()) {
272 SslErrorDialog dialog(mainApp->mainWindow());
273 dialog.setText(message);
274 dialog.exec();
275
276 switch (dialog.result()) {
277 case SslErrorDialog::Yes:
278 foreach (const QSslCertificate &cert, errorHash.keys()) {
279 if (!localCerts_.contains(cert)) {
280 addLocalCertificate(cert);
281 }
282 }
283 break;
284 case SslErrorDialog::OnlyForThisSession:
285 foreach (const QSslCertificate &cert, errorHash.keys()) {
286 if (!tempAllowedCerts_.contains(cert)) {
287 tempAllowedCerts_.append(cert);
288 }
289 }
290 break;
291 default:
292 // To prevent asking user more than once for the same certificate
293 addRejectedCerts(errorHash.keys());
294 return;
295 }
296 }
297
298 reply->ignoreSslErrors(errors);
299 }
300
createRequest(QNetworkAccessManager::Operation op,const QNetworkRequest & request,QIODevice * outgoingData)301 QNetworkReply *NetworkManager::createRequest(QNetworkAccessManager::Operation op,
302 const QNetworkRequest &request,
303 QIODevice *outgoingData)
304 {
305 if (mainApp->networkManager() == this) {
306 QNetworkReply *reply = 0;
307
308 // Adblock
309 if (op == QNetworkAccessManager::GetOperation) {
310 if (!adblockManager_) {
311 adblockManager_ = AdBlockManager::instance();
312 }
313
314 reply = adblockManager_->block(request);
315 if (reply) {
316 return reply;
317 }
318 }
319 }
320
321 return QNetworkAccessManager::createRequest(op, request, outgoingData);
322 }
323
addRejectedCerts(const QList<QSslCertificate> & certs)324 void NetworkManager::addRejectedCerts(const QList<QSslCertificate> &certs)
325 {
326 foreach (const QSslCertificate &cert, certs) {
327 if (!rejectedSslCerts_.contains(cert)) {
328 rejectedSslCerts_.append(cert);
329 }
330 }
331 }
332
containsRejectedCerts(const QList<QSslCertificate> & certs)333 bool NetworkManager::containsRejectedCerts(const QList<QSslCertificate> &certs)
334 {
335 int matches = 0;
336
337 foreach (const QSslCertificate &cert, certs) {
338 if (rejectedSslCerts_.contains(cert)) {
339 ++matches;
340 }
341 }
342
343 return matches == certs.count();
344 }
345
addLocalCertificate(const QSslCertificate & cert)346 void NetworkManager::addLocalCertificate(const QSslCertificate &cert)
347 {
348 localCerts_.append(cert);
349 QSslSocket::addDefaultCaCertificate(cert);
350
351 QDir dir(mainApp->dataDir());
352 if (!dir.exists("certificates")) {
353 dir.mkdir("certificates");
354 }
355
356 QString certFileName = fileNameForCert(cert);
357 QString fileName = Common::ensureUniqueFilename(mainApp->dataDir() + "/certificates/" + certFileName);
358
359 QFile file(fileName);
360 if (file.open(QFile::WriteOnly)) {
361 file.write(cert.toPem());
362 file.close();
363 }
364 else {
365 qWarning() << "NetworkManager::addLocalCertificate cannot write to file: " << fileName;
366 }
367 }
368
removeLocalCertificate(const QSslCertificate & cert)369 void NetworkManager::removeLocalCertificate(const QSslCertificate &cert)
370 {
371 localCerts_.removeOne(cert);
372
373 QList<QSslCertificate> certs = QSslSocket::defaultCaCertificates();
374 certs.removeOne(cert);
375 QSslSocket::setDefaultCaCertificates(certs);
376
377 // Delete cert file from profile
378 bool deleted = false;
379 QDirIterator it(mainApp->dataDir() + "/certificates", QDir::Files, QDirIterator::FollowSymlinks | QDirIterator::Subdirectories);
380 while (it.hasNext()) {
381 const QString filePath = it.next();
382 const QList<QSslCertificate> &certs = QSslCertificate::fromPath(filePath);
383 if (certs.isEmpty()) {
384 continue;
385 }
386
387 const QSslCertificate cert_ = certs.at(0);
388 if (cert == cert_) {
389 QFile file(filePath);
390 if (!file.remove()) {
391 qWarning() << "NetworkManager::removeLocalCertificate cannot remove file" << filePath;
392 }
393
394 deleted = true;
395 break;
396 }
397 }
398
399 if (!deleted) {
400 qWarning() << "NetworkManager::removeLocalCertificate cannot remove certificate";
401 }
402 }
403