File indexing completed on 2024-05-12 08:42:56
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"