WIP: proxy_support #3

Draft
sectapunterx wants to merge 6 commits from proxy_support into main
2 changed files with 450 additions and 0 deletions
Showing only changes of commit 1c8cce13ea - Show all commits

View file

@ -157,6 +157,261 @@ 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 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);
// статус ответа
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 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 <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}};
// 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<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 Socket>
auto ConnectViaProxyV(Socket& socket, const Socks5Proxy& proxy, std::string_view target_hostname, std::uint16_t target_port)
-> boost::asio::awaitable<void> {
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<std::uint8_t, 2> response{};
Review

camelCase

camelCase
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<std::uint8_t>(target_hostname.size());
auto portBytes = std::bit_cast<std::array<std::uint8_t, 2>>(htons(target_port));
auto request =
std::array{std::span<const std::uint8_t>(header),
std::span<const std::uint8_t>(&hostnameLength, 1),
std::span<const std::uint8_t>(utils::StartLifetimeAsArray<uint8_t>(target_hostname.data(), target_hostname.size()),
target_hostname.size()),
std::span<const std::uint8_t>(portBytes)} |
std::views::join;
std::array<std::uint8_t, kMaxRequestSize> 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<std::uint8_t, 4> 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<std::uint8_t, 18> 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 <typename Socket>
auto ConnectViaProxy(Socket&, const NoProxy&, std::string_view, std::uint16_t) -> boost::asio::awaitable<void> {
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")) {
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> {
co_await std::visit(
[&socket, host, port](const auto& proxy_variant) -> boost::asio::awaitable<void> {
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;

195
tests/proxy.cpp Normal file
View file

@ -0,0 +1,195 @@
#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 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<void> {
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<void> {
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<std::uint8_t, 262> 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<std::uint8_t>(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<std::uint8_t>((network_order_port >> 8) & 0xFF);
expected_request[req_len++] = static_cast<std::uint8_t>(network_order_port & 0xFF);
std::string expected_data = expected_greeting;
expected_data.append(reinterpret_cast<const char*>(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<void> {
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<void> {
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());
}
*/