File indexing completed on 2025-02-16 10:02:06
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"