Features: iq::Bind and iq::Roster #4
7 changed files with 207 additions and 16 deletions
12
.vscode/launch.json
vendored
12
.vscode/launch.json
vendored
|
@ -12,6 +12,18 @@
|
||||||
"args": [],
|
"args": [],
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
"preLaunchTask": "GCC: Build"
|
"preLaunchTask": "GCC: Build"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug: tests",
|
||||||
|
"program": "${workspaceFolder}/build/larra_xmpp_tests",
|
||||||
|
"args": [
|
||||||
|
// --gtest_filter=POSTIVE_PATTERNS[-NEGATIVE_PATTERNS]
|
||||||
|
"--gtest_filter=Roster*"
|
||||||
|
],
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"preLaunchTask": "GCC: Build"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
2
.vscode/tasks.json
vendored
2
.vscode/tasks.json
vendored
|
@ -104,7 +104,7 @@
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"clear": true
|
"clear": true
|
||||||
},
|
},
|
||||||
"hide": true,
|
"hide": false,
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "build"
|
"kind": "build"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
#include <spdlog/common.h>
|
||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
#include <boost/asio/co_spawn.hpp>
|
#include <boost/asio/co_spawn.hpp>
|
||||||
|
@ -7,6 +8,8 @@
|
||||||
#include <larra/printer_stream.hpp>
|
#include <larra/printer_stream.hpp>
|
||||||
#include <print>
|
#include <print>
|
||||||
|
|
||||||
|
namespace iq = larra::xmpp::iq;
|
||||||
|
|
||||||
auto Coroutine() -> boost::asio::awaitable<void> {
|
auto Coroutine() -> boost::asio::awaitable<void> {
|
||||||
SPDLOG_INFO("Connecting client...");
|
SPDLOG_INFO("Connecting client...");
|
||||||
|
|
||||||
|
@ -14,11 +17,32 @@ auto Coroutine() -> boost::asio::awaitable<void> {
|
||||||
auto client = co_await larra::xmpp::client::CreateClient<larra::xmpp::PrintStream<boost::asio::ip::tcp::socket>>(
|
auto client = co_await larra::xmpp::client::CreateClient<larra::xmpp::PrintStream<boost::asio::ip::tcp::socket>>(
|
||||||
larra::xmpp::PlainUserAccount{.jid = {.username = "test1", .server = "localhost"}, .password = "test1"},
|
larra::xmpp::PlainUserAccount{.jid = {.username = "test1", .server = "localhost"}, .password = "test1"},
|
||||||
{.useTls = larra::xmpp::client::Options::kNever});
|
{.useTls = larra::xmpp::client::Options::kNever});
|
||||||
|
|
||||||
|
// rfc6120 7.1
|
||||||
|
// After a client authenticates with a server,
|
||||||
|
// it MUST bind a specific resource to the stream so that the server can properly address the client.
|
||||||
co_await std::visit(
|
co_await std::visit(
|
||||||
[](auto& client) -> boost::asio::awaitable<void> {
|
[](auto& client) -> boost::asio::awaitable<void> {
|
||||||
|
co_await client.CreateResourceBind();
|
||||||
|
},
|
||||||
|
client);
|
||||||
|
|
||||||
|
co_await std::visit(
|
||||||
|
[](auto& client) -> boost::asio::awaitable<void> {
|
||||||
|
co_await client.UpdateListOfContacts();
|
||||||
|
},
|
||||||
|
client);
|
||||||
|
|
||||||
|
// rfc6120 2.2
|
||||||
|
// Upon authenticating with a server and binding a resource (thus becoming a connected resource as defined in [XMPP‑CORE]),
|
||||||
|
// a client SHOULD request the roster before sending initial presence
|
||||||
|
co_await std::visit(
|
||||||
|
[](auto& client) -> boost::asio::awaitable<void> {
|
||||||
|
SPDLOG_INFO("Send presence: Available");
|
||||||
co_await client.Send(larra::xmpp::presence::c2s::Available{});
|
co_await client.Send(larra::xmpp::presence::c2s::Available{});
|
||||||
},
|
},
|
||||||
client);
|
client);
|
||||||
|
|
||||||
} catch(const std::exception& err) {
|
} catch(const std::exception& err) {
|
||||||
SPDLOG_ERROR("{}", err.what());
|
SPDLOG_ERROR("{}", err.what());
|
||||||
co_return;
|
co_return;
|
||||||
|
|
|
@ -11,16 +11,24 @@
|
||||||
#include <boost/asio/ssl.hpp>
|
#include <boost/asio/ssl.hpp>
|
||||||
#include <boost/asio/use_awaitable.hpp>
|
#include <boost/asio/use_awaitable.hpp>
|
||||||
#include <charconv>
|
#include <charconv>
|
||||||
|
#include <larra/bind.hpp>
|
||||||
#include <larra/client/challenge_response.hpp>
|
#include <larra/client/challenge_response.hpp>
|
||||||
#include <larra/client/options.hpp>
|
#include <larra/client/options.hpp>
|
||||||
#include <larra/client/starttls_response.hpp>
|
#include <larra/client/starttls_response.hpp>
|
||||||
#include <larra/client/xmpp_client_stream_features.hpp>
|
#include <larra/client/xmpp_client_stream_features.hpp>
|
||||||
#include <larra/encryption.hpp>
|
#include <larra/encryption.hpp>
|
||||||
#include <larra/features.hpp>
|
#include <larra/features.hpp>
|
||||||
|
#include <larra/roster.hpp>
|
||||||
#include <larra/stream.hpp>
|
#include <larra/stream.hpp>
|
||||||
#include <larra/user_account.hpp>
|
#include <larra/user_account.hpp>
|
||||||
#include <larra/xml_stream.hpp>
|
#include <larra/xml_stream.hpp>
|
||||||
#include <ranges>
|
#include <ranges>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace rng = std::ranges;
|
||||||
|
namespace views = std::views;
|
||||||
|
namespace iq = larra::xmpp::iq;
|
||||||
|
|
||||||
namespace larra::xmpp {
|
namespace larra::xmpp {
|
||||||
|
|
||||||
constexpr auto kDefaultXmppPort = 5222;
|
constexpr auto kDefaultXmppPort = 5222;
|
||||||
|
@ -29,12 +37,10 @@ constexpr auto kDefaultXmppPort = 5222;
|
||||||
|
|
||||||
namespace larra::xmpp::client {
|
namespace larra::xmpp::client {
|
||||||
|
|
||||||
namespace rng = std::ranges;
|
|
||||||
namespace views = std::views;
|
|
||||||
|
|
||||||
template <typename Connection>
|
template <typename Connection>
|
||||||
struct Client {
|
struct Client {
|
||||||
constexpr Client(BareJid jid, XmlStream<Connection> connection) : jid(std::move(jid)), connection(std::move(connection)) {};
|
constexpr Client(BareJid jid, XmlStream<Connection> connection) : jid(std::move(jid)), connection(std::move(connection)) {
|
||||||
|
}
|
||||||
template <boost::asio::completion_token_for<void()> Token = boost::asio::use_awaitable_t<>>
|
template <boost::asio::completion_token_for<void()> Token = boost::asio::use_awaitable_t<>>
|
||||||
constexpr auto Close(Token token = {}) {
|
constexpr auto Close(Token token = {}) {
|
||||||
this->active = false;
|
this->active = false;
|
||||||
|
@ -68,14 +74,35 @@ struct Client {
|
||||||
auto Send(const T& object) -> boost::asio::awaitable<void> {
|
auto Send(const T& object) -> boost::asio::awaitable<void> {
|
||||||
co_await this->connection.Send(object);
|
co_await this->connection.Send(object);
|
||||||
}
|
}
|
||||||
[[nodiscard]] constexpr auto Jid() const -> const BareJid& {
|
[[nodiscard]] constexpr auto Jid() const -> const FullJid& {
|
||||||
return this->jid;
|
return this->jid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto CreateResourceBind() -> boost::asio::awaitable<void> {
|
||||||
|
SPDLOG_INFO("Send IQ: Set::Bind");
|
||||||
|
co_await this->Send(::iq::MakeSetBind());
|
||||||
|
|
||||||
|
auto bind_result = co_await connection.template Read<::iq::ResultBind>();
|
||||||
|
jid.resource = std::move(bind_result.payload.jid->resource);
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto UpdateListOfContacts() -> boost::asio::awaitable<void> {
|
||||||
|
SPDLOG_INFO("Send IQ: Get::Roster");
|
||||||
|
co_await this->Send(::iq::MakeGetRoster(jid));
|
||||||
|
|
||||||
|
const auto roster_result = co_await connection.template Read<::iq::ResultRoster>();
|
||||||
|
roster = std::move(roster_result.payload);
|
||||||
|
|
||||||
|
SPDLOG_INFO("New roster: {}", ToString(roster));
|
||||||
|
co_return;
|
||||||
|
|||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool active = true;
|
bool active = true;
|
||||||
XmlStream<Connection> connection{};
|
XmlStream<Connection> connection{};
|
||||||
BareJid jid;
|
FullJid jid;
|
||||||
|
::iq::Roster roster;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct StartTlsNegotiationError : std::runtime_error {
|
struct StartTlsNegotiationError : std::runtime_error {
|
||||||
|
@ -116,15 +143,14 @@ struct Challenge {
|
||||||
throw std::runtime_error(std::format("Invalid name {} for challenge", node->get_name()));
|
throw std::runtime_error(std::format("Invalid name {} for challenge", node->get_name()));
|
||||||
}
|
}
|
||||||
std::string decoded = DecodeBase64(node->get_first_child_text()->get_content());
|
std::string decoded = DecodeBase64(node->get_first_child_text()->get_content());
|
||||||
auto params = std::views::split(decoded, ',') //
|
auto params = std::views::split(decoded, ',') | std::views::transform([](auto param) {
|
||||||
| std::views::transform([](auto param) { //
|
return std::string_view{param};
|
||||||
return std::string_view{param}; //
|
}) |
|
||||||
}) //
|
std::views::transform([](std::string_view param) -> std::pair<std::string_view, std::string_view> {
|
||||||
| std::views::transform([](std::string_view param) -> std::pair<std::string_view, std::string_view> { //
|
auto v = param.find("=");
|
||||||
auto v = param.find("="); //
|
return {param.substr(0, v), param.substr(v + 1)};
|
||||||
return {param.substr(0, v), param.substr(v + 1)}; //
|
}) |
|
||||||
}) //
|
std::ranges::to<std::unordered_map<std::string_view, std::string_view>>();
|
||||||
| std::ranges::to<std::unordered_map<std::string_view, std::string_view>>();
|
|
||||||
return {.body = std::move(decoded),
|
return {.body = std::move(decoded),
|
||||||
.serverNonce = params.at("r"),
|
.serverNonce = params.at("r"),
|
||||||
.salt = DecodeBase64(params.at("s")),
|
.salt = DecodeBase64(params.at("s")),
|
||||||
|
|
76
library/include/larra/roster.hpp
Normal file
76
library/include/larra/roster.hpp
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
#pragma once
|
||||||
|
#include <libxml++/libxml++.h>
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
|
#include <larra/iq.hpp>
|
||||||
|
#include <larra/jid.hpp>
|
||||||
|
#include <larra/utils.hpp>
|
||||||
|
#include <ranges>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace larra::xmpp::iq {
|
||||||
|
|
||||||
|
struct Roster {
|
||||||
|
static constexpr auto kDefaultName = "query";
|
||||||
|
static constexpr auto kDefaultNamespace = "jabber:iq:roster";
|
||||||
|
|
||||||
|
std::vector<BareJid> items;
|
||||||
|
|
||||||
|
friend auto ToString(const Roster& roster) -> std::string {
|
||||||
|
static constexpr std::string_view prefix = "Roster: [\n\t";
|
||||||
|
static constexpr std::string_view suffix = "]";
|
||||||
|
// \n\r\t
|
||||||
|
std::size_t total_length = std::ranges::fold_left(roster.items | std::views::transform([](const auto& el) {
|
||||||
|
return larra::xmpp::utils::AccumulateFieldLength(el) + 3;
|
||||||
|
}),
|
||||||
|
prefix.length() + suffix.length(),
|
||||||
|
std::plus<>{});
|
||||||
|
|
||||||
|
std::string s;
|
||||||
|
s.resize(total_length);
|
||||||
|
s = prefix;
|
||||||
|
for(const auto& el : roster.items) {
|
||||||
|
s += ToString(el);
|
||||||
|
s += "\n\t";
|
||||||
|
}
|
||||||
|
return s += suffix;
|
||||||
|
}
|
||||||
|
friend constexpr auto operator<<(xmlpp::Element* element, const Roster& roster) {
|
||||||
|
element->set_attribute("xmlns", Roster::kDefaultNamespace);
|
||||||
|
std::ranges::for_each(roster.items, [element](const auto& item) {
|
||||||
|
element->add_child_element("item")->set_attribute("jid", ToString(item));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
[[nodiscard]] static constexpr auto Parse(xmlpp::Element* element) -> Roster {
|
||||||
|
const auto& item_nodes = element->get_children("item");
|
||||||
|
if(item_nodes.empty()) {
|
||||||
|
SPDLOG_DEBUG("No items at Iq::Roster");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {.items = item_nodes | std::views::transform([](const xmlpp::Node* node) {
|
||||||
|
auto item_element = dynamic_cast<const xmlpp::Element*>(node);
|
||||||
|
if(!item_element) {
|
||||||
|
throw std::runtime_error("Can't convert xmlpp::Node to xmlpp::Element");
|
||||||
sha512sum
commented
You can create a structure that contains jid as an attribute and use automatic generation of serialization and deserialization You can create a structure that contains jid as an attribute and use automatic generation of serialization and deserialization
|
|||||||
|
}
|
||||||
|
|
||||||
|
auto jid_ptr = item_element->get_attribute("jid");
|
||||||
|
if(!jid_ptr) {
|
||||||
|
throw std::runtime_error("Not found attribute 'jid' for parse Roster item");
|
||||||
|
}
|
||||||
|
|
||||||
|
return BareJid::Parse(jid_ptr->get_value());
|
||||||
|
}) |
|
||||||
|
std::ranges::to<std::vector<BareJid>>()};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using GetRoster = Get<Roster>;
|
||||||
|
using ResultRoster = Result<Roster>;
|
||||||
|
using IqRoster = Iq<Roster>;
|
||||||
|
|
||||||
|
inline auto MakeGetRoster(const FullJid& jid) {
|
||||||
|
return GetRoster{.id = "1", .from = ToString(jid), .payload = Roster{}};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace larra::xmpp::iq
|
|
@ -295,4 +295,18 @@ struct RangeToWrapper : T {
|
||||||
: T{std::forward<Args>(args)...} {};
|
: T{std::forward<Args>(args)...} {};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
concept LengthCalculatable = requires(const T& obj) {
|
||||||
|
{ obj.length() } -> std::convertible_to<std::size_t>; // Checks if obj has a length() method returning a type convertible to std::size_t
|
||||||
|
} || std::convertible_to<T, std::string>;
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
auto AccumulateFieldLength(const T& obj) -> std::size_t {
|
||||||
|
std::size_t total_length = 0;
|
||||||
|
boost::pfr::for_each_field(obj, [&](const LengthCalculatable auto& field) {
|
||||||
|
total_length += field.length(); // Accumulate length of each field
|
||||||
|
});
|
||||||
|
return total_length;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace larra::xmpp::utils
|
} // namespace larra::xmpp::utils
|
||||||
|
|
39
tests/roster.cpp
Normal file
39
tests/roster.cpp
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <larra/roster.hpp>
|
||||||
|
|
||||||
|
#include "larra/jid.hpp"
|
||||||
|
|
||||||
|
namespace larra::xmpp {
|
||||||
|
|
||||||
|
TEST(Roster, SerializeAndParse) {
|
||||||
|
FullJid jid{.username = "test", .server = "server", .resource = "res"}; // NOLINT
|
||||||
|
auto roster = iq::MakeGetRoster(jid);
|
||||||
|
roster.payload.items.emplace_back("u1", "s1");
|
||||||
|
roster.payload.items.emplace_back("u2", "s2");
|
||||||
Ivan-lis marked this conversation as resolved
sha512sum
commented
Why not just throw all the necessary elements through the vector constructor in Roster constructor? Why not just throw all the necessary elements through the vector constructor in Roster constructor?
|
|||||||
|
roster.payload.items.emplace_back("u3", "s3");
|
||||||
|
|
||||||
|
xmlpp::Document doc;
|
||||||
|
auto node = doc.create_root_node("iq");
|
||||||
|
node << roster;
|
||||||
|
|
||||||
|
auto parse_res = decltype(roster)::Parse(node);
|
||||||
|
|
||||||
|
ASSERT_EQ(roster.payload.items.size(), parse_res.payload.items.size());
|
||||||
|
for(const auto& [idx, expect_el, parsed_el] : std::views::zip(std::views::iota(0), roster.payload.items, parse_res.payload.items)) {
|
||||||
|
EXPECT_EQ(expect_el, parsed_el) << "Mismatched on idx: " << idx;
|
||||||
|
// std::cerr << " " << "idx: " << idx << "; expect_el: " << expect_el << "; parsed_el: " << parsed_el << '\n';
|
||||||
Ivan-lis marked this conversation as resolved
sha512sum
commented
Code left in the comment Code left in the comment
|
|||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Roster, Print) {
|
||||||
|
FullJid jid{.username = "test", .server = "server", .resource = "res"}; // NOLINT
|
||||||
|
auto roster = iq::MakeGetRoster(jid);
|
||||||
|
roster.payload.items.emplace_back("u1", "s1");
|
||||||
|
roster.payload.items.emplace_back("u2", "s2");
|
||||||
|
roster.payload.items.emplace_back("u3", "s3");
|
||||||
|
|
||||||
|
EXPECT_NO_THROW({ std::cerr << "[ ] Roster payload: " << ToString(roster.payload) << '\n'; });
|
||||||
Ivan-lis marked this conversation as resolved
sha512sum
commented
EXPECT_EQ and test content EXPECT_EQ and test content
|
|||||||
|
}
|
||||||
|
|
||||||
|
} // namespace larra::xmpp
|
Loading…
Reference in a new issue
Maybe we can change namings later