From df83dd19ae0888e206d58f2eb3bf2476f1b28755 Mon Sep 17 00:00:00 2001 From: Luca Matei Pintilie Date: Sat, 1 Aug 2026 14:54:23 +0200 Subject: [PATCH] Don't hardcode ver, update node name, and add tests --- src/xmpp/biboumi_component.cpp | 27 +++-- src/xmpp/biboumi_component.hpp | 3 + src/xmpp/hash_disco.cpp | 50 +++++++++ src/xmpp/hash_disco.hpp | 16 +++ src/xmpp/xmpp_component.hpp | 3 +- src/xmpp/xmpp_stanza.hpp | 2 +- tests/end_to_end/functions.py | 1 + tests/end_to_end/scenarios/self_caps.py | 23 ++++ tests/end_to_end/scenarios/self_disco_info.py | 5 +- tests/hash_disco.cpp | 106 ++++++++++++++++++ 10 files changed, 223 insertions(+), 13 deletions(-) create mode 100644 src/xmpp/hash_disco.cpp create mode 100644 src/xmpp/hash_disco.hpp create mode 100644 tests/end_to_end/scenarios/self_caps.py create mode 100644 tests/hash_disco.cpp diff --git a/src/xmpp/biboumi_component.cpp b/src/xmpp/biboumi_component.cpp index 60e06bc..dc83317 100644 --- a/src/xmpp/biboumi_component.cpp +++ b/src/xmpp/biboumi_component.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -13,7 +14,6 @@ #include #include -#include #include @@ -545,7 +545,8 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) const std::string node = query->get_tag("node"); if (to_str == this->served_hostname) { - if (node.empty() || node.rfind("https://biboumi.louiz.org/#", 0) == 0) + const std::string expected_node_name = (NODE_NAME "#" + this->get_self_ver()); + if (node.empty() || node == expected_node_name) { // On the gateway itself this->send_self_disco_info(id, from, node); @@ -952,21 +953,18 @@ std::vector BiboumiComponent::get_bridges() const return res; } -void BiboumiComponent::send_self_disco_info(const std::string& id, const std::string& jid_to, const std::string& node) +Stanza BiboumiComponent::get_self_disco_info(const std::string &node) { Stanza iq("iq"); { iq["type"] = "result"; - iq["id"] = id; - if (!node.empty()) iq["node"] = node; - iq["to"] = jid_to; - iq["from"] = this->served_hostname; XmlSubNode query(iq, "query"); query["xmlns"] = DISCO_INFO_NS; + if (!node.empty()) query["node"] = node; XmlSubNode gwid(query, "identity"); gwid["category"] = "gateway"; gwid["type"] = "irc"; - gwid["name"] = "Biboumi"; + gwid["name"] = "Biboumi (" SOFTWARE_VERSION ")"; XmlSubNode identity(query, "identity"); identity["category"] = "conference"; identity["type"] = "irc"; @@ -977,6 +975,15 @@ void BiboumiComponent::send_self_disco_info(const std::string& id, const std::st feature["var"] = ns; } } + return iq; +} + +void BiboumiComponent::send_self_disco_info(const std::string& id, const std::string& jid_to, const std::string& node) +{ + Stanza iq = this->get_self_disco_info(node); + iq["id"] = id; + iq["to"] = jid_to; + iq["from"] = this->served_hostname; this->send_stanza(iq); } @@ -1213,8 +1220,8 @@ void BiboumiComponent::send_presence_to_contact(const std::string& from, const s XmlSubNode c(presence, "c"); c["xmlns"] = CAPS_NS; c["hash"] = "sha-1"; - c["node"] = "https://biboumi.louiz.org/"; - c["ver"] = "URGCt+50NTdI6Ifcm1/YYNVxSjc="; + c["node"] = NODE_NAME; + c["ver"] = this->get_self_ver(); } this->send_stanza(presence); } diff --git a/src/xmpp/biboumi_component.hpp b/src/xmpp/biboumi_component.hpp index 6b63366..1655a97 100644 --- a/src/xmpp/biboumi_component.hpp +++ b/src/xmpp/biboumi_component.hpp @@ -1,5 +1,6 @@ #pragma once +#include "xmpp/hash_disco.hpp" #include #include #include @@ -90,6 +91,8 @@ public: void send_invitation(const std::string& room_target, const std::string& jid_to, const std::string& author_nick); private: void send_invitation_from_fulljid(const std::string& room_target, const std::string& jid_to, const std::string& from); + static Stanza get_self_disco_info(const std::string &node); + static std::string get_self_ver() { return Xmpp::hash_disco_sha1(get_self_disco_info("")); }; public: void accept_subscription(const std::string& from, const std::string& to); void ask_subscription(const std::string& from, const std::string& to); diff --git a/src/xmpp/hash_disco.cpp b/src/xmpp/hash_disco.cpp new file mode 100644 index 0000000..bb63766 --- /dev/null +++ b/src/xmpp/hash_disco.cpp @@ -0,0 +1,50 @@ +#include +#include +#include + +namespace Xmpp { +const std::string hash_disco_sha1(const Stanza &stanza) { + std::string s(""); + const Stanza *query = stanza.get_child("query", DISCO_INFO_NS); + std::vector identities = query->get_children("identity", ""); + + std::sort(identities.begin(), identities.end(), + [](const XmlNode *a, const XmlNode *b) { + const std::string category_a = a->get_tag("category"); + const std::string category_b = b->get_tag("category"); + const int category_compare = category_a > category_b; + if (category_compare != 0) + return category_compare; + const std::string type_a = a->get_tag("type"); + const std::string type_b = b->get_tag("type"); + const int type_compare = type_a > type_b; + return type_compare; + }); + + for (const XmlNode *identity : identities) { + const std::string category = identity->get_tag("category"); + const std::string type = identity->get_tag("type"); + const std::string name = identity->get_tag("name"); + s += category + "/" + type + "/" /* + lang + */ "/" + name + "<"; + } + std::vector features = query->get_children("feature", ""); + + std::sort(features.begin(), features.end(), + [](const XmlNode *a, const XmlNode *b) { + const std::string var_a = a->get_tag("var"); + const std::string var_b = b->get_tag("var"); + const int var_compare = var_a > var_b; + return var_compare; + }); + + for (const XmlNode *feature : features) { + const std::string var = feature->get_tag("var"); + s += var + "<"; + } + + const std::string hashed = sha1(s); + const std::string encoded = base64::encode(hashed); + + return encoded; +} +} // namespace Xmpp diff --git a/src/xmpp/hash_disco.hpp b/src/xmpp/hash_disco.hpp new file mode 100644 index 0000000..d111e69 --- /dev/null +++ b/src/xmpp/hash_disco.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace Xmpp +{ + /** + * Generate a verification string according to XEP-0115 + * https://xmpp.org/extensions/xep-0115.html#ver + * + * This does NOT support Service Discovery Extensions (XEP-0128). + * Nor does this sort identities by language + */ + const std::string hash_disco_sha1(const Stanza &stanza); +} + diff --git a/src/xmpp/xmpp_component.hpp b/src/xmpp/xmpp_component.hpp index 6bc2892..3ad68e4 100644 --- a/src/xmpp/xmpp_component.hpp +++ b/src/xmpp/xmpp_component.hpp @@ -12,7 +12,8 @@ #include #include #include -#include + +#define NODE_NAME "https://biboumi.codeberg.page/" #define STREAM_NS "http://etherx.jabber.org/streams" #define COMPONENT_NS "jabber:component:accept" diff --git a/src/xmpp/xmpp_stanza.hpp b/src/xmpp/xmpp_stanza.hpp index a706337..8f9a6c1 100644 --- a/src/xmpp/xmpp_stanza.hpp +++ b/src/xmpp/xmpp_stanza.hpp @@ -163,4 +163,4 @@ public: } private: XmlNode& parent_to_add; -}; \ No newline at end of file +}; diff --git a/tests/end_to_end/functions.py b/tests/end_to_end/functions.py index 86ac04e..7588489 100644 --- a/tests/end_to_end/functions.py +++ b/tests/end_to_end/functions.py @@ -46,6 +46,7 @@ def match(stanza, xpath): 'disco_info': 'http://jabber.org/protocol/disco#info', 'muc_traffic': 'http://jabber.org/protocol/muc#traffic', 'disco_items': 'http://jabber.org/protocol/disco#items', + 'caps': 'http://jabber.org/protocol/caps', 'commands': 'http://jabber.org/protocol/commands', 'dataform': 'jabber:x:data', 'version': 'jabber:iq:version', diff --git a/tests/end_to_end/scenarios/self_caps.py b/tests/end_to_end/scenarios/self_caps.py new file mode 100644 index 0000000..9bd57d2 --- /dev/null +++ b/tests/end_to_end/scenarios/self_caps.py @@ -0,0 +1,23 @@ +from scenarios import * + +scenario = ( + send_stanza(""), + expect_stanza("/presence/caps:c[@hash='sha-1'][@node][@ver]", + after = ( + save_value("ver", extract_attribute("/presence/caps:c[@hash='sha-1'][@node][@ver]", "ver")), + save_value("node", extract_attribute("/presence/caps:c[@hash='sha-1'][@node][@ver]", "node")), + ) + ), + # Assert node and ver are consistent + send_stanza(""), + expect_stanza("/presence/caps:c[@hash='sha-1'][@node='{node}'][@ver='{ver}']"), + # If we ask for this node then we should get it back + send_stanza(""), + expect_stanza("/iq/disco_info:query[@node='{node}#{ver}']"), + # But if we ask for invalid values then we don't + send_stanza(""), + expect_stanza("!/iq/disco_info:query[@node='{node}#{ver}']"), + send_stanza(""), + expect_stanza("!/iq/disco_info:query[@node='{node}#{ver}']"), +) + diff --git a/tests/end_to_end/scenarios/self_disco_info.py b/tests/end_to_end/scenarios/self_disco_info.py index 6430dbd..fef5905 100644 --- a/tests/end_to_end/scenarios/self_disco_info.py +++ b/tests/end_to_end/scenarios/self_disco_info.py @@ -7,5 +7,8 @@ scenario = ( "/iq/disco_info:query/disco_info:feature[@var='http://jabber.org/protocol/commands']", "/iq/disco_info:query/disco_info:feature[@var='urn:xmpp:ping']", "/iq/disco_info:query/disco_info:feature[@var='urn:xmpp:mam:2']", - "/iq/disco_info:query/disco_info:feature[@var='jabber:iq:version']"), + "/iq/disco_info:query/disco_info:feature[@var='jabber:iq:version']", + "/iq/disco_info:query/disco_info:identity[@category='conference'][@type='irc']", + "/iq/disco_info:query/disco_info:identity[@category='gateway'][@type='irc']", + ), ) diff --git a/tests/hash_disco.cpp b/tests/hash_disco.cpp new file mode 100644 index 0000000..6d570a2 --- /dev/null +++ b/tests/hash_disco.cpp @@ -0,0 +1,106 @@ +#include "catch2/catch_test_macros.hpp" +#include +#include +#include + +// WARN: This generation might be wrong!!! These test might be wrong!!! + +TEST_CASE("Hash disco 1") { + XmppParser xml; + + const std::string doc = + R"( + + + + + + + + + + + + + + + + + + +)"; + + auto check_stanza = [](const Stanza &stanza) { + const std::string hash = Xmpp::hash_disco_sha1(stanza); + CHECK(hash == "YjVkN2NhODU2ZWQyZjhhNjJjOTE1MWMxYjQ0YmUwM2Q0ODhhZDI2MA=="); + }; + xml.add_stanza_callback([check_stanza](const Stanza &stanza) { + check_stanza(stanza); + // Do the same checks on a copy of that stanza. + Stanza copy(stanza); + check_stanza(copy); + // And do the same checks on moved-constructed stanza + Stanza moved(std::move(copy)); + }); + CHECK(doc.size() <= std::numeric_limits::max()); + xml.feed(doc.data(), static_cast(doc.size()), true); +} + +// Just one more test case to make sure we aren't always generating the same string +TEST_CASE("Hash disco 2") { + XmppParser xml; + + const std::string doc = + R"( + + + + + + +)"; + + auto check_stanza = [](const Stanza &stanza) { + const std::string hash = Xmpp::hash_disco_sha1(stanza); + CHECK(hash == "ZmY1ZTJjZjUyZjkwMjA2OTliZGFhYzYxMmVmMTI5YjdmYzNhZTIwYw=="); + }; + xml.add_stanza_callback([check_stanza](const Stanza &stanza) { + check_stanza(stanza); + // Do the same checks on a copy of that stanza. + Stanza copy(stanza); + check_stanza(copy); + // And do the same checks on moved-constructed stanza + Stanza moved(std::move(copy)); + }); + CHECK(doc.size() <= std::numeric_limits::max()); + xml.feed(doc.data(), static_cast(doc.size()), true); +} + +TEST_CASE("Hash disco 2 different order") { + XmppParser xml; + + const std::string doc = + R"( + + + + + + +)"; + + auto check_stanza = [](const Stanza &stanza) { + const std::string hash = Xmpp::hash_disco_sha1(stanza); + CHECK(hash == "ZmY1ZTJjZjUyZjkwMjA2OTliZGFhYzYxMmVmMTI5YjdmYzNhZTIwYw=="); + }; + xml.add_stanza_callback([check_stanza](const Stanza &stanza) { + check_stanza(stanza); + // Do the same checks on a copy of that stanza. + Stanza copy(stanza); + check_stanza(copy); + // And do the same checks on moved-constructed stanza + Stanza moved(std::move(copy)); + }); + CHECK(doc.size() <= std::numeric_limits::max()); + xml.feed(doc.data(), static_cast(doc.size()), true); +}