WIP: proxy_support #3
3 changed files with 291 additions and 0 deletions
14
.gitignore
vendored
14
.gitignore
vendored
|
@ -30,3 +30,17 @@ libxmlplusplus-prefix/
|
||||||
spdlog.pc
|
spdlog.pc
|
||||||
build*
|
build*
|
||||||
temp*
|
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/
|
||||||
|
|
||||||
sha512sum marked this conversation as resolved
sha512sum
commented
Why .githooks ??? Why .githooks ???
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
sha512sum
commented
Code style. Use camelCase Code style. Use camelCase
|
|||||||
|
|
||||||
|
// статус ответа
|
||||||
sha512sum
commented
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;
|
||||||
sha512sum
commented
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")) {
|
||||||
sha512sum
commented
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 {
|
struct ClientCreateVisitor {
|
||||||
UserAccount account;
|
UserAccount account;
|
||||||
const Options& options;
|
const Options& options;
|
||||||
|
|
114
tests/proxy.cpp
Normal file
114
tests/proxy.cpp
Normal 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;
|
||||||
sha512sum
commented
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};
|
||||||
sha512sum
commented
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();
|
||||||
|
}
|
Loading…
Reference in a new issue
Why commented in commit ?