diff --git a/.gitignore b/.gitignore index d45daeb..6c2ba0b 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,17 @@ libxmlplusplus-prefix/ spdlog.pc build* temp* +#/.idea/codeStyles/codeStyleConfig.xml +#/.idea/discord.xml +#/.idea/editor.xml +#/.idea/larra.iml +#/.idea/material_theme_project_new.xml +#/.idea/misc.xml +#/.idea/modules.xml +#/.idea/codeStyles/Project.xml +#/.idea/vcs.xml +/.idea/ + + + + diff --git a/library/include/larra/client/client.hpp b/library/include/larra/client/client.hpp index 5ee658c..cf65660 100644 --- a/library/include/larra/client/client.hpp +++ b/library/include/larra/client/client.hpp @@ -157,6 +157,169 @@ struct StartTlsRequest { } }; +template +auto ConnectViaProxy(Socket& socket, const HttpProxy& param_proxy, 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; + // HTTP CONNECT запрос + std::string request = std::format("CONNECT {}:{} {}\r\nHost: {}:{}\r\n\r\n", host, port, kHttpVersion, host, port); + + co_await boost::asio::async_write(socket, boost::asio::buffer(request), boost::asio::transfer_all(), boost::asio::use_awaitable); + + // ответ от прокси-сервера + boost::asio::streambuf response; + std::size_t bytesTransferred = co_await boost::asio::async_read_until(socket, response, kEndOfHeaders, boost::asio::use_awaitable); + + // статус ответа + std::istream responseStream(&response); + 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, Socks5Proxy& socksProxy, 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* proxy_env = std::getenv("socks_proxy")) { + constexpr std::uint16_t kSocksPort = 1080; + return GetProxySettings(proxy_env, kSocksPort); + } + + return NoProxy{}; +} + +template +auto ConnectViaProxy(Socket& socket, const SystemConfiguredProxy&, std::string_view host, std::uint16_t port) + -> boost::asio::awaitable { + auto proxy_opt = GetSystemProxySettings(); + + co_await std::visit( + [&](auto&& proxy_variant) -> boost::asio::awaitable { + co_await ConnectViaProxy(socket, proxy_variant, host, port); + }, + proxy_opt); +} + +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); + 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); + + if constexpr(!std::same_as) { + 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 { + auto awaitable = std::visit( + [&socket, host, port](const auto& proxy_variant) { + return ConnectToServer(socket, proxy_variant, host, port); + }, + proxy); + co_await awaitable; +} + +inline auto GetAuthData(const PlainUserAccount& account) -> std::string { + return EncodeBase64('\0' + account.jid.username + '\0' + account.password); +} + struct ClientCreateVisitor { UserAccount account; const Options& options; diff --git a/tests/proxy.cpp b/tests/proxy.cpp new file mode 100644 index 0000000..13be5f3 --- /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_context; + larra::xmpp::impl::MockSocket mock_socket{io_context.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"; + + mock_socket.AddReceivedData(proxyResponse); + + bool connectSuccessful = false; + + asio::co_spawn( + io_context, + [&]() -> asio::awaitable { + try { + co_await client::impl::ConnectViaProxy(mock_socket, proxy, targetHost, targetPort); + connectSuccessful = true; + } catch(...) { + connectSuccessful = false; + } + }, + asio::detached); + + io_context.run(); + + std::string sentData = mock_socket.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 req_len = 0; + + expectedRequest[req_len++] = 0x05; // VER + expectedRequest[req_len++] = 0x01; // CMD: CONNECT + expectedRequest[req_len++] = 0x00; // RSV + expectedRequest[req_len++] = 0x03; // ATYP: DOMAINNAME + + expectedRequest[req_len++] = static_cast(targetHostname.size()); // domain length + + std::memcpy(&expectedRequest[req_len], targetHostname.data(), targetHostname.size()); + req_len += targetHostname.size(); + + std::uint16_t networkOrderPort = htons(targetPort); + expectedRequest[req_len++] = static_cast((networkOrderPort >> 8) & 0xFF); + expectedRequest[req_len++] = static_cast(networkOrderPort & 0xFF); + + std::string expectedData = expectedGreeting; + auto transformed_view = expectedRequest | std::views::take(req_len) | std::views::transform([](std::uint8_t byte) { + return static_cast(byte); + }); + + expectedData.append(std::ranges::to(transformed_view)); + EXPECT_EQ(sentData, expectedData); + + co_return; + }, + boost::asio::detached); + + io.run(); +} \ No newline at end of file