proxy_support #3

Merged
sha512sum merged 1 commit from proxy_support into main 2024-11-26 16:06:03 +00:00
3 changed files with 300 additions and 4 deletions
Showing only changes of commit 46b17dd5f5 - Show all commits

5
.gitignore vendored
View file

@ -30,3 +30,8 @@ libxmlplusplus-prefix/
spdlog.pc spdlog.pc
build* build*
temp* temp*
/.idea/
sha512sum marked this conversation as resolved Outdated

Why commented in commit ?

Why commented in commit ?

View file

@ -157,6 +157,174 @@ struct StartTlsRequest {
} }
}; };
template <typename Socket>
auto ConnectViaProxy(Socket& socket, const NoProxy&, 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_return;
}
template <typename Socket>
auto ConnectViaProxy(Socket& socket, const HttpProxy&, 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;
sha512sum marked this conversation as resolved Outdated

Code style. Use camelCase

Code style. Use camelCase
std::string httpConnectRequest = std::format("CONNECT {}:{} {}\r\nHost: {}:{}\r\n\r\n", host, port, kHttpVersion, host, port);
sha512sum marked this conversation as resolved Outdated

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
co_await boost::asio::async_write(
sha512sum marked this conversation as resolved Outdated

camelCase

camelCase
socket, boost::asio::buffer(httpConnectRequest), boost::asio::transfer_all(), boost::asio::use_awaitable);
boost::asio::streambuf proxyServerResponse;
co_await boost::asio::async_read_until(socket, proxyServerResponse, kEndOfHeaders, boost::asio::use_awaitable);
std::istream responseStream(&proxyServerResponse);
std::string httpVersion;
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, const Socks5Proxy&, 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()};
}
sha512sum marked this conversation as resolved Outdated

camelCase

camelCase
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* proxyEnv = std::getenv("socks_proxy")) {
constexpr std::uint16_t kSocksPort = 1080;
return GetProxySettings<Socks5Proxy>(proxyEnv, 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 proxyOpt = GetSystemProxySettings();
co_await std::visit(
[&](auto&& proxyVariant) -> boost::asio::awaitable<void> { // NOLINT
co_await ConnectViaProxy(socket, proxyVariant, host, port); // GCC error if co_return
},
proxyOpt);
}
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);
if constexpr(!std::same_as<ProxyType, NoProxy>) {
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);
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& proxyVariant) {
return ConnectToServer(socket, proxyVariant, host, port);
},
proxy);
}
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;
@ -251,9 +419,18 @@ struct ClientCreateVisitor {
boost::asio::use_awaitable); boost::asio::use_awaitable);
} }
auto Connect(auto& socket, boost::asio::ip::tcp::resolver::results_type resolveResults) -> boost::asio::awaitable<void> { template <typename Socket>
auto Connect(Socket& socket) -> boost::asio::awaitable<void> {
if(!std::holds_alternative<NoProxy>(this->options.proxy)) {
auto host = this->options.hostname.value_or(account.Jid().server);
auto port = this->options.port.value_or(kDefaultXmppPort);

Remove old code

Remove old code
co_await ConnectWithProxy(socket, this->options.proxy, host, port);
} else {
auto resolveResults = co_await this->Resolve();
co_await boost::asio::async_connect(socket, resolveResults, boost::asio::use_awaitable); co_await boost::asio::async_connect(socket, resolveResults, boost::asio::use_awaitable);
} }
}
template <typename Socket> template <typename Socket>
auto ProcessTls(XmlStream<boost::asio::ssl::stream<Socket>>& stream) -> boost::asio::awaitable<void> { auto ProcessTls(XmlStream<boost::asio::ssl::stream<Socket>>& stream) -> boost::asio::awaitable<void> {
@ -283,7 +460,7 @@ struct ClientCreateVisitor {
template <typename Socket> template <typename Socket>
inline auto operator()(XmlStream<Socket> stream) inline auto operator()(XmlStream<Socket> stream)
-> boost::asio::awaitable<std::variant<Client<Socket>, Client<boost::asio::ssl::stream<Socket>>>> { -> boost::asio::awaitable<std::variant<Client<Socket>, Client<boost::asio::ssl::stream<Socket>>>> {
co_await this->Connect(stream.next_layer(), co_await this->Resolve()); co_await this->Connect(stream.next_layer());
co_await stream.Send(UserStream{.from = account.Jid(), .to = account.Jid().server, .version = "1.0", .xmlLang = "en"}); co_await stream.Send(UserStream{.from = account.Jid(), .to = account.Jid().server, .version = "1.0", .xmlLang = "en"});
SPDLOG_DEBUG("UserStream sended"); SPDLOG_DEBUG("UserStream sended");
@ -306,7 +483,7 @@ struct ClientCreateVisitor {
inline auto operator()(XmlStream<boost::asio::ssl::stream<Socket>> stream) inline auto operator()(XmlStream<boost::asio::ssl::stream<Socket>> stream)
-> boost::asio::awaitable<std::variant<Client<Socket>, Client<boost::asio::ssl::stream<Socket>>>> { -> boost::asio::awaitable<std::variant<Client<Socket>, Client<boost::asio::ssl::stream<Socket>>>> {
auto& socket = stream.next_layer(); auto& socket = stream.next_layer();
co_await this->Connect(socket.next_layer(), co_await this->Resolve()); co_await this->Connect(socket.next_layer());
co_await stream.Send( co_await stream.Send(
UserStream{.from = account.Jid().Username("anonymous"), .to = account.Jid().server, .version = "1.0", .xmlLang = "en"}, UserStream{.from = account.Jid().Username("anonymous"), .to = account.Jid().server, .version = "1.0", .xmlLang = "en"},
socket.next_layer()); socket.next_layer());

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;
larra::xmpp::impl::MockSocket mockSocket{io.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";
mockSocket.AddReceivedData(proxyResponse);
bool connectSuccessful = false;
sha512sum marked this conversation as resolved Outdated

camelCase

camelCase
asio::co_spawn(
io,
[&]() -> asio::awaitable<void> {
try {
co_await client::impl::ConnectViaProxy(mockSocket, proxy, targetHost, targetPort);
connectSuccessful = true;
} catch(...) {
connectSuccessful = false;
}
},
asio::detached);
io.run();
std::string sentData = mockSocket.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 marked this conversation as resolved Outdated

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 reqLen = 0;

camelCase

camelCase
expectedRequest[reqLen++] = 0x05; // VER
expectedRequest[reqLen++] = 0x01; // CMD: CONNECT
expectedRequest[reqLen++] = 0x00; // RSV
expectedRequest[reqLen++] = 0x03; // ATYP: DOMAINNAME
expectedRequest[reqLen++] = static_cast<std::uint8_t>(targetHostname.size()); // domain length
std::memcpy(&expectedRequest[reqLen], targetHostname.data(), targetHostname.size());
reqLen += targetHostname.size();
std::uint16_t networkOrderPort = htons(targetPort);
expectedRequest[reqLen++] = static_cast<std::uint8_t>((networkOrderPort >> 8) & 0xFF);
expectedRequest[reqLen++] = static_cast<std::uint8_t>(networkOrderPort & 0xFF);
std::string expectedData = expectedGreeting;
auto transformedView = expectedRequest | std::views::take(reqLen) | std::views::transform([](std::uint8_t byte) {
return static_cast<char>(byte);
});
expectedData.append(std::ranges::to<std::string>(transformedView));
EXPECT_EQ(sentData, expectedData);
co_return;
},
boost::asio::detached);
io.run();
}