* https://codeberg.org/poezio/biboumi: (65 commits)
  build: fix wrong constextpr preventing some compilers from building biboumi
  database: various optimizations for SQLite and PostgreSQL
  doc(admin): format verify_certificate documentation into a list
  Fix RPM builds on Fedora Linux
  Systemd service dir is  /usr/lib/systemd/system
  cmake: Explicitly search for Catch2 version 3
  add CAP LS 302 support and max-lines support for draft/multiline messages
  install files using GNUInstallDirs
  change to direct /doc/
  Fix link to documentation
  fix multiline private messages
  add archive_expires_after option
  Fix documentation warnings
  fix fedora package
  add ctest support and remove absolute path from etc directory
  Rename Ports to Unencrypted ports
  build docker image from local files, not git clone
  update some version strings
  add default_persistence_allowed
  add persist_user_denylist and persist_user_allowlist
  ...
This commit is contained in:
Stephen Paul Weber
2026-09-23 09:59:13 -05:00
109 changed files with 1935 additions and 471 deletions
+125 -7
View File
@@ -2,18 +2,22 @@
#include <utility>
#include <xmpp/biboumi_component.hpp>
#include <network/poller.hpp>
#include <utils/chunk.hpp>
#include <utils/empty_if_fixed_server.hpp>
#include <utils/encoding.hpp>
#include <utils/tolower.hpp>
#include <utils/uuid.hpp>
#include <logger/logger.hpp>
#include <utils/is_requester_allowed_to_persist.hpp>
#include <utils/revstr.hpp>
#include <utils/split.hpp>
#include <xmpp/jid.hpp>
#include <database/database.hpp>
#include <irc/parse_caps.hpp>
#include "result_set_management.hpp"
#include <algorithm>
#include <cstring>
#include <utils/string.hpp>
using namespace std::string_literals;
@@ -64,7 +68,7 @@ void Bridge::shutdown(const std::string& exit_message)
{
for (auto& pair: this->irc_clients)
{
std::unique_ptr<IrcClient>& irc = pair.second;
std::shared_ptr<IrcClient>& irc = pair.second;
irc->send_quit_command(exit_message);
}
}
@@ -135,11 +139,11 @@ IrcClient* Bridge::make_irc_client(const std::string& hostname, const std::strin
realname = this->get_bare_jid();
}
this->irc_clients.emplace(hostname,
std::make_unique<IrcClient>(this->poller, hostname,
std::make_shared<IrcClient>(this->poller, hostname,
nickname, username,
realname, jid.domain,
*this));
std::unique_ptr<IrcClient>& irc = this->irc_clients.at(hostname);
std::shared_ptr<IrcClient> irc = this->irc_clients.at(hostname);
return irc.get();
}
}
@@ -221,6 +225,83 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body, std::
std::vector<std::string> lines = utils::split(body, '\n', true);
if (lines.empty())
return ;
if (irc->has_capability("draft/multiline")
&& lines.size() > 1
// /me and /mode cannot be multi lined
&& lines[0].substr(0, 5) != "/mode"
&& lines[0].substr(0, 4) != "/me ")
{
std::string uuid;
const auto xmpp_body = this->make_xmpp_body(body);
#ifdef USE_DATABASE
if (this->record_history)
uuid = Database::store_muc_message(this->get_bare_jid(), iid.get_local(), iid.get_server(), std::chrono::system_clock::now(),
std::get<0>(xmpp_body), irc->get_own_nick());
#else
uuid = utils::gen_uuid();
#endif
if (id.empty())
id = utils::gen_uuid();
// A message with a lot of lines may need to be split, even if the
// server supports draft/multiline, as we may not be allowed to send
// that many lines all at once. As such check max-lines
// https://ircv3.net/specs/extensions/multiline#capabilities
std::vector<std::vector<std::string>> lines_chunked = {lines};
auto cap_value = irc->get_capability_value("draft/multiline");
if (cap_value) {
auto cap_value_parsed = parse_cap_keyval(cap_value.value());
if (cap_value_parsed.contains("max-lines") &&
cap_value_parsed.at("max-lines").has_value()) {
std::stringstream max_lines_string(
cap_value_parsed.at("max-lines").value());
size_t size;
max_lines_string >> size;
if (size > 0)
lines_chunked = utils::chunk(lines, size);
}
}
for (const auto &lines : lines_chunked) {
std::vector<IrcMessage> messages;
for (const std::string &line : lines) {
constexpr auto max_username_size = 10;
constexpr auto max_hostname_size = 63;
const auto line_size = 512 - 1 - ::strlen("batch=") - uuid.length() - 1 - ::strlen("draft/multiline-concat") - 1 -
irc->get_own_nick().size() - max_username_size - max_hostname_size -
::strlen(":!@ PRIVMSG ") - iid.get_local().length() - ::strlen(" :\r\n");
const auto privmsgs = cut(line, line_size);
bool first = true;
for (const auto& privmsg: privmsgs)
{
if (first)
{
messages.push_back(IrcMessage({}, std::string(), "PRIVMSG", {iid.get_local(), privmsg}));
first = false;
}
else
messages.push_back(IrcMessage({{"draft/multiline-concat", {}}}, std::string(), "PRIVMSG", {iid.get_local(), privmsg}));
}
}
MessageCallback mirror_to_all_resources = [this, iid, uuid, id, nodes_to_reflect, body](const IrcClient* irc, const IrcMessage&) {
for (const auto& resource: this->resources_in_chan[iid.to_tuple()])
{
auto stanza = this->xmpp.make_muc_message(std::to_string(iid), irc->get_own_nick(), this->make_xmpp_body(body),
this->user_jid + "/"
+ resource, uuid, id);
for (const auto& node: nodes_to_reflect)
stanza.add_child(node);
this->xmpp.send_stanza(stanza);
}
};
irc->send_batch(uuid, "draft/multiline", {iid.get_local()}, messages, std::move(mirror_to_all_resources));
id = utils::gen_uuid();
uuid = utils::gen_uuid();
}
return;
}
bool first = true;
for (const std::string& line: lines)
{
@@ -403,6 +484,41 @@ void Bridge::send_private_message(const Iid& iid, const std::string& body, const
std::vector<std::string> lines = utils::split(body, '\n', true);
if (lines.empty())
return ;
if (irc->has_capability("draft/multiline")
&& lines.size() > 1
// /me and /mode cannot be multi lined
&& lines[0].substr(0, 5) != "/mode"
&& lines[0].substr(0, 4) != "/me ")
{
std::basic_string<char> uuid = utils::gen_uuid();
std::vector<IrcMessage> messages;
for (const std::string &line : lines) {
constexpr auto max_username_size = 10;
constexpr auto max_hostname_size = 63;
const auto line_size = 512 - 1 - ::strlen("batch=") - uuid.length() - 1 -
::strlen("draft/multiline-concat") - 1 -
irc->get_own_nick().size() - max_username_size -
max_hostname_size - ::strlen(":!@ PRIVMSG ") -
iid.get_local().length() - ::strlen(" :\r\n");
const auto privmsgs = cut(line, line_size);
bool first = true;
for (const auto &privmsg : privmsgs) {
if (first) {
messages.push_back(IrcMessage({}, std::string(), "PRIVMSG",
{iid.get_local(), privmsg}));
first = false;
} else
messages.push_back(IrcMessage({{"draft/multiline-concat", {}}},
std::string(), "PRIVMSG",
{iid.get_local(), privmsg}));
}
}
irc->send_batch(uuid, "draft/multiline", {iid.get_local()}, messages,
[](const IrcClient *, const IrcMessage &) {});
return;
}
for (const std::string& line: lines)
{
if (line.substr(0, 4) == "/me ")
@@ -436,7 +552,9 @@ void Bridge::leave_irc_channel(Iid&& iid, const std::string& status_message, con
bool persistent = false;
#ifdef USE_DATABASE
const auto goptions = Database::get_global_options(this->user_jid);
if (goptions.col<Database::GlobalPersistent>())
if (!utils::is_requester_allowed_to_persist(Jid(this->user_jid)))
persistent = false;
else if (goptions.col<Database::GlobalPersistent>())
persistent = true;
else
{
@@ -1151,12 +1269,12 @@ void Bridge::trigger_on_irc_message(const std::string& irc_hostname, const IrcMe
}
}
std::unordered_map<std::string, std::unique_ptr<IrcClient>>& Bridge::get_irc_clients()
std::unordered_map<std::string, std::shared_ptr<IrcClient>>& Bridge::get_irc_clients()
{
return this->irc_clients;
}
const std::unordered_map<std::string, std::unique_ptr<IrcClient>>& Bridge::get_irc_clients() const
const std::unordered_map<std::string, std::shared_ptr<IrcClient>>& Bridge::get_irc_clients() const
{
return this->irc_clients;
}
@@ -1165,7 +1283,7 @@ std::set<char> Bridge::get_chantypes(const std::string& hostname) const
{
IrcClient* irc = this->find_irc_client(hostname);
if (!irc)
return {'#', '&'};
return {'&', '#', '+', '!'};
return irc->get_chantypes();
}
+3 -3
View File
@@ -226,8 +226,8 @@ public:
* iq_responder_callback_t and remove the callback from the list.
*/
void trigger_on_irc_message(const std::string& irc_hostname, const IrcMessage& message);
std::unordered_map<std::string, std::unique_ptr<IrcClient>>& get_irc_clients();
const std::unordered_map<std::string, std::unique_ptr<IrcClient>>& get_irc_clients() const;
std::unordered_map<std::string, std::shared_ptr<IrcClient>>& get_irc_clients();
const std::unordered_map<std::string, std::shared_ptr<IrcClient>>& get_irc_clients() const;
std::set<char> get_chantypes(const std::string& hostname) const;
#ifdef USE_DATABASE
void set_record_history(const bool val);
@@ -260,7 +260,7 @@ private:
* One IrcClient for each IRC server we need to be connected to.
* The pointer is shared by the bridge and the poller.
*/
std::unordered_map<std::string, std::unique_ptr<IrcClient>> irc_clients;
std::unordered_map<std::string, std::shared_ptr<IrcClient>> irc_clients;
/**
* To communicate back with the XMPP component
*/
+1 -1
View File
@@ -24,7 +24,7 @@ static const char* irc_colors_to_css[IRC_NUM_COLORS] = {
"cyan",
"lightcyan",
"lightblue",
"lightmagenta",
"hotpink",
"gray",
"white",
};
+1
View File
@@ -2,6 +2,7 @@
#include <utils/tolower.hpp>
#include <utils/split.hpp>
#include <fstream>
#include <algorithm>
#include <cstdlib>
#include <cstring>
-2
View File
@@ -16,8 +16,6 @@
#pragma once
#include <functional>
#include <fstream>
#include <memory>
#include <vector>
#include <string>
#include <map>
+9
View File
@@ -2,6 +2,7 @@
#ifdef USE_DATABASE
#include <database/select_query.hpp>
#include <database/delete_query.hpp>
#include <database/save.hpp>
#include <database/database.hpp>
#include <utils/get_first_non_empty.hpp>
@@ -317,6 +318,14 @@ std::vector<Database::RosterItem> Database::get_full_roster()
return query.execute(*Database::db);
}
void Database::delete_old_muclines(const long& seconds)
{
auto query = DeleteQuery(Database::muc_log_lines.get_name());
query << " WHERE " << Database::Date{} << "<" << seconds;
query.execute(*Database::db);
}
void Database::close()
{
Database::db = nullptr;
+1
View File
@@ -153,6 +153,7 @@ class Database
static void delete_roster_item(const std::string& local, const std::string& remote);
static std::vector<Database::RosterItem> get_contact_list(const std::string& local);
static std::vector<Database::RosterItem> get_full_roster();
static void delete_old_muclines(const long& seconds);
static void close();
static void open(const std::string& filename);
+9
View File
@@ -0,0 +1,9 @@
#pragma once
enum class DatabaseType
{
none,
sqlite3,
postgresql,
};
+2 -1
View File
@@ -7,11 +7,11 @@
* Everything else (all portable stuf) should go outside of this class.
*/
#include <database/database_type.hpp>
#include <database/statement.hpp>
#include <memory>
#include <string>
#include <vector>
#include <tuple>
#include <set>
@@ -36,6 +36,7 @@ class DatabaseEngine
return {};
}
virtual std::string id_column_type() = 0;
virtual DatabaseType get_type() { return DatabaseType::none; };
int64_t last_inserted_rowid{-1};
};
+3
View File
@@ -101,4 +101,7 @@ std::string PostgresqlEngine::id_column_type()
return "SERIAL";
}
DatabaseType PostgresqlEngine::get_type() {
return DatabaseType::postgresql;
};
#endif
+3 -1
View File
@@ -2,9 +2,9 @@
#include <biboumi.h>
#include <string>
#include <stdexcept>
#include <memory>
#include <database/database_type.hpp>
#include <database/statement.hpp>
#include <database/engine.hpp>
@@ -30,6 +30,8 @@ class PostgresqlEngine: public DatabaseEngine
void extract_last_insert_rowid(Statement& statement) override;
std::string get_returning_id_sql_string(const std::string& col_name) override;
std::string id_column_type() override;
DatabaseType get_type() override final;
private:
PGconn* const conn;
};
+1
View File
@@ -1,5 +1,6 @@
#pragma once
#include <string>
#include <type_traits>
template <typename... T>
+46 -2
View File
@@ -7,9 +7,14 @@
#include <database/query.hpp>
#include <config/config.hpp>
#include <utils/tolower.hpp>
#include <logger/logger.hpp>
#include <vector>
#include <utils/dirname.hpp>
#include <filesystem>
#include <sys/stat.h>
Sqlite3Engine::Sqlite3Engine(sqlite3* db):
db(db)
@@ -46,6 +51,8 @@ std::set<std::string> Sqlite3Engine::get_all_columns_from_table(const std::strin
std::unique_ptr<DatabaseEngine> Sqlite3Engine::open(const std::string& filename)
{
sqlite3* new_db;
const std::string dirname = utils::dirname(filename);
std::filesystem::create_directories(dirname);
auto res = sqlite3_open_v2(filename.data(), &new_db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr);
if (res != SQLITE_OK)
{
@@ -53,7 +60,31 @@ std::unique_ptr<DatabaseEngine> Sqlite3Engine::open(const std::string& filename)
sqlite3_close(new_db);
throw std::runtime_error("");
}
return std::make_unique<Sqlite3Engine>(new_db);
std::unique_ptr<DatabaseEngine> db = std::make_unique<Sqlite3Engine>(new_db);
// Enable Write-Ahead Logging
// https://sqlite.org/wal.html
auto sqlite_journal_mode = Config::get("sqlite_journal_mode", "WAL");
if (sqlite_journal_mode != "none") {
if (Sqlite3Engine::is_valid_sqlite_journal_mode(sqlite_journal_mode)) {
auto [success, err_msg] =
db->raw_exec("PRAGMA journal_mode = " + sqlite_journal_mode);
if (!success)
log_error("Failed enabling SQLite WAL with error: ", err_msg);
} else {
log_error("Invalid config option sqlite_journal_mode: ",
sqlite_journal_mode);
}
}
for (const auto &s : Sqlite3Engine::startup_commands) {
auto [success, err_msg] = db->raw_exec(s);
if (!success) {
log_error("Failed running startup command: ", s,
"; with error: ", err_msg);
}
}
return db;
}
std::tuple<bool, std::string> Sqlite3Engine::raw_exec(const std::string& query)
@@ -97,4 +128,17 @@ std::string Sqlite3Engine::id_column_type()
return "INTEGER PRIMARY KEY AUTOINCREMENT";
}
DatabaseType Sqlite3Engine::get_type() {
return DatabaseType::sqlite3;
};
bool Sqlite3Engine::is_valid_sqlite_journal_mode(std::string &sqlite_journal_mode) {
// https://sqlite.org/pragma.html#pragma_journal_mode
for (const char* s : Sqlite3Engine::valid_sqlite_journal_modes) {
if (sqlite_journal_mode == s)
return true;
}
return false;
}
#endif
+21
View File
@@ -29,8 +29,29 @@ class Sqlite3Engine: public DatabaseEngine
std::unique_ptr<Statement> prepare(const std::string& query) override;
void extract_last_insert_rowid(Statement& statement) override;
std::string id_column_type() override;
DatabaseType get_type() override final;
private:
sqlite3* const db;
constexpr static const char *const startup_commands[3] = {
// Wait for a maximum of 5 seconds if a write is busy
"PRAGMA busy_timeout = 5000",
// Reduce disk syncs
// >When synchronous is NORMAL (1), the SQLite database engine will still
// >sync at the most critical moments
// https://sqlite.org/pragma.html#pragma_synchronous
"PRAGMA synchronous = NORMAL",
// Run PRAGMA command early to analyze all tables, as recommended by
// sqlite
// >Applications that use long-lived database connections should run
// >"PRAGMA optimize=0x10002;" when the connection is first opened
// https://sqlite.org/lang_analyze.html
"PRAGMA optimize=0x10002",
};
constexpr static const char *const valid_sqlite_journal_modes[6] = {
"DELETE", "TRUNCATE", "PERSIST", "MEMORY", "WAL", "OFF",
};
static bool is_valid_sqlite_journal_mode(std::string &sqlite_journal_mode);
};
#else
-1
View File
@@ -5,7 +5,6 @@
#include <database/delete_query.hpp>
#include <database/row.hpp>
#include <algorithm>
#include <string>
#include <set>
+1 -1
View File
@@ -20,7 +20,7 @@ class Bridge;
* It’s possible to have an empty-string server, but it makes no sense in
* biboumi’s context.
*
* Assuming the chantypes are '#' and '&':
* Assuming the chantypes are '&', '#', '+' and '!':
*
* #test%irc.example.org has :
* - local: "#test" (the # is part of the name, it could very well be absent, or & (for example) instead)
+195 -15
View File
@@ -11,6 +11,7 @@
#include <config/config.hpp>
#include <utils/tolower.hpp>
#include <utils/split.hpp>
#include <irc/parse_caps.hpp>
#include <utils/string.hpp>
#include <sstream>
@@ -83,6 +84,7 @@ static const std::unordered_map<std::string,
{"KICK", {&IrcClient::on_kick, {3, 0}}},
{"INVITE", {&IrcClient::on_invite, {2, 0}}},
{"CAP", {&IrcClient::on_cap, {3, 0}}},
{"BATCH", {&IrcClient::on_batch, {1, 0}}},
#ifdef WITH_SASL
{"AUTHENTICATE", {&IrcClient::on_authenticate, {1, 0}}},
{"900", {&IrcClient::on_sasl_login, {3, 0}}},
@@ -155,7 +157,7 @@ IrcClient::IrcClient(std::shared_ptr<Poller>& poller, std::string hostname,
bridge(bridge),
welcomed(false),
chanmodes({"", "", "", ""}),
chantypes({'#', '&'}),
chantypes({'&', '#', '+', '!'}),
tokens_bucket(this->get_throttle_limit(), 1s, [this]() {
if (message_queue.empty())
return true;
@@ -213,7 +215,7 @@ void IrcClient::start()
auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(),
this->get_hostname());
# ifdef BOTAN_FOUND
this->credential_manager.set_trusted_fingerprint(options.col<Database::TrustedFingerprint>());
this->credential_manager->set_trusted_fingerprint(options.col<Database::TrustedFingerprint>());
# endif
if (Config::get("fixed_irc_server", "").empty() &&
!options.col<Database::Address>().empty())
@@ -287,7 +289,10 @@ void IrcClient::on_connected()
this->send_gateway_message("Connected to IRC server"s + (this->use_tls ? " (encrypted)": "") + ".");
this->capabilities["batch"] = {[]{}, []{}};
this->capabilities["message-tags"] = {[]{}, []{}};
this->capabilities["multi-prefix"] = {[]{}, []{}};
this->capabilities["draft/multiline"] = {[]{}, []{}};
#ifdef USE_DATABASE
auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(),
@@ -318,10 +323,16 @@ void IrcClient::on_connected()
}
#endif
{
for (const auto &pair : this->capabilities)
this->send_message({ "CAP", {"REQ", pair.first}});
}
this->send_message({ "CAP", {"LS", "302"}});
// If we never get a response from CAP LS start requesting things blindly
TimedEventsManager::instance().add_event(TimedEvent(
std::chrono::steady_clock::now() + 10s,
[this]() {
for (const auto &pair : this->capabilities)
this->send_message({"CAP", {"REQ", pair.first}});
},
"cap_fallback_" + this->get_unique_identifier()));
this->send_nick_command(this->current_nick);
#ifdef USE_DATABASE
@@ -416,12 +427,34 @@ void IrcClient::parse_in_buffer(const size_t)
"”: ", args_size);
else
{
const auto& cb = it->second.first;
try {
(this->*(cb))(message);
} catch (const std::exception& e) {
log_error("Unhandled exception: ", e.what());
}
bool added_to_batch = false;
if (this->has_capability("batch"))
{
auto it_tags = message.tags.find("batch");
if (it_tags != message.tags.end())
if (it_tags->second.has_value())
{
auto it_batches = this->batches.find(it_tags->second.value());
if (it_batches != this->batches.end())
{
auto message_tags = message.tags;
auto message_prefix = message.prefix;
auto message_command = message.command;
auto message_arguments = message.arguments;
it_batches->second.second.emplace_back(std::move(message_tags), std::move(message_prefix), std::move(message_command), std::move(message_arguments));
added_to_batch = true;
}
}
}
if (!added_to_batch)
{
const auto& cb = it->second.first;
try {
(this->*(cb))(message);
} catch (const std::exception& e) {
log_error("Unhandled exception: ", e.what());
}
}
}
}
else
@@ -441,6 +474,16 @@ void IrcClient::actual_send(std::pair<IrcMessage, MessageCallback>&& message_pai
const MessageCallback& callback = message_pair.second;
log_debug("IRC SENDING: (", this->get_hostname(), ") ", message);
std::string res;
if (!message.tags.empty())
{
res += "@";
for (auto& tag: message.tags)
if (tag.second.has_value())
res += tag.first + "=" + tag.second.value() + ";";
else
res += tag.first + ";";
res.back() = ' ';
}
if (!message.prefix.empty())
res += ":" + message.prefix + " ";
res += message.command;
@@ -493,7 +536,7 @@ void IrcClient::send_pass_command(const std::string& password)
void IrcClient::send_webirc_command(const std::string& password, const std::string& user_ip)
{
this->send_message(IrcMessage("WEBIRC", {password, "biboumi", this->user_hostname, user_ip}));
this->send_message(IrcMessage("WEBIRC", {password, "biboumi", this->user_hostname, user_ip, "secure"}));
}
void IrcClient::send_kick_command(const std::string& chan_name, const std::string& target, const std::string& reason)
@@ -606,6 +649,22 @@ void IrcClient::send_ping_command()
this->send_message(IrcMessage("PING", {"biboumi"}));
}
void IrcClient::send_batch(const std::string& reference_tag, const std::string& type, const std::vector<std::string>& parameters, std::vector<IrcMessage>& messages, MessageCallback callback)
{
std::vector<std::string> args;
args.push_back("+" + reference_tag);
args.push_back(type);
for (const std::string& parameter: parameters)
args.push_back(parameter);
this->send_message(IrcMessage("BATCH", std::move(args)));
for (IrcMessage& message: messages)
{
message.tags["batch"] = reference_tag;
this->send_message(std::move(message));
}
this->send_message(IrcMessage("BATCH", {"-" + reference_tag}), callback);
}
void IrcClient::forward_server_message(const IrcMessage& message)
{
const std::string from = message.prefix;
@@ -1340,6 +1399,30 @@ long int IrcClient::get_throttle_limit() const
void IrcClient::on_cap(const IrcMessage &message)
{
const auto& sub_command = message.arguments[1];
if (sub_command == "LS") {
/**
* CAP LS 302 can return either a multiline response or a one line response
* - CAP * LS * :multiline
* - CAP * LS :last line
*/
const bool last_cap_ls = message.arguments[2] != "*";
const auto caps =
last_cap_ls ? message.arguments[2] : message.arguments[3];
auto caps_parsed = parse_caps(caps);
this->cap_ls_response.merge(caps_parsed);
if (last_cap_ls) {
// The CAP LS response has finished. Cancel the fallback
TimedEventsManager::instance().cancel("cap_fallback_" + this->get_unique_identifier());
std::erase_if(this->capabilities, [this](auto &pair) {
if (this->cap_ls_response.contains(pair.first)) {
this->send_message({"CAP", {"REQ", pair.first}});
return false;
} else
return true;
});
}
return;
}
const auto& caps = utils::split(message.arguments[2], ' ', false);
for (const auto& cap: caps)
{
@@ -1351,11 +1434,16 @@ void IrcClient::on_cap(const IrcMessage &message)
}
Capability& capability = it->second;
if (sub_command == "ACK")
capability.on_ack();
{
capability.on_ack();
this->enabled_capabilities.insert(it->first);
}
else if (sub_command == "NACK")
capability.on_nack();
capability.on_nack();
this->capabilities.erase(it);
}
for (const auto& batch: this->batches)
log_debug("Batch: ", batch.first, " exists");
if (this->capabilities.empty())
this->cap_end();
}
@@ -1419,3 +1507,95 @@ void IrcClient::cap_end()
this->send_message({"CAP", {"END"}});
this->bridge.on_irc_client_connected(this->get_hostname());
}
bool IrcClient::has_capability(const std::string& capability) const
{
auto it = this->enabled_capabilities.find(capability);
return it != this->enabled_capabilities.end();
}
const std::optional<const std::string> IrcClient::get_capability_value(const std::string& capability) const
{
return this->cap_ls_response.at(capability);
}
void IrcClient::on_batch(const IrcMessage& message)
{
if (message.arguments[0][0] == '+')
{
if (message.arguments.size() < 2)
return;
if (message.arguments[1] == "draft/multiline" && message.arguments.size() != 3)
return;
this->batches.emplace(message.arguments[0].substr(1), std::make_pair(std::make_pair(message.prefix, message.arguments), std::vector<IrcMessage>()));
}
else if (message.arguments[0][0] == '-')
{
const std::string reference_tag = message.arguments[0].substr(1);
this->process_batch(reference_tag);
this->batches.erase(reference_tag);
}
return;
}
void IrcClient::process_batch(const std::string& reference_tag)
{
auto it = this->batches.find(reference_tag);
if (it == this->batches.end())
return;
if (it->second.first.second[1] == "draft/multiline")
{
const IrcUser user(it->second.first.first);
const std::string nick = user.nick;
Iid iid;
iid.set_local(it->second.first.second[2]);
iid.set_server(this->hostname);
bool muc = true;
if (!this->get_channel(iid.get_local())->joined)
{
iid.type = Iid::Type::User;
iid.set_local(nick);
muc = false;
}
else
iid.type = Iid::Type::Channel;
std::string body;
for (const IrcMessage& message: it->second.second)
{
auto it_tags = message.tags.find("draft/multiline-concat");
if (!body.empty() && it_tags == message.tags.end())
body += "\n" + message.arguments[1];
else
body += message.arguments[1];
}
if (body.find_first_not_of("\n") == std::string::npos)
// Empty message
return;
this->bridge.send_message(iid, nick, body, muc);
}
else
{
for (const IrcMessage& message: it->second.second)
{
auto it_callback = irc_callbacks.find(message.command);
if (it_callback == irc_callbacks.end())
continue;
const auto& cb = it_callback->second.first;
try
{
(this->*(cb))(message);
}
catch (const std::exception& e)
{
log_error("Unhandled exception: ", e.what());
}
}
}
return;
}
const std::string IrcClient::get_unique_identifier() const {
return this->bridge.get_jid() + this->hostname;
}
+14
View File
@@ -26,6 +26,7 @@
#include <map>
#include <set>
#include <utils/tokens_bucket.hpp>
#include <unordered_set>
class IrcClient;
@@ -155,6 +156,10 @@ public:
* Send the QUIT irc command
*/
void send_quit_command(const std::string& reason);
/**
* Send a batch of messages
*/
void send_batch(const std::string& reference_tag, const std::string& type, const std::vector<std::string>& parameters, std::vector<IrcMessage>& messages, MessageCallback callback);
/**
* Send a message to the gateway user, not generated by the IRC server,
* but that might be useful because we want to be verbose (for example we
@@ -244,6 +249,8 @@ public:
void on_cap(const IrcMessage& message);
private:
void cap_end();
void process_batch(const std::string& reference_tag);
const std::string get_unique_identifier() const;
public:
#ifdef WITH_SASL
void on_authenticate(const IrcMessage& message);
@@ -306,6 +313,7 @@ public:
*/
void on_channel_mode(const IrcMessage& message);
void on_quit(const IrcMessage& message);
void on_batch(const IrcMessage& message);
void on_unknown_message(const IrcMessage& message);
/**
* Return the number of joined channels
@@ -326,6 +334,9 @@ public:
* Store the history limit that the client asked when joining this room.
*/
HistoryLimit history_limit;
bool has_capability(const std::string& capability) const;
const std::optional<const std::string> get_capability_value(const std::string& capability) const;
private:
/**
* The hostname of the server we are connected to.
@@ -386,7 +397,9 @@ private:
*/
SaslState sasl_state{SaslState::unneeded};
#endif
std::unordered_map<std::string, std::optional<std::string>> cap_ls_response;
std::map<std::string, Capability> capabilities;
std::unordered_set<std::string> enabled_capabilities;
/**
* See http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt section 3.3
* We store the possible chanmodes in this object.
@@ -431,6 +444,7 @@ private:
Resolver dns_resolver;
TokensBucket tokens_bucket;
long int get_throttle_limit() const;
std::unordered_map<std::string, std::pair<std::pair<std::string, std::vector<std::string>>, std::vector<IrcMessage>>> batches;
};
+41 -1
View File
@@ -1,8 +1,27 @@
#include <irc/irc_message.hpp>
#include <iostream>
#include <string>
#include <utils/split.hpp>
#include <optional>
IrcMessage::IrcMessage(std::stringstream ss)
{
if (ss.peek() == '@')
{
std::string raw_tags;
ss.ignore();
ss >> raw_tags;
std::vector<std::string> tags = utils::split(raw_tags, ';', false);
for (auto &tag: tags)
{
auto pos = tag.find('=');
if (pos == std::string::npos)
this->tags[tag] = {};
else
this->tags[tag.substr(0, pos)] = tag.substr(pos+1);
}
ss.ignore();
}
if (ss.peek() == ':')
{
ss.ignore();
@@ -30,6 +49,7 @@ IrcMessage::IrcMessage(std::stringstream ss)
IrcMessage::IrcMessage(std::string&& prefix,
std::string&& command,
std::vector<std::string>&& args):
tags(),
prefix(std::move(prefix)),
command(std::move(command)),
arguments(std::move(args))
@@ -38,16 +58,36 @@ IrcMessage::IrcMessage(std::string&& prefix,
IrcMessage::IrcMessage(std::string&& command,
std::vector<std::string>&& args):
tags(),
prefix(),
command(std::move(command)),
arguments(std::move(args))
{
}
IrcMessage::IrcMessage(std::unordered_map<std::string, std::optional<std::string>>&& tags,
std::string&& prefix,
std::string&& command,
std::vector<std::string>&& args):
tags(std::move(tags)),
prefix(std::move(prefix)),
command(std::move(command)),
arguments(std::move(args))
{
}
std::ostream& operator<<(std::ostream& os, const IrcMessage& message)
{
os << "IrcMessage";
os << "[" << message.command << "]";
os << "{";
for (auto& tag: message.tags)
{
os << tag.first;
if (tag.second.has_value())
os << "=" << tag.second.value();
os << ";";
}
os << "}[" << message.command << "]";
for (const std::string& arg: message.arguments)
{
os << "{" << arg << "}";
+4
View File
@@ -5,6 +5,8 @@
#include <string>
#include <ostream>
#include <sstream>
#include <unordered_map>
#include <optional>
class IrcMessage
{
@@ -13,6 +15,7 @@ public:
IrcMessage(std::string str): IrcMessage{std::stringstream{str}} {}
IrcMessage(std::string&& prefix, std::string&& command, std::vector<std::string>&& args);
IrcMessage(std::string&& command, std::vector<std::string>&& args);
IrcMessage(std::unordered_map<std::string, std::optional<std::string>>&& tags, std::string&& prefix, std::string&& command, std::vector<std::string>&& args);
~IrcMessage() = default;
IrcMessage(const IrcMessage&) = delete;
@@ -20,6 +23,7 @@ public:
IrcMessage& operator=(const IrcMessage&) = delete;
IrcMessage& operator=(IrcMessage&&) = default;
std::unordered_map<std::string, std::optional<std::string>> tags;
std::string prefix;
std::string command;
std::vector<std::string> arguments;
+56
View File
@@ -0,0 +1,56 @@
#include "irc/parse_caps.hpp"
const std::unordered_map<std::string, std::optional<std::string>>
parse_caps(const std::string &caps) {
std::unordered_map<std::string, std::optional<std::string>> output;
// caps => multi-prefix sasl=PLAIN,EXTERNAL
// draft/multiline=max-bytes=40000,max-lines=10
const std::vector<std::string> &caps_list = utils::split(caps, ' ', false);
for (const std::string &cap : caps_list) {
// cap => multi-prefix
// cap => sasl=PLAIN,EXTERNAL
// cap => draft/multiline=max-bytes=40000,max-lines=10
const size_t delimiter = cap.find("=");
// delimiter(multi-prefix) => npos
// delimiter(sasl=PLAIN,EXTERNAL) => 5
// delimiter(draft/multiline=max-bytes=40000,max-lines=10) => 16
const std::string cap_name = cap.substr(0, delimiter);
std::optional<std::string> cap_value;
if (delimiter != std::string::npos)
cap_value = cap.substr(delimiter + 1);
output.insert_or_assign(cap_name, cap_value);
}
return output;
}
const std::vector<std::string> parse_cap_val_list(const std::string &value) {
// value(PLAIN,EXTERNAL) => PLAIN,EXTERNAL
return utils::split(value, ',', false);
}
const std::unordered_map<std::string, std::optional<std::string>>
parse_cap_keyval(const std::string &value) {
std::unordered_map<std::string, std::optional<std::string>> output;
// value(PLAIN,EXTERNAL) => PLAIN,EXTERNAL
// value(max-bytes=40000,max-lines=10) =>
// max-bytes=40000,max-lines=10
const std::vector<std::string> &values_list = utils::split(value, ',', false);
// values_list(PLAIN,EXTERNAL) => { "PLAIN", "EXTERNAL" }
// values_list(draft/multiline=max-bytes=40000,max-lines=10) => {
// "max-bytes=40000", "max-lines=10" }
for (const std::string &value : values_list) {
const size_t value_delimiter = value.find("=");
// key(PLAIN) => PLAIN
// key(max-bytes=40000) => max-bytes
const std::string key = value.substr(0, value_delimiter);
std::optional<std::string> val;
if (value_delimiter != std::string::npos) {
// val(max-bytes=40000) => 40000
val = value.substr(value_delimiter + 1);
}
output.insert_or_assign(key, val);
}
return output;
}
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include "utils/split.hpp"
#include <optional>
#include <set>
#include <sstream>
#include <unordered_map>
#include <vector>
/**
* Parse a list of IRC caps into a map of capabilities
*/
const std::unordered_map<std::string, std::optional<std::string>>
parse_caps(const std::string &caps);
/**
* Parse the value of a cap into a list of values
*
* Used for the sasl cap
*/
const std::vector<std::string> parse_cap_val_list(const std::string &value);
/**
* Parse the value of a cap into a key-value map
*
* Used for the multiline cap
*/
const std::unordered_map<std::string, std::optional<std::string>>
parse_cap_keyval(const std::string &value);
+3
View File
@@ -14,6 +14,7 @@
#include <csignal>
#include <identd/identd_server.hpp>
#include <scheduler/scheduler.hpp>
// A flag set by the SIGINT signal handler.
static std::atomic<bool> stop(false);
@@ -107,6 +108,7 @@ static int main_loop(std::string hostname, std::string password)
if (Config::get_int("identd_port", 113) != 0)
identd = std::make_unique<IdentdServer>(*xmpp_component, p, static_cast<uint16_t>(Config::get_int("identd_port", 113)));
Scheduler::start_scheduler();
auto timeout = TimedEventsManager::instance().get_timeout();
while (p->poll(timeout) != -1)
{
@@ -130,6 +132,7 @@ static int main_loop(std::string hostname, std::string password)
#endif
if (identd)
identd->shutdown();
Scheduler::stop_scheduler();
// Cancel the timer for a potential reconnection
TimedEventsManager::instance().cancel("XMPP reconnection");
}
+1 -1
View File
@@ -39,7 +39,7 @@ const std::string& BasicCredentialsManager::get_trusted_fingerprint() const
}
void check_tls_certificate(const std::vector<Botan::X509_Certificate>& certs,
const std::string& hostname, const std::string& trusted_fingerprint,
const std::string_view hostname, const std::string& trusted_fingerprint,
const std::exception_ptr& exc)
{
+1 -1
View File
@@ -19,7 +19,7 @@ class TCPSocketHandler;
* on a trusted fingerprint.
*/
void check_tls_certificate(const std::vector<Botan::X509_Certificate>& certs,
const std::string& hostname, const std::string& trusted_fingerprint,
const std::string_view hostname, const std::string& trusted_fingerprint,
const std::exception_ptr& exc);
class BasicCredentialsManager: public Botan::Credentials_Manager
+32 -27
View File
@@ -16,23 +16,29 @@
# include <botan/hex.h>
# include <botan/auto_rng.h>
# include <botan/tls_exceptn.h>
# include <botan/tls_session_manager_memory.h>
# include <botan/tls_session_manager_hybrid.h>
# include <config/config.hpp>
# include <utils/dirname.hpp>
namespace
{
Botan::AutoSeeded_RNG& get_rng()
std::shared_ptr<Botan::RandomNumberGenerator>& get_rng()
{
static Botan::AutoSeeded_RNG rng{};
static std::shared_ptr<Botan::RandomNumberGenerator> rng;
if (!rng)
rng = std::make_shared<Botan::AutoSeeded_RNG>();
return rng;
}
Botan::TLS::Session_Manager_In_Memory& get_session_manager()
std::shared_ptr<Botan::TLS::Session_Manager>& get_session_manager(std::shared_ptr<BasicCredentialsManager> creds)
{
static Botan::TLS::Session_Manager_In_Memory session_manager{get_rng()};
#if BOTAN_VERSION_CODE < BOTAN_VERSION_CODE_FOR(2,4,0)
// workaround for https://github.com/randombit/botan/issues/1276
session_manager.remove_all();
#endif
static std::shared_ptr<Botan::TLS::Session_Manager> session_manager;
if (!session_manager)
session_manager = std::make_shared<Botan::TLS::Session_Manager_Hybrid>(
std::make_unique<Botan::TLS::Session_Manager_In_Memory>(get_rng()),
creds, get_rng());
return session_manager;
}
}
@@ -50,7 +56,8 @@ TCPSocketHandler::TCPSocketHandler(std::shared_ptr<Poller>& poller):
SocketHandler(poller, -1),
use_tls(false)
#ifdef BOTAN_FOUND
,credential_manager()
,credential_manager(std::make_shared<BasicCredentialsManager>())
,policy(std::make_shared<BiboumiTLSPolicy>())
#endif
{}
@@ -242,11 +249,11 @@ void TCPSocketHandler::start_tls(const std::string& address, const std::string&
auto policy_directory = Config::get("policy_directory", utils::dirname(Config::get_filename()));
if (!policy_directory.empty() && policy_directory[policy_directory.size()-1] != '/')
policy_directory += '/';
this->policy.load(policy_directory + "policy.txt");
this->policy.load(policy_directory + address + ".policy.txt");
this->policy->load(policy_directory + "policy.txt");
this->policy->load(policy_directory + address + ".policy.txt");
this->tls = std::make_unique<Botan::TLS::Client>(
*this,
get_session_manager(), this->credential_manager, this->policy,
shared_from_this(),
get_session_manager(this->credential_manager), this->credential_manager, this->policy,
get_rng(), server_info, Botan::TLS::Protocol_Version::latest_tls_version());
}
@@ -298,17 +305,16 @@ void TCPSocketHandler::tls_send(std::string&& data)
std::make_move_iterator(data.end()));
}
void TCPSocketHandler::tls_record_received(uint64_t, const Botan::byte *data, size_t size)
void TCPSocketHandler::tls_record_received(uint64_t, std::span<const uint8_t> data)
{
this->in_buf += std::string(reinterpret_cast<const char*>(data),
size);
this->in_buf += std::string(data.begin(), data.end());
if (!this->in_buf.empty())
this->parse_in_buffer(size);
this->parse_in_buffer(data.size());
}
void TCPSocketHandler::tls_emit_data(const Botan::byte *data, size_t size)
void TCPSocketHandler::tls_emit_data(std::span<const uint8_t> data)
{
this->raw_send(std::string(reinterpret_cast<const char*>(data), size));
this->raw_send(std::string(data.begin(), data.end()));
}
void TCPSocketHandler::tls_alert(Botan::TLS::Alert alert)
@@ -316,25 +322,24 @@ void TCPSocketHandler::tls_alert(Botan::TLS::Alert alert)
log_debug("tls_alert: ", alert.type_string());
}
bool TCPSocketHandler::tls_session_established(const Botan::TLS::Session& session)
void TCPSocketHandler::tls_session_established(const Botan::TLS::Session_Summary& session)
{
log_debug("Handshake with ", session.server_info().hostname(), " complete.",
" Version: ", session.version().to_string(),
" using ", session.ciphersuite().to_string());
if (!session.session_id().empty())
log_debug("Session ID ", Botan::hex_encode(session.session_id()));
if (!session.session_ticket().empty())
log_debug("Session ticket ", Botan::hex_encode(session.session_ticket()));
return true;
if (session.session_ticket().has_value())
log_debug("Session ticket ", Botan::hex_encode(*session.session_ticket()));
}
void TCPSocketHandler::tls_verify_cert_chain(const std::vector<Botan::X509_Certificate>& cert_chain,
const std::vector<std::shared_ptr<const Botan::OCSP::Response>>& ocsp_responses,
const std::vector<std::optional<Botan::OCSP::Response>>& ocsp_responses,
const std::vector<Botan::Certificate_Store*>& trusted_roots,
Botan::Usage_Type usage, const std::string& hostname,
Botan::Usage_Type usage, std::string_view hostname,
const Botan::TLS::Policy& policy)
{
if (!this->policy.verify_certificate)
if (!this->policy->verify_certificate)
{
log_debug("Not verifying certificate due to domain policy ");
return;
@@ -352,7 +357,7 @@ void TCPSocketHandler::tls_verify_cert_chain(const std::vector<Botan::X509_Certi
if (this->abort_on_invalid_cert())
exception_ptr = std::current_exception();
check_tls_certificate(cert_chain, hostname, this->credential_manager.get_trusted_fingerprint(), exception_ptr);
check_tls_certificate(cert_chain, hostname, this->credential_manager->get_trusted_fingerprint(), exception_ptr);
}
}
+8 -7
View File
@@ -30,6 +30,7 @@
* But doesn’t do any connect() or accept() or anything else.
*/
class TCPSocketHandler: public SocketHandler
,public std::enable_shared_from_this<TCPSocketHandler>
#ifdef BOTAN_FOUND
,public Botan::TLS::Callbacks
#endif
@@ -137,12 +138,12 @@ private:
* Called by the tls object that some data has been decrypt. We call
* parse_in_buffer() to handle that unencrypted data.
*/
void tls_record_received(uint64_t rec_no, const Botan::byte* data, size_t size) override final;
void tls_record_received(uint64_t rec_no, std::span<const uint8_t> data) override final;
/**
* Called by the tls object to indicate that some data has been encrypted
* and is now ready to be sent on the socket as is.
*/
void tls_emit_data(const Botan::byte* data, size_t size) override final;
void tls_emit_data(std::span<const uint8_t> data) override final;
/**
* Called by the tls object to indicate that a TLS alert has been
* received. We don’t use it, we just log some message, at the moment.
@@ -152,13 +153,13 @@ private:
* Called by the tls object at the end of the TLS handshake. We don't do
* anything here appart from logging the TLS session information.
*/
bool tls_session_established(const Botan::TLS::Session& session) override final;
void tls_session_established(const Botan::TLS::Session_Summary& session) override final;
void tls_verify_cert_chain(const std::vector<Botan::X509_Certificate>& cert_chain,
const std::vector<std::shared_ptr<const Botan::OCSP::Response>>& ocsp_responses,
const std::vector<std::optional<Botan::OCSP::Response>>& ocsp_responses,
const std::vector<Botan::Certificate_Store*>& trusted_roots,
Botan::Usage_Type usage,
const std::string& hostname,
std::string_view hostname,
const Botan::TLS::Policy& policy) override final;
/**
* Called whenever the tls session goes from inactive to active. This
@@ -203,9 +204,9 @@ protected:
#ifdef BOTAN_FOUND
protected:
BasicCredentialsManager credential_manager;
std::shared_ptr<BasicCredentialsManager> credential_manager;
private:
BiboumiTLSPolicy policy;
std::shared_ptr<BiboumiTLSPolicy> policy;
/**
* We use a unique_ptr because we may not want to create the object at
* all. The Botan::TLS::Client object generates a handshake message and
+53 -2
View File
@@ -8,7 +8,6 @@
#include <network/tls_policy.hpp>
#include <logger/logger.hpp>
#include <botan/parsing.h>
#include <botan/exceptn.h>
bool BiboumiTLSPolicy::load(const std::string& filename)
@@ -31,7 +30,7 @@ bool BiboumiTLSPolicy::load(const std::string& filename)
void BiboumiTLSPolicy::load(std::istream& is)
{
const auto dict = Botan::read_cfg(is);
const auto dict = BiboumiTLSPolicy::read_cfg(is);
for (const auto& pair: dict)
{
// Workaround for options that are not overridden in Botan::TLS::Text_Policy
@@ -49,4 +48,56 @@ bool BiboumiTLSPolicy::require_cert_revocation_info() const
return this->req_cert_revocation_info;
}
std::map<std::string, std::string> BiboumiTLSPolicy::read_cfg(std::istream& is) {
std::map<std::string, std::string> kv;
size_t line = 0;
while(is.good()) {
std::string s;
std::getline(is, s);
++line;
if(s.empty() || s[0] == '#') {
continue;
}
s = clean_ws(s.substr(0, s.find('#')));
if(s.empty()) {
continue;
}
auto eq = s.find('=');
if(eq == std::string::npos || eq == 0 || eq == s.size() - 1) {
throw Botan::Decoding_Error("Bad read_cfg input '" + s + "' on line " + std::to_string(line));
}
const std::string key = BiboumiTLSPolicy::clean_ws(s.substr(0, eq));
const std::string val = BiboumiTLSPolicy::clean_ws(s.substr(eq + 1, std::string::npos));
kv[key] = val;
}
return kv;
}
std::string BiboumiTLSPolicy::clean_ws(std::string_view s) {
const char* ws = " \t\n";
auto start = s.find_first_not_of(ws);
auto end = s.find_last_not_of(ws);
if(start == std::string::npos) {
return "";
}
if(end == std::string::npos) {
return std::string(s.substr(start, end));
} else {
return std::string(s.substr(start, start + end + 1));
}
}
#endif
+6
View File
@@ -24,6 +24,12 @@ public:
bool verify_certificate{true};
protected:
bool req_cert_revocation_info{true};
private:
// Copied from botan-3's src/lib/utils/read_cfg.cpp
// These were made internal in v3, but they are in use by biboumi so they have
// to be vendored in
static std::map<std::string, std::string> read_cfg(std::istream& is);
static std::string clean_ws(std::string_view s);
};
#endif
+107
View File
@@ -0,0 +1,107 @@
#include "biboumi.h"
#include <config/config.hpp>
#include <logger/logger.hpp>
#include <scheduler/scheduler.hpp>
#include <utils/parse_duration.hpp>
#include <utils/timed_events.hpp>
#ifdef USE_DATABASE
#include <database/database_type.hpp>
#include <database/database.hpp>
#endif
static const std::unordered_map<
std::string, std::pair<std::chrono::seconds, std::function<void()>>>
events = {
#ifdef USE_DATABASE
{"cleanup_old_messages",
{std::chrono::seconds(10min), &Scheduler::cleanup_old_messages}},
{"optimize_db",
{std::chrono::seconds(24h), &Scheduler::optimize_db}},
#endif // USE_DATABASE
};
void Scheduler::start_scheduler() {
for (const auto &job : events) {
const std::string name = job.first;
const std::chrono::seconds interval = job.second.first;
const std::function<void()> job_function = job.second.second;
// Run it once at startup
job_function();
// Then every interval
TimedEventsManager::instance().add_event(
TimedEvent(interval, job_function, name));
}
}
void Scheduler::stop_scheduler() {
for (const auto &job : events) {
const std::string name = job.first;
TimedEventsManager::instance().cancel(name);
}
}
#ifdef USE_DATABASE
void Scheduler::cleanup_old_messages() {
// Get the config value
const std::string archive_expires_after =
Config::get("archive_expires_after", "0");
const std::optional<long> seconds =
utils::parse_duration(archive_expires_after);
// If it's actually set and relevant, start cleanup
if (seconds.has_value() && seconds.value() != 0) {
// Calculate age of messages to be deleted
log_debug("[cleanup_old_messages] Deleting messages older than ",
archive_expires_after, " (", seconds.value(), " seconds)");
// Convert the config value to a duration
const auto duration = std::chrono::duration<long>(seconds.value());
// Get timestamp now relative to unix epoch
const auto now = std::chrono::system_clock::now();
// Older than this get deleted
const auto ago = now - duration;
// Convert to seconds since epoch
const auto since_epoch = std::chrono::duration_cast<std::chrono::seconds>(
ago.time_since_epoch());
Database::delete_old_muclines(since_epoch.count());
} else
log_debug("[cleanup_old_messages] Not deleting anything. "
"archive_expires_after is ",
archive_expires_after);
}
void Scheduler::optimize_db() {
log_debug("[optimize_db] Starting. Type: ", (int)Database::db->get_type());
switch (Database::db->get_type()) {
# ifdef SQLITE3_FOUND
case DatabaseType::sqlite3: {
log_debug("[optimize_db] Optimizing Sqlite database");
// Run PRAGMA optimize periodically, as recommended by sqlite
// https://sqlite.org/lang_analyze.html#automatically_running_analyze
auto [success, err_msg] = Database::db->raw_exec("PRAGMA optimize");
if (!success) {
log_error("[optimize_db] Failed optimizing Sqlite database: ", err_msg);
}
break;
}
# endif // SQLITE3_FOUND
# ifdef PQ_FOUND
case DatabaseType::postgresql: {
log_debug("[optimize_db] Optimizing PostgreSQL database");
// Run VACUUM and ANALYZE periodically
// https://www.postgresql.org/docs/current/sql-vacuum.html
// https://www.postgresql.org/docs/current/sql-analyze.html
auto [success, err_msg] = Database::db->raw_exec("VACUUM ANALYZE");
if (!success) {
log_error("[optimize_db] Failed optimizing PostgreSQL database: ",
err_msg);
}
break;
}
# endif // PQ_FOUND
case DatabaseType::none:
default:
break;
}
}
#endif // USE_DATABASE
+11
View File
@@ -0,0 +1,11 @@
#pragma once
class Scheduler {
public:
static void start_scheduler();
static void stop_scheduler();
// Jobs
static void cleanup_old_messages();
static void optimize_db();
};
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <config/config.hpp>
namespace utils {
template <typename T>
inline std::vector<std::vector<T>> chunk(const std::vector<T> &vector,
const std::size_t size) {
std::vector<std::vector<T>> output;
std::vector<T> current_chunk;
for (const auto &el : vector) {
current_chunk.push_back(el);
if (current_chunk.size() >= size) {
output.push_back(current_chunk);
current_chunk = {};
}
}
if (current_chunk.size() != 0)
output.push_back(current_chunk);
return output;
}
} // namespace utils
@@ -0,0 +1,55 @@
#pragma once
#include "xmpp/jid.hpp"
#include <config/config.hpp>
namespace utils
{
/**
* Check config if a given requester is allowed to persist target
*
* The following values are checked
*
* - A user's jid (foo@example.com)
* - A user's domain (example.com)
*
* The following config options are checked in order
*
* 1. `persist_user_denylist` contains the requester's jid
* 2. `persist_user_allowlist` contains the requester's jid
* 3. `persist_user_denylist` contains the requester's domain
* 4. `persist_user_allowlist` contains the contains the requester's domain
* 5. `default_persistence_allowed` is true (default true)
*/
inline bool is_requester_allowed_to_persist(const Jid &requester) {
// requester.bare checks
// deny
// 1.
bool jid_in_user_denylist = Config::is_in_list("persist_user_denylist", requester.bare());
if (jid_in_user_denylist)
return false;
// allow
// 2.
bool jid_in_user_allowlist = Config::is_in_list("persist_user_allowlist", requester.bare());
if (jid_in_user_allowlist)
return true;
// requester.domain checks
// deny
// 3.
bool domain_in_user_denylist = Config::is_in_list("persist_user_denylist", requester.domain);
if (domain_in_user_denylist)
return false;
// allow
// 4.
bool domain_in_user_allowlist = Config::is_in_list("persist_user_allowlist", requester.domain);
if (domain_in_user_allowlist)
return true;
// default allow
// 5.
return Config::get_bool("default_persistence_allowed", true);
}
}
+36
View File
@@ -0,0 +1,36 @@
#include <cstdio>
#include <optional>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <utils/parse_duration.hpp>
namespace utils {
// Strongly inspired from prosody
// https://hg.prosody.im/trunk/file/0ff11f2e87cd/util/human/io.lua#l214
static const long day = 86400;
static const std::unordered_map<std::string, long> multipliers = {
{"d", day}, {"w", day * 7}, {"mon", day * 31}, {"y", day * 365.2425},
{"s", 1}, {"min", 60}, {"h", 3600}, {"ho", 3600},
};
std::optional<long> parse_duration(const std::string &duration) {
std::size_t ptr;
long number;
try {
number = std::stol(duration, &ptr, 10);
} catch (std::invalid_argument &) {
return {};
}
const std::string m = duration.substr(ptr);
if (!multipliers.contains(m))
return {};
const long multiplier = multipliers.at(m);
return multiplier * number;
}
} // namespace utils
+8
View File
@@ -0,0 +1,8 @@
#pragma once
#include <optional>
#include <string>
namespace utils {
std::optional<long> parse_duration(const std::string &duration);
} // namespace utils
+1
View File
@@ -13,6 +13,7 @@
# include <vector>
# include <iomanip>
# include <sstream>
# include <cstdint>
#endif
std::string sha1(const std::string& input)
+1 -1
View File
@@ -22,7 +22,7 @@ class TimedEvent
friend class TimedEventsManager;
public:
/**
* An event the occurs only once, at the given time_point
* An event that occurs only once, at the given time_point
*/
explicit TimedEvent(std::chrono::steady_clock::time_point&& time_point,
std::function<void()> callback, std::string name="");
+3 -3
View File
@@ -26,7 +26,7 @@ void PingStep1(XmppComponent&, AdhocSession&, XmlNode& command_node)
void HelloStep1(XmppComponent&, AdhocSession&, XmlNode& command_node)
{
XmlSubNode x(command_node, "jabber:x:data:x");
XmlSubNode x(command_node, "jabber:x:data", "x");
x["type"] = "form";
XmlSubNode title(x, "title");
title.set_inner("Configure your name.");
@@ -65,9 +65,9 @@ void HelloStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node)
}
}
command_node.delete_all_children();
XmlSubNode error(command_node, ADHOC_NS":error");
XmlSubNode error(command_node, ADHOC_NS, "error");
error["type"] = "modify";
XmlSubNode condition(error, STANZA_NS":bad-request");
XmlSubNode condition(error, STANZA_NS, "bad-request");
session.terminate();
}
+6 -6
View File
@@ -36,16 +36,16 @@ XmlNode AdhocCommandsHandler::handle_request(const std::string& executor_jid, co
auto command_it = this->commands.find(node);
if (command_it == this->commands.end())
{
XmlSubNode error(command_node, ADHOC_NS":error");
XmlSubNode error(command_node, ADHOC_NS, "error");
error["type"] = "cancel";
XmlSubNode condition(error, STANZA_NS":item-not-found");
XmlSubNode condition(error, STANZA_NS, "item-not-found");
}
else if (command_it->second.is_admin_only() &&
!Config::is_in_list("admin", jid.bare()))
{
XmlSubNode error(command_node, ADHOC_NS":error");
XmlSubNode error(command_node, ADHOC_NS, "error");
error["type"] = "cancel";
XmlSubNode condition(error, STANZA_NS":forbidden");
XmlSubNode condition(error, STANZA_NS, "forbidden");
}
else
{
@@ -94,9 +94,9 @@ XmlNode AdhocCommandsHandler::handle_request(const std::string& executor_jid, co
}
else // unsupported action
{
XmlSubNode error(command_node, ADHOC_NS":error");
XmlSubNode error(command_node, ADHOC_NS, "error");
error["type"] = "modify";
XmlSubNode condition(error, STANZA_NS":bad-request");
XmlSubNode condition(error, STANZA_NS, "bad-request");
}
}
return command_node;
+22 -17
View File
@@ -3,6 +3,7 @@
#include <utils/scopeguard.hpp>
#include <bridge/bridge.hpp>
#include <config/config.hpp>
#include <utils/is_requester_allowed_to_persist.hpp>
#include <utils/string.hpp>
#include <utils/split.hpp>
#include <xmpp/jid.hpp>
@@ -34,7 +35,7 @@ void DisconnectUserStep1(XmppComponent& xmpp_component, AdhocSession&, XmlNode&
{
auto& biboumi_component = dynamic_cast<BiboumiComponent&>(xmpp_component);
XmlSubNode x(command_node, "jabber:x:data:x");
XmlSubNode x(command_node, "jabber:x:data", "x");
x["type"] = "form";
XmlSubNode title(x, "title");
title.set_inner("Disconnect a user from the gateway");
@@ -108,9 +109,9 @@ void DisconnectUserStep2(XmppComponent& xmpp_component, AdhocSession& session, X
return;
}
}
XmlSubNode error(command_node, ADHOC_NS":error");
XmlSubNode error(command_node, ADHOC_NS, "error");
error["type"] = "modify";
XmlSubNode condition(error, STANZA_NS":bad-request");
XmlSubNode condition(error, STANZA_NS, "bad-request");
session.terminate();
}
@@ -124,7 +125,7 @@ void ConfigureGlobalStep1(XmppComponent&, AdhocSession& session, XmlNode& comman
auto options = Database::get_global_options(owner.bare());
command_node.delete_all_children();
XmlSubNode x(command_node, "jabber:x:data:x");
XmlSubNode x(command_node, "jabber:x:data", "x");
x["type"] = "form";
XmlSubNode title(x, "title");
title.set_inner("Configure some global default settings.");
@@ -159,6 +160,7 @@ void ConfigureGlobalStep1(XmppComponent&, AdhocSession& session, XmlNode& comman
}
}
if (utils::is_requester_allowed_to_persist(Jid(session.get_owner_jid())))
{
XmlSubNode persistent(x, "field");
persistent["var"] = "persistent";
@@ -208,7 +210,8 @@ void ConfigureGlobalStep2(XmppComponent& xmpp_component, AdhocSession& session,
bridge->set_record_history(options.col<Database::RecordHistory>());
}
else if (field->get_tag("var") == "persistent" &&
value)
value
&& utils::is_requester_allowed_to_persist(Jid(session.get_owner_jid())))
options.col<Database::GlobalPersistent>() = to_bool(value->get_inner());
}
@@ -220,9 +223,9 @@ void ConfigureGlobalStep2(XmppComponent& xmpp_component, AdhocSession& session,
note.set_inner("Configuration successfully applied.");
return;
}
XmlSubNode error(command_node, ADHOC_NS":error");
XmlSubNode error(command_node, ADHOC_NS, "error");
error["type"] = "modify";
XmlSubNode condition(error, STANZA_NS":bad-request");
XmlSubNode condition(error, STANZA_NS, "bad-request");
session.terminate();
}
@@ -238,7 +241,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
auto commands = Database::get_after_connection_commands(options);
command_node.delete_all_children();
XmlSubNode x(command_node, "jabber:x:data:x");
XmlSubNode x(command_node, "jabber:x:data", "x");
x["type"] = "form";
XmlSubNode title(x, "title");
title.set_inner("Configure the IRC server " + server_domain);
@@ -263,7 +266,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
XmlSubNode ports(x, "field");
ports["var"] = "ports";
ports["type"] = "text-multi";
ports["label"] = "Ports";
ports["label"] = "Unencrypted ports";
set_desc(ports, "List of ports to try, without TLS. Defaults: 6667.");
for (const auto& val: utils::split(options.col<Database::Ports>(), ';', false))
{
@@ -565,9 +568,9 @@ void ConfigureIrcServerStep2(XmppComponent& xmpp_component, AdhocSession& sessio
note.set_inner("Configuration successfully applied.");
return;
}
XmlSubNode error(command_node, ADHOC_NS":error");
XmlSubNode error(command_node, ADHOC_NS, "error");
error["type"] = "modify";
XmlSubNode condition(error, STANZA_NS":bad-request");
XmlSubNode condition(error, STANZA_NS, "bad-request");
session.terminate();
}
@@ -586,7 +589,7 @@ void insert_irc_channel_configuration_form(XmlNode& node, const Jid& requester,
auto options = Database::get_irc_channel_options_with_server_default(requester.local + "@" + requester.domain,
iid.get_server(), iid.get_local());
node.delete_all_children();
XmlSubNode x(node, "jabber:x:data:x");
XmlSubNode x(node, "jabber:x:data", "x");
x["type"] = "form";
XmlSubNode title(x, "title");
title.set_inner("Configure the IRC channel " + iid.get_local() + " on server " + iid.get_server());
@@ -640,6 +643,7 @@ void insert_irc_channel_configuration_form(XmlNode& node, const Jid& requester,
}
}
if (utils::is_requester_allowed_to_persist(requester))
{
XmlSubNode persistent(x, "field");
persistent["var"] = "persistent";
@@ -671,9 +675,9 @@ void ConfigureIrcChannelStep2(XmppComponent& xmpp_component, AdhocSession& sessi
}
else
{
XmlSubNode error(command_node, ADHOC_NS":error");
XmlSubNode error(command_node, ADHOC_NS, "error");
error["type"] = "modify";
XmlSubNode condition(error, STANZA_NS":bad-request");
XmlSubNode condition(error, STANZA_NS, "bad-request");
session.terminate();
}
}
@@ -699,7 +703,7 @@ bool handle_irc_channel_configuration_form(XmppComponent& xmpp_component, const
else if (field->get_tag("var") == "encoding_in" && value)
options.col<Database::EncodingIn>() = value->get_inner();
else if (field->get_tag("var") == "persistent" && value)
else if (field->get_tag("var") == "persistent" && value && utils::is_requester_allowed_to_persist(requester))
options.col<Database::Persistent>() = to_bool(value->get_inner());
else if (field->get_tag("var") == "record_history" &&
value && !value->get_inner().empty())
@@ -749,7 +753,7 @@ void DisconnectUserFromServerStep1(XmppComponent& xmpp_component, AdhocSession&
{ // Send a form to select the user to disconnect
auto& biboumi_component = dynamic_cast<BiboumiComponent&>(xmpp_component);
XmlSubNode x(command_node, "jabber:x:data:x");
XmlSubNode x(command_node, "jabber:x:data", "x");
x["type"] = "form";
XmlSubNode title(x, "title");
title.set_inner("Disconnect a user from selected IRC servers");
@@ -794,7 +798,7 @@ void DisconnectUserFromServerStep2(XmppComponent& xmpp_component, AdhocSession&
command_node.delete_all_children();
auto& biboumi_component = dynamic_cast<BiboumiComponent&>(xmpp_component);
XmlSubNode x(command_node, "jabber:x:data:x");
XmlSubNode x(command_node, "jabber:x:data", "x");
x["type"] = "form";
XmlSubNode title(x, "title");
title.set_inner("Disconnect a user from selected IRC servers");
@@ -953,3 +957,4 @@ void GetIrcConnectionInfoStep1(XmppComponent& component, AdhocSession& session,
message = ss.str();
}
+14 -5
View File
@@ -295,6 +295,15 @@ void BiboumiComponent::handle_message(const Stanza& stanza)
error_type, error_name, error_text);
});
const XmlNode* body = stanza.get_child("body", COMPONENT_NS);
const XmlNode* replace = stanza.get_child("replace", MESSAGE_CORRECT_NS);
if (replace)
{
error_type = "cancel";
error_name = "feature-not-implemented";
error_text = "This room is mapped from IRC, which does not support message correction.";
return;
}
try { // catch IRCNotConnected exceptions
if (type == "groupchat" && iid.type == Iid::Type::Channel)
@@ -482,7 +491,7 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
// Depending on the 'to' jid in the request, we use one adhoc
// command handler or an other
Iid iid(to.local, {'#', '&'});
Iid iid(to.local, {'&', '#', '+', '!'});
AdhocCommandsHandler* adhoc_handler;
if (to.local.empty())
adhoc_handler = &this->adhoc_commands_handler;
@@ -532,7 +541,7 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
const XmlNode* query;
if ((query = stanza.get_child("query", DISCO_INFO_NS)))
{ // Disco info
Iid iid(to.local, {'#', '&'});
Iid iid(to.local, {'&', '#', '+', '!'});
const std::string node = query->get_tag("node");
if (to_str == this->served_hostname)
{
@@ -736,7 +745,7 @@ bool BiboumiComponent::handle_mam_request(const Stanza& stanza)
const XmlNode* query = stanza.get_child("query", MAM_NS);
Iid iid(to.local, {'#', '&'});
Iid iid(to.local, {'&', '#', '+', '!'});
if (query && iid.type == Iid::Type::Channel && to.resource.empty())
{
const std::string query_id = query->get_tag("queryid");
@@ -865,7 +874,7 @@ void BiboumiComponent::send_archived_message(const Database::MucLogLine& log_lin
bool BiboumiComponent::handle_room_configuration_form_request(const std::string& from, const Jid& to, const std::string& id)
{
Iid iid(to.local, {'#', '&'});
Iid iid(to.local, {'&', '#', '+', '!'});
if (iid.type != Iid::Type::Channel || !to.resource.empty())
return false;
@@ -887,7 +896,7 @@ bool BiboumiComponent::handle_room_configuration_form_request(const std::string&
bool BiboumiComponent::handle_room_configuration_form(const XmlNode& query, const std::string &from, const Jid &to, const std::string &id)
{
Iid iid(to.local, {'#', '&'});
Iid iid(to.local, {'&', '#', '+', '!'});
if (iid.type != Iid::Type::Channel || !to.resource.empty())
return false;
+1 -1
View File
@@ -175,7 +175,7 @@ void XmppComponent::on_stanza(const Stanza& stanza)
void XmppComponent::send_stream_error(const std::string& name, const std::string& explanation)
{
Stanza node("stream:error");
Stanza node("stream", "error");
{
XmlSubNode error(node, name);
error["xmlns"] = STREAM_NS;
+1
View File
@@ -40,6 +40,7 @@
#define STABLE_ID_NS "urn:xmpp:sid:0"
#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"
/**
* An XMPP component, communicating with an XMPP server using the protocole
+1 -1
View File
@@ -38,7 +38,7 @@ XmppParser::XmppParser():
void XmppParser::init_xml_parser()
{
// Create the expat parser
this->parser = XML_ParserCreateNS("UTF-8", ':');
this->parser = XML_ParserCreateNS("UTF-8", '\1');
XML_SetUserData(this->parser, static_cast<void*>(this));
// Install Expat handlers
+2 -2
View File
@@ -18,9 +18,9 @@
* stanza is reasonnably short.
*
* The element names generated by expat contain the namespace of the
* element, a colon (':') and then the actual name of the element. To get
* element, a \1 separator and then the actual name of the element. To get
* an element "x" with a namespace of "http://jabber.org/protocol/muc", you
* just look for an XmlNode named "http://jabber.org/protocol/muc:x"
* just look for an XmlNode named "http://jabber.org/protocol/muc\1x"
*
* TODO: enforce the size-limit for the stanza (limit the number of childs
* it can contain). For example forbid the parser going further than level
+13 -1
View File
@@ -52,7 +52,7 @@ XmlNode::XmlNode(const std::string& name, XmlNode* parent):
parent(parent)
{
// split the namespace and the name
auto n = name.rfind(':');
auto n = name.rfind('\1');
if (n == std::string::npos)
this->name = name;
else
@@ -67,6 +67,18 @@ XmlNode::XmlNode(const std::string& name):
{
}
XmlNode::XmlNode(const std::string& xmlns, const std::string& name, XmlNode* parent):
name(name),
parent(parent)
{
this->attributes["xmlns"] = xmlns;
}
XmlNode::XmlNode(const std::string& xmlns, const std::string& name):
XmlNode(xmlns, name, nullptr)
{
}
void XmlNode::delete_all_children()
{
this->children.clear();
+6
View File
@@ -25,6 +25,8 @@ class XmlNode
public:
explicit XmlNode(const std::string& name, XmlNode* parent);
explicit XmlNode(const std::string& name);
explicit XmlNode(const std::string& xmlns, const std::string& name, XmlNode* parent);
explicit XmlNode(const std::string& xmlns, const std::string& name);
/**
* The copy constructor does not copy the parent attribute. The children
* nodes are all copied recursively.
@@ -150,6 +152,10 @@ public:
XmlNode(name),
parent_to_add(parent_ref)
{}
XmlSubNode(XmlNode& parent_ref, const std::string& xmlns, const std::string& name):
XmlNode(xmlns, name),
parent_to_add(parent_ref)
{}
~XmlSubNode()
{