WIP: proxy_support #3

Draft
sectapunterx wants to merge 6 commits from proxy_support into main
3 changed files with 291 additions and 0 deletions

14
.gitignore vendored
View file

@ -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
Review

Why commented in commit ?

Why commented in commit ?
#/.idea/misc.xml
#/.idea/modules.xml
#/.idea/codeStyles/Project.xml
#/.idea/vcs.xml
/.idea/
sha512sum marked this conversation as resolved
Review

Why .githooks ???

Why .githooks ???

View file

@ -157,6 +157,169 @@ struct StartTlsRequest {
}
};
template <typename Socket>
auto ConnectViaProxy(Socket& socket, const HttpProxy& param_proxy, std::string_view host, std::uint16_t port)
-> boost::asio::awaitable<void> {
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);

Code style. Use camelCase

Code style. Use camelCase
// статус ответа
Review

Meaningless comments, variable names reflect what they contain. No need to duplicate. And use English for comments in code

Meaningless comments, variable names reflect what they contain. No need to duplicate. And use English for comments in code
std::istream responseStream(&response);
std::string httpVersion;

camelCase

camelCase
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 <typename Socket>
auto ConnectViaProxy(Socket& socket, Socks5Proxy& socksProxy, std::string_view address, std::uint16_t port)
-> boost::asio::awaitable<void> {
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<std::byte, 2> 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<std::byte>(address.size());
const auto htonsPort = std::bit_cast<std::array<std::byte, 2>>(htons(port));
auto range = std::array{std::span{kSocks5RequestStart.begin(), kSocks5RequestStart.size()},
std::span{&size, 1},
std::span{utils::StartLifetimeAsArray<const std::byte>(address.data(), address.size()), address.size()},
std::span{htonsPort.data(), 2}} |
std::views::join;
std::array<std::byte, kSocks5RequestMaxSize> 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<std::byte, kSocks5ReplyTypeSize> 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 <typename ProxyType>
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<uint16_t>(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<HttpProxy>(proxyEnv, kHttpPort);
}
if(const char* proxy_env = std::getenv("socks_proxy")) {
Review

camelCase

camelCase
constexpr std::uint16_t kSocksPort = 1080;
return GetProxySettings<Socks5Proxy>(proxy_env, kSocksPort);
}
return NoProxy{};
}
template <typename Socket>
auto ConnectViaProxy(Socket& socket, const SystemConfiguredProxy&, std::string_view host, std::uint16_t port)
-> boost::asio::awaitable<void> {
auto proxy_opt = GetSystemProxySettings();
co_await std::visit(
[&](auto&& proxy_variant) -> boost::asio::awaitable<void> {
co_await ConnectViaProxy(socket, proxy_variant, host, port);
},
proxy_opt);
}
template <typename Socket, typename ProxyType>
auto ConnectToServer(Socket& socket, const ProxyType& proxy, std::string_view host, std::uint16_t port) -> boost::asio::awaitable<void> {
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<ProxyType, NoProxy>) {
co_await ConnectViaProxy(socket, proxy, host, port);
}
}
template <typename Socket>
auto ConnectToServer(Socket& socket, const SystemConfiguredProxy& proxy, std::string_view host, std::uint16_t port)
-> boost::asio::awaitable<void> {
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 <typename Socket>
auto ConnectWithProxy(Socket& socket, const Proxy& proxy, std::string_view host, std::uint16_t port) -> boost::asio::awaitable<void> {
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;

114
tests/proxy.cpp Normal file
View file

@ -0,0 +1,114 @@
#include <gtest/gtest.h>
#include <larra/client/client.hpp>
#include <larra/impl/mock_socket.hpp>
#include <larra/proxy.hpp>
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;

camelCase

camelCase
asio::co_spawn(
io_context,
[&]() -> asio::awaitable<void> {
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};

camelCase

camelCase
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<void> {
co_await client::impl::ConnectViaProxy(socket, proxy, targetHostname, targetPort);
auto sentData = socket.GetSentData();
std::string expectedGreeting = "\x05\x01\x00";
std::array<std::uint8_t, kAvailableUdpBufferSpaceForSocks> 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<std::uint8_t>(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<std::uint8_t>((networkOrderPort >> 8) & 0xFF);
expectedRequest[req_len++] = static_cast<std::uint8_t>(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<char>(byte);
});
expectedData.append(std::ranges::to<std::string>(transformed_view));
EXPECT_EQ(sentData, expectedData);
co_return;
},
boost::asio::detached);
io.run();
}