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