From c9a9e44e1fa3eb0d3e67607ce47b22c55d612a81 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 12 Feb 2023 00:16:51 +0100 Subject: [PATCH] Add HTTP & SOCKS5 proxy support (#4321) This can be configured using the `CHATTERINO2_PROXY_URL` environment variable. The behaviour is similar to curl's CURLOPT_PROXY --- CHANGELOG.md | 1 + src/CMakeLists.txt | 2 + src/common/Env.cpp | 12 +++ src/common/Env.hpp | 2 + src/common/QLogging.cpp | 1 + src/common/QLogging.hpp | 1 + src/main.cpp | 4 + .../NetworkConfigurationProvider.cpp | 76 +++++++++++++++++++ .../NetworkConfigurationProvider.hpp | 61 +++++++++++++++ .../liveupdates/BasicPubSubManager.hpp | 3 + src/providers/twitch/PubSubManager.cpp | 3 + 11 files changed, 166 insertions(+) create mode 100644 src/providers/NetworkConfigurationProvider.cpp create mode 100644 src/providers/NetworkConfigurationProvider.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b6c8df38..67b8811f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Minor: Added link to streamlink docs for easier user setup. (#4217) - Minor: Added setting to turn off rendering of reply context. (#4224) - Minor: Added setting to select which channels to log. (#4302) +- Minor: Added support for HTTP and Socks5 proxies through environment variables. (#4321) - Minor: Remove sending part of the multipart emoji workaround (#4361) - Bugfix: Fixed crash that would occur when performing certain actions after removing all tabs. (#4271) - Bugfix: Fixed highlight sounds not reloading on change properly. (#4194) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b412c6895..b9cb6da35 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -195,6 +195,8 @@ set(SOURCE_FILES providers/IvrApi.hpp providers/LinkResolver.cpp providers/LinkResolver.hpp + providers/NetworkConfigurationProvider.cpp + providers/NetworkConfigurationProvider.hpp providers/RecentMessagesApi.cpp providers/RecentMessagesApi.hpp diff --git a/src/common/Env.cpp b/src/common/Env.cpp index 36757b841..605e49247 100644 --- a/src/common/Env.cpp +++ b/src/common/Env.cpp @@ -44,6 +44,17 @@ namespace { return defaultValue; } + boost::optional readOptionalStringEnv(const char *envName) + { + auto envString = std::getenv(envName); + if (envString != nullptr) + { + return QString(envString); + } + + return boost::none; + } + uint16_t readPortEnv(const char *envName, uint16_t defaultValue) { auto envString = std::getenv(envName); @@ -89,6 +100,7 @@ Env::Env() readStringEnv("CHATTERINO2_TWITCH_SERVER_HOST", "irc.chat.twitch.tv")) , twitchServerPort(readPortEnv("CHATTERINO2_TWITCH_SERVER_PORT", 443)) , twitchServerSecure(readBoolEnv("CHATTERINO2_TWITCH_SERVER_SECURE", true)) + , proxyUrl(readOptionalStringEnv("CHATTERINO2_PROXY_URL")) { } diff --git a/src/common/Env.hpp b/src/common/Env.hpp index b334e8e96..97e5040d8 100644 --- a/src/common/Env.hpp +++ b/src/common/Env.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include namespace chatterino { @@ -16,6 +17,7 @@ public: const QString twitchServerHost; const uint16_t twitchServerPort; const bool twitchServerSecure; + const boost::optional proxyUrl; }; } // namespace chatterino diff --git a/src/common/QLogging.cpp b/src/common/QLogging.cpp index 8679e7a8d..473d5e4d7 100644 --- a/src/common/QLogging.cpp +++ b/src/common/QLogging.cpp @@ -28,6 +28,7 @@ Q_LOGGING_CATEGORY(chatterinoMain, "chatterino.main", logThreshold); Q_LOGGING_CATEGORY(chatterinoMessage, "chatterino.message", logThreshold); Q_LOGGING_CATEGORY(chatterinoNativeMessage, "chatterino.nativemessage", logThreshold); +Q_LOGGING_CATEGORY(chatterinoNetwork, "chatterino.network", logThreshold); Q_LOGGING_CATEGORY(chatterinoNotification, "chatterino.notification", logThreshold); Q_LOGGING_CATEGORY(chatterinoNuulsuploader, "chatterino.nuulsuploader", diff --git a/src/common/QLogging.hpp b/src/common/QLogging.hpp index 0aa50fae6..8c8f0d6c4 100644 --- a/src/common/QLogging.hpp +++ b/src/common/QLogging.hpp @@ -22,6 +22,7 @@ Q_DECLARE_LOGGING_CATEGORY(chatterinoLiveupdates); Q_DECLARE_LOGGING_CATEGORY(chatterinoMain); Q_DECLARE_LOGGING_CATEGORY(chatterinoMessage); Q_DECLARE_LOGGING_CATEGORY(chatterinoNativeMessage); +Q_DECLARE_LOGGING_CATEGORY(chatterinoNetwork); Q_DECLARE_LOGGING_CATEGORY(chatterinoNotification); Q_DECLARE_LOGGING_CATEGORY(chatterinoNuulsuploader); Q_DECLARE_LOGGING_CATEGORY(chatterinoPubSub); diff --git a/src/main.cpp b/src/main.cpp index 8ad92e3da..b91631c6d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,9 +1,11 @@ #include "BrowserExtension.hpp" #include "common/Args.hpp" +#include "common/Env.hpp" #include "common/Modes.hpp" #include "common/QLogging.hpp" #include "common/Version.hpp" #include "providers/IvrApi.hpp" +#include "providers/NetworkConfigurationProvider.hpp" #include "providers/twitch/api/Helix.hpp" #include "RunGui.hpp" #include "singletons/Paths.hpp" @@ -81,6 +83,8 @@ int main(int argc, char **argv) attachToConsole(); } + NetworkConfigurationProvider::applyFromEnv(Env::get()); + IvrApi::initialize(); Helix::initialize(); diff --git a/src/providers/NetworkConfigurationProvider.cpp b/src/providers/NetworkConfigurationProvider.cpp new file mode 100644 index 000000000..57291ac25 --- /dev/null +++ b/src/providers/NetworkConfigurationProvider.cpp @@ -0,0 +1,76 @@ +#include "providers/NetworkConfigurationProvider.hpp" + +#include "common/Env.hpp" +#include "common/QLogging.hpp" + +#include +#include +#include + +namespace { +/** + * Creates a QNetworkProxy from a given URL. + * + * Creates an HTTP proxy by default, a Socks5 will be created if the scheme is 'socks5'. + */ +QNetworkProxy createProxyFromUrl(const QUrl &url) +{ + QNetworkProxy proxy; + proxy.setHostName(url.host(QUrl::FullyEncoded)); + proxy.setUser(url.userName(QUrl::FullyEncoded)); + proxy.setPassword(url.password(QUrl::FullyEncoded)); + proxy.setPort(url.port(1080)); + + if (url.scheme().compare(QStringLiteral("socks5"), Qt::CaseInsensitive) == + 0) + { + proxy.setType(QNetworkProxy::Socks5Proxy); + } + else + { + proxy.setType(QNetworkProxy::HttpProxy); + if (!proxy.user().isEmpty() || !proxy.password().isEmpty()) + { + // for some reason, Qt doesn't set the Proxy-Authorization header + const auto auth = proxy.user() + ":" + proxy.password(); + const auto base64 = auth.toUtf8().toBase64(); + proxy.setRawHeader("Proxy-Authorization", + QByteArray("Basic ").append(base64)); + } + } + + return proxy; +} + +/** + * Attempts to apply the proxy specified by `url` as the application proxy. + */ +void applyProxy(const QString &url) +{ + auto proxyUrl = QUrl(url); + if (!proxyUrl.isValid() || proxyUrl.isEmpty()) + { + qCDebug(chatterinoNetwork) + << "Invalid or empty proxy url: " << proxyUrl; + return; + } + + const auto proxy = createProxyFromUrl(proxyUrl); + + QNetworkProxy::setApplicationProxy(proxy); + qCDebug(chatterinoNetwork) << "Set application proxy to" << proxy; +} + +} // namespace + +namespace chatterino { + +void NetworkConfigurationProvider::applyFromEnv(const Env &env) +{ + if (env.proxyUrl) + { + applyProxy(env.proxyUrl.get()); + } +} + +} // namespace chatterino diff --git a/src/providers/NetworkConfigurationProvider.hpp b/src/providers/NetworkConfigurationProvider.hpp new file mode 100644 index 000000000..6818e761c --- /dev/null +++ b/src/providers/NetworkConfigurationProvider.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include "common/QLogging.hpp" + +#include +#include + +#include + +namespace chatterino { + +class Env; + +/** This class manipulates the global network configuration (e.g. proxies). */ +class NetworkConfigurationProvider +{ +public: + /** This class should never be instantiated. */ + NetworkConfigurationProvider() = delete; + + /** + * Applies the configuration requested from the environment variables. + * + * Currently a proxy is applied if configured. + */ + static void applyFromEnv(const Env &env); + + template + static void applyToWebSocket( + const std::shared_ptr> &connection) + { + const auto applicationProxy = QNetworkProxy::applicationProxy(); + if (applicationProxy.type() != QNetworkProxy::HttpProxy) + { + return; + } + std::string url = "http://"; + url += applicationProxy.hostName().toStdString(); + url += ":"; + url += std::to_string(applicationProxy.port()); + websocketpp::lib::error_code ec; + connection->set_proxy(url, ec); + if (ec) + { + qCDebug(chatterinoNetwork) + << "Couldn't set websocket proxy:" << ec.value(); + return; + } + + connection->set_proxy_basic_auth( + applicationProxy.user().toStdString(), + applicationProxy.password().toStdString(), ec); + if (ec) + { + qCDebug(chatterinoNetwork) + << "Couldn't set websocket proxy auth:" << ec.value(); + } + } +}; + +} // namespace chatterino diff --git a/src/providers/liveupdates/BasicPubSubManager.hpp b/src/providers/liveupdates/BasicPubSubManager.hpp index f849eefda..d596866bc 100644 --- a/src/providers/liveupdates/BasicPubSubManager.hpp +++ b/src/providers/liveupdates/BasicPubSubManager.hpp @@ -4,6 +4,7 @@ #include "common/Version.hpp" #include "providers/liveupdates/BasicPubSubClient.hpp" #include "providers/liveupdates/BasicPubSubWebsocket.hpp" +#include "providers/NetworkConfigurationProvider.hpp" #include "providers/twitch/PubSubHelpers.hpp" #include "util/DebugCount.hpp" #include "util/ExponentialBackoff.hpp" @@ -336,6 +337,8 @@ private: return; } + NetworkConfigurationProvider::applyToWebSocket(con); + this->websocketClient_.connect(con); } diff --git a/src/providers/twitch/PubSubManager.cpp b/src/providers/twitch/PubSubManager.cpp index 6bfc67d10..644e494cb 100644 --- a/src/providers/twitch/PubSubManager.cpp +++ b/src/providers/twitch/PubSubManager.cpp @@ -1,6 +1,7 @@ #include "providers/twitch/PubSubManager.hpp" #include "common/QLogging.hpp" +#include "providers/NetworkConfigurationProvider.hpp" #include "providers/twitch/PubSubActions.hpp" #include "providers/twitch/PubSubClient.hpp" #include "providers/twitch/PubSubHelpers.hpp" @@ -514,6 +515,8 @@ void PubSub::addClient() return; } + NetworkConfigurationProvider::applyToWebSocket(con); + this->websocketClient.connect(con); }