From 3b78412da416001d9ee3444555d5350c8048f91a Mon Sep 17 00:00:00 2001 From: sha512sum Date: Tue, 3 Sep 2024 15:36:08 +0000 Subject: [PATCH] Initial client support --- CMakeLists.txt | 27 +- examples/src/connect.cpp | 23 ++ library/include/larra/client/client.hpp | 411 +++++++++++++++++++++++ library/include/larra/features.hpp | 54 +++ library/include/larra/jid.hpp | 39 ++- library/include/larra/printer_stream.hpp | 137 ++++++++ library/include/larra/stream.hpp | 19 +- library/include/larra/user_account.hpp | 71 ++++ library/include/larra/utils.hpp | 94 +++--- library/src/client/client.cpp | 6 + library/src/features.cpp | 45 +++ library/src/stream.cpp | 37 +- tests/stream.cpp | 6 +- 13 files changed, 901 insertions(+), 68 deletions(-) create mode 100644 examples/src/connect.cpp create mode 100644 library/include/larra/client/client.hpp create mode 100644 library/include/larra/features.hpp create mode 100644 library/include/larra/printer_stream.hpp create mode 100644 library/include/larra/user_account.hpp create mode 100644 library/src/client/client.cpp create mode 100644 library/src/features.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 6779fe6..c577084 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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() + diff --git a/examples/src/connect.cpp b/examples/src/connect.cpp new file mode 100644 index 0000000..013806d --- /dev/null +++ b/examples/src/connect.cpp @@ -0,0 +1,23 @@ +#include +#include +#include +#include +#include + +auto Coroutine() -> boost::asio::awaitable { + std::println("Connecting client..."); + try { + auto client = co_await larra::xmpp::client::CreateClient>( + 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(); +} diff --git a/library/include/larra/client/client.hpp b/library/include/larra/client/client.hpp new file mode 100644 index 0000000..1eca4ee --- /dev/null +++ b/library/include/larra/client/client.hpp @@ -0,0 +1,411 @@ +#pragma once +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace larra::xmpp { + +constexpr auto kDefaultXmppPort = 5222; + +} // namespace larra::xmpp + +namespace larra::xmpp::client { + +template +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, 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>; // 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>(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>(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 // NOLINT +inline auto Pbkdf2(std::string_view password, + std::basic_string_view salt, + int iterations) -> std::array { + std::array key; + PKCS5_PBKDF2_HMAC(password.data(), password.length(), salt.data(), salt.size(), iterations, F(), 64, key.data()); // NOLINT + return key; +} +template +inline auto SHA(std::basic_string_view input) -> std::array { + std::array response; + F(input.data(), input.size(), response.data()); + return response; +} + +template +inline auto HMAC(std::string_view key, + std::basic_string_view message) -> std::basic_string { + unsigned char* result = HMAC(F(), key.data(), static_cast(key.size()), message.data(), message.size(), nullptr, nullptr); + return {result, Size}; +} + +inline auto StartStream(const BareJid& from, auto& connection) -> boost::asio::awaitable { + auto stream = UserStream{}.To(from.server).From(std::move(from)).Version("1.0").XmlLang("en"); + auto buffer = "" + ToString(stream); + co_await boost::asio::async_write(connection, boost::asio::buffer(buffer), boost::asio::transfer_all(), boost::asio::use_awaitable); + co_return; +} + +template +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 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 { + 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 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(password, ToUnsignedCharStringView(salt), iterations); + std::string clientKeyStr = "Client Key"; // NOLINT + auto clientKey = HMAC(ToCharStringView(saltedPassword), ToUnsignedCharStringView(clientKeyStr)); + auto storedKey = SHA(clientKey); + auto authMessage = std::format("{},{},{}", initialMessage, firstServerMessage, clientFinalMessageBare); + auto clientSignature = HMAC(ToCharStringView(storedKey), ToUnsignedCharStringView(authMessage)); + auto clientProof = Xor(clientKey, clientSignature) | std::ranges::to(); + std::string serverKeyStr = "Server Key"; + auto serverKey = HMAC(ToCharStringView(saltedPassword), ToUnsignedCharStringView(serverKeyStr)); + auto serverSignature = HMAC(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 { + auto v = param.find("="); + return {param.substr(0, v), param.substr(v + 1)}; + }) | + std::ranges::to>(); +}; + +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 { + 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 ScramAuth(std::string_view methodName, const EncryptionUserAccount& account, auto& socket) -> boost::asio::awaitable { + 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), "", 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( + 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 { + // NOLINTBEGIN + if(std::ranges::contains(features.saslMechanisms.mechanisms, "SCRAM-SHA-512")) { + co_return co_await ScramAuth("SCRAM-SHA-512", account, socket); + } + if(std::ranges::contains(features.saslMechanisms.mechanisms, "SCRAM-SHA-256")) { + co_return co_await ScramAuth("SCRAM-SHA-256", account, socket); + } + if(std::ranges::contains(features.saslMechanisms.mechanisms, "SCRAM-SHA-1")) { + co_return co_await ScramAuth("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 { + 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(std::move(account)), socket, std::move(features), std::move(stream)); + } + + auto Auth(auto& socket, pugi::xml_document doc) -> boost::asio::awaitable { + co_return co_await std::visit>( + [&](auto& account) -> boost::asio::awaitable { + 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 { + 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 { + co_await boost::asio::async_connect(socket, resolveResults, boost::asio::use_awaitable); + } + auto ReadStream(auto& socket, std::string& buffer) -> boost::asio::awaitable { + co_await boost::asio::async_read_until(socket, boost::asio::dynamic_buffer(buffer), "", boost::asio::use_awaitable); + } + auto ReadStream(auto& socket) -> boost::asio::awaitable { + std::string buffer; + co_await ReadStream(socket, buffer); + co_return buffer; + } + template + auto ProcessTls(boost::asio::ssl::stream& socket, std::string& buffer) -> boost::asio::awaitable { + co_await boost::asio::async_write(socket.next_layer(), + boost::asio::buffer(""), + 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::handshake_type::client, boost::asio::use_awaitable); + } catch(const std::exception& e) { + throw StartTlsNegotiationError{e.what()}; + } + }; + template + inline auto operator()(Socket&& socket) + -> boost::asio::awaitable>, Client>>>> { + 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 + inline auto operator()(boost::asio::ssl::stream&& socket) + -> boost::asio::awaitable, Client>>> { + 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 +inline auto CreateClient(UserAccount account, const Options& options = {}) + -> boost::asio::awaitable, Client>>> { + 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, Client>>>>( + impl::ClientCreateVisitor{std::move(account), options}, + options.useTls == Options::kNever ? std::variant>{Socket{executor}} + : boost::asio::ssl::stream(executor, ctx)); +} + +} // namespace larra::xmpp::client diff --git a/library/include/larra/features.hpp b/library/include/larra/features.hpp new file mode 100644 index 0000000..3f55695 --- /dev/null +++ b/library/include/larra/features.hpp @@ -0,0 +1,54 @@ +#pragma once +#include +#include +#include +#include + +namespace larra::xmpp { + +enum class Required : bool { kNotRequired = false, kRequired = true }; + +struct SaslMechanisms { + std::vector 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 startTls; + std::optional bind; + SaslMechanisms saslMechanisms; + std::vector others; + template + [[nodiscard]] constexpr auto StartTls(this Self&& self, std::optional value) { + return utils::FieldSetHelper::With<"startTls">(std::forward(self), std::move(value)); + } + template + [[nodiscard]] constexpr auto Bind(this Self&& self, std::optional value) { + return utils::FieldSetHelper::With<"bind">(std::forward(self), std::move(value)); + } + template + [[nodiscard]] constexpr auto SaslMechanisms(this Self&& self, SaslMechanisms value) { + return utils::FieldSetHelper::With<"saslMechanisms">(std::forward(self), std::move(value)); + } + template + [[nodiscard]] constexpr auto Others(this Self&& self, std::vector value) { + return utils::FieldSetHelper::With<"others">(std::forward(self), std::move(value)); + } + static auto Parse(pugi::xml_node) -> StreamFeatures; +}; + +} // namespace larra::xmpp diff --git a/library/include/larra/jid.hpp b/library/include/larra/jid.hpp index 92ed2cd..a61d10f 100644 --- a/library/include/larra/jid.hpp +++ b/library/include/larra/jid.hpp @@ -1,4 +1,5 @@ #pragma once +#include #include #include #include @@ -50,17 +51,17 @@ struct FullJid { template [[nodiscard]] constexpr auto Username(this Self&& self, std::string username) -> FullJid { return utils::FieldSetHelper::With<"username", FullJid>(std::forward(self), std::move(username)); - }; + } template [[nodiscard]] constexpr auto Server(this Self&& self, std::string server) -> FullJid { return utils::FieldSetHelper::With<"server", FullJid>(std::forward(self), std::move(server)); - }; + } template [[nodiscard]] constexpr auto Resource(this Self&& self, std::string resource) -> FullJid { return utils::FieldSetHelper::With<"resource", FullJid>(std::forward(self), std::move(resource)); - }; + } }; using JidVariant = std::variant; @@ -72,3 +73,35 @@ struct Jid : JidVariant { }; } // namespace larra::xmpp + +template <> +struct std::formatter : std::formatter { + template + constexpr auto format(const larra::xmpp::Jid& arg, FormatContext& ctx) const -> FormatContext::iterator { + return std::formatter::format(ToString(arg), ctx); + }; +}; + +template <> +struct std::formatter : std::formatter { + template + constexpr auto format(const larra::xmpp::FullJid& arg, FormatContext& ctx) const -> FormatContext::iterator { + return std::formatter::format(ToString(arg), ctx); + }; +}; + +template <> +struct std::formatter : std::formatter { + template + constexpr auto format(const larra::xmpp::BareJid& arg, FormatContext& ctx) const -> FormatContext::iterator { + return std::formatter::format(ToString(arg), ctx); + }; +}; + +template <> +struct std::formatter : std::formatter { + template + constexpr auto format(const larra::xmpp::BareResourceJid& arg, FormatContext& ctx) const -> FormatContext::iterator { + return std::formatter::format(ToString(arg), ctx); + }; +}; diff --git a/library/include/larra/printer_stream.hpp b/library/include/larra/printer_stream.hpp new file mode 100644 index 0000000..3a1bad4 --- /dev/null +++ b/library/include/larra/printer_stream.hpp @@ -0,0 +1,137 @@ +#pragma once +#include +#include +#include +#include + +namespace larra::xmpp { + +template +struct PrintStream : Socket { + using Socket::Socket; + using Executor = Socket::executor_type; + template > + 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(buf.data()), buf.size()}; + } + std::println("{}", stream.str()); + return boost::asio::async_initiate( + [this](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 > + auto async_read_some(const MutableBufferSequence& buffers, ReadToken&& token) { + std::println("Reading data from stream"); + return boost::asio::async_initiate( + [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(buf.data()), buf.size()}; + } + std::println("{}", stream.str()); + token(err, s); + }); + }, + token, + buffers); + } +}; + +namespace impl { + +template +struct PrintStream : ::larra::xmpp::PrintStream { + using ::larra::xmpp::PrintStream::PrintStream; +}; + +} // namespace impl + +} // namespace larra::xmpp + +template +struct boost::asio::ssl::stream> : public boost::asio::ssl::stream { + using Base = boost::asio::ssl::stream; + using Base::Base; + + using next_layer_type = larra::xmpp::PrintStream; + using Executor = Socket::executor_type; + template > + 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(buf.data()), buf.size()}; + } + std::println("{}", stream.str()); + return boost::asio::async_initiate( + [this](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 > + auto async_read_some(const MutableBufferSequence& buffers, ReadToken&& token) { + std::println("Reading data from stream(SSL)"); + return boost::asio::async_initiate( + [this](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(buf.data()), buf.size()}; + } + std::println("{}", stream.str()); + token(err, s); + }); + }, + token, + buffers); + } + template > + auto async_handshake(Base::handshake_type type, HandshakeToken&& token = default_completion_token_t{}) { + std::println("SSL Handshake start"); + return boost::asio::async_initiate( + [this](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(this->Base::next_layer()); + } + auto next_layer() const -> const next_layer_type& { + return static_cast(this->Base::next_layer()); + } +}; diff --git a/library/include/larra/stream.hpp b/library/include/larra/stream.hpp index ac8497e..ba09026 100644 --- a/library/include/larra/stream.hpp +++ b/library/include/larra/stream.hpp @@ -21,39 +21,40 @@ struct BasicStream { template constexpr auto From(this Self&& self, FromType value) -> BasicStream { return utils::FieldSetHelper::With<"from", BasicStream>(std::forward(self), std::move(value)); - }; + } template constexpr auto To(this Self&& self, ToType value) -> BasicStream { return utils::FieldSetHelper::With<"to", BasicStream>(std::forward(self), std::move(value)); - }; + } template constexpr auto Id(this Self&& self, std::optional value) -> BasicStream { return utils::FieldSetHelper::With<"id", BasicStream>(std::forward(self), std::move(value)); - }; + } template constexpr auto Version(this Self&& self, std::optional value) -> BasicStream { return utils::FieldSetHelper::With<"version", BasicStream>(std::forward(self), std::move(value)); - }; + } template constexpr auto XmlLang(this Self&& self, std::optional value) -> BasicStream { return utils::FieldSetHelper::With<"xmlLang", BasicStream>(std::forward_like(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&) -> std::string; static auto Parse(const pugi::xml_node& node) -> BasicStream; }; } // namespace impl using UserStream = impl::BasicStream; -using UserToUserStream = impl::BasicStream; using ServerStream = impl::BasicStream; using ServerToUserStream = impl::BasicStream; diff --git a/library/include/larra/user_account.hpp b/library/include/larra/user_account.hpp new file mode 100644 index 0000000..906ea56 --- /dev/null +++ b/library/include/larra/user_account.hpp @@ -0,0 +1,71 @@ +#pragma once +#include +#include + +namespace larra::xmpp { + +struct PlainUserAccount { + BareJid jid; + std::string password; + template + constexpr auto Jid(this Self&& self, BareJid value) { + return utils::FieldSetHelper::With<"jid", PlainUserAccount>(std::forward(self), std::move(value)); + } + template + constexpr auto Password(this Self&& self, std::string value) { + return utils::FieldSetHelper::With<"password", PlainUserAccount>(std::forward(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; + +struct UserAccount : UserAccountVariant { + using UserAccountVariant::variant; + template + constexpr auto Jid(this Self&& self, BareJid value) -> UserAccount { + return std::visit( + [&](auto& ref) -> std::decay_t { + return {std::forward_like(ref).Jid(std::move(value))}; + }, + self); + } + template + constexpr auto Password(this Self&& self, std::string value) -> UserAccount { + return std::visit( + [&](auto& ref) -> std::decay_t { + return std::forward_like(ref).Password(std::move(value)); + }, + self); + } + template + constexpr auto Jid(this Self&& self) -> decltype(auto) { + return std::visit(std::declval()))>( + [](auto& ref) -> decltype(auto) { + return std::forward_like(ref.jid); + }, + self); + } + template + constexpr auto Password(this Self&& self) -> decltype(auto) { + return std::visit(std::declval()))>( + [](auto& ref) -> decltype(auto) { + return std::forward_like(ref.password); + }, + self); + } +}; + +} // namespace larra::xmpp diff --git a/library/include/larra/utils.hpp b/library/include/larra/utils.hpp index d053c82..0303d37 100644 --- a/library/include/larra/utils.hpp +++ b/library/include/larra/utils.hpp @@ -46,14 +46,17 @@ struct FieldsDescription { utempl::Tuple 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 with data from T with fields from \ref self and the \ref value of the field with index + * \ref I */ template - constexpr auto With(Self&& self, Value&& value) const - requires(std::is_constructible_v...> && impl::SetConcept, Fs...> && + constexpr auto With(Self&& self, Value&& value) const -> std::decay_t + requires(std::constructible_from, T> && std::is_constructible_v...> && + impl::SetConcept, Fs...> && [] { if constexpr(I < sizeof...(Fs)) { return std::is_constructible_v(tuple))>, decltype(value)>; @@ -62,7 +65,7 @@ struct FieldsDescription { }()) { - return [&](auto... is) -> T { + return std::decay_t{[&](auto... is) -> T { return {[&] -> decltype(auto) { if constexpr(*is == I) { return std::forward(value); @@ -70,7 +73,7 @@ struct FieldsDescription { return std::forward_like(self.*Get<*is>(this->tuple)); }; }()...}; - } | utempl::kSeq; + } | utempl::kSeq}; }; /* 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 with data from T with fields from \ref self and the \ref value of the field \ref ptr */ template constexpr auto With(Type(T::*ptr), Self&& self, Value&& value) const - requires(std::is_constructible_v...> && std::is_constructible_v && - impl::SetConcept) + requires(std::is_constructible_v...> && std::is_constructible_v, T> && + std::is_constructible_v && impl::SetConcept) { - return utempl::Unpack(this->tuple, [&](auto... fs) -> T { + return std::decay_t{utempl::Unpack(this->tuple, [&](auto... fs) -> T { return {[&] { if constexpr(std::is_same_v) { if(fs == ptr) { @@ -94,7 +97,7 @@ struct FieldsDescription { }; return std::forward_like(self.*fs); }()...}; - }); + })}; }; }; // namespace larra::xmpp::utils @@ -105,6 +108,20 @@ consteval auto CreateFieldsDescriptionHelper(Fs&&... fs) -> FieldsDescription(fs)...}}; } +// Bug: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=109781 +template +consteval auto GCCWorkaround() -> decltype(auto) { + if constexpr(std::same_as) { + return [] -> Self { + std::unreachable(); + }(); + } else { + return [] -> std::remove_reference_t(std::declval()))> { + 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 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::decay_t, T> */ template , std::decay_t, T>, - typename TT = decltype([] -> decltype(auto) { - if constexpr(std::same_as) { - return [] -> Self { - std::unreachable(); - }(); - } else { - return [] -> std::remove_reference_t(std::declval()))> { - std::unreachable(); - }(); - } - }())> - static constexpr auto With(Self&& self, Value&& value) -> Type - requires( - std::is_aggregate_v && (std::same_as || requires { static_cast(self); }) && - [] { - if constexpr(I < boost::pfr::tuple_size_v) { - return std::is_constructible_v(std::declval()))>, decltype(value)>; - }; - return false; - }() && - [](auto... is) { - return ((*is == I ? true - : std::is_reference_v ? std::copy_constructible(std::declval()))> - : std::move_constructible(std::declval()))>) && - ...) && - std::is_constructible_v(std::declval()))...>; - } | utempl::kSeq>) + typename TT = decltype(impl::GCCWorkaround())> + static constexpr auto With(Self&& self, Value&& value) -> std::decay_t + requires(std::constructible_from, Type> && std::is_aggregate_v && + (std::same_as || requires { static_cast(self); }) && I < boost::pfr::tuple_size_v && + std::is_constructible_v(std::declval()))>, decltype(value)> && + [](auto... is) { + return ((*is == I ? true + : std::is_reference_v ? std::copy_constructible(std::declval()))> + : std::move_constructible(std::declval()))>) && + ...) && + std::is_constructible_v(std::declval()))...>; + } | utempl::kSeq>) { - return [&](auto... is) -> Type { + return std::decay_t{[&](auto... is) -> Type { return {[&] -> decltype(auto) { if constexpr(I == *is) { return std::forward(value); @@ -186,17 +189,18 @@ struct FieldSetHelper { return std::forward_like(boost::pfr::get<*is>(static_cast(self))); } }()...}; - } | utempl::kSeq>; + } | utempl::kSeq>}; }; // 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 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::decay_t, \ref T> */ template static constexpr auto With(Self&& self, Value&& value) -> decltype(With<[] { diff --git a/library/src/client/client.cpp b/library/src/client/client.cpp new file mode 100644 index 0000000..af9b378 --- /dev/null +++ b/library/src/client/client.cpp @@ -0,0 +1,6 @@ +#include +#include +#include +#include + +namespace larra::xmpp::client {} // namespace larra::xmpp::client diff --git a/library/src/features.cpp b/library/src/features.cpp new file mode 100644 index 0000000..313d134 --- /dev/null +++ b/library/src/features.cpp @@ -0,0 +1,45 @@ +#include + +namespace { + +template +inline auto ToOptional(const pugi::xml_node& node) -> std::optional { + return node ? std::optional{T::Parse(node)} : std::nullopt; +} + +} // namespace + +namespace larra::xmpp { + +auto SaslMechanisms::Parse(pugi::xml_node node) -> SaslMechanisms { + std::vector 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 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(node.child("starttls")), + ToOptional(node.child("bind")), + SaslMechanisms::Parse(node.child("mechanisms")), + std::move(others)}; +} + +} // namespace larra::xmpp diff --git a/library/src/stream.cpp b/library/src/stream.cpp index 1c2e5b0..ae94d64 100644 --- a/library/src/stream.cpp +++ b/library/src/stream.cpp @@ -1,3 +1,4 @@ +#include #include namespace { @@ -40,14 +41,45 @@ auto impl::BasicStream::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 +inline auto ToStringHelper(const BasicStream& stream) { + return std::format("", + 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 auto impl::BasicStream::Parse(const pugi::xml_node& node) -> impl::BasicStream { return {ToOptionalUser(node.attribute("from")), @@ -57,7 +89,6 @@ auto impl::BasicStream::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; diff --git a/tests/stream.cpp b/tests/stream.cpp index ea460a0..bb65ad7 100644 --- a/tests/stream.cpp +++ b/tests/stream.cpp @@ -9,13 +9,13 @@ constexpr std::string_view kSerializedData = )"; constexpr std::string_view kCheckSerializeData = - R"( + R"( )"; 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";