diff --git a/library/include/larra/client/client.hpp b/library/include/larra/client/client.hpp index 5ee658c..4949152 100644 --- a/library/include/larra/client/client.hpp +++ b/library/include/larra/client/client.hpp @@ -157,6 +157,261 @@ struct StartTlsRequest { } }; +template +auto ConnectViaProxy(Socket& socket, const HttpProxy& param_proxy, std::string_view host, std::uint16_t port) + -> boost::asio::awaitable { + constexpr char kHttpVersion[] = "HTTP/1.1"; + constexpr unsigned int kSuccessStatusCode = 200; + constexpr char kEndOfHeaders[] = "\r\n\r\n"; + + // 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 bytes_transferred = co_await boost::asio::async_read_until(socket, response, kEndOfHeaders, boost::asio::use_awaitable); + + // статус ответа + std::istream response_stream(&response); + std::string http_version; + unsigned int status_code; + std::string status_message; + + response_stream >> http_version >> status_code; + std::getline(response_stream, status_message); + + if(!response_stream || http_version.substr(0, 5) != "HTTP/") { + throw std::runtime_error("Invalid HTTP response from proxy"); + } + + if(status_code != kSuccessStatusCode) { + std::ostringstream error_stream; + error_stream << http_version << " " << status_code << " " << status_message; + throw std::runtime_error("HTTP proxy CONNECT failed: " + error_stream.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}}; + + // auto executor = co_await boost::asio::this_coro::executor; + // boost::asio::ip::tcp::resolver resolver{executor}; + // auto resolved = co_await resolver.async_resolve({std::move(socksProxy.hostname), std::to_string(socksProxy.port)}, + // boost::asio::use_awaitable); + // boost::asio::ip::tcp::socket socket{executor}; + // co_await socket.async_connect(*resolved, boost::asio::use_awaitable); + + 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 ConnectViaProxyV(Socket& socket, const Socks5Proxy& proxy, std::string_view target_hostname, std::uint16_t target_port) + -> boost::asio::awaitable { + constexpr std::uint8_t kSocks5Version = 0x05; // Version 5 + constexpr std::uint8_t kNoAuthMethod = 0x00; // No auth required + constexpr std::uint8_t kRsv = 0x00; // Reserved + constexpr std::uint8_t kConnectCommand = 0x01; // Command CONNECT + constexpr std::uint8_t kDomainNameType = 0x03; // Address type: Domain name + constexpr std::uint8_t kIpv4 = 0x01; // Address type: IPv4 + constexpr std::uint8_t kIpv6 = 0x04; // Address type: IPv6 + constexpr std::uint8_t kMaxAddressL = 255; // Max address length + constexpr std::size_t kMaxRequestSize = 257; + + if(target_hostname.size() > kMaxAddressL) { + throw std::runtime_error("Hostname too long for SOCKS5"); + } + + std::array greeting = {kSocks5Version, kConnectCommand, kRsv, kDomainNameType}; + co_await boost::asio::async_write(socket, boost::asio::buffer(greeting), boost::asio::transfer_all(), boost::asio::use_awaitable); + + std::array response{}; + co_await boost::asio::async_read(socket, boost::asio::buffer(response), boost::asio::transfer_all(), boost::asio::use_awaitable); + if(response[0] != kSocks5Version || response[1] != kNoAuthMethod) { + throw std::runtime_error("SOCKS5 proxy authentication failed"); + } + + std::array header{ + kSocks5Version, kConnectCommand, kNoAuthMethod, kDomainNameType}; // 4 байта для заголовка, до 255 байт для адреса, 2 байта для порта + auto hostnameLength = static_cast(target_hostname.size()); + + auto portBytes = std::bit_cast>(htons(target_port)); + + auto request = + std::array{std::span(header), + std::span(&hostnameLength, 1), + std::span(utils::StartLifetimeAsArray(target_hostname.data(), target_hostname.size()), + target_hostname.size()), + std::span(portBytes)} | + std::views::join; + + std::array requestBuffer{}; + + auto it = std::ranges::copy(request, requestBuffer.begin()).out; + size_t requestSize = std::distance(requestBuffer.begin(), it); + + // Отправляем запрос + co_await boost::asio::async_write( + socket, boost::asio::buffer(requestBuffer.data(), requestSize), boost::asio::transfer_all(), boost::asio::use_awaitable); + + // ответ сервера + std::array reply{}; + co_await boost::asio::async_read(socket, boost::asio::buffer(reply), boost::asio::transfer_all(), boost::asio::use_awaitable); + + if(reply[0] != kSocks5Version || reply[1] != kNoAuthMethod) { + throw std::runtime_error("SOCKS5 proxy connection failed"); + } + + const std::uint8_t addr_type = reply[3]; + size_t addr_len = 0; + if(addr_type == kIpv4) { + // IPv4 + addr_len = 4; + } else if(addr_type == kDomainNameType) { + // Domain name + std::uint8_t len{}; + co_await boost::asio::async_read(socket, boost::asio::buffer(&len, 1), boost::asio::transfer_all(), boost::asio::use_awaitable); + addr_len = len; + } else if(addr_type == kIpv6) { + // IPv6 + addr_len = 16; + } else { + throw std::runtime_error("Unknown address type in SOCKS5 reply"); + } + + std::array addr{}; // Максимальный размер для IPv6 адреса + порт + co_await boost::asio::async_read( + socket, boost::asio::buffer(addr.data(), addr_len + 2), boost::asio::transfer_all(), boost::asio::use_awaitable); + + co_return; +} + +template +auto ConnectViaProxy(Socket&, const NoProxy&, std::string_view, std::uint16_t) -> boost::asio::awaitable { + 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 { + co_await std::visit( + [&socket, host, port](const auto& proxy_variant) -> boost::asio::awaitable { + co_await ConnectToServer(socket, proxy_variant, 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; diff --git a/tests/proxy.cpp b/tests/proxy.cpp new file mode 100644 index 0000000..56f13c5 --- /dev/null +++ b/tests/proxy.cpp @@ -0,0 +1,195 @@ +#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 target_host = "target_host"; + uint16_t target_port = 80; + + std::string expected_request = + std::format("CONNECT {}:{} HTTP/1.1\r\nHost: {}:{}\r\n\r\n", target_host, target_port, target_host, target_port); + + std::string proxy_response = "HTTP/1.1 200 Connection established\r\n\r\n"; + + mock_socket.AddReceivedData(proxy_response); + + bool connect_successful = false; + + asio::co_spawn( + io_context, + [&]() -> asio::awaitable { + try { + co_await client::impl::ConnectViaProxy(mock_socket, proxy, target_host, target_port); + connect_successful = true; + } catch(...) { + connect_successful = false; + } + }, + asio::detached); + + io_context.run(); + + std::string sent_data = mock_socket.GetSentData(); + + EXPECT_EQ(sent_data, expected_request); + EXPECT_TRUE(connect_successful); +} + +// Test 2: Connect via SOCKS proxy +TEST(Socks5ProxyTest, ConnectViaProxy) { + boost::asio::io_context io; + auto executor = io.get_executor(); + + larra::xmpp::impl::MockSocket socket{executor}; + + // expected server responses + std::string server_response; + server_response += "\x05\x00"; // VER, METHOD + server_response += "\x05\x00\x00\x01"; // VER, REP, RSV, ATYP (IPv4) + server_response += "\x7F\x00\x00\x01"; // BND.ADDR (127.0.0.1) + server_response += "\x1F\x90"; // BND.PORT (8080) + + socket.AddReceivedData(server_response); + + Socks5Proxy proxy{"proxy.example.com", 1080}; + std::string target_hostname = "target.example.com"; + std::uint16_t target_port = 80; + + boost::asio::co_spawn( + executor, + [&]() -> boost::asio::awaitable { + co_await client::impl::ConnectViaProxy(socket, proxy, target_hostname, target_port); + + auto sent_data = socket.GetSentData(); + + // Expected client greeting + std::string expected_greeting = "\x05\x01\x00"; + + // Expected CONNECT request + std::array expected_request{}; + std::size_t req_len = 0; + + expected_request[req_len++] = 0x05; // VER + expected_request[req_len++] = 0x01; // CMD: CONNECT + expected_request[req_len++] = 0x00; // RSV + expected_request[req_len++] = 0x03; // ATYP: DOMAINNAME + + expected_request[req_len++] = static_cast(target_hostname.size()); // domain length + + std::memcpy(&expected_request[req_len], target_hostname.data(), target_hostname.size()); + req_len += target_hostname.size(); + + std::uint16_t network_order_port = htons(target_port); + expected_request[req_len++] = static_cast((network_order_port >> 8) & 0xFF); + expected_request[req_len++] = static_cast(network_order_port & 0xFF); + + std::string expected_data = expected_greeting; + expected_data.append(reinterpret_cast(expected_request.data()), req_len); + + EXPECT_EQ(sent_data, expected_data); + + co_return; + }, + boost::asio::detached); + + io.run(); +} + +/* +// Test 3: Connect via system configured proxy +TEST_F(ProxyTest, ConnectViaSystemProxy_HttpProxy) { + // Set the environment variable for the system proxy + const char* original_http_proxy = std::getenv("http_proxy"); + setenv("http_proxy", "http://proxy_host:8080", 1); + + std::string target_host = "target_host"; + uint16_t target_port = 80; + + std::string expected_request = std::format( + "CONNECT {}:{} HTTP/1.1\r\nHost: {}:{}\r\n\r\n", + target_host, target_port, target_host, target_port); + + std::string proxy_response = "HTTP/1.1 200 Connection established\r\n\r\n"; + + mock_socket.AddReceivedData(proxy_response); + + bool connect_successful = false; + + asio::co_spawn(io_context, [&]() -> asio::awaitable { + try { + SystemConfiguredProxy proxy; + co_await client::impl::ConnectToServer(mock_socket, proxy, target_host, target_port); + connect_successful = true; + } catch (...) { + connect_successful = false; + } + }, asio::detached); + + io_context.run(); + + std::string sent_data = mock_socket.GetSentData(); + + EXPECT_EQ(sent_data, expected_request); + EXPECT_TRUE(connect_successful); + + // Restore the original environment variable + if (original_http_proxy) { + setenv("http_proxy", original_http_proxy, 1); + } else { + unsetenv("http_proxy"); + } +} + +// Test 4: Incorrect proxy data +TEST_F(ProxyTest, ConnectViaHttpProxy_IncorrectData) { + HttpProxy proxy{"proxy_host", 8080}; + + std::string target_host = "target_host"; + uint16_t target_port = 80; + + std::string expected_request = std::format( + "CONNECT {}:{} HTTP/1.1\r\nHost: {}:{}\r\n\r\n", + target_host, target_port, target_host, target_port); + + // Simulate an incorrect response from the proxy server + std::string proxy_response = "HTTP/1.1 400 Bad Request\r\n\r\n"; + + mock_socket.AddReceivedData(proxy_response); + + bool connect_successful = false; + std::string error_message; + + asio::co_spawn(io_context, [&]() -> asio::awaitable { + try { + co_await client::impl::ConnectToServer(mock_socket, proxy, target_host, target_port); + connect_successful = true; + } catch (const std::runtime_error& e) { + connect_successful = false; + error_message = e.what(); + } + }, asio::detached); + + io_context.run(); + + std::string sent_data = mock_socket.GetSentData(); + + EXPECT_EQ(sent_data, expected_request); + EXPECT_FALSE(connect_successful); + EXPECT_FALSE(error_message.empty()); +} +*/