Compare commits
4 commits
6064be2b71
...
9d95d08ab7
Author | SHA1 | Date | |
---|---|---|---|
9d95d08ab7 | |||
fc5e4f19a8 | |||
7f5c9cfd49 | |||
![]() |
46b17dd5f5 |
11 changed files with 697 additions and 36 deletions
|
@ -13,9 +13,9 @@ jobs:
|
|||
pacman -S --noconfirm cmake make gcc ninja meson
|
||||
pacman -S --noconfirm gtk4 gtkmm-4.0 boost spdlog fmt libxml++-5.0 gtest
|
||||
|
||||
- name: Setup environment - Install LLVM-19.1.3
|
||||
- name: Setup environment - Install LLVM-19.1.5
|
||||
run: |
|
||||
export LLVM_VER=19.1.3
|
||||
export LLVM_VER=19.1.5
|
||||
|
||||
echo "::group::Download LLVM-${LLVM_VER}"
|
||||
wget https://github.com/llvm/llvm-project/releases/download/llvmorg-${LLVM_VER}/LLVM-${LLVM_VER}-Linux-X64.tar.xz -O /LLVM-${LLVM_VER}-Linux-X64.tar.xz
|
||||
|
@ -108,7 +108,7 @@ jobs:
|
|||
id: clang_format_check
|
||||
continue-on-error: true
|
||||
run: |
|
||||
export LLVM_VER=19.1.3
|
||||
export LLVM_VER=19.1.5
|
||||
export PATH="/home/LLVM-${LLVM_VER}/bin:${PATH}"
|
||||
cd ${{ github.workspace }}
|
||||
export REPO_FILES=$(cat repo_files_to_check.txt)
|
||||
|
@ -148,7 +148,7 @@ jobs:
|
|||
id: clang_build
|
||||
continue-on-error: true
|
||||
run: |
|
||||
export LLVM_VER=19.1.3
|
||||
export LLVM_VER=19.1.5
|
||||
export PATH="/home/LLVM-${LLVM_VER}/bin:${PATH}"
|
||||
mkdir -p ${{ github.workspace }}/build_clang
|
||||
|
||||
|
@ -172,7 +172,7 @@ jobs:
|
|||
id: clang_tidy
|
||||
continue-on-error: true
|
||||
run: |
|
||||
export LLVM_VER=19.1.3
|
||||
export LLVM_VER=19.1.5
|
||||
export PATH="/home/LLVM-${LLVM_VER}/bin:${PATH}"
|
||||
cd ${{ github.workspace }}
|
||||
|
||||
|
@ -197,7 +197,7 @@ jobs:
|
|||
|
||||
#- name: Clang unit tests with -fsanitize=address
|
||||
# run: |
|
||||
# export LLVM_VER=19.1.3
|
||||
# export LLVM_VER=19.1.5
|
||||
# export PATH="/home/LLVM-${LLVM_VER}/bin:${PATH}"
|
||||
# cd ${{ github.workspace }}/build_clang
|
||||
# ASAN_SYMBOLIZER_PATH=llvm-symbolizer ASAN_OPTIONS=detect_stack_use_after_return=1:check_initialization_order=1:detect_leaks=1:atexit=1:abort_on_error=1 ./larra_xmpp_tests
|
||||
|
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -30,3 +30,8 @@ libxmlplusplus-prefix/
|
|||
spdlog.pc
|
||||
build*
|
||||
temp*
|
||||
/.idea/
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -17,27 +17,17 @@ auto Coroutine() -> boost::asio::awaitable<void> {
|
|||
auto client = co_await larra::xmpp::client::CreateClient<larra::xmpp::PrintStream<boost::asio::ip::tcp::socket>>(
|
||||
larra::xmpp::PlainUserAccount{.jid = {.username = "test1", .server = "localhost"}, .password = "test1"},
|
||||
{.useTls = larra::xmpp::client::Options::kNever});
|
||||
|
||||
// rfc6120 7.1
|
||||
// After a client authenticates with a server,
|
||||
// it MUST bind a specific resource to the stream so that the server can properly address the client.
|
||||
co_await std::visit(
|
||||
[](auto& client) -> boost::asio::awaitable<void> {
|
||||
// rfc6120 7.1
|
||||
// After a client authenticates with a server,
|
||||
// it MUST bind a specific resource to the stream so that the server can properly address the client.
|
||||
co_await client.CreateResourceBind();
|
||||
},
|
||||
client);
|
||||
|
||||
co_await std::visit(
|
||||
[](auto& client) -> boost::asio::awaitable<void> {
|
||||
co_await client.UpdateListOfContacts();
|
||||
},
|
||||
client);
|
||||
// rfc6120 2.2
|
||||
// Upon authenticating with a server and binding a resource (thus becoming a connected resource as defined in [XMPP‑CORE]),
|
||||
// a client SHOULD request the roster before sending initial presence
|
||||
|
||||
// rfc6120 2.2
|
||||
// Upon authenticating with a server and binding a resource (thus becoming a connected resource as defined in [XMPP‑CORE]),
|
||||
// a client SHOULD request the roster before sending initial presence
|
||||
co_await std::visit(
|
||||
[](auto& client) -> boost::asio::awaitable<void> {
|
||||
SPDLOG_INFO("Send presence: Available");
|
||||
co_await client.Send(larra::xmpp::presence::c2s::Available{});
|
||||
},
|
||||
|
|
|
@ -111,6 +111,9 @@ struct Client {
|
|||
get_roster_response);
|
||||
co_return;
|
||||
}
|
||||
[[nodiscard]] auto Roster() const -> const std::vector<iq::RosterItem>& {
|
||||
return this->roster.items;
|
||||
}
|
||||
|
||||
private:
|
||||
bool active = true;
|
||||
|
@ -198,6 +201,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;
|
||||
|
||||
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;
|
||||
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()};
|
||||
}
|
||||
|
||||
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 {
|
||||
UserAccount account;
|
||||
const Options& options;
|
||||
|
@ -292,8 +463,17 @@ struct ClientCreateVisitor {
|
|||
boost::asio::use_awaitable);
|
||||
}
|
||||
|
||||
auto Connect(auto& socket, boost::asio::ip::tcp::resolver::results_type resolveResults) -> boost::asio::awaitable<void> {
|
||||
co_await boost::asio::async_connect(socket, resolveResults, boost::asio::use_awaitable);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Socket>
|
||||
|
@ -324,7 +504,7 @@ struct ClientCreateVisitor {
|
|||
template <typename Socket>
|
||||
inline auto operator()(XmlStream<Socket> stream)
|
||||
-> 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"});
|
||||
SPDLOG_DEBUG("UserStream sended");
|
||||
|
@ -347,7 +527,7 @@ struct ClientCreateVisitor {
|
|||
inline auto operator()(XmlStream<boost::asio::ssl::stream<Socket>> stream)
|
||||
-> boost::asio::awaitable<std::variant<Client<Socket>, Client<boost::asio::ssl::stream<Socket>>>> {
|
||||
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(
|
||||
UserStream{.from = account.Jid().Username("anonymous"), .to = account.Jid().server, .version = "1.0", .xmlLang = "en"},
|
||||
socket.next_layer());
|
||||
|
|
156
library/include/larra/message.hpp
Normal file
156
library/include/larra/message.hpp
Normal file
|
@ -0,0 +1,156 @@
|
|||
#pragma once
|
||||
#include <larra/serialization/auto.hpp>
|
||||
#include <larra/xml_language.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace larra::xmpp {
|
||||
|
||||
namespace message {
|
||||
struct Body {
|
||||
std::string content;
|
||||
std::optional<XmlLanguage> language;
|
||||
static constexpr auto kDefaultName = "body";
|
||||
constexpr auto operator==(const Body& other) const -> bool = default;
|
||||
friend constexpr auto operator<<(xmlpp::Element* node, const Body& message) -> void {
|
||||
node->add_child_text(message.content);
|
||||
}
|
||||
static constexpr auto Parse(xmlpp::Element* node) -> Body {
|
||||
auto ptr = node->get_first_child_text();
|
||||
if(!ptr) {
|
||||
throw std::runtime_error("Message::Body: [ Text node not found ]");
|
||||
}
|
||||
auto lang = node->get_attribute("lang", "xml");
|
||||
return {
|
||||
.content = ptr->get_content(), //
|
||||
.language = lang ? std::optional{XmlLanguage::Parse(lang->get_value())} : std::nullopt //
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
namespace type {
|
||||
|
||||
struct Chat {
|
||||
static constexpr auto TryParse(std::string_view value) -> std::optional<Chat> {
|
||||
return value == "chat" ? std::optional{Chat{}} : std::nullopt;
|
||||
}
|
||||
friend constexpr auto ToString(const Chat&) -> std::string {
|
||||
return "chat";
|
||||
}
|
||||
constexpr auto operator==(const Chat&) const -> bool {
|
||||
return true;
|
||||
};
|
||||
};
|
||||
|
||||
static constexpr auto kChat = Chat{};
|
||||
|
||||
struct Error {
|
||||
static constexpr auto TryParse(std::string_view value) -> std::optional<Error> {
|
||||
return value == "error" ? std::optional{Error{}} : std::nullopt;
|
||||
}
|
||||
friend constexpr auto ToString(const Error&) -> std::string {
|
||||
return "error";
|
||||
}
|
||||
constexpr auto operator==(const Error&) const -> bool {
|
||||
return true;
|
||||
};
|
||||
};
|
||||
|
||||
static constexpr auto kError = Error{};
|
||||
|
||||
struct GroupChat {
|
||||
static constexpr auto TryParse(std::string_view value) -> std::optional<GroupChat> {
|
||||
return value == "groupchat" ? std::optional{GroupChat{}} : std::nullopt;
|
||||
}
|
||||
friend constexpr auto ToString(const GroupChat&) -> std::string {
|
||||
return "groupchat";
|
||||
}
|
||||
constexpr auto operator==(const GroupChat&) const -> bool {
|
||||
return true;
|
||||
};
|
||||
};
|
||||
|
||||
static constexpr auto kGroupChat = GroupChat{};
|
||||
|
||||
struct Headline {
|
||||
static constexpr auto TryParse(std::string_view value) -> std::optional<Headline> {
|
||||
return value == "headline" ? std::optional{Headline{}} : std::nullopt;
|
||||
}
|
||||
friend constexpr auto ToString(const Headline&) -> std::string {
|
||||
return "headline";
|
||||
}
|
||||
constexpr auto operator==(const Headline&) const -> bool {
|
||||
return true;
|
||||
};
|
||||
};
|
||||
|
||||
static constexpr auto kHeadline = Headline{};
|
||||
|
||||
struct Normal {
|
||||
static constexpr auto Parse(std::string_view value) -> Normal {
|
||||
return value == "normal" ? Normal{}
|
||||
: throw std::runtime_error(
|
||||
std::format(R"(message::type::Normal Parsing error: [ expected "normal" but "{}" found ])", value));
|
||||
}
|
||||
friend constexpr auto ToString(const Normal&) -> std::string {
|
||||
return "normal";
|
||||
}
|
||||
constexpr auto operator==(const Normal&) const -> bool {
|
||||
return true;
|
||||
};
|
||||
};
|
||||
|
||||
static constexpr auto kNormal = Normal{};
|
||||
|
||||
} // namespace type
|
||||
|
||||
using TypeVariant = std::variant<type::Chat, type::GroupChat, type::Headline, type::Error, type::Normal>;
|
||||
|
||||
struct Type : TypeVariant {
|
||||
using TypeVariant::variant;
|
||||
constexpr Type(TypeVariant variant) : TypeVariant(std::move(variant)) {
|
||||
}
|
||||
|
||||
static constexpr auto GetDefault() -> Type {
|
||||
return type::kNormal;
|
||||
}
|
||||
|
||||
friend constexpr auto ToString(const Type& value) -> std::string {
|
||||
return std::visit(
|
||||
[](const auto& value) {
|
||||
return ToString(value);
|
||||
},
|
||||
value);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace message
|
||||
|
||||
template <typename From, typename To>
|
||||
struct Message {
|
||||
static constexpr auto kDefaultName = "message";
|
||||
static auto Parse(xmlpp::Element* element) -> Message {
|
||||
return serialization::Parse<Message>(element);
|
||||
}
|
||||
friend auto operator<<(xmlpp::Element* element, const Message& message) -> void {
|
||||
serialization::Serialize(element, message);
|
||||
}
|
||||
constexpr auto operator==(const Message& other) const -> bool = default;
|
||||
From from;
|
||||
To to;
|
||||
message::Type type;
|
||||
std::optional<std::string> id;
|
||||
XmlLanguage language;
|
||||
std::vector<message::Body> body;
|
||||
};
|
||||
|
||||
template <typename From, typename To>
|
||||
struct serialization::SerializationConfigT<Message<From, To>> {
|
||||
static constexpr auto kValue = serialization::SerializationConfig<Message<From, To>>{} //
|
||||
.template With<"type">(serialization::AttributeConfig{})
|
||||
.template With<"body">(serialization::Config<std::vector<message::Body>>{});
|
||||
};
|
||||
|
||||
template <typename Info>
|
||||
struct serialization::AttributeSerializer<message::Type, Info> : serialization::AttributeSerializer<message::TypeVariant, Info> {};
|
||||
|
||||
} // namespace larra::xmpp
|
|
@ -4,7 +4,6 @@
|
|||
#include <boost/pfr.hpp>
|
||||
#include <larra/serialization.hpp>
|
||||
#include <larra/serialization/error.hpp>
|
||||
#include <ranges>
|
||||
#include <utempl/constexpr_string.hpp>
|
||||
#include <utempl/tuple.hpp>
|
||||
#include <utempl/utils.hpp>
|
||||
|
@ -16,10 +15,20 @@ template <typename T>
|
|||
struct Tag {};
|
||||
|
||||
template <typename T>
|
||||
inline constexpr auto kSerializationConfig = std::monostate{};
|
||||
struct SerializationConfigT {
|
||||
static constexpr auto kValue = std::monostate{};
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
inline constexpr auto kDeserializationConfig = kSerializationConfig<T>;
|
||||
inline constexpr auto kSerializationConfig = SerializationConfigT<T>::kValue;
|
||||
|
||||
template <typename T>
|
||||
struct DeserializationConfigT {
|
||||
static constexpr auto kValue = kSerializationConfig<T>;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
inline constexpr auto kDeserializationConfig = DeserializationConfigT<T>::kValue;
|
||||
|
||||
template <typename T>
|
||||
constexpr auto Parse(xmlpp::Element* element, Tag<T> = {}) -> T
|
||||
|
@ -55,10 +64,24 @@ struct MetaInfo {
|
|||
}
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct DefaultValue {};
|
||||
|
||||
template <typename T>
|
||||
requires requires {
|
||||
{ T::GetDefault() } -> std::convertible_to<T>;
|
||||
}
|
||||
struct DefaultValue<T> {
|
||||
static constexpr auto Default() {
|
||||
return T::GetDefault();
|
||||
}
|
||||
};
|
||||
|
||||
template <typename MainT, std::size_t Element>
|
||||
struct FieldInfo {
|
||||
using Main = MainT;
|
||||
using Info = MetaInfo<Main>;
|
||||
using Type = Info::template TupleElement<Element>;
|
||||
static inline const std::string kName = [] {
|
||||
if constexpr(requires { Info::template TupleElement<Element>::kDefaultName; }) {
|
||||
return Info::template TupleElement<Element>::kDefaultName;
|
||||
|
@ -66,6 +89,13 @@ struct FieldInfo {
|
|||
return static_cast<std::string>(Info::template kFieldName<Element>);
|
||||
}
|
||||
}();
|
||||
static inline const std::string kNamespace = [] {
|
||||
if constexpr(requires { Info::template TupleElement<Element>::kDefaultNamespace; }) {
|
||||
return Info::template TupleElement<Element>::kDefaultNamespace;
|
||||
} else {
|
||||
return std::string{};
|
||||
}
|
||||
}();
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
|
@ -216,9 +246,16 @@ struct ElementSerializer<std::vector<T>, Config, Info> {
|
|||
template <typename T, typename Info>
|
||||
struct AttributeSerializer {
|
||||
static constexpr auto Parse(xmlpp::Element* element) -> T {
|
||||
auto node = element->get_attribute(Info::kName);
|
||||
auto node = element->get_attribute(Info::kName, Info::kNamespace);
|
||||
if(!node) {
|
||||
throw AttributeParsingError(std::format("Attribute [{}: {}] parsing error", Info::kName, nameof::nameof_full_type<T>()));
|
||||
if constexpr(requires { DefaultValue<T>::Default(); }) {
|
||||
return DefaultValue<T>::Default();
|
||||
}
|
||||
throw AttributeParsingError(std::format("Attribute [{}{}{}: {}] parsing error",
|
||||
Info::kNamespace,
|
||||
Info::kNamespace == std::string{} ? "" : ":",
|
||||
Info::kName,
|
||||
nameof::nameof_full_type<T>()));
|
||||
}
|
||||
if constexpr(requires(std::string_view view) { T::Parse(view); }) {
|
||||
return T::Parse(node->get_value());
|
||||
|
@ -226,13 +263,26 @@ struct AttributeSerializer {
|
|||
return node->get_value();
|
||||
}
|
||||
}
|
||||
static constexpr auto TryParse(xmlpp::Element* element) -> std::optional<T>
|
||||
requires requires { T::TryParse(std::string_view{}); }
|
||||
{
|
||||
auto node = element->get_attribute(Info::kName, Info::kNamespace);
|
||||
if(!node) {
|
||||
throw AttributeParsingError(std::format("Attribute [{}{}{}: {}] parsing error",
|
||||
Info::kNamespace,
|
||||
Info::kNamespace == std::string{} ? "" : ":",
|
||||
Info::kName,
|
||||
nameof::nameof_full_type<T>()));
|
||||
}
|
||||
return T::TryParse(node->get_value());
|
||||
}
|
||||
static constexpr auto Serialize(xmlpp::Element* element, const T& obj) {
|
||||
if constexpr(requires {
|
||||
{ ToString(obj) } -> std::convertible_to<const std::string&>;
|
||||
}) {
|
||||
element->set_attribute(Info::kName, ToString(obj));
|
||||
element->set_attribute(Info::kName, ToString(obj), Info::kNamespace);
|
||||
} else {
|
||||
element->set_attribute(Info::kName, obj);
|
||||
element->set_attribute(Info::kName, obj, Info::kNamespace);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -240,8 +290,13 @@ struct AttributeSerializer {
|
|||
template <typename T, typename Info>
|
||||
struct AttributeSerializer<std::optional<T>, Info> {
|
||||
static constexpr auto Parse(xmlpp::Element* element) -> std::optional<T> {
|
||||
auto node = element->get_attribute(Info::kName);
|
||||
return node ? std::optional{AttributeSerializer<T, Info>::Parse(element)} : std::nullopt;
|
||||
auto node = element->get_attribute(Info::kName, Info::kNamespace);
|
||||
return node ? std::optional{AttributeSerializer<T, Info>::Parse(element)} : [] -> std::optional<T> {
|
||||
if constexpr(requires { DefaultValue<T>::Default(); }) {
|
||||
return DefaultValue<T>::Default();
|
||||
}
|
||||
return std::nullopt;
|
||||
}();
|
||||
}
|
||||
static constexpr auto Serialize(xmlpp::Element* element, const std::optional<T>& obj) {
|
||||
if(obj) {
|
||||
|
@ -250,6 +305,52 @@ struct AttributeSerializer<std::optional<T>, Info> {
|
|||
}
|
||||
};
|
||||
|
||||
template <typename... Ts, typename Info>
|
||||
struct AttributeSerializer<std::variant<Ts...>, Info> {
|
||||
template <typename T>
|
||||
static constexpr auto FunctionForType(xmlpp::Element* element) {
|
||||
return [=] -> std::optional<T> {
|
||||
if constexpr(requires { AttributeSerializer<T, Info>::TryParse(element); }) {
|
||||
return AttributeSerializer<T, Info>::TryParse(element);
|
||||
} else {
|
||||
try {
|
||||
return AttributeSerializer<T, Info>::Parse(element);
|
||||
} catch(...) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
static constexpr auto Parse(xmlpp::Element* element) -> std::variant<Ts...> {
|
||||
using T = Info::Type;
|
||||
auto node = element->get_attribute(Info::kName, Info::kNamespace);
|
||||
if(!node) {
|
||||
if constexpr(requires { DefaultValue<T>::Default(); }) {
|
||||
return DefaultValue<T>::Default();
|
||||
}
|
||||
throw AttributeParsingError(std::format("Attribute [{}{}{}: {}] parsing error",
|
||||
Info::kNamespace,
|
||||
Info::kNamespace == std::string{} ? "" : ":",
|
||||
Info::kName,
|
||||
nameof::nameof_full_type<T>()));
|
||||
}
|
||||
return [&]<typename... TTs>(utempl::TypeList<TTs...>) {
|
||||
// operator* is safe because in or_else Parse returns Ts...[sizeof...(Ts) - 1] (not optional)
|
||||
return *utempl::FirstOf(utempl::Tuple{FunctionForType<TTs>(element)...}, std::optional<std::variant<Ts...>>{})
|
||||
.or_else([&] -> std::optional<std::variant<Ts...>> {
|
||||
return AttributeSerializer<decltype(utempl::Get<sizeof...(Ts) - 1>(utempl::kTypeList<Ts...>)), Info>::Parse(element);
|
||||
});
|
||||
}(utempl::TakeFrom<sizeof...(Ts) - 1>(utempl::kTypeList<Ts...>));
|
||||
}
|
||||
static constexpr auto Serialize(xmlpp::Element* element, const std::variant<Ts...>& obj) {
|
||||
std::visit(
|
||||
[&]<typename T>(const T& value) {
|
||||
AttributeSerializer<T, Info>::Serialize(element, value);
|
||||
},
|
||||
obj);
|
||||
}
|
||||
};
|
||||
|
||||
namespace impl {
|
||||
|
||||
template <typename T>
|
||||
|
|
20
library/include/larra/xml_language.hpp
Normal file
20
library/include/larra/xml_language.hpp
Normal file
|
@ -0,0 +1,20 @@
|
|||
#pragma once
|
||||
#include <string>
|
||||
namespace larra::xmpp {
|
||||
|
||||
struct XmlLanguage : std::string {
|
||||
static constexpr auto kDefaultName = "lang";
|
||||
static constexpr auto kDefaultNamespace = "xml";
|
||||
using std::string::basic_string;
|
||||
constexpr XmlLanguage() : std::string{"en"} {};
|
||||
constexpr XmlLanguage(std::string str) : std::string(std::move(str)) {
|
||||
}
|
||||
static constexpr auto Parse(std::string_view view) -> XmlLanguage {
|
||||
return static_cast<std::string>(view);
|
||||
}
|
||||
friend constexpr auto ToString(XmlLanguage value) -> std::string {
|
||||
return std::move(static_cast<std::string&>(value));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace larra::xmpp
|
53
tests/message.cpp
Normal file
53
tests/message.cpp
Normal file
|
@ -0,0 +1,53 @@
|
|||
#include <gtest/gtest.h>
|
||||
|
||||
#include <larra/jid.hpp>
|
||||
#include <larra/message.hpp>
|
||||
|
||||
namespace larra::xmpp {
|
||||
|
||||
namespace {
|
||||
|
||||
auto CreateTestData() {
|
||||
auto doc = std::make_unique<xmlpp::Document>();
|
||||
auto node = doc->create_root_node("message");
|
||||
node->set_attribute("from", "user1@server.i2p");
|
||||
node->set_attribute("to", "user2@server.i2p");
|
||||
node->set_attribute("type", "chat");
|
||||
node->set_attribute("id", "1");
|
||||
node->set_attribute("lang", "en", "xml");
|
||||
|
||||
auto bodyNode = node->add_child_element("body");
|
||||
bodyNode->add_child_text("hello");
|
||||
return doc;
|
||||
}
|
||||
|
||||
const Message<BareJid, BareJid> kMessage{
|
||||
.from = {.username = "user1", .server = "server.i2p"},
|
||||
.to = {.username = "user2", .server = "server.i2p"},
|
||||
.type = message::type::kChat,
|
||||
.id = "1",
|
||||
.language = "en",
|
||||
.body = {{.content = "hello"}} //
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(Parse, Message) {
|
||||
auto doc = CreateTestData();
|
||||
auto node = doc->get_root_node();
|
||||
auto message = Serialization<Message<BareJid, BareJid>>::Parse(node);
|
||||
|
||||
EXPECT_EQ(message, kMessage);
|
||||
}
|
||||
|
||||
TEST(Serialize, Message) {
|
||||
auto expected =
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<message from=\"user1@server.i2p\" to=\"user2@server.i2p\" type=\"chat\" id=\"1\" "
|
||||
"xml:lang=\"en\"><body>hello</body></message>\n";
|
||||
xmlpp::Document doc;
|
||||
auto node = doc.create_root_node("message");
|
||||
Serialization<Message<BareJid, BareJid>>::Serialize(node, kMessage);
|
||||
EXPECT_EQ(doc.write_to_string(), expected);
|
||||
}
|
||||
|
||||
} // namespace larra::xmpp
|
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;
|
||||
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;
|
||||
|
||||
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};
|
||||
|
||||
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;
|
||||
|
||||
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();
|
||||
}
|
|
@ -28,7 +28,7 @@ TEST(Roster, Print) {
|
|||
|
||||
EXPECT_NO_THROW({
|
||||
auto rosterStr = ToString(roster.payload);
|
||||
EXPECT_EQ(kRosterPrintExpectedData.length(), rosterStr.capacity());
|
||||
EXPECT_EQ(kRosterPrintExpectedData.length(), rosterStr.length());
|
||||
EXPECT_EQ(kRosterPrintExpectedData, rosterStr);
|
||||
});
|
||||
}
|
||||
|
|
42
tests/xml_lang.cpp
Normal file
42
tests/xml_lang.cpp
Normal file
|
@ -0,0 +1,42 @@
|
|||
#include <gtest/gtest.h>
|
||||
|
||||
#include <larra/serialization/auto.hpp>
|
||||
#include <larra/xml_language.hpp>
|
||||
|
||||
namespace larra::xmpp {
|
||||
|
||||
namespace tests::message {
|
||||
|
||||
struct SomeStruct {
|
||||
static constexpr auto kDefaultName = "some";
|
||||
XmlLanguage language;
|
||||
friend auto operator<<(xmlpp::Element*, const SomeStruct&) -> void;
|
||||
static auto Parse(xmlpp::Element* node) -> SomeStruct;
|
||||
};
|
||||
|
||||
} // namespace tests::message
|
||||
template <>
|
||||
constexpr auto serialization::kSerializationConfig<tests::message::SomeStruct> =
|
||||
serialization::SerializationConfig<tests::message::SomeStruct>{};
|
||||
|
||||
namespace tests::message {
|
||||
|
||||
auto SomeStruct::Parse(xmlpp::Element* element) -> SomeStruct {
|
||||
return serialization::Parse<SomeStruct>(element);
|
||||
}
|
||||
|
||||
auto operator<<(xmlpp::Element* element, const SomeStruct& some) -> void {
|
||||
serialization::Serialize(element, some);
|
||||
};
|
||||
|
||||
} // namespace tests::message
|
||||
|
||||
TEST(Parse, XmlLanguage) {
|
||||
xmlpp::Document doc;
|
||||
auto node = doc.create_root_node("some");
|
||||
node->set_attribute("lang", "en", "xml");
|
||||
auto value = Serialization<tests::message::SomeStruct>::Parse(node);
|
||||
EXPECT_EQ(value.language, "en");
|
||||
}
|
||||
|
||||
} // namespace larra::xmpp
|
Loading…
Reference in a new issue