WIP: proxy_support #3
2 changed files with 450 additions and 0 deletions
|
@ -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);
|
||||
|
||||
// статус ответа
|
||||
|
||||
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{};
|
||||
sha512sum
commented
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
195
tests/proxy.cpp
Normal 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());
|
||||
}
|
||||
*/
|
Loading…
Reference in a new issue
Meaningless comments, variable names reflect what they contain. No need to duplicate. And use English for comments in code