Initial client support
This commit is contained in:
parent
540f3ad68d
commit
3b78412da4
13 changed files with 901 additions and 68 deletions
|
@ -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
23
examples/src/connect.cpp
Normal 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();
|
||||
}
|
411
library/include/larra/client/client.hpp
Normal file
411
library/include/larra/client/client.hpp
Normal 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
|
54
library/include/larra/features.hpp
Normal file
54
library/include/larra/features.hpp
Normal 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
|
|
@ -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);
|
||||
};
|
||||
};
|
||||
|
|
137
library/include/larra/printer_stream.hpp
Normal file
137
library/include/larra/printer_stream.hpp
Normal 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());
|
||||
}
|
||||
};
|
|
@ -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>;
|
||||
|
|
71
library/include/larra/user_account.hpp
Normal file
71
library/include/larra/user_account.hpp
Normal 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
|
|
@ -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,26 +168,11 @@ 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;
|
||||
}() &&
|
||||
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>()))>
|
||||
|
@ -178,7 +181,7 @@ struct FieldSetHelper {
|
|||
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<[] {
|
||||
|
|
6
library/src/client/client.cpp
Normal file
6
library/src/client/client.cpp
Normal 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
45
library/src/features.cpp
Normal 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
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
|
|
Loading…
Reference in a new issue