Compare commits

..

No commits in common. "proxy_support" and "main" have entirely different histories.

3 changed files with 4 additions and 304 deletions

5
.gitignore vendored
View file

@ -30,8 +30,3 @@ libxmlplusplus-prefix/
spdlog.pc spdlog.pc
build* build*
temp* temp*
/.idea/

View file

@ -157,178 +157,6 @@ 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;
@ -423,18 +251,9 @@ struct ClientCreateVisitor {
boost::asio::use_awaitable); boost::asio::use_awaitable);
} }
template <typename Socket> auto Connect(auto& socket, boost::asio::ip::tcp::resolver::results_type resolveResults) -> boost::asio::awaitable<void> {
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> {
@ -464,7 +283,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->Connect(stream.next_layer(), co_await this->Resolve());
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");
@ -487,7 +306,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->Connect(socket.next_layer(), co_await this->Resolve());
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());

View file

@ -1,114 +0,0 @@
#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();
}