Don't hardcode ver, update node name, and add tests

This commit is contained in:
Luca Matei Pintilie
2026-08-19 21:30:27 +02:00
parent 567b139e1c
commit df83dd19ae
10 changed files with 223 additions and 13 deletions
+17 -10
View File
@@ -1,3 +1,4 @@
#include <xmpp/hash_disco.hpp>
#include <xmpp/biboumi_component.hpp>
#include <utils/timed_events.hpp>
@@ -13,7 +14,6 @@
#include <xmpp/jid.hpp>
#include <stdexcept>
#include <iostream>
#include <cstdlib>
@@ -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<Bridge*> 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);
}
+3
View File
@@ -1,5 +1,6 @@
#pragma once
#include "xmpp/hash_disco.hpp"
#include <database/database.hpp>
#include <xmpp/xmpp_component.hpp>
#include <xmpp/jid.hpp>
@@ -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);
+50
View File
@@ -0,0 +1,50 @@
#include <utils/base64.hpp>
#include <utils/sha1.hpp>
#include <xmpp/xmpp_component.hpp>
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<const XmlNode *> 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<const XmlNode *> 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
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include <xmpp/xmpp_stanza.hpp>
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);
}
+2 -1
View File
@@ -12,7 +12,8 @@
#include <memory>
#include <string>
#include <ctime>
#include <map>
#define NODE_NAME "https://biboumi.codeberg.page/"
#define STREAM_NS "http://etherx.jabber.org/streams"
#define COMPONENT_NS "jabber:component:accept"
+1 -1
View File
@@ -163,4 +163,4 @@ public:
}
private:
XmlNode& parent_to_add;
};
};
+1
View File
@@ -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',
+23
View File
@@ -0,0 +1,23 @@
from scenarios import *
scenario = (
send_stanza("<presence id='get1' from='{jid_one}/{resource_one}' to='{biboumi_host}' xmlns='jabber:component:accept'></presence>"),
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("<presence id='get2' from='{jid_one}/{resource_one}' to='{biboumi_host}' xmlns='jabber:component:accept'></presence>"),
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("<iq type='get' id='get3' from='{jid_one}/{resource_one}' to='{biboumi_host}'><query xmlns='http://jabber.org/protocol/disco#info' node='{node}#{ver}' /></iq>"),
expect_stanza("/iq/disco_info:query[@node='{node}#{ver}']"),
# But if we ask for invalid values then we don't
send_stanza("<iq type='get' id='get4' from='{jid_one}/{resource_one}' to='{biboumi_host}' xmlns='jabber:component:accept'><query xmlns='http://jabber.org/protocol/disco#info' node='{node}#asdf' /></iq>"),
expect_stanza("!/iq/disco_info:query[@node='{node}#{ver}']"),
send_stanza("<iq type='get' id='get5' from='{jid_one}/{resource_one}' to='{biboumi_host}' xmlns='jabber:component:accept'><query xmlns='http://jabber.org/protocol/disco#info' node='asdf' /></iq>"),
expect_stanza("!/iq/disco_info:query[@node='{node}#{ver}']"),
)
@@ -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']",
),
)
+106
View File
@@ -0,0 +1,106 @@
#include "catch2/catch_test_macros.hpp"
#include <xmpp/hash_disco.hpp>
#include <xmpp/xmpp_parser.hpp>
#include <xmpp/xmpp_stanza.hpp>
// WARN: This generation might be wrong!!! These test might be wrong!!!
TEST_CASE("Hash disco 1") {
XmppParser xml;
const std::string doc =
R"(<stream><iq type='result'><query xmlns="http://jabber.org/protocol/disco#info">
<identity xmlns='' category="client" name="BombusMod" type="mobile"/>
<feature xmlns='' var="http://jabber.org/protocol/si"/>
<feature xmlns='' var="http://jabber.org/protocol/bytestreams"/>
<feature xmlns='' var="http://jabber.org/protocol/chatstates"/>
<feature xmlns='' var="http://jabber.org/protocol/disco#info"/>
<feature xmlns='' var="http://jabber.org/protocol/disco#items"/>
<feature xmlns='' var="urn:xmpp:ping"/>
<feature xmlns='' var="jabber:iq:time"/>
<feature xmlns='' var="jabber:iq:privacy"/>
<feature xmlns='' var="jabber:iq:version"/>
<feature xmlns='' var="http://jabber.org/protocol/rosterx"/>
<feature xmlns='' var="urn:xmpp:time"/>
<feature xmlns='' var="jabber:x:oob"/>
<feature xmlns='' var="http://jabber.org/protocol/ibb"/>
<feature xmlns='' var="http://jabber.org/protocol/si/profile/file-transfer"/>
<feature xmlns='' var="urn:xmpp:receipts"/>
<feature xmlns='' var="jabber:iq:roster"/>
<feature xmlns='' var="jabber:iq:last"/>
</query></iq></stream>)";
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<int>::max());
xml.feed(doc.data(), static_cast<int>(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"(<stream><iq type='result'><query xmlns='http://jabber.org/protocol/disco#info'>
<identity xmlns='' xml:lang='el' category='client' name='Ψ 0.11' type='pc'/>
<identity xmlns='' xml:lang='en' category='client' name='Psi 0.11' type='mobile'/>
<feature xmlns='' var='http://jabber.org/protocol/caps'/>
<feature xmlns='' var='http://jabber.org/protocol/disco#info'/>
<feature xmlns='' var='http://jabber.org/protocol/disco#items'/>
<feature xmlns='' var='http://jabber.org/protocol/muc'/>
</query></iq></stream>)";
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<int>::max());
xml.feed(doc.data(), static_cast<int>(doc.size()), true);
}
TEST_CASE("Hash disco 2 different order") {
XmppParser xml;
const std::string doc =
R"(<stream><iq type='result'><query xmlns='http://jabber.org/protocol/disco#info'>
<identity xmlns='' xml:lang='en' category='client' name='Psi 0.11' type='mobile'/>
<identity xmlns='' xml:lang='el' category='client' name='Ψ 0.11' type='pc'/>
<feature xmlns='' var='http://jabber.org/protocol/disco#info'/>
<feature xmlns='' var='http://jabber.org/protocol/caps'/>
<feature xmlns='' var='http://jabber.org/protocol/disco#items'/>
<feature xmlns='' var='http://jabber.org/protocol/muc'/>
</query></iq></stream>)";
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<int>::max());
xml.feed(doc.data(), static_cast<int>(doc.size()), true);
}