File indexing completed on 2024-04-28 15:27:12

0001 /*
0002     SPDX-FileCopyrightText: 2003 Malte Starostik <malte@kde.org>
0003     SPDX-FileCopyrightText: 2011 Dawit Alemayehu <adawit@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "proxyscout.h"
0009 
0010 #include "config-kpac.h"
0011 
0012 #include "discovery.h"
0013 #include "script.h"
0014 
0015 #include <KLocalizedString>
0016 #include <KPluginFactory>
0017 #include <QDebug>
0018 #include <kprotocolmanager.h>
0019 
0020 #ifdef HAVE_KF5NOTIFICATIONS
0021 #include <KNotification>
0022 #endif
0023 
0024 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0025 #include <QNetworkConfigurationManager>
0026 #endif
0027 
0028 #include <QDBusConnection>
0029 #include <QFileSystemWatcher>
0030 
0031 #include <cstdlib>
0032 #include <ctime>
0033 
0034 #include <QLoggingCategory>
0035 Q_DECLARE_LOGGING_CATEGORY(KIO_KPAC)
0036 Q_LOGGING_CATEGORY(KIO_CORE_DIRLISTER, "kf.kio.kpac", QtWarningMsg)
0037 
0038 namespace KPAC
0039 {
0040 K_PLUGIN_CLASS_WITH_JSON(ProxyScout, "proxyscout.json")
0041 
0042 enum ProxyType {
0043     Unknown = -1,
0044     Proxy,
0045     Socks,
0046     Direct,
0047 };
0048 
0049 static ProxyType proxyTypeFor(const QString &mode)
0050 {
0051     if (mode.compare(QLatin1String("PROXY"), Qt::CaseInsensitive) == 0) {
0052         return Proxy;
0053     }
0054 
0055     if (mode.compare(QLatin1String("DIRECT"), Qt::CaseInsensitive) == 0) {
0056         return Direct;
0057     }
0058 
0059     if (mode.compare(QLatin1String("SOCKS"), Qt::CaseInsensitive) == 0 || mode.compare(QLatin1String("SOCKS5"), Qt::CaseInsensitive) == 0) {
0060         return Socks;
0061     }
0062 
0063     return Unknown;
0064 }
0065 
0066 ProxyScout::QueuedRequest::QueuedRequest(const QDBusMessage &reply, const QUrl &u, bool sendall)
0067     : transaction(reply)
0068     , url(u)
0069     , sendAll(sendall)
0070 {
0071 }
0072 
0073 // Silence deprecation warnings as there is no Qt 5 substitute for QNetworkConfigurationManager
0074 QT_WARNING_PUSH
0075 QT_WARNING_DISABLE_CLANG("-Wdeprecated-declarations")
0076 QT_WARNING_DISABLE_GCC("-Wdeprecated-declarations")
0077 ProxyScout::ProxyScout(QObject *parent, const QList<QVariant> &)
0078     : KDEDModule(parent)
0079     , m_componentName(QStringLiteral("proxyscout"))
0080     , m_downloader(nullptr)
0081     , m_script(nullptr)
0082     , m_suspendTime(0)
0083     , m_watcher(nullptr)
0084 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0085     , m_networkConfig(new QNetworkConfigurationManager(this))
0086 #endif
0087 {
0088 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0089     QNetworkInformation::load(QNetworkInformation::Feature::Reachability);
0090     connect(QNetworkInformation::instance(), &QNetworkInformation::reachabilityChanged, this, &ProxyScout::disconnectNetwork);
0091 #else
0092     connect(m_networkConfig, &QNetworkConfigurationManager::configurationChanged, this, &ProxyScout::disconnectNetwork);
0093 #endif
0094 }
0095 QT_WARNING_POP
0096 
0097 ProxyScout::~ProxyScout()
0098 {
0099     delete m_script;
0100 }
0101 
0102 QStringList ProxyScout::proxiesForUrl(const QString &checkUrl, const QDBusMessage &msg)
0103 {
0104     QUrl url(checkUrl);
0105 
0106     if (m_suspendTime) {
0107         if (std::time(nullptr) - m_suspendTime < 300) {
0108             return QStringList(QStringLiteral("DIRECT"));
0109         }
0110         m_suspendTime = 0;
0111     }
0112 
0113     // Never use a proxy for the script itself
0114     if (m_downloader && url.matches(m_downloader->scriptUrl(), QUrl::StripTrailingSlash)) {
0115         return QStringList(QStringLiteral("DIRECT"));
0116     }
0117 
0118     if (m_script) {
0119         return handleRequest(url);
0120     }
0121 
0122     if (m_downloader || startDownload()) {
0123         msg.setDelayedReply(true);
0124         m_requestQueue.append(QueuedRequest(msg, url, true));
0125         return QStringList(); // return value will be ignored
0126     }
0127 
0128     return QStringList(QStringLiteral("DIRECT"));
0129 }
0130 
0131 QString ProxyScout::proxyForUrl(const QString &checkUrl, const QDBusMessage &msg)
0132 {
0133     QUrl url(checkUrl);
0134 
0135     if (m_suspendTime) {
0136         if (std::time(nullptr) - m_suspendTime < 300) {
0137             return QStringLiteral("DIRECT");
0138         }
0139         m_suspendTime = 0;
0140     }
0141 
0142     // Never use a proxy for the script itself
0143     if (m_downloader && url.matches(m_downloader->scriptUrl(), QUrl::StripTrailingSlash)) {
0144         return QStringLiteral("DIRECT");
0145     }
0146 
0147     if (m_script) {
0148         return handleRequest(url).constFirst();
0149     }
0150 
0151     if (m_downloader || startDownload()) {
0152         msg.setDelayedReply(true);
0153         m_requestQueue.append(QueuedRequest(msg, url));
0154         return QString(); // return value will be ignored
0155     }
0156 
0157     return QStringLiteral("DIRECT");
0158 }
0159 
0160 void ProxyScout::blackListProxy(const QString &proxy)
0161 {
0162     m_blackList[proxy] = std::time(nullptr);
0163 }
0164 
0165 void ProxyScout::reset()
0166 {
0167     delete m_script;
0168     m_script = nullptr;
0169     delete m_downloader;
0170     m_downloader = nullptr;
0171     delete m_watcher;
0172     m_watcher = nullptr;
0173     m_blackList.clear();
0174     m_suspendTime = 0;
0175     KProtocolManager::reparseConfiguration();
0176 }
0177 
0178 bool ProxyScout::startDownload()
0179 {
0180     switch (KProtocolManager::proxyType()) {
0181     case KProtocolManager::WPADProxy:
0182         if (m_downloader && !qobject_cast<Discovery *>(m_downloader)) {
0183             delete m_downloader;
0184             m_downloader = nullptr;
0185         }
0186         if (!m_downloader) {
0187             m_downloader = new Discovery(this);
0188             connect(m_downloader, qOverload<bool>(&Downloader::result), this, &ProxyScout::downloadResult);
0189         }
0190         break;
0191     case KProtocolManager::PACProxy: {
0192         if (m_downloader && !qobject_cast<Downloader *>(m_downloader)) {
0193             delete m_downloader;
0194             m_downloader = nullptr;
0195         }
0196         if (!m_downloader) {
0197             m_downloader = new Downloader(this);
0198             connect(m_downloader, qOverload<bool>(&Downloader::result), this, &ProxyScout::downloadResult);
0199         }
0200 
0201         const QUrl url(KProtocolManager::proxyConfigScript());
0202         if (url.isLocalFile()) {
0203             if (!m_watcher) {
0204                 m_watcher = new QFileSystemWatcher(this);
0205                 connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &ProxyScout::proxyScriptFileChanged);
0206             }
0207             proxyScriptFileChanged(url.path());
0208         } else {
0209             delete m_watcher;
0210             m_watcher = nullptr;
0211             m_downloader->download(url);
0212         }
0213         break;
0214     }
0215     default:
0216         return false;
0217     }
0218 
0219     return true;
0220 }
0221 
0222 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0223 void ProxyScout::disconnectNetwork(QNetworkInformation::Reachability newReachability)
0224 {
0225     if (!QNetworkInformation::instance()->supports(QNetworkInformation::Feature::Reachability)) {
0226         qCWarning(KIO_KPAC) << "Current QNetworkInformation backend doesn't support QNetworkInformation::Feature::Reachability";
0227     }
0228 
0229     // NOTE: We only care about "Local" and "Site" states because we only
0230     // want to redo WPAD when a network interface is brought out of hibernation
0231     // or restarted for whatever reason.
0232     switch (newReachability) {
0233     case QNetworkInformation::Reachability::Local:
0234     case QNetworkInformation::Reachability::Site:
0235         reset();
0236         break;
0237     default:
0238         // Nothing else to do
0239         break;
0240     }
0241 }
0242 #else
0243 void ProxyScout::disconnectNetwork(const QNetworkConfiguration &config)
0244 {
0245     // NOTE: We only care of Defined state because we only want
0246     // to redo WPAD when a network interface is brought out of
0247     // hibernation or restarted for whatever reason.
0248     // Silence deprecation warnings as there is no Qt 5 substitute for QNetworkConfigurationManager
0249     QT_WARNING_PUSH
0250     QT_WARNING_DISABLE_CLANG("-Wdeprecated-declarations")
0251     QT_WARNING_DISABLE_GCC("-Wdeprecated-declarations")
0252     if (config.state() == QNetworkConfiguration::Defined) {
0253         reset();
0254     }
0255     QT_WARNING_POP
0256 }
0257 #endif
0258 
0259 void ProxyScout::downloadResult(bool success)
0260 {
0261     if (success) {
0262         try {
0263             if (!m_script) {
0264                 m_script = new Script(m_downloader->script());
0265             }
0266         } catch (const Script::Error &e) {
0267             qWarning() << "Error:" << e.message();
0268 #ifdef HAVE_KF5NOTIFICATIONS
0269             KNotification *notify = new KNotification(QStringLiteral("script-error"));
0270             notify->setText(i18n("The proxy configuration script is invalid:\n%1", e.message()));
0271             notify->setComponentName(m_componentName);
0272             notify->sendEvent();
0273 #endif
0274             success = false;
0275         }
0276     } else {
0277 #ifdef HAVE_KF5NOTIFICATIONS
0278         KNotification *notify = new KNotification(QStringLiteral("download-error"));
0279         notify->setText(m_downloader->error());
0280         notify->setComponentName(m_componentName);
0281         notify->sendEvent();
0282 #endif
0283     }
0284 
0285     if (success) {
0286         for (const QueuedRequest &request : std::as_const(m_requestQueue)) {
0287             if (request.sendAll) {
0288                 const QVariant result(handleRequest(request.url));
0289                 QDBusConnection::sessionBus().send(request.transaction.createReply(result));
0290             } else {
0291                 const QVariant result(handleRequest(request.url).constFirst());
0292                 QDBusConnection::sessionBus().send(request.transaction.createReply(result));
0293             }
0294         }
0295     } else {
0296         for (const QueuedRequest &request : std::as_const(m_requestQueue)) {
0297             QDBusConnection::sessionBus().send(request.transaction.createReply(QLatin1String("DIRECT")));
0298         }
0299     }
0300 
0301     m_requestQueue.clear();
0302 
0303     // Suppress further attempts for 5 minutes
0304     if (!success) {
0305         m_suspendTime = std::time(nullptr);
0306     }
0307 }
0308 
0309 void ProxyScout::proxyScriptFileChanged(const QString &path)
0310 {
0311     // Should never get called if we do not have a watcher...
0312     Q_ASSERT(m_watcher);
0313 
0314     // Remove the current file being watched...
0315     if (!m_watcher->files().isEmpty()) {
0316         m_watcher->removePaths(m_watcher->files());
0317     }
0318 
0319     // NOTE: QFileSystemWatcher only adds a path if it either exists or
0320     // is not already being monitored.
0321     m_watcher->addPath(path);
0322 
0323     // Reload...
0324     m_downloader->download(QUrl::fromLocalFile(path));
0325 }
0326 
0327 QStringList ProxyScout::handleRequest(const QUrl &url)
0328 {
0329     try {
0330         QStringList proxyList;
0331         const QString result = m_script->evaluate(url).trimmed();
0332         const QStringList proxies = result.split(QLatin1Char(';'), Qt::SkipEmptyParts);
0333         const int size = proxies.count();
0334 
0335         for (int i = 0; i < size; ++i) {
0336             QString mode;
0337             QString address;
0338             const QString proxy = proxies.at(i).trimmed();
0339             const int index = proxy.indexOf(QLatin1Char(' '));
0340             if (index == -1) { // Only "DIRECT" should match this!
0341                 mode = proxy;
0342                 address = proxy;
0343             } else {
0344                 mode = proxy.left(index);
0345                 address = proxy.mid(index + 1).trimmed();
0346             }
0347 
0348             const ProxyType type = proxyTypeFor(mode);
0349             if (type == Unknown) {
0350                 continue;
0351             }
0352 
0353             if (type == Proxy || type == Socks) {
0354                 const int index = address.indexOf(QLatin1Char(':'));
0355                 if (index == -1 || !KProtocolInfo::isKnownProtocol(address.left(index))) {
0356                     const QString protocol((type == Proxy ? QStringLiteral("http://") : QStringLiteral("socks://")));
0357                     const QUrl url(protocol + address);
0358                     if (url.isValid()) {
0359                         address = url.toString();
0360                     } else {
0361                         continue;
0362                     }
0363                 }
0364             }
0365 
0366             if (type == Direct || !m_blackList.contains(address)) {
0367                 proxyList << address;
0368             } else if (std::time(nullptr) - m_blackList[address] > 1800) { // 30 minutes
0369                 // black listing expired
0370                 m_blackList.remove(address);
0371                 proxyList << address;
0372             }
0373         }
0374 
0375         if (!proxyList.isEmpty()) {
0376             // qDebug() << proxyList;
0377             return proxyList;
0378         }
0379         // FIXME: blacklist
0380     } catch (const Script::Error &e) {
0381         qCritical() << e.message();
0382 #ifdef HAVE_KF5NOTIFICATIONS
0383         KNotification *n = new KNotification(QStringLiteral("evaluation-error"));
0384         n->setText(i18n("The proxy configuration script returned an error:\n%1", e.message()));
0385         n->setComponentName(m_componentName);
0386         n->sendEvent();
0387 #endif
0388     }
0389 
0390     return QStringList(QStringLiteral("DIRECT"));
0391 }
0392 }
0393 
0394 #include "moc_proxyscout.cpp"
0395 #include "proxyscout.moc"