Compare commits
13 commits
main
...
proxy_supp
Author | SHA1 | Date | |
---|---|---|---|
7d6b783fe5 | |||
20e31808ba | |||
e1993511ac | |||
9f408fd4b8 | |||
5d64a8ca26 | |||
d4e78e63b2 | |||
8299d01fe5 | |||
665ceb9604 | |||
760bc2ab68 | |||
|
479c3b628b | ||
|
dcd0619d40 | ||
|
e6e86c12c4 | ||
|
1c8cce13ea |
3 changed files with 304 additions and 4 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -30,3 +30,8 @@ libxmlplusplus-prefix/
|
||||||
spdlog.pc
|
spdlog.pc
|
||||||
build*
|
build*
|
||||||
temp*
|
temp*
|
||||||
|
/.idea/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -157,6 +157,178 @@ 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& 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;
|
||||||
|
|
||||||
|
std::string httpConnectRequest = std::format("CONNECT {}:{} {}\r\nHost: {}:{}\r\n\r\n", host, port, kHttpVersion, host, port);
|
||||||
|
|
||||||
|
co_await boost::asio::async_write(
|
||||||
|
socket, boost::asio::buffer(httpConnectRequest), boost::asio::transfer_all(), boost::asio::use_awaitable);
|
||||||
|
|
||||||
|
boost::asio::streambuf proxyServerResponse;
|
||||||
|
std::size_t bytesTransferred =
|
||||||
|
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& 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* 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&& proxy_variant) -> boost::asio::awaitable<void> { // NOLINT
|
||||||
|
co_await ConnectViaProxy(socket, proxy_variant, 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& 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;
|
||||||
|
@ -251,9 +423,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);
|
||||||
|
|
||||||
|
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 +464,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 +487,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
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;
|
||||||
|
|
||||||
|
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};
|
||||||
|
|
||||||
|
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