File indexing completed on 2024-04-28 04:41:44

0001 /*
0002     SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include <QCoreApplication>
0008 #include <QDebug>
0009 #include <QDirIterator>
0010 #include <QJsonArray>
0011 #include <QJsonDocument>
0012 #include <QJsonObject>
0013 #include <QNetworkAccessManager>
0014 #include <QNetworkReply>
0015 #include <QNetworkRequest>
0016 #include <QRectF>
0017 
0018 #include <deque>
0019 #include <iostream>
0020 
0021 class Job
0022 {
0023 public:
0024     QString fileName;
0025     QUrl endpoint;
0026     QList<QSslCertificate> caCert;
0027 
0028     enum Task {
0029         HttpsProbe,
0030         HttpsCustomCaProbe,
0031         HttpProbe,
0032     };
0033     int task = HttpsProbe;
0034 };
0035 
0036 class EndpointProber : public QObject
0037 {
0038     Q_OBJECT
0039 public:
0040     void addJob(Job &&job);
0041 
0042 Q_SIGNALS:
0043     void finished();
0044 
0045 public:
0046     void processNext();
0047     QNetworkAccessManager m_nam;
0048 
0049 private:
0050     std::deque<Job> m_queue;
0051     bool m_jobRunning = false;
0052 };
0053 
0054 
0055 void EndpointProber::addJob(Job &&job)
0056 {
0057     // we need to do this strictly sequential, so we can reset the QNAM connection cache
0058     // without doing that changes to the SSL config will have no effect
0059     m_queue.push_back(std::move(job));
0060     processNext();
0061 }
0062 
0063 void EndpointProber::processNext()
0064 {
0065     if (m_jobRunning) {
0066         return;
0067     }
0068     if (m_queue.empty()) {
0069         Q_EMIT finished();
0070         return;
0071     }
0072 
0073     auto job = m_queue.front();
0074     m_queue.pop_front();
0075 
0076     QNetworkReply *reply = nullptr;
0077     switch (job.task) {
0078         case Job::HttpsProbe:
0079         {
0080             auto url = job.endpoint;
0081             url.setScheme(QStringLiteral("https"));
0082             qDebug() << "https probe on" << job.fileName;
0083             reply = m_nam.get(QNetworkRequest(url));
0084             break;
0085         }
0086         case Job::HttpsCustomCaProbe:
0087         {
0088             if (job.caCert.isEmpty()) {
0089                 job.task = Job::HttpProbe;
0090                 addJob(std::move(job));
0091                 return;
0092             } else {
0093                 m_nam.clearConnectionCache();
0094                 auto url = job.endpoint;
0095                 url.setScheme(QStringLiteral("https"));
0096                 QNetworkRequest req(url);
0097                 auto sslConfig = req.sslConfiguration();
0098                 sslConfig.setCaCertificates(job.caCert);
0099                 req.setSslConfiguration(sslConfig);
0100                 qDebug() << "https w/ custom CA probe on" << job.fileName;
0101                 reply = m_nam.get(req);
0102             }
0103             break;
0104         }
0105         case Job::HttpProbe:
0106         {
0107             auto url = job.endpoint;
0108             url.setScheme(QStringLiteral("http"));
0109             qDebug() << "http probe on" << job.fileName;
0110             reply = m_nam.get(QNetworkRequest(url));
0111             break;
0112         }
0113     }
0114 
0115     m_jobRunning = true;
0116     connect(reply, &QNetworkReply::sslErrors, this, [job](const auto &errors) {
0117         if (job.task == Job::HttpsProbe && !job.caCert.empty()) {
0118             return;
0119         }
0120         qWarning() << "SSL failure on" << job.fileName;
0121         for (const auto &error : errors) {
0122             qWarning() << "  " << error << error.certificate();
0123         }
0124     });
0125     QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, job]() {
0126         reply->deleteLater();
0127         m_jobRunning = false;
0128         bool success = true;
0129         switch (reply->error()) {
0130             case QNetworkReply::ConnectionRefusedError:
0131             case QNetworkReply::RemoteHostClosedError:
0132             case QNetworkReply::HostNotFoundError:
0133             case QNetworkReply::TimeoutError:
0134             case QNetworkReply::TooManyRedirectsError:
0135             case QNetworkReply::InsecureRedirectError:
0136             case QNetworkReply::UnknownNetworkError:
0137             case QNetworkReply::ProtocolFailure:
0138             case QNetworkReply::UnknownServerError:
0139                 qWarning() << "Network error on" << job.fileName << reply->errorString() << job.task << reply->request().url();
0140                 success = false;
0141                 break;
0142             case QNetworkReply::SslHandshakeFailedError:
0143                 success = (job.task == Job::HttpsCustomCaProbe || job.caCert.isEmpty());
0144                 break; // handled above
0145             case QNetworkReply::NoError:
0146             case QNetworkReply::ContentNotFoundError:
0147             case QNetworkReply::ContentAccessDenied:
0148             case QNetworkReply::InternalServerError:
0149                 break; // considered successful
0150             default:
0151                 qWarning() << "Unhandled error case:" << reply->error() << reply->errorString();
0152                 break;
0153         }
0154 
0155         // TODO update config file with the results here!
0156         if (success) {
0157             switch (job.task) {
0158                 case Job::HttpsProbe:
0159                     if (job.endpoint.scheme() == QLatin1String("http")) {
0160                         qWarning() << job.fileName << "can be upgraded to https!";
0161                     }
0162                     break;
0163                 case Job::HttpsCustomCaProbe:
0164                     qWarning() << job.fileName << "needs a custom CA workaround";
0165                     break;
0166                 case Job::HttpProbe:
0167                     qWarning() << job.fileName << "does not support transport security!";
0168                     break;
0169             }
0170         } else {
0171             if (job.task != Job::HttpProbe) {
0172                 auto nextJob = job;
0173                 nextJob.task++;
0174                 addJob(std::move(nextJob));
0175             } else {
0176                 qWarning() << job.fileName << "seems dead!";
0177             }
0178         }
0179 
0180         processNext();
0181     });
0182 }
0183 
0184 
0185 /** Probes availabiliy of endpoint URLs and their SSL setups. */
0186 int main(int argc, char **argv)
0187 {
0188     QCoreApplication app(argc, argv);
0189     if (app.arguments().size() <= 1) {
0190         std::cerr << "Usage: " << argv[0] << " [path to network configs]" << std::endl;
0191         return 1;
0192     }
0193 
0194     EndpointProber prober;
0195     prober.m_nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
0196 
0197     QDirIterator it(app.arguments().at(1), QDir::Files);
0198     while (it.hasNext()) {
0199         Job job;
0200         job.fileName = it.next();
0201         QFile f(job.fileName);
0202         if (!f.fileName().endsWith(QLatin1String(".json"))) {
0203             continue;
0204         }
0205         if (!f.open(QFile::ReadOnly)) {
0206             qWarning() << "Failed to open" << f.fileName() << f.errorString();
0207             continue;
0208         }
0209 
0210         const auto doc = QJsonDocument::fromJson(f.readAll());
0211         const auto options = doc.object().value(QLatin1String("options")).toObject();
0212         job.endpoint = QUrl(options.value(QLatin1String("endpoint")).toString());
0213         if (!job.endpoint.isValid() || job.endpoint.scheme().isEmpty()) {
0214             qWarning() << "No API endpoint in" << job.fileName;
0215             continue;
0216         }
0217         job.caCert = QSslCertificate::fromPath(it.path() + QStringLiteral("/certs/") + options.value(QLatin1String("customCaCertificate")).toString());
0218 
0219         prober.addJob(std::move(job));
0220     }
0221 
0222     QObject::connect(&prober, &EndpointProber::finished, &app, &QCoreApplication::quit, Qt::QueuedConnection);
0223     return app.exec();
0224 }
0225 
0226 #include "endpointprobe.moc"