Initial client support

This commit is contained in:
sha512sum 2024-09-03 15:36:08 +00:00
parent 540f3ad68d
commit 3b78412da4
13 changed files with 901 additions and 68 deletions

View file

@ -11,7 +11,7 @@ set(CMAKE_CXX_EXTENSIONS OFF)
set(FMT_MODULE OFF)
set(UTEMPL_MODULE OFF)
set(CXX_EXTENSIONS NO)
set(BOOST_INCLUDE_LIBRARIES "pfr;asio")
set(BOOST_INCLUDE_LIBRARIES "pfr;asio;serialization")
option(CPM_USE_LOCAL_PACKAGES "Use local packages" ON)
option(UTEMPL_USE_LOCAL_PACKAGE "Use utempl local package" OFF)
set(UTEMPL_URL
@ -31,7 +31,9 @@ file(
)
include(${CMAKE_CURRENT_BINARY_DIR}/cmake/CPM.cmake)
find_package(PkgConfig)
find_package(OpenSSL REQUIRED)
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTKMM gtkmm-4.0)
@ -51,7 +53,7 @@ CPMAddPackage(
CPMAddPackage("gh:zeux/pugixml@1.14")
CPMAddPackage("gh:fmtlib/fmt#11.0.2")
CPMAddPackage("gh:fmtlib/fmt#10.2.1")
set(TMP ${CPM_USE_LOCAL_PACKAGES})
@ -99,11 +101,13 @@ install(TARGETS larra_xmpp
if(TARGET Boost::pfr)
target_link_libraries(larra_xmpp PUBLIC
Boost::asio utempl::utempl pugixml::pugixml)
Boost::asio utempl::utempl pugixml::pugixml OpenSSL::SSL
OpenSSL::Crypto)
else()
find_package(Boost 1.85.0 REQUIRED)
target_link_libraries(larra_xmpp PUBLIC
utempl::utempl ${Boost_LIBRARIES} pugixml::pugixml)
utempl::utempl ${Boost_LIBRARIES} pugixml::pugixml OpenSSL::SSL
OpenSSL::Crypto)
endif()
@ -167,3 +171,16 @@ if(ENABLE_TESTS)
include(GoogleTest)
gtest_discover_tests(larra_xmpp_tests)
endif()
if(ENABLE_EXAMPLES)
file(GLOB EXAMPLES_SRC "examples/src/*.cpp")
foreach(EXAMPLE_SRC ${EXAMPLES_SRC})
get_filename_component(EXAMPLE_NAME ${EXAMPLE_SRC} NAME_WE)
add_executable(${EXAMPLE_NAME} ${EXAMPLE_SRC})
target_link_libraries(${EXAMPLE_NAME} larra_xmpp)
set_property(TARGET ${EXAMPLE_NAME} PROPERTY CXX_STANDARD 23)
set_target_properties(${EXAMPLE_NAME} PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/examples/output")
endforeach()
endif()

23
examples/src/connect.cpp Normal file
View file

@ -0,0 +1,23 @@
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/detached.hpp>
#include <larra/client/client.hpp>
#include <larra/printer_stream.hpp>
#include <print>
auto Coroutine() -> boost::asio::awaitable<void> {
std::println("Connecting client...");
try {
auto client = co_await larra::xmpp::client::CreateClient<larra::xmpp::PrintStream<boost::asio::ip::tcp::socket>>(
larra::xmpp::EncryptionUserAccount{{"sha512sum", "localhost"}, "12345"}, {.useTls = larra::xmpp::client::Options::kNever});
} catch(const std::exception& err) {
std::println("Err: {}", err.what());
co_return;
}
std::println("Done!");
}
auto main() -> int {
boost::asio::io_context io_context;
boost::asio::co_spawn(io_context, Coroutine(), boost::asio::detached);
io_context.run();
}

View file

@ -0,0 +1,411 @@
#pragma once
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <openssl/rand.h>
#include <openssl/sha.h>
#include <boost/algorithm/string.hpp>
#include <boost/archive/iterators/base64_from_binary.hpp>
#include <boost/archive/iterators/binary_from_base64.hpp>
#include <boost/archive/iterators/transform_width.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/connect.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/read.hpp>
#include <boost/asio/read_until.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <charconv>
#include <larra/client/options.hpp>
#include <larra/features.hpp>
#include <larra/stream.hpp>
#include <larra/user_account.hpp>
#include <random>
#include <ranges>
namespace larra::xmpp {
constexpr auto kDefaultXmppPort = 5222;
} // namespace larra::xmpp
namespace larra::xmpp::client {
template <typename Connection>
struct Client {
Client(BareJid jid, Connection connection) : jid(std::move(jid)), connection(std::move(connection)) {};
const BareJid jid; // NOLINT: const
private:
Connection connection;
};
struct StartTlsNegotiationError : std::runtime_error {
inline StartTlsNegotiationError(std::string_view error) : std::runtime_error(std::format("STARTTLS negotiation error: {}", error)) {};
};
struct ServerRequiresStartTls : std::exception {
[[nodiscard]] auto what() const noexcept -> const char* override {
return "XMPP Server requires STARTTLS";
};
};
namespace impl {
auto DecodeBase64(std::string_view val) -> std::string {
using namespace boost::archive::iterators; // NOLINT
using It = transform_width<binary_from_base64<std::string_view::const_iterator>, 8, 6>; // NOLINT
return boost::algorithm::trim_right_copy_if(std::string(It(std::begin(val)), It(std::end(val))), [](char c) {
return c == '\0';
});
}
auto EncodeBase64(std::string_view val) -> std::string {
using namespace boost::archive::iterators; // NOLINT
using It = base64_from_binary<transform_width<std::string_view::const_iterator, 6, 8>>; // NOLINT
auto tmp = std::string(It(std::begin(val)), It(std::end(val)));
return tmp.append((3 - val.size() % 3) % 3, '=');
}
struct CharTrait {
using char_type = unsigned char;
static constexpr auto assign(char_type& c1, const char_type& c2) -> void {
c1 = c2;
}
static constexpr auto assign(char_type* c1, std::size_t n, const char_type c2) -> char_type* {
std::ranges::fill_n(c1, static_cast<std::iter_difference_t<unsigned char>>(n), c2);
return c1;
}
static constexpr auto copy(char_type* c1, const char_type* c2, std::size_t n) -> char_type* {
std::ranges::copy_n(c2, static_cast<std::iter_difference_t<unsigned char>>(n), c1);
return c1;
}
};
inline auto GenerateNonce(std::size_t length = 24) -> std::string { // NOLINT
constexpr std::string_view characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789";
std::random_device rd;
std::mt19937 generator(rd());
std::uniform_int_distribution<> distribution(0, characters.size() - 1);
std::ostringstream nonceStream;
for(size_t i = 0; i < length; ++i) {
nonceStream << characters[distribution(generator)];
}
return std::move(nonceStream.str());
}
template <auto F = EVP_sha512, std::size_t Size = 64> // NOLINT
inline auto Pbkdf2(std::string_view password,
std::basic_string_view<unsigned char, CharTrait> salt,
int iterations) -> std::array<unsigned char, Size> {
std::array<unsigned char, Size> key;
PKCS5_PBKDF2_HMAC(password.data(), password.length(), salt.data(), salt.size(), iterations, F(), 64, key.data()); // NOLINT
return key;
}
template <auto F = SHA512, std::size_t Size = 64>
inline auto SHA(std::basic_string_view<unsigned char, CharTrait> input) -> std::array<unsigned char, Size> {
std::array<unsigned char, Size> response;
F(input.data(), input.size(), response.data());
return response;
}
template <auto F = EVP_sha512, std::size_t Size = 64>
inline auto HMAC(std::string_view key,
std::basic_string_view<unsigned char, CharTrait> message) -> std::basic_string<unsigned char, CharTrait> {
unsigned char* result = HMAC(F(), key.data(), static_cast<int>(key.size()), message.data(), message.size(), nullptr, nullptr);
return {result, Size};
}
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>
auto Contains(Range&& range, Args&&... values) {
for(auto& value : range) {
if(((value == values) || ...)) {
return true;
}
}
return false;
}
inline auto ToInt(std::string_view input) -> std::optional<int> {
int 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};
}
auto ToCharStringView(std::ranges::range auto& str) -> std::string_view {
return {new(&*str.begin()) char[str.size()], str.size()};
}
inline auto ToUnsignedCharStringView(std::string& str) -> std::basic_string_view<unsigned char, CharTrait> {
return {new(str.data()) unsigned char[str.size()], str.size()};
}
auto Xor(auto str1, auto str2) -> auto {
if(str1.length() != str2.length()) {
throw std::invalid_argument("Strings must be of equal length for XOR.");
}
return std::views::iota(std::size_t{}, str1.size()) | std::views::transform([str1 = std::move(str1), str2 = std::move(str2)](auto i) {
return str1[i] ^ str2[i];
});
}
template <auto F = EVP_sha512, auto F2 = SHA512, std::size_t Size = 64>
auto GenerateAuthScramMessage(std::string_view password,
std::string salt,
std::string_view serverNonce,
std::string_view firstServerMessage,
std::string_view initialMessage,
int iterations) -> std::string {
auto clientFinalMessageBare = std::format("c=biws,r={}", serverNonce);
auto saltedPassword = Pbkdf2<F, Size>(password, ToUnsignedCharStringView(salt), iterations);
std::string clientKeyStr = "Client Key"; // NOLINT
auto clientKey = HMAC<F, Size>(ToCharStringView(saltedPassword), ToUnsignedCharStringView(clientKeyStr));
auto storedKey = SHA<F2, Size>(clientKey);
auto authMessage = std::format("{},{},{}", initialMessage, firstServerMessage, clientFinalMessageBare);
auto clientSignature = HMAC<F, Size>(ToCharStringView(storedKey), ToUnsignedCharStringView(authMessage));
auto clientProof = Xor(clientKey, clientSignature) | std::ranges::to<std::string>();
std::string serverKeyStr = "Server Key";
auto serverKey = HMAC<F, Size>(ToCharStringView(saltedPassword), ToUnsignedCharStringView(serverKeyStr));
auto serverSignature = HMAC<F, Size>(ToCharStringView(serverKey), ToUnsignedCharStringView(authMessage));
return std::format("{},p={}", clientFinalMessageBare, EncodeBase64(ToCharStringView(clientProof)));
}
inline auto ParseChallenge(std::string_view str) {
return std::views::split(str, ',') | 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>>();
};
inline auto GetAuthData(const PlainUserAccount& account) -> std::string {
return EncodeBase64('\0' + account.jid.username + '\0' + account.password);
}
struct ClientCreateVisitor {
UserAccount account;
const Options& options;
auto Auth(PlainUserAccount account, auto& socket, StreamFeatures features, ServerToUserStream stream) -> boost::asio::awaitable<void> {
if(!std::ranges::contains(features.saslMechanisms.mechanisms, "PLAIN")) {
throw std::runtime_error("Server not support PLAIN auth");
}
pugi::xml_document doc;
auto data = GetAuthData(account);
auto auth = doc.append_child("auth");
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);
}
template <auto F, auto F2, std::size_t Size>
auto ScramAuth(std::string_view methodName, const EncryptionUserAccount& account, auto& socket) -> boost::asio::awaitable<void> {
pugi::xml_document doc;
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();
auto initialMessage = std::format("n,,n={},r={}", account.jid.username, nonce);
auto data = EncodeBase64(initialMessage);
auth.text().set(data.c_str());
std::ostringstream strstream;
doc.print(strstream,
"",
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) {
throw std::runtime_error("XMPP Server SCRAM nonce not started with client nonce");
}
doc = pugi::xml_document{};
auto success = doc.append_child("response");
success.append_attribute("xmlns") = "urn:ietf:params:xml:ns:xmpp-sasl";
success.text().set(
EncodeBase64(GenerateAuthScramMessage<F, F2, Size>(
account.password, DecodeBase64(params["s"]), serverNonce, decoded, initialMessage, ToInt(params["i"]).value()))
.c_str());
std::ostringstream strstream2;
doc.print(strstream2,
"",
pugi::format_default | pugi::format_no_empty_element_tags | pugi::format_attribute_single_quote | pugi::format_raw |
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()));
}
}
auto Auth(EncryptionRequiredUserAccount account,
auto& socket,
StreamFeatures features,
ServerToUserStream stream) -> boost::asio::awaitable<void> {
// NOLINTBEGIN
if(std::ranges::contains(features.saslMechanisms.mechanisms, "SCRAM-SHA-512")) {
co_return co_await ScramAuth<EVP_sha512, SHA512, 64>("SCRAM-SHA-512", account, socket);
}
if(std::ranges::contains(features.saslMechanisms.mechanisms, "SCRAM-SHA-256")) {
co_return co_await ScramAuth<EVP_sha256, SHA256, 32>("SCRAM-SHA-256", account, socket);
}
if(std::ranges::contains(features.saslMechanisms.mechanisms, "SCRAM-SHA-1")) {
co_return co_await ScramAuth<EVP_sha1, SHA1, 20>("SCRAM-SHA-1", account, socket);
}
// NOLINTEND
throw std::runtime_error("Server not support SCRAM SHA 1 or SCRAM SHA 256 or SCRAM SHA 512 auth");
}
auto Auth(EncryptionUserAccount account,
auto& socket,
StreamFeatures features,
ServerToUserStream stream) -> boost::asio::awaitable<void> {
return Contains(features.saslMechanisms.mechanisms, "SCRAM-SHA-1", "SCRAM-SHA-256", "SCRAM-SHA-512")
? this->Auth(EncryptionRequiredUserAccount{std::move(account)}, socket, std::move(features), std::move(stream))
: 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> {
co_return co_await std::visit<boost::asio::awaitable<void>>(
[&](auto& account) -> boost::asio::awaitable<void> {
return this->Auth(std::move(account),
socket,
StreamFeatures::Parse(doc.child("stream:stream").child("stream:features")),
ServerToUserStream::Parse(doc.child("stream:stream")));
},
this->account);
}
auto Resolve() -> boost::asio::awaitable<boost::asio::ip::tcp::resolver::results_type> {
auto executor = co_await boost::asio::this_coro::executor;
boost::asio::ip::tcp::resolver resolver(executor);
co_return co_await resolver.async_resolve(this->options.hostname.value_or(account.Jid().server),
std::to_string(this->options.port.value_or(kDefaultXmppPort)),
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);
}
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>
auto ProcessTls(boost::asio::ssl::stream<Socket>& socket, std::string& buffer) -> boost::asio::awaitable<void> {
co_await boost::asio::async_write(socket.next_layer(),
boost::asio::buffer("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>"),
boost::asio::transfer_all(),
boost::asio::use_awaitable);
buffer.clear();
pugi::xml_document doc;
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"};
};
SSL_set_tlsext_host_name(socket.native_handle(), account.Jid().server.c_str());
try {
co_await socket.async_handshake(boost::asio::ssl::stream<Socket>::handshake_type::client, boost::asio::use_awaitable);
} catch(const std::exception& e) {
throw StartTlsNegotiationError{e.what()};
}
};
template <typename Socket>
inline auto operator()(Socket&& socket)
-> boost::asio::awaitable<std::variant<Client<std::decay_t<Socket>>, Client<boost::asio::ssl::stream<std::decay_t<Socket>>>>> {
co_await this->Connect(socket, co_await this->Resolve());
co_await impl::StartStream(account.Jid(), socket);
auto response = co_await ReadStream(socket);
pugi::xml_document doc;
doc.load_string(response.c_str());
auto streamNode = doc.child("stream:stream");
auto features = streamNode.child("stream:features");
if(features.child("starttls").child("required")) {
throw ServerRequiresStartTls{};
}
co_await this->Auth(socket, std::move(doc));
co_return Client{std::move(this->account).Jid(), std::move(socket)};
}
template <typename Socket>
inline auto operator()(boost::asio::ssl::stream<Socket>&& socket)
-> boost::asio::awaitable<std::variant<Client<Socket>, Client<boost::asio::ssl::stream<Socket>>>> {
co_await this->Connect(socket.next_layer(), co_await this->Resolve());
co_await impl::StartStream(account.Jid().Username("anonymous"), socket.next_layer());
auto response = co_await this->ReadStream(socket.next_layer());
pugi::xml_document doc;
doc.load_string(response.c_str());
auto streamNode = doc.child("stream:stream");
auto stream = ServerToUserStream::Parse(streamNode);
auto features = streamNode.child("stream:features");
if(!features.child("starttls")) {
if(this->options.useTls == Options::kRequire) {
throw std::runtime_error("XMPP server not support STARTTLS");
}
socket.next_layer().close();
co_return co_await (*this)(socket.next_layer());
}
response.clear();
co_await this->ProcessTls(socket, response);
co_await impl::StartStream(account.Jid(), socket);
response.clear();
co_await this->ReadStream(socket, response);
doc.load_string(response.c_str());
co_await this->Auth(socket, std::move(doc));
co_return Client{std::move(this->account).Jid(), std::move(socket)};
}
};
} // namespace impl
template <typename Socket = boost::asio::ip::tcp::socket>
inline auto CreateClient(UserAccount account, const Options& options = {})
-> boost::asio::awaitable<std::variant<Client<Socket>, Client<boost::asio::ssl::stream<Socket>>>> {
auto executor = co_await boost::asio::this_coro::executor;
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>>>>>(
impl::ClientCreateVisitor{std::move(account), options},
options.useTls == Options::kNever ? std::variant<Socket, boost::asio::ssl::stream<Socket>>{Socket{executor}}
: boost::asio::ssl::stream<Socket>(executor, ctx));
}
} // namespace larra::xmpp::client

View file

@ -0,0 +1,54 @@
#pragma once
#include <larra/utils.hpp>
#include <optional>
#include <pugixml.hpp>
#include <vector>
namespace larra::xmpp {
enum class Required : bool { kNotRequired = false, kRequired = true };
struct SaslMechanisms {
std::vector<std::string> mechanisms;
static auto Parse(pugi::xml_node) -> SaslMechanisms;
};
struct StreamFeatures {
struct StartTlsType {
Required required;
[[nodiscard]] constexpr auto Required(Required required) const -> StartTlsType {
return {required};
};
static auto Parse(pugi::xml_node) -> StartTlsType;
};
struct BindType {
Required required;
[[nodiscard]] constexpr auto Required(Required required) const -> BindType {
return {required};
};
static auto Parse(pugi::xml_node) -> BindType;
};
std::optional<StartTlsType> startTls;
std::optional<BindType> bind;
SaslMechanisms saslMechanisms;
std::vector<pugi::xml_node> others;
template <typename Self>
[[nodiscard]] constexpr auto StartTls(this Self&& self, std::optional<StartTlsType> value) {
return utils::FieldSetHelper::With<"startTls">(std::forward<Self>(self), std::move(value));
}
template <typename Self>
[[nodiscard]] constexpr auto Bind(this Self&& self, std::optional<BindType> value) {
return utils::FieldSetHelper::With<"bind">(std::forward<Self>(self), std::move(value));
}
template <typename Self>
[[nodiscard]] constexpr auto SaslMechanisms(this Self&& self, SaslMechanisms value) {
return utils::FieldSetHelper::With<"saslMechanisms">(std::forward<Self>(self), std::move(value));
}
template <typename Self>
[[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));
}
static auto Parse(pugi::xml_node) -> StreamFeatures;
};
} // namespace larra::xmpp

View file

@ -1,4 +1,5 @@
#pragma once
#include <format>
#include <larra/utils.hpp>
#include <string>
#include <utility>
@ -50,17 +51,17 @@ struct FullJid {
template <typename Self>
[[nodiscard]] constexpr auto Username(this Self&& self, std::string username) -> FullJid {
return utils::FieldSetHelper::With<"username", FullJid>(std::forward<Self>(self), std::move(username));
};
}
template <typename Self>
[[nodiscard]] constexpr auto Server(this Self&& self, std::string server) -> FullJid {
return utils::FieldSetHelper::With<"server", FullJid>(std::forward<Self>(self), std::move(server));
};
}
template <typename Self>
[[nodiscard]] constexpr auto Resource(this Self&& self, std::string resource) -> FullJid {
return utils::FieldSetHelper::With<"resource", FullJid>(std::forward<Self>(self), std::move(resource));
};
}
};
using JidVariant = std::variant<BareJid, BareResourceJid, FullJid>;
@ -72,3 +73,35 @@ struct Jid : JidVariant {
};
} // namespace larra::xmpp
template <>
struct std::formatter<larra::xmpp::Jid> : std::formatter<std::string> {
template <typename FormatContext>
constexpr auto format(const larra::xmpp::Jid& arg, FormatContext& ctx) const -> FormatContext::iterator {
return std::formatter<std::string>::format(ToString(arg), ctx);
};
};
template <>
struct std::formatter<larra::xmpp::FullJid> : std::formatter<std::string> {
template <typename FormatContext>
constexpr auto format(const larra::xmpp::FullJid& arg, FormatContext& ctx) const -> FormatContext::iterator {
return std::formatter<std::string>::format(ToString(arg), ctx);
};
};
template <>
struct std::formatter<larra::xmpp::BareJid> : std::formatter<std::string> {
template <typename FormatContext>
constexpr auto format(const larra::xmpp::BareJid& arg, FormatContext& ctx) const -> FormatContext::iterator {
return std::formatter<std::string>::format(ToString(arg), ctx);
};
};
template <>
struct std::formatter<larra::xmpp::BareResourceJid> : std::formatter<std::string> {
template <typename FormatContext>
constexpr auto format(const larra::xmpp::BareResourceJid& arg, FormatContext& ctx) const -> FormatContext::iterator {
return std::formatter<std::string>::format(ToString(arg), ctx);
};
};

View file

@ -0,0 +1,137 @@
#pragma once
#include <boost/asio/ssl.hpp>
#include <boost/asio/write.hpp>
#include <print>
#include <sstream>
namespace larra::xmpp {
template <typename Socket>
struct PrintStream : Socket {
using Socket::Socket;
using Executor = Socket::executor_type;
template <typename ConstBufferSequence,
BOOST_ASIO_COMPLETION_TOKEN_FOR(void(boost::system::error_code, std::size_t))
WriteToken = boost::asio::default_completion_token_t<Executor>>
auto async_write_some(const ConstBufferSequence& buffers, WriteToken&& token) {
std::ostringstream stream; // Write to buffer for concurrent logging
// std::osyncstream not realized in libc++
stream << "Writing data to stream: ";
for(boost::asio::const_buffer buf : buffers) {
stream << std::string_view{static_cast<const char*>(buf.data()), buf.size()};
}
std::println("{}", stream.str());
return boost::asio::async_initiate<WriteToken, void(boost::system::error_code, std::size_t)>(
[this]<typename Handler>(Handler&& token, const ConstBufferSequence& buffers) {
Socket::async_write_some(buffers, [token = std::move(token)](boost::system::error_code err, std::size_t s) mutable {
std::println("Data writing completed");
token(err, s);
});
},
token,
buffers);
}
template <typename MutableBufferSequence,
BOOST_ASIO_COMPLETION_TOKEN_FOR(void(boost::system::error_code, std::size_t))
ReadToken = boost::asio::default_completion_token_t<Executor>>
auto async_read_some(const MutableBufferSequence& buffers, ReadToken&& token) {
std::println("Reading data from stream");
return boost::asio::async_initiate<ReadToken, void(boost::system::error_code, std::size_t)>(
[this](ReadToken&& token, const MutableBufferSequence& buffers) {
Socket::async_read_some(buffers, [buffers, token = std::move(token)](boost::system::error_code err, std::size_t s) mutable {
std::ostringstream stream; // Write to buffer for concurrent logging
// std::osyncstream not realized in libc++
stream << "Data after read: ";
for(boost::asio::mutable_buffer buf : buffers) {
stream << std::string_view{static_cast<const char*>(buf.data()), buf.size()};
}
std::println("{}", stream.str());
token(err, s);
});
},
token,
buffers);
}
};
namespace impl {
template <typename Socket>
struct PrintStream : ::larra::xmpp::PrintStream<Socket> {
using ::larra::xmpp::PrintStream<Socket>::PrintStream;
};
} // namespace impl
} // namespace larra::xmpp
template <typename Socket>
struct boost::asio::ssl::stream<larra::xmpp::PrintStream<Socket>> : public boost::asio::ssl::stream<Socket> {
using Base = boost::asio::ssl::stream<Socket>;
using Base::Base;
using next_layer_type = larra::xmpp::PrintStream<Socket>;
using Executor = Socket::executor_type;
template <typename ConstBufferSequence,
BOOST_ASIO_COMPLETION_TOKEN_FOR(void(boost::system::error_code, std::size_t))
WriteToken = boost::asio::default_completion_token_t<Executor>>
auto async_write_some(const ConstBufferSequence& buffers, WriteToken&& token) {
std::ostringstream stream; // Write to buffer for concurrent logging
// std::osyncstream not realized in libc++
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()};
}
std::println("{}", stream.str());
return boost::asio::async_initiate<WriteToken, void(boost::system::error_code, std::size_t)>(
[this]<typename Handler>(Handler&& token, const ConstBufferSequence& buffers) {
Base::async_write_some(buffers, [token = std::move(token)](boost::system::error_code err, std::size_t s) mutable {
std::println("Data writing completed(SSL)");
token(err, s);
});
},
token,
buffers);
}
template <typename MutableBufferSequence,
BOOST_ASIO_COMPLETION_TOKEN_FOR(void(boost::system::error_code, std::size_t))
ReadToken = boost::asio::default_completion_token_t<Executor>>
auto async_read_some(const MutableBufferSequence& buffers, ReadToken&& token) {
std::println("Reading data from stream(SSL)");
return boost::asio::async_initiate<ReadToken, void(boost::system::error_code, std::size_t)>(
[this]<typename Handler>(Handler&& token, const MutableBufferSequence& buffers) {
Base::async_read_some(buffers, [buffers, token = std::move(token)](boost::system::error_code err, std::size_t s) mutable {
std::ostringstream stream; // Write to buffer for concurrent logging
// std::osyncstream not realized in libc++
stream << "Data after read(SSL): ";
for(boost::asio::mutable_buffer buf : buffers) {
stream << std::string_view{static_cast<const char*>(buf.data()), buf.size()};
}
std::println("{}", stream.str());
token(err, s);
});
},
token,
buffers);
}
template <BOOST_ASIO_COMPLETION_TOKEN_FOR(void(boost::system::error_code))
HandshakeToken = boost::asio::default_completion_token_t<Executor>>
auto async_handshake(Base::handshake_type type, HandshakeToken&& token = default_completion_token_t<Executor>{}) {
std::println("SSL Handshake start");
return boost::asio::async_initiate<HandshakeToken, void(boost::system::error_code)>(
[this]<typename Handler>(Handler&& token, Base::handshake_type type) {
Base::async_handshake(type, [token = std::move(token)](boost::system::error_code error) mutable {
std::println("SSL Handshake completed");
token(error);
});
},
token,
type);
}
auto next_layer() -> next_layer_type& {
return static_cast<next_layer_type&>(this->Base::next_layer());
}
auto next_layer() const -> const next_layer_type& {
return static_cast<const next_layer_type&>(this->Base::next_layer());
}
};

View file

@ -21,39 +21,40 @@ struct BasicStream {
template <typename Self>
constexpr auto From(this Self&& self, FromType value) -> BasicStream {
return utils::FieldSetHelper::With<"from", BasicStream>(std::forward<Self>(self), std::move(value));
};
}
template <typename Self>
constexpr auto To(this Self&& self, ToType value) -> BasicStream {
return utils::FieldSetHelper::With<"to", BasicStream>(std::forward<Self>(self), std::move(value));
};
}
template <typename Self>
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));
};
}
template <typename Self>
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));
};
}
template <typename Self>
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));
};
}
auto SerializeStream(pugi::xml_node& node) const -> void;
friend auto operator<<(pugi::xml_node& node, const BasicStream& stream) -> pugi::xml_node& {
return (stream.SerializeStream(node), node);
};
friend auto operator<<(pugi::xml_node node, const BasicStream& stream) -> pugi::xml_node {
stream.SerializeStream(node);
return (std::move(node));
}
friend auto ToString(const BasicStream<JidFrom, JidTo>&) -> std::string;
static auto Parse(const pugi::xml_node& node) -> BasicStream<JidFrom, JidTo>;
};
} // namespace impl
using UserStream = impl::BasicStream<true, false>;
using UserToUserStream = impl::BasicStream<true, true>;
using ServerStream = impl::BasicStream<false, false>;
using ServerToUserStream = impl::BasicStream<false, true>;

View file

@ -0,0 +1,71 @@
#pragma once
#include <larra/jid.hpp>
#include <larra/utils.hpp>
namespace larra::xmpp {
struct PlainUserAccount {
BareJid jid;
std::string password;
template <typename Self>
constexpr auto Jid(this Self&& self, BareJid value) {
return utils::FieldSetHelper::With<"jid", PlainUserAccount>(std::forward<Self>(self), std::move(value));
}
template <typename Self>
constexpr auto Password(this Self&& self, std::string value) {
return utils::FieldSetHelper::With<"password", PlainUserAccount>(std::forward<Self>(self), std::move(value));
}
};
struct EncryptionUserAccount : PlainUserAccount {
using PlainUserAccount::PlainUserAccount;
constexpr EncryptionUserAccount(PlainUserAccount base) : PlainUserAccount{std::move(base)} {};
constexpr EncryptionUserAccount(BareJid jid, std::string password) : PlainUserAccount{std::move(jid), std::move(password)} {};
};
struct EncryptionRequiredUserAccount : EncryptionUserAccount {
using EncryptionUserAccount::EncryptionUserAccount;
constexpr EncryptionRequiredUserAccount(PlainUserAccount base) : EncryptionUserAccount{std::move(base)} {};
constexpr EncryptionRequiredUserAccount(BareJid jid, std::string password) :
EncryptionUserAccount{std::move(jid), std::move(password)} {};
};
using UserAccountVariant = std::variant<PlainUserAccount, EncryptionUserAccount, EncryptionRequiredUserAccount>;
struct UserAccount : UserAccountVariant {
using UserAccountVariant::variant;
template <typename Self>
constexpr auto Jid(this Self&& self, BareJid value) -> UserAccount {
return std::visit<UserAccount>(
[&](auto& ref) -> std::decay_t<decltype(ref)> {
return {std::forward_like<Self>(ref).Jid(std::move(value))};
},
self);
}
template <typename Self>
constexpr auto Password(this Self&& self, std::string value) -> UserAccount {
return std::visit<UserAccount>(
[&](auto& ref) -> std::decay_t<decltype(ref)> {
return std::forward_like<Self>(ref).Password(std::move(value));
},
self);
}
template <typename Self>
constexpr auto Jid(this Self&& self) -> decltype(auto) {
return std::visit<decltype(std::forward_like<Self>(std::declval<BareJid&>()))>(
[](auto& ref) -> decltype(auto) {
return std::forward_like<Self>(ref.jid);
},
self);
}
template <typename Self>
constexpr auto Password(this Self&& self) -> decltype(auto) {
return std::visit<decltype(std::forward_like<Self>(std::declval<BareJid&>()))>(
[](auto& ref) -> decltype(auto) {
return std::forward_like<Self>(ref.password);
},
self);
}
};
} // namespace larra::xmpp

View file

@ -46,14 +46,17 @@ struct FieldsDescription {
utempl::Tuple<Fs...> tuple; /*!< tuple for field ptrs */
/* Method accepting field index, self and new value for field and returns object with new field value
*
* \param I field index for object T
* \tparam I field index for object T
* \tparam Type return type
* \param self old object
* \param value new value for field
* \return T with new field value and values from self
* \return an object of type std::decay_t<Self> with data from T with fields from \ref self and the \ref value of the field with index
* \ref I
*/
template <std::size_t I, typename Self, typename Value>
constexpr auto With(Self&& self, Value&& value) const
requires(std::is_constructible_v<T, impl::GetTypeT<Fs>...> && impl::SetConcept<Self, std::decay_t<Value>, Fs...> &&
constexpr auto With(Self&& self, Value&& value) const -> std::decay_t<Self>
requires(std::constructible_from<std::decay_t<Self>, T> && std::is_constructible_v<T, impl::GetTypeT<Fs>...> &&
impl::SetConcept<Self, std::decay_t<Value>, Fs...> &&
[] {
if constexpr(I < sizeof...(Fs)) {
return std::is_constructible_v<impl::GetTypeT<decltype(Get<I>(tuple))>, decltype(value)>;
@ -62,7 +65,7 @@ struct FieldsDescription {
}())
{
return [&](auto... is) -> T {
return std::decay_t<Self>{[&](auto... is) -> T {
return {[&] -> decltype(auto) {
if constexpr(*is == I) {
return std::forward<Value>(value);
@ -70,7 +73,7 @@ struct FieldsDescription {
return std::forward_like<Self>(self.*Get<*is>(this->tuple));
};
}()...};
} | utempl::kSeq<sizeof...(Fs)>;
} | utempl::kSeq<sizeof...(Fs)>};
};
/* Method accepting field pointer, self and new value for field and returns object with new field value
@ -78,14 +81,14 @@ struct FieldsDescription {
* \param ptr field ptr for object T
* \param self old object
* \param value new value for field
* \return T with new field value and values from self
* \return an object of type std::decay_t<Self> with data from T with fields from \ref self and the \ref value of the field \ref ptr
*/
template <typename Self, typename Value, typename Type>
constexpr auto With(Type(T::*ptr), Self&& self, Value&& value) const
requires(std::is_constructible_v<T, impl::GetTypeT<Fs>...> && std::is_constructible_v<Type, decltype(value)> &&
impl::SetConcept<Self, Type, Fs...>)
requires(std::is_constructible_v<T, impl::GetTypeT<Fs>...> && std::is_constructible_v<std::decay_t<Self>, T> &&
std::is_constructible_v<Type, decltype(value)> && impl::SetConcept<Self, Type, Fs...>)
{
return utempl::Unpack(this->tuple, [&](auto... fs) -> T {
return std::decay_t<Self>{utempl::Unpack(this->tuple, [&](auto... fs) -> T {
return {[&] {
if constexpr(std::is_same_v<decltype(fs), decltype(ptr)>) {
if(fs == ptr) {
@ -94,7 +97,7 @@ struct FieldsDescription {
};
return std::forward_like<Self>(self.*fs);
}()...};
});
})};
};
}; // namespace larra::xmpp::utils
@ -105,6 +108,20 @@ consteval auto CreateFieldsDescriptionHelper(Fs&&... fs) -> FieldsDescription<T,
return {.tuple = {std::forward<Fs>(fs)...}};
}
// Bug: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=109781
template <typename T, typename Self>
consteval auto GCCWorkaround() -> decltype(auto) {
if constexpr(std::same_as<T, void>) {
return [] -> Self {
std::unreachable();
}();
} else {
return [] -> std::remove_reference_t<decltype(std::forward_like<Self>(std::declval<T>()))> {
std::unreachable();
}();
}
}
} // namespace impl
/* Method accepting field ptrs and returns FieldsDescription
@ -138,11 +155,12 @@ consteval auto CreateFieldsDescription(Rs(T::*... ptrs)) {
struct FieldSetHelper {
/* Method accepting field index, self and new value for field and returns object with new field value
*
* \param I field index for object T
* \param T Type for return and pfr reflection
* \tparam I field index for object T
* \tparam T Type for return and pfr reflection
* \param self old object
* \param value new value for field
* \return T with new field value and values from self
* \returns an object of type std::decay_t<Self> with data from Type constructed with fields with \ref self and the \ref value of the
* field with index \ref I where Type is std::conditional_t<std::same_as<\ref T, void>, std::decay_t<Self>, T>
*/
template <std::size_t I,
typename T = void,
@ -150,35 +168,20 @@ struct FieldSetHelper {
typename Value,
typename...,
typename Type = std::conditional_t<std::same_as<T, void>, std::decay_t<Self>, T>,
typename TT = decltype([] -> decltype(auto) {
if constexpr(std::same_as<T, void>) {
return [] -> Self {
std::unreachable();
}();
} else {
return [] -> std::remove_reference_t<decltype(std::forward_like<Self>(std::declval<T>()))> {
std::unreachable();
}();
}
}())>
static constexpr auto With(Self&& self, Value&& value) -> Type
requires(
std::is_aggregate_v<Type> && (std::same_as<T, void> || requires { static_cast<const T&>(self); }) &&
[] {
if constexpr(I < boost::pfr::tuple_size_v<Type>) {
return std::is_constructible_v<std::decay_t<decltype(boost::pfr::get<I>(std::declval<TT>()))>, decltype(value)>;
};
return false;
}() &&
[](auto... is) {
return ((*is == I ? true
: std::is_reference_v<Self> ? std::copy_constructible<decltype(boost::pfr::get<*is>(std::declval<TT>()))>
: std::move_constructible<decltype(boost::pfr::get<*is>(std::declval<TT>()))>) &&
...) &&
std::is_constructible_v<Type, decltype(boost::pfr::get<*is>(std::declval<TT>()))...>;
} | utempl::kSeq<boost::pfr::tuple_size_v<Type>>)
typename TT = decltype(impl::GCCWorkaround<T, Self>())>
static constexpr auto With(Self&& self, Value&& value) -> std::decay_t<Self>
requires(std::constructible_from<std::decay_t<Self>, Type> && std::is_aggregate_v<Type> &&
(std::same_as<T, void> || requires { static_cast<const T&>(self); }) && I < boost::pfr::tuple_size_v<Type> &&
std::is_constructible_v<std::decay_t<decltype(boost::pfr::get<I>(std::declval<TT>()))>, decltype(value)> &&
[](auto... is) {
return ((*is == I ? true
: std::is_reference_v<Self> ? std::copy_constructible<decltype(boost::pfr::get<*is>(std::declval<TT>()))>
: std::move_constructible<decltype(boost::pfr::get<*is>(std::declval<TT>()))>) &&
...) &&
std::is_constructible_v<Type, decltype(boost::pfr::get<*is>(std::declval<TT>()))...>;
} | utempl::kSeq<boost::pfr::tuple_size_v<Type>>)
{
return [&](auto... is) -> Type {
return std::decay_t<Self>{[&](auto... is) -> Type {
return {[&] -> decltype(auto) {
if constexpr(I == *is) {
return std::forward<Value>(value);
@ -186,17 +189,18 @@ struct FieldSetHelper {
return std::forward_like<Self>(boost::pfr::get<*is>(static_cast<TT&>(self)));
}
}()...};
} | utempl::kSeq<boost::pfr::tuple_size_v<TT>>;
} | utempl::kSeq<boost::pfr::tuple_size_v<TT>>};
};
// clang-format off
/* Method accepting field name, self and new value for field and returns object with new field value
*
* \param FieldName field name for object T
* \param T Type for return and pfr reflection
* \tparam T Type for return and pfr reflection
* \param self old object
* \param value new value for field
* \return T with new field value and values from self
* \returns an object of type std::decay_t<Self> with data from Type constructed with fields with \ref self and the \ref value of the field named \ref FieldName where Type is
* std::conditional_t<std::same_as<\ref T, void>, std::decay_t<Self>, \ref T>
*/
template <utempl::ConstexprString FieldName, typename T = void, typename Self, typename Value>
static constexpr auto With(Self&& self, Value&& value) -> decltype(With<[] {

View file

@ -0,0 +1,6 @@
#include <boost/asio/connect.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <larra/client/client.hpp>
#include <larra/stream.hpp>
namespace larra::xmpp::client {} // namespace larra::xmpp::client

45
library/src/features.cpp Normal file
View file

@ -0,0 +1,45 @@
#include <larra/features.hpp>
namespace {
template <typename T>
inline auto ToOptional(const pugi::xml_node& node) -> std::optional<T> {
return node ? std::optional{T::Parse(node)} : std::nullopt;
}
} // namespace
namespace larra::xmpp {
auto SaslMechanisms::Parse(pugi::xml_node node) -> SaslMechanisms {
std::vector<std::string> response;
for(pugi::xml_node mechanism = node.child("mechanism"); mechanism; mechanism = mechanism.next_sibling("mechanism")) {
response.emplace_back(mechanism.child_value());
}
return {response};
}
auto StreamFeatures::StartTlsType::Parse(pugi::xml_node node) -> StreamFeatures::StartTlsType {
return {node.child("required") ? Required::kRequired : Required::kNotRequired};
}
auto StreamFeatures::BindType::Parse(pugi::xml_node node) -> StreamFeatures::BindType {
return {node.child("required") ? Required::kRequired : Required::kNotRequired};
}
auto StreamFeatures::Parse(pugi::xml_node node) -> StreamFeatures {
std::vector<pugi::xml_node> others;
for(pugi::xml_node current = node.first_child(); current; current = current.next_sibling()) {
// Проверяем, не является ли узел starttls, bind или mechanisms
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")),
ToOptional<BindType>(node.child("bind")),
SaslMechanisms::Parse(node.child("mechanisms")),
std::move(others)};
}
} // namespace larra::xmpp

View file

@ -1,3 +1,4 @@
#include <format>
#include <larra/stream.hpp>
namespace {
@ -40,14 +41,45 @@ auto impl::BasicStream<JidFrom, JidTo>::SerializeStream(pugi::xml_node& node) co
if(this->xmlLang) {
node.append_attribute("xml:lang") = this->xmlLang->c_str();
}
if constexpr(JidFrom || JidTo) {
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 ServerToUserStream::SerializeStream(pugi::xml_node& node) const -> void;
template auto UserToUserStream::SerializeStream(pugi::xml_node& node) const -> void;
template auto UserStream::SerializeStream(pugi::xml_node& 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>
auto impl::BasicStream<JidFrom, JidTo>::Parse(const pugi::xml_node& node) -> impl::BasicStream<JidFrom, JidTo> {
return {ToOptionalUser<JidFrom>(node.attribute("from")),
@ -57,7 +89,6 @@ auto impl::BasicStream<JidFrom, JidTo>::Parse(const pugi::xml_node& node) -> imp
ToOptionalString(node.attribute("xml:lang"))};
}
template auto UserStream::Parse(const pugi::xml_node& node) -> UserStream;
template auto UserToUserStream::Parse(const pugi::xml_node& node) -> UserToUserStream;
template auto ServerStream::Parse(const pugi::xml_node& node) -> ServerStream;
template auto ServerToUserStream::Parse(const pugi::xml_node& node) -> ServerToUserStream;

View file

@ -9,13 +9,13 @@ constexpr std::string_view kSerializedData =
)";
constexpr std::string_view kCheckSerializeData =
R"(<stream:stream from="user@example.com" to="anotheruser@example.com" id="abc" version="1.0" xml:lang="en" />
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" />
)";
TEST(Stream, Serialize) {
UserToUserStream originalStream;
UserStream originalStream;
originalStream.from = BareJid{"user", "example.com"};
originalStream.to = BareJid{"anotheruser", "example.com"};
originalStream.to = "example.com";
originalStream.id = "abc";
originalStream.version = "1.0";
originalStream.xmlLang = "en";