From 46b17dd5f53e681fe3067cfd6e3d665f31ce2e56 Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 12 Nov 2024 01:55:26 +0300 Subject: [PATCH] Successful Connect() with proxy --- .gitignore | 5 + library/include/larra/client/client.hpp | 185 +++++++++++++++++++++++- tests/proxy.cpp | 114 +++++++++++++++ 3 files changed, 300 insertions(+), 4 deletions(-) create mode 100644 tests/proxy.cpp diff --git a/.gitignore b/.gitignore index d45daeb..3f7db93 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,8 @@ libxmlplusplus-prefix/ spdlog.pc build* temp* +/.idea/ + + + + diff --git a/library/include/larra/client/client.hpp b/library/include/larra/client/client.hpp index 5ee658c..1b415da 100644 --- a/library/include/larra/client/client.hpp +++ b/library/include/larra/client/client.hpp @@ -157,6 +157,174 @@ struct StartTlsRequest { } }; +template +auto ConnectViaProxy(Socket& socket, const NoProxy&, std::string_view host, std::uint16_t port) -> boost::asio::awaitable { + auto executor = co_await boost::asio::this_coro::executor; + boost::asio::ip::tcp::resolver resolver(executor); + auto endpoints = co_await resolver.async_resolve(host, std::to_string(port), boost::asio::use_awaitable); + co_await boost::asio::async_connect(socket, endpoints, boost::asio::use_awaitable); + co_return; +} + +template +auto ConnectViaProxy(Socket& socket, const HttpProxy&, std::string_view host, std::uint16_t port) -> boost::asio::awaitable { + constexpr std::string_view kHttpVersion = "HTTP/1.1"; + constexpr unsigned int kSuccessStatusCode = 200; + constexpr std::string_view kEndOfHeaders = "\r\n\r\n"; + constexpr int kEndOfHttpSubstring = 5; + + std::string httpConnectRequest = std::format("CONNECT {}:{} {}\r\nHost: {}:{}\r\n\r\n", host, port, kHttpVersion, host, port); + + co_await boost::asio::async_write( + socket, boost::asio::buffer(httpConnectRequest), boost::asio::transfer_all(), boost::asio::use_awaitable); + + boost::asio::streambuf proxyServerResponse; + co_await boost::asio::async_read_until(socket, proxyServerResponse, kEndOfHeaders, boost::asio::use_awaitable); + + std::istream responseStream(&proxyServerResponse); + std::string httpVersion; + unsigned int statusCode = 0; + std::string statusMessage; + + responseStream >> httpVersion >> statusCode; + std::getline(responseStream, statusMessage); + + if(!responseStream || httpVersion.substr(0, kEndOfHttpSubstring) != "HTTP/") { + throw std::runtime_error("Invalid HTTP response from proxy"); + } + + if(statusCode != kSuccessStatusCode) { + std::ostringstream errorStream; + errorStream << httpVersion << " " << statusCode << " " << statusMessage; + throw std::runtime_error("HTTP proxy CONNECT failed: " + errorStream.str()); + } + + co_return; +} + +template +auto ConnectViaProxy(Socket& socket, const Socks5Proxy&, std::string_view address, std::uint16_t port) -> boost::asio::awaitable { + constexpr std::array kSocks5RequestStart = {std::byte{0x05}, std::byte{0x01}, std::byte{0x00}, std::byte{0x03}}; + + constexpr std::size_t kSocks5RequestMaxSize = 257; + + constexpr std::size_t kSocks5ReplyTypeSize = 10; + + constexpr std::array kHandshakeRequest{std::byte{0x05}, std::byte{0x01}, std::byte{0x00}}; + + std::array handshakeResponse; // NOLINT + + co_await boost::asio::async_write( + socket, boost::asio::buffer(kHandshakeRequest), boost::asio::transfer_all(), boost::asio::use_awaitable); + co_await boost::asio::async_read(socket, boost::asio::buffer(handshakeResponse), boost::asio::transfer_all(), boost::asio::use_awaitable); + if(handshakeResponse[0] != std::byte{0x05} || handshakeResponse[1] != std::byte{0x00}) { // NOLINT + throw std::exception{}; + }; + const auto [connectRequest, size] = [&] { + const auto size = static_cast(address.size()); + const auto htonsPort = std::bit_cast>(htons(port)); + auto range = std::array{std::span{kSocks5RequestStart.begin(), kSocks5RequestStart.size()}, + std::span{&size, 1}, + std::span{utils::StartLifetimeAsArray(address.data(), address.size()), address.size()}, + std::span{htonsPort.data(), 2}} | + std::views::join; + std::array response; // NOLINT + auto sizee = std::ranges::copy(range, response.begin()).out - response.begin(); + return std::pair{response, sizee}; + }(); + co_await boost::asio::async_write( + socket, boost::asio::buffer(connectRequest.begin(), size), boost::asio::transfer_all(), boost::asio::use_awaitable); + std::array connectReplyType; // NOLINT + co_await boost::asio::async_read(socket, boost::asio::buffer(connectReplyType), boost::asio::transfer_all(), boost::asio::use_awaitable); + if(connectReplyType[1] != std::byte{0x00}) { + throw std::exception{}; + }; + co_return; +} + +template +auto GetProxySettings(const char* proxyEnv, std::uint16_t port) -> Proxy { + std::string_view rawProxyStr = proxyEnv; + + const std::size_t protocolNamePos = rawProxyStr.find("://"); + std::string_view proxyStr = protocolNamePos == std::string_view::npos ? rawProxyStr : rawProxyStr.substr(protocolNamePos + 3); + + const std::size_t portPos = proxyStr.find(':'); + + if(portPos == std::string_view::npos) { + return ProxyType{std::string(proxyStr), port}; + } + auto host = std::string(proxyStr.substr(0, portPos)); + auto portStr = proxyStr.substr(portPos + 1); + auto portOpt = ToInt(portStr); + + if(!portOpt) { + throw std::runtime_error("Invalid port number in proxy settings"); + } + return ProxyType{host, portOpt.value()}; +} + +inline auto GetSystemProxySettings() -> Proxy { + if(const char* proxyEnv = std::getenv("http_proxy")) { + constexpr std::uint16_t kHttpPort = 8080; + return GetProxySettings(proxyEnv, kHttpPort); + } + if(const char* proxyEnv = std::getenv("socks_proxy")) { + constexpr std::uint16_t kSocksPort = 1080; + return GetProxySettings(proxyEnv, kSocksPort); + } + + return NoProxy{}; +} + +template +auto ConnectViaProxy(Socket& socket, const SystemConfiguredProxy&, std::string_view host, std::uint16_t port) + -> boost::asio::awaitable { + auto proxyOpt = GetSystemProxySettings(); + + co_await std::visit( + [&](auto&& proxyVariant) -> boost::asio::awaitable { // NOLINT + co_await ConnectViaProxy(socket, proxyVariant, host, port); // GCC error if co_return + }, + proxyOpt); +} + +template +auto ConnectToServer(Socket& socket, const ProxyType& proxy, std::string_view host, std::uint16_t port) -> boost::asio::awaitable { + auto executor = co_await boost::asio::this_coro::executor; + boost::asio::ip::tcp::resolver resolver(executor); + + if constexpr(!std::same_as) { + auto endpoints = co_await resolver.async_resolve(proxy.hostname, std::to_string(proxy.port), boost::asio::use_awaitable); + co_await boost::asio::async_connect(socket, endpoints, boost::asio::use_awaitable); + co_await ConnectViaProxy(socket, proxy, host, port); + } +} + +template +auto ConnectToServer(Socket& socket, const SystemConfiguredProxy& proxy, std::string_view host, std::uint16_t port) + -> boost::asio::awaitable { + auto executor = co_await boost::asio::this_coro::executor; + boost::asio::ip::tcp::resolver resolver(executor); + auto endpoints = co_await resolver.async_resolve(host, std::to_string(port), boost::asio::use_awaitable); + co_await boost::asio::async_connect(socket, endpoints, boost::asio::use_awaitable); + + co_await ConnectViaProxy(socket, proxy, host, port); +} + +template +auto ConnectWithProxy(Socket& socket, const Proxy& proxy, std::string_view host, std::uint16_t port) -> boost::asio::awaitable { + co_await std::visit( + [&socket, host, port](const auto& proxyVariant) { + return ConnectToServer(socket, proxyVariant, host, port); + }, + proxy); +} + +inline auto GetAuthData(const PlainUserAccount& account) -> std::string { + return EncodeBase64('\0' + account.jid.username + '\0' + account.password); +} + struct ClientCreateVisitor { UserAccount account; const Options& options; @@ -251,8 +419,17 @@ struct ClientCreateVisitor { boost::asio::use_awaitable); } - auto Connect(auto& socket, boost::asio::ip::tcp::resolver::results_type resolveResults) -> boost::asio::awaitable { - co_await boost::asio::async_connect(socket, resolveResults, boost::asio::use_awaitable); + template + auto Connect(Socket& socket) -> boost::asio::awaitable { + if(!std::holds_alternative(this->options.proxy)) { + auto host = this->options.hostname.value_or(account.Jid().server); + auto port = this->options.port.value_or(kDefaultXmppPort); + + co_await ConnectWithProxy(socket, this->options.proxy, host, port); + } else { + auto resolveResults = co_await this->Resolve(); + co_await boost::asio::async_connect(socket, resolveResults, boost::asio::use_awaitable); + } } template @@ -283,7 +460,7 @@ struct ClientCreateVisitor { template inline auto operator()(XmlStream stream) -> boost::asio::awaitable, Client>>> { - co_await this->Connect(stream.next_layer(), co_await this->Resolve()); + co_await this->Connect(stream.next_layer()); co_await stream.Send(UserStream{.from = account.Jid(), .to = account.Jid().server, .version = "1.0", .xmlLang = "en"}); SPDLOG_DEBUG("UserStream sended"); @@ -306,7 +483,7 @@ struct ClientCreateVisitor { inline auto operator()(XmlStream> stream) -> boost::asio::awaitable, Client>>> { auto& socket = stream.next_layer(); - co_await this->Connect(socket.next_layer(), co_await this->Resolve()); + co_await this->Connect(socket.next_layer()); co_await stream.Send( UserStream{.from = account.Jid().Username("anonymous"), .to = account.Jid().server, .version = "1.0", .xmlLang = "en"}, socket.next_layer()); diff --git a/tests/proxy.cpp b/tests/proxy.cpp new file mode 100644 index 0000000..cc1a053 --- /dev/null +++ b/tests/proxy.cpp @@ -0,0 +1,114 @@ +#include + +#include +#include +#include + +using namespace larra::xmpp; +using boost::asio::ip::tcp; +namespace asio = boost::asio; + +class ProxyTest : public ::testing::Test { + protected: + boost::asio::io_context io; + larra::xmpp::impl::MockSocket mockSocket{io.get_executor()}; +}; + +// Test 1: Connect via HTTP proxy with successful server response +TEST_F(ProxyTest, ConnectViaHttpProxy_SuccessfulResponse) { + HttpProxy proxy{"proxy_host", 8080}; + + std::string targetHost = "target_host"; + uint16_t targetPort = 80; + + std::string expectedRequest = + std::format("CONNECT {}:{} HTTP/1.1\r\nHost: {}:{}\r\n\r\n", targetHost, targetPort, targetHost, targetPort); + + std::string proxyResponse = "HTTP/1.1 200 Connection established\r\n\r\n"; + + mockSocket.AddReceivedData(proxyResponse); + + bool connectSuccessful = false; + + asio::co_spawn( + io, + [&]() -> asio::awaitable { + try { + co_await client::impl::ConnectViaProxy(mockSocket, proxy, targetHost, targetPort); + connectSuccessful = true; + } catch(...) { + connectSuccessful = false; + } + }, + asio::detached); + + io.run(); + + std::string sentData = mockSocket.GetSentData(); + + EXPECT_EQ(sentData, expectedRequest); + EXPECT_TRUE(connectSuccessful); +} + +// Test 2: Connect via SOCKS proxy +TEST(Socks5ProxyTest, ConnectViaProxy) { + constexpr std::uint16_t kSocksPort = 1080; + constexpr std::uint16_t kAvailableUdpBufferSpaceForSocks = 262; + + boost::asio::io_context io; + auto executor = io.get_executor(); + + larra::xmpp::impl::MockSocket socket{executor}; + + std::string expectedServerResponse; + expectedServerResponse += "\x05\x00"; // VER, METHOD + expectedServerResponse += "\x05\x00\x00\x01"; // VER, REP, RSV, ATYP (IPv4) + expectedServerResponse += "\x7F\x00\x00\x01"; // BND.ADDR (127.0.0.1) + expectedServerResponse += "\x1F\x90"; // BND.PORT (8080) + + socket.AddReceivedData(expectedServerResponse); + + Socks5Proxy proxy{.hostname = "proxy.example.com", .port = kSocksPort}; + std::string targetHostname = "target.example.com"; + std::uint16_t targetPort = 80; + + boost::asio::co_spawn( + executor, + [&]() -> boost::asio::awaitable { + co_await client::impl::ConnectViaProxy(socket, proxy, targetHostname, targetPort); + + auto sentData = socket.GetSentData(); + + std::string expectedGreeting = "\x05\x01\x00"; + + std::array expectedRequest{}; + std::size_t reqLen = 0; + + expectedRequest[reqLen++] = 0x05; // VER + expectedRequest[reqLen++] = 0x01; // CMD: CONNECT + expectedRequest[reqLen++] = 0x00; // RSV + expectedRequest[reqLen++] = 0x03; // ATYP: DOMAINNAME + + expectedRequest[reqLen++] = static_cast(targetHostname.size()); // domain length + + std::memcpy(&expectedRequest[reqLen], targetHostname.data(), targetHostname.size()); + reqLen += targetHostname.size(); + + std::uint16_t networkOrderPort = htons(targetPort); + expectedRequest[reqLen++] = static_cast((networkOrderPort >> 8) & 0xFF); + expectedRequest[reqLen++] = static_cast(networkOrderPort & 0xFF); + + std::string expectedData = expectedGreeting; + auto transformedView = expectedRequest | std::views::take(reqLen) | std::views::transform([](std::uint8_t byte) { + return static_cast(byte); + }); + + expectedData.append(std::ranges::to(transformedView)); + EXPECT_EQ(sentData, expectedData); + + co_return; + }, + boost::asio::detached); + + io.run(); +} \ No newline at end of file