diff options
Diffstat (limited to 'src')
39 files changed, 526 insertions, 239 deletions
diff --git a/src/bridge/bridge.cpp b/src/bridge/bridge.cpp index 7a0157a..71c0ea4 100644 --- a/src/bridge/bridge.cpp +++ b/src/bridge/bridge.cpp @@ -63,7 +63,8 @@ void Bridge::shutdown(const std::string& exit_message) { for (auto& pair: this->irc_clients) { - pair.second->send_quit_command(exit_message); + std::unique_ptr<IrcClient>& irc = pair.second; + irc->send_quit_command(exit_message); } } @@ -133,11 +134,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_shared<IrcClient>(this->poller, hostname, + std::make_unique<IrcClient>(this->poller, hostname, nickname, username, realname, jid.domain, *this)); - std::shared_ptr<IrcClient> irc = this->irc_clients.at(hostname); + std::unique_ptr<IrcClient>& irc = this->irc_clients.at(hostname); return irc.get(); } } @@ -223,6 +224,27 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body, std:: bool first = true; for (const std::string& line: lines) { + std::string uuid; +#ifdef USE_DATABASE + const auto xmpp_body = this->make_xmpp_body(line); + 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()); +#endif + if (!first || id.empty()) + id = utils::gen_uuid(); + + MessageCallback mirror_to_all_resources = [this, iid, uuid, id](const IrcClient* irc, const IrcMessage& message) { + std::string line = message.arguments[1]; + // “temporary” workaround for \01ACTION…\01 -> /me messages + if ((line.size() > strlen("\01ACTION\01")) && + (line.substr(0, 7) == "\01ACTION") && line[line.size() - 1] == '\01') + line = "/me " + line.substr(8, line.size() - 9); + for (const auto& resource: this->resources_in_chan[iid.to_tuple()]) + this->xmpp.send_muc_message(std::to_string(iid), irc->get_own_nick(), this->make_xmpp_body(line), + this->user_jid + "/" + resource, uuid, id); + }; + if (line.substr(0, 5) == "/mode") { std::vector<std::string> args = utils::split(line.substr(5), ' ', false); @@ -231,22 +253,11 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body, std:: // XMPP user, that’s not a textual message. } else if (line.substr(0, 4) == "/me ") - irc->send_channel_message(iid.get_local(), action_prefix + line.substr(4) + "\01"); + irc->send_channel_message(iid.get_local(), action_prefix + line.substr(4) + "\01", + std::move(mirror_to_all_resources)); else - irc->send_channel_message(iid.get_local(), line); + irc->send_channel_message(iid.get_local(), line, std::move(mirror_to_all_resources)); - std::string uuid; -#ifdef USE_DATABASE - const auto xmpp_body = this->make_xmpp_body(line); - 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()); -#endif - if (!first || id.empty()) - id = utils::gen_uuid(); - for (const auto& resource: this->resources_in_chan[iid.to_tuple()]) - this->xmpp.send_muc_message(std::to_string(iid), irc->get_own_nick(), this->make_xmpp_body(line), - this->user_jid + "/" + resource, uuid, id); first = false; } } @@ -449,9 +460,8 @@ void Bridge::leave_irc_channel(Iid&& iid, const std::string& status_message, con true, true, resource, irc); this->remove_resource_from_chan(key, resource); } - if (this->number_of_channels_the_resource_is_in(iid.get_server(), resource) == 0) - this->remove_resource_from_server(iid.get_server(), resource); - + if (this->number_of_channels_the_resource_is_in(iid.get_server(), resource) == 0) + this->remove_resource_from_server(iid.get_server(), resource); } void Bridge::send_irc_nick_change(const Iid& iid, const std::string& new_nick, const std::string& requesting_resource) @@ -737,10 +747,27 @@ void Bridge::send_irc_participant_ping_request(const Iid& iid, const std::string IrcChannel* chan = irc->get_channel(iid.get_local()); if (!chan->joined || !this->is_resource_in_chan(iid.to_tuple(), from.resource)) { - this->xmpp.send_stanza_error("iq", to_jid, from_jid, iq_id, "cancel", "not-allowed", + this->xmpp.send_stanza_error("iq", to_jid, from_jid, iq_id, "cancel", "not-acceptable", "", true); return; } + if (chan->get_self()->nick == nick) + { + // XEP-0410 self-ping optimisation: always reply without going the full + // round-trip through IRC and possibly another XMPP client. See the XEP + // for details. + Jid iq_from(from_jid); + iq_from.local = std::to_string(iid); + iq_from.resource = nick; + + Stanza iq("iq"); + iq["from"] = iq_from.full(); + iq["to"] = to_jid; + iq["id"] = iq_id; + iq["type"] = "result"; + this->xmpp.send_stanza(iq); + return; + } if (chan->get_self()->nick != nick && !chan->find_user(nick)) { this->xmpp.send_stanza_error("iq", to_jid, from_jid, iq_id, "cancel", "item-not-found", @@ -816,7 +843,7 @@ void Bridge::send_irc_version_request(const std::string& irc_hostname, const std this->add_waiting_irc(std::move(cb)); } -void Bridge::send_message(const Iid& iid, const std::string& nick, const std::string& body, const bool muc) +void Bridge::send_message(const Iid& iid, const std::string& nick, const std::string& body, const bool muc, const bool log) { const auto encoding = in_encoding_for(*this, iid); std::string uuid{}; @@ -824,9 +851,11 @@ void Bridge::send_message(const Iid& iid, const std::string& nick, const std::st { #ifdef USE_DATABASE const auto xmpp_body = this->make_xmpp_body(body, encoding); - if (!nick.empty() && this->record_history) + if (log && 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), nick); +#else + (void)log; #endif for (const auto& resource: this->resources_in_chan[iid.to_tuple()]) { @@ -891,9 +920,7 @@ void Bridge::send_muc_leave(const Iid& iid, const IrcUser& user, for (const auto& r: resources_in_chan) if (this->number_of_channels_the_resource_is_in(iid.get_server(), r) == 0) this->remove_resource_from_server(iid.get_server(), r); - } - } IrcClient* irc = this->find_irc_client(iid.get_server()); if (self && irc && irc->number_of_joined_channels() == 0) @@ -967,8 +994,18 @@ void Bridge::send_user_join(const std::string& hostname, const std::string& chan std::string encoded_chan_name(chan_name); xep0106::encode(encoded_chan_name); - this->xmpp.send_user_join(encoded_chan_name + utils::empty_if_fixed_server("%" + hostname), user->nick, user->host, - affiliation, role, this->user_jid + "/" + resource, self); + std::string encoded_nick_name(user->nick); + xep0106::encode(encoded_nick_name); + + std::string full_jid = + encoded_nick_name + utils::empty_if_fixed_server("%" + hostname) + + "@" + this->xmpp.get_served_hostname(); + if (!user->host.empty()) + full_jid += "/" + user->host; + + this->xmpp.send_user_join(encoded_chan_name + utils::empty_if_fixed_server("%" + hostname), + user->nick, full_jid, affiliation, role, + this->user_jid + "/" + resource, self); } void Bridge::send_topic(const std::string& hostname, const std::string& chan_name, const std::string& topic, @@ -1001,11 +1038,13 @@ void Bridge::send_room_history(const std::string& hostname, const std::string& c void Bridge::send_room_history(const std::string& hostname, std::string chan_name, const std::string& resource, const HistoryLimit& history_limit) { #ifdef USE_DATABASE - const auto coptions = Database::get_irc_channel_options_with_server_and_global_default(this->user_jid, hostname, chan_name); - auto limit = coptions.col<Database::MaxHistoryLength>(); + const auto goptions = Database::get_global_options(this->user_jid); + auto limit = goptions.col<Database::MaxHistoryLength>(); + if (limit < 0) + limit = 20; if (history_limit.stanzas >= 0 && history_limit.stanzas < limit) limit = history_limit.stanzas; - const auto result = Database::get_muc_logs(this->user_jid, chan_name, hostname, limit, history_limit.since, {}, Id::unset_value, Database::Paging::last); + const auto result = Database::get_muc_logs(this->user_jid, chan_name, hostname, static_cast<std::size_t>(limit), history_limit.since, {}, Id::unset_value, Database::Paging::last); const auto& lines = std::get<1>(result); chan_name.append(utils::empty_if_fixed_server("%" + hostname)); for (const auto& line: lines) @@ -1142,12 +1181,12 @@ void Bridge::trigger_on_irc_message(const std::string& irc_hostname, const IrcMe } } -std::unordered_map<std::string, std::shared_ptr<IrcClient>>& Bridge::get_irc_clients() +std::unordered_map<std::string, std::unique_ptr<IrcClient>>& Bridge::get_irc_clients() { return this->irc_clients; } -const std::unordered_map<std::string, std::shared_ptr<IrcClient>>& Bridge::get_irc_clients() const +const std::unordered_map<std::string, std::unique_ptr<IrcClient>>& Bridge::get_irc_clients() const { return this->irc_clients; } @@ -1214,15 +1253,6 @@ void Bridge::remove_resource_from_server(const Bridge::IrcHostname& irc_hostname } } -bool Bridge::is_resource_in_server(const Bridge::IrcHostname& irc_hostname, const std::string& resource) const -{ - auto it = this->resources_in_server.find(irc_hostname); - if (it != this->resources_in_server.end()) - if (it->second.count(resource) == 1) - return true; - return false; -} - std::size_t Bridge::number_of_resources_in_chan(const Bridge::ChannelKey& channel) const { auto it = this->resources_in_chan.find(channel); diff --git a/src/bridge/bridge.hpp b/src/bridge/bridge.hpp index 8e7d9d7..fa2a31f 100644 --- a/src/bridge/bridge.hpp +++ b/src/bridge/bridge.hpp @@ -164,9 +164,9 @@ public: void send_room_history(const std::string& hostname, const std::string& chan_name, const HistoryLimit& history_limit); void send_room_history(const std::string& hostname, std::string chan_name, const std::string& resource, const HistoryLimit& history_limit); /** - * Send a MUC message from some participant + * Send a message from a MUC participant or a direct message */ - void send_message(const Iid& iid, const std::string& nick, const std::string& body, const bool muc); + void send_message(const Iid& iid, const std::string& nick, const std::string& body, const bool muc, const bool log=true); /** * Send a presence of type error, from a room. */ @@ -241,8 +241,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::shared_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::unique_ptr<IrcClient>>& get_irc_clients(); + const std::unordered_map<std::string, std::unique_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); @@ -275,7 +275,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::shared_ptr<IrcClient>> irc_clients; + std::unordered_map<std::string, std::unique_ptr<IrcClient>> irc_clients; /** * To communicate back with the XMPP component */ @@ -316,13 +316,14 @@ private: */ void add_resource_to_chan(const ChannelKey& channel, const std::string& resource); void remove_resource_from_chan(const ChannelKey& channel, const std::string& resource); +public: bool is_resource_in_chan(const ChannelKey& channel, const std::string& resource) const; +private: void remove_all_resources_from_chan(const ChannelKey& channel); std::size_t number_of_resources_in_chan(const ChannelKey& channel) const; void add_resource_to_server(const IrcHostname& irc_hostname, const std::string& resource); void remove_resource_from_server(const IrcHostname& irc_hostname, const std::string& resource); - bool is_resource_in_server(const IrcHostname& irc_hostname, const std::string& resource) const; size_t number_of_channels_the_resource_is_in(const std::string& irc_hostname, const std::string& resource) const; /** diff --git a/src/bridge/history_limit.hpp b/src/bridge/history_limit.hpp index 9c75256..93e36e1 100644 --- a/src/bridge/history_limit.hpp +++ b/src/bridge/history_limit.hpp @@ -1,5 +1,7 @@ #pragma once +#include <string> + // Default values means no limit struct HistoryLimit { diff --git a/src/database/count_query.hpp b/src/database/count_query.hpp index 118ce44..3990eb9 100644 --- a/src/database/count_query.hpp +++ b/src/database/count_query.hpp @@ -8,10 +8,10 @@ struct CountQuery: public Query { - CountQuery(std::string name): + CountQuery(const std::string& name): Query("SELECT count(*) FROM ") { - this->body += std::move(name); + this->body += name; } int64_t execute(DatabaseEngine& db) diff --git a/src/database/database.cpp b/src/database/database.cpp index 6e08ee1..861abcb 100644 --- a/src/database/database.cpp +++ b/src/database/database.cpp @@ -162,10 +162,6 @@ Database::IrcChannelOptions Database::get_irc_channel_options_with_server_and_gl coptions.col<EncodingOut>() = get_first_non_empty(coptions.col<EncodingOut>(), soptions.col<EncodingOut>()); - coptions.col<MaxHistoryLength>() = get_first_non_empty(coptions.col<MaxHistoryLength>(), - soptions.col<MaxHistoryLength>(), - goptions.col<MaxHistoryLength>()); - return coptions; } @@ -338,7 +334,6 @@ Transaction::Transaction() log_error("Failed to create SQL transaction: ", std::get<std::string>(result)); else this->success = true; - } Transaction::~Transaction() diff --git a/src/database/database.hpp b/src/database/database.hpp index 3e25b30..a53f87b 100644 --- a/src/database/database.hpp +++ b/src/database/database.hpp @@ -63,8 +63,8 @@ class Database struct EncodingIn: Column<std::string> { static constexpr auto name = "encodingin_"; }; - struct MaxHistoryLength: Column<int> { static constexpr auto name = "maxhistorylength_"; - MaxHistoryLength(): Column<int>(20) {} }; + struct MaxHistoryLength: Column<std::int64_t> { static constexpr auto name = "maxhistorylength_"; + MaxHistoryLength(): Column<std::int64_t>(20) {} }; struct RecordHistory: Column<bool> { static constexpr auto name = "recordhistory_"; RecordHistory(): Column<bool>(true) {}}; @@ -86,13 +86,16 @@ class Database struct Address: Column<std::string> { static constexpr auto name = "address_"; }; + struct ThrottleLimit: Column<std::int64_t> { static constexpr auto name = "throttlelimit_"; + ThrottleLimit(): Column<std::int64_t>(10) {} }; + using MucLogLineTable = Table<Id, Uuid, Owner, IrcChanName, IrcServerName, Date, Body, Nick>; using MucLogLine = MucLogLineTable::RowType; using GlobalOptionsTable = Table<Id, Owner, MaxHistoryLength, RecordHistory, GlobalPersistent>; using GlobalOptions = GlobalOptionsTable::RowType; - using IrcServerOptionsTable = Table<Id, Owner, Server, Pass, TlsPorts, Ports, Username, Realname, VerifyCert, TrustedFingerprint, EncodingOut, EncodingIn, MaxHistoryLength, Address, Nick>; + using IrcServerOptionsTable = Table<Id, Owner, Server, Pass, TlsPorts, Ports, Username, Realname, VerifyCert, TrustedFingerprint, EncodingOut, EncodingIn, MaxHistoryLength, Address, Nick, ThrottleLimit>; using IrcServerOptions = IrcServerOptionsTable::RowType; using IrcChannelOptionsTable = Table<Id, Owner, Server, Channel, EncodingOut, EncodingIn, MaxHistoryLength, Persistent, RecordHistoryOptional>; diff --git a/src/database/postgresql_statement.hpp b/src/database/postgresql_statement.hpp index 37e8ea0..345a942 100644 --- a/src/database/postgresql_statement.hpp +++ b/src/database/postgresql_statement.hpp @@ -15,7 +15,7 @@ class PostgresqlStatement: public Statement body(std::move(body)), conn(conn) {} - ~PostgresqlStatement() + virtual ~PostgresqlStatement() { PQclear(this->result); this->result = nullptr; @@ -89,8 +89,6 @@ class PostgresqlStatement: public Statement return true; } - private: - private: bool execute(const bool second_attempt=false) { @@ -119,11 +117,7 @@ private: PQreset(this->conn); return this->execute(true); } - else - { - log_error("Givin up."); - return false; - } + return false; } return true; } diff --git a/src/database/query.cpp b/src/database/query.cpp index d72066e..5ec8599 100644 --- a/src/database/query.cpp +++ b/src/database/query.cpp @@ -6,11 +6,6 @@ void actual_bind(Statement& statement, const std::string& value, int index) statement.bind_text(index, value); } -void actual_bind(Statement& statement, const std::int64_t& value, int index) -{ - statement.bind_int64(index, value); -} - void actual_bind(Statement& statement, const OptionalBool& value, int index) { if (!value.is_set) diff --git a/src/database/query.hpp b/src/database/query.hpp index ba28b1a..c89371f 100644 --- a/src/database/query.hpp +++ b/src/database/query.hpp @@ -12,13 +12,14 @@ #include <string> void actual_bind(Statement& statement, const std::string& value, int index); -void actual_bind(Statement& statement, const std::int64_t& value, int index); -template <typename T, typename std::enable_if_t<std::is_integral<T>::value>* = 0> +void actual_bind(Statement& statement, const OptionalBool& value, int index); +template <typename T> void actual_bind(Statement& statement, const T& value, int index) { - actual_bind(statement, static_cast<std::int64_t>(value), index); + static_assert(std::is_integral<T>::value, + "Only a string, an optional-bool or an integer can be used."); + statement.bind_int64(index, static_cast<std::int64_t>(value)); } -void actual_bind(Statement& statement, const OptionalBool& value, int index); #ifdef DEBUG_SQL_QUERIES #include <utils/scopetimer.hpp> diff --git a/src/database/row.hpp b/src/database/row.hpp index 1253f93..4004b5d 100644 --- a/src/database/row.hpp +++ b/src/database/row.hpp @@ -28,7 +28,7 @@ struct Row this->clear_col<0>(); } - std::tuple<T...> columns; + std::tuple<T...> columns{}; std::string table_name; private: diff --git a/src/database/select_query.hpp b/src/database/select_query.hpp index b9fdc06..e372f2e 100644 --- a/src/database/select_query.hpp +++ b/src/database/select_query.hpp @@ -135,7 +135,7 @@ struct SelectQuery: public Query }; template <typename... T> -auto select(const Table<T...> table) +auto select(const Table<T...>& table) { SelectQuery<T...> query(table.name); return query; diff --git a/src/irc/irc_client.cpp b/src/irc/irc_client.cpp index d7fa2cd..de38d42 100644 --- a/src/irc/irc_client.cpp +++ b/src/irc/irc_client.cpp @@ -135,7 +135,7 @@ IrcClient::IrcClient(std::shared_ptr<Poller>& poller, std::string hostname, std::string realname, std::string user_hostname, Bridge& bridge): TCPClientSocketHandler(poller), - hostname(std::move(hostname)), + hostname(hostname), user_hostname(std::move(user_hostname)), username(std::move(username)), realname(std::move(realname)), @@ -143,7 +143,14 @@ 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; + this->actual_send(std::move(this->message_queue.front())); + this->message_queue.pop_front(); + return false; + }, "TokensBucket" + this->hostname + this->bridge.get_jid()) { #ifdef USE_DATABASE auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(), @@ -171,6 +178,7 @@ IrcClient::~IrcClient() // This event may or may not exist (if we never got connected, it // doesn't), but it's ok TimedEventsManager::instance().cancel("PING" + this->hostname + this->bridge.get_jid()); + TimedEventsManager::instance().cancel("TokensBucket" + this->hostname + this->bridge.get_jid()); } void IrcClient::start() @@ -317,9 +325,21 @@ IrcChannel* IrcClient::get_channel(const std::string& n) } catch (const std::out_of_range& exception) { - this->channels.emplace(name, std::make_unique<IrcChannel>()); + return this->channels.emplace(name, std::make_unique<IrcChannel>()).first->second.get(); + } +} + +const IrcChannel* IrcClient::find_channel(const std::string& n) const +{ + const std::string name = utils::tolower(n); + try + { return this->channels.at(name).get(); } + catch (const std::out_of_range& exception) + { + return nullptr; + } } bool IrcClient::is_channel_joined(const std::string& name) @@ -378,25 +398,39 @@ void IrcClient::parse_in_buffer(const size_t) } } -void IrcClient::send_message(IrcMessage&& message) +void IrcClient::actual_send(std::pair<IrcMessage, MessageCallback>&& message_pair) { - log_debug("IRC SENDING: (", this->get_hostname(), ") ", message); - std::string res; - if (!message.prefix.empty()) - res += ":" + std::move(message.prefix) + " "; - res += message.command; - for (const std::string& arg: message.arguments) - { - if (arg.find(' ') != std::string::npos || - (!arg.empty() && arg[0] == ':')) - { - res += " :" + arg; - break; - } - res += " " + arg; - } - res += "\r\n"; - this->send_data(std::move(res)); + const IrcMessage& message = message_pair.first; + const MessageCallback& callback = message_pair.second; + log_debug("IRC SENDING: (", this->get_hostname(), ") ", message); + std::string res; + if (!message.prefix.empty()) + res += ":" + message.prefix + " "; + res += message.command; + for (const std::string& arg: message.arguments) + { + if (arg.find(' ') != std::string::npos + || (!arg.empty() && arg[0] == ':')) + { + res += " :" + arg; + break; + } + res += " " + arg; + } + res += "\r\n"; + this->send_data(std::move(res)); + + if (callback) + callback(this, message); + } + +void IrcClient::send_message(IrcMessage message, MessageCallback callback, bool throttle) +{ + auto message_pair = std::make_pair(std::move(message), std::move(callback)); + if (this->tokens_bucket.use_token() || !throttle) + this->actual_send(std::move(message_pair)); + else + message_queue.push_back(std::move(message_pair)); } void IrcClient::send_raw(const std::string& txt) @@ -447,12 +481,12 @@ void IrcClient::send_topic_command(const std::string& chan_name, const std::stri void IrcClient::send_quit_command(const std::string& reason) { - this->send_message(IrcMessage("QUIT", {reason})); + this->send_message(IrcMessage("QUIT", {reason}), {}, false); } void IrcClient::send_join_command(const std::string& chan_name, const std::string& password) { - if (this->welcomed == false) + if (!this->welcomed) { const auto it = std::find_if(begin(this->channels_to_join), end(this->channels_to_join), [&chan_name](const auto& pair) { return std::get<0>(pair) == chan_name; }); @@ -466,10 +500,11 @@ void IrcClient::send_join_command(const std::string& chan_name, const std::strin this->start(); } -bool IrcClient::send_channel_message(const std::string& chan_name, const std::string& body) +bool IrcClient::send_channel_message(const std::string& chan_name, const std::string& body, + MessageCallback callback) { IrcChannel* channel = this->get_channel(chan_name); - if (channel->joined == false) + if (!channel->joined) { log_warning("Cannot send message to channel ", chan_name, ", it is not joined"); return false; @@ -489,7 +524,7 @@ bool IrcClient::send_channel_message(const std::string& chan_name, const std::st ::strlen(":!@ PRIVMSG ") - chan_name.length() - ::strlen(" :\r\n"); const auto lines = cut(body, line_size); for (const auto& line: lines) - this->send_message(IrcMessage("PRIVMSG", {chan_name, line})); + this->send_message(IrcMessage("PRIVMSG", {chan_name, line}), callback); return true; } @@ -1123,8 +1158,6 @@ void IrcClient::on_channel_bad_key(const IrcMessage& message) void IrcClient::on_channel_mode(const IrcMessage& message) { - // For now, just transmit the modes so the user can know what happens - // TODO, actually interprete the mode. Iid iid; iid.set_local(message.arguments[0]); iid.set_server(this->hostname); @@ -1142,7 +1175,7 @@ void IrcClient::on_channel_mode(const IrcMessage& message) } this->bridge.send_message(iid, "", "Mode " + iid.get_local() + " [" + mode_arguments + "] by " + user.nick, - true); + true, this->is_channel_joined(iid.get_local())); const IrcChannel* channel = this->get_channel(iid.get_local()); if (!channel) return; @@ -1215,6 +1248,11 @@ void IrcClient::on_channel_mode(const IrcMessage& message) } } +void IrcClient::set_throttle_limit(long int limit) +{ + this->tokens_bucket.set_limit(limit); +} + void IrcClient::on_user_mode(const IrcMessage& message) { this->bridge.send_xmpp_message(this->hostname, "", @@ -1252,3 +1290,12 @@ bool IrcClient::abort_on_invalid_cert() const return true; } #endif + +long int IrcClient::get_throttle_limit() const +{ +#ifdef USE_DATABASE + return Database::get_irc_server_options(this->bridge.get_bare_jid(), this->hostname).col<Database::ThrottleLimit>(); +#else + return 10; +#endif +} diff --git a/src/irc/irc_client.hpp b/src/irc/irc_client.hpp index fd97fe6..cfb3d21 100644 --- a/src/irc/irc_client.hpp +++ b/src/irc/irc_client.hpp @@ -16,8 +16,14 @@ #include <vector> #include <string> #include <stack> +#include <deque> #include <map> #include <set> +#include <utils/tokens_bucket.hpp> + +class IrcClient; + +using MessageCallback = std::function<void(const IrcClient*, const IrcMessage&)>; class Bridge; @@ -28,7 +34,7 @@ class Bridge; class IrcClient: public TCPClientSocketHandler { public: - explicit IrcClient(std::shared_ptr<Poller>& poller, std::string hostname, + explicit IrcClient(std::shared_ptr<Poller>& poller, std::string hostname, std::string nickname, std::string username, std::string realname, std::string user_hostname, Bridge& bridge); @@ -68,6 +74,10 @@ public: */ IrcChannel* get_channel(const std::string& name); /** + * Return the channel with this name. Nullptr if it is not found + */ + const IrcChannel* find_channel(const std::string& name) const; + /** * Returns true if the channel is joined */ bool is_channel_joined(const std::string& name); @@ -80,8 +90,9 @@ public: * (actually, into our out_buf and signal the poller that we want to wach * for send events to be ready) */ - void send_message(IrcMessage&& message); + void send_message(IrcMessage message, MessageCallback callback={}, bool throttle=true); void send_raw(const std::string& txt); + void actual_send(std::pair<IrcMessage, MessageCallback>&& message_pair); /** * Send the PONG irc command */ @@ -110,7 +121,8 @@ public: * Send a PRIVMSG command for a channel * Return true if the message was actually sent */ - bool send_channel_message(const std::string& chan_name, const std::string& body); + bool send_channel_message(const std::string& chan_name, const std::string& body, + MessageCallback callback); /** * Send a PRIVMSG command for an user */ @@ -289,7 +301,7 @@ public: const std::vector<char>& get_sorted_user_modes() const { return this->sorted_user_modes; } std::set<char> get_chantypes() const { return this->chantypes; } - + void set_throttle_limit(long int limit); /** * Store the history limit that the client asked when joining this room. */ @@ -327,6 +339,10 @@ private: */ Bridge& bridge; /** + * Where messaged are stored when they are throttled. + */ + std::deque<std::pair<IrcMessage, MessageCallback>> message_queue{}; + /** * The list of joined channels, indexed by name */ std::unordered_map<std::string, std::unique_ptr<IrcChannel>> channels; @@ -385,6 +401,8 @@ private: * the WebIRC protocole. */ Resolver dns_resolver; + TokensBucket tokens_bucket; + long int get_throttle_limit() const; }; diff --git a/src/irc/irc_message.hpp b/src/irc/irc_message.hpp index fe954e4..269a12a 100644 --- a/src/irc/irc_message.hpp +++ b/src/irc/irc_message.hpp @@ -14,9 +14,9 @@ public: ~IrcMessage() = default; IrcMessage(const IrcMessage&) = delete; - IrcMessage(IrcMessage&&) = delete; + IrcMessage(IrcMessage&&) = default; IrcMessage& operator=(const IrcMessage&) = delete; - IrcMessage& operator=(IrcMessage&&) = delete; + IrcMessage& operator=(IrcMessage&&) = default; std::string prefix; std::string command; diff --git a/src/main.cpp b/src/main.cpp index 59fda4e..2448197 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -55,45 +55,8 @@ static void sigusr_handler(int, siginfo_t*, void*) reload.store(true); } -int main(int ac, char** av) +static void setup_signals() { - if (ac > 1) - { - const std::string arg = av[1]; - if (arg.size() >= 2 && arg[0] == '-' && arg[1] == '-') - { - if (arg == "--help") - return display_help(); - else - { - std::cerr << "Unknow command line option: " << arg << std::endl; - return 1; - } - } - } - const std::string conf_filename = ac > 1 ? av[1] : xdg_config_path("biboumi.cfg"); - std::cout << "Using configuration file: " << conf_filename << std::endl; - - if (!Config::read_conf(conf_filename)) - return config_help(""); - - const std::string password = Config::get("password", ""); - if (password.empty()) - return config_help("password"); - const std::string hostname = Config::get("hostname", ""); - if (hostname.empty()) - return config_help("hostname"); - - -#ifdef USE_DATABASE - try { - open_database(); - } catch (const std::exception& e) { - log_error(e.what()); - return 1; - } -#endif - // Block the signals we want to manage. They will be unblocked only during // the epoll_pwait or ppoll calls. This avoids some race conditions, // explained in man 2 pselect on linux @@ -103,6 +66,7 @@ int main(int ac, char** av) sigaddset(&mask, SIGTERM); sigaddset(&mask, SIGUSR1); sigaddset(&mask, SIGUSR2); + sigaddset(&mask, SIGHUP); sigprocmask(SIG_BLOCK, &mask, nullptr); // Install the signals used to exit the process cleanly, or reload the @@ -113,7 +77,7 @@ int main(int ac, char** av) sigfillset(&on_sigint.sa_mask); // we want to catch that signal only once. // Sending SIGINT again will "force" an exit - on_sigint.sa_flags = SA_RESETHAND; + on_sigint.sa_flags = 0 & SA_RESETHAND; sigaction(SIGINT, &on_sigint, nullptr); sigaction(SIGTERM, &on_sigint, nullptr); @@ -124,7 +88,11 @@ int main(int ac, char** av) on_sigusr.sa_flags = 0; sigaction(SIGUSR1, &on_sigusr, nullptr); sigaction(SIGUSR2, &on_sigusr, nullptr); + sigaction(SIGHUP, &on_sigusr, nullptr); +} +static int main_loop(std::string hostname, std::string password) +{ auto p = std::make_shared<Poller>(); #ifdef UDNS_FOUND @@ -161,7 +129,7 @@ int main(int ac, char** av) dns_handler.destroy(); #endif if (identd) - identd->shutdown(); + identd->shutdown(); // Cancel the timer for a potential reconnection TimedEventsManager::instance().cancel("XMPP reconnection"); } @@ -204,7 +172,7 @@ int main(int ac, char** av) dns_handler.destroy(); #endif if (identd) - identd->shutdown(); + identd->shutdown(); } } // If the only existing connection is the one to the XMPP component: @@ -223,3 +191,51 @@ int main(int ac, char** av) log_info("All connections cleanly closed, have a nice day."); return 0; } + +int main(int ac, char** av) +{ + if (ac > 1) + { + const std::string arg = av[1]; + if (arg.size() >= 2 && arg[0] == '-' && arg[1] == '-') + { + if (arg == "--help") + return display_help(); + else + { + std::cerr << "Unknow command line option: " << arg + << std::endl; + return 1; + } + } + } + const std::string conf_filename = + ac > 1 ? av[1]: xdg_config_path("biboumi.cfg"); + std::cout << "Using configuration file: " << conf_filename << std::endl; + + if (!Config::read_conf(conf_filename)) + return config_help(""); + + const std::string password = Config::get("password", ""); + if (password.empty()) + return config_help("password"); + const std::string hostname = Config::get("hostname", ""); + if (hostname.empty()) + return config_help("hostname"); + +#ifdef USE_DATABASE + try + { + open_database(); + } + catch (const std::exception& e) + { + log_error(e.what()); + return 1; + } +#endif + + setup_signals(); + + return main_loop(std::move(hostname), std::move(password)); +} diff --git a/src/network/credentials_manager.cpp b/src/network/credentials_manager.cpp index b25f442..89c694c 100644 --- a/src/network/credentials_manager.cpp +++ b/src/network/credentials_manager.cpp @@ -21,9 +21,8 @@ static const std::vector<std::string> default_cert_files = { Botan::Certificate_Store_In_Memory BasicCredentialsManager::certificate_store; bool BasicCredentialsManager::certs_loaded = false; -BasicCredentialsManager::BasicCredentialsManager(const TCPSocketHandler* const socket_handler): +BasicCredentialsManager::BasicCredentialsManager(): Botan::Credentials_Manager(), - socket_handler(socket_handler), trusted_fingerprint{} { BasicCredentialsManager::load_certs(); diff --git a/src/network/credentials_manager.hpp b/src/network/credentials_manager.hpp index 3a37bdc..210a628 100644 --- a/src/network/credentials_manager.hpp +++ b/src/network/credentials_manager.hpp @@ -25,7 +25,7 @@ void check_tls_certificate(const std::vector<Botan::X509_Certificate>& certs, class BasicCredentialsManager: public Botan::Credentials_Manager { public: - BasicCredentialsManager(const TCPSocketHandler* const socket_handler); + BasicCredentialsManager(); BasicCredentialsManager(BasicCredentialsManager&&) = delete; BasicCredentialsManager(const BasicCredentialsManager&) = delete; @@ -38,7 +38,6 @@ public: const std::string& get_trusted_fingerprint() const; private: - const TCPSocketHandler* const socket_handler; static bool try_to_open_one_ca_bundle(const std::vector<std::string>& paths); static void load_certs(); diff --git a/src/network/resolver.cpp b/src/network/resolver.cpp index ae5cecd..d9242e2 100644 --- a/src/network/resolver.cpp +++ b/src/network/resolver.cpp @@ -214,6 +214,12 @@ void Resolver::on_hostname6_resolved(dns_rr_a6 *result) this->call_getaddrinfo(buf, this->port.data(), AI_NUMERICHOST); } } + else + { + const auto error = dns_error_messages.find(status); + if (error != end(dns_error_messages)) + this->error_msg = error->second; + } } void Resolver::after_resolved() diff --git a/src/network/tcp_client_socket_handler.cpp b/src/network/tcp_client_socket_handler.cpp index dcf38f9..7d1029f 100644 --- a/src/network/tcp_client_socket_handler.cpp +++ b/src/network/tcp_client_socket_handler.cpp @@ -46,15 +46,14 @@ void TCPClientSocketHandler::init_socket(const struct addrinfo* rp) else { utils::ScopeGuard sg([result](){ freeaddrinfo(result); }); - struct addrinfo* rp; - for (rp = result; rp; rp = rp->ai_next) + for (; result; result = result->ai_next) { if ((::bind(this->socket, - reinterpret_cast<const struct sockaddr*>(rp->ai_addr), - rp->ai_addrlen)) == 0) + reinterpret_cast<const struct sockaddr*>(result->ai_addr), + result->ai_addrlen)) == 0) break; } - if (!rp) + if (!result) log_error("Failed to bind socket to ", this->bind_addr, ": ", strerror(errno)); else diff --git a/src/network/tcp_socket_handler.cpp b/src/network/tcp_socket_handler.cpp index 642cf03..e05caad 100644 --- a/src/network/tcp_socket_handler.cpp +++ b/src/network/tcp_socket_handler.cpp @@ -50,7 +50,7 @@ TCPSocketHandler::TCPSocketHandler(std::shared_ptr<Poller>& poller): SocketHandler(poller, -1), use_tls(false) #ifdef BOTAN_FOUND - ,credential_manager(this) + ,credential_manager() #endif {} @@ -84,10 +84,11 @@ void TCPSocketHandler::plain_recv() if (recv_buf == nullptr) recv_buf = buf; - const ssize_t size = this->do_recv(recv_buf, buf_size); + const ssize_t ssize = this->do_recv(recv_buf, buf_size); - if (size > 0) + if (ssize > 0) { + auto size = static_cast<std::size_t>(ssize); if (buf == recv_buf) { // data needs to be placed in the in_buf string, because no buffer @@ -149,21 +150,22 @@ void TCPSocketHandler::on_send() } else { + auto size = static_cast<std::size_t>(res); // remove all the strings that were successfully sent. auto it = this->out_buf.begin(); while (it != this->out_buf.end()) { - if (static_cast<size_t>(res) >= it->size()) + if (size >= it->size()) { - res -= it->size(); + size -= it->size(); ++it; } else { // If one string has partially been sent, we use substr to // crop it - if (res > 0) - *it = it->substr(res, std::string::npos); + if (size > 0) + *it = it->substr(size, std::string::npos); break; } } @@ -332,6 +334,11 @@ void TCPSocketHandler::tls_verify_cert_chain(const std::vector<Botan::X509_Certi Botan::Usage_Type usage, const std::string& hostname, const Botan::TLS::Policy& policy) { + if (!this->policy.verify_certificate) + { + log_debug("Not verifying certificate due to domain policy "); + return; + } log_debug("Checking remote certificate for hostname ", hostname); try { diff --git a/src/network/tls_policy.cpp b/src/network/tls_policy.cpp index b88eb88..f32557e 100644 --- a/src/network/tls_policy.cpp +++ b/src/network/tls_policy.cpp @@ -37,6 +37,8 @@ void BiboumiTLSPolicy::load(std::istream& is) // Workaround for options that are not overridden in Botan::TLS::Text_Policy if (pair.first == "require_cert_revocation_info") this->req_cert_revocation_info = !(pair.second == "0" || utils::tolower(pair.second) == "false"); + else if (pair.first == "verify_certificate") + this->verify_certificate = !(pair.second == "0" || utils::tolower(pair.second) == "false"); else this->set(pair.first, pair.second); } diff --git a/src/network/tls_policy.hpp b/src/network/tls_policy.hpp index 29fd2b3..e915646 100644 --- a/src/network/tls_policy.hpp +++ b/src/network/tls_policy.hpp @@ -21,6 +21,7 @@ public: BiboumiTLSPolicy &operator=(BiboumiTLSPolicy &&) = delete; bool require_cert_revocation_info() const override; + bool verify_certificate{true}; protected: bool req_cert_revocation_info{true}; }; diff --git a/src/utils/dirname.cpp b/src/utils/dirname.cpp index 71c9c38..a304117 100644 --- a/src/utils/dirname.cpp +++ b/src/utils/dirname.cpp @@ -2,7 +2,7 @@ namespace utils { - std::string dirname(const std::string filename) + std::string dirname(const std::string& filename) { if (filename.empty()) return "./"; diff --git a/src/utils/dirname.hpp b/src/utils/dirname.hpp index c1df81b..c13393d 100644 --- a/src/utils/dirname.hpp +++ b/src/utils/dirname.hpp @@ -1,6 +1,8 @@ +#pragma once + #include <string> namespace utils { -std::string dirname(const std::string filename); +std::string dirname(const std::string& filename); } diff --git a/src/utils/encoding.cpp b/src/utils/encoding.cpp index cff0039..8532292 100644 --- a/src/utils/encoding.cpp +++ b/src/utils/encoding.cpp @@ -48,16 +48,16 @@ namespace utils if (codepoint_size == 4) { if (!str[1] || !str[2] || !str[3] - || ((str[1] & 0b11000000) != 0b10000000) - || ((str[2] & 0b11000000) != 0b10000000) - || ((str[3] & 0b11000000) != 0b10000000)) + || ((str[1] & 0b11000000u) != 0b10000000u) + || ((str[2] & 0b11000000u) != 0b10000000u) + || ((str[3] & 0b11000000u) != 0b10000000u)) return false; } else if (codepoint_size == 3) { if (!str[1] || !str[2] - || ((str[1] & 0b11000000) != 0b10000000) - || ((str[2] & 0b11000000) != 0b10000000)) + || ((str[1] & 0b11000000u) != 0b10000000u) + || ((str[2] & 0b11000000u) != 0b10000000u)) return false; } else if (codepoint_size == 2) @@ -81,7 +81,7 @@ namespace utils // pointer where we write valid chars char* r = res.data(); - const char* str = original.c_str(); + const unsigned char* str = reinterpret_cast<const unsigned char*>(original.c_str()); std::bitset<20> codepoint; while (*str) @@ -89,10 +89,10 @@ namespace utils // 4 bytes: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx if ((str[0] & 0b11111000) == 0b11110000) { - codepoint = ((str[0] & 0b00000111) << 18); - codepoint |= ((str[1] & 0b00111111) << 12); - codepoint |= ((str[2] & 0b00111111) << 6 ); - codepoint |= ((str[3] & 0b00111111) << 0 ); + codepoint = ((str[0] & 0b00000111u) << 18u); + codepoint |= ((str[1] & 0b00111111u) << 12u); + codepoint |= ((str[2] & 0b00111111u) << 6u ); + codepoint |= ((str[3] & 0b00111111u) << 0u ); if (codepoint.to_ulong() <= 0x10FFFF) { ::memcpy(r, str, 4); @@ -103,9 +103,9 @@ namespace utils // 3 bytes: 1110xxx 10xxxxxx 10xxxxxx else if ((str[0] & 0b11110000) == 0b11100000) { - codepoint = ((str[0] & 0b00001111) << 12); - codepoint |= ((str[1] & 0b00111111) << 6); - codepoint |= ((str[2] & 0b00111111) << 0 ); + codepoint = ((str[0] & 0b00001111u) << 12u); + codepoint |= ((str[1] & 0b00111111u) << 6u); + codepoint |= ((str[2] & 0b00111111u) << 0u ); if (codepoint.to_ulong() <= 0xD7FF || (codepoint.to_ulong() >= 0xE000 && codepoint.to_ulong() <= 0xFFFD)) { diff --git a/src/utils/get_first_non_empty.cpp b/src/utils/get_first_non_empty.cpp index 5b3bedb..17585b1 100644 --- a/src/utils/get_first_non_empty.cpp +++ b/src/utils/get_first_non_empty.cpp @@ -1,11 +1,8 @@ #include <utils/get_first_non_empty.hpp> +template <> bool is_empty(const std::string& val) { return val.empty(); } -bool is_empty(const int& val) -{ - return val == 0; -} diff --git a/src/utils/get_first_non_empty.hpp b/src/utils/get_first_non_empty.hpp index a38f5fb..1877ee8 100644 --- a/src/utils/get_first_non_empty.hpp +++ b/src/utils/get_first_non_empty.hpp @@ -2,8 +2,13 @@ #include <string> +template <typename T> +bool is_empty(const T& val) +{ + return val == 0; +} +template <> bool is_empty(const std::string& val); -bool is_empty(const int& val); template <typename T> T get_first_non_empty(T&& last) diff --git a/src/utils/optional_bool.hpp b/src/utils/optional_bool.hpp index 867aca2..3d00d23 100644 --- a/src/utils/optional_bool.hpp +++ b/src/utils/optional_bool.hpp @@ -6,7 +6,7 @@ struct OptionalBool { OptionalBool() = default; - OptionalBool(bool value): + explicit OptionalBool(bool value): is_set(true), value(value) {} void set_value(bool value) diff --git a/src/utils/string.cpp b/src/utils/string.cpp index 635e71a..366ec1f 100644 --- a/src/utils/string.cpp +++ b/src/utils/string.cpp @@ -15,11 +15,11 @@ std::vector<std::string> cut(const std::string& val, const std::size_t size) // Get the number of chars, <= size, that contain only whole // UTF-8 codepoints. std::size_t s = 0; - auto codepoint_size = utils::get_next_codepoint_size(val[pos + s]); + auto codepoint_size = utils::get_next_codepoint_size(static_cast<unsigned char>(val[pos + s])); while (s + codepoint_size <= size && pos + s < val.size()) { s += codepoint_size; - codepoint_size = utils::get_next_codepoint_size(val[pos + s]); + codepoint_size = utils::get_next_codepoint_size(static_cast<unsigned char>(val[pos + s])); } res.emplace_back(val.substr(pos, s)); pos += s; diff --git a/src/utils/time.cpp b/src/utils/time.cpp index 71306fd..d848e70 100644 --- a/src/utils/time.cpp +++ b/src/utils/time.cpp @@ -1,9 +1,8 @@ #include <utils/time.hpp> -#include <ctime> +#include <time.h> #include <sstream> #include <iomanip> -#include <locale> #include "biboumi.h" @@ -12,9 +11,10 @@ namespace utils std::string to_string(const std::chrono::system_clock::time_point::rep& time) { constexpr std::size_t stamp_size = 21; - const std::time_t timestamp = static_cast<std::time_t>(time); + const auto timestamp = static_cast<std::time_t>(time); char date_buf[stamp_size]; - if (std::strftime(date_buf, stamp_size, "%FT%TZ", std::gmtime(×tamp)) != stamp_size - 1) + struct tm tm; + if (std::strftime(date_buf, stamp_size, "%FT%TZ", gmtime_r(×tamp, &tm)) != stamp_size - 1) return ""; return {std::begin(date_buf), std::end(date_buf) - 1}; } diff --git a/src/utils/tokens_bucket.hpp b/src/utils/tokens_bucket.hpp new file mode 100644 index 0000000..263359a --- /dev/null +++ b/src/utils/tokens_bucket.hpp @@ -0,0 +1,60 @@ +/** + * Implementation of the token bucket algorithm. + * + * It uses a repetitive TimedEvent, started at construction, to fill the + * bucket. + * + * Every n seconds, it executes the given callback. If the callback + * returns true, we add a token (if the limit is not yet reached). + * + */ + +#pragma once + +#include <utils/timed_events.hpp> +#include <logger/logger.hpp> + +class TokensBucket +{ +public: + TokensBucket(long int max_size, std::chrono::milliseconds fill_duration, std::function<bool()> callback, std::string name): + limit(max_size), + tokens(static_cast<std::size_t>(limit)), + callback(std::move(callback)) + { + log_debug("creating TokensBucket with max size: ", max_size); + TimedEvent event(std::move(fill_duration), [this]() { this->add_token(); }, std::move(name)); + TimedEventsManager::instance().add_event(std::move(event)); + } + + bool use_token() + { + if (this->limit < 0) + return true; + if (this->tokens > 0) + { + this->tokens--; + return true; + } + else + return false; + } + + void set_limit(long int limit) + { + this->limit = limit; + } + +private: + long int limit; + std::size_t tokens; + std::function<bool()> callback; + + void add_token() + { + if (this->limit < 0) + return; + if (this->callback() && this->tokens != static_cast<decltype(this->tokens)>(this->limit)) + this->tokens++; + } +}; diff --git a/src/xmpp/adhoc_commands_handler.cpp b/src/xmpp/adhoc_commands_handler.cpp index bc4c108..ff4c1e5 100644 --- a/src/xmpp/adhoc_commands_handler.cpp +++ b/src/xmpp/adhoc_commands_handler.cpp @@ -80,7 +80,10 @@ XmlNode AdhocCommandsHandler::handle_request(const std::string& executor_jid, co { command_node["status"] = "executing"; XmlSubNode actions(command_node, "actions"); - XmlSubNode next(actions, "next"); + if (session.remaining_steps() == 1) + XmlSubNode next(actions, "complete"); + else + XmlSubNode next(actions, "next"); } } else if (session_it != this->sessions.end() && action == "cancel") diff --git a/src/xmpp/biboumi_adhoc_commands.cpp b/src/xmpp/biboumi_adhoc_commands.cpp index 3bd2e5a..113943c 100644 --- a/src/xmpp/biboumi_adhoc_commands.cpp +++ b/src/xmpp/biboumi_adhoc_commands.cpp @@ -15,10 +15,17 @@ #ifdef USE_DATABASE #include <database/database.hpp> #include <database/save.hpp> + +static void set_desc(XmlSubNode& field, const char* text) +{ + XmlSubNode desc(field, "desc"); + desc.set_inner(text); +} + #endif #ifndef HAS_PUT_TIME -#include <ctime> +# include <time.h> #endif using namespace std::string_literals; @@ -116,6 +123,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"); x["type"] = "form"; XmlSubNode title(x, "title"); @@ -128,7 +136,7 @@ void ConfigureGlobalStep1(XmppComponent&, AdhocSession& session, XmlNode& comman max_histo_length["var"] = "max_history_length"; max_histo_length["type"] = "text-single"; max_histo_length["label"] = "Max history length"; - max_histo_length["desc"] = "The maximum number of lines in the history that the server sends when joining a channel"; + set_desc(max_histo_length, "The maximum number of lines in the history that the server sends when joining a channel"); { XmlSubNode value(max_histo_length, "value"); value.set_inner(std::to_string(options.col<Database::MaxHistoryLength>())); @@ -140,7 +148,7 @@ void ConfigureGlobalStep1(XmppComponent&, AdhocSession& session, XmlNode& comman record_history["var"] = "record_history"; record_history["type"] = "boolean"; record_history["label"] = "Record history"; - record_history["desc"] = "Whether to save the messages into the database, or not"; + set_desc(record_history, "Whether to save the messages into the database, or not"); { XmlSubNode value(record_history, "value"); value.set_name("value"); @@ -156,7 +164,7 @@ void ConfigureGlobalStep1(XmppComponent&, AdhocSession& session, XmlNode& comman persistent["var"] = "persistent"; persistent["type"] = "boolean"; persistent["label"] = "Make all channels persistent"; - persistent["desc"] = "If true, all channels will be persistent"; + set_desc(persistent, "If true, all channels will be persistent"); { XmlSubNode value(persistent, "value"); value.set_name("value"); @@ -184,7 +192,13 @@ void ConfigureGlobalStep2(XmppComponent& xmpp_component, AdhocSession& session, if (field->get_tag("var") == "max_history_length" && value && !value->get_inner().empty()) - options.col<Database::MaxHistoryLength>() = atoi(value->get_inner().data()); + { + try { + options.col<Database::MaxHistoryLength>() = std::stol(value->get_inner().data()); + } catch (const std::logic_error&) { + options.col<Database::MaxHistoryLength>() = 20; + } + } else if (field->get_tag("var") == "record_history" && value && !value->get_inner().empty()) { @@ -223,6 +237,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com server_domain); auto commands = Database::get_after_connection_commands(options); + command_node.delete_all_children(); XmlSubNode x(command_node, "jabber:x:data:x"); x["type"] = "form"; XmlSubNode title(x, "title"); @@ -236,7 +251,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com field["var"] = "address"; field["type"] = "text-single"; field["label"] = "Address"; - field["desc"] = "The address (hostname or IP) to connect to."; + set_desc(field, "The address (hostname or IP) to connect to."); XmlSubNode value(field, "value"); if (options.col<Database::Address>().empty()) value.set_inner(server_domain); @@ -249,7 +264,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com ports["var"] = "ports"; ports["type"] = "text-multi"; ports["label"] = "Ports"; - ports["desc"] = "List of ports to try, without TLS. Defaults: 6667."; + set_desc(ports, "List of ports to try, without TLS. Defaults: 6667."); for (const auto& val: utils::split(options.col<Database::Ports>(), ';', false)) { XmlSubNode ports_value(ports, "value"); @@ -263,7 +278,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com tls_ports["var"] = "tls_ports"; tls_ports["type"] = "text-multi"; tls_ports["label"] = "TLS ports"; - tls_ports["desc"] = "List of ports to try, with TLS. Defaults: 6697, 6670."; + set_desc(tls_ports, "List of ports to try, with TLS. Defaults: 6697, 6670."); for (const auto& val: utils::split(options.col<Database::TlsPorts>(), ';', false)) { XmlSubNode tls_ports_value(tls_ports, "value"); @@ -276,7 +291,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com verify_cert["var"] = "verify_cert"; verify_cert["type"] = "boolean"; verify_cert["label"] = "Verify certificate"; - verify_cert["desc"] = "Whether or not to abort the connection if the server’s TLS certificate is invalid"; + set_desc(verify_cert, "Whether or not to abort the connection if the server’s TLS certificate is invalid"); XmlSubNode verify_cert_value(verify_cert, "value"); if (options.col<Database::VerifyCert>()) verify_cert_value.set_inner("true"); @@ -302,7 +317,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com field["var"] = "nick"; field["type"] = "text-single"; field["label"] = "Nickname"; - field["desc"] = "If set, will override the nickname provided in the initial presence sent to join the first server channel"; + set_desc(field, "If set, will override the nickname provided in the initial presence sent to join the first server channel"); if (!options.col<Database::Nick>().empty()) { XmlSubNode value(field, "value"); @@ -315,7 +330,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com pass["var"] = "pass"; pass["type"] = "text-private"; pass["label"] = "Server password"; - pass["desc"] = "Will be used in a PASS command when connecting"; + set_desc(pass, "Will be used in a PASS command when connecting"); if (!options.col<Database::Pass>().empty()) { XmlSubNode pass_value(pass, "value"); @@ -327,7 +342,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com XmlSubNode after_cnt_cmd(x, "field"); after_cnt_cmd["var"] = "after_connect_commands"; after_cnt_cmd["type"] = "text-multi"; - after_cnt_cmd["desc"] = "Custom IRC commands sent after the connection is established with the server."; + set_desc(after_cnt_cmd, "Custom IRC commands sent after the connection is established with the server."); after_cnt_cmd["label"] = "After-connection IRC commands"; for (const auto& command: commands) { @@ -364,10 +379,28 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com } { + XmlSubNode throttle_limit(x, "field"); + throttle_limit["var"] = "throttle_limit"; + throttle_limit["type"] = "text-single"; + throttle_limit["label"] = "Throttle limit"; + XmlSubNode value(throttle_limit, "value"); + value.set_inner(std::to_string(options.col<Database::ThrottleLimit>())); + } + + { + XmlSubNode max_history_length(x, "field"); + max_history_length["var"] = "max_history_length"; + max_history_length["type"] = "text-single"; + max_history_length["label"] = "Throttle limit"; + XmlSubNode value(max_history_length, "value"); + value.set_inner(std::to_string(options.col<Database::MaxHistoryLength>())); + } + + { XmlSubNode encoding_out(x, "field"); encoding_out["var"] = "encoding_out"; encoding_out["type"] = "text-single"; - encoding_out["desc"] = "The encoding used when sending messages to the IRC server."; + set_desc(encoding_out, "The encoding used when sending messages to the IRC server."); encoding_out["label"] = "Out encoding"; if (!options.col<Database::EncodingOut>().empty()) { @@ -380,7 +413,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com XmlSubNode encoding_in(x, "field"); encoding_in["var"] = "encoding_in"; encoding_in["type"] = "text-single"; - encoding_in["desc"] = "The encoding used to decode message received from the IRC server."; + set_desc(encoding_in, "The encoding used to decode message received from the IRC server."); encoding_in["label"] = "In encoding"; if (!options.col<Database::EncodingIn>().empty()) { @@ -390,8 +423,10 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com } } -void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node) +void ConfigureIrcServerStep2(XmppComponent& xmpp_component, AdhocSession& session, XmlNode& command_node) { + auto& biboumi_component = dynamic_cast<BiboumiComponent&>(xmpp_component); + const XmlNode* x = command_node.get_child("x", "jabber:x:data"); if (x) { @@ -472,6 +507,31 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com else if (field->get_tag("var") == "realname" && value) options.col<Database::Realname>() = value->get_inner(); + else if (field->get_tag("var") == "throttle_limit" && value) + { + try { + options.col<Database::ThrottleLimit>() = std::stol(value->get_inner()); + } catch (const std::logic_error&) { + options.col<Database::ThrottleLimit>() = 10; + } + Bridge* bridge = biboumi_component.find_user_bridge(session.get_owner_jid()); + if (bridge) + { + IrcClient* client = bridge->find_irc_client(server_domain); + if (client) + client->set_throttle_limit(options.col<Database::ThrottleLimit>()); + } + } + + else if (field->get_tag("var") == "max_history_length" && value) + { + try { + options.col<Database::MaxHistoryLength>() = std::stol(value->get_inner()); + } catch (const std::logic_error&) { + options.col<Database::MaxHistoryLength>() = 20; + } + } + else if (field->get_tag("var") == "encoding_out" && value) options.col<Database::EncodingOut>() = value->get_inner(); @@ -509,6 +569,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"); x["type"] = "form"; XmlSubNode title(x, "title"); @@ -521,7 +582,7 @@ void insert_irc_channel_configuration_form(XmlNode& node, const Jid& requester, record_history["var"] = "record_history"; record_history["type"] = "list-single"; record_history["label"] = "Record history for this channel"; - record_history["desc"] = "If unset, the value is the one configured globally"; + set_desc(record_history, "If unset, the value is the one configured globally"); { // Value selected by default XmlSubNode value(record_history, "value"); @@ -541,7 +602,7 @@ void insert_irc_channel_configuration_form(XmlNode& node, const Jid& requester, XmlSubNode encoding_out(x, "field"); encoding_out["var"] = "encoding_out"; encoding_out["type"] = "text-single"; - encoding_out["desc"] = "The encoding used when sending messages to the IRC server. Defaults to the server's “out encoding” if unset for the channel"; + set_desc(encoding_out, "The encoding used when sending messages to the IRC server. Defaults to the server's “out encoding” if unset for the channel"); encoding_out["label"] = "Out encoding"; if (!options.col<Database::EncodingOut>().empty()) { @@ -554,7 +615,7 @@ void insert_irc_channel_configuration_form(XmlNode& node, const Jid& requester, XmlSubNode encoding_in(x, "field"); encoding_in["var"] = "encoding_in"; encoding_in["type"] = "text-single"; - encoding_in["desc"] = "The encoding used to decode message received from the IRC server. Defaults to the server's “in encoding” if unset for the channel"; + set_desc(encoding_in, "The encoding used to decode message received from the IRC server. Defaults to the server's “in encoding” if unset for the channel"); encoding_in["label"] = "In encoding"; if (!options.col<Database::EncodingIn>().empty()) { @@ -567,7 +628,7 @@ void insert_irc_channel_configuration_form(XmlNode& node, const Jid& requester, XmlSubNode persistent(x, "field"); persistent["var"] = "persistent"; persistent["type"] = "boolean"; - persistent["desc"] = "If set to true, when all XMPP clients have left this channel, biboumi will stay idle in it, without sending a PART command."; + set_desc(persistent, "If set to true, when all XMPP clients have left this channel, biboumi will stay idle in it, without sending a PART command."); persistent["label"] = "Persistent"; { XmlSubNode value(persistent, "value"); @@ -847,12 +908,13 @@ void GetIrcConnectionInfoStep1(XmppComponent& component, AdhocSession& session, if (irc->is_using_tls()) ss << " (using TLS)"; const std::time_t now_c = std::chrono::system_clock::to_time_t(irc->connection_date); + struct tm tm; #ifdef HAS_PUT_TIME - ss << " since " << std::put_time(std::localtime(&now_c), "%F %T"); + ss << " since " << std::put_time(localtime_r(&now_c, &tm), "%F %T"); #else constexpr std::size_t timestamp_size{10 + 1 + 8 + 1}; char buf[timestamp_size] = {}; - const auto res = std::strftime(buf, timestamp_size, "%F %T", std::localtime(&now_c)); + const auto res = std::strftime(buf, timestamp_size, "%F %T", localtime(&now_c, &tm)); if (res > 0) ss << " since " << buf; #endif diff --git a/src/xmpp/biboumi_component.cpp b/src/xmpp/biboumi_component.cpp index be34873..6fe6972 100644 --- a/src/xmpp/biboumi_component.cpp +++ b/src/xmpp/biboumi_component.cpp @@ -102,8 +102,8 @@ void BiboumiComponent::shutdown() void BiboumiComponent::clean() { - auto it = this->bridges.begin(); - while (it != this->bridges.end()) + auto it = std::begin(this->bridges); + while (it != std::end(this->bridges)) { it->second->clean(); if (it->second->active_clients() == 0) @@ -185,8 +185,13 @@ void BiboumiComponent::handle_presence(const Stanza& stanza) } bridge->join_irc_channel(iid, to.resource, password ? password->get_inner(): "", from.resource, history_limit, x != nullptr); - if (!own_nick.empty() && own_nick != to.resource) - bridge->send_irc_nick_change(iid, to.resource, from.resource); + const IrcClient* irc = bridge->find_irc_client(iid.get_server()); + if (irc) + { + const auto chan = irc->find_channel(iid.get_local()); + if (chan->joined) + bridge->send_irc_nick_change(iid, to.resource, from.resource); + } } else if (type == "unavailable") { @@ -273,9 +278,10 @@ void BiboumiComponent::handle_message(const Stanza& stanza) std::string error_type("cancel"); std::string error_name("internal-server-error"); - utils::ScopeGuard stanza_error([this, &from_str, &to_str, &id, &error_type, &error_name](){ + std::string error_text{}; + utils::ScopeGuard stanza_error([this, &from_str, &to_str, &id, &error_type, &error_name, &error_text](){ this->send_stanza_error("message", from_str, to_str, id, - error_type, error_name, ""); + error_type, error_name, error_text); }); const XmlNode* body = stanza.get_child("body", COMPONENT_NS); @@ -284,7 +290,15 @@ void BiboumiComponent::handle_message(const Stanza& stanza) { if (body && !body->get_inner().empty()) { - bridge->send_channel_message(iid, body->get_inner(), id); + if (bridge->is_resource_in_chan(iid.to_tuple(), from.resource)) + bridge->send_channel_message(iid, body->get_inner(), id); + else + { + error_type = "modify"; + error_name = "not-acceptable"; + error_text = "You are not a participant in this room."; + return; + } } const XmlNode* subject = stanza.get_child("subject", COMPONENT_NS); if (subject) @@ -350,7 +364,6 @@ void BiboumiComponent::handle_message(const Stanza& stanza) this->send_invitation_from_fulljid(std::to_string(iid), invite_to, from_str); } } - } } catch (const IRCNotConnected& ex) { @@ -514,7 +527,11 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) { if (node.empty()) { - this->send_irc_channel_disco_info(id, from, to_str); + const IrcClient* irc_client = bridge->find_irc_client(iid.get_server()); + const IrcChannel* irc_channel{}; + if (irc_client) + irc_channel = irc_client->find_channel(iid.get_local()); + this->send_irc_channel_disco_info(id, from, to_str, irc_channel); stanza_error.disable(); } else if (node == MUC_TRAFFIC_NS) @@ -592,7 +609,6 @@ void BiboumiComponent::handle_iq(const Stanza& stanza) const XmlNode* max = set_node->get_child("max", RSM_NS); if (max) rs_info.max = std::atoi(max->get_inner().data()); - } if (rs_info.max == -1) rs_info.max = 100; @@ -751,7 +767,7 @@ bool BiboumiComponent::handle_mam_request(const Stanza& stanza) if (limit < 0 || limit > 100) limit = 100; auto result = Database::get_muc_logs(from.bare(), iid.get_local(), iid.get_server(), - limit, + static_cast<std::size_t>(limit), start, end, reference_record_id, paging_order); bool complete = std::get<bool>(result); @@ -964,7 +980,8 @@ void BiboumiComponent::send_irc_channel_muc_traffic_info(const std::string& id, this->send_stanza(iq); } -void BiboumiComponent::send_irc_channel_disco_info(const std::string& id, const std::string& jid_to, const std::string& jid_from) +void BiboumiComponent::send_irc_channel_disco_info(const std::string& id, const std::string& jid_to, + const std::string& jid_from, const IrcChannel* irc_channel) { Jid from(jid_from); Iid iid(from.local, {}); @@ -980,11 +997,31 @@ void BiboumiComponent::send_irc_channel_disco_info(const std::string& id, const identity["category"] = "conference"; identity["type"] = "irc"; identity["name"] = ""s + iid.get_local() + " on " + iid.get_server(); - for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS, STABLE_MUC_ID_NS}) + for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS, STABLE_MUC_ID_NS, SELF_PING_FLAG, "muc_nonanonymous"}) { XmlSubNode feature(query, "feature"); feature["var"] = ns; } + + XmlSubNode x(query, "x"); + x["xmlns"] = DATAFORM_NS; + x["type"] = "result"; + { + XmlSubNode field(x, "field"); + field["var"] = "FORM_TYPE"; + field["type"] = "hidden"; + XmlSubNode value(field, "value"); + value.set_inner("http://jabber.org/protocol/muc#roominfo"); + } + + if (irc_channel && irc_channel->joined) + { + XmlSubNode field(x, "field"); + field["var"] = "muc#roominfo_occupants"; + field["label"] = "Number of occupants"; + XmlSubNode value(field, "value"); + value.set_inner(std::to_string(irc_channel->get_users().size())); + } } this->send_stanza(iq); } diff --git a/src/xmpp/biboumi_component.hpp b/src/xmpp/biboumi_component.hpp index caf990e..f59ed9b 100644 --- a/src/xmpp/biboumi_component.hpp +++ b/src/xmpp/biboumi_component.hpp @@ -73,7 +73,8 @@ public: * http://xmpp.org/extensions/xep-0045.html#impl-service-traffic */ void send_irc_channel_muc_traffic_info(const std::string& id, const std::string& jid_to, const std::string& jid_from); - void send_irc_channel_disco_info(const std::string& id, const std::string& jid_to, const std::string& jid_from); + void send_irc_channel_disco_info(const std::string& id, const std::string& jid_to, const std::string& jid_from, + const IrcChannel* irc_channel); /** * Send a ping request */ diff --git a/src/xmpp/jid.cpp b/src/xmpp/jid.cpp index 19d1b55..3c54fd4 100644 --- a/src/xmpp/jid.cpp +++ b/src/xmpp/jid.cpp @@ -106,7 +106,7 @@ std::string jidprep(const std::string& original) --domain_end; if (domain_end != domain && special_chars.count(domain[0])) { - std::memmove(domain, domain + 1, domain_end - domain + 1); + std::memmove(domain, domain + 1, static_cast<std::size_t>(domain_end - domain) + 1); --domain_end; } // And if the final result is an empty string, return a dummy hostname diff --git a/src/xmpp/xmpp_component.cpp b/src/xmpp/xmpp_component.cpp index b3d925e..f82f9ce 100644 --- a/src/xmpp/xmpp_component.cpp +++ b/src/xmpp/xmpp_component.cpp @@ -298,8 +298,8 @@ void XmppComponent::send_message(const std::string& from, Xmpp::body&& body, con { XmlSubNode private_node(message, "private"); private_node["xmlns"] = "urn:xmpp:carbons:2"; - XmlSubNode nocopy(message, "no-copy"); - nocopy["xmlns"] = "urn:xmpp:hints"; + XmlSubNode nocopy_node(message, "no-copy"); + nocopy_node["xmlns"] = "urn:xmpp:hints"; } if (muc_private) { @@ -340,8 +340,12 @@ void XmppComponent::send_user_join(const std::string& from, if (self) { - XmlSubNode status(x, "status"); - status["code"] = "110"; + XmlSubNode status_self(x, "status"); + status_self["code"] = "110"; + XmlSubNode status_nick_modified(x, "status"); + status_nick_modified["code"] = "210"; + XmlSubNode status_nonanonymous(x, "status"); + status_nonanonymous["code"] = "100"; } } this->send_stanza(presence); @@ -588,8 +592,8 @@ void XmppComponent::send_version(const std::string& id, const std::string& jid_t name.set_inner("biboumi"); } { - XmlSubNode version(query, "version"); - version.set_inner(SOFTWARE_VERSION); + XmlSubNode version_node(query, "version"); + version_node.set_inner(SOFTWARE_VERSION); } { XmlSubNode os(query, "os"); diff --git a/src/xmpp/xmpp_component.hpp b/src/xmpp/xmpp_component.hpp index e18da40..156e286 100644 --- a/src/xmpp/xmpp_component.hpp +++ b/src/xmpp/xmpp_component.hpp @@ -38,6 +38,7 @@ #define MUC_TRAFFIC_NS "http://jabber.org/protocol/muc#traffic" #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" /** * An XMPP component, communicating with an XMPP server using the protocole diff --git a/src/xmpp/xmpp_parser.cpp b/src/xmpp/xmpp_parser.cpp index 0488be9..781fe4c 100644 --- a/src/xmpp/xmpp_parser.cpp +++ b/src/xmpp/xmpp_parser.cpp @@ -20,7 +20,7 @@ static void end_element_handler(void* user_data, const XML_Char* name) static void character_data_handler(void *user_data, const XML_Char *s, int len) { - static_cast<XmppParser*>(user_data)->char_data(s, len); + static_cast<XmppParser*>(user_data)->char_data(s, static_cast<std::size_t>(len)); } /** |