Add RawXmlStream

This commit is contained in:
sha512sum 2024-09-28 19:15:31 +00:00
parent 57ecd21014
commit 2eb9d4e0b8
23 changed files with 1264 additions and 340 deletions

View file

@ -4,9 +4,13 @@
FROM archlinux@sha256:a10e51dd0694d6c4142754e9d06cbce7baf91ace8031a30df37064d1091ab414 FROM archlinux@sha256:a10e51dd0694d6c4142754e9d06cbce7baf91ace8031a30df37064d1091ab414
# Update the package database and install clang # Update the package database and install clang
# 1. system tools
# 2. build tools
# 3. libraries
RUN pacman -Syyu --noconfirm \ RUN pacman -Syyu --noconfirm \
&& pacman -S --noconfirm git less vim sudo base-devel \ && pacman -S --noconfirm git less vim sudo base-devel python-pip \
&& pacman -S --noconfirm clang cmake make ninja gtk4 gtkmm-4.0 boost spdlog fmt pugixml && pacman -S --noconfirm clang cmake make ninja meson \
&& pacman -S --noconfirm gtk4 gtkmm-4.0 boost spdlog fmt libxml++-5.0
# Create a non-root user 'dev' # Create a non-root user 'dev'
RUN useradd -ms /bin/bash dev \ RUN useradd -ms /bin/bash dev \

View file

@ -2,6 +2,7 @@
"name": "Arch Linux with GCC & Clang++", "name": "Arch Linux with GCC & Clang++",
"dockerComposeFile": "docker-compose.yml", "dockerComposeFile": "docker-compose.yml",
"service": "devcontainer", "service": "devcontainer",
"initializeCommand": "docker stop ejabberd > /dev/null 2>&1 ; docker rm ejabberd > /dev/null 2>&1 ; exit 0",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
"forwardPorts": [ "forwardPorts": [
5222, 5222,

View file

@ -21,15 +21,16 @@ services:
ejabberd_server: ejabberd_server:
image: ghcr.io/processone/ejabberd image: ghcr.io/processone/ejabberd
container_name: ejabberd container_name: ejabberd
# pull_policy: always # Do not use cache for ejabberd
# For some reasons below environment variables doesn't work inside vs code dev container
# Please, use devcontainer.json 'postStartCommand' for configuring ejabberd_server
#
environment: environment:
- CTL_ON_CREATE=register admin localhost "admin" ; - CTL_ON_CREATE=register admin localhost admin ;
register test1 localhost "test1" register test1 localhost test1
- CTL_ON_START=registered_users localhost ; - CTL_ON_START=registered_users localhost ;
status status ;
check_password test1 localhost test1 ;
help accounts
ports: ports:
- "5222:5222" - "5222:5222"
- "5269:5269" - "5269:5269"

View file

@ -17,7 +17,13 @@
hosts: hosts:
- localhost - localhost
loglevel: info auth_method: internal
#auth_password_format: scram
#auth_scram_hash: sha256
auth_use_cache: false
loglevel: debug
hide_sensitive_log_data: false
ca_file: /opt/ejabberd/conf/cacert.pem ca_file: /opt/ejabberd/conf/cacert.pem
@ -89,6 +95,11 @@ listen:
s2s_use_starttls: optional s2s_use_starttls: optional
c2s_protocol_options:
- no_sslv3
- cipher_server_preference
- no_compression
acl: acl:
admin: admin:
user: admin@localhost user: admin@localhost

View file

@ -1,8 +1,12 @@
sleep 1 sleep 1
# Check that ejabberd server started successfully
red='\e[1;31m'
off='\e[0m'
if [ "$( sudo docker container inspect -f '{{.State.Status}}' ejabberd )" != "running" ]; then printf "\n\n\t$red ERROR: ejabberd container is not running! $off Stop vscode dev environment \n\n\n\n"; exit 1; fi
printf "\n\n\tConfigure ejabber server\n\n" printf "\n\n\tConfigure ejabber server\n\n"
sudo docker exec -it ejabberd ejabberdctl register admin localhost admin # sudo docker exec -it ejabberd ejabberdctl register user localhost password
sudo docker exec -it ejabberd ejabberdctl register sha512sum localhost 12345
printf "\n\n\tList of registered users:\n" printf "\n\n\tList of registered users:\n"
sudo docker exec -it ejabberd ejabberdctl registered_users localhost sudo docker exec -it ejabberd ejabberdctl registered_users localhost

1
.gitignore vendored
View file

@ -11,7 +11,6 @@ compile_commands.json
cpm-package-lock.cmake cpm-package-lock.cmake
larraXMPPConfig.cmake larraXMPPConfig.cmake
larraXMPPVersionConfig.cmake larraXMPPVersionConfig.cmake
larra
larra_xmpp_tests larra_xmpp_tests
larra_xmpp_tests\[1\]_include.cmake larra_xmpp_tests\[1\]_include.cmake
larra_xmpp_tests\[1\]_tests.cmake larra_xmpp_tests\[1\]_tests.cmake

17
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,17 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug: connect",
"program": "${workspaceFolder}/build/examples/output/connect",
"args": [],
"cwd": "${workspaceFolder}",
"preLaunchTask": "Build Debug GCC"
}
]
}

View file

@ -75,8 +75,15 @@ CPMAddPackage(
) )
set(CPM_USE_LOCAL_PACKAGES ${TMP}) set(CPM_USE_LOCAL_PACKAGES ${TMP})
find_package(LibXml2 REQUIRED)
pkg_check_modules(xmlplusplus libxml++-5.0) pkg_check_modules(xmlplusplus libxml++-5.0)
if(xmlplusplus_FOUND) if(xmlplusplus_FOUND)
add_library(xmlplusplus INTERFACE EXCLUDE_FROM_ALL) add_library(xmlplusplus INTERFACE EXCLUDE_FROM_ALL)
@ -156,12 +163,12 @@ target_include_directories(larra_xmpp PUBLIC
if(TARGET Boost::pfr) if(TARGET Boost::pfr)
target_link_libraries(larra_xmpp PUBLIC target_link_libraries(larra_xmpp PUBLIC
Boost::asio Boost::serialization utempl::utempl pugixml::pugixml OpenSSL::SSL Boost::asio Boost::serialization utempl::utempl pugixml::pugixml OpenSSL::SSL
OpenSSL::Crypto spdlog xmlplusplus) OpenSSL::Crypto spdlog xmlplusplus ${LIBXML2_LIBRARIES})
else() else()
find_package(Boost 1.85.0 REQUIRED) find_package(Boost 1.85.0 REQUIRED)
target_link_libraries(larra_xmpp PUBLIC target_link_libraries(larra_xmpp PUBLIC
utempl::utempl ${Boost_LIBRARIES} pugixml::pugixml OpenSSL::SSL utempl::utempl ${Boost_LIBRARIES} pugixml::pugixml OpenSSL::SSL
OpenSSL::Crypto spdlog xmlplusplus) OpenSSL::Crypto spdlog xmlplusplus ${LIBXML2_LIBRARIES})
endif() endif()

View file

@ -11,7 +11,8 @@ auto Coroutine() -> boost::asio::awaitable<void> {
try { try {
auto client = co_await larra::xmpp::client::CreateClient<larra::xmpp::PrintStream<boost::asio::ip::tcp::socket>>( auto client = co_await larra::xmpp::client::CreateClient<larra::xmpp::PrintStream<boost::asio::ip::tcp::socket>>(
larra::xmpp::EncryptionUserAccount{{"test1", "localhost"}, "test1"}, {.useTls = larra::xmpp::client::Options::kNever}); larra::xmpp::PlainUserAccount{.jid = {.username = "test1", .server = "localhost"}, .password = "test1"},
{.useTls = larra::xmpp::client::Options::kNever});
} catch(const std::exception& err) { } catch(const std::exception& err) {
SPDLOG_ERROR("{}", err.what()); SPDLOG_ERROR("{}", err.what());
co_return; co_return;

View file

@ -14,9 +14,12 @@
#include <larra/client/options.hpp> #include <larra/client/options.hpp>
#include <larra/encryption.hpp> #include <larra/encryption.hpp>
#include <larra/features.hpp> #include <larra/features.hpp>
#include <larra/raw_xml_stream.hpp>
#include <larra/stream.hpp> #include <larra/stream.hpp>
#include <larra/user_account.hpp> #include <larra/user_account.hpp>
#include <ranges> #include <ranges>
#include "larra/client/xmpp_client_stream_features.hpp"
namespace larra::xmpp { namespace larra::xmpp {
constexpr auto kDefaultXmppPort = 5222; constexpr auto kDefaultXmppPort = 5222;
@ -25,22 +28,26 @@ constexpr auto kDefaultXmppPort = 5222;
namespace larra::xmpp::client { namespace larra::xmpp::client {
namespace rng = std::ranges;
namespace views = std::views;
template <typename Connection> template <typename Connection>
struct Client { struct Client {
constexpr Client(BareJid jid, Connection connection) : jid(std::move(jid)), connection(std::move(connection)) {}; constexpr Client(BareJid jid, RawXmlStream<Connection> connection) : jid(std::move(jid)), connection(std::move(connection)) {};
template <boost::asio::completion_token_for<void()> Token = boost::asio::use_awaitable_t<>> template <boost::asio::completion_token_for<void()> Token = boost::asio::use_awaitable_t<>>
constexpr auto Close(Token token = {}) { constexpr auto Close(Token token = {}) {
this->active = false; this->active = false;
return boost::asio::async_initiate<Token, void()>( return boost::asio::async_initiate<Token, void()>(
[]<typename Handler>(Handler&& h, Connection connection) { []<typename Handler>(Handler&& h, RawXmlStream<Connection> connection) { // NOLINT
boost::asio::co_spawn( boost::asio::co_spawn(
connection.get_executor(), connection.next_layer().get_executor(),
[](auto h, Connection connection) -> boost::asio::awaitable<void> { [](auto h, RawXmlStream<Connection> connection) -> boost::asio::awaitable<void> {
co_await boost::asio::async_write(connection, boost::asio::buffer("</stream:stream>"), boost::asio::use_awaitable); co_await boost::asio::async_write(
connection.next_layer(), boost::asio::buffer("</stream:stream>"), boost::asio::use_awaitable);
std::string response; std::string response;
co_await boost::asio::async_read_until( co_await boost::asio::async_read_until(
connection, boost::asio::dynamic_buffer(response), "</stream:stream>", boost::asio::use_awaitable); connection.next_layer(), boost::asio::dynamic_buffer(response), "</stream:stream>", boost::asio::use_awaitable);
std::move(h)(); h();
}(std::move(h), std::move(connection)), }(std::move(h), std::move(connection)),
boost::asio::detached); boost::asio::detached);
}, },
@ -48,7 +55,7 @@ struct Client {
std::move(this->connection)); std::move(this->connection));
} }
constexpr Client(const Client&) = delete; constexpr Client(const Client&) = delete;
constexpr Client(Client&& client) : connection(std::move(client.connection)), jid(std::move(client.jid)) { constexpr Client(Client&& client) noexcept : connection(std::move(client.connection)), jid(std::move(client.jid)) {
client.active = false; client.active = false;
} }
constexpr ~Client() { constexpr ~Client() {
@ -63,7 +70,7 @@ struct Client {
private: private:
bool active = true; bool active = true;
Connection connection; RawXmlStream<Connection> connection;
BareJid jid; BareJid jid;
}; };
@ -79,159 +86,154 @@ struct ServerRequiresStartTls : std::exception {
namespace impl { namespace impl {
inline auto StartStream(const BareJid& from, auto& connection) -> boost::asio::awaitable<void> {
auto stream = UserStream{}.To(from.server).From(std::move(from)).Version("1.0").XmlLang("en");
auto buffer = "<?xml version='1.0'?>" + ToString(stream);
co_await boost::asio::async_write(connection, boost::asio::buffer(buffer), boost::asio::transfer_all(), boost::asio::use_awaitable);
co_return;
}
template <std::ranges::range Range, typename... Args> template <std::ranges::range Range, typename... Args>
auto Contains(Range&& range, Args&&... values) { auto Contains(Range&& range, Args&&... values) { // NOLINT
for(auto& value : range) { for(auto& value : range) {
if(((value == values) || ...)) { if(((value == std::forward<Args>(values)) || ...)) { // NOLINT
return true; return true;
} }
} }
return false; return false;
} }
template <typename T = int>
inline auto ToInt(std::string_view input) -> std::optional<int> { inline auto ToInt(std::string_view input) -> std::optional<T> {
int out{}; T out{};
const std::from_chars_result result = std::from_chars(input.data(), input.data() + input.size(), out); const std::from_chars_result result = std::from_chars(input.data(), input.data() + input.size(), out);
return result.ec == std::errc::invalid_argument || result.ec == std::errc::result_out_of_range ? std::nullopt : std::optional{out}; return result.ec == std::errc::invalid_argument || result.ec == std::errc::result_out_of_range ? std::nullopt : std::optional{out};
} }
inline auto ParseChallenge(std::string_view str) { struct Challenge {
return std::views::split(str, ',') | std::views::transform([](auto param) { std::string body;
return std::string_view{param}; std::string_view serverNonce;
}) | std::string salt;
std::views::transform([](std::string_view param) -> std::pair<std::string_view, std::string_view> { int iterations;
auto v = param.find("="); [[nodiscard]] inline static auto Parse(const xmlpp::Element* node) -> Challenge {
return {param.substr(0, v), param.substr(v + 1)}; if(node->get_name() != "challenge") {
}) | throw std::runtime_error(std::format("Invalid name {} for challenge", node->get_name()));
std::ranges::to<std::unordered_map<std::string_view, std::string_view>>(); }
std::string decoded = DecodeBase64(node->get_first_child_text()->get_content());
auto params = std::views::split(decoded, ',') //
| std::views::transform([](auto param) { //
return std::string_view{param}; //
}) //
| std::views::transform([](std::string_view param) -> std::pair<std::string_view, std::string_view> { //
auto v = param.find("="); //
return {param.substr(0, v), param.substr(v + 1)}; //
}) //
| std::ranges::to<std::unordered_map<std::string_view, std::string_view>>();
return {.body = std::move(decoded),
.serverNonce = params.at("r"),
.salt = DecodeBase64(params.at("s")),
.iterations = ToInt(params.at("i")).value()};
}
}; };
inline auto GetAuthData(const PlainUserAccount& account) -> std::string { template <typename Tag>
return EncodeBase64('\0' + account.jid.username + '\0' + account.password); struct ChallengeResponse {
} static constexpr auto kDefaultName = "response";
static constexpr auto kDefaultNamespace = "urn:ietf:params:xml:ns:xmpp-sasl";
std::string_view password;
std::string& salt;
std::string_view serverNonce;
std::string_view firstServerMessage;
std::string_view initialMessage;
int iterations{};
Tag tag;
friend constexpr auto operator<<(xmlpp::Element* element, const ChallengeResponse& self) {
auto text = EncodeBase64(GenerateScramAuthMessage(
self.password, std::move(self.salt), self.serverNonce, self.firstServerMessage, self.initialMessage, self.iterations, self.tag));
element->add_child_text(text);
}
};
struct StartTlsRequest {
static constexpr auto kDefaultName = "starttls";
static constexpr auto kDefaultNamespace = "urn:ietf:params:xml:ns:xmpp-tls";
friend constexpr auto operator<<(xmlpp::Element*, const StartTlsRequest&) {
}
};
struct ClientCreateVisitor { struct ClientCreateVisitor {
UserAccount account; UserAccount account;
const Options& options; const Options& options;
auto Auth(PlainUserAccount account, auto& socket, StreamFeatures features, ServerToUserStream stream) -> boost::asio::awaitable<void> { template <typename Socket>
auto Auth(PlainUserAccount account, RawXmlStream<Socket>& stream, ServerToUserStream streamHeader, StreamFeatures features)
-> boost::asio::awaitable<void> {
SPDLOG_DEBUG("Start Plain Auth");
if(!std::ranges::contains(features.saslMechanisms.mechanisms, "PLAIN")) { if(!std::ranges::contains(features.saslMechanisms.mechanisms, "PLAIN")) {
throw std::runtime_error("Server not support PLAIN auth"); throw std::runtime_error("Server not support PLAIN auth");
} }
pugi::xml_document doc; const features::PlainAuthData data{.username = account.jid.username, .password = account.password};
auto data = GetAuthData(account); co_await stream.Send(data);
auto auth = doc.append_child("auth"); std::ignore = co_await stream.Read();
auth.text().set(data.c_str(), data.size());
auth.append_attribute("xmlns") = "urn:ietf:params:xml:ns:xmpp-sasl";
auth.append_attribute("mechanism") = "PLAIN";
std::ostringstream strstream;
doc.print(
strstream, "", pugi::format_default | pugi::format_no_empty_element_tags | pugi::format_attribute_single_quote | pugi::format_raw);
std::string str = std::move(strstream.str());
co_await boost::asio::async_write(socket, boost::asio::buffer(str), boost::asio::transfer_all(), boost::asio::use_awaitable);
std::string response;
co_await boost::asio::async_read_until(socket, boost::asio::dynamic_buffer(response), '>', boost::asio::use_awaitable);
} }
auto ScramAuth(std::string_view methodName, template <typename Socket, typename Tag>
const EncryptionUserAccount& account, auto ScramAuth(std::string methodName, EncryptionUserAccount account, RawXmlStream<Socket>& stream, Tag tag)
auto& socket, -> boost::asio::awaitable<void> {
auto tag) -> boost::asio::awaitable<void> { SPDLOG_DEBUG("Start Scram Auth using '{}'", methodName);
pugi::xml_document doc; const auto nonce = GenerateNonce();
auto auth = doc.append_child("auth");
auth.append_attribute("xmlns") = "urn:ietf:params:xml:ns:xmpp-sasl";
auth.append_attribute("mechanism") = methodName.data();
auto nonce = GenerateNonce();
SPDLOG_DEBUG("nonce: {}", nonce); SPDLOG_DEBUG("nonce: {}", nonce);
auto initialMessage = std::format("n,,n={},r={}", account.jid.username, nonce); const auto initialMessage = std::format("n,,n={},r={}", account.jid.username, nonce);
auto data = EncodeBase64(initialMessage); const features::ScramAuthData authData{.mechanism = methodName, .initialMessage = initialMessage, .tag = tag};
auth.text().set(data.c_str()); co_await stream.Send(authData);
std::ostringstream strstream; Challenge challenge = co_await stream.template Read<Challenge>();
doc.print(strstream, const std::string_view serverNonce = challenge.serverNonce;
"",
pugi::format_default | pugi::format_no_empty_element_tags | pugi::format_attribute_single_quote | pugi::format_raw |
pugi::format_no_escapes);
std::string str = std::move(strstream.str());
co_await boost::asio::async_write(socket, boost::asio::buffer(str), boost::asio::transfer_all(), boost::asio::use_awaitable);
std::string response;
co_await boost::asio::async_read_until(socket, boost::asio::dynamic_buffer(response), "</challenge>", boost::asio::use_awaitable);
doc.load_string(response.c_str());
auto decoded = DecodeBase64(doc.child("challenge").text().get());
auto params = ParseChallenge(decoded);
auto serverNonce = params["r"];
if(serverNonce.substr(0, nonce.size()) != nonce) { if(serverNonce.substr(0, nonce.size()) != nonce) {
throw std::runtime_error("XMPP Server SCRAM nonce not started with client nonce"); throw std::runtime_error("XMPP Server SCRAM nonce not started with client nonce");
} }
doc = pugi::xml_document{}; const ChallengeResponse challengeResponse{.password = account.password,
auto success = doc.append_child("response"); .salt = challenge.salt, // Mutable reference
success.append_attribute("xmlns") = "urn:ietf:params:xml:ns:xmpp-sasl"; .serverNonce = serverNonce,
success.text().set(EncodeBase64(GenerateScramAuthMessage(account.password, .firstServerMessage = challenge.body,
DecodeBase64(params["s"]), .initialMessage = std::string_view{initialMessage}.substr(3),
serverNonce, .iterations = challenge.iterations,
decoded, .tag = tag};
std::string_view{initialMessage}.substr(3), co_await stream.Send(challengeResponse);
ToInt(params["i"]).value(), std::unique_ptr<xmlpp::Document> doc = co_await stream.Read();
tag)) auto root = doc->get_root_node();
.c_str()); if(!root || root->get_name() == "failure") {
std::ostringstream strstream2; if(auto textNode = root->get_first_child("text")) {
doc.print(strstream2, if(auto text = dynamic_cast<xmlpp::Element*>(textNode)) {
"", if(auto childText = text->get_first_child_text()) {
pugi::format_default | pugi::format_no_empty_element_tags | pugi::format_attribute_single_quote | pugi::format_raw | throw std::runtime_error(std::format("Auth failed: {}", childText->get_content()));
pugi::format_no_escapes);
str.clear();
str = std::move(strstream2.str());
co_await boost::asio::async_write(socket, boost::asio::buffer(str), boost::asio::transfer_all(), boost::asio::use_awaitable);
response.clear();
co_await boost::asio::async_read_until(socket, boost::asio::dynamic_buffer(response), '>', boost::asio::use_awaitable);
doc.load_string(response.c_str());
if(auto failure = doc.child("failure")) {
throw std::runtime_error(std::format("Auth failed: {}", failure.child("text").text().get()));
} }
} }
}
throw std::runtime_error("Auth failed");
}
SPDLOG_DEBUG("Success auth for JID {}", ToString(account.jid));
}
auto Auth(EncryptionRequiredUserAccount account, template <typename Socket>
auto& socket, auto Auth(EncryptionRequiredUserAccount account, RawXmlStream<Socket>& stream, ServerToUserStream streamHeader, StreamFeatures features)
StreamFeatures features, -> boost::asio::awaitable<void> {
ServerToUserStream stream) -> boost::asio::awaitable<void> {
// NOLINTBEGIN
if(std::ranges::contains(features.saslMechanisms.mechanisms, "SCRAM-SHA-512")) { if(std::ranges::contains(features.saslMechanisms.mechanisms, "SCRAM-SHA-512")) {
co_return co_await ScramAuth("SCRAM-SHA-512", account, socket, sha512sum::EncryptionTag{}); co_return co_await ScramAuth("SCRAM-SHA-512", std::move(account), stream, sha512sum::EncryptionTag{});
} }
if(std::ranges::contains(features.saslMechanisms.mechanisms, "SCRAM-SHA-256")) { if(std::ranges::contains(features.saslMechanisms.mechanisms, "SCRAM-SHA-256")) {
co_return co_await ScramAuth("SCRAM-SHA-256", account, socket, sha256sum::EncryptionTag{}); co_return co_await ScramAuth("SCRAM-SHA-256", std::move(account), stream, sha256sum::EncryptionTag{});
} }
if(std::ranges::contains(features.saslMechanisms.mechanisms, "SCRAM-SHA-1")) { if(std::ranges::contains(features.saslMechanisms.mechanisms, "SCRAM-SHA-1")) {
co_return co_await ScramAuth("SCRAM-SHA-1", account, socket, sha1sum::EncryptionTag{}); co_return co_await ScramAuth("SCRAM-SHA-1", std::move(account), stream, sha1sum::EncryptionTag{});
} }
// NOLINTEND
throw std::runtime_error("Server not support SCRAM SHA 1 or SCRAM SHA 256 or SCRAM SHA 512 auth"); throw std::runtime_error("Server not support SCRAM SHA 1 or SCRAM SHA 256 or SCRAM SHA 512 auth");
} }
auto Auth(EncryptionUserAccount account, template <typename Socket>
auto& socket, auto Auth(EncryptionUserAccount account, RawXmlStream<Socket>& stream, ServerToUserStream streamHeader, StreamFeatures features)
StreamFeatures features, -> boost::asio::awaitable<void> {
ServerToUserStream stream) -> boost::asio::awaitable<void> { Contains(features.saslMechanisms.mechanisms, "SCRAM-SHA-1", "SCRAM-SHA-256", "SCRAM-SHA-512")
return Contains(features.saslMechanisms.mechanisms, "SCRAM-SHA-1", "SCRAM-SHA-256", "SCRAM-SHA-512") ? co_await this->Auth(EncryptionRequiredUserAccount{std::move(account)}, stream, std::move(streamHeader), std::move(features))
? this->Auth(EncryptionRequiredUserAccount{std::move(account)}, socket, std::move(features), std::move(stream)) : co_await this->Auth(static_cast<PlainUserAccount>(std::move(account)), stream, std::move(streamHeader), std::move(features));
: this->Auth(static_cast<PlainUserAccount>(std::move(account)), socket, std::move(features), std::move(stream));
} }
auto Auth(auto& socket, pugi::xml_document doc) -> boost::asio::awaitable<void> { template <typename Socket>
co_return co_await std::visit<boost::asio::awaitable<void>>( auto Auth(RawXmlStream<Socket>& stream, ServerToUserStream streamHeader, StreamFeatures features) -> boost::asio::awaitable<void> {
co_return co_await std::visit(
[&](auto& account) -> boost::asio::awaitable<void> { [&](auto& account) -> boost::asio::awaitable<void> {
return this->Auth(std::move(account), return this->Auth(std::move(account), stream, std::move(streamHeader), std::move(features));
socket,
StreamFeatures::Parse(doc.child("stream:stream").child("stream:features")),
ServerToUserStream::Parse(doc.child("stream:stream")));
}, },
this->account); this->account);
} }
@ -247,92 +249,162 @@ struct ClientCreateVisitor {
auto Connect(auto& socket, boost::asio::ip::tcp::resolver::results_type resolveResults) -> boost::asio::awaitable<void> { 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); co_await boost::asio::async_connect(socket, resolveResults, boost::asio::use_awaitable);
} }
auto ReadStream(auto& socket, std::string& buffer) -> boost::asio::awaitable<void> {
co_await boost::asio::async_read_until(socket, boost::asio::dynamic_buffer(buffer), "</stream:features>", boost::asio::use_awaitable);
}
auto ReadStream(auto& socket) -> boost::asio::awaitable<std::string> {
std::string buffer;
co_await ReadStream(socket, buffer);
co_return buffer;
}
template <typename Socket> template <typename Socket>
auto ProcessTls(boost::asio::ssl::stream<Socket>& socket, std::string& buffer) -> boost::asio::awaitable<void> { auto ProcessTls(RawXmlStream<boost::asio::ssl::stream<Socket>>& stream) -> boost::asio::awaitable<void> {
co_await boost::asio::async_write(socket.next_layer(), const StartTlsRequest request;
boost::asio::buffer("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>"), co_await stream.Send(request);
boost::asio::transfer_all(), std::unique_ptr<xmlpp::Document> doc = co_await stream.Read();
boost::asio::use_awaitable); if(auto node = doc->get_root_node()) {
buffer.clear(); if(node->get_name() == "proceed") {
pugi::xml_document doc; goto proceed; // NOLINT
co_await boost::asio::async_read_until(socket.next_layer(), boost::asio::dynamic_buffer(buffer), ">", boost::asio::use_awaitable); }
doc.load_string(buffer.c_str());
if(doc.child("proceed").attribute("xmlns").as_string() != std::string_view{"urn:ietf:params:xml:ns:xmpp-tls"}) {
throw StartTlsNegotiationError{"Failure XMPP"}; throw StartTlsNegotiationError{"Failure XMPP"};
}; }
SSL_set_tlsext_host_name(socket.native_handle(), account.Jid().server.c_str()); proceed:
auto& socket = stream.next_layer();
SSL_set_tlsext_host_name(socket.native_handle(), this->account.Jid().server.c_str());
try { try {
co_await socket.async_handshake(boost::asio::ssl::stream<Socket>::handshake_type::client, boost::asio::use_awaitable); co_await socket.async_handshake(boost::asio::ssl::stream<Socket>::handshake_type::client, boost::asio::use_awaitable);
} catch(const std::exception& e) { } catch(const std::exception& e) {
throw StartTlsNegotiationError{e.what()}; throw StartTlsNegotiationError{e.what()};
} }
}
static constexpr auto GetEnumerated(boost::asio::streambuf& streambuf) {
return std::views::zip(std::views::iota(std::size_t{}, streambuf.size()), ::larra::xmpp::impl::GetCharsRangeFromBuf(streambuf));
}
using EnumeratedT = decltype(std::views::zip(std::views::iota(std::size_t{}, std::size_t{}),
::larra::xmpp::impl::GetCharsRangeFromBuf(std::declval<boost::asio::streambuf&>())));
struct Splitter {
EnumeratedT range;
struct Sentinel {
std::ranges::sentinel_t<EnumeratedT> end;
}; };
struct Iterator {
std::ranges::iterator_t<EnumeratedT> it;
std::ranges::sentinel_t<EnumeratedT> end;
friend constexpr auto operator==(const Iterator& self, const Sentinel& it) -> bool {
return self.it == it.end;
}
auto operator++() -> Iterator& {
if(this->it == this->end) {
return *this;
}
this->it = std::ranges::find(this->it, this->end, '>', [](auto v) {
auto [_, c] = v;
return c;
});
if(this->it != this->end) {
++it;
}
return *this;
};
auto operator*() const {
return *it;
}
};
auto begin() -> Iterator {
return Iterator{.it = std::ranges::begin(this->range), .end = std::ranges::end(this->range)};
}
auto end() -> Sentinel {
return {.end = std::ranges::end(this->range)};
}
};
auto GetStartStreamIndex(auto& socket, boost::asio::streambuf& streambuf) -> boost::asio::awaitable<std::size_t> {
auto buf = streambuf.prepare(4096); // NOLINT
std::size_t n = co_await socket.async_read_some(buf, boost::asio::use_awaitable);
streambuf.commit(n);
auto splited = Splitter{GetEnumerated(streambuf)};
using It = decltype(splited.begin());
// clang-format off
co_return co_await
[&](this auto&& self, std::size_t n, It it) -> std::optional<It> {
return n == 0 ? std::move(it) : it == splited.end() ? std::nullopt : self(n - 1, (++it, std::move(it)));
}(2, splited.begin())
.transform([](auto value) -> boost::asio::awaitable<std::size_t> {
auto [n, _] = *value;
co_return n;
})
.or_else([&] -> std::optional<boost::asio::awaitable<std::size_t>> {
return this->GetStartStreamIndex(socket, streambuf);
})
.value();
// clang-format on
}
auto ReadStartStream(auto& socket, boost::asio::streambuf& streambuf) -> boost::asio::awaitable<ServerToUserStream> {
auto n = co_await this->GetStartStreamIndex(socket, streambuf);
xmlpp::DomParser parser;
std::string dataToReed =
(::larra::xmpp::impl::GetCharsRangeFromBuf(streambuf) | std::views::take(n - 1) | std::ranges::to<std::string>()) + "/>";
parser.parse_memory(dataToReed);
auto doc = parser.get_document();
SPDLOG_DEBUG("Stream readed. Consuming {} bytes with stream data {}. Total buffer size: {}", n, dataToReed, streambuf.size());
streambuf.consume(n);
co_return ServerToUserStream::Parse(doc->get_root_node());
}
template <typename Socket> template <typename Socket>
inline auto operator()(Socket&& socket) inline auto operator()(RawXmlStream<Socket> stream)
-> boost::asio::awaitable<std::variant<Client<std::decay_t<Socket>>, Client<boost::asio::ssl::stream<std::decay_t<Socket>>>>> { -> boost::asio::awaitable<std::variant<Client<Socket>, Client<boost::asio::ssl::stream<Socket>>>> {
co_await this->Connect(socket, co_await this->Resolve()); co_await this->Connect(stream.next_layer(), co_await this->Resolve());
co_await impl::StartStream(account.Jid(), socket);
auto response = co_await ReadStream(socket); co_await stream.Send(UserStream{.from = account.Jid(), .to = account.Jid().server, .version = "1.0", .xmlLang = "en"});
pugi::xml_document doc; SPDLOG_DEBUG("UserStream sended");
doc.load_string(response.c_str()); ServerToUserStream sToUStream = co_await ReadStartStream(stream.next_layer(), *stream.streambuf);
auto streamNode = doc.child("stream:stream"); StreamFeatures features = co_await stream.template Read<StreamFeatures>();
auto features = streamNode.child("stream:features"); SPDLOG_DEBUG("features parsed");
if(features.child("starttls").child("required")) {
if(features.startTls && features.startTls->required == Required::kRequired) {
throw ServerRequiresStartTls{}; throw ServerRequiresStartTls{};
} }
co_await this->Auth(socket, std::move(doc));
co_return Client{std::move(this->account).Jid(), std::move(socket)}; co_await this->Auth(stream, std::move(sToUStream), std::move(features));
co_return Client{std::move(this->account).Jid(), std::move(stream)};
} }
template <typename Socket> template <typename Socket>
inline auto operator()(boost::asio::ssl::stream<Socket>&& socket) inline auto operator()(RawXmlStream<boost::asio::ssl::stream<Socket>> stream)
-> boost::asio::awaitable<std::variant<Client<Socket>, Client<boost::asio::ssl::stream<Socket>>>> { -> boost::asio::awaitable<std::variant<Client<Socket>, Client<boost::asio::ssl::stream<Socket>>>> {
auto& socket = stream.next_layer();
co_await this->Connect(socket.next_layer(), co_await this->Resolve()); co_await this->Connect(socket.next_layer(), co_await this->Resolve());
co_await impl::StartStream(account.Jid().Username("anonymous"), socket.next_layer()); co_await stream.Send(UserStream{.from = account.Jid().Username("anonymous"), .to = account.Jid().server}, socket.next_layer());
auto response = co_await this->ReadStream(socket.next_layer()); SPDLOG_DEBUG("UserStream sended");
pugi::xml_document doc; auto streamHeader = co_await this->ReadStartStream(socket, *stream.streambuf);
doc.load_string(response.c_str()); StreamFeatures features = co_await stream.template Read<StreamFeatures>();
auto streamNode = doc.child("stream:stream"); SPDLOG_DEBUG("features parsed(SSL)");
auto stream = ServerToUserStream::Parse(streamNode); if(!features.startTls) {
auto features = streamNode.child("stream:features");
if(!features.child("starttls")) {
if(this->options.useTls == Options::kRequire) { if(this->options.useTls == Options::kRequire) {
throw std::runtime_error("XMPP server not support STARTTLS"); throw std::runtime_error("XMPP server not support STARTTLS");
} }
socket.next_layer().close(); socket.next_layer().close();
co_return co_await (*this)(socket.next_layer()); co_return co_await (*this)(RawXmlStream<Socket>{Socket{std::move(socket.next_layer())}, std::move(stream.streambuf)});
} }
response.clear(); co_await this->ProcessTls(stream);
co_await this->ProcessTls(socket, response); co_await stream.Send(UserStream{.from = account.Jid(), .to = account.Jid().server}, socket.next_layer());
co_await impl::StartStream(account.Jid(), socket); auto newStreamHeader = co_await this->ReadStartStream(socket, *stream.streambuf);
response.clear(); auto newFeatures = co_await stream.template Read<StreamFeatures>();
co_await this->ReadStream(socket, response); co_await this->Auth(stream, std::move(newStreamHeader), std::move(newFeatures));
doc.load_string(response.c_str()); co_return Client{std::move(this->account).Jid(), RawXmlStream{std::move(socket)}};
co_await this->Auth(socket, std::move(doc));
co_return Client{std::move(this->account).Jid(), std::move(socket)};
} }
}; };
} // namespace impl } // namespace impl
template <typename Socket = boost::asio::ip::tcp::socket> template <typename Socket = boost::asio::ip::tcp::socket>
inline auto CreateClient(UserAccount account, const Options& options = {}) inline auto CreateClient(UserAccount account, Options options = {})
-> boost::asio::awaitable<std::variant<Client<Socket>, Client<boost::asio::ssl::stream<Socket>>>> { -> boost::asio::awaitable<std::variant<Client<Socket>, Client<boost::asio::ssl::stream<Socket>>>> {
auto executor = co_await boost::asio::this_coro::executor; auto executor = co_await boost::asio::this_coro::executor;
boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23); boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23);
co_return co_await std::visit<boost::asio::awaitable<std::variant<Client<Socket>, Client<boost::asio::ssl::stream<Socket>>>>>( co_return co_await std::visit(
impl::ClientCreateVisitor{std::move(account), options}, impl::ClientCreateVisitor{.account = std::move(account), .options = options},
options.useTls == Options::kNever ? std::variant<Socket, boost::asio::ssl::stream<Socket>>{Socket{executor}} options.useTls == Options::kNever
: boost::asio::ssl::stream<Socket>(executor, ctx)); ? std::variant<RawXmlStream<Socket>, RawXmlStream<boost::asio::ssl::stream<Socket>>>{RawXmlStream{Socket{executor}}}
: RawXmlStream{boost::asio::ssl::stream<Socket>(executor, ctx)});
} }
} // namespace larra::xmpp::client } // namespace larra::xmpp::client

View file

@ -0,0 +1,53 @@
#pragma once
#include <spdlog/spdlog.h>
#include <larra/encryption.hpp>
#include <larra/jid.hpp>
#include <larra/raw_xml_stream.hpp>
#include <utility>
namespace larra::xmpp::client::features {
/*
* Auth features
*/
template <typename Self>
struct AuthData {
static constexpr auto kDefaultName = "auth";
static constexpr auto kDefaultNamespace = "urn:ietf:params:xml:ns:xmpp-sasl";
friend constexpr auto operator<<(xmlpp::Element* element, const AuthData<Self>& self) {
SPDLOG_DEBUG("AuthData operator<<");
element->set_attribute("mechanism", static_cast<std::string>(static_cast<const Self&>(self).mechanism));
static_cast<const Self&>(self).Write(element);
}
};
struct PlainAuthData : AuthData<PlainAuthData> {
// Not by code style
static constexpr auto mechanism = "PLAIN";
const std::string& username; // Very ugly, but shit happends
const std::string& password; // std::format can't work with '\0' in fmt string and operator+ requires std::string's
[[nodiscard]] inline auto GetAuthData() const -> std::string {
return EncodeBase64('\0' + this->username + '\0' + this->password);
}
constexpr auto Write(xmlpp::Element* element) const {
element->add_child_text(this->GetAuthData());
}
};
template <typename Tag>
struct ScramAuthData : AuthData<ScramAuthData<Tag>> {
std::string_view mechanism;
std::string_view initialMessage;
Tag tag;
constexpr auto Write(xmlpp::Element* element) const {
element->add_child_text(EncodeBase64(this->initialMessage));
}
};
} // namespace larra::xmpp::client::features

View file

@ -1,4 +1,6 @@
#pragma once #pragma once
#include <libxml++/libxml++.h>
#include <larra/utils.hpp> #include <larra/utils.hpp>
#include <optional> #include <optional>
#include <pugixml.hpp> #include <pugixml.hpp>
@ -10,7 +12,8 @@ enum class Required : bool { kNotRequired = false, kRequired = true };
struct SaslMechanisms { struct SaslMechanisms {
std::vector<std::string> mechanisms; std::vector<std::string> mechanisms;
static auto Parse(pugi::xml_node) -> SaslMechanisms;
[[nodiscard]] static auto Parse(const xmlpp::Element*) -> SaslMechanisms;
}; };
struct StreamFeatures { struct StreamFeatures {
@ -19,19 +22,21 @@ struct StreamFeatures {
[[nodiscard]] constexpr auto Required(Required required) const -> StartTlsType { [[nodiscard]] constexpr auto Required(Required required) const -> StartTlsType {
return {required}; return {required};
}; };
static auto Parse(pugi::xml_node) -> StartTlsType;
[[nodiscard]] static auto Parse(const xmlpp::Element*) -> StartTlsType;
}; };
struct BindType { struct BindType {
Required required; Required required;
[[nodiscard]] constexpr auto Required(Required required) const -> BindType { [[nodiscard]] constexpr auto Required(Required required) const -> BindType {
return {required}; return {required};
}; };
static auto Parse(pugi::xml_node) -> BindType;
[[nodiscard]] static auto Parse(const xmlpp::Element*) -> BindType;
}; };
std::optional<StartTlsType> startTls; std::optional<StartTlsType> startTls;
std::optional<BindType> bind; std::optional<BindType> bind;
SaslMechanisms saslMechanisms; SaslMechanisms saslMechanisms;
std::vector<pugi::xml_node> others; std::vector<const xmlpp::Node*> others;
template <typename Self> template <typename Self>
[[nodiscard]] constexpr auto StartTls(this Self&& self, std::optional<StartTlsType> value) { [[nodiscard]] constexpr auto StartTls(this Self&& self, std::optional<StartTlsType> value) {
return utils::FieldSetHelper::With<"startTls">(std::forward<Self>(self), std::move(value)); return utils::FieldSetHelper::With<"startTls">(std::forward<Self>(self), std::move(value));
@ -48,7 +53,8 @@ struct StreamFeatures {
[[nodiscard]] constexpr auto Others(this Self&& self, std::vector<pugi::xml_node> value) { [[nodiscard]] constexpr auto Others(this Self&& self, std::vector<pugi::xml_node> value) {
return utils::FieldSetHelper::With<"others">(std::forward<Self>(self), std::move(value)); return utils::FieldSetHelper::With<"others">(std::forward<Self>(self), std::move(value));
} }
static auto Parse(pugi::xml_node) -> StreamFeatures; [[nodiscard]] static auto Parse(pugi::xml_node) -> StreamFeatures;
[[nodiscard]] static auto Parse(const xmlpp::Element*) -> StreamFeatures;
}; };
} // namespace larra::xmpp } // namespace larra::xmpp

View file

@ -0,0 +1,73 @@
#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/associated_executor.hpp>
#include <boost/asio/buffer.hpp>
namespace larra::xmpp::impl {
class MockSocket {
public:
using executor_type = boost::asio::any_io_executor;
MockSocket(boost::asio::any_io_executor executor, std::size_t writeWithMaxBlocksBy = 5) : // NOLINT
executor(std::move(executor)), writeWithMaxBlocksBy(writeWithMaxBlocksBy) {
}
auto get_executor() -> executor_type {
return executor;
}
auto lowest_layer() -> MockSocket& {
return *this;
}
template <typename EndpointType, typename CompletionToken>
auto async_connect(const EndpointType&, CompletionToken&& token) {
return boost::asio::async_initiate<CompletionToken, void(boost::system::error_code)>(
[](auto&& handler) {
handler(boost::system::error_code{});
},
token);
}
template <typename ConstBufferSequence, typename CompletionToken>
auto async_write_some(const ConstBufferSequence& buffers, CompletionToken&& token) {
sentData.append(boost::asio::buffer_cast<const char*>(*buffers.begin()), boost::asio::buffer_size(*buffers.begin()));
return boost::asio::async_initiate<CompletionToken, void(boost::system::error_code, std::size_t)>(
[](auto&& handler, std::size_t bytes_transferred) {
handler(boost::system::error_code{}, bytes_transferred);
},
token,
boost::asio::buffer_size(*buffers.begin()));
}
template <typename MutableBufferSequence, typename CompletionToken>
auto async_read_some(const MutableBufferSequence& buffers, CompletionToken&& token) {
std::size_t bytesToRead =
std::min({boost::asio::buffer_size(buffers), this->writeWithMaxBlocksBy, this->receivedData.size() - this->readPosition});
std::memcpy(boost::asio::buffer_cast<void*>(buffers), receivedData.data() + readPosition, bytesToRead); // NOLINT
readPosition += bytesToRead;
return boost::asio::async_initiate<CompletionToken, void(boost::system::error_code, std::size_t)>(
[](auto&& handler, std::size_t bytes_transferred) {
handler(bytes_transferred == 0 ? boost::asio::error::eof : boost::system::error_code{}, bytes_transferred);
},
token,
bytesToRead);
}
auto AddReceivedData(std::string_view data) -> void {
receivedData += data;
}
auto GetSentData() -> std::string {
auto sentData = std::move(this->sentData);
this->sentData = std::string{};
return sentData;
}
boost::asio::any_io_executor executor;
std::string sentData;
std::string receivedData;
std::size_t writeWithMaxBlocksBy;
std::size_t readPosition = 0;
};
} // namespace larra::xmpp::impl

View file

@ -0,0 +1,34 @@
#pragma once
#include <cstddef>
#include <type_traits>
namespace larra::xmpp::impl {
template <typename T, std::size_t I = 0>
struct PublicCastTag {
friend constexpr auto MagicGetPrivateMember(PublicCastTag);
};
template <auto ptr, std::size_t I = 0>
struct PublicCast {};
template <typename T, typename R, R T::*ptr, std::size_t I>
struct PublicCast<ptr, I> {
friend constexpr auto MagicGetPrivateMember(PublicCastTag<T, I>) {
return ptr;
}
};
template <typename T, typename R, typename... Args, R (T::*ptr)(Args...), std::size_t I>
struct PublicCast<ptr, I> {
friend constexpr auto MagicGetPrivateMember(PublicCastTag<T, I>) {
return ptr;
}
};
template <typename T, std::size_t I = 0>
constexpr auto GetPrivateMember(const T&) {
return MagicGetPrivateMember(PublicCastTag<std::decay_t<T>, I>{});
};
} // namespace larra::xmpp::impl

View file

@ -3,31 +3,51 @@
#include <boost/asio/ssl.hpp> #include <boost/asio/ssl.hpp>
#include <boost/asio/write.hpp> #include <boost/asio/write.hpp>
#include <larra/raw_xml_stream.hpp>
#include <print> #include <print>
#include <sstream> #include <ranges>
namespace larra::xmpp { namespace larra::xmpp {
namespace impl {
constexpr auto GetStringFromBuf(const auto& buffers, std::size_t n) -> std::string {
auto f = [&] {
if constexpr(requires {
{ buffers.data() } -> std::convertible_to<const void*>;
}) {
return impl::BufferToStringView(buffers);
} else {
return GetCharsRangeFromBuf(buffers);
}
};
return f() | std::views::take(n) | std::ranges::to<std::string>();
}
constexpr auto GetStringFromBuf(const auto& buffers) -> std::string {
return GetStringFromBuf(buffers, buffers.size());
}
} // namespace impl
template <typename Socket> template <typename Socket>
struct PrintStream : Socket { struct PrintStream : Socket {
using Socket::Socket; using Socket::Socket;
PrintStream(PrintStream&&) = default;
using Executor = Socket::executor_type; using Executor = Socket::executor_type;
template <typename ConstBufferSequence, template <typename ConstBufferSequence,
BOOST_ASIO_COMPLETION_TOKEN_FOR(void(boost::system::error_code, std::size_t)) BOOST_ASIO_COMPLETION_TOKEN_FOR(void(boost::system::error_code, std::size_t))
WriteToken = boost::asio::default_completion_token_t<Executor>> WriteToken = boost::asio::default_completion_token_t<Executor>>
auto async_write_some(const ConstBufferSequence& buffers, WriteToken&& token) { auto async_write_some(const ConstBufferSequence& buffers, WriteToken&& token) { // NOLINT
std::ostringstream stream; // Write to buffer for concurrent logging
// std::osyncstream not realized in libc++ SPDLOG_INFO("Writing data to stream: {}", impl::GetStringFromBuf(buffers));
stream << "Writing data to stream: ";
for(boost::asio::const_buffer buf : buffers) {
stream << std::string_view{static_cast<const char*>(buf.data()), buf.size()};
}
SPDLOG_INFO("{}", stream.str());
return boost::asio::async_initiate<WriteToken, void(boost::system::error_code, std::size_t)>( return boost::asio::async_initiate<WriteToken, void(boost::system::error_code, std::size_t)>(
[this]<typename Handler>(Handler&& token, const ConstBufferSequence& buffers) { [this]<typename Handler>(Handler&& handler, const ConstBufferSequence& buffers) { // NOLINT
Socket::async_write_some(buffers, [token = std::move(token)](boost::system::error_code err, std::size_t s) mutable { Socket::async_write_some(buffers, [h = std::move(handler)](boost::system::error_code err, std::size_t s) mutable {
SPDLOG_INFO("Data writing completed"); SPDLOG_INFO("Data writing completed: {}", s);
token(err, s); h(err, s);
}); });
}, },
token, token,
@ -36,19 +56,14 @@ struct PrintStream : Socket {
template <typename MutableBufferSequence, template <typename MutableBufferSequence,
BOOST_ASIO_COMPLETION_TOKEN_FOR(void(boost::system::error_code, std::size_t)) BOOST_ASIO_COMPLETION_TOKEN_FOR(void(boost::system::error_code, std::size_t))
ReadToken = boost::asio::default_completion_token_t<Executor>> ReadToken = boost::asio::default_completion_token_t<Executor>>
auto async_read_some(const MutableBufferSequence& buffers, ReadToken&& token) { auto async_read_some(const MutableBufferSequence& buffers, ReadToken&& token) { // NOLINT
SPDLOG_INFO("Reading data from stream"); SPDLOG_INFO("Reading data from stream:");
return boost::asio::async_initiate<ReadToken, void(boost::system::error_code, std::size_t)>( return boost::asio::async_initiate<ReadToken, void(boost::system::error_code, std::size_t)>(
[this](ReadToken&& token, const MutableBufferSequence& buffers) { [this]<typename Handler>(Handler&& handler, const MutableBufferSequence& buffers) { // NOLINT
Socket::async_read_some(buffers, [buffers, token = std::move(token)](boost::system::error_code err, std::size_t s) mutable { Socket::async_read_some(buffers, [buffers, h = std::move(handler)](boost::system::error_code err, std::size_t s) mutable {
std::ostringstream stream; // Write to buffer for concurrent logging SPDLOG_INFO("Readed data: {}", impl::GetStringFromBuf(buffers, s));
// std::osyncstream not realized in libc++
stream << "Data after read: "; h(err, s);
for(boost::asio::mutable_buffer buf : buffers) {
stream << std::string_view{static_cast<const char*>(buf.data()), buf.size()};
}
SPDLOG_INFO("{}", stream.str());
token(err, s);
}); });
}, },
token, token,
@ -77,19 +92,15 @@ struct boost::asio::ssl::stream<larra::xmpp::PrintStream<Socket>> : public boost
template <typename ConstBufferSequence, template <typename ConstBufferSequence,
BOOST_ASIO_COMPLETION_TOKEN_FOR(void(boost::system::error_code, std::size_t)) BOOST_ASIO_COMPLETION_TOKEN_FOR(void(boost::system::error_code, std::size_t))
WriteToken = boost::asio::default_completion_token_t<Executor>> WriteToken = boost::asio::default_completion_token_t<Executor>>
auto async_write_some(const ConstBufferSequence& buffers, WriteToken&& token) { auto async_write_some(const ConstBufferSequence& buffers, WriteToken&& token) { // NOLINT
std::ostringstream stream; // Write to buffer for concurrent logging
// std::osyncstream not realized in libc++ SPDLOG_INFO("Writing data to stream(SSL): {}", ::larra::xmpp::impl::GetStringFromBuf(buffers));
stream << "Writing data to stream(SSL): ";
for(boost::asio::const_buffer buf : buffers) {
stream << std::string_view{static_cast<const char*>(buf.data()), buf.size()};
}
SPDLOG_INFO("{}", stream.str());
return boost::asio::async_initiate<WriteToken, void(boost::system::error_code, std::size_t)>( return boost::asio::async_initiate<WriteToken, void(boost::system::error_code, std::size_t)>(
[this]<typename Handler>(Handler&& token, const ConstBufferSequence& buffers) { [this]<typename Handler>(Handler&& handler, const ConstBufferSequence& buffers) { // NOLINT
Base::async_write_some(buffers, [token = std::move(token)](boost::system::error_code err, std::size_t s) mutable { Base::async_write_some(buffers, [h = std::move(handler)](boost::system::error_code err, std::size_t s) mutable {
SPDLOG_INFO("Data writing completed(SSL)"); SPDLOG_INFO("Data writing completed(SSL): {}", s);
std::move(token)(err, s); h(err, s);
}); });
}, },
token, token,
@ -98,19 +109,13 @@ struct boost::asio::ssl::stream<larra::xmpp::PrintStream<Socket>> : public boost
template <typename MutableBufferSequence, template <typename MutableBufferSequence,
BOOST_ASIO_COMPLETION_TOKEN_FOR(void(boost::system::error_code, std::size_t)) BOOST_ASIO_COMPLETION_TOKEN_FOR(void(boost::system::error_code, std::size_t))
ReadToken = boost::asio::default_completion_token_t<Executor>> ReadToken = boost::asio::default_completion_token_t<Executor>>
auto async_read_some(const MutableBufferSequence& buffers, ReadToken&& token) { auto async_read_some(const MutableBufferSequence& buffers, ReadToken&& token) { // NOLINT
SPDLOG_INFO("Reading data from stream(SSL)"); SPDLOG_INFO("Reading data from stream(SSL)");
return boost::asio::async_initiate<ReadToken, void(boost::system::error_code, std::size_t)>( return boost::asio::async_initiate<ReadToken, void(boost::system::error_code, std::size_t)>(
[this]<typename Handler>(Handler&& token, const MutableBufferSequence& buffers) { [this]<typename Handler>(Handler&& handler, const MutableBufferSequence& buffers) { // NOLINT
Base::async_read_some(buffers, [buffers, token = std::move(token)](boost::system::error_code err, std::size_t s) mutable { Base::async_read_some(buffers, [buffers, h = std::move(handler)](boost::system::error_code err, std::size_t s) mutable {
std::ostringstream stream; // Write to buffer for concurrent logging SPDLOG_INFO("Readed data(SSL): {}", ::larra::xmpp::impl::GetStringFromBuf(buffers, s));
// std::osyncstream not realized in libc++ h(err, s);
stream << "Data after read(SSL): ";
for(boost::asio::mutable_buffer buf : buffers) {
stream << std::string_view{static_cast<const char*>(buf.data()), buf.size()};
}
SPDLOG_INFO("{}", stream.str());
std::move(token)(err, s);
}); });
}, },
token, token,
@ -118,13 +123,13 @@ struct boost::asio::ssl::stream<larra::xmpp::PrintStream<Socket>> : public boost
} }
template <BOOST_ASIO_COMPLETION_TOKEN_FOR(void(boost::system::error_code)) template <BOOST_ASIO_COMPLETION_TOKEN_FOR(void(boost::system::error_code))
HandshakeToken = boost::asio::default_completion_token_t<Executor>> HandshakeToken = boost::asio::default_completion_token_t<Executor>>
auto async_handshake(Base::handshake_type type, HandshakeToken&& token = default_completion_token_t<Executor>{}) { auto async_handshake(Base::handshake_type type, HandshakeToken&& token = default_completion_token_t<Executor>{}) { // NOLINT
std::println("SSL Handshake start"); std::println("SSL Handshake start");
return boost::asio::async_initiate<HandshakeToken, void(boost::system::error_code)>( return boost::asio::async_initiate<HandshakeToken, void(boost::system::error_code)>(
[this]<typename Handler>(Handler&& token, Base::handshake_type type) { [this]<typename Handler>(Handler&& handler, Base::handshake_type type) { // NOLINT
Base::async_handshake(type, [token = std::move(token)](boost::system::error_code error) mutable { Base::async_handshake(type, [h = std::move(handler)](boost::system::error_code error) mutable {
SPDLOG_INFO("SSL Handshake completed"); SPDLOG_INFO("SSL Handshake completed");
std::move(token)(error); h(error);
}); });
}, },
token, token,

View file

@ -0,0 +1,282 @@
#pragma once
#include <libxml++/libxml++.h>
#include <spdlog/spdlog.h>
#include <boost/asio/as_tuple.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/read.hpp>
#include <boost/asio/streambuf.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/asio/write.hpp>
#include <boost/system/result.hpp>
#include <larra/utils.hpp>
#include <stack>
#include <utempl/utils.hpp>
struct _xmlError;
namespace larra::xmpp {
template <typename T>
concept AsXml = requires(xmlpp::Element* element, const T& obj) {
element << obj;
{ T::kDefaultName } -> std::convertible_to<const std::string&>;
};
template <typename T>
concept HasDefaultNamespace = requires {
{ T::kDefaultNamespace } -> std::convertible_to<const std::string&>;
};
template <typename T>
concept HasDefaultPrefix = requires {
{ T::kPrefix } -> std::convertible_to<const std::string&>;
};
template <typename T>
concept HasAddXmlDecl = requires {
{ T::kAddXmlDecl } -> std::convertible_to<bool>;
};
template <typename T>
concept HasRemoveEnd = requires {
{ T::kRemoveEnd } -> std::convertible_to<bool>;
};
struct XmlGroup : xmlpp::Element {
using Element::Element;
};
struct XmlPath : public xmlpp::Element {
public:
using Element::Element;
[[nodiscard]] auto GetData() const -> std::string {
return get_attribute("d")->get_value();
}
};
namespace impl {
constexpr auto BufferToStringView(const boost::asio::const_buffer& buffer, size_t size) -> std::string_view {
assert(size <= buffer.size());
return {boost::asio::buffer_cast<const char*>(buffer), size};
};
constexpr auto BufferToStringView(const boost::asio::const_buffer& buffer) -> std::string_view {
return BufferToStringView(buffer, buffer.size());
};
class Parser : private xmlpp::SaxParser {
public:
inline explicit Parser(xmlpp::Document& document) : doc(document) {};
~Parser() override = default;
auto ParseChunk(std::string_view str) -> const _xmlError*;
std::stack<xmlpp::Element*> context;
xmlpp::Document& doc;
private:
inline auto on_start_document() -> void override {
}
inline auto on_end_document() -> void override {
}
auto on_start_element(const std::string& name, const AttributeList& properties) -> void override;
auto on_end_element(const std::string& name) -> void override;
auto on_characters(const std::string& characters) -> void override;
auto on_cdata_block(const std::string& characters) -> void override;
};
constexpr auto GetCharsRangeFromBuf(auto&& buf) {
return buf.data() //
| std::views::transform([](const auto& buf) -> std::string_view { //
return ::larra::xmpp::impl::BufferToStringView(buf); //
}) //
| std::views::join;
};
constexpr auto SplitStreamBuf(auto&& buf, char delim) {
return GetCharsRangeFromBuf(buf) //
| std::views::lazy_split(delim); //
};
auto GetIndex(const boost::asio::streambuf&, const _xmlError* error, std::size_t alreadyCountedLines = 1) -> std::size_t;
auto CountLines(const boost::asio::streambuf&) -> std::size_t;
auto CountLines(std::string_view) -> std::size_t;
auto IsExtraContentAtTheDocument(const _xmlError* error) -> bool;
} // namespace impl
template <typename Stream, typename BufferType = boost::asio::streambuf>
struct RawXmlStream : Stream {
constexpr RawXmlStream(Stream stream, std::unique_ptr<BufferType> buff = std::make_unique<BufferType>()) :
Stream(std::forward<Stream>(stream)), streambuf(std::move(buff)) {};
using Stream::Stream;
auto next_layer() -> Stream& {
return *this;
}
auto next_layer() const -> const Stream& {
return *this;
}
inline auto Read(auto& socket) -> boost::asio::awaitable<std::unique_ptr<xmlpp::Document>> {
auto doc = std::make_unique<xmlpp::Document>(); // Not movable :(
impl::Parser parser(*doc);
std::size_t lines = 1;
std::size_t size{};
for(auto elem : this->streambuf->data()) {
auto error = parser.ParseChunk(impl::BufferToStringView(elem));
if(!error) {
auto linesAdd = impl::CountLines(impl::BufferToStringView(elem));
lines += linesAdd;
if(linesAdd == 0) {
size += elem.size();
}
if(parser.context.empty() && parser.doc.get_root_node() != nullptr) {
SPDLOG_DEBUG("Object already transferred");
co_return doc;
}
continue;
}
if(!impl::IsExtraContentAtTheDocument(error)) {
throw std::runtime_error(std::format("Bad xml object: {}", xmlpp::format_xml_error(error)));
}
std::size_t size = impl::GetIndex(*this->streambuf, error, lines) - size;
this->streambuf->consume(size);
SPDLOG_DEBUG("Object already transferred");
co_return doc;
}
this->streambuf->consume(this->streambuf->size());
for(;;) {
auto buff = this->streambuf->prepare(4096); // NOLINT
auto [e, n] = co_await socket.async_read_some(buff, boost::asio::as_tuple(boost::asio::use_awaitable));
if(e) {
boost::system::throw_exception_from_error(e, boost::source_location());
}
this->streambuf->commit(n);
auto error = parser.ParseChunk(impl::BufferToStringView(buff, n));
if(!error) {
auto linesAdd = impl::CountLines(impl::BufferToStringView(buff, n));
SPDLOG_DEBUG("Readed {} bytes for RawXmlStream with {} lines", n, linesAdd);
lines += linesAdd;
if(linesAdd == 0) {
size += n;
}
this->streambuf->consume(this->streambuf->size());
if(parser.context.empty() && parser.doc.get_root_node() != nullptr) {
co_return doc;
}
SPDLOG_DEBUG(
"Object not transferred. context size: {}, isValidRootNode: {}", parser.context.size(), parser.doc.get_root_node() != nullptr);
continue;
}
if(!impl::IsExtraContentAtTheDocument(error)) {
throw std::runtime_error(std::format("Bad xml object: {}", xmlpp::format_xml_error(error)));
}
auto toConsume = impl::GetIndex(*this->streambuf, error, lines) - size;
this->streambuf->consume(toConsume);
co_return doc;
}
}
auto Read() -> boost::asio::awaitable<std::unique_ptr<xmlpp::Document>> {
co_return co_await this->Read(this->next_layer());
}
template <typename T>
auto Read(auto& stream) -> boost::asio::awaitable<T> {
auto doc = co_await this->Read(stream);
co_return T::Parse(doc->get_root_node());
}
template <typename T>
auto Read(auto& stream) -> boost::asio::awaitable<T>
requires requires(std::unique_ptr<xmlpp::Document> ptr) {
{ T::Parse(std::move(ptr)) } -> std::same_as<T>;
}
{
co_return T::Parse(co_await this->Read(stream));
}
template <typename T>
auto Read() -> boost::asio::awaitable<T> {
co_return co_await this->template Read<T>(this->next_layer());
}
auto Send(xmlpp::Document& doc, auto& stream, bool bAddXmlDecl, bool removeEnd) const -> boost::asio::awaitable<void> {
constexpr auto beginSize = sizeof("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n") - 1;
auto str = doc.write_to_string();
auto view = std::string_view{str}.substr(beginSize, str.size() - beginSize - 1);
if(bAddXmlDecl) {
if(removeEnd) {
std::string data = "<?xml version=\"1.0\"?>" + static_cast<std::string>(view.substr(0, view.size() - 2)) + ">";
co_await boost::asio::async_write(stream, boost::asio::buffer(data), boost::asio::use_awaitable);
co_return;
}
std::string data = "<?xml version=\"1.0\"?>" + static_cast<std::string>(view);
co_await boost::asio::async_write(stream, boost::asio::buffer(data), boost::asio::use_awaitable);
co_return;
}
if(removeEnd) {
std::string data = static_cast<std::string>(view.substr(0, view.size() - 2)) + ">";
co_await boost::asio::async_write(stream, boost::asio::buffer(data), boost::asio::use_awaitable);
} else {
co_await boost::asio::async_write(stream, boost::asio::buffer(view), boost::asio::use_awaitable);
}
}
auto Send(xmlpp::Document& doc, bool bAddXmlDecl = false) -> boost::asio::awaitable<void> {
co_await this->Send(doc, this->next_layer(), bAddXmlDecl);
}
template <AsXml T>
auto Send(const T& xso, auto& stream) const -> boost::asio::awaitable<void> {
xmlpp::Document doc;
const std::string empty;
const std::string namespaceStr = [&] -> std::string {
if constexpr(HasDefaultNamespace<T>) {
return T::kDefaultNamespace;
} else {
return empty;
}
}();
const std::string prefixStr = [&] -> decltype(auto) {
if constexpr(HasDefaultPrefix<T>) {
return T::kPrefix;
} else {
return empty;
}
}();
const bool bAddXmlDecl = [&] -> bool {
if constexpr(HasAddXmlDecl<T>) {
return T::kAddXmlDecl;
}
return false;
}();
const bool removeEnd = [&] -> bool {
if constexpr(HasRemoveEnd<T>) {
return T::kRemoveEnd;
}
return false;
}();
doc.create_root_node(T::kDefaultName, namespaceStr, prefixStr) << xso;
co_await this->Send(doc, stream, bAddXmlDecl, removeEnd);
}
auto Send(const AsXml auto& xso) -> boost::asio::awaitable<void> {
co_await this->Send(xso, this->next_layer());
}
RawXmlStream(RawXmlStream&& other) = default;
std::unique_ptr<BufferType> streambuf; // Not movable :(
};
} // namespace larra::xmpp

View file

@ -1,4 +1,6 @@
#pragma once #pragma once
#include <libxml++/libxml++.h>
#include <larra/jid.hpp> #include <larra/jid.hpp>
#include <pugixml.hpp> #include <pugixml.hpp>
@ -12,6 +14,11 @@ struct BasicStream {
static constexpr bool kJidTo = JidTo; static constexpr bool kJidTo = JidTo;
using FromType = std::optional<std::conditional_t<JidFrom, BareJid, std::string>>; using FromType = std::optional<std::conditional_t<JidFrom, BareJid, std::string>>;
using ToType = std::optional<std::conditional_t<JidTo, BareJid, std::string>>; using ToType = std::optional<std::conditional_t<JidTo, BareJid, std::string>>;
static inline const std::string kDefaultNamespace = JidFrom || JidTo ? "jabber:client" : "jabber:server";
static constexpr auto kRemoveEnd = true;
static constexpr auto kAddXmlDecl = true;
static inline const std::string kDefaultPrefix = "";
static inline const std::string kDefaultName = "stream:stream";
FromType from; FromType from;
ToType to; ToType to;
std::optional<std::string> id; std::optional<std::string> id;
@ -19,37 +26,39 @@ struct BasicStream {
std::optional<std::string> xmlLang; std::optional<std::string> xmlLang;
template <typename Self> template <typename Self>
constexpr auto From(this Self&& self, FromType value) -> BasicStream { [[nodiscard]] constexpr auto From(this Self&& self, FromType value) -> BasicStream {
return utils::FieldSetHelper::With<"from", BasicStream>(std::forward<Self>(self), std::move(value)); return utils::FieldSetHelper::With<"from", BasicStream>(std::forward<Self>(self), std::move(value));
} }
template <typename Self> template <typename Self>
constexpr auto To(this Self&& self, ToType value) -> BasicStream { [[nodiscard]] constexpr auto To(this Self&& self, ToType value) -> BasicStream {
return utils::FieldSetHelper::With<"to", BasicStream>(std::forward<Self>(self), std::move(value)); return utils::FieldSetHelper::With<"to", BasicStream>(std::forward<Self>(self), std::move(value));
} }
template <typename Self> template <typename Self>
constexpr auto Id(this Self&& self, std::optional<std::string> value) -> BasicStream { [[nodiscard]] constexpr auto Id(this Self&& self, std::optional<std::string> value) -> BasicStream {
return utils::FieldSetHelper::With<"id", BasicStream>(std::forward<Self>(self), std::move(value)); return utils::FieldSetHelper::With<"id", BasicStream>(std::forward<Self>(self), std::move(value));
} }
template <typename Self> template <typename Self>
constexpr auto Version(this Self&& self, std::optional<std::string> value) -> BasicStream { [[nodiscard]] constexpr auto Version(this Self&& self, std::optional<std::string> value) -> BasicStream {
return utils::FieldSetHelper::With<"version", BasicStream>(std::forward<Self>(self), std::move(value)); return utils::FieldSetHelper::With<"version", BasicStream>(std::forward<Self>(self), std::move(value));
} }
template <typename Self> template <typename Self>
constexpr auto XmlLang(this Self&& self, std::optional<std::string> value) -> BasicStream { [[nodiscard]] constexpr auto XmlLang(this Self&& self, std::optional<std::string> value) -> BasicStream {
return utils::FieldSetHelper::With<"xmlLang", BasicStream>(std::forward_like<Self>(self), std::move(value)); return utils::FieldSetHelper::With<"xmlLang", BasicStream>(std::forward_like<Self>(self), std::move(value));
} }
auto SerializeStream(pugi::xml_node& node) const -> void; auto SerializeStream(xmlpp::Element* node) const -> void;
friend auto operator<<(pugi::xml_node node, const BasicStream& stream) -> pugi::xml_node {
friend auto operator<<(xmlpp::Element* node, const BasicStream& stream) -> xmlpp::Element* {
stream.SerializeStream(node); stream.SerializeStream(node);
return (std::move(node)); return node;
} }
friend auto ToString(const BasicStream<JidFrom, JidTo>&) -> std::string; friend auto ToString(const BasicStream<JidFrom, JidTo>&) -> std::string;
static auto Parse(const pugi::xml_node& node) -> BasicStream<JidFrom, JidTo>; static auto Parse(const pugi::xml_node& node) -> BasicStream<JidFrom, JidTo>;
[[nodiscard]] static auto Parse(const xmlpp::Element*) -> BasicStream;
}; };
} // namespace impl } // namespace impl

View file

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <boost/pfr.hpp> #include <boost/pfr.hpp>
#include <ranges>
#include <utempl/utils.hpp> #include <utempl/utils.hpp>
namespace larra::xmpp::utils { namespace larra::xmpp::utils {
@ -215,4 +216,53 @@ struct FieldSetHelper {
// clang-format on // clang-format on
}; };
/*
template <typename Range, typename Delim>
SplitView(Range&& range, Delim&& delim) -> SplitView<std::views::all_t<Range>, Delim>;
*/
#if __has_cpp_attribute(__cpp_lib_start_lifetime_as)
template <typename T>
inline auto StartLifetimeAsArray(void* ptr, std::size_t n) -> T* {
return std::start_lifetime_as_array<T>(ptr, n);
}
template <typename T>
inline auto StartLifetimeAs(void* ptr) -> T* {
return std::start_lifetime_as<T>(ptr);
}
template <typename T>
inline auto StartLifetimeAsArray(const void* ptr, std::size_t n) -> const T* {
return std::start_lifetime_as_array<T>(ptr, n);
}
template <typename T>
inline auto StartLifetimeAs(const void* ptr) -> const T* {
return std::start_lifetime_as<T>(ptr);
}
#else
template <typename T>
inline auto StartLifetimeAsArray(void* ptr, std::size_t n) -> T* {
return std::launder(reinterpret_cast<T*>(new(ptr) std::byte[n * sizeof(T)]));
}
template <typename T>
inline auto StartLifetimeAs(void* ptr) -> T* {
return StartLifetimeAsArray<T>(ptr, 1);
}
template <typename T>
inline auto StartLifetimeAsArray(const void* ptr, std::size_t n) -> const T* {
return std::launder(reinterpret_cast<const T*>(new(const_cast<void*>(ptr)) std::byte[n * sizeof(T)])); // NOLINT
}
template <typename T>
inline auto StartLifetimeAs(const void* ptr) -> const T* {
return StartLifetimeAsArray<T>(ptr, 1);
}
#endif
} // namespace larra::xmpp::utils } // namespace larra::xmpp::utils

View file

@ -3,43 +3,51 @@
namespace { namespace {
template <typename T> template <typename T>
inline auto ToOptional(const pugi::xml_node& node) -> std::optional<T> { inline auto ToOptional(const xmlpp::Node* node) -> std::optional<T> {
return node ? std::optional{T::Parse(node)} : std::nullopt; auto ptr = dynamic_cast<const xmlpp::Element*>(node);
return ptr ? std::optional{T::Parse(ptr)} : std::nullopt;
} }
} // namespace } // namespace
namespace larra::xmpp { namespace larra::xmpp {
auto SaslMechanisms::Parse(pugi::xml_node node) -> SaslMechanisms { auto SaslMechanisms::Parse(const xmlpp::Element* node) -> SaslMechanisms {
std::vector<std::string> response; return {node->get_children("mechanism") | std::views::transform([](const xmlpp::Node* node) -> std::string {
for(pugi::xml_node mechanism = node.child("mechanism"); mechanism; mechanism = mechanism.next_sibling("mechanism")) { auto ptr = dynamic_cast<const xmlpp::Element*>(node);
response.emplace_back(mechanism.child_value()); if(!ptr) {
throw std::runtime_error("Invalid node for mechanisms");
} }
return {response}; if(!ptr->has_child_text()) {
throw std::runtime_error("Invalid node for mechanisms");
}
return ptr->get_first_child_text()->get_content();
}) |
std::ranges::to<std::vector<std::string>>()};
} }
auto StreamFeatures::StartTlsType::Parse(pugi::xml_node node) -> StreamFeatures::StartTlsType { auto StreamFeatures::StartTlsType::Parse(const xmlpp::Element* node) -> StreamFeatures::StartTlsType {
return {node.child("required") ? Required::kRequired : Required::kNotRequired}; return {node->get_first_child("required") ? Required::kRequired : Required::kNotRequired};
} }
auto StreamFeatures::BindType::Parse(pugi::xml_node node) -> StreamFeatures::BindType { auto StreamFeatures::BindType::Parse(const xmlpp::Element* node) -> StreamFeatures::BindType {
return {node.child("required") ? Required::kRequired : Required::kNotRequired}; return {node->get_first_child("required") ? Required::kRequired : Required::kNotRequired};
} }
auto StreamFeatures::Parse(pugi::xml_node node) -> StreamFeatures { auto StreamFeatures::Parse(const xmlpp::Element* node) -> StreamFeatures {
std::vector<pugi::xml_node> others; auto ptr = dynamic_cast<const xmlpp::Element*>(node->get_first_child("mechanisms"));
for(pugi::xml_node current = node.first_child(); current; current = current.next_sibling()) { if(!ptr) {
// Проверяем, не является ли узел starttls, bind или mechanisms throw std::runtime_error("Not found or invalid node mechanisms for StreamFeatures");
if(std::string_view(current.name()) != "starttls" && std::string_view(current.name()) != "bind" &&
std::string_view(current.name()) != "mechanisms") {
others.push_back(node);
} }
}
return {ToOptional<StartTlsType>(node.child("starttls")), return {.startTls = ToOptional<StartTlsType>(node->get_first_child("starttls")),
ToOptional<BindType>(node.child("bind")), .bind = ToOptional<BindType>(node->get_first_child("bind")),
SaslMechanisms::Parse(node.child("mechanisms")), .saslMechanisms = SaslMechanisms::Parse(ptr),
std::move(others)}; .others = node->get_children() | std::views::filter([](const xmlpp::Node* node) -> bool {
auto name = node->get_name();
return name != "starttls" && name != "mechanisms" && name != "bind";
}) |
std::ranges::to<std::vector<const xmlpp::Node*>>()};
} }
} // namespace larra::xmpp } // namespace larra::xmpp

View file

@ -0,0 +1,133 @@
#include <libxml/parser.h>
#include <larra/impl/public_cast.hpp>
#include <larra/raw_xml_stream.hpp>
#include <larra/utils.hpp>
#include <ranges>
#include <span>
namespace larra::xmpp::impl {
template struct PublicCast<&xmlpp::SaxParser::sax_handler_>;
auto Parser::ParseChunk(std::string_view str) -> const xmlError* {
xmlResetLastError();
if(!context_) {
this->context_ = xmlCreatePushParserCtxt((this->*GetPrivateMember(static_cast<xmlpp::SaxParser&>(*this))).get(),
nullptr, // user_data
nullptr, // chunk
0, // size
nullptr); // no filename for fetching external entities
if(!this->context_) {
throw xmlpp::internal_error("Could not create parser context\n" + xmlpp::format_xml_error());
}
initialize_context();
} else {
xmlCtxtResetLastError(this->context_);
}
xmlParseChunk(this->context_, str.data(), static_cast<int>(str.size()), 0);
return xmlCtxtGetLastError(this->context_);
}
auto Parser::on_start_element(const std::string& name, const AttributeList& attributes) -> void {
SPDLOG_DEBUG("Start element with name {}", name);
std::string::size_type idx = name.find(':');
std::string elementPrefix = idx == std::string::npos ? std::string{} : name.substr(0, idx);
xmlpp::Element* elementNormal = nullptr;
if(this->doc.get_root_node() == nullptr) {
elementNormal = this->doc.create_root_node(name);
} else {
elementNormal = this->context.top()->add_child_element(name);
}
auto node = elementNormal->cobj();
delete elementNormal; // NOLINT: Api
elementNormal = nullptr;
xmlpp::Element* elementDerived = nullptr;
if(name == "g") {
elementDerived = new XmlGroup(node); // NOLINT: Owned
} else if(name == "path") {
elementDerived = new XmlPath(node); // NOLINT: Owned
} else {
elementDerived = new xmlpp::Element(node); // NOLINT: Owned
}
if(elementDerived) {
this->context.push(elementDerived);
for(const auto& attr_pair : attributes) {
const auto attr_name = attr_pair.name;
const auto attr_value = attr_pair.value;
const auto idx_colon = attr_name.find(':');
if(idx_colon == std::string::npos) {
if(attr_name == "xmlns") {
elementDerived->set_namespace_declaration(attr_value);
} else {
elementDerived->set_attribute(attr_name, attr_value);
}
} else {
auto prefix = attr_name.substr(0, idx_colon);
auto suffix = attr_name.substr(idx_colon + 1);
if(prefix == "xmlns") {
elementDerived->set_namespace_declaration(attr_value, suffix);
} else {
auto attr = elementDerived->set_attribute(suffix, attr_value);
attr->set_namespace(prefix);
}
}
}
}
}
auto Parser::on_end_element(const std::string& name) -> void {
SPDLOG_DEBUG("End element with name {}", name);
this->context.pop();
}
auto Parser::on_characters(const std::string& text) -> void {
SPDLOG_DEBUG("Add characters to element: {}", text);
if(!this->context.empty()) {
this->context.top()->add_child_text(text);
}
}
auto Parser::on_cdata_block(const std::string& text) -> void {
this->on_characters(text);
}
inline auto GetLines(const boost::asio::streambuf& buf) {
return SplitStreamBuf(buf, '\n');
}
auto CountLines(const boost::asio::streambuf& buf) -> std::size_t {
return std::ranges::fold_left(GetLines(buf), 0, [](auto accum, auto&&) {
return accum + 1;
});
};
auto CountLines(std::string_view buf) -> std::size_t {
return std::ranges::fold_left(buf | std::views::split('\n'), 0, [](auto accum, auto&&) {
return accum + 1;
});
}
auto GetIndex(const boost::asio::streambuf& buf, const xmlError* error, std::size_t alreadyCountedLines) -> std::size_t {
return std::ranges::fold_left(
GetLines(buf) | std::views::take(error->line - alreadyCountedLines) | std::views::transform([](auto&& line) -> std::size_t {
return std::ranges::fold_left(line, std::size_t{1}, [](auto accum, auto&&) {
return accum + 1;
});
}),
error->int2 - 1, // columns
std::plus<>{});
}
auto IsExtraContentAtTheDocument(const xmlError* error) -> bool {
return error->code == XML_ERR_DOCUMENT_END;
}
} // namespace larra::xmpp::impl

View file

@ -7,6 +7,10 @@ inline auto ToOptionalString(const pugi::xml_attribute& attribute) -> std::optio
return attribute ? std::optional{std::string{attribute.as_string()}} : std::nullopt; return attribute ? std::optional{std::string{attribute.as_string()}} : std::nullopt;
} }
inline auto ToOptionalString(const xmlpp::Attribute* attribute) -> std::optional<std::string> {
return attribute ? std::optional{std::string{attribute->get_value()}} : std::nullopt;
}
template <bool IsJid> template <bool IsJid>
inline auto ToOptionalUser(const pugi::xml_attribute& attribute) { inline auto ToOptionalUser(const pugi::xml_attribute& attribute) {
if constexpr(IsJid) { if constexpr(IsJid) {
@ -16,6 +20,15 @@ inline auto ToOptionalUser(const pugi::xml_attribute& attribute) {
} }
} }
template <bool IsJid>
inline auto ToOptionalUser(const xmlpp::Attribute* attribute) {
if constexpr(IsJid) {
return attribute ? std::optional{larra::xmpp::BareJid::Parse(attribute->get_value())} : std::nullopt;
} else {
return ToOptionalString(attribute);
}
}
auto ToString(std::string data) -> std::string { auto ToString(std::string data) -> std::string {
return std::move(data); return std::move(data);
}; };
@ -25,60 +38,28 @@ auto ToString(std::string data) -> std::string {
namespace larra::xmpp { namespace larra::xmpp {
template <bool JidFrom, bool JidTo> template <bool JidFrom, bool JidTo>
auto impl::BasicStream<JidFrom, JidTo>::SerializeStream(pugi::xml_node& node) const -> void { auto impl::BasicStream<JidFrom, JidTo>::SerializeStream(xmlpp::Element* node) const -> void {
if(this->from) { if(this->from) {
node.append_attribute("from") = ToString(*this->from).c_str(); node->set_attribute("from", ToString(*this->from));
} }
if(this->to) { if(this->to) {
node.append_attribute("to") = ToString(*this->to).c_str(); node->set_attribute("to", ToString(*this->to));
} }
if(this->id) { if(this->id) {
node.append_attribute("id") = this->id->c_str(); node->set_attribute("id", *this->id);
} }
if(this->version) { if(this->version) {
node.append_attribute("version") = this->version->c_str(); node->set_attribute("version", *this->version);
} }
if(this->xmlLang) { if(this->xmlLang) {
node.append_attribute("xml:lang") = this->xmlLang->c_str(); node->set_attribute("lang", *this->xmlLang, "xml");
} }
if constexpr(JidFrom || JidTo) { node->set_namespace_declaration("http://etherx.jabber.org/streams", "stream");
node.append_attribute("xmlns") = "jabber:client";
} else {
node.append_attribute("xmlns") = "jabber:server";
}
node.append_attribute("xmlns:stream") = "http://etherx.jabber.org/streams";
} }
template auto ServerStream::SerializeStream(pugi::xml_node& node) const -> void; template auto ServerStream::SerializeStream(xmlpp::Element* node) const -> void;
template auto ServerToUserStream::SerializeStream(pugi::xml_node& node) const -> void; template auto ServerToUserStream::SerializeStream(xmlpp::Element* node) const -> void;
template auto UserStream::SerializeStream(pugi::xml_node& node) const -> void; template auto UserStream::SerializeStream(xmlpp::Element* node) const -> void;
namespace impl {
template <bool JidFrom, bool JidTo>
inline auto ToStringHelper(const BasicStream<JidFrom, JidTo>& stream) {
return std::format("<stream:stream{}{}{}{}{}{} xmlns:stream='http://etherx.jabber.org/streams'>",
stream.id ? std::format(" id='{}'", *stream.id) : "",
stream.from ? std::format(" from='{}'", *stream.from) : "",
stream.to ? std::format(" to='{}'", *stream.to) : "",
stream.version ? std::format(" version='{}'", *stream.version) : "",
stream.xmlLang ? std::format(" xml:lang='{}'", *stream.xmlLang) : "",
JidFrom || JidTo ? " xmlns='jabber:client'" : "xmlns='jabber:server'");
};
auto ToString(const ServerStream& ref) -> std::string {
return ToStringHelper(ref);
}
auto ToString(const UserStream& ref) -> std::string {
return ToStringHelper(ref);
}
auto ToString(const ServerToUserStream& ref) -> std::string {
return ToStringHelper(ref);
}
} // namespace impl
template <bool JidFrom, bool JidTo> template <bool JidFrom, bool JidTo>
auto impl::BasicStream<JidFrom, JidTo>::Parse(const pugi::xml_node& node) -> impl::BasicStream<JidFrom, JidTo> { auto impl::BasicStream<JidFrom, JidTo>::Parse(const pugi::xml_node& node) -> impl::BasicStream<JidFrom, JidTo> {
@ -88,8 +69,24 @@ auto impl::BasicStream<JidFrom, JidTo>::Parse(const pugi::xml_node& node) -> imp
ToOptionalString(node.attribute("version")), ToOptionalString(node.attribute("version")),
ToOptionalString(node.attribute("xml:lang"))}; ToOptionalString(node.attribute("xml:lang"))};
} }
template <bool JidFrom, bool JidTo>
auto impl::BasicStream<JidFrom, JidTo>::Parse(const xmlpp::Element* node) -> impl::BasicStream<JidFrom, JidTo> {
return {ToOptionalUser<JidFrom>(node->get_attribute("from")),
ToOptionalUser<JidTo>(node->get_attribute("to")),
ToOptionalString(node->get_attribute("id")),
ToOptionalString(node->get_attribute("version")),
ToOptionalString(node->get_attribute("lang", "xml"))};
}
template auto UserStream::Parse(const pugi::xml_node& node) -> UserStream; template auto UserStream::Parse(const pugi::xml_node& node) -> UserStream;
template auto ServerStream::Parse(const pugi::xml_node& node) -> ServerStream; template auto ServerStream::Parse(const pugi::xml_node& node) -> ServerStream;
template auto ServerToUserStream::Parse(const pugi::xml_node& node) -> ServerToUserStream; template auto ServerToUserStream::Parse(const pugi::xml_node& node) -> ServerToUserStream;
template auto UserStream::Parse(const xmlpp::Element* node) -> UserStream;
template auto ServerStream::Parse(const xmlpp::Element* node) -> ServerStream;
template auto ServerToUserStream::Parse(const xmlpp::Element* node) -> ServerToUserStream;
} // namespace larra::xmpp } // namespace larra::xmpp

159
tests/raw_xml_stream.cpp Normal file
View file

@ -0,0 +1,159 @@
#include <gtest/gtest.h>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <larra/features.hpp>
#include <larra/impl/mock_socket.hpp>
#include <larra/raw_xml_stream.hpp>
#include <utempl/utils.hpp>
namespace larra::xmpp {
constexpr std::string_view kDoc = "<doc></doc>\n<doc2></doc2>\n<doc3/>";
constexpr std::string_view kDoc2 = "<doc></doc>\n<doc2></doc2>";
constexpr std::string_view kDoc3 =
"<stream:features><mechanisms "
"xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><mechanism>PLAIN</mechanism><mechanism>SCRAM-SHA-256</mechanism><mechanism>X-OAUTH2</"
"mechanism></mechanisms><register xmlns='http://jabber.org/features/iq-register'/></stream:features>";
TEST(RawXmlStream, ReadByOne) {
boost::asio::io_context context;
bool error{};
boost::asio::co_spawn(
context,
// NOLINTNEXTLINE: Safe
[&] -> boost::asio::awaitable<void> {
RawXmlStream stream{impl::MockSocket{context.get_executor(), 1}};
stream.AddReceivedData(kDoc);
try {
auto doc = co_await stream.Read();
auto node = doc->get_root_node();
EXPECT_EQ(node->get_name(), std::string_view{"doc"});
EXPECT_FALSE(node->has_child_text());
auto doc2 = co_await stream.Read();
auto node2 = doc2->get_root_node();
EXPECT_EQ(node2->get_name(), std::string_view{"doc2"});
EXPECT_FALSE(node2->has_child_text());
} catch(const std::exception& err) {
SPDLOG_ERROR("{}", err.what());
error = true;
}
},
boost::asio::detached);
context.run();
EXPECT_FALSE(error);
}
TEST(RawXmlStream, ReadAll) {
boost::asio::io_context context;
bool error{};
boost::asio::co_spawn(
context, // NOLINTNEXTLINE: Safe
[&] -> boost::asio::awaitable<void> {
RawXmlStream stream{impl::MockSocket{context.get_executor(), kDoc.size()}};
stream.AddReceivedData(kDoc);
try {
auto doc = co_await stream.Read();
auto node = doc->get_root_node();
EXPECT_EQ(node->get_name(), std::string_view{"doc"});
EXPECT_FALSE(node->has_child_text());
auto doc2 = co_await stream.Read();
auto node2 = doc2->get_root_node();
EXPECT_EQ(node2->get_name(), std::string_view{"doc2"});
EXPECT_FALSE(node2->has_child_text());
} catch(const std::exception& err) {
SPDLOG_ERROR("{}", err.what());
error = true;
}
},
boost::asio::detached);
context.run();
EXPECT_FALSE(error);
}
TEST(RawXmlStream, ReadAllWithEnd) {
boost::asio::io_context context;
bool error{};
boost::asio::co_spawn(
context, // NOLINTNEXTLINE: Safe
[&] -> boost::asio::awaitable<void> {
RawXmlStream stream{impl::MockSocket{context.get_executor(), kDoc2.size()}};
stream.AddReceivedData(kDoc2);
try {
auto doc = co_await stream.Read();
auto node = doc->get_root_node();
EXPECT_EQ(node->get_name(), std::string_view{"doc"});
EXPECT_FALSE(node->has_child_text());
auto doc2 = co_await stream.Read();
auto node2 = doc2->get_root_node();
EXPECT_EQ(node2->get_name(), std::string_view{"doc2"});
EXPECT_FALSE(node2->has_child_text());
} catch(const std::exception& err) {
SPDLOG_ERROR("{}", err.what());
error = true;
}
},
boost::asio::detached);
context.run();
EXPECT_FALSE(error);
}
TEST(RawXmlStream, ReadFeatures) {
boost::asio::io_context context;
bool error{};
boost::asio::co_spawn(
context, // NOLINTNEXTLINE: Safe
[&] -> boost::asio::awaitable<void> {
RawXmlStream stream{impl::MockSocket{context.get_executor(), kDoc3.size()}};
stream.AddReceivedData(kDoc3);
try {
auto features = co_await stream.template Read<StreamFeatures>();
} catch(const std::exception& err) {
SPDLOG_ERROR("{}", err.what());
error = true;
}
},
boost::asio::detached);
context.run();
EXPECT_FALSE(error);
}
struct SomeStruct {
static constexpr auto kDefaultName = "some";
static constexpr auto kDefaultNamespace = "namespace";
static constexpr auto kPrefix = "prefix";
friend auto operator<<(xmlpp::Element* node, const SomeStruct&) {
node->add_child_text("text");
}
};
TEST(RawXmlStream, Write) {
boost::asio::io_context context;
bool error{};
boost::asio::co_spawn(
context, // NOLINTNEXTLINE: Safe
[&] -> boost::asio::awaitable<void> {
RawXmlStream stream1{impl::MockSocket{context.get_executor()}};
auto stream = std::move(stream1);
try {
co_await stream.Send(SomeStruct{});
EXPECT_EQ(stream.GetSentData(), std::string_view{"<prefix:some xmlns:prefix=\"namespace\">text</prefix:some>"});
} catch(const std::exception& err) {
SPDLOG_ERROR("{}", err.what());
error = true;
}
},
boost::asio::detached);
context.run();
EXPECT_FALSE(error);
}
} // namespace larra::xmpp

View file

@ -9,33 +9,31 @@ constexpr std::string_view kSerializedData =
)"; )";
constexpr std::string_view kCheckSerializeData = constexpr std::string_view kCheckSerializeData =
R"(<stream:stream from="user@example.com" to="example.com" id="abc" version="1.0" xml:lang="en" xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams" /> "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<stream:stream xmlns=\"jabber:client\" xmlns:stream=\"http://etherx.jabber.org/streams\" "
)"; "from=\"user@example.com\" to=\"example.com\" id=\"abc\" version=\"1.0\" xml:lang=\"en\"/>\n";
TEST(Stream, Serialize) { TEST(Stream, Serialize) {
UserStream originalStream; UserStream originalStream;
originalStream.from = BareJid{"user", "example.com"}; originalStream.from = BareJid{.username = "user", .server = "example.com"};
originalStream.to = "example.com"; originalStream.to = "example.com";
originalStream.id = "abc"; originalStream.id = "abc";
originalStream.version = "1.0"; originalStream.version = "1.0";
originalStream.xmlLang = "en"; originalStream.xmlLang = "en";
xmlpp::Document doc;
pugi::xml_document doc; auto streamNode = doc.create_root_node(UserStream::kDefaultName, UserStream::kDefaultNamespace, UserStream::kDefaultPrefix);
pugi::xml_node streamNode = doc.append_child("stream:stream");
streamNode << originalStream; streamNode << originalStream;
std::ostringstream oss; const std::string serializedData = doc.write_to_string();
doc.child("stream:stream").print(oss, "\t");
const std::string serializedData = oss.str();
ASSERT_EQ(serializedData, kCheckSerializeData); ASSERT_EQ(serializedData, kCheckSerializeData);
} }
TEST(Stream, Deserialize) { TEST(Stream, Deserialize) {
pugi::xml_document parsedDoc; xmlpp::DomParser parser;
parsedDoc.load_string(kSerializedData.data()); parser.parse_memory(static_cast<std::string>(kSerializedData));
auto parsedDoc = parser.get_document();
const UserStream deserializedStream = UserStream::Parse(parsedDoc.child("stream:stream")); const UserStream deserializedStream = UserStream::Parse(parsedDoc->get_root_node());
ASSERT_TRUE(deserializedStream.from.has_value()); ASSERT_TRUE(deserializedStream.from.has_value());
ASSERT_EQ(ToString(*deserializedStream.from), "user@example.com"); ASSERT_EQ(ToString(*deserializedStream.from), "user@example.com");