Add support for XEP-0100 jabber:iq:gateway

This commit is contained in:
Luca Matei Pintilie
2026-08-02 18:12:11 +02:00
parent ad14748a30
commit dbafe37e13
17 changed files with 323 additions and 5 deletions
+3
View File
@@ -14,6 +14,8 @@ For users
- XMPP users can send multiline messages on IRC servers which support
draft/multiline, as well as IRC users sending multiline messages will appear as
one message on XMPP.
- Add partial support for XEP-0100. Specifically section 6.3 "The
jabber:iq:gateway Protocol".
For admins
----------
@@ -33,6 +35,7 @@ For packagers
- The minimum required version of botan has been upgraded to version 3
- clang 3.4 and gcc 5.0 are no longer supported. The new minimum required
versions are clang >= 14 and gcc >= 11.2
- Add optional curl library for XEP-0100 jabber:iq:gateway support
Version 9.0 - 2020-09-22
========================
+13
View File
@@ -131,6 +131,12 @@ elseif(NOT WITHOUT_POSTGRESQL)
find_package(PQ)
endif()
if(WITH_CURL)
find_package(CURL REQUIRED)
elseif(NOT WITHOUT_CURL)
find_package(CURL)
endif()
#
## Set all the include directories, depending on what libraries are used
#
@@ -146,6 +152,9 @@ endif()
if(UDNS_FOUND)
include_directories(${UDNS_INCLUDE_DIRS})
endif()
if(CURL_FOUND)
include_directories(${CURL_INCLUDE_DIRS})
endif()
# To be able to include the config.h and other files generated by cmake
include_directories("${CMAKE_CURRENT_BINARY_DIR}/src/")
@@ -270,6 +279,10 @@ if(UDNS_FOUND)
target_link_libraries(${PROJECT_NAME} ${UDNS_LIBRARIES})
target_link_libraries(test_suite ${UDNS_LIBRARIES})
endif()
if(CURL_FOUND)
target_link_libraries(${PROJECT_NAME} ${CURL_LIBRARIES})
target_link_libraries(test_suite ${CURL_LIBRARIES})
endif()
if(LIBIDN_FOUND)
target_link_libraries(${PROJECT_NAME} ${LIBIDN_LIBRARIES})
target_link_libraries(test_suite ${LIBIDN_LIBRARIES})
+4
View File
@@ -63,6 +63,9 @@ systemd_ (optional)
Provides the support for a systemd service of Type=notify. This is useful only
if you are packaging biboumi in a distribution with Systemd.
curl_ (optional)
Provides support for XEP-0100 jabber:iq:gateway (to parse URL).
Customize
---------
@@ -189,6 +192,7 @@ man page biboumi(1)) or the usage page.
.. _udns: http://www.corpit.ru/mjt/udns.html
.. _sqlite3: https://sqlite.org
.. _systemd: https://www.freedesktop.org/wiki/Software/systemd/
.. _curl: https://curl.se/
.. _biboumi.1.rst: doc/biboumi.1.rst
.. _gcrypt: https://www.gnu.org/software/libgcrypt/
.. _libpq: https://www.postgresql.org/docs/current/static/libpq.html
+2 -2
View File
@@ -7,7 +7,7 @@
FROM docker.io/alpine:latest as builder
RUN apk add --no-cache --virtual .build cmake expat-dev g++ git libidn-dev \
RUN apk add --no-cache --virtual .build cmake expat-dev g++ git curl-dev libidn-dev \
make postgresql-dev python3 sqlite-dev udns-dev util-linux-dev botan3-dev
COPY . /source
@@ -31,7 +31,7 @@ RUN mkdir build && cd build && \
FROM docker.io/alpine:latest
RUN apk add --no-cache libidn libpq libstdc++ libuuid postgresql-libs \
RUN apk add --no-cache libcurl libidn libpq libstdc++ libuuid postgresql-libs \
sqlite-libs udns expat ca-certificates botan3
COPY --from=builder /etc/biboumi /etc/biboumi
+1
View File
@@ -16,6 +16,7 @@ make \
cmake \
g++ \
libuuid \
curl-dev \
udns-dev \
expat-dev \
libidn-dev \
+1
View File
@@ -11,6 +11,7 @@ make \
cmake \
g++ \
ca-certificates \
libcurl4-openssl-dev \
libuuid1 \
libudns-dev \
libexpat1-dev \
+1
View File
@@ -18,6 +18,7 @@ libidn-devel \
sqlite-devel \
botan3-devel \
systemd-devel \
libcurl-devel \
libuuid-devel \
libgcrypt-devel \
postgresql-devel \
+1
View File
@@ -1,6 +1,7 @@
#cmakedefine USE_DATABASE
#cmakedefine ICONV_SECOND_ARGUMENT_IS_CONST
#cmakedefine LIBIDN_FOUND
#cmakedefine CURL_FOUND
#cmakedefine SYSTEMD_FOUND
#cmakedefine POLLER ${POLLER}
#cmakedefine BOTAN_FOUND
+68
View File
@@ -0,0 +1,68 @@
#include <irc/irc_uri.hpp>
#include <utils/split.hpp>
#include <cstdio>
#include <cstring>
IrcUri::IrcUri(const std::string &uri) {
#ifdef CURL_FOUND
this->url = curl_url();
curl_url_set(this->url, CURLUPART_URL, uri.c_str(), CURLU_NON_SUPPORT_SCHEME | CURLU_NO_AUTHORITY);
#endif
(void)uri;
}
IrcUri::~IrcUri(void) {
#ifdef CURL_FOUND
curl_url_cleanup(this->url);
#endif
}
const std::optional<std::string> IrcUri::get_server(void) const {
#ifdef CURL_FOUND
char *server;
CURLUcode rc = curl_url_get(this->url, CURLUPART_HOST, &server, 0);
if (rc) {
return std::nullopt;
}
return server;
#else
return std::nullopt;
#endif // CURL_FOUND
};
const std::optional<std::string> IrcUri::get_entity(void) const {
#ifdef CURL_FOUND
char *entity;
std::string output;
CURLUcode rc;
rc = curl_url_get(this->url, CURLUPART_PATH, &entity, CURLU_URLDECODE);
if (rc)
return std::nullopt;
output = entity;
if (!output.empty())
output.erase(0, 1);
if (output.empty()) {
rc = curl_url_get(this->url, CURLUPART_FRAGMENT, &entity, CURLU_URLDECODE);
if (rc)
return std::nullopt;
output = entity;
output.insert(0, 1, '#');
}
return IrcUri::parse_entity(output);
#else
return std::nullopt;
#endif // CURL_FOUND
};
const std::string IrcUri::parse_entity(const std::string entity) {
const std::vector<std::string> entity_name_and_type =
utils::split(entity, ',');
if (entity_name_and_type.size() > 0)
return entity_name_and_type.at(0);
return entity;
};
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include <biboumi.h>
#include <optional>
#include <xmpp/jid.hpp>
#include <string>
#include <curl/urlapi.h>
/**
* Parses and keeps information about one IRC URI
*
* https://datatracker.ietf.org/doc/html/draft-butcher-irc-url-04
*
* Requires CURL
*/
class IrcUri
{
public:
explicit IrcUri(const std::string &uri);
~IrcUri(void);
IrcUri(const IrcUri&) = delete;
IrcUri(IrcUri&&) = delete;
IrcUri& operator=(const IrcUri&) = delete;
IrcUri& operator=(IrcUri&&) = delete;
const std::optional<std::string> get_server(void) const;
const std::optional<std::string> get_entity(void) const;
private:
#ifdef CURL_FOUND
CURLU *url;
#endif
static const std::string parse_entity(const std::string entity);
};
+89 -3
View File
@@ -1,19 +1,23 @@
#include <xmpp/biboumi_component.hpp>
#include <utils/empty_if_fixed_server.hpp>
#include <utils/timed_events.hpp>
#include <utils/scopeguard.hpp>
#include <utils/tolower.hpp>
#include <logger/logger.hpp>
#include <xmpp/adhoc_command.hpp>
#include <xmpp/biboumi_adhoc_commands.hpp>
#include <xmpp/xmpp_component.hpp>
#include <bridge/list_element.hpp>
#include <utils/encoding.hpp>
#include <config/config.hpp>
#include <utils/time.hpp>
#include <xmpp/jid.hpp>
#include <irc/irc_uri.hpp>
#include <optional>
#include <string>
#include <stdexcept>
#include <iostream>
#include <cstdlib>
@@ -449,9 +453,10 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
// the scopeguard.
std::string error_type("cancel");
std::string error_name("internal-server-error");
utils::ScopeGuard stanza_error([this, &from, &to_str, &id, &error_type, &error_name](){
std::string error_text("");
utils::ScopeGuard stanza_error([this, &from, &to_str, &id, &error_type, &error_name, &error_text](){
this->send_stanza_error("iq", from, to_str, id,
error_type, error_name, "");
error_type, error_name, error_text);
});
try {
if (type == "set")
@@ -518,6 +523,21 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
this->send_stanza(response);
stanza_error.disable();
}
#ifdef CURL_FOUND
else if ((query = stanza.get_child("query", GATEWAY_NS))) {
std::optional<Stanza> stanza =
this->handle_iq_gateway(query, &to, from, id);
if (stanza.has_value()) {
this->send_stanza(stanza.value());
stanza_error.disable();
} else {
error_name = "bad-request";
error_type = "modify";
error_text = "Invalid input. Check that the provided link is a valid irc URI";
return;
}
}
#endif // CURL_FOUND
#ifdef USE_DATABASE
else if ((query = stanza.get_child("query", MAM_NS)))
{
@@ -673,6 +693,28 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
}
stanza_error.disable();
}
#ifdef CURL_FOUND
else if ((query = stanza.get_child("query", GATEWAY_NS)))
{
Stanza iq("iq");
{
iq["from"] = to_str;
iq["to"] = from;
iq["id"] = id;
iq["type"] = "result";
XmlSubNode query(iq, GATEWAY_NS, "query");
{
query["xmlns"] = GATEWAY_NS;
XmlSubNode desc(query, "desc");
desc.set_inner("Enter an IRC URI. For example "
"irc://irc.example.com/#channame");
XmlSubNode prompt(query, "prompt");
prompt.set_inner("IRC URI");
};
}
this->send_stanza(iq);
}
#endif // CURL_FOUND
#ifdef USE_DATABASE
else if ((query = stanza.get_child("query", MUC_OWNER_NS)))
{
@@ -736,6 +778,50 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
error_name = "feature-not-implemented";
}
#ifdef CURL_FOUND
std::optional<Stanza>
BiboumiComponent::handle_iq_gateway(const Stanza *stanza, const Jid *from,
const std::string to,
const std::string id) {
const XmlNode *prompt = stanza->get_child("prompt", GATEWAY_NS);
const std::string inner = prompt->get_inner();
std::string local;
bool is_irc_uri =
(inner.starts_with("irc://") || inner.starts_with("ircs://"));
if (is_irc_uri) {
IrcUri irc_uri(inner);
const std::optional<std::string> entity = irc_uri.get_entity();
const std::optional<std::string> server = irc_uri.get_server();
const std::string irc_server = utils::empty_if_fixed_server("%" + server.value_or(""));
if (irc_server != "%")
local = entity.value_or("") + irc_server;
} else {
Iid i(inner, this->get_user_bridge(to));
const std::string iid_local = i.get_local();
if (!iid_local.empty())
local = iid_local + utils::empty_if_fixed_server("%" + i.get_server());
}
if (local.empty())
return std::nullopt;
Jid j(local + "@" + from->domain);
Stanza iq("iq");
{
iq["from"] = from->full();
iq["to"] = to;
iq["id"] = id;
iq["type"] = "result";
XmlSubNode query(iq, GATEWAY_NS, "query");
{
query["xmlns"] = GATEWAY_NS;
XmlSubNode jid(query, "jid");
jid.set_inner(j.full());
};
}
return iq;
}
#endif // CURL_FOUND
#ifdef USE_DATABASE
bool BiboumiComponent::handle_mam_request(const Stanza& stanza)
{
+3
View File
@@ -90,6 +90,9 @@ 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);
#ifdef CURL_FOUND
std::optional<Stanza> handle_iq_gateway(const Stanza *stanza, const Jid *from, const std::string to, const std::string id);
#endif
public:
void accept_subscription(const std::string& from, const std::string& to);
void ask_subscription(const std::string& from, const std::string& to);
+1
View File
@@ -40,6 +40,7 @@
#define STABLE_MUC_ID_NS "http://jabber.org/protocol/muc#stable_id"
#define SELF_PING_FLAG MUC_NS"#self-ping-optimization"
#define MESSAGE_CORRECT_NS "urn:xmpp:message-correct:0"
#define GATEWAY_NS "jabber:iq:gateway"
/**
* An XMPP component, communicating with an XMPP server using the protocole
+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',
'gateway': 'jabber:iq:gateway',
'commands': 'http://jabber.org/protocol/commands',
'dataform': 'jabber:x:data',
'version': 'jabber:iq:version',
@@ -0,0 +1,36 @@
from scenarios import *
conf = 'fixed_server'
def _test_case(input: str, output: str):
return (
send_stanza(f"<iq type='set' to='{{biboumi_host}}' from='{{jid_one}}/{{resource_one}}' id='iq1' xmlns='jabber:client'><query xmlns='jabber:iq:gateway'><prompt>{input}</prompt></query></iq>"),
expect_stanza(f"/iq[@type='result'][@to='{{jid_one}}/{{resource_one}}'][@from='{{biboumi_host}}']/gateway:query/gateway:jid[text()='{output}']"),
)
def _error_case(input: str):
return (
send_stanza(f"<iq type='set' to='{{biboumi_host}}' from='{{jid_one}}/{{resource_one}}' id='iq1' xmlns='jabber:client'><query xmlns='jabber:iq:gateway'><prompt>{input}</prompt></query></iq>"),
expect_stanza(f"/iq[@type='error'][@to='{{jid_one}}/{{resource_one}}'][@from='{{biboumi_host}}']/error[@type='modify']/stanza:bad-request"),
)
scenario = (
*_test_case("ircs://{irc_host_one}/%23test", "#test@{biboumi_host}"),
*_test_case("ircs://{irc_host_one}/#test", "#test@{biboumi_host}"),
*_test_case("irc://{irc_host_one}/%23test", "#test@{biboumi_host}"),
*_test_case("irc://{irc_host_one}/#test", "#test@{biboumi_host}"),
*_test_case("ircs:///#test", "#test@{biboumi_host}"),
*_test_case("irc:///#test", "#test@{biboumi_host}"),
# TODO: Some applications use the following URL scheme which should be supported
# *_test_case("ircs:///user,isuser", "user@{biboumi_host}"),
# *_test_case("irc:///user,isuser", "user@{biboumi_host}"),
*_test_case("#test", "#test@{biboumi_host}"),
*_test_case("#test%{irc_host_one}", "#test%{irc_host_one}@{biboumi_host}"),
*_test_case("nick", "nick@{biboumi_host}"),
*_test_case("{nick_one}%{irc_host_one}", "{lower_nick_one}%{irc_host_one}@{biboumi_host}"),
*_error_case(""),
*_error_case("irc://"),
*_error_case("irc:// asdf"),
*_error_case("ircs://"),
*_error_case("ircs:// asdf"),
)
+31
View File
@@ -0,0 +1,31 @@
from scenarios import *
def _test_case(input: str, output: str):
return (
send_stanza(f"<iq type='set' to='{{biboumi_host}}' from='{{jid_one}}/{{resource_one}}' id='iq1' xmlns='jabber:client'><query xmlns='jabber:iq:gateway'><prompt>{input}</prompt></query></iq>"),
expect_stanza(f"/iq[@type='result'][@to='{{jid_one}}/{{resource_one}}'][@from='{{biboumi_host}}']/gateway:query/gateway:jid[text()='{output}']"),
)
def _error_case(input: str):
return (
send_stanza(f"<iq type='set' to='{{biboumi_host}}' from='{{jid_one}}/{{resource_one}}' id='iq1' xmlns='jabber:client'><query xmlns='jabber:iq:gateway'><prompt>{input}</prompt></query></iq>"),
expect_stanza(f"/iq[@type='error'][@to='{{jid_one}}/{{resource_one}}'][@from='{{biboumi_host}}']/error[@type='modify']/stanza:bad-request"),
)
scenario = (
*_test_case("ircs://{irc_host_one}/%23test", "#test%{irc_host_one}@{biboumi_host}"),
*_test_case("ircs://{irc_host_one}/#test", "#test%{irc_host_one}@{biboumi_host}"),
*_test_case("irc://{irc_host_one}/%23test", "#test%{irc_host_one}@{biboumi_host}"),
*_test_case("irc://{irc_host_one}/#test", "#test%{irc_host_one}@{biboumi_host}"),
*_test_case("#test%{irc_host_one}", "#test%{irc_host_one}@{biboumi_host}"),
*_test_case("{nick_one}%{irc_host_one}", "{lower_nick_one}%{irc_host_one}@{biboumi_host}"),
*_error_case(""),
*_error_case("#test"),
*_error_case("{lower_nick_one}"),
*_error_case("irc:///#test"),
*_error_case("irc://"),
*_error_case("irc:// asdf"),
*_error_case("ircs:///#test"),
*_error_case("ircs://"),
*_error_case("ircs:// asdf"),
)
+31
View File
@@ -0,0 +1,31 @@
#include <biboumi.h>
#include "catch2/catch_test_macros.hpp"
#include "catch2/generators/catch_generators.hpp"
#include <irc/irc_uri.hpp>
#include <optional>
#ifdef CURL_FOUND
TEST_CASE("URI") {
auto scheme = GENERATE("irc", "ircs");
auto i = GENERATE("%23channame", "%23channame,ischannel", "#channame",
"#channame,ischannel");
const std::string input =
std::string(scheme) + "://irc.example.com/" + std::string(i);
IrcUri uri(input);
CHECK(uri.get_server() == "irc.example.com");
CHECK(uri.get_entity() == "#channame");
}
#else
TEST_CASE("No CURL") {
auto scheme = GENERATE("irc", "ircs");
auto i = GENERATE("%23channame", "%23channame,ischannel", "#channame",
"#channame,ischannel");
const std::string input =
std::string(scheme) + "://irc.example.com/" + std::string(i);
IrcUri uri(input);
CHECK(uri.get_server() == std::nullopt);
CHECK(uri.get_entity() == std::nullopt);
}
#endif // CURL_FOUND