Compare commits

...

8 commits

Author SHA1 Message Date
6064be2b71 Add message serialization and deserialization
Some checks are pending
PR Check / on-push-commit-check (push) Waiting to run
2024-12-24 06:18:33 +00:00
48f99c0727 Use one std::visit instead std::visit for call 2024-12-24 06:18:33 +00:00
b908baf794 Added cmd arg handling to set log level
All checks were successful
PR Check / on-push-commit-check (push) Successful in 14m38s
2024-12-24 06:09:38 +00:00
a6c89dee5a Added tests for Iq Stanza errors 2024-12-24 06:09:38 +00:00
b565a09c1a Added std::variant and Visitor for all IQ Stanza errors 2024-12-24 06:09:38 +00:00
54e9556478 Moved ToKebapCasName to utils 2024-12-24 06:09:38 +00:00
f36b544b11 Fixed spdlog loglevel initialization 2024-12-24 06:09:38 +00:00
Anton
7856046951 Successful Connect() with proxy
Some checks failed
PR Check / on-push-commit-check (push) Has been cancelled
2024-12-24 17:08:58 +11:00
22 changed files with 1174 additions and 101 deletions

View file

@ -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)
@ -134,6 +134,8 @@ jobs:
-GNinja -DCMAKE_BUILD_TYPE=Release \
-DENABLE_EXAMPLES=ON \
-DENABLE_TESTS=ON \
-DMAX_LOG_LEVEL=0 \
-DTEST_MAX_LOG_LEVEL=0 \
-DCMAKE_CXX_FLAGS="-ftemplate-backtrace-limit=0"
cmake --build ${{ github.workspace }}/build_gcc --parallel `nproc`
@ -142,13 +144,13 @@ jobs:
continue-on-error: true
run: |
cd ${{ github.workspace }}/build_gcc
./larra_xmpp_tests
./larra_xmpp_tests --log_level=0
- name: Clang build (only configuring)
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
@ -160,6 +162,8 @@ jobs:
-GNinja -DCMAKE_BUILD_TYPE=Release \
-DENABLE_EXAMPLES=ON \
-DENABLE_TESTS=ON \
-DMAX_LOG_LEVEL=0 \
-DTEST_MAX_LOG_LEVEL=0 \
-DCMAKE_CXX_FLAGS="-stdlib=libc++ -I/home/LLVM-${LLVM_VER}/include/c++/v1 -fno-modules"
echo "::group::compile_commands.json content"
@ -172,7 +176,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 +201,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
# 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 --log_level=0

5
.gitignore vendored
View file

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

6
.vscode/launch.json vendored
View file

@ -9,7 +9,10 @@
"request": "launch",
"name": "Debug: connect",
"program": "${workspaceFolder}/build/examples/output/connect",
"args": [],
"args": [
"--log_level=0"
// --gtest_filter=POSTIVE_PATTERNS[-NEGATIVE_PATTERNS]
],
"cwd": "${workspaceFolder}",
"preLaunchTask": "GCC: Build"
},
@ -19,6 +22,7 @@
"name": "Debug: tests",
"program": "${workspaceFolder}/build/larra_xmpp_tests",
"args": [
"--log_level=0"
// --gtest_filter=POSTIVE_PATTERNS[-NEGATIVE_PATTERNS]
// "--gtest_filter=Roster*"
],

13
.vscode/tasks.json vendored
View file

@ -79,8 +79,8 @@
"command": [
"cd ${workspaceFolder} &&",
"mkdir -p build && cd build &&",
"cmake cmake -Wno-dev ",
" -DCMAKE_BUILD_TYPE=Debug -DENABLE_EXAMPLES=ON -DENABLE_TESTS=ON .."
"cmake -Wno-dev ",
" -DCMAKE_BUILD_TYPE=Debug -DENABLE_EXAMPLES=ON -DENABLE_TESTS=ON -DMAX_LOG_LEVEL=0 -DTEST_MAX_LOG_LEVEL=0 .."
],
"options": {
"env": {
@ -138,6 +138,7 @@
"mkdir -p build_clang && cd build_clang &&",
"cmake -Wno-dev ",
" -DCMAKE_BUILD_TYPE=Debug -DENABLE_EXAMPLES=ON -DENABLE_TESTS=ON ",
" -DMAX_LOG_LEVEL=0 -DTEST_MAX_LOG_LEVEL=0",
" -DCMAKE_CXX_FLAGS=\"-stdlib=libstdc++\"",
" -DCMAKE_EXE_LINKER_FLAGS=\"-L/usr/lib/x86_64-linux-gnu -lstdc++\" .."
],
@ -160,6 +161,7 @@
"mkdir -p build_clang && cd build_clang &&",
"cmake -Wno-dev ",
" -DCMAKE_BUILD_TYPE=Debug -DENABLE_EXAMPLES=ON -DENABLE_TESTS=ON",
" -DMAX_LOG_LEVEL=0 -DTEST_MAX_LOG_LEVEL=0",
//
// Uncomment for GCC standart library: libstdc++
//" -DCMAKE_CXX_FLAGS=\"-stdlib=libstdc++ -fno-omit-frame-pointer -g -fsanitize=address,undefined,leak,function,nullability,vptr\"",
@ -191,7 +193,7 @@
"command": [
"cd ${workspaceFolder} &&",
"mkdir -p build_clang && cd 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 ; echo \"exit code: $?\"",
"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 --log_level=0 ; echo \"exit code: $?\"",
],
"presentation": {
"clear": true
@ -211,6 +213,7 @@
"mkdir -p build_clang && cd build_clang &&",
"cmake -Wno-dev ",
" -DCMAKE_BUILD_TYPE=Debug -DENABLE_EXAMPLES=ON -DENABLE_TESTS=ON ",
" -DMAX_LOG_LEVEL=0 -DTEST_MAX_LOG_LEVEL=0",
" -DCMAKE_CXX_FLAGS=\"-stdlib=libstdc++ -fsanitize-memory-track-origins=2 -fno-omit-frame-pointer -fno-optimize-sibling-calls -g -fsanitize=memory\"",
" -DCMAKE_EXE_LINKER_FLAGS=\"-L/usr/lib/x86_64-linux-gnu -lstdc++ -fsanitize-memory-track-origins=2 -fno-omit-frame-pointer -fno-optimize-sibling-calls -g -fsanitize=memory\" ..",
],
@ -236,7 +239,7 @@
"command": [
"cd ${workspaceFolder} &&",
"mkdir -p build_clang && cd build_clang &&",
"MSAN_SYMBOLIZER_PATH=llvm-symbolizer MSAN_OPTIONS=abort_on_error=1 ./larra_xmpp_tests ; echo \"exit code: $?\"",
"MSAN_SYMBOLIZER_PATH=llvm-symbolizer MSAN_OPTIONS=abort_on_error=1 ./larra_xmpp_tests --log_level=0 ; echo \"exit code: $?\"",
],
"presentation": {
"clear": true
@ -270,7 +273,7 @@
"command": [
"cd ${workspaceFolder} &&",
"mkdir -p build_clang && cd build_clang &&",
"MSAN_SYMBOLIZER_PATH=llvm-symbolizer MSAN_OPTIONS=abort_on_error=1 ./larra_xmpp_tests ; echo \"exit code: $?\"",
"MSAN_SYMBOLIZER_PATH=llvm-symbolizer MSAN_OPTIONS=abort_on_error=1 ./larra_xmpp_tests --log_level=0 ; echo \"exit code: $?\"",
],
"presentation": {
"clear": true

View file

@ -18,10 +18,12 @@ set(CMAKE_CXX_EXTENSIONS OFF)
set(FMT_MODULE OFF)
set(UTEMPL_MODULE OFF)
set(CXX_EXTENSIONS NO)
set(BOOST_INCLUDE_LIBRARIES "pfr;asio;serialization")
set(BOOST_INCLUDE_LIBRARIES "pfr;asio;serialization;program_options")
option(CPM_USE_LOCAL_PACKAGES "Use local packages" ON)
option(UTEMPL_USE_LOCAL_PACKAGE "Use utempl local package" OFF)
option(BUILD_EXECUTABLE ON)
set(MAX_LOG_LEVEL 2 CACHE STRING "Available log levels: 0=TRACE, 1=DEBUG, 2=INFO, 3=WARN, 4=ERROR, 5=CRITICAL, 6=OFF")
set(TEST_MAX_LOG_LEVEL 2 CACHE STRING "Available log levels: 0=TRACE, 1=DEBUG, 2=INFO, 3=WARN, 4=ERROR, 5=CRITICAL, 6=OFF")
set(UTEMPL_URL
"https://sha512sum.xyz/git/sha512sum/utempl"
CACHE STRING "utempl repository URL")
@ -175,11 +177,11 @@ target_include_directories(larra_xmpp PUBLIC
if(TARGET Boost::pfr)
target_link_libraries(larra_xmpp PUBLIC
Boost::asio Boost::serialization utempl::utempl
Boost::asio Boost::serialization Boost::program_options utempl::utempl
OpenSSL::SSL nameof::nameof fmt::fmt
OpenSSL::Crypto spdlog xmlplusplus ${LIBXML2_LIBRARIES})
else()
find_package(Boost 1.85.0 COMPONENTS serialization REQUIRED)
find_package(Boost 1.85.0 COMPONENTS serialization program_options REQUIRED)
target_link_libraries(larra_xmpp PUBLIC
utempl::utempl ${Boost_LIBRARIES} OpenSSL::SSL
nameof::nameof fmt::fmt
@ -259,6 +261,7 @@ if(ENABLE_TESTS)
target_sources(larra_xmpp_tests PUBLIC ${SOURCES})
target_link_libraries(larra_xmpp_tests GTest::gtest_main
larra_xmpp)
target_compile_definitions(larra_xmpp_tests PRIVATE SPDLOG_ACTIVE_LEVEL=${TEST_MAX_LOG_LEVEL})
set_property(TARGET larra_xmpp_tests PROPERTY CXX_STANDARD 23)
include(GoogleTest)
gtest_discover_tests(larra_xmpp_tests)
@ -270,6 +273,7 @@ if(ENABLE_EXAMPLES)
get_filename_component(EXAMPLE_NAME ${EXAMPLE_SRC} NAME_WE)
add_executable(${EXAMPLE_NAME} ${EXAMPLE_SRC})
target_link_libraries(${EXAMPLE_NAME} larra_xmpp)
target_compile_definitions(${EXAMPLE_NAME} PRIVATE SPDLOG_ACTIVE_LEVEL=${TEST_MAX_LOG_LEVEL})
set_property(TARGET ${EXAMPLE_NAME} PROPERTY CXX_STANDARD 23)
set_target_properties(${EXAMPLE_NAME} PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/examples/output")

View file

@ -3,11 +3,28 @@
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/detached.hpp>
#include <boost/program_options.hpp>
#include <larra/client/client.hpp>
#include <larra/presence.hpp>
#include <larra/printer_stream.hpp>
#include <print>
// clang-format off
constexpr auto ToString(spdlog::level::level_enum e) {
switch (e) {
case spdlog::level::trace: return "TRACE";
case spdlog::level::debug: return "DEBUG";
case spdlog::level::info: return "INFO";
case spdlog::level::warn: return "WARNING";
case spdlog::level::err: return "ERROR";
case spdlog::level::critical: return "CRITICAL";
case spdlog::level::off: return "OFF";
default:
return "INVALID";
}
}
// clang-format on
namespace iq = larra::xmpp::iq;
auto Coroutine() -> boost::asio::awaitable<void> {
@ -17,27 +34,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 [XMPPCORE]),
// 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 [XMPPCORE]),
// 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{});
},
@ -50,8 +57,39 @@ auto Coroutine() -> boost::asio::awaitable<void> {
SPDLOG_INFO("Done connecting client!");
}
auto main() -> int {
spdlog::set_level(spdlog::level::trace);
namespace po = boost::program_options;
auto main(int argc, char** argv) -> int {
// Define options
po::options_description desc("Allowed options");
desc.add_options()("help,h", "Print help message")("log_level,l",
po::value<int>()->default_value(SPDLOG_LEVEL_INFO),
"Set log level: 0=TRACE, 1=DEBUG, 2=INFO, 3=WARN, 4=ERROR, 5=CRITICAL, 6=OFF");
// Parse command-line arguments
po::variables_map vm;
try {
po::store(po::parse_command_line(argc, argv, desc), vm);
po::notify(vm);
if(vm["log_level"].as<int>() < spdlog::level::level_enum::trace || vm["log_level"].as<int>() > spdlog::level::level_enum::off) {
throw std::invalid_argument{
std::format("Invalid argument value for '--log_level' option. Check option description for more details")};
}
if(vm["log_level"].as<int>() < SPDLOG_ACTIVE_LEVEL) {
SPDLOG_WARN("Specified log_level '{}' is lower than max available one '{}'. Log level will be changed according to the maximum one",
vm["log_level"].as<int>(),
SPDLOG_ACTIVE_LEVEL);
}
} catch(const std::exception& e) {
SPDLOG_CRITICAL("Cmd parse error: {}", e.what());
return 1;
}
// Cmd options handling
spdlog::set_level(static_cast<spdlog::level::level_enum>(vm["log_level"].as<int>()));
std::println("\nEnvironment setup:\n\tCurrently set log level: {}\n", ToString(spdlog::get_level()));
boost::asio::io_context io_context;
boost::asio::co_spawn(io_context, Coroutine(), boost::asio::detached);
io_context.run();

View file

@ -84,14 +84,12 @@ struct Client {
auto set_bind_response = co_await connection.template Read<Iq<::iq::Bind>>();
std::visit(utempl::Overloaded(
[](auto error) {
throw "Error response on IQ: Set::Bind: ''"; // TODO(unknown): Add exact error parsing
},
[&](::iq::ResultBind r) {
jid.resource = std::move(r.payload.jid->resource);
SPDLOG_INFO("Allocated resource: {}", jid.resource);
}),
set_bind_response);
},
IqErrThrowVisitor{"Error response on IQ: Set::Bind"}),
std::move(set_bind_response));
co_return;
}
@ -101,16 +99,17 @@ struct Client {
const auto get_roster_response = co_await connection.template Read<Iq<::iq::Roster>>();
std::visit(utempl::Overloaded(
[](auto error) {
throw "Error response on IQ: Get::Roster: ''"; // TODO(unknown): Add exact error parsing
},
[&](::iq::ResultRoster r) {
roster = std::move(r.payload);
SPDLOG_INFO("New roster: {}", ToString(roster));
}),
get_roster_response);
},
IqErrThrowVisitor{"Error response on IQ: Get::Roster"}),
std::move(get_roster_response));
co_return;
}
[[nodiscard]] auto Roster() const -> const std::vector<iq::RosterItem>& {
return this->roster.items;
}
private:
bool active = true;
@ -198,6 +197,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 +459,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 +500,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 +523,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());

View file

@ -1,9 +1,12 @@
#pragma once
#include <spdlog/spdlog.h>
#include <larra/jid.hpp>
#include <larra/serialization.hpp>
#include <larra/stream_error.hpp>
#include <larra/stanza_error.hpp>
#include <larra/utils.hpp>
#include <optional>
#include <stdexcept>
#include <string>
namespace larra::xmpp {
@ -115,7 +118,41 @@ using Error = BaseImplWithPayload<kErrorName, Payload>;
} // namespace iq
using IqError = iq::Error<stanza::error::StanzaError>;
template <typename Payload>
using Iq = std::variant<iq::Get<Payload>, iq::Set<Payload>, iq::Result<Payload>, iq::Error<Payload>>;
using Iq = std::variant<iq::Get<Payload>, iq::Set<Payload>, iq::Result<Payload>, IqError>;
struct IqErrThrowVisitor {
constexpr IqErrThrowVisitor(std::string_view baseErrorMsg) : baseErrorMsg(baseErrorMsg) {
}
template <typename Payload>
void operator()(const iq::Get<Payload>&) {
static constexpr std::string_view getErrorMsg = ": 'Get' is an invalid type for IQ result. Expected 'Result' or 'Error'";
auto finalErrorMsg = std::string(baseErrorMsg).append(getErrorMsg);
SPDLOG_ERROR(finalErrorMsg);
throw std::runtime_error{finalErrorMsg};
}
template <typename Payload>
void operator()(const iq::Set<Payload>&) {
static constexpr std::string_view getErrorMsg = ": 'Set' is an invalid type for IQ result. Expected 'Result' or 'Error'";
auto finalErrorMsg = std::string(baseErrorMsg).append(getErrorMsg);
SPDLOG_ERROR(finalErrorMsg);
throw std::runtime_error{finalErrorMsg};
}
void operator()(IqError err) {
SPDLOG_ERROR(baseErrorMsg);
std::visit(
[](auto exception) {
throw exception;
},
std::move(err.payload));
}
std::string_view baseErrorMsg;
};
} // namespace larra::xmpp

View 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

View file

@ -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>

View file

@ -0,0 +1,148 @@
#pragma once
#include <libxml++/libxml++.h>
#include <larra/utils.hpp>
#include <optional>
#include <variant>
namespace larra::xmpp::stanza::error {
struct StanzaBaseError : std::exception {};
// DO NOT MOVE TO ANOTHER NAMESPACE(where no heirs). VIA friend A FUNCTION IS ADDED THAT VIA ADL WILL BE SEARCHED FOR HEIRS
// C++20 modules very unstable in clangd :(
template <typename T>
struct StanzaErrorImpl : StanzaBaseError {
static constexpr auto kDefaultName = "error";
static constexpr auto kDefaultNamespace = "urn:ietf:params:xml:ns:xmpp-stanzas";
static inline const auto kKebabCaseName = static_cast<std::string>(utils::ToKebabCaseName<T>());
static constexpr auto kErrorMessage = [] -> std::string_view {
static constexpr auto name = nameof::nameof_short_type<T>();
static constexpr auto str = [] {
return std::array{std::string_view{"Stanza IQ Error: "}, std::string_view{name}, std::string_view{"\0", 1}} | std::views::join;
};
static constexpr auto array = str() | std::ranges::to<utils::RangeToWrapper<std::array<char, std::ranges::distance(str())>>>();
return {array.data(), array.size() - 1};
}();
std::optional<std::string> by{};
std::string type;
// TODO(unknown): Add "optional text children" support for stanza error. Check "XML Stanzas" -> "Syntax" for more details
static constexpr auto TryParse(xmlpp::Element* element) -> std::optional<T> {
if(not element) {
return std::nullopt;
}
auto by = element->get_attribute("by");
auto type = element->get_attribute("type");
if(not type) {
return std::nullopt;
}
auto node = element->get_first_child(kKebabCaseName);
if(not node) {
return std::nullopt;
}
T obj;
obj.type = type->get_value();
if(by) {
obj.by = std::optional{by->get_value()};
}
return obj;
}
static constexpr auto Parse(xmlpp::Element* element) -> T {
return TryParse(element).value();
}
friend constexpr auto operator<<(xmlpp::Element* element, const T& obj) -> void {
element->set_attribute("type", obj.type);
if(obj.by) {
element->set_attribute("by", *obj.by);
}
auto node = element->add_child_element(kKebabCaseName);
node->set_namespace_declaration(kDefaultNamespace);
}
constexpr auto operator==(const StanzaErrorImpl<T>&) const -> bool {
return true;
};
[[nodiscard]] constexpr auto what() const noexcept -> const char* override {
return kErrorMessage.data();
}
};
// Helper class to prevent parsing response stream into an expected return type if its name is an 'error'
struct UnknownStanzaError : StanzaBaseError {
static constexpr auto kDefaultName = "stream:error";
static constexpr std::string_view kErrorMessage = "Unknown XMPP stream error";
static constexpr auto TryParse(xmlpp::Element* element) {
return std::optional{UnknownStanzaError{}};
}
static constexpr auto Parse(xmlpp::Element* element) {
return TryParse(element).value();
}
friend constexpr auto operator<<(xmlpp::Element* element, const UnknownStanzaError& obj) -> void {
throw std::runtime_error{std::format("'{}' must never be written into the real stream!", kErrorMessage)};
}
constexpr auto operator==(const UnknownStanzaError&) const -> bool {
return true;
};
[[nodiscard]] constexpr auto what() const noexcept -> const char* override {
return kErrorMessage.data();
}
};
struct BadRequest : StanzaErrorImpl<BadRequest> {};
struct Conflict : StanzaErrorImpl<Conflict> {};
struct FeatureNotImplemented : StanzaErrorImpl<FeatureNotImplemented> {};
struct Forbidden : StanzaErrorImpl<Forbidden> {};
struct Gone : StanzaErrorImpl<Gone> {};
struct InternalServerError : StanzaErrorImpl<InternalServerError> {};
struct ItemNotFound : StanzaErrorImpl<ItemNotFound> {};
struct JidMalformed : StanzaErrorImpl<JidMalformed> {};
struct NotAcceptable : StanzaErrorImpl<NotAcceptable> {};
struct NotAllowed : StanzaErrorImpl<NotAllowed> {};
struct NotAuthorized : StanzaErrorImpl<NotAuthorized> {};
struct PolicyViolation : StanzaErrorImpl<PolicyViolation> {};
struct RecipientUnavailable : StanzaErrorImpl<RecipientUnavailable> {};
struct Redirect : StanzaErrorImpl<Redirect> {};
struct RegistrationRequired : StanzaErrorImpl<RegistrationRequired> {};
struct RemoteServerNotFound : StanzaErrorImpl<RemoteServerNotFound> {};
struct RemoteServerTimeout : StanzaErrorImpl<RemoteServerTimeout> {};
struct ResourceConstraint : StanzaErrorImpl<ResourceConstraint> {};
struct ServiceUnavailable : StanzaErrorImpl<ServiceUnavailable> {};
struct SubscriptionRequired : StanzaErrorImpl<SubscriptionRequired> {};
struct UndefinedCondition : StanzaErrorImpl<UndefinedCondition> {};
struct UnexpectedRequest : StanzaErrorImpl<UnexpectedRequest> {};
using StanzaError = std::variant<BadRequest,
Conflict,
FeatureNotImplemented,
Forbidden,
Gone,
InternalServerError,
ItemNotFound,
JidMalformed,
NotAcceptable,
NotAllowed,
NotAuthorized,
PolicyViolation,
RecipientUnavailable,
Redirect,
RegistrationRequired,
RemoteServerNotFound,
RemoteServerTimeout,
ResourceConstraint,
ServiceUnavailable,
SubscriptionRequired,
UndefinedCondition,
UnexpectedRequest,
UnknownStanzaError>;
static_assert(std::is_same_v<typename std::variant_alternative_t<std::variant_size_v<StanzaError> - 1, StanzaError>, UnknownStanzaError>,
"'UnknownStanzaError' must be at the end of 'StanzaError' variant");
} // namespace larra::xmpp::stanza::error

View file

@ -2,43 +2,10 @@
#include <libxml++/libxml++.h>
#include <larra/utils.hpp>
#include <nameof.hpp>
#include <ranges>
#include <variant>
namespace larra::xmpp {
namespace impl {
// std::isupper not declared as constexpr
constexpr auto IsUpper(char ch) -> bool {
return ch >= 'A' && ch <= 'Z';
}
constexpr auto ToLower(char ch) -> char {
return (ch >= 'A' && ch <= 'Z') ? static_cast<char>(ch + ('a' - 'A')) : ch;
}
template <typename T>
constexpr auto ToKebabCaseName() -> std::string_view {
static constexpr auto rawStr = nameof::nameof_short_type<T>();
constexpr auto str = [] {
return rawStr //
| std::views::transform([](auto ch) {
return impl::IsUpper(ch) ? std::array<char, 2>{'-', impl::ToLower(ch)} : std::array<char, 2>{ch, '\0'};
}) //
| std::views::join //
| std::views::filter([](char ch) {
return ch != '\0';
}) //
| std::views::drop(1);
};
static constexpr auto arr = str() | std::ranges::to<utils::RangeToWrapper<std::array<char, std::ranges::distance(str())>>>();
return {arr.data(), arr.size()};
}
} // namespace impl
namespace error::stream {
struct BaseError : std::exception {};
@ -48,7 +15,7 @@ struct BaseError : std::exception {};
template <typename T>
struct ErrorImpl : BaseError {
static constexpr auto kDefaultName = "stream:error";
static inline const auto kKebabCaseName = static_cast<std::string>(impl::ToKebabCaseName<T>());
static inline const auto kKebabCaseName = static_cast<std::string>(utils::ToKebabCaseName<T>());
static constexpr auto kErrorMessage = [] -> std::string_view {
static constexpr auto name = nameof::nameof_short_type<T>();
@ -85,7 +52,7 @@ struct UnknownXmppError : BaseError {
return TryParse(element).value();
}
friend constexpr auto operator<<(xmlpp::Element* element, const UnknownXmppError& obj) -> void {
throw std::format("'{}' must never be written into the real stream!", kErrorMessage);
throw std::runtime_error{std::format("'{}' must never be written into the real stream!", kErrorMessage)};
}
[[nodiscard]] constexpr auto what() const noexcept -> const char* override {
return kErrorMessage.data();
@ -145,7 +112,8 @@ using StreamError = std::variant<error::stream::BadFormat,
error::stream::UnsupportedVersion,
error::stream::UnknownXmppError>;
static_assert(!std::is_same_v<typename std::variant_alternative_t<std::variant_size_v<StreamError> - 1, StreamError>, StreamError>,
"'UnknownXmppError' must be at the end of 'StreamError' variant");
static_assert(
std::is_same_v<typename std::variant_alternative_t<std::variant_size_v<StreamError> - 1, StreamError>, error::stream::UnknownXmppError>,
"'UnknownXmppError' must be at the end of 'StreamError' variant");
} // namespace larra::xmpp

View file

@ -1,5 +1,7 @@
#pragma once
#include <boost/pfr.hpp>
#include <nameof.hpp>
#include <ranges>
#include <utempl/utils.hpp>
namespace larra::xmpp::utils {
@ -309,4 +311,31 @@ auto AccumulateFieldLength(const T& obj) -> std::size_t {
return totalLength;
}
template <typename T>
constexpr auto ToKebabCaseName() -> std::string_view {
static constexpr auto rawStr = nameof::nameof_short_type<T>();
// std::isupper and std::tolower are not declared as constexpr
static constexpr auto isUpper = [](char ch) {
return ch >= 'A' && ch <= 'Z';
};
static constexpr auto toLower = [](char ch) {
return (ch >= 'A' && ch <= 'Z') ? static_cast<char>(ch + ('a' - 'A')) : ch;
};
constexpr auto str = [] {
return rawStr //
| std::views::transform([](auto ch) {
return isUpper(ch) ? std::array<char, 2>{'-', toLower(ch)} : std::array<char, 2>{ch, '\0'};
}) //
| std::views::join //
| std::views::filter([](char ch) {
return ch != '\0';
}) //
| std::views::drop(1);
};
static constexpr auto arr = str() | std::ranges::to<utils::RangeToWrapper<std::array<char, std::ranges::distance(str())>>>();
return {arr.data(), arr.size()};
}
} // namespace larra::xmpp::utils

View 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

View file

@ -2,7 +2,7 @@
namespace larra::xmpp::iq {
auto operator<<(xmlpp::Element* element, const Bind& bind) -> void {
element->set_attribute("xmlns", Bind::kDefaultNamespace);
element->set_namespace_declaration(Bind::kDefaultNamespace);
if(bind.jid) {
auto* jid_el = element->add_child_element("jid");

View file

@ -21,7 +21,7 @@ auto RosterItem::Parse(xmlpp::Element* element) -> RosterItem {
}
auto operator<<(xmlpp::Element* element, const Roster& self) -> void {
element->set_attribute("xmlns", Roster::kDefaultNamespace);
element->set_namespace_declaration(Roster::kDefaultNamespace);
S::Serialize(element, self);
}
auto Roster::Parse(xmlpp::Element* element) -> Roster {

View file

@ -1,10 +1,23 @@
#include <gtest/gtest.h>
#include <spdlog/spdlog.h>
#include <larra/iq.hpp>
#include <stdexcept>
namespace {
static constexpr auto kExpectedData = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<iq id=\"id\" type=\"get\"><some>37</some></iq>\n";
static constexpr auto kForbiddenErrorData = R"(<?xml version="1.0" encoding="UTF-8"?>
<iq id="42" to="name@server/res" type="error"><error type="auth"><forbidden xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/></error>
</iq>)";
static constexpr auto kExpectedSetData = R"(<?xml version="1.0" encoding="UTF-8"?>
<iq id="id" type="set"><some>37</some></iq>
)";
static constexpr auto kExpectedData = R"(<?xml version="1.0" encoding="UTF-8"?>
<iq id="id" type="get"><some>37</some></iq>
)";
struct SomeStruct {
int value;
constexpr auto operator==(const SomeStruct&) const -> bool = default;
@ -43,4 +56,101 @@ TEST(IQ, Parse) {
EXPECT_EQ(S::Parse(node), iq);
}
TEST(IQ, ParseForbiddenError) {
std::istringstream xml_stream(kForbiddenErrorData);
xmlpp::DomParser parser;
parser.parse_stream(xml_stream);
using S = Serialization<Iq<SomeStruct>>;
xmlpp::Document* doc = parser.get_document();
auto iqRes = S::Parse(doc->get_root_node());
ASSERT_TRUE(std::holds_alternative<IqError>(iqRes));
auto errorRes = std::get<IqError>(iqRes);
ASSERT_TRUE(std::holds_alternative<stanza::error::Forbidden>(errorRes.payload));
}
TEST(IQ, IqErrThrowVisitorThrow) {
std::istringstream xml_stream(kForbiddenErrorData);
xmlpp::DomParser parser;
parser.parse_stream(xml_stream);
using S = Serialization<Iq<SomeStruct>>;
xmlpp::Document* doc = parser.get_document();
auto iqRes = S::Parse(doc->get_root_node());
ASSERT_TRUE(std::holds_alternative<IqError>(iqRes));
static constexpr auto visitorErrMsg = "Test Error";
static constexpr auto throwErrMsg = "Stanza IQ Error: Forbidden";
try {
std::visit(utempl::Overloaded([](iq::Result<SomeStruct> r) {}, IqErrThrowVisitor{visitorErrMsg}), std::move(iqRes));
} catch(const stanza::error::StanzaBaseError& err) {
ASSERT_STREQ(throwErrMsg, err.what());
return;
} catch(const std::runtime_error& err) {
ASSERT_TRUE(false) << "Invalid throw type throw";
} catch(...) {
ASSERT_TRUE(false) << "Unexpected throw";
}
ASSERT_TRUE(false) << "Expected throwing an exception due to an error in output";
}
TEST(IQ, IqErrThrowVisitorThrowGet) {
std::istringstream xml_stream(kExpectedData);
xmlpp::DomParser parser;
parser.parse_stream(xml_stream);
using S = Serialization<Iq<SomeStruct>>;
xmlpp::Document* doc = parser.get_document();
auto iqRes = S::Parse(doc->get_root_node());
ASSERT_TRUE(std::holds_alternative<iq::Get<SomeStruct>>(iqRes)) << "\tERROR: Unexpected parse result";
static constexpr auto visitorErrMsg = "Test Error";
static constexpr auto throwErrMsg = "Test Error: 'Get' is an invalid type for IQ result. Expected 'Result' or 'Error'";
try {
std::visit(utempl::Overloaded([](iq::Result<SomeStruct> r) {}, IqErrThrowVisitor{"Test Error"}), std::move(iqRes));
} catch(const stanza::error::StanzaBaseError& err) {
ASSERT_TRUE(false) << "\tERROR: Invalid throw type throw";
} catch(const std::runtime_error& err) {
ASSERT_STREQ(throwErrMsg, err.what());
return;
} catch(...) {
ASSERT_TRUE(false) << "\tERROR: Unexpected throw";
}
ASSERT_TRUE(false) << "\tERROR: Expected throwing an exception due to an error in output";
}
TEST(IQ, IqErrThrowVisitorThrowSet) {
std::istringstream xml_stream(kExpectedSetData);
xmlpp::DomParser parser;
parser.parse_stream(xml_stream);
using S = Serialization<Iq<SomeStruct>>;
xmlpp::Document* doc = parser.get_document();
auto iqRes = S::Parse(doc->get_root_node());
ASSERT_TRUE(std::holds_alternative<iq::Set<SomeStruct>>(iqRes)) << "\tERROR: Unexpected parse result";
static constexpr auto visitorErrMsg = "Test Error";
static constexpr auto throwErrMsg = "Test Error: 'Set' is an invalid type for IQ result. Expected 'Result' or 'Error'";
try {
std::visit(utempl::Overloaded([](iq::Result<SomeStruct> r) {}, IqErrThrowVisitor{"Test Error"}), std::move(iqRes));
} catch(const stanza::error::StanzaBaseError& err) {
ASSERT_TRUE(false) << "\tERROR: Invalid throw type throw";
} catch(const std::runtime_error& err) {
ASSERT_STREQ(throwErrMsg, err.what());
return;
} catch(...) {
ASSERT_TRUE(false) << "\tERROR: Unexpected throw";
}
ASSERT_TRUE(false) << "\tERROR: Expected throwing an exception due to an error in output";
}
} // namespace larra::xmpp

61
tests/main.cpp Normal file
View file

@ -0,0 +1,61 @@
#include <gtest/gtest.h>
#include <spdlog/spdlog.h>
#include <boost/program_options.hpp>
#include <exception>
#include <print>
#include <stdexcept>
// clang-format off
constexpr auto ToString(spdlog::level::level_enum e) {
switch (e) {
case spdlog::level::trace: return "TRACE";
case spdlog::level::debug: return "DEBUG";
case spdlog::level::info: return "INFO";
case spdlog::level::warn: return "WARNING";
case spdlog::level::err: return "ERROR";
case spdlog::level::critical: return "CRITICAL";
case spdlog::level::off: return "OFF";
default:
return "INVALID";
}
}
// clang-format on
namespace po = boost::program_options;
auto main(int argc, char** argv) -> int {
::testing::InitGoogleTest(&argc, argv);
// Define options
po::options_description desc("Allowed options");
desc.add_options()("help,h", "Print help message")("log_level,l",
po::value<int>()->default_value(SPDLOG_LEVEL_INFO),
"Set log level: 0=TRACE, 1=DEBUG, 2=INFO, 3=WARN, 4=ERROR, 5=CRITICAL, 6=OFF");
// Parse command-line arguments
po::variables_map vm;
try {
po::store(po::parse_command_line(argc, argv, desc), vm);
po::notify(vm);
if(vm["log_level"].as<int>() < spdlog::level::level_enum::trace || vm["log_level"].as<int>() > spdlog::level::level_enum::off) {
throw std::invalid_argument{
std::format("Invalid argument value for '--log_level' option. Check option description for more details")};
}
if(vm["log_level"].as<int>() < SPDLOG_ACTIVE_LEVEL) {
SPDLOG_WARN("Specified log_level '{}' is lower than max available one '{}'. Log level will be changed according to the maximum one",
vm["log_level"].as<int>(),
SPDLOG_ACTIVE_LEVEL);
}
} catch(const std::exception& e) {
SPDLOG_CRITICAL("Cmd parse error: {}", e.what());
return 1;
}
// Cmd options handling
spdlog::set_level(static_cast<spdlog::level::level_enum>(vm["log_level"].as<int>()));
std::println("\nEnvironment setup:\n\tCurrently set log level: {}\n", ToString(spdlog::get_level()));
return RUN_ALL_TESTS();
}

53
tests/message.cpp Normal file
View 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
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;
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();
}

View file

@ -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
View 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