Compare commits

..
10 Commits
Author SHA1 Message Date
Stephen Paul Weber 25dfcba60d Sort was backward 2026-09-24 22:07:38 -05:00
Stephen Paul Weber 2217cbeab3 Fix for doing base64(hex(sha1))
Instead we want to base64 encode the raw sha1
2026-09-24 21:38:34 -05:00
Stephen Paul Weber 69b8797d3b Also support caps on irc server JIDs 2026-09-24 21:10:55 -05:00
Stephen Paul Weber 3dbbf5910e Fix guix build 2026-09-24 20:44:21 -05:00
Stephen Paul Weber 600068440f Add new dependencies 2026-09-23 22:01:59 -05:00
Stephen Paul Weber 9eb19d53e7 Update CI image 2026-09-23 22:00:43 -05:00
Stephen Paul Weber c438554f9a Ignore nick change when there is a configured nick
The user has configured their nick and that's what we should use. If
they send a nick change, just reply letting them know what their nick
actually is.
2026-09-23 21:59:20 -05:00
Stephen Paul Weber d8bc3d17bb IRC nicks cannot contain spaces
Rather than trying and hoping the server gives a proper failure, just
fail early and avoid the whole mess. Some servers will instead send
messages to the user, etc, which is not very helpful.
2026-09-23 21:58:20 -05:00
Stephen Paul Weber 99d7f8a41a Do not send NICK command when nick not changed
If we get a presence, that is not always a nick change. Don't bother the
IRC server with a command to change the nick if we're not changing it.
2026-09-23 21:57:29 -05:00
Stephen Paul Weber ff173a8ea3 Memory fixes 2026-09-23 21:12:31 -05:00
15 changed files with 312 additions and 24 deletions

No files matched your search

+3 -2
View File
@@ -1,4 +1,4 @@
image: debian/buster
image: debian/stable
sources:
- https://git.sr.ht/~singpolyma/biboumi
packages:
@@ -12,8 +12,9 @@ packages:
- libidn11-dev
- libidn2-dev
- libudns-dev
- libbotan-2-dev
- libbotan-3-dev
- libsystemd-dev
- libcurl4-openssl-dev
artifacts:
- biboumi/biboumi
environment:
+133 -2
View File
@@ -3,17 +3,144 @@
(guix packages)
(guix download)
(guix git-download)
(guix gexp)
(guix build-system cmake)
(guix build-system gnu)
(guix utils)
(gnu packages)
(gnu packages compression)
(gnu packages crypto)
(gnu packages curl)
(gnu packages databases)
(gnu packages gcc)
(gnu packages libidn)
(gnu packages linux)
(gnu packages python)
(gnu packages sqlite)
(gnu packages python-xyz)
(gnu packages readline)
(gnu packages version-control)
(gnu packages xml)
(ice-9 match)
(ice-9 rdelim)
(ice-9 popen))
(ice-9 popen)
(srfi srfi-26))
(define (sqlite-uri version year)
(let ((numeric-version
(match (string-split version #\.)
((first-digit other-digits ...)
(string-append first-digit
(string-pad-right
(string-concatenate
(map (cut string-pad <> 2 #\0)
other-digits))
6 #\0))))))
(string-append "https://sqlite.org/" (number->string year)
"/sqlite-autoconf-" numeric-version ".tar.gz")))
(define-public sqlite
(package
(name "sqlite")
(version "3.39.3")
(source (origin
(method url-fetch)
(uri (sqlite-uri version 2022))
(patches (search-patches "sqlite-hurd.patch"))
(sha256
(base32
"1f922kq16g7f4h3gpzim78lvrp5xw9nvlvqw97s2qgxyh8qgns3q"))))
(build-system gnu-build-system)
(inputs (list readline))
(outputs '("out" "static"))
(arguments
`(#:configure-flags
;; Add -DSQLITE_SECURE_DELETE, -DSQLITE_ENABLE_FTS3,
;; -DSQLITE_ENABLE_UNLOCK_NOTIFY and -DSQLITE_ENABLE_DBSTAT_VTAB
;; to CFLAGS. GNU Icecat will refuse to use the system SQLite
;; unless these options are enabled.
(list (string-append "CFLAGS=-O2 -g -DSQLITE_SECURE_DELETE "
"-DSQLITE_ENABLE_FTS3 "
"-DSQLITE_ENABLE_UNLOCK_NOTIFY "
"-DSQLITE_ENABLE_DBSTAT_VTAB "
;; Column metadata is required by GNU Jami and Qt, et.al.
"-DSQLITE_ENABLE_COLUMN_METADATA"))
#:phases (modify-phases %standard-phases
(add-after 'install 'move-static-library
(lambda* (#:key outputs #:allow-other-keys)
(let* ((out (assoc-ref outputs "out"))
(static (assoc-ref outputs "static"))
(source (string-append out "/lib/libsqlite3.a")))
(mkdir-p (string-append static "/lib"))
(link source (string-append static "/lib/libsqlite3.a"))
(delete-file source)
;; Remove reference to the static library from the .la file
;; so that Libtool looks for it in the usual places.
(substitute* (string-append out "/lib/libsqlite3.la")
(("^old_library=.*")
"old_library=''\n"))
#t))))))
(home-page "https://www.sqlite.org/")
(synopsis "The SQLite database management system")
(description
"SQLite is a software library that implements a self-contained, serverless,
zero-configuration, transactional SQL database engine. SQLite is the most
widely deployed SQL database engine in the world. The source code for SQLite
is in the public domain.")
(license license:public-domain)))
(define-public botan
(package
(name "botan")
(version "3.9.0")
(source (origin
(method git-fetch)
(uri (git-reference
(url "https://github.com/randombit/botan/")
(commit version)))
(file-name (git-file-name name version))
(sha256
(base32
"1k5dyn39b77bfawly2yms0crbkn6g7msh16q7bxnh7dra4cnkmsn"))))
(build-system gnu-build-system)
(arguments
(list
#:phases
#~(modify-phases %standard-phases
(replace 'configure
(lambda* (#:key inputs outputs #:allow-other-keys)
;; Upstream tests and benchmarks with -O3.
(setenv "CXXFLAGS" "-O3")
(invoke "python" "./configure.py"
(string-append "--prefix=" #$output)
"--disable-static"
;; Otherwise, the `botan` executable cannot find
;; libbotan.
(string-append "--ldflags=-Wl,-rpath=" #$output "/lib")
"--with-os-feature=getentropy"
"--with-rst2man"
;; Recommended by upstream
"--with-zlib" "--with-bzip2" "--with-sqlite3")))
(add-before 'check 'library-path-for-tests
(lambda _ (setenv "LD_LIBRARY_PATH" (getcwd))))
(replace 'check
(lambda* (#:key tests? #:allow-other-keys)
(when tests?
(invoke "./botan-test")))))))
(native-inputs (list python-wrapper python-docutils gcc-12))
(inputs (list sqlite bzip2 zlib))
(synopsis "Cryptographic library in C++11")
(description "Botan is a cryptography library, written in C++11, offering
the tools necessary to implement a range of practical systems, such as TLS/DTLS,
PKIX certificate handling, PKCS#11 and TPM hardware support, password hashing,
and post-quantum crypto schemes. In addition to the C++, botan has a C89 API
specifically designed to be easy to call from other languages. A Python binding
using ctypes is included, and several other language bindings are available.")
(home-page "https://botan.randombit.net")
(license license:bsd-2)))
;;;;
@@ -47,9 +174,13 @@
(lambda* (#:key outputs #:allow-other-keys)
(substitute* "CMakeLists.txt"
((".etc.biboumi") (string-append (assoc-ref outputs "out") "/etc/biboumi"))))))))
(native-inputs
'(list
gcc-12))
(inputs
'(list
botan
curl
expat
git
libidn2
+26
View File
@@ -596,6 +596,32 @@ void Bridge::send_irc_nick_change(const Iid& iid, const std::string& new_nick, c
if (!res_in_chan)
return;
IrcClient* irc = this->get_irc_client(iid.get_server());
if (new_nick == irc->get_own_nick())
return;
#ifdef USE_DATABASE
auto soptions = Database::get_irc_server_options(this->get_bare_jid(), iid.get_server());
const auto& db_nick = soptions.col<Database::Nick>();
if (!db_nick.empty())
{
IrcChannel* channel = irc->get_channel(iid.get_local());
const IrcUser* self = channel ? channel->get_self() : nullptr;
// Send presence 110+210 to let them know the nick we're actually using
if (self)
{
this->send_user_join(iid.get_server(), iid.get_local(), self,
self->get_most_significant_mode(irc->get_sorted_user_modes()),
true, requesting_resource);
}
else
{
IrcUser user(irc->get_own_nick());
this->send_user_join(iid.get_server(), iid.get_local(), &user, '\0', true, requesting_resource);
}
return;
}
#endif
irc->send_nick_command(new_nick);
}
+5
View File
@@ -526,6 +526,11 @@ void IrcClient::send_user_command(const std::string& username, const std::string
void IrcClient::send_nick_command(const std::string& nick)
{
if (nick.find(' ') != std::string::npos)
{
this->on_erroneous_nickname(IrcMessage(std::string(this->hostname), "432", {this->get_own_nick(), nick, "Erroneous nickname"}));
return;
}
this->send_message(IrcMessage("NICK", {nick}));
}
+9 -1
View File
@@ -20,6 +20,10 @@ IrcUri::~IrcUri(void) {
#endif
}
#ifdef CURL_FOUND
#include <curl/curl.h>
#endif
const std::optional<std::string> IrcUri::get_server(void) const {
#ifdef CURL_FOUND
char *server;
@@ -27,7 +31,9 @@ const std::optional<std::string> IrcUri::get_server(void) const {
if (rc) {
return std::nullopt;
}
return server;
std::string result(server);
curl_free(server);
return result;
#else
return std::nullopt;
#endif // CURL_FOUND
@@ -42,6 +48,7 @@ const std::optional<std::string> IrcUri::get_entity(void) const {
if (rc)
return std::nullopt;
output = entity;
curl_free(entity);
if (!output.empty())
output.erase(0, 1);
if (output.empty()) {
@@ -49,6 +56,7 @@ const std::optional<std::string> IrcUri::get_entity(void) const {
if (rc)
return std::nullopt;
output = entity;
curl_free(entity);
output.insert(0, 1, '#');
}
return IrcUri::parse_entity(output);
+19
View File
@@ -33,3 +33,22 @@ std::string sha1(const std::string& input)
return digest.str();
#endif
}
std::string sha1_raw(const std::string& input)
{
#ifdef BOTAN_FOUND
auto sha1 = Botan::HashFunction::create_or_throw("SHA-1");
sha1->update(input);
const auto digest = sha1->final();
return std::string(
reinterpret_cast<const char*>(digest.data()),
digest.size()
);
#endif
#ifdef GCRYPT_FOUND
const auto hash_length = gcry_md_get_algo_dlen(GCRY_MD_SHA1);
std::string output(hash_length, '\0');
gcry_md_hash_buffer(GCRY_MD_SHA1, output.data(), input.data(), input.size());
return output;
#endif
}
+1
View File
@@ -3,3 +3,4 @@
#include <string>
std::string sha1(const std::string& input);
std::string sha1_raw(const std::string& input);
+31 -12
View File
@@ -576,9 +576,10 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
}
else if (iid.type == Iid::Type::Server)
{
if (node.empty())
const std::string expected_node_name = std::string(NODE_NAME) + "#" + get_irc_server_ver(to_str);
if (node.empty() || node == expected_node_name)
{
this->send_irc_server_disco_info(id, from, to_str);
this->send_irc_server_disco_info(id, from, to_str, node);
stanza_error.disable();
}
}
@@ -1074,28 +1075,35 @@ void BiboumiComponent::send_self_disco_info(const std::string& id, const std::st
this->send_stanza(iq);
}
void BiboumiComponent::send_irc_server_disco_info(const std::string& id, const std::string& jid_to, const std::string& jid_from)
void BiboumiComponent::send_irc_server_disco_info(const std::string& id, const std::string& jid_to,
const std::string& jid_from, const std::string& node)
{
Jid from(jid_from);
Stanza iq("iq");
{
iq["type"] = "result";
Stanza iq = get_irc_server_disco_info(jid_from, node);
iq["id"] = id;
iq["to"] = jid_to;
iq["from"] = jid_from;
this->send_stanza(iq);
}
Stanza BiboumiComponent::get_irc_server_disco_info(const std::string& jid_from, const std::string& node)
{
Jid from(jid_from);
Stanza iq("iq");
iq["type"] = "result";
XmlSubNode query(iq, "query");
query["xmlns"] = DISCO_INFO_NS;
if (!node.empty())
query["node"] = node;
XmlSubNode identity(query, "identity");
identity["category"] = "gateway";
identity["type"] = "irc";
identity["name"] = "IRC server " + from.local + " over Biboumi";
for (const char *ns: {DISCO_INFO_NS, ADHOC_NS, PING_NS, VERSION_NS})
for (const char *ns: {DISCO_INFO_NS, CAPS_NS, ADHOC_NS, PING_NS, VERSION_NS})
{
XmlSubNode feature(query, "feature");
feature["var"] = ns;
}
}
this->send_stanza(iq);
return iq;
}
void BiboumiComponent::send_irc_channel_muc_traffic_info(const std::string& id, const std::string& jid_to, const std::string& jid_from)
@@ -1303,12 +1311,23 @@ void BiboumiComponent::send_presence_to_contact(const std::string& from, const s
if (!id.empty())
presence["id"] = id;
if (type.empty() && from == this->served_hostname) {
if (type.empty())
{
Jid from_jid(from);
const Iid from_iid(from_jid.local, {'&', '#', '+', '!'});
const std::string fixed_irc_server = Config::get("fixed_irc_server", "");
const bool is_server_jid = !from_jid.local.empty() && from_jid.resource.empty() &&
((fixed_irc_server.empty() && from_iid.type == Iid::Type::Server) ||
(!fixed_irc_server.empty() && from_jid.local == fixed_irc_server));
if (from == this->served_hostname ||
(from_jid.domain == this->served_hostname && is_server_jid))
{
XmlSubNode c(presence, "c");
c["xmlns"] = CAPS_NS;
c["hash"] = "sha-1";
c["node"] = NODE_NAME;
c["ver"] = this->get_self_ver();
c["ver"] = from == this->served_hostname ? this->get_self_ver() : get_irc_server_ver(from);
}
}
this->send_stanza(presence);
}
+5 -1
View File
@@ -68,7 +68,8 @@ public:
/**
* Send a result IQ with the disco informations regarding IRC server JIDs.
*/
void send_irc_server_disco_info(const std::string& id, const std::string& jid_to, const std::string& jid_from);
void send_irc_server_disco_info(const std::string& id, const std::string& jid_to,
const std::string& jid_from, const std::string& node);
/**
* Sends the allowed namespaces in MUC message, according to
* http://xmpp.org/extensions/xep-0045.html#impl-service-traffic
@@ -93,6 +94,9 @@ 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("")); };
static Stanza get_irc_server_disco_info(const std::string& jid_from, const std::string& node);
static std::string get_irc_server_ver(const std::string& jid_from)
{ return Xmpp::hash_disco_sha1(get_irc_server_disco_info(jid_from, "")); }
#ifdef CURL_FOUND
std::optional<Stanza> handle_iq_gateway(const Stanza *stanza, const Jid *from, const std::string to, const std::string id);
#endif
+3 -3
View File
@@ -17,7 +17,7 @@ const std::string hash_disco_sha1(const Stanza &stanza) {
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;
const int type_compare = type_a < type_b;
return type_compare;
});
@@ -33,7 +33,7 @@ const std::string hash_disco_sha1(const Stanza &stanza) {
[](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;
const int var_compare = var_a < var_b;
return var_compare;
});
@@ -42,7 +42,7 @@ const std::string hash_disco_sha1(const Stanza &stanza) {
s += var + "<";
}
const std::string hashed = sha1(s);
const std::string hashed = sha1_raw(s);
const std::string encoded = base64::encode(hashed);
return encoded;
@@ -0,0 +1,23 @@
from scenarios import *
scenario = (
# IRC server JIDs advertise caps for their own disco info
send_stanza("<iq type='get' id='get6' from='{jid_one}/{resource_one}' to='{irc_server_one}'><query xmlns='http://jabber.org/protocol/disco#info'/></iq>"),
expect_stanza("/iq[@from='{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='result']/disco_info:query/disco_info:identity[@category='gateway'][@type='irc']",
"/iq/disco_info:query/disco_info:feature[@var='http://jabber.org/protocol/disco#info']",
"/iq/disco_info:query/disco_info:feature[@var='http://jabber.org/protocol/caps']"
),
send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}'><x xmlns='http://jabber.org/protocol/muc'/></presence>"),
sequences.connection(),
scenarios.simple_channel_join.expect_self_join_presence(),
send_stanza("<presence from='{jid_one}/{resource_one}' to='{irc_server_one}' type='subscribe'/>"),
expect_stanza("/presence[@from='{irc_server_one}'][@to='{jid_one}'][@type='subscribed']"),
expect_stanza("/presence[@from='{irc_server_one}'][@to='{jid_one}']/caps:c[@hash='sha-1'][@node][@ver]",
after = (
save_value("server_ver", extract_attribute("/presence/caps:c[@hash='sha-1'][@node][@ver]", "ver")),
save_value("server_node", extract_attribute("/presence/caps:c[@hash='sha-1'][@node][@ver]", "node")),
)
),
send_stanza("<iq type='get' id='get8' from='{jid_one}/{resource_one}' to='{irc_server_one}'><query xmlns='http://jabber.org/protocol/disco#info' node='{server_node}#{server_ver}'/></iq>"),
expect_stanza("/iq[@from='{irc_server_one}'][@type='result']/disco_info:query[@node='{server_node}#{server_ver}']"),
)
@@ -0,0 +1,13 @@
from scenarios import *
scenario = (
send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"),
sequences.connection(),
expect_stanza("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
"/presence/muc_user:x/muc_user:status[@code='110']"),
expect_stanza("/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
# Attempt to change nick to a nickname containing a space
send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/Nick With Space' />"),
expect_stanza("/message[@from='{irc_server_one}'][@to='{jid_one}/{resource_one}']/body[text()='irc.localhost: Erroneous nickname: Nick With Space']"),
)
@@ -0,0 +1,26 @@
from scenarios import *
scenario = (
send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"),
sequences.connection(),
expect_stanza("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
"/presence/muc_user:x/muc_user:status[@code='110']"),
expect_stanza("/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
# Configure a nickname in the database for this server
send_stanza("<iq type='set' id='conf1' from='{jid_one}/{resource_one}' to='{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"),
expect_stanza("/iq[@type='result']",
after = save_value("sessionid", extract_attribute("/iq[@type='result']/commands:command[@node='configure']", "sessionid"))),
send_stanza("<iq type='set' id='conf2' from='{jid_one}/{resource_one}' to='{irc_server_one}'>"
"<command xmlns='http://jabber.org/protocol/commands' node='configure' sessionid='{sessionid}' action='complete'>"
"<x xmlns='jabber:x:data' type='submit'>"
"<field var='nick'><value>{nick_one}</value></field>"
"</x></command></iq>"),
expect_stanza("/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"),
# Now with a DB nick configured, attempt to change nick: should reply with 110 + 210 presence rewriting to DB nick
send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/SomeOtherNick' />"),
expect_stanza("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
"/presence/muc_user:x/muc_user:status[@code='110']",
"/presence/muc_user:x/muc_user:status[@code='210']"),
)
@@ -0,0 +1,12 @@
from scenarios import *
scenario = (
send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' ><x xmlns='http://jabber.org/protocol/muc'/></presence>"),
sequences.connection(),
expect_stanza("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
"/presence/muc_user:x/muc_user:status[@code='110']"),
expect_stanza("/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
# Presence with the current nick (status update) should be ignored (no error, no change)
send_stanza("<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}'><show>away</show></presence>"),
)
+3 -3
View File
@@ -32,7 +32,7 @@ TEST_CASE("Hash disco 1") {
auto check_stanza = [](const Stanza &stanza) {
const std::string hash = Xmpp::hash_disco_sha1(stanza);
CHECK(hash == "YjVkN2NhODU2ZWQyZjhhNjJjOTE1MWMxYjQ0YmUwM2Q0ODhhZDI2MA==");
CHECK(hash == "GRREviyyjLzK2wK4QLX5NNF9FmQ=");
};
xml.add_stanza_callback([check_stanza](const Stanza &stanza) {
check_stanza(stanza);
@@ -62,7 +62,7 @@ TEST_CASE("Hash disco 2") {
auto check_stanza = [](const Stanza &stanza) {
const std::string hash = Xmpp::hash_disco_sha1(stanza);
CHECK(hash == "ZmY1ZTJjZjUyZjkwMjA2OTliZGFhYzYxMmVmMTI5YjdmYzNhZTIwYw==");
CHECK(hash == "ga7DGXbUk3tyt5T/WMlqa5qM0zM=");
};
xml.add_stanza_callback([check_stanza](const Stanza &stanza) {
check_stanza(stanza);
@@ -91,7 +91,7 @@ TEST_CASE("Hash disco 2 different order") {
auto check_stanza = [](const Stanza &stanza) {
const std::string hash = Xmpp::hash_disco_sha1(stanza);
CHECK(hash == "ZmY1ZTJjZjUyZjkwMjA2OTliZGFhYzYxMmVmMTI5YjdmYzNhZTIwYw==");
CHECK(hash == "ga7DGXbUk3tyt5T/WMlqa5qM0zM=");
};
xml.add_stanza_callback([check_stanza](const Stanza &stanza) {
check_stanza(stanza);