diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/bridge/bridge.cpp | 907 | ||||
-rw-r--r-- | src/bridge/bridge.hpp | 293 | ||||
-rw-r--r-- | src/bridge/colors.cpp | 170 | ||||
-rw-r--r-- | src/bridge/colors.hpp | 56 | ||||
-rw-r--r-- | src/bridge/list_element.hpp | 19 | ||||
-rw-r--r-- | src/database/database.cpp | 87 | ||||
-rw-r--r-- | src/database/database.hpp | 53 | ||||
-rw-r--r-- | src/irc/iid.cpp | 118 | ||||
-rw-r--r-- | src/irc/iid.hpp | 79 | ||||
-rw-r--r-- | src/irc/irc_channel.cpp | 60 | ||||
-rw-r--r-- | src/irc/irc_channel.hpp | 70 | ||||
-rw-r--r-- | src/irc/irc_client.cpp | 1120 | ||||
-rw-r--r-- | src/irc/irc_client.hpp | 383 | ||||
-rw-r--r-- | src/irc/irc_message.cpp | 61 | ||||
-rw-r--r-- | src/irc/irc_message.hpp | 28 | ||||
-rw-r--r-- | src/irc/irc_user.cpp | 57 | ||||
-rw-r--r-- | src/irc/irc_user.hpp | 33 | ||||
-rw-r--r-- | src/main.cpp | 202 | ||||
-rw-r--r-- | src/utils/empty_if_fixed_server.hpp | 26 | ||||
-rw-r--r-- | src/utils/reload.cpp | 34 | ||||
-rw-r--r-- | src/utils/reload.hpp | 4 | ||||
-rw-r--r-- | src/xmpp/biboumi_adhoc_commands.cpp | 635 | ||||
-rw-r--r-- | src/xmpp/biboumi_adhoc_commands.hpp | 23 | ||||
-rw-r--r-- | src/xmpp/biboumi_component.cpp | 632 | ||||
-rw-r--r-- | src/xmpp/biboumi_component.hpp | 109 |
25 files changed, 5259 insertions, 0 deletions
diff --git a/src/bridge/bridge.cpp b/src/bridge/bridge.cpp new file mode 100644 index 0000000..17d3ec6 --- /dev/null +++ b/src/bridge/bridge.cpp @@ -0,0 +1,907 @@ +#include <bridge/bridge.hpp> +#include <bridge/list_element.hpp> +#include <xmpp/biboumi_component.hpp> +#include <network/poller.hpp> +#include <utils/empty_if_fixed_server.hpp> +#include <utils/encoding.hpp> +#include <utils/tolower.hpp> +#include <logger/logger.hpp> +#include <utils/revstr.hpp> +#include <utils/split.hpp> +#include <xmpp/jid.hpp> +#include <database/database.hpp> + +using namespace std::string_literals; + +static const char* action_prefix = "\01ACTION "; + + +static std::string in_encoding_for(const Bridge& bridge, const Iid& iid) +{ +#ifdef USE_DATABASE + const auto jid = bridge.get_bare_jid(); + auto options = Database::get_irc_channel_options_with_server_default(jid, iid.get_server(), iid.get_local()); + return options.encodingIn.value(); +#else + return {"ISO-8859-1"}; +#endif +} + +Bridge::Bridge(const std::string& user_jid, BiboumiComponent& xmpp, std::shared_ptr<Poller> poller): + user_jid(user_jid), + xmpp(xmpp), + poller(poller) +{ +} + +/** + * Return the role and affiliation, corresponding to the given irc mode + */ +static std::tuple<std::string, std::string> get_role_affiliation_from_irc_mode(const char mode) +{ + if (mode == 'a' || mode == 'q'){ + return std::make_tuple("moderator", "owner");} + else if (mode == 'o') + return std::make_tuple("moderator", "admin"); + else if (mode == 'h') + return std::make_tuple("moderator", "member"); + else if (mode == 'v') + return std::make_tuple("participant", "member"); + else + return std::make_tuple("participant", "none"); +} + +void Bridge::shutdown(const std::string& exit_message) +{ + for (auto it = this->irc_clients.begin(); it != this->irc_clients.end(); ++it) + { + it->second->send_quit_command(exit_message); + it->second->leave_dummy_channel(exit_message); + } +} + +void Bridge::clean() +{ + auto it = this->irc_clients.begin(); + while (it != this->irc_clients.end()) + { + IrcClient* client = it->second.get(); + if (!client->is_connected() && !client->is_connecting() && + !client->get_resolver().is_resolving()) + it = this->irc_clients.erase(it); + else + ++it; + } +} + +const std::string& Bridge::get_jid() const +{ + return this->user_jid; +} + +std::string Bridge::get_bare_jid() const +{ + Jid jid(this->user_jid); + return jid.local + "@" + jid.domain; +} + +Xmpp::body Bridge::make_xmpp_body(const std::string& str, const std::string& encoding) +{ + std::string res; + if (utils::is_valid_utf8(str.c_str())) + res = str; + else + res = utils::convert_to_utf8(str, encoding.data()); + return irc_format_to_xhtmlim(res); +} + +IrcClient* Bridge::make_irc_client(const std::string& hostname, const std::string& nickname) +{ + try + { + return this->irc_clients.at(hostname).get(); + } + catch (const std::out_of_range& exception) + { + auto username = nickname; + auto realname = nickname; + Jid jid(this->user_jid); + if (Config::get("realname_from_jid", "false") == "true") + { + username = jid.local; + realname = this->get_bare_jid(); + } + this->irc_clients.emplace(hostname, + std::make_shared<IrcClient>(this->poller, hostname, + nickname, username, + realname, jid.domain, + *this)); + std::shared_ptr<IrcClient> irc = this->irc_clients.at(hostname); + return irc.get(); + } +} + +IrcClient* Bridge::get_irc_client(const std::string& hostname) +{ + try + { + return this->irc_clients.at(hostname).get(); + } + catch (const std::out_of_range& exception) + { + throw IRCNotConnected(hostname); + } +} + +IrcClient* Bridge::find_irc_client(const std::string& hostname) +{ + try + { + return this->irc_clients.at(hostname).get(); + } + catch (const std::out_of_range& exception) + { + return nullptr; + } +} + +bool Bridge::join_irc_channel(const Iid& iid, const std::string& nickname, const std::string& password, + const std::string& resource) +{ + const auto hostname = iid.get_server(); + IrcClient* irc = this->make_irc_client(hostname, nickname); + this->add_resource_to_server(hostname, resource); + auto res_in_chan = this->is_resource_in_chan(ChannelKey{iid.get_local(), hostname}, resource); + if (!res_in_chan) + this->add_resource_to_chan(ChannelKey{iid.get_local(), hostname}, resource); + if (iid.get_local().empty()) + { // Join the dummy channel + if (irc->is_welcomed()) + { + if (irc->get_dummy_channel().joined) + return false; + // Immediately simulate a message coming from the IRC server saying that we + // joined the channel + const IrcMessage join_message(irc->get_nick(), "JOIN", {""}); + irc->on_channel_join(join_message); + const IrcMessage end_join_message(std::string(iid.get_server()), "366", + {irc->get_nick(), + "", "End of NAMES list"}); + irc->on_channel_completely_joined(end_join_message); + } + else + { + irc->get_dummy_channel().joining = true; + irc->start(); + } + return true; + } + if (irc->is_channel_joined(iid.get_local()) == false) + { + irc->send_join_command(iid.get_local(), password); + return true; + } else if (!res_in_chan) { + this->generate_channel_join_for_resource(iid, resource); + } + return false; +} + +void Bridge::send_channel_message(const Iid& iid, const std::string& body) +{ + if (iid.get_server().empty()) + { + for (const auto& resource: this->resources_in_chan[iid.to_tuple()]) + this->xmpp.send_stanza_error("message", this->user_jid + "/" + resource, std::to_string(iid), "", + "cancel", "remote-server-not-found", + std::to_string(iid) + " is not a valid channel name. " + "A correct room jid is of the form: #<chan>%<server>", + false); + return; + } + IrcClient* irc = this->get_irc_client(iid.get_server()); + + // Because an IRC message cannot contain \n, we need to convert each line + // of text into a separate IRC message. For conveniance, we also cut the + // message into submessages on the XMPP side, this way the user of the + // gateway sees what was actually sent over IRC. For example if an user + // sends “hello\n/me waves”, two messages will be generated: “hello” and + // “/me waves”. Note that the “command” handling (messages starting with + // /me, /mode, etc) is done for each message generated this way. In the + // above example, the /me will be interpreted. + std::vector<std::string> lines = utils::split(body, '\n', true); + if (lines.empty()) + return ; + for (const std::string& line: lines) + { + if (line.substr(0, 5) == "/mode") + { + std::vector<std::string> args = utils::split(line.substr(5), ' ', false); + irc->send_mode_command(iid.get_local(), args); + continue; // We do not want to send that back to the + // 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"); + else + irc->send_channel_message(iid.get_local(), line); + 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); + } +} + +void Bridge::forward_affiliation_role_change(const Iid& iid, const std::string& nick, + const std::string& affiliation, + const std::string& role) +{ + IrcClient* irc = this->get_irc_client(iid.get_server()); + IrcChannel* chan = irc->get_channel(iid.get_local()); + if (!chan || !chan->joined) + return; + IrcUser* user = chan->find_user(nick); + if (!user) + return; + // For each affiliation or role, we have a “maximal” mode that we want to + // set. We must remove any superior mode at the same time. For example if + // the user already has +o mode, and we set its affiliation to member, we + // remove the +o mode, and add +v. For each “superior” mode (for example, + // for +v, the superior modes are 'h', 'a', 'o' and 'q') we check if that + // user has it, and if yes we remove that mode + + std::size_t nb = 1; // the number of times the nick must be + // repeated in the argument list + std::string modes; // The string of modes to + // add/remove. For example "+v-aoh" + std::vector<char> modes_to_remove; // List of modes to check for removal + if (affiliation == "none") + { + modes = ""; + nb = 0; + modes_to_remove = {'v', 'h', 'o', 'a', 'q'}; + } + else if (affiliation == "member") + { + modes = "+v"; + modes_to_remove = {'h', 'o', 'a', 'q'}; + } + else if (role == "moderator") + { + modes = "+h"; + modes_to_remove = {'o', 'a', 'q'}; + } + else if (affiliation == "admin") + { + modes = "+o"; + modes_to_remove = {'a', 'q'}; + } + else if (affiliation == "owner") + { + modes = "+a"; + modes_to_remove = {'q'}; + } + else + return; + for (const char mode: modes_to_remove) + if (user->modes.find(mode) != user->modes.end()) + { + modes += "-"s + mode; + nb++; + } + if (modes.empty()) + return; + std::vector<std::string> args(nb, nick); + args.insert(args.begin(), modes); + irc->send_mode_command(iid.get_local(), args); +} + +void Bridge::send_private_message(const Iid& iid, const std::string& body, const std::string& type) +{ + if (iid.get_local().empty() || iid.get_server().empty()) + { + this->xmpp.send_stanza_error("message", this->user_jid, std::to_string(iid), "", + "cancel", "remote-server-not-found", + std::to_string(iid) + " is not a valid channel name. " + "A correct room jid is of the form: #<chan>%<server>", + false); + return; + } + IrcClient* irc = this->get_irc_client(iid.get_server()); + std::vector<std::string> lines = utils::split(body, '\n', true); + if (lines.empty()) + return ; + for (const std::string& line: lines) + { + if (line.substr(0, 4) == "/me ") + irc->send_private_message(iid.get_local(), action_prefix + line.substr(4) + "\01", type); + else + irc->send_private_message(iid.get_local(), line, type); + } +} + +void Bridge::send_raw_message(const std::string& hostname, const std::string& body) +{ + IrcClient* irc = this->get_irc_client(hostname); + irc->send_raw(body); +} + +void Bridge::leave_irc_channel(Iid&& iid, std::string&& status_message, const std::string& resource) +{ + IrcClient* irc = this->get_irc_client(iid.get_server()); + const auto key = iid.to_tuple(); + if (!this->is_resource_in_chan(key, resource)) + return ; + + const auto resources = this->number_of_resources_in_chan(key); + if (resources == 1) + { + irc->send_part_command(iid.get_local(), status_message); + // Since there are no resources left in that channel, we don't + // want to receive private messages using this room's JID + this->remove_all_preferred_from_jid_of_room(iid.get_local()); + } + else + { + IrcChannel* chan = irc->get_channel(iid.get_local()); + if (chan) + { + auto nick = chan->get_self()->nick; + this->remove_resource_from_chan(key, resource); + this->send_muc_leave(std::move(iid), std::move(nick), + "Biboumi note: "s + std::to_string(resources - 1) + " resources are still in this channel.", + true, 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) +{ + IrcClient* irc = this->get_irc_client(iid.get_server()); + irc->send_nick_command(new_nick); +} + +void Bridge::send_irc_channel_list_request(const Iid& iid, const std::string& iq_id, + const std::string& to_jid) +{ + IrcClient* irc = this->get_irc_client(iid.get_server()); + + irc->send_list_command(); + + std::vector<ListElement> list; + + irc_responder_callback_t cb = [this, iid, iq_id, to_jid, list=std::move(list)](const std::string& irc_hostname, + const IrcMessage& message) mutable -> bool + { + if (irc_hostname != iid.get_server()) + return false; + if (message.command == "263" || message.command == "RPL_TRYAGAIN" || + message.command == "ERR_TOOMANYMATCHES" || message.command == "ERR_NOSUCHSERVER") + { + std::string text; + if (message.arguments.size() >= 2) + text = message.arguments[1]; + this->xmpp.send_stanza_error("iq", to_jid, std::to_string(iid), iq_id, + "wait", "service-unavailable", text, false); + return true; + } + else if (message.command == "322" || message.command == "RPL_LIST") + { // Add element to list + if (message.arguments.size() == 4) + list.emplace_back(message.arguments[1], message.arguments[2], + message.arguments[3]); + return false; + } + else if (message.command == "323" || message.command == "RPL_LISTEND") + { // Send the iq response with the content of the list + this->xmpp.send_iq_room_list_result(iq_id, to_jid, std::to_string(iid), list); + return true; + } + return false; + }; + this->add_waiting_irc(std::move(cb)); +} + +void Bridge::send_irc_kick(const Iid& iid, const std::string& target, const std::string& reason, + const std::string& iq_id, const std::string& to_jid) +{ + IrcClient* irc = this->get_irc_client(iid.get_server()); + + irc->send_kick_command(iid.get_local(), target, reason); + irc_responder_callback_t cb = [this, target, iq_id, to_jid, iid](const std::string& irc_hostname, + const IrcMessage& message) -> bool + { + if (irc_hostname != iid.get_server()) + return false; + if (message.command == "KICK" && message.arguments.size() >= 2) + { + const std::string target_later = message.arguments[1]; + const std::string chan_name_later = utils::tolower(message.arguments[0]); + if (target_later != target || chan_name_later != iid.get_local()) + return false; + this->xmpp.send_iq_result(iq_id, to_jid, std::to_string(iid)); + } + else if (message.command == "401" && message.arguments.size() >= 2) + { + const std::string target_later = message.arguments[1]; + if (target_later != target) + return false; + std::string error_message = "No such nick"; + if (message.arguments.size() >= 3) + error_message = message.arguments[2]; + this->xmpp.send_stanza_error("iq", to_jid, std::to_string(iid), iq_id, "cancel", "item-not-found", + error_message, false); + } + else if (message.command == "482" && message.arguments.size() >= 2) + { + const std::string chan_name_later = utils::tolower(message.arguments[1]); + if (chan_name_later != iid.get_local()) + return false; + std::string error_message = "You're not channel operator"; + if (message.arguments.size() >= 3) + error_message = message.arguments[2]; + this->xmpp.send_stanza_error("iq", to_jid, std::to_string(iid), iq_id, "cancel", "not-allowed", + error_message, false); + } + return true; + }; + this->add_waiting_irc(std::move(cb)); +} + +void Bridge::set_channel_topic(const Iid& iid, const std::string& subject) +{ + IrcClient* irc = this->get_irc_client(iid.get_server()); + irc->send_topic_command(iid.get_local(), subject); +} + +void Bridge::send_xmpp_version_to_irc(const Iid& iid, const std::string& name, const std::string& version, const std::string& os) +{ + std::string result(name + " " + version + " " + os); + + this->send_private_message(iid, "\01VERSION "s + result + "\01", "NOTICE"); +} + +void Bridge::send_irc_ping_result(const Iid& iid, const std::string& id) +{ + this->send_private_message(iid, "\01PING "s + utils::revstr(id) + "\01", "NOTICE"); +} + +void Bridge::send_irc_user_ping_request(const std::string& irc_hostname, const std::string& nick, + const std::string& iq_id, const std::string& to_jid, + const std::string& from_jid) +{ + Iid iid(nick + "!" + irc_hostname); + this->send_private_message(iid, "\01PING " + iq_id + "\01"); + + irc_responder_callback_t cb = [this, nick=utils::tolower(nick), iq_id, to_jid, irc_hostname, from_jid](const std::string& hostname, const IrcMessage& message) -> bool + { + if (irc_hostname != hostname || message.arguments.size() < 2) + return false; + IrcUser user(message.prefix); + const std::string body = message.arguments[1]; + if (message.command == "NOTICE" && utils::tolower(user.nick) == nick + && body.substr(0, 6) == "\01PING ") + { + const std::string id = body.substr(6, body.size() - 7); + if (id != iq_id) + return false; + this->xmpp.send_iq_result_full_jid(iq_id, to_jid, from_jid); + return true; + } + if (message.command == "401" && message.arguments[1] == nick) + { + std::string error_message = "No such nick"; + if (message.arguments.size() >= 3) + error_message = message.arguments[2]; + this->xmpp.send_stanza_error("iq", to_jid, from_jid, iq_id, "cancel", "service-unavailable", + error_message, true); + return true; + } + + return false; + }; + this->add_waiting_irc(std::move(cb)); +} + +void Bridge::send_irc_participant_ping_request(const Iid& iid, const std::string& nick, + const std::string& iq_id, const std::string& to_jid, + const std::string& from_jid) +{ + IrcClient* irc = this->get_irc_client(iid.get_server()); + IrcChannel* chan = irc->get_channel(iid.get_local()); + if (!chan->joined) + { + this->xmpp.send_stanza_error("iq", to_jid, from_jid, iq_id, "cancel", "not-allowed", + "", true); + 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", + "Recipient not in room", true); + return; + } + + // The user is in the room, send it a direct PING + this->send_irc_user_ping_request(iid.get_server(), nick, iq_id, to_jid, from_jid); +} + +void Bridge::on_gateway_ping(const std::string& irc_hostname, const std::string& iq_id, const std::string& to_jid, + const std::string& from_jid) +{ + Jid jid(from_jid); + if (irc_hostname.empty() || this->find_irc_client(irc_hostname)) + this->xmpp.send_iq_result(iq_id, to_jid, jid.local); + else + this->xmpp.send_stanza_error("iq", to_jid, from_jid, iq_id, "cancel", "service-unavailable", + "", true); +} + +void Bridge::send_irc_version_request(const std::string& irc_hostname, const std::string& target, + const std::string& iq_id, const std::string& to_jid, + const std::string& from_jid) +{ + Iid iid(target + "!" + irc_hostname); + this->send_private_message(iid, "\01VERSION\01"); + // TODO, add a timer to remove that waiting iq if the server does not + // respond with a matching command before n seconds + irc_responder_callback_t cb = [this, target, iq_id, to_jid, irc_hostname, from_jid](const std::string& hostname, const IrcMessage& message) -> bool + { + if (irc_hostname != hostname) + return false; + IrcUser user(message.prefix); + if (message.command == "NOTICE" && user.nick == target && + message.arguments.size() >= 2 && message.arguments[1].substr(0, 9) == "\01VERSION ") + { + // remove the "\01VERSION " and the "\01" parts from the string + const std::string version = message.arguments[1].substr(9, message.arguments[1].size() - 10); + this->xmpp.send_version(iq_id, to_jid, from_jid, version); + return true; + } + if (message.command == "401" && message.arguments.size() >= 2 + && message.arguments[1] == target) + { + std::string error_message = "No such nick"; + if (message.arguments.size() >= 3) + error_message = message.arguments[2]; + this->xmpp.send_stanza_error("iq", to_jid, from_jid, iq_id, "cancel", "item-not-found", + error_message, true); + return true; + } + return false; + }; + 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) +{ + const auto encoding = in_encoding_for(*this, iid); + if (muc) + { + for (const auto& resource: this->resources_in_chan[iid.to_tuple()]) + { + this->xmpp.send_muc_message(std::to_string(iid), nick, + this->make_xmpp_body(body, encoding), this->user_jid + "/" + resource); + } + } + else + { + std::string target = std::to_string(iid); + const auto it = this->preferred_user_from.find(iid.get_local()); + if (it != this->preferred_user_from.end()) + { + const auto chan_name = Iid(Jid(it->second).local).get_local(); + for (const auto& resource: this->resources_in_chan[ChannelKey{chan_name, iid.get_server()}]) + this->xmpp.send_message(it->second, this->make_xmpp_body(body, encoding), + this->user_jid + "/" + resource, "chat", true); + } + else + { + for (const auto& resource: this->resources_in_server[iid.get_server()]) + this->xmpp.send_message(std::to_string(iid), this->make_xmpp_body(body, encoding), + this->user_jid + "/" + resource, "chat", false); + } + } +} + +void Bridge::send_presence_error(const Iid& iid, const std::string& nick, + const std::string& type, const std::string& condition, + const std::string& error_code, const std::string& text) +{ + this->xmpp.send_presence_error(std::to_string(iid), nick, this->user_jid, type, condition, error_code, text); +} + +void Bridge::send_muc_leave(Iid&& iid, std::string&& nick, const std::string& message, const bool self, const std::string& resource) +{ + if (!resource.empty()) + this->xmpp.send_muc_leave(std::to_string(iid), std::move(nick), this->make_xmpp_body(message), this->user_jid + "/" + resource, + self); + else + for (const auto& res: this->resources_in_chan[iid.to_tuple()]) + this->xmpp.send_muc_leave(std::to_string(iid), std::move(nick), this->make_xmpp_body(message), this->user_jid + "/" + res, + self); + IrcClient* irc = this->find_irc_client(iid.get_server()); + if (irc && irc->number_of_joined_channels() == 0) + irc->send_quit_command(""); +} + +void Bridge::send_nick_change(Iid&& iid, + const std::string& old_nick, + const std::string& new_nick, + const char user_mode, + const bool self) +{ + std::string affiliation; + std::string role; + std::tie(role, affiliation) = get_role_affiliation_from_irc_mode(user_mode); + + for (const auto& resource: this->resources_in_chan[iid.to_tuple()]) + this->xmpp.send_nick_change(std::to_string(iid), + old_nick, new_nick, affiliation, role, this->user_jid + "/" + resource, self); +} + +void Bridge::send_xmpp_message(const std::string& from, const std::string& author, const std::string& msg) +{ + std::string body; + if (!author.empty()) + { + IrcUser user(author); + body = "\u000303"s + user.nick + (user.host.empty()? + "\u0003: ": + (" (\u000310" + user.host + "\u000303)\u0003: ")) + msg; + } + else + body = msg; + + const auto encoding = in_encoding_for(*this, {from}); + for (const auto& resource: this->resources_in_server[from]) + { + this->xmpp.send_message(from, this->make_xmpp_body(body, encoding), this->user_jid + "/" + resource, "chat"); + } +} + +void Bridge::send_user_join(const std::string& hostname, const std::string& chan_name, + const IrcUser* user, const char user_mode, const bool self) +{ + for (const auto& resource: this->resources_in_chan[ChannelKey{chan_name, hostname}]) + this->send_user_join(hostname, chan_name, user, user_mode, self, resource); +} + +void Bridge::send_user_join(const std::string& hostname, const std::string& chan_name, + const IrcUser* user, const char user_mode, + const bool self, const std::string& resource) +{ + std::string affiliation; + std::string role; + std::tie(role, affiliation) = get_role_affiliation_from_irc_mode(user_mode); + + 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); +} + +void Bridge::send_topic(const std::string& hostname, const std::string& chan_name, const std::string& topic, const std::string& who) +{ + for (const auto& resource: this->resources_in_chan[ChannelKey{chan_name, hostname}]) + { + this->send_topic(hostname, chan_name, topic, who, resource); + } +} + +void Bridge::send_topic(const std::string& hostname, const std::string& chan_name, + const std::string& topic, const std::string& who, + const std::string& resource) +{ + std::string encoded_chan_name(chan_name); + xep0106::encode(encoded_chan_name); + const auto encoding = in_encoding_for(*this, {encoded_chan_name + '%' + hostname}); + this->xmpp.send_topic(encoded_chan_name + utils::empty_if_fixed_server( + "%" + hostname), this->make_xmpp_body(topic, encoding), this->user_jid + "/" + resource, who); + +} + +std::string Bridge::get_own_nick(const Iid& iid) +{ + IrcClient* irc = this->find_irc_client(iid.get_server()); + if (irc) + return irc->get_own_nick(); + return ""; +} + +size_t Bridge::active_clients() const +{ + return this->irc_clients.size(); +} + +void Bridge::kick_muc_user(Iid&& iid, const std::string& target, const std::string& reason, const std::string& author) +{ + for (const auto& resource: this->resources_in_chan[iid.to_tuple()]) + this->xmpp.kick_user(std::to_string(iid), target, reason, author, this->user_jid + "/" + resource); +} + +void Bridge::send_nickname_conflict_error(const Iid& iid, const std::string& nickname) +{ + for (const auto& resource: this->resources_in_chan[iid.to_tuple()]) + this->xmpp.send_presence_error(std::to_string(iid), nickname, this->user_jid + "/" + resource, "cancel", "conflict", "409", ""); +} + +void Bridge::send_affiliation_role_change(const Iid& iid, const std::string& target, const char mode) +{ + std::string role; + std::string affiliation; + + std::tie(role, affiliation) = get_role_affiliation_from_irc_mode(mode); + for (const auto& resource: this->resources_in_chan[iid.to_tuple()]) + this->xmpp.send_affiliation_role_change(std::to_string(iid), target, affiliation, role, this->user_jid + "/" + resource); +} + +void Bridge::send_iq_version_request(const std::string& nick, const std::string& hostname) +{ + const auto resources = this->resources_in_server[hostname]; + if (resources.begin() != resources.end()) + this->xmpp.send_iq_version_request(utils::tolower(nick) + "!" + utils::empty_if_fixed_server(hostname), this->user_jid + "/" + *resources.begin()); +} + +void Bridge::send_xmpp_ping_request(const std::string& nick, const std::string& hostname, + const std::string& id) +{ + // Use revstr because the forwarded ping to target XMPP user must not be + // the same that the request iq, but we also need to get it back easily + // (revstr again) + // Forward to the first resource (arbitrary, based on the “order” of the std::set) only + const auto resources = this->resources_in_server[hostname]; + if (resources.begin() != resources.end()) + this->xmpp.send_ping_request(utils::tolower(nick) + "!" + utils::empty_if_fixed_server(hostname), this->user_jid + "/" + *resources.begin(), utils::revstr(id)); +} + +void Bridge::set_preferred_from_jid(const std::string& nick, const std::string& full_jid) +{ + auto it = this->preferred_user_from.find(nick); + if (it == this->preferred_user_from.end()) + this->preferred_user_from.emplace(nick, full_jid); + else + this->preferred_user_from[nick] = full_jid; +} + +void Bridge::remove_preferred_from_jid(const std::string& nick) +{ + auto it = this->preferred_user_from.find(nick); + if (it != this->preferred_user_from.end()) + this->preferred_user_from.erase(it); +} + +void Bridge::remove_all_preferred_from_jid_of_room(const std::string& channel_name) +{ + for (auto it = this->preferred_user_from.begin(); it != this->preferred_user_from.end();) + { + Iid iid(Jid(it->second).local); + if (iid.get_local() == channel_name) + it = this->preferred_user_from.erase(it); + else + ++it; + } +} + +void Bridge::add_waiting_irc(irc_responder_callback_t&& callback) +{ + this->waiting_irc.emplace_back(std::move(callback)); +} + +void Bridge::trigger_on_irc_message(const std::string& irc_hostname, const IrcMessage& message) +{ + auto it = this->waiting_irc.begin(); + while (it != this->waiting_irc.end()) + { + if ((*it)(irc_hostname, message) == true) + it = this->waiting_irc.erase(it); + else + ++it; + } +} + +std::unordered_map<std::string, std::shared_ptr<IrcClient>>& Bridge::get_irc_clients() +{ + return this->irc_clients; +} + +void Bridge::add_resource_to_chan(const Bridge::ChannelKey& channel, const std::string& resource) +{ + auto it = this->resources_in_chan.find(channel); + if (it == this->resources_in_chan.end()) + this->resources_in_chan[channel] = {resource}; + else + it->second.insert(resource); +} + +void Bridge::remove_resource_from_chan(const Bridge::ChannelKey& channel, const std::string& resource) +{ + auto it = this->resources_in_chan.find(channel); + if (it != this->resources_in_chan.end()) + { + it->second.erase(resource); + if (it->second.empty()) + this->resources_in_chan.erase(it); + } +} + +bool Bridge::is_resource_in_chan(const Bridge::ChannelKey& channel, const std::string& resource) const +{ + auto it = this->resources_in_chan.find(channel); + if (it != this->resources_in_chan.end()) + if (it->second.count(resource) == 1) + return true; + return false; +} + +void Bridge::add_resource_to_server(const Bridge::IrcHostname& irc_hostname, const std::string& resource) +{ + auto it = this->resources_in_server.find(irc_hostname); + if (it == this->resources_in_server.end()) + this->resources_in_server[irc_hostname] = {resource}; + else + it->second.insert(resource); +} + +void Bridge::remove_resource_from_server(const Bridge::IrcHostname& irc_hostname, const std::string& resource) +{ + auto it = this->resources_in_server.find(irc_hostname); + if (it != this->resources_in_server.end()) + { + it->second.erase(resource); + if (it->second.empty()) + this->resources_in_server.erase(it); + } +} + +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_key) const +{ + auto it = this->resources_in_chan.find(channel_key); + if (it == this->resources_in_chan.end()) + return 0; + return it->second.size(); +} + +std::size_t Bridge::number_of_channels_the_resource_is_in(const std::string& irc_hostname, const std::string& resource) const +{ + std::size_t res = 0; + for (auto pair: this->resources_in_chan) + { + if (std::get<0>(pair.first) == irc_hostname && pair.second.count(resource) != 0) + res++; + } + return res; +} + +void Bridge::generate_channel_join_for_resource(const Iid& iid, const std::string& resource) +{ + IrcClient* irc = this->get_irc_client(iid.get_server()); + IrcChannel* channel = irc->get_channel(iid.get_local()); + const auto self = channel->get_self(); + + // Send the occupant list + for (const auto& user: channel->get_users()) + { + if (user->nick != self->nick) + { + log_debug(user->nick); + this->send_user_join(iid.get_server(), iid.get_encoded_local(), + user.get(), user->get_most_significant_mode(irc->get_sorted_user_modes()), + false, resource); + } + } + this->send_user_join(iid.get_server(), iid.get_encoded_local(), + self, self->get_most_significant_mode(irc->get_sorted_user_modes()), + true, resource); + this->send_topic(iid.get_server(), iid.get_encoded_local(), channel->topic, channel->topic_author, resource); +} diff --git a/src/bridge/bridge.hpp b/src/bridge/bridge.hpp new file mode 100644 index 0000000..69b7bd5 --- /dev/null +++ b/src/bridge/bridge.hpp @@ -0,0 +1,293 @@ +#pragma once + + +#include <irc/irc_message.hpp> +#include <irc/irc_client.hpp> +#include <bridge/colors.hpp> +#include <irc/irc_user.hpp> +#include <irc/iid.hpp> + +#include <unordered_map> +#include <functional> +#include <exception> +#include <string> +#include <memory> + +class BiboumiComponent; +class Poller; + +/** + * A callback called for each IrcMessage we receive. If the message triggers + * a response, it must send ore or more iq and return true (in that case it + * is removed from the list), otherwise it must do nothing and just return + * false. + */ +using irc_responder_callback_t = std::function<bool(const std::string& irc_hostname, const IrcMessage& message)>; + +/** + * One bridge is spawned for each XMPP user that uses the component. The + * bridge spawns IrcClients when needed (when the user wants to join a + * channel on a new server) and does the translation between the two + * protocols. + */ +class Bridge +{ +public: + explicit Bridge(const std::string& user_jid, BiboumiComponent& xmpp, std::shared_ptr<Poller> poller); + ~Bridge() = default; + + Bridge(const Bridge&) = delete; + Bridge(Bridge&& other) = delete; + Bridge& operator=(const Bridge&) = delete; + Bridge& operator=(Bridge&&) = delete; + /** + * QUIT all connected IRC servers. + */ + void shutdown(const std::string& exit_message); + /** + * Remove all inactive IrcClients + */ + void clean(); + /** + * Return the jid of the XMPP user using this bridge + */ + const std::string& get_jid() const; + std::string get_bare_jid() const; + + static Xmpp::body make_xmpp_body(const std::string& str, const std::string& encoding = "ISO-8859-1"); + /*** + ** + ** From XMPP to IRC. + ** + **/ + + /** + * Try to join an irc_channel, does nothing and return true if the channel + * was already joined. + */ + bool join_irc_channel(const Iid& iid, const std::string& nickname, const std::string& password, const std::string& resource); + + void send_channel_message(const Iid& iid, const std::string& body); + void send_private_message(const Iid& iid, const std::string& body, const std::string& type="PRIVMSG"); + void send_raw_message(const std::string& hostname, const std::string& body); + void leave_irc_channel(Iid&& iid, std::string&& status_message, const std::string& resource); + void send_irc_nick_change(const Iid& iid, const std::string& new_nick); + void send_irc_kick(const Iid& iid, const std::string& target, const std::string& reason, + const std::string& iq_id, const std::string& to_jid); + void set_channel_topic(const Iid& iid, const std::string& subject); + void send_xmpp_version_to_irc(const Iid& iid, const std::string& name, const std::string& version, + const std::string& os); + void send_irc_ping_result(const Iid& iid, const std::string& id); + void send_irc_version_request(const std::string& irc_hostname, const std::string& target, + const std::string& iq_id, const std::string& to_jid, + const std::string& from_jid); + void send_irc_channel_list_request(const Iid& iid, const std::string& iq_id, + const std::string& to_jid); + void forward_affiliation_role_change(const Iid& iid, const std::string& nick, + const std::string& affiliation, const std::string& role); + /** + * Directly send a CTCP PING request to the IRC user + */ + void send_irc_user_ping_request(const std::string& irc_hostname, const std::string& nick, + const std::string& iq_id, const std::string& to_jid, + const std::string& from_jid); + /** + * First check if the participant is in the room, before sending a direct + * CTCP PING request to the IRC user + */ + void send_irc_participant_ping_request(const Iid& iid, const std::string& nick, + const std::string& iq_id, const std::string& to_jid, + const std::string& from_jid); + /** + * Directly send back a result if it's a gateway ping or if we are + * connected to the given IRC server, an error otherwise. + */ + void on_gateway_ping(const std::string& irc_hostname, const std::string& iq_id, const std::string& to_jid, + const std::string& from_jid); + + /*** + ** + ** From IRC to XMPP. + ** + **/ + + /** + * Send a message corresponding to a server NOTICE, the from attribute + * should be juste the server hostname. + */ + void send_xmpp_message(const std::string& from, const std::string& author, const std::string& msg); + /** + * Send the presence of a new user in the MUC. + */ + void send_user_join(const std::string& hostname, const std::string& chan_name, + const IrcUser* user, const char user_mode, + const bool self, const std::string& resource); + void send_user_join(const std::string& hostname, const std::string& chan_name, + const IrcUser* user, const char user_mode, + const bool self); + + /** + * Send the topic of the MUC to the user + */ + void send_topic(const std::string& hostname, const std::string& chan_name, const std::string& topic, const std::string& who); + void send_topic(const std::string& hostname, const std::string& chan_name, const std::string& topic, const std::string& who, const std::string& resource); + /** + * Send a MUC message from some participant + */ + void send_message(const Iid& iid, const std::string& nick, const std::string& body, const bool muc); + /** + * Send a presence of type error, from a room. + */ + void send_presence_error(const Iid& iid, const std::string& nick, const std::string& type, const std::string& condition, const std::string& error_code, const std::string& text); + /** + * Send an unavailable presence from this participant + */ + void send_muc_leave(Iid&& iid, std::string&& nick, const std::string& message, const bool self, const std::string& resource=""); + /** + * Send presences to indicate that an user old_nick (ourself if self == + * true) changed his nick to new_nick. The user_mode is needed because + * the xmpp presence needs ton contain the role and affiliation of the + * user. + */ + void send_nick_change(Iid&& iid, + const std::string& old_nick, + const std::string& new_nick, + const char user_mode, + const bool self); + void kick_muc_user(Iid&& iid, const std::string& target, const std::string& reason, const std::string& author); + void send_nickname_conflict_error(const Iid& iid, const std::string& nickname); + /** + * Send a role/affiliation change, matching the change of mode for that user + */ + void send_affiliation_role_change(const Iid& iid, const std::string& target, const char mode); + /** + * Send an iq version request coming from nick!hostname@ + */ + void send_iq_version_request(const std::string& nick, const std::string& hostname); + /** + * Send an iq ping request coming from nick!hostname@ + */ + void send_xmpp_ping_request(const std::string& nick, const std::string& hostname, + const std::string& id); + /** + * Misc + */ + std::string get_own_nick(const Iid& iid); + /** + * Get the number of server to which this bridge is connected or connecting. + */ + size_t active_clients() const; + /** + * Add (or replace the existing) <nick, jid> into the preferred_user_from map + */ + void set_preferred_from_jid(const std::string& nick, const std::string& full_jid); + /** + * Remove the preferred jid for the given IRC nick + */ + void remove_preferred_from_jid(const std::string& nick); + /** + * Given a channel_name, remove all preferred from_jid that come + * from this chan. + */ + void remove_all_preferred_from_jid_of_room(const std::string& channel_name); + /** + * Add a callback to the waiting list of irc callbacks. + */ + void add_waiting_irc(irc_responder_callback_t&& callback); + /** + * Iter over all the waiting_iq, call the iq_responder_filter_t for each, + * whenever one of them returns true: call the corresponding + * 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(); + +private: + /** + * Returns the client for the given hostname, create one (and use the + * username in this case) if none is found, and connect that newly-created + * client immediately. + */ + IrcClient* make_irc_client(const std::string& hostname, const std::string& nickname); + /** + * This version does not create the IrcClient if it does not exist, throws + * a IRCServerNotConnected error in that case. + */ + IrcClient* get_irc_client(const std::string& hostname); + /** + * Idem, but returns nullptr if the server does not exist. + */ + IrcClient* find_irc_client(const std::string& hostname); + /** + * The bare JID of the user associated with this bridge. Messages from/to this + * JID are only managed by this bridge. + */ + const std::string user_jid; + /** + * 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; + /** + * To communicate back with the XMPP component + */ + BiboumiComponent& xmpp; + /** + * Poller, to give it the IrcClients that we spawn, to make it manage + * their sockets. + */ + std::shared_ptr<Poller> poller; + /** + * A map of <nick, full_jid>. For example if this map contains <"toto", + * "#somechan%server@biboumi/ToTo">, whenever a private message is + * received from the user "toto", instead of forwarding it to XMPP with + * from='toto!server@biboumi', we use instead + * from='#somechan%server@biboumi/ToTo' + */ + std::unordered_map<std::string, std::string> preferred_user_from; + /** + * A list of callbacks that are waiting for some IrcMessage to trigger a + * response. We add callbacks in this list whenever we received an IQ + * request and we need a response from IRC to be able to provide the + * response iq. + */ + std::vector<irc_responder_callback_t> waiting_irc; + + /** + * Resources to IRC channel/server mapping: + */ + using Resource = std::string; + using ChannelName = std::string; + using IrcHostname = std::string; + using ChannelKey = std::tuple<ChannelName, IrcHostname>; + std::map<ChannelKey, std::set<Resource>> resources_in_chan; + std::map<IrcHostname, std::set<Resource>> resources_in_server; + /** + * Manage which resource is in which channel + */ + void add_resource_to_chan(const ChannelKey& channel_key, const std::string& resource); + void remove_resource_from_chan(const ChannelKey& channel_key, const std::string& resource); + bool is_resource_in_chan(const ChannelKey& channel_key, const std::string& resource) const; + std::size_t number_of_resources_in_chan(const ChannelKey& channel_key) 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; + + /** + * Generate all the stanzas to be sent to this resource, simulating a join on this channel. + * This means sending the whole user list, the topic, etc + * TODO: send message history + */ + void generate_channel_join_for_resource(const Iid& iid, const std::string& resource); +}; + +struct IRCNotConnected: public std::exception +{ + IRCNotConnected(const std::string& hostname): + hostname(hostname) {} + const std::string hostname; +}; + + diff --git a/src/bridge/colors.cpp b/src/bridge/colors.cpp new file mode 100644 index 0000000..66f51ee --- /dev/null +++ b/src/bridge/colors.cpp @@ -0,0 +1,170 @@ +#include <bridge/colors.hpp> +#include <xmpp/xmpp_stanza.hpp> + +#include <algorithm> +#include <iostream> + +#include <string.h> + +using namespace std::string_literals; + +static const char IRC_NUM_COLORS = 16; + +static const char* irc_colors_to_css[IRC_NUM_COLORS] = { + "white", + "black", + "blue", + "green", + "indianred", + "red", + "magenta", + "brown", + "yellow", + "lightgreen", + "cyan", + "lightcyan", + "lightblue", + "lightmagenta", + "gray", + "white", +}; + +#define XHTML_NS "http://www.w3.org/1999/xhtml" + +struct styles_t +{ + bool strong; + bool underline; + bool italic; + int fg; + int bg; +}; + +/** We keep the currently-applied CSS styles in a structure. Each time a tag + * is found, update this style list, then close the current span XML element + * (if it is open), then reopen it with all the new styles in it. This is + * done this way because IRC formatting does not map well with XML + * (hierarchical tags), it’s a lot easier and cleaner to remove all styles + * and reapply them for each tag, instead of trying to keep a consistent + * hierarchy of span, strong, em etc tags. The generated XML is one-level + * deep only. +*/ +Xmpp::body irc_format_to_xhtmlim(const std::string& s) +{ + if (s.find_first_of(irc_format_char) == std::string::npos) + // there is no special formatting at all + return std::make_tuple(s, nullptr); + + std::string cleaned; + + styles_t styles = {false, false, false, -1, -1}; + + std::unique_ptr<XmlNode> result = std::make_unique<XmlNode>("body"); + (*result)["xmlns"] = XHTML_NS; + + std::unique_ptr<XmlNode> current_node_up; + XmlNode* current_node = result.get(); + + std::string::size_type pos_start = 0; + std::string::size_type pos_end; + + while ((pos_end = s.find_first_of(irc_format_char, pos_start)) != std::string::npos) + { + const std::string txt = s.substr(pos_start, pos_end-pos_start); + cleaned += txt; + if (current_node->has_children()) + current_node->get_last_child()->add_to_tail(txt); + else + current_node->add_to_inner(txt); + + if (s[pos_end] == IRC_FORMAT_BOLD_CHAR) + styles.strong = !styles.strong; + else if (s[pos_end] == IRC_FORMAT_NEWLINE_CHAR) + { + current_node->add_child(std::make_unique<XmlNode>("br")); + cleaned += '\n'; + } + else if (s[pos_end] == IRC_FORMAT_UNDERLINE_CHAR) + styles.underline = !styles.underline; + else if (s[pos_end] == IRC_FORMAT_ITALIC_CHAR) + styles.italic = !styles.italic; + else if (s[pos_end] == IRC_FORMAT_RESET_CHAR) + styles = {false, false, false, -1, -1}; + else if (s[pos_end] == IRC_FORMAT_REVERSE_CHAR) + { } // TODO + else if (s[pos_end] == IRC_FORMAT_REVERSE2_CHAR) + { } // TODO + else if (s[pos_end] == IRC_FORMAT_FIXED_CHAR) + { } // TODO + else if (s[pos_end] == IRC_FORMAT_COLOR_CHAR) + { + size_t pos = pos_end + 1; + styles.fg = -1; + styles.bg = -1; + // get the first number following the format char + if (pos < s.size() && s[pos] >= '0' && s[pos] <= '9') + { // first digit + styles.fg = s[pos++] - '0'; + if (pos < s.size() && s[pos] >= '0' && s[pos] <= '9') + // second digit + styles.fg = styles.fg * 10 + s[pos++] - '0'; + } + if (pos < s.size() && s[pos] == ',') + { // get bg color after the comma + pos++; + if (pos < s.size() && s[pos] >= '0' && s[pos] <= '9') + { // first digit + styles.bg = s[pos++] - '0'; + if (pos < s.size() && s[pos] >= '0' && s[pos] <= '9') + // second digit + styles.bg = styles.bg * 10 + s[pos++] - '0'; + } + } + pos_end = pos - 1; + } + + // close opened span, if any + if (current_node != result.get()) + { + result->add_child(std::move(current_node_up)); + current_node = result.get(); + } + // Take all currently-applied style and create a new span with it + std::string styles_str; + if (styles.strong) + styles_str += "font-weight:bold;"; + if (styles.underline) + styles_str += "text-decoration:underline;"; + if (styles.italic) + styles_str += "font-style:italic;"; + if (styles.fg != -1) + styles_str += "color:"s + + irc_colors_to_css[styles.fg % IRC_NUM_COLORS] + ";"; + if (styles.bg != -1) + styles_str += "background-color:"s + + irc_colors_to_css[styles.bg % IRC_NUM_COLORS] + ";"; + if (!styles_str.empty()) + { + current_node_up = std::make_unique<XmlNode>("span"); + current_node = current_node_up.get(); + (*current_node)["style"] = styles_str; + } + + pos_start = pos_end + 1; + } + + // If some text remains, without any format char, just append that text at + // the end of the current node + const std::string txt = s.substr(pos_start, pos_end-pos_start); + cleaned += txt; + if (current_node->has_children()) + current_node->get_last_child()->add_to_tail(txt); + else + current_node->add_to_inner(txt); + + if (current_node != result.get()) + result->add_child(std::move(current_node_up)); + + Xmpp::body body_res = std::make_tuple(cleaned, std::move(result)); + return body_res; +} diff --git a/src/bridge/colors.hpp b/src/bridge/colors.hpp new file mode 100644 index 0000000..e2c8a87 --- /dev/null +++ b/src/bridge/colors.hpp @@ -0,0 +1,56 @@ +#pragma once + + +/** + * A module handling the conversion between IRC colors and XHTML-IM, and + * vice versa. + */ + +#include <string> +#include <memory> +#include <tuple> + +class XmlNode; + +namespace Xmpp +{ +// Contains: +// - an XMPP-valid UTF-8 body +// - an XML node representing the XHTML-IM body, or null + using body = std::tuple<const std::string, std::unique_ptr<XmlNode>>; +} + +#define IRC_FORMAT_BOLD_CHAR '\x02' // done +#define IRC_FORMAT_COLOR_CHAR '\x03' // done +#define IRC_FORMAT_RESET_CHAR '\x0F' // done +#define IRC_FORMAT_FIXED_CHAR '\x11' // ?? +#define IRC_FORMAT_REVERSE_CHAR '\x12' // maybe one day +#define IRC_FORMAT_REVERSE2_CHAR '\x16' // wat +#define IRC_FORMAT_ITALIC_CHAR '\x1D' // done +#define IRC_FORMAT_UNDERLINE_CHAR '\x1F' // done +#define IRC_FORMAT_NEWLINE_CHAR '\n' // done + +static const char irc_format_char[] = { + IRC_FORMAT_BOLD_CHAR, + IRC_FORMAT_COLOR_CHAR, + IRC_FORMAT_RESET_CHAR, + IRC_FORMAT_FIXED_CHAR, + IRC_FORMAT_REVERSE_CHAR, + IRC_FORMAT_REVERSE2_CHAR, + IRC_FORMAT_ITALIC_CHAR, + IRC_FORMAT_UNDERLINE_CHAR, + IRC_FORMAT_NEWLINE_CHAR, + '\x00' +}; + +/** + * Convert the passed string into an XML tree representing the XHTML version + * of the message, converting the IRC colors symbols into xhtml-im + * formatting. + * + * Returns the body cleaned from any IRC formatting (but without any xhtml), + * and the body as XHTML-IM + */ +Xmpp::body irc_format_to_xhtmlim(const std::string& str); + + diff --git a/src/bridge/list_element.hpp b/src/bridge/list_element.hpp new file mode 100644 index 0000000..1eff2ee --- /dev/null +++ b/src/bridge/list_element.hpp @@ -0,0 +1,19 @@ +#pragma once + + +#include <string> + +struct ListElement +{ + ListElement(const std::string& channel, const std::string& nb_users, + const std::string& topic): + channel(channel), + nb_users(nb_users), + topic(topic){} + + std::string channel; + std::string nb_users; + std::string topic; +}; + + diff --git a/src/database/database.cpp b/src/database/database.cpp new file mode 100644 index 0000000..61e1b47 --- /dev/null +++ b/src/database/database.cpp @@ -0,0 +1,87 @@ +#include "biboumi.h" +#ifdef USE_DATABASE + +#include <database/database.hpp> +#include <logger/logger.hpp> +#include <string> + +using namespace std::string_literals; + +std::unique_ptr<db::BibouDB> Database::db; + +void Database::open(const std::string& filename, const std::string& db_type) +{ + try + { + auto new_db = std::make_unique<db::BibouDB>(db_type, + "database="s + filename); + if (new_db->needsUpgrade()) + new_db->upgrade(); + Database::db.reset(new_db.release()); + } catch (const litesql::DatabaseError& e) { + log_error("Failed to open database ", filename, ". ", e.what()); + throw; + } +} + +void Database::set_verbose(const bool val) +{ + Database::db->verbose = val; +} + +db::IrcServerOptions Database::get_irc_server_options(const std::string& owner, + const std::string& server) +{ + try { + auto options = litesql::select<db::IrcServerOptions>(*Database::db, + db::IrcServerOptions::Owner == owner && + db::IrcServerOptions::Server == server).one(); + return options; + } catch (const litesql::NotFound& e) { + db::IrcServerOptions options(*Database::db); + options.owner = owner; + options.server = server; + // options.update(); + return options; + } +} + +db::IrcChannelOptions Database::get_irc_channel_options(const std::string& owner, + const std::string& server, + const std::string& channel) +{ + try { + auto options = litesql::select<db::IrcChannelOptions>(*Database::db, + db::IrcChannelOptions::Owner == owner && + db::IrcChannelOptions::Server == server && + db::IrcChannelOptions::Channel == channel).one(); + return options; + } catch (const litesql::NotFound& e) { + db::IrcChannelOptions options(*Database::db); + options.owner = owner; + options.server = server; + options.channel = channel; + return options; + } +} + +db::IrcChannelOptions Database::get_irc_channel_options_with_server_default(const std::string& owner, + const std::string& server, + const std::string& channel) +{ + auto coptions = Database::get_irc_channel_options(owner, server, channel); + auto soptions = Database::get_irc_server_options(owner, server); + if (coptions.encodingIn.value().empty()) + coptions.encodingIn = soptions.encodingIn; + if (coptions.encodingOut.value().empty()) + coptions.encodingOut = soptions.encodingOut; + + return coptions; +} + +void Database::close() +{ + Database::db.reset(nullptr); +} + +#endif diff --git a/src/database/database.hpp b/src/database/database.hpp new file mode 100644 index 0000000..7173bcd --- /dev/null +++ b/src/database/database.hpp @@ -0,0 +1,53 @@ +#pragma once + + +#include <biboumi.h> +#ifdef USE_DATABASE + +#include "biboudb.hpp" + +#include <memory> + +#include <litesql.hpp> + +class Database +{ +public: + Database() = default; + ~Database() = default; + + Database(const Database&) = delete; + Database(Database&&) = delete; + Database& operator=(const Database&) = delete; + Database& operator=(Database&&) = delete; + + static void set_verbose(const bool val); + + template<typename PersistentType> + static size_t count() + { + return litesql::select<PersistentType>(*Database::db).count(); + } + /** + * Return the object from the db. Create it beforehand (with all default + * values) if it is not already present. + */ + static db::IrcServerOptions get_irc_server_options(const std::string& owner, + const std::string& server); + static db::IrcChannelOptions get_irc_channel_options(const std::string& owner, + const std::string& server, + const std::string& channel); + static db::IrcChannelOptions get_irc_channel_options_with_server_default(const std::string& owner, + const std::string& server, + const std::string& channel); + + static void close(); + static void open(const std::string& filename, const std::string& db_type="sqlite3"); + + +private: + static std::unique_ptr<db::BibouDB> db; +}; +#endif /* USE_DATABASE */ + + diff --git a/src/irc/iid.cpp b/src/irc/iid.cpp new file mode 100644 index 0000000..0e2841e --- /dev/null +++ b/src/irc/iid.cpp @@ -0,0 +1,118 @@ +#include <utils/tolower.hpp> +#include <config/config.hpp> + +#include <irc/iid.hpp> + +#include <utils/encoding.hpp> + +Iid::Iid(const std::string& iid): + is_channel(false), + is_user(false) +{ + const std::string fixed_irc_server = Config::get("fixed_irc_server", ""); + if (fixed_irc_server.empty()) + this->init(iid); + else + this->init_with_fixed_server(iid, fixed_irc_server); +} + + +void Iid::init(const std::string& iid) +{ + const std::string::size_type sep = iid.find_first_of("%!"); + if (sep != std::string::npos) + { + if (iid[sep] == '%') + this->is_channel = true; + else + this->is_user = true; + this->set_local(iid.substr(0, sep)); + this->set_server(iid.substr(sep + 1)); + } + else + this->set_server(iid); +} + +void Iid::init_with_fixed_server(const std::string& iid, const std::string& hostname) +{ + this->set_server(hostname); + + const std::string::size_type sep = iid.find("!"); + + // Without any separator, we consider that it's a channel + if (sep == std::string::npos) + { + this->is_channel = true; + this->set_local(iid); + } + else // A separator can be present to differenciate a channel from a user, + // but the part behind it (the hostname) is ignored + { + this->set_local(iid.substr(0, sep)); + this->is_user = true; + } +} + +Iid::Iid(): + is_channel(false), + is_user(false) +{ +} + +void Iid::set_local(const std::string& loc) +{ + std::string local(utils::tolower(loc)); + xep0106::decode(local); + this->local = local; +} + +void Iid::set_server(const std::string& serv) +{ + this->server = utils::tolower(serv); +} + +const std::string& Iid::get_local() const +{ + return this->local; +} + +const std::string Iid::get_encoded_local() const +{ + std::string local(this->local); + xep0106::encode(local); + return local; +} + +const std::string& Iid::get_server() const +{ + return this->server; +} + +std::string Iid::get_sep() const +{ + if (this->is_channel) + return "%"; + else if (this->is_user) + return "!"; + return ""; +} + +namespace std { + const std::string to_string(const Iid& iid) + { + if (Config::get("fixed_irc_server", "").empty()) + return iid.get_encoded_local() + iid.get_sep() + iid.get_server(); + else + { + if (iid.get_sep() == "!") + return iid.get_encoded_local() + iid.get_sep(); + else + return iid.get_encoded_local(); + } + } +} + +std::tuple<std::string, std::string> Iid::to_tuple() const +{ + return std::make_tuple(this->get_local(), this->get_server()); +} diff --git a/src/irc/iid.hpp b/src/irc/iid.hpp new file mode 100644 index 0000000..3b11470 --- /dev/null +++ b/src/irc/iid.hpp @@ -0,0 +1,79 @@ +#pragma once + + +#include <string> + +/** + * A name representing an IRC channel on an IRC server, or an IRC user on an + * IRC server, or just an IRC server. + * + * The separator for an user is '!', for a channel it's '%'. If no separator + * is present, it's just an irc server. + * It’s possible to have an empty-string server, but it makes no sense in + * the biboumi context. + * + * #test%irc.example.org has : + * - local: "#test" (the # is part of the name, it could very well be absent, or & (for example) instead) + * - server: "irc.example.org" + * - is_channel: true + * - is_user: false + * + * %irc.example.org: + * - local: "" + * - server: "irc.example.org" + * - is_channel: true + * - is_user: false + * Note: this is the special empty-string channel, used internal in biboumi + * but has no meaning on IRC. + * + * foo!irc.example.org + * - local: "foo" + * - server: "irc.example.org" + * - is_channel: false + * - is_user: true + * Note: the empty-string user (!irc.example.org) has no special meaning in biboumi + * + * irc.example.org: + * - local: "" + * - server: "irc.example.org" + * - is_channel: false + * - is_user: false + */ +class Iid +{ +public: + Iid(const std::string& iid); + Iid(); + Iid(const Iid&) = default; + + Iid(Iid&&) = delete; + Iid& operator=(const Iid&) = delete; + Iid& operator=(Iid&&) = delete; + + void set_local(const std::string& loc); + void set_server(const std::string& serv); + const std::string& get_local() const; + const std::string get_encoded_local() const; + const std::string& get_server() const; + + bool is_channel; + bool is_user; + + std::string get_sep() const; + + std::tuple<std::string, std::string> to_tuple() const; + +private: + + void init(const std::string& iid); + void init_with_fixed_server(const std::string& iid, const std::string& hostname); + + std::string local; + std::string server; +}; + +namespace std { + const std::string to_string(const Iid& iid); +} + + diff --git a/src/irc/irc_channel.cpp b/src/irc/irc_channel.cpp new file mode 100644 index 0000000..e769245 --- /dev/null +++ b/src/irc/irc_channel.cpp @@ -0,0 +1,60 @@ +#include <irc/irc_channel.hpp> +#include <algorithm> + +IrcChannel::IrcChannel(): + joined(false), + self(nullptr) +{ +} + +void IrcChannel::set_self(const std::string& name) +{ + this->self = std::make_unique<IrcUser>(name); +} + +IrcUser* IrcChannel::add_user(const std::string& name, + const std::map<char, char>& prefix_to_mode) +{ + this->users.emplace_back(std::make_unique<IrcUser>(name, prefix_to_mode)); + return this->users.back().get(); +} + +IrcUser* IrcChannel::get_self() const +{ + return this->self.get(); +} + +IrcUser* IrcChannel::find_user(const std::string& name) const +{ + IrcUser user(name); + for (const auto& u: this->users) + { + if (u->nick == user.nick) + return u.get(); + } + return nullptr; +} + +void IrcChannel::remove_user(const IrcUser* user) +{ + const auto nick = user->nick; + const auto it = std::find_if(this->users.begin(), this->users.end(), + [nick](const std::unique_ptr<IrcUser>& u) + { + return nick == u->nick; + }); + if (it != this->users.end()) + this->users.erase(it); +} + +void IrcChannel::remove_all_users() +{ + this->users.clear(); + this->self.reset(); +} + +DummyIrcChannel::DummyIrcChannel(): + IrcChannel(), + joining(false) +{ +} diff --git a/src/irc/irc_channel.hpp b/src/irc/irc_channel.hpp new file mode 100644 index 0000000..2bcefaf --- /dev/null +++ b/src/irc/irc_channel.hpp @@ -0,0 +1,70 @@ +#pragma once + + +#include <irc/irc_user.hpp> +#include <memory> +#include <string> +#include <vector> +#include <map> + +/** + * Keep the state of a joined channel (the list of occupants with their + * informations (mode, etc), the modes, etc) + */ +class IrcChannel +{ +public: + explicit IrcChannel(); + + IrcChannel(const IrcChannel&) = delete; + IrcChannel(IrcChannel&&) = delete; + IrcChannel& operator=(const IrcChannel&) = delete; + IrcChannel& operator=(IrcChannel&&) = delete; + + bool joined; + std::string topic; + std::string topic_author; + void set_self(const std::string& name); + IrcUser* get_self() const; + IrcUser* add_user(const std::string& name, + const std::map<char, char>& prefix_to_mode); + IrcUser* find_user(const std::string& name) const; + void remove_user(const IrcUser* user); + void remove_all_users(); + const std::vector<std::unique_ptr<IrcUser>>& get_users() const + { return this->users; } + +protected: + std::unique_ptr<IrcUser> self; + std::vector<std::unique_ptr<IrcUser>> users; +}; + +/** + * A special channel that is not actually linked to any real irc + * channel. This is just a channel representing a connection to the + * server. If an user wants to maintain the connection to the server without + * having to be on any irc channel of that server, he can just join this + * dummy channel. + * It’s not actually dummy because it’s useful and it does things, but well. + */ +class DummyIrcChannel: public IrcChannel +{ +public: + explicit DummyIrcChannel(); + DummyIrcChannel(const DummyIrcChannel&) = delete; + DummyIrcChannel(DummyIrcChannel&&) = delete; + DummyIrcChannel& operator=(const DummyIrcChannel&) = delete; + DummyIrcChannel& operator=(DummyIrcChannel&&) = delete; + + /** + * This flag is at true whenever the user wants to join this channel, but + * he is not yet connected to the server. When the connection is made, we + * check that flag and if it’s true, we inform the user that he has just + * joined that channel. + * If the user is already connected to the server when he tries to join + * the channel, we don’t use that flag, we just join it immediately. + */ + bool joining; +}; + + diff --git a/src/irc/irc_client.cpp b/src/irc/irc_client.cpp new file mode 100644 index 0000000..dd83307 --- /dev/null +++ b/src/irc/irc_client.cpp @@ -0,0 +1,1120 @@ +#include <utils/timed_events.hpp> +#include <database/database.hpp> +#include <irc/irc_message.hpp> +#include <irc/irc_client.hpp> +#include <bridge/bridge.hpp> +#include <irc/irc_user.hpp> + +#include <logger/logger.hpp> +#include <config/config.hpp> +#include <utils/tolower.hpp> +#include <utils/split.hpp> +#include <utils/string.hpp> + +#include <sstream> +#include <iostream> +#include <stdexcept> +#include <cstring> + +#include <chrono> +#include <string> + +#include "biboumi.h" +#include "louloulibs.h" + +using namespace std::string_literals; +using namespace std::chrono_literals; + +/** + * Define a map of functions to be called for each IRC command we can + * handle. + */ +using IrcCallback = void (IrcClient::*)(const IrcMessage&); + +static const std::unordered_map<std::string, + std::pair<IrcCallback, std::pair<std::size_t, std::size_t>>> irc_callbacks = { + {"NOTICE", {&IrcClient::on_notice, {2, 0}}}, + {"002", {&IrcClient::forward_server_message, {2, 0}}}, + {"003", {&IrcClient::forward_server_message, {2, 0}}}, + {"004", {&IrcClient::on_server_myinfo, {4, 0}}}, + {"005", {&IrcClient::on_isupport_message, {0, 0}}}, + {"RPL_LISTSTART", {&IrcClient::on_rpl_liststart, {0, 0}}}, + {"321", {&IrcClient::on_rpl_liststart, {0, 0}}}, + {"RPL_LIST", {&IrcClient::on_rpl_list, {0, 0}}}, + {"322", {&IrcClient::on_rpl_list, {0, 0}}}, + {"RPL_LISTEND", {&IrcClient::on_rpl_listend, {0, 0}}}, + {"323", {&IrcClient::on_rpl_listend, {0, 0}}}, + {"RPL_NOTOPIC", {&IrcClient::on_empty_topic, {0, 0}}}, + {"331", {&IrcClient::on_empty_topic, {0, 0}}}, + {"RPL_MOTDSTART", {&IrcClient::empty_motd, {0, 0}}}, + {"375", {&IrcClient::empty_motd, {0, 0}}}, + {"RPL_MOTD", {&IrcClient::on_motd_line, {2, 0}}}, + {"372", {&IrcClient::on_motd_line, {2, 0}}}, + {"RPL_MOTDEND", {&IrcClient::send_motd, {0, 0}}}, + {"376", {&IrcClient::send_motd, {0, 0}}}, + {"JOIN", {&IrcClient::on_channel_join, {1, 0}}}, + {"PRIVMSG", {&IrcClient::on_channel_message, {2, 0}}}, + {"353", {&IrcClient::set_and_forward_user_list, {4, 0}}}, + {"332", {&IrcClient::on_topic_received, {2, 0}}}, + {"TOPIC", {&IrcClient::on_topic_received, {2, 0}}}, + {"333", {&IrcClient::on_topic_who_time_received, {4, 0}}}, + {"RPL_TOPICWHOTIME", {&IrcClient::on_topic_who_time_received, {4, 0}}}, + {"366", {&IrcClient::on_channel_completely_joined, {2, 0}}}, + {"396", {&IrcClient::on_own_host_received, {2, 0}}}, + {"432", {&IrcClient::on_erroneous_nickname, {2, 0}}}, + {"433", {&IrcClient::on_nickname_conflict, {2, 0}}}, + {"438", {&IrcClient::on_nickname_change_too_fast, {2, 0}}}, + {"001", {&IrcClient::on_welcome_message, {1, 0}}}, + {"PART", {&IrcClient::on_part, {1, 0}}}, + {"ERROR", {&IrcClient::on_error, {1, 0}}}, + {"QUIT", {&IrcClient::on_quit, {0, 0}}}, + {"NICK", {&IrcClient::on_nick, {1, 0}}}, + {"MODE", {&IrcClient::on_mode, {1, 0}}}, + {"PING", {&IrcClient::send_pong_command, {1, 0}}}, + {"PONG", {&IrcClient::on_pong, {0, 0}}}, + {"KICK", {&IrcClient::on_kick, {3, 0}}}, + + {"401", {&IrcClient::on_generic_error, {2, 0}}}, + {"402", {&IrcClient::on_generic_error, {2, 0}}}, + {"403", {&IrcClient::on_generic_error, {2, 0}}}, + {"404", {&IrcClient::on_generic_error, {2, 0}}}, + {"405", {&IrcClient::on_generic_error, {2, 0}}}, + {"406", {&IrcClient::on_generic_error, {2, 0}}}, + {"407", {&IrcClient::on_generic_error, {2, 0}}}, + {"408", {&IrcClient::on_generic_error, {2, 0}}}, + {"409", {&IrcClient::on_generic_error, {2, 0}}}, + {"410", {&IrcClient::on_generic_error, {2, 0}}}, + {"411", {&IrcClient::on_generic_error, {2, 0}}}, + {"412", {&IrcClient::on_generic_error, {2, 0}}}, + {"414", {&IrcClient::on_generic_error, {2, 0}}}, + {"421", {&IrcClient::on_generic_error, {2, 0}}}, + {"422", {&IrcClient::on_generic_error, {2, 0}}}, + {"423", {&IrcClient::on_generic_error, {2, 0}}}, + {"424", {&IrcClient::on_generic_error, {2, 0}}}, + {"431", {&IrcClient::on_generic_error, {2, 0}}}, + {"436", {&IrcClient::on_generic_error, {2, 0}}}, + {"441", {&IrcClient::on_generic_error, {2, 0}}}, + {"442", {&IrcClient::on_generic_error, {2, 0}}}, + {"443", {&IrcClient::on_generic_error, {2, 0}}}, + {"444", {&IrcClient::on_generic_error, {2, 0}}}, + {"446", {&IrcClient::on_generic_error, {2, 0}}}, + {"451", {&IrcClient::on_generic_error, {2, 0}}}, + {"461", {&IrcClient::on_generic_error, {2, 0}}}, + {"462", {&IrcClient::on_generic_error, {2, 0}}}, + {"463", {&IrcClient::on_generic_error, {2, 0}}}, + {"464", {&IrcClient::on_generic_error, {2, 0}}}, + {"465", {&IrcClient::on_generic_error, {2, 0}}}, + {"467", {&IrcClient::on_generic_error, {2, 0}}}, + {"470", {&IrcClient::on_generic_error, {2, 0}}}, + {"471", {&IrcClient::on_generic_error, {2, 0}}}, + {"472", {&IrcClient::on_generic_error, {2, 0}}}, + {"473", {&IrcClient::on_generic_error, {2, 0}}}, + {"474", {&IrcClient::on_generic_error, {2, 0}}}, + {"475", {&IrcClient::on_generic_error, {2, 0}}}, + {"476", {&IrcClient::on_generic_error, {2, 0}}}, + {"477", {&IrcClient::on_generic_error, {2, 0}}}, + {"481", {&IrcClient::on_generic_error, {2, 0}}}, + {"482", {&IrcClient::on_generic_error, {2, 0}}}, + {"483", {&IrcClient::on_generic_error, {2, 0}}}, + {"484", {&IrcClient::on_generic_error, {2, 0}}}, + {"485", {&IrcClient::on_generic_error, {2, 0}}}, + {"487", {&IrcClient::on_generic_error, {2, 0}}}, + {"491", {&IrcClient::on_generic_error, {2, 0}}}, + {"501", {&IrcClient::on_generic_error, {2, 0}}}, + {"502", {&IrcClient::on_generic_error, {2, 0}}}, +}; + +IrcClient::IrcClient(std::shared_ptr<Poller> poller, const std::string& hostname, + const std::string& nickname, const std::string& username, + const std::string& realname, const std::string& user_hostname, + Bridge& bridge): + TCPSocketHandler(poller), + hostname(hostname), + user_hostname(user_hostname), + username(username), + realname(realname), + current_nick(nickname), + bridge(bridge), + welcomed(false), + chanmodes({"", "", "", ""}), + chantypes({'#', '&'}) +{ + this->dummy_channel.topic = "This is a virtual channel provided for " + "convenience by biboumi, it is not connected " + "to any actual IRC channel of the server '" + this->hostname + + "', and sending messages in it has no effect. " + "Its main goal is to keep the connection to the IRC server " + "alive without having to join a real channel of that server. " + "To disconnect from the IRC server, leave this room and all " + "other IRC channels of that server."; +#ifdef USE_DATABASE + auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(), + this->get_hostname()); + std::vector<std::string> ports = utils::split(options.ports, ';', false); + for (auto it = ports.rbegin(); it != ports.rend(); ++it) + this->ports_to_try.emplace(*it, false); +# ifdef BOTAN_FOUND + ports = utils::split(options.tlsPorts, ';', false); + for (auto it = ports.rbegin(); it != ports.rend(); ++it) + this->ports_to_try.emplace(*it, true); +# endif // BOTAN_FOUND + +#else // not USE_DATABASE + this->ports_to_try.emplace("6667", false); // standard non-encrypted port +# ifdef BOTAN_FOUND + this->ports_to_try.emplace("6670", true); // non-standard but I want it for some servers + this->ports_to_try.emplace("6697", true); // standard encrypted port +# endif // BOTAN_FOUND +#endif // USE_DATABASE +} + +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"s + this->hostname + this->bridge.get_jid()); +} + +void IrcClient::start() +{ + if (this->is_connecting() || this->is_connected()) + return; + std::string port; + bool tls; + std::tie(port, tls) = this->ports_to_try.top(); + this->ports_to_try.pop(); + this->bridge.send_xmpp_message(this->hostname, "", "Connecting to "s + + this->hostname + ":" + port + " (" + + (tls ? "encrypted" : "not encrypted") + ")"); + + this->bind_addr = Config::get("outgoing_bind", ""); + +#ifdef BOTAN_FOUND +# ifdef USE_DATABASE + auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(), + this->get_hostname()); + this->credential_manager.set_trusted_fingerprint(options.trustedFingerprint); +# endif +#endif + this->connect(this->hostname, port, tls); +} + +void IrcClient::on_connection_failed(const std::string& reason) +{ + this->bridge.send_xmpp_message(this->hostname, "", + "Connection failed: "s + reason); + + if (this->hostname_resolution_failed) + while (!this->ports_to_try.empty()) + this->ports_to_try.pop(); + + if (this->ports_to_try.empty()) + { + // Send an error message for all room that the user wanted to join + for (const auto& tuple: this->channels_to_join) + { + Iid iid(std::get<0>(tuple) + "%" + this->hostname); + this->bridge.send_presence_error(iid, this->current_nick, + "cancel", "item-not-found", + "", reason); + } + } + else // try the next port + this->start(); +} + +void IrcClient::on_connected() +{ + const auto webirc_password = Config::get("webirc_password", ""); + static std::string resolved_ip; + + if (!webirc_password.empty()) + { + if (!resolved_ip.empty()) + this->send_webirc_command(webirc_password, resolved_ip); + else + { // Start resolving the hostname of the user, and call + // on_connected again when it’s done + this->dns_resolver.resolve(this->user_hostname, "5222", + [this](const struct addrinfo* addr) + { + resolved_ip = addr_to_string(addr); + // Only continue the process if we + // didn’t get connected while we were + // resolving + if (this->is_connected()) + this->on_connected(); + }, + [this](const char* error_msg) + { + if (this->is_connected()) + { + this->on_connection_close("Could not resolve hostname "s + this->user_hostname + + ": " + error_msg); + this->send_quit_command(""); + } + }); + return; + } + } + + this->send_message({"CAP", {"REQ", "multi-prefix"}}); + this->send_message({"CAP", {"END"}}); + +#ifdef USE_DATABASE + auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(), + this->get_hostname()); + if (!options.pass.value().empty()) + this->send_pass_command(options.pass.value()); +#endif + + this->send_nick_command(this->current_nick); + +#ifdef USE_DATABASE + if (Config::get("realname_customization", "true") == "true") + { + if (!options.username.value().empty()) + this->username = options.username.value(); + if (!options.realname.value().empty()) + this->realname = options.realname.value(); + this->send_user_command(username, realname); + } + else + this->send_user_command(this->username, this->realname); +#else + this->send_user_command(this->username, this->realname); +#endif + this->send_gateway_message("Connected to IRC server"s + (this->use_tls ? " (encrypted)": "") + "."); + this->send_pending_data(); +} + +void IrcClient::on_connection_close(const std::string& error_msg) +{ + std::string message = "Connection closed"; + if (!error_msg.empty()) + message += ": " + error_msg; + else + message += "."; + const IrcMessage error{"ERROR", {message}}; + this->on_error(error); + log_warning(message); +} + +IrcChannel* IrcClient::get_channel(const std::string& n) +{ + if (n.empty()) + return &this->dummy_channel; + const std::string name = utils::tolower(n); + try + { + return this->channels.at(name).get(); + } + catch (const std::out_of_range& exception) + { + this->channels.emplace(name, std::make_unique<IrcChannel>()); + return this->channels.at(name).get(); + } +} + +bool IrcClient::is_channel_joined(const std::string& name) +{ + IrcChannel* channel = this->get_channel(name); + return channel->joined; +} + +std::string IrcClient::get_own_nick() const +{ + return this->current_nick; +} + +void IrcClient::parse_in_buffer(const size_t) +{ + while (true) + { + auto pos = this->in_buf.find("\r\n"); + if (pos == std::string::npos) + break ; + IrcMessage message(this->in_buf.substr(0, pos)); + this->in_buf = this->in_buf.substr(pos + 2, std::string::npos); + log_debug("IRC RECEIVING: (", this->get_hostname(), ") ", message); + + // Call the standard callback (if any), associated with the command + // name that we just received. + auto it = irc_callbacks.find(message.command); + if (it != irc_callbacks.end()) + { + const auto& limits = it->second.second; + // Check that the Message is well formed before actually calling + // the callback. limits.first is the min number of arguments, + // second is the max + if (message.arguments.size() < limits.first || + (limits.second > 0 && message.arguments.size() > limits.second)) + log_warning("Invalid number of arguments for IRC command “", message.command, + "”: ", message.arguments.size()); + else + { + const auto& cb = it->second.first; + try { + (this->*(cb))(message); + } catch (const std::exception& e) { + log_error("Unhandled exception: ", e.what()); + } + } + } + else + { + log_info("No handler for command ", message.command, + ", forwarding the arguments to the user"); + this->on_unknown_message(message); + } + // Try to find a waiting_iq, which response will be triggered by this IrcMessage + this->bridge.trigger_on_irc_message(this->hostname, message); + } +} + +void IrcClient::send_message(IrcMessage&& message) +{ + log_debug("IRC SENDING: (", this->get_hostname(), ") ", message); + std::string res; + if (!message.prefix.empty()) + res += ":" + std::move(message.prefix) + " "; + res += std::move(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)); +} + +void IrcClient::send_raw(const std::string& txt) +{ + log_debug("IRC SENDING (raw): (", this->get_hostname(), ") ", txt); + this->send_data(txt + "\r\n"); +} + +void IrcClient::send_user_command(const std::string& username, const std::string& realname) +{ + this->send_message(IrcMessage("USER", {username, this->user_hostname, "ignored", realname})); +} + +void IrcClient::send_nick_command(const std::string& nick) +{ + this->send_message(IrcMessage("NICK", {nick})); +} + +void IrcClient::send_pass_command(const std::string& password) +{ + this->send_message(IrcMessage("PASS", {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})); +} + +void IrcClient::send_kick_command(const std::string& chan_name, const std::string& target, const std::string& reason) +{ + this->send_message(IrcMessage("KICK", {chan_name, target, reason})); +} + +void IrcClient::send_list_command() +{ + this->send_message(IrcMessage("LIST", {})); +} + +void IrcClient::send_topic_command(const std::string& chan_name, const std::string& topic) +{ + this->send_message(IrcMessage("TOPIC", {chan_name, topic})); +} + +void IrcClient::send_quit_command(const std::string& reason) +{ + this->send_message(IrcMessage("QUIT", {reason})); +} + +void IrcClient::send_join_command(const std::string& chan_name, const std::string& password) +{ + if (this->welcomed == false) + this->channels_to_join.emplace_back(chan_name, password); + else if (password.empty()) + this->send_message(IrcMessage("JOIN", {chan_name})); + else + this->send_message(IrcMessage("JOIN", {chan_name, password})); + this->start(); +} + +bool IrcClient::send_channel_message(const std::string& chan_name, const std::string& body) +{ + IrcChannel* channel = this->get_channel(chan_name); + if (channel->joined == false) + { + log_warning("Cannot send message to channel ", chan_name, ", it is not joined"); + return false; + } + // The max size is 512, taking into account the whole message, not just + // the text we send. + // This includes our own nick, username and host (because this will be + // added by the server into our message), in addition to the basic + // components of the message we send (command name, chan name, \r\n et) + // : + NICK + ! + USER + @ + HOST + <space> + PRIVMSG + <space> + CHAN + <space> + : + \r\n + const auto line_size = 512 - + this->current_nick.size() - this->username.size() - this->own_host.size() - + ::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})); + return true; +} + +void IrcClient::send_private_message(const std::string& username, const std::string& body, const std::string& type) +{ + std::string::size_type pos = 0; + while (pos < body.size()) + { + this->send_message(IrcMessage(std::string(type), {username, body.substr(pos, 400)})); + pos += 400; + } + // We always try to insert and we don't care if the username was already + // in the set. + this->nicks_to_treat_as_private.insert(username); +} + +void IrcClient::send_part_command(const std::string& chan_name, const std::string& status_message) +{ + IrcChannel* channel = this->get_channel(chan_name); + if (channel->joined == true) + { + if (chan_name.empty()) + this->leave_dummy_channel(status_message); + else + this->send_message(IrcMessage("PART", {chan_name, status_message})); + } +} + +void IrcClient::send_mode_command(const std::string& chan_name, const std::vector<std::string>& arguments) +{ + std::vector<std::string> args(arguments); + args.insert(args.begin(), chan_name); + IrcMessage m("MODE", std::move(args)); + this->send_message(std::move(m)); +} + +void IrcClient::send_pong_command(const IrcMessage& message) +{ + const std::string id = message.arguments[0]; + this->send_message(IrcMessage("PONG", {id})); +} + +void IrcClient::on_pong(const IrcMessage&) +{ +} + +void IrcClient::send_ping_command() +{ + this->send_message(IrcMessage("PING", {"biboumi"})); +} + +void IrcClient::forward_server_message(const IrcMessage& message) +{ + const std::string from = message.prefix; + const std::string body = message.arguments[1]; + + this->bridge.send_xmpp_message(this->hostname, from, body); +} + +void IrcClient::on_notice(const IrcMessage& message) +{ + std::string from = message.prefix; + const std::string to = message.arguments[0]; + const std::string body = message.arguments[1]; + + if (!body.empty() && body[0] == '\01' && body[body.size() - 1] == '\01') + // Do not forward the notice to the user if it's a CTCP command + return ; + + if (!to.empty() && this->chantypes.find(to[0]) == this->chantypes.end()) + { + // The notice is for us precisely. + + // Find out if we already sent a private message to this user. If yes + // we treat that message as a private message coming from + // it. Otherwise we treat it as a notice coming from the server. + IrcUser user(from); + std::string nick = utils::tolower(user.nick); + if (this->nicks_to_treat_as_private.find(nick) != + this->nicks_to_treat_as_private.end()) + { // We previously sent a message to that nick) + this->bridge.send_message({nick + "!" + this->hostname}, nick, body, + false); + } + else + this->bridge.send_xmpp_message(this->hostname, from, body); + } + else + { + // The notice was directed at a channel we are in. Modify the message + // to indicate that it is a notice, and make it a MUC message coming + // from the MUC JID + IrcMessage modified_message(std::move(from), "PRIVMSG", {to, "\u000303[notice]\u0003 "s + body}); + this->on_channel_message(modified_message); + } +} + +void IrcClient::on_isupport_message(const IrcMessage& message) +{ + const size_t len = message.arguments.size(); + for (size_t i = 1; i < len; ++i) + { + const std::string token = message.arguments[i]; + if (token.substr(0, 10) == "CHANMODES=") + { + this->chanmodes = utils::split(token.substr(11), ','); + // make sure we have 4 strings + this->chanmodes.resize(4); + } + else if (token.substr(0, 7) == "PREFIX=") + { + size_t i = 8; // jump PREFIX=( + size_t j = 9; + // Find the ) char + while (j < token.size() && token[j] != ')') + j++; + j++; + while (j < token.size() && token[i] != ')') + { + this->sorted_user_modes.push_back(token[i]); + this->prefix_to_mode[token[j++]] = token[i++]; + } + } + else if (token.substr(0, 10) == "CHANTYPES=") + { + // Remove the default types, they apply only if no other value is + // specified. + this->chantypes.clear(); + size_t i = 10; + while (i < token.size()) + this->chantypes.insert(token[i++]); + } + } +} + +void IrcClient::on_server_myinfo(const IrcMessage&) +{ +} + +void IrcClient::send_gateway_message(const std::string& message, const std::string& from) +{ + this->bridge.send_xmpp_message(this->hostname, from, message); +} + +void IrcClient::set_and_forward_user_list(const IrcMessage& message) +{ + const std::string chan_name = utils::tolower(message.arguments[2]); + IrcChannel* channel = this->get_channel(chan_name); + std::vector<std::string> nicks = utils::split(message.arguments[3], ' '); + for (const std::string& nick: nicks) + { + const IrcUser* user = channel->add_user(nick, this->prefix_to_mode); + if (user->nick != channel->get_self()->nick) + { + this->bridge.send_user_join(this->hostname, chan_name, user, user->get_most_significant_mode(this->sorted_user_modes), false); + } + else + { + // we now know the modes of self, so copy the modes into self + channel->get_self()->modes = user->modes; + } + } +} + +void IrcClient::on_channel_join(const IrcMessage& message) +{ + const std::string chan_name = utils::tolower(message.arguments[0]); + IrcChannel* channel; + if (chan_name.empty()) + channel = &this->dummy_channel; + else + channel = this->get_channel(chan_name); + const std::string nick = message.prefix; + if (channel->joined == false) + channel->set_self(nick); + else + { + const IrcUser* user = channel->add_user(nick, this->prefix_to_mode); + this->bridge.send_user_join(this->hostname, chan_name, user, user->get_most_significant_mode(this->sorted_user_modes), false); + } +} + +void IrcClient::on_channel_message(const IrcMessage& message) +{ + const IrcUser user(message.prefix); + const std::string nick = user.nick; + Iid iid; + iid.set_local(message.arguments[0]); + iid.set_server(this->hostname); + const std::string body = message.arguments[1]; + bool muc = true; + if (!this->get_channel(iid.get_local())->joined) + { + iid.is_user = true; + iid.set_local(nick); + muc = false; + } + else + iid.is_channel = true; + if (!body.empty() && body[0] == '\01') + { + if (body.substr(1, 6) == "ACTION") + this->bridge.send_message(iid, nick, + "/me"s + body.substr(7, body.size() - 8), muc); + else if (body.substr(1, 8) == "VERSION\01") + this->bridge.send_iq_version_request(nick, this->hostname); + else if (body.substr(1, 5) == "PING ") + this->bridge.send_xmpp_ping_request(utils::tolower(nick), this->hostname, + body.substr(6, body.size() - 7)); + } + else + this->bridge.send_message(iid, nick, body, muc); +} + +void IrcClient::on_rpl_liststart(const IrcMessage&) +{ +} + +void IrcClient::on_rpl_list(const IrcMessage&) +{ +} + +void IrcClient::on_rpl_listend(const IrcMessage&) +{ +} + +void IrcClient::empty_motd(const IrcMessage&) +{ + this->motd.erase(); +} + +void IrcClient::on_empty_topic(const IrcMessage& message) +{ + const std::string chan_name = utils::tolower(message.arguments[1]); + log_debug("empty topic for ", chan_name); + IrcChannel* channel = this->get_channel(chan_name); + if (channel) + channel->topic.clear(); +} + +void IrcClient::on_motd_line(const IrcMessage& message) +{ + const std::string body = message.arguments[1]; + // We could send the MOTD without a line break between each IRC-message, + // but sometimes it contains some ASCII art, we use line breaks to keep + // them intact. + this->motd += body+"\n"; +} + +void IrcClient::send_motd(const IrcMessage&) +{ + this->bridge.send_xmpp_message(this->hostname, "", this->motd); +} + +void IrcClient::on_topic_received(const IrcMessage& message) +{ + const std::string chan_name = utils::tolower(message.arguments[message.arguments.size() - 2]); + IrcUser author(message.prefix); + IrcChannel* channel = this->get_channel(chan_name); + channel->topic = message.arguments[message.arguments.size() - 1]; + channel->topic_author = author.nick; + if (channel->joined) + this->bridge.send_topic(this->hostname, chan_name, channel->topic, channel->topic_author); +} + +void IrcClient::on_topic_who_time_received(const IrcMessage& message) +{ + IrcUser author(message.arguments[2]); + const std::string chan_name = utils::tolower(message.arguments[1]); + IrcChannel* channel = this->get_channel(chan_name); + channel->topic_author = author.nick; +} + +void IrcClient::on_channel_completely_joined(const IrcMessage& message) +{ + const std::string chan_name = utils::tolower(message.arguments[1]); + IrcChannel* channel = this->get_channel(chan_name); + channel->joined = true; + this->bridge.send_user_join(this->hostname, chan_name, channel->get_self(), channel->get_self()->get_most_significant_mode(this->sorted_user_modes), true); + this->bridge.send_topic(this->hostname, chan_name, channel->topic, channel->topic_author); +} + +void IrcClient::on_own_host_received(const IrcMessage& message) +{ + this->own_host = message.arguments[1]; + const std::string from = message.prefix; + if (message.arguments.size() >= 3) + this->bridge.send_xmpp_message(this->hostname, from, + this->own_host + " " + message.arguments[2]); + else + this->bridge.send_xmpp_message(this->hostname, from, this->own_host + + " is now your displayed host"); +} + +void IrcClient::on_erroneous_nickname(const IrcMessage& message) +{ + const std::string error_msg = message.arguments.size() >= 3 ? + message.arguments[2]: "Erroneous nickname"; + this->send_gateway_message(error_msg + ": " + message.arguments[1], message.prefix); +} + +void IrcClient::on_nickname_conflict(const IrcMessage& message) +{ + const std::string nickname = message.arguments[1]; + this->on_generic_error(message); + for (auto it = this->channels.begin(); it != this->channels.end(); ++it) + { + Iid iid; + iid.set_local(it->first); + iid.set_server(this->hostname); + iid.is_channel = true; + this->bridge.send_nickname_conflict_error(iid, nickname); + } +} + +void IrcClient::on_nickname_change_too_fast(const IrcMessage& message) +{ + const std::string nickname = message.arguments[1]; + std::string txt; + if (message.arguments.size() >= 3) + txt = message.arguments[2]; + this->on_generic_error(message); + for (auto it = this->channels.begin(); it != this->channels.end(); ++it) + { + Iid iid; + iid.set_local(it->first); + iid.set_server(this->hostname); + iid.is_channel = true; + this->bridge.send_presence_error(iid, nickname, + "cancel", "not-acceptable", + "", txt); + } +} + +void IrcClient::on_generic_error(const IrcMessage& message) +{ + const std::string error_msg = message.arguments.size() >= 3 ? + message.arguments[2]: "Unspecified error"; + this->send_gateway_message(message.arguments[1] + ": " + error_msg, message.prefix); +} + +void IrcClient::on_welcome_message(const IrcMessage& message) +{ + this->current_nick = message.arguments[0]; + this->welcomed = true; +#ifdef USE_DATABASE + auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(), + this->get_hostname()); + if (!options.afterConnectionCommand.value().empty()) + this->send_raw(options.afterConnectionCommand.value()); +#endif + // Install a repeated events to regularly send a PING + TimedEventsManager::instance().add_event(TimedEvent(240s, std::bind(&IrcClient::send_ping_command, this), + "PING"s + this->hostname + this->bridge.get_jid())); + for (const auto& tuple: this->channels_to_join) + this->send_join_command(std::get<0>(tuple), std::get<1>(tuple)); + this->channels_to_join.clear(); + // Indicate that the dummy channel is joined as well, if needed + if (this->dummy_channel.joining) + { + // Simulate a message coming from the IRC server saying that we joined + // the channel + const IrcMessage join_message(this->get_nick(), "JOIN", {""}); + this->on_channel_join(join_message); + const IrcMessage end_join_message(std::string(this->hostname), "366", + {this->get_nick(), + "", "End of NAMES list"}); + this->on_channel_completely_joined(end_join_message); + } +} + +void IrcClient::on_part(const IrcMessage& message) +{ + const std::string chan_name = message.arguments[0]; + IrcChannel* channel = this->get_channel(chan_name); + if (!channel->joined) + return ; + std::string txt; + if (message.arguments.size() >= 2) + txt = message.arguments[1]; + const IrcUser* user = channel->find_user(message.prefix); + if (user) + { + std::string nick = user->nick; + channel->remove_user(user); + Iid iid; + iid.set_local(chan_name); + iid.set_server(this->hostname); + iid.is_channel = true; + bool self = channel->get_self()->nick == nick; + if (self) + { + channel->joined = false; + this->channels.erase(utils::tolower(chan_name)); + // channel pointer is now invalid + channel = nullptr; + } + this->bridge.send_muc_leave(std::move(iid), std::move(nick), std::move(txt), self); + } +} + +void IrcClient::on_error(const IrcMessage& message) +{ + const std::string leave_message = message.arguments[0]; + // The user is out of all the channels + for (auto it = this->channels.begin(); it != this->channels.end(); ++it) + { + Iid iid; + iid.set_local(it->first); + iid.set_server(this->hostname); + iid.is_channel = true; + IrcChannel* channel = it->second.get(); + if (!channel->joined) + continue; + std::string own_nick = channel->get_self()->nick; + this->bridge.send_muc_leave(std::move(iid), std::move(own_nick), leave_message, true); + } + this->channels.clear(); + this->send_gateway_message("ERROR: "s + leave_message); +} + +void IrcClient::on_quit(const IrcMessage& message) +{ + std::string txt; + if (message.arguments.size() >= 1) + txt = message.arguments[0]; + for (auto it = this->channels.begin(); it != this->channels.end(); ++it) + { + const std::string chan_name = it->first; + IrcChannel* channel = it->second.get(); + const IrcUser* user = channel->find_user(message.prefix); + if (user) + { + std::string nick = user->nick; + channel->remove_user(user); + Iid iid; + iid.set_local(chan_name); + iid.set_server(this->hostname); + iid.is_channel = true; + this->bridge.send_muc_leave(std::move(iid), std::move(nick), txt, false); + } + } +} + +void IrcClient::on_nick(const IrcMessage& message) +{ + const std::string new_nick = message.arguments[0]; + for (auto it = this->channels.begin(); it != this->channels.end(); ++it) + { + const std::string chan_name = it->first; + IrcChannel* channel = it->second.get(); + IrcUser* user = channel->find_user(message.prefix); + if (user) + { + std::string old_nick = user->nick; + Iid iid; + iid.set_local(chan_name); + iid.set_server(this->hostname); + iid.is_channel = true; + const bool self = channel->get_self()->nick == old_nick; + const char user_mode = user->get_most_significant_mode(this->sorted_user_modes); + this->bridge.send_nick_change(std::move(iid), old_nick, new_nick, user_mode, self); + user->nick = new_nick; + if (self) + { + channel->get_self()->nick = new_nick; + this->current_nick = new_nick; + } + } + } +} + +void IrcClient::on_kick(const IrcMessage& message) +{ + const std::string chan_name = utils::tolower(message.arguments[0]); + const std::string target = message.arguments[1]; + const std::string reason = message.arguments[2]; + IrcChannel* channel = this->get_channel(chan_name); + if (!channel->joined) + return ; + if (channel->get_self()->nick == target) + channel->joined = false; + IrcUser author(message.prefix); + Iid iid; + iid.set_local(chan_name); + iid.set_server(this->hostname); + iid.is_channel = true; + this->bridge.kick_muc_user(std::move(iid), target, reason, author.nick); +} + +void IrcClient::on_mode(const IrcMessage& message) +{ + const std::string target = message.arguments[0]; + if (this->chantypes.find(target[0]) != this->chantypes.end()) + this->on_channel_mode(message); + else + this->on_user_mode(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); + iid.is_channel = true; + IrcUser user(message.prefix); + std::string mode_arguments; + for (size_t i = 1; i < message.arguments.size(); ++i) + { + if (!message.arguments[i].empty()) + { + if (i != 1) + mode_arguments += " "; + mode_arguments += message.arguments[i]; + } + } + this->bridge.send_message(iid, "", "Mode "s + iid.get_local() + + " [" + mode_arguments + "] by " + user.nick, + true); + const IrcChannel* channel = this->get_channel(iid.get_local()); + if (!channel) + return; + + // parse the received modes, we need to handle things like "+m-oo coucou toutou" + const std::string modes = message.arguments[1]; + // a list of modified IrcUsers. When we applied all modes, we check the + // modes that now applies to each of them, and send a notification for + // each one. This is to disallow sending two notifications or more when a + // single MODE command changes two or more modes on the same participant + std::set<const IrcUser*> modified_users; + // If it is true, the modes are added, if it’s false they are + // removed. When we encounter the '+' char, the value is changed to true, + // and with '-' it is changed to false. + bool add = true; + bool use_arg; + size_t arg_pos = 2; + for (const char c: modes) + { + if (c == '+') + add = true; + else if (c == '-') + add = false; + else + { // lookup the mode symbol in the 4 chanmodes lists, depending on + // the list where it is found, it takes an argument or not + size_t type; + for (type = 0; type < 4; ++type) + if (this->chanmodes[type].find(c) != std::string::npos) + break; + if (type == 4) // if mode was not found + { + // That mode can also be of type B if it is present in the + // prefix_to_mode map + for (const std::pair<char, char>& pair: this->prefix_to_mode) + if (pair.second == c) + { + type = 1; + break; + } + } + // modes of type A, B or C (but only with add == true) + if (type == 0 || type == 1 || + (type == 2 && add == true)) + use_arg = true; + else // modes of type C (but only with add == false), D, or unknown + use_arg = false; + if (use_arg == true && message.arguments.size() > arg_pos) + { + const std::string target = message.arguments[arg_pos++]; + IrcUser* user = channel->find_user(target); + if (!user) + { + log_warning("Trying to set mode for non-existing user '", target + , "' in channel", iid.get_local()); + return; + } + if (add) + user->add_mode(c); + else + user->remove_mode(c); + modified_users.insert(user); + } + } + } + for (const IrcUser* u: modified_users) + { + char most_significant_mode = u->get_most_significant_mode(this->sorted_user_modes); + this->bridge.send_affiliation_role_change(iid, u->nick, most_significant_mode); + } +} + +void IrcClient::on_user_mode(const IrcMessage& message) +{ + this->bridge.send_xmpp_message(this->hostname, "", + "User mode for "s + message.arguments[0] + + " is [" + message.arguments[1] + "]"); +} + +void IrcClient::on_unknown_message(const IrcMessage& message) +{ + if (message.arguments.size() < 2) + return ; + std::string from = message.prefix; + std::stringstream ss; + for (auto it = message.arguments.begin() + 1; it != message.arguments.end(); ++it) + { + ss << *it; + if (it + 1 != message.arguments.end()) + ss << " "; + } + this->bridge.send_xmpp_message(this->hostname, from, ss.str()); +} + +size_t IrcClient::number_of_joined_channels() const +{ + if (this->dummy_channel.joined) + return this->channels.size() + 1; + else + return this->channels.size(); +} + +DummyIrcChannel& IrcClient::get_dummy_channel() +{ + return this->dummy_channel; +} + +void IrcClient::leave_dummy_channel(const std::string& exit_message) +{ + if (!this->dummy_channel.joined) + return; + this->dummy_channel.joined = false; + this->dummy_channel.joining = false; + this->dummy_channel.remove_all_users(); + this->bridge.send_muc_leave(Iid("%"s + this->hostname), std::string(this->current_nick), exit_message, true); +} + +#ifdef BOTAN_FOUND +bool IrcClient::abort_on_invalid_cert() const +{ +#ifdef USE_DATABASE + auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(), this->hostname); + return options.verifyCert.value(); +#endif + return true; +} +#endif diff --git a/src/irc/irc_client.hpp b/src/irc/irc_client.hpp new file mode 100644 index 0000000..fc3918e --- /dev/null +++ b/src/irc/irc_client.hpp @@ -0,0 +1,383 @@ +#pragma once + + +#include <irc/irc_message.hpp> +#include <irc/irc_channel.hpp> +#include <irc/iid.hpp> + +#include <network/tcp_socket_handler.hpp> +#include <network/resolver.hpp> + +#include <unordered_map> +#include <utility> +#include <memory> +#include <vector> +#include <string> +#include <stack> +#include <map> +#include <set> + +class Bridge; + +/** + * Represent one IRC client, i.e. an endpoint connected to a single IRC + * server, through a TCP socket, receiving and sending commands to it. + */ +class IrcClient: public TCPSocketHandler +{ +public: + explicit IrcClient(std::shared_ptr<Poller> poller, const std::string& hostname, + const std::string& nickname, const std::string& username, + const std::string& realname, const std::string& user_hostname, + Bridge& bridge); + ~IrcClient(); + + IrcClient(const IrcClient&) = delete; + IrcClient(IrcClient&&) = delete; + IrcClient& operator=(const IrcClient&) = delete; + IrcClient& operator=(IrcClient&&) = delete; + + /** + * Connect to the IRC server + */ + void start(); + /** + * Called when the connection to the server cannot be established + */ + void on_connection_failed(const std::string& reason) override final; + /** + * Called when successfully connected to the server + */ + void on_connected() override final; + /** + * Close the connection, remove us from the poller + */ + void on_connection_close(const std::string& error) override final; + /** + * Parse the data we have received so far and try to get one or more + * complete messages from it. + */ + void parse_in_buffer(const size_t) override final; +#ifdef BOTAN_FOUND + virtual bool abort_on_invalid_cert() const override final; +#endif + /** + * Return the channel with this name, create it if it does not yet exist + */ + IrcChannel* get_channel(const std::string& name); + /** + * Returns true if the channel is joined + */ + bool is_channel_joined(const std::string& name); + /** + * Return our own nick + */ + std::string get_own_nick() const; + /** + * Serialize the given message into a line, and send that into the socket + * (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_raw(const std::string& txt); + /** + * Send the PONG irc command + */ + void send_pong_command(const IrcMessage& message); + /** + * Do nothing when we receive a PONG command (but also do not log that no + * handler exist) + */ + void on_pong(const IrcMessage& message); + void send_ping_command(); + /** + * Send the USER irc command + */ + void send_user_command(const std::string& username, const std::string& realname); + /** + * Send the NICK irc command + */ + void send_nick_command(const std::string& username); + void send_pass_command(const std::string& password); + void send_webirc_command(const std::string& password, const std::string& user_ip); + /** + * Send the JOIN irc command. + */ + void send_join_command(const std::string& chan_name, const std::string& password); + /** + * 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); + /** + * Send a PRIVMSG command for an user + */ + void send_private_message(const std::string& username, const std::string& body, const std::string& type); + /** + * Send the PART irc command + */ + void send_part_command(const std::string& chan_name, const std::string& status_message); + /** + * Send the MODE irc command + */ + void send_mode_command(const std::string& chan_name, const std::vector<std::string>& arguments); + /** + * Send the KICK irc command + */ + void send_kick_command(const std::string& chan_name, const std::string& target, const std::string& reason); + /** + * Send the LIST irc command + */ + void send_list_command(); + void send_topic_command(const std::string& chan_name, const std::string& topic); + /** + * Send the QUIT irc command + */ + void send_quit_command(const std::string& reason); + /** + * 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 + * might want to notify the user about the connexion state) + */ + void send_gateway_message(const std::string& message, const std::string& from=""); + /** + * Forward the server message received from IRC to the XMPP component + */ + void forward_server_message(const IrcMessage& message); + /** + * When receiving the isupport informations. See + * http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt + */ + void on_isupport_message(const IrcMessage& message); + /** + * Does nothing yet. Isn’t that duplicating features from 005? + */ + void on_server_myinfo(const IrcMessage& message); + /** + * Just empty the motd we kept as a string + */ + void empty_motd(const IrcMessage& message); + /** + * Send the MOTD string as one single "big" message + */ + void send_motd(const IrcMessage& message); + /** + * Append this line to the MOTD + */ + void on_motd_line(const IrcMessage& message); + /** + * Forward the join of an other user into an IRC channel, and save the + * IrcUsers in the IrcChannel + */ + void set_and_forward_user_list(const IrcMessage& message); + /** + * Signal the start of the LIST response. The RFC says its obsolete and + * “not used”, but I we receive it on some servers, so just ignore it. + */ + void on_rpl_liststart(const IrcMessage& message); + /** + * A single LIST response line (one channel) + * + * The command is handled in a wait_irc callback. This general handler is + * empty and just used to avoid sending a message stanza for each received + * channel. + */ + void on_rpl_list(const IrcMessage& message); + /** + * Signal the end of the LIST response, ignore. + */ + void on_rpl_listend(const IrcMessage& message); + /** + * Remember our nick and host, when we are joined to the channel. The list + * of user comes after so we do not send the self-presence over XMPP yet. + */ + void on_channel_join(const IrcMessage& message); + /** + * When a channel message is received + */ + void on_channel_message(const IrcMessage& message); + /** + * A notice is received + */ + void on_notice(const IrcMessage& message); + /** + * Save the topic in the IrcChannel + */ + void on_topic_received(const IrcMessage& message); + /** + * Save the topic author in the IrcChannel + */ + void on_topic_who_time_received(const IrcMessage& message); + /** + * Empty the topic + */ + void on_empty_topic(const IrcMessage& message); + /** + * The channel has been completely joined (self presence, topic, all names + * received etc), send the self presence and topic to the XMPP user. + */ + void on_channel_completely_joined(const IrcMessage& message); + /** + * Save our own host, as reported by the server + */ + void on_own_host_received(const IrcMessage& message); + /** + * We tried to set an invalid nickname + */ + void on_erroneous_nickname(const IrcMessage& message); + /** + * When the IRC servers denies our nickname because of a conflict. Send a + * presence conflict from all channels, because the name is server-wide. + */ + void on_nickname_conflict(const IrcMessage& message); + /** + * Idem, but for when the user changes their nickname too quickly + */ + void on_nickname_change_too_fast(const IrcMessage& message); + /** + * Handles most errors from the server by just forwarding the message to the user. + */ + void on_generic_error(const IrcMessage& message); + /** + * When a message 001 is received, join the rooms we wanted to join, and set our actual nickname + */ + void on_welcome_message(const IrcMessage& message); + void on_part(const IrcMessage& message); + void on_error(const IrcMessage& message); + void on_nick(const IrcMessage& message); + void on_kick(const IrcMessage& message); + void on_mode(const IrcMessage& message); + /** + * A mode towards our own user is received (note, that is different from a + * channel mode towards or own nick, see + * http://tools.ietf.org/html/rfc2812#section-3.1.5 VS #section-3.2.3) + */ + void on_user_mode(const IrcMessage& message); + /** + * A mode towards a channel. Note that this can change the mode of the + * channel itself or an IrcUser in it. + */ + void on_channel_mode(const IrcMessage& message); + void on_quit(const IrcMessage& message); + void on_unknown_message(const IrcMessage& message); + /** + * Return the number of joined channels + */ + size_t number_of_joined_channels() const; + /** + * Get a reference to the unique dummy channel + */ + DummyIrcChannel& get_dummy_channel(); + /** + * Leave the dummy channel: forward a message to the user to indicate that + * he left it, and mark it as not joined. + */ + void leave_dummy_channel(const std::string& exit_message); + + const std::string& get_hostname() const { return this->hostname; } + std::string get_nick() const { return this->current_nick; } + bool is_welcomed() const { return this->welcomed; } + + const Resolver& get_resolver() const { return this->dns_resolver; } + + const std::vector<char>& get_sorted_user_modes() const { return sorted_user_modes; } + +private: + /** + * The hostname of the server we are connected to. + */ + const std::string hostname; + /** + * Our own host, as reported by the IRC server. + * By default (and if it is not overridden by the server), it is a + * meaningless string, with the maximum allowed size + */ + std::string own_host{63, '*'}; + /** + * The hostname of the user. This is used in the USER and the WEBIRC + * commands, but only the one in WEBIRC will be used by the IRC server. + */ + const std::string user_hostname; + /** + * The username used in the USER irc command + */ + std::string username; + /** + * The realname used in the USER irc command + */ + std::string realname; + /** + * Our current nickname on the server + */ + std::string current_nick; + /** + * To communicate back with the bridge + */ + Bridge& bridge; + /** + * The list of joined channels, indexed by name + */ + std::unordered_map<std::string, std::unique_ptr<IrcChannel>> channels; + /** + * A single channel with a iid of the form "hostname" (normal channel have + * an iid of the form "chan%hostname". + */ + DummyIrcChannel dummy_channel; + /** + * A list of chan we want to join (tuples with the channel name and the + * password, if any), but we need a response 001 from the server before + * sending the actual JOIN commands. So we just keep the channel names in + * a list, and send the JOIN commands for each of them whenever the + * WELCOME message is received. + */ + std::vector<std::tuple<std::string, std::string>> channels_to_join; + /** + * This flag indicates that the server is completely joined (connection + * has been established, we are authentified and we have a nick) + */ + bool welcomed; + /** + * See http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt section 3.3 + * We store the possible chanmodes in this object. + * chanmodes[0] contains modes of type A, [1] of type B etc + */ + std::vector<std::string> chanmodes; + /** + * See http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt + * section 3.5 + */ + std::set<char> chantypes; + /** + * Each motd line received is appended to this string, which we send when + * the motd is completely received + */ + std::string motd; + /** + * See http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt section 3.14 + * The example given would be transformed into + * modes_to_prefix = {{'&', 'a'}, {'*', 'b'}} + */ + std::map<char, char> prefix_to_mode; + /** + * Available user modes, sorted from most significant to least significant + * (for example 'ahov' is a common order). + */ + std::vector<char> sorted_user_modes; + /** + * A list of ports to which we will try to connect, in reverse. Each port + * is associated with a boolean telling if we should use TLS or not if the + * connection succeeds on that port. + */ + std::stack<std::pair<std::string, bool>> ports_to_try; + /** + * A set of (lowercase) nicknames to which we sent a private message. + */ + std::set<std::string> nicks_to_treat_as_private; + /** + * DNS resolver, used to resolve the hostname of the user if we are using + * the WebIRC protocole. + */ + Resolver dns_resolver; +}; + + diff --git a/src/irc/irc_message.cpp b/src/irc/irc_message.cpp new file mode 100644 index 0000000..966a47c --- /dev/null +++ b/src/irc/irc_message.cpp @@ -0,0 +1,61 @@ +#include <irc/irc_message.hpp> +#include <iostream> + +IrcMessage::IrcMessage(std::string&& line) +{ + std::string::size_type pos; + + // optional prefix + if (line[0] == ':') + { + pos = line.find(" "); + this->prefix = line.substr(1, pos - 1); + line = line.substr(pos + 1, std::string::npos); + } + // command + pos = line.find(" "); + this->command = line.substr(0, pos); + line = line.substr(pos + 1, std::string::npos); + // arguments + do + { + if (line[0] == ':') + { + this->arguments.emplace_back(line.substr(1, std::string::npos)); + break ; + } + pos = line.find(" "); + this->arguments.emplace_back(line.substr(0, pos)); + line = line.substr(pos + 1, std::string::npos); + } while (pos != std::string::npos); +} + +IrcMessage::IrcMessage(std::string&& prefix, + std::string&& command, + std::vector<std::string>&& args): + prefix(std::move(prefix)), + command(std::move(command)), + arguments(std::move(args)) +{ +} + +IrcMessage::IrcMessage(std::string&& command, + std::vector<std::string>&& args): + prefix(), + command(std::move(command)), + arguments(std::move(args)) +{ +} + +std::ostream& operator<<(std::ostream& os, const IrcMessage& message) +{ + os << "IrcMessage"; + os << "[" << message.command << "]"; + for (const std::string& arg: message.arguments) + { + os << "{" << arg << "}"; + } + if (!message.prefix.empty()) + os << "(from: " << message.prefix << ")"; + return os; +} diff --git a/src/irc/irc_message.hpp b/src/irc/irc_message.hpp new file mode 100644 index 0000000..fe954e4 --- /dev/null +++ b/src/irc/irc_message.hpp @@ -0,0 +1,28 @@ +#pragma once + + +#include <vector> +#include <string> +#include <ostream> + +class IrcMessage +{ +public: + IrcMessage(std::string&& line); + IrcMessage(std::string&& prefix, std::string&& command, std::vector<std::string>&& args); + IrcMessage(std::string&& command, std::vector<std::string>&& args); + ~IrcMessage() = default; + + IrcMessage(const IrcMessage&) = delete; + IrcMessage(IrcMessage&&) = delete; + IrcMessage& operator=(const IrcMessage&) = delete; + IrcMessage& operator=(IrcMessage&&) = delete; + + std::string prefix; + std::string command; + std::vector<std::string> arguments; +}; + +std::ostream& operator<<(std::ostream& os, const IrcMessage& message); + + diff --git a/src/irc/irc_user.cpp b/src/irc/irc_user.cpp new file mode 100644 index 0000000..9fa3612 --- /dev/null +++ b/src/irc/irc_user.cpp @@ -0,0 +1,57 @@ +#include <irc/irc_user.hpp> + +#include <iostream> + +IrcUser::IrcUser(const std::string& name, + const std::map<char, char>& prefix_to_mode) +{ + if (name.empty()) + return ; + + // One or more prefix (with multi-prefix support) may come before the + // actual nick + std::string::size_type name_begin = 0; + while (name_begin != name.size()) + { + const auto prefix = prefix_to_mode.find(name[name_begin]); + // This is not a prefix + if (prefix == prefix_to_mode.end()) + break; + this->modes.insert(prefix->second); + name_begin++; + } + + const std::string::size_type sep = name.find("!", name_begin); + if (sep == std::string::npos) + this->nick = name.substr(name_begin); + else + { + this->nick = name.substr(name_begin, sep-name_begin); + this->host = name.substr(sep+1); + } +} + +IrcUser::IrcUser(const std::string& name): + IrcUser(name, {}) +{ +} + +void IrcUser::add_mode(const char mode) +{ + this->modes.insert(mode); +} + +void IrcUser::remove_mode(const char mode) +{ + this->modes.erase(mode); +} + +char IrcUser::get_most_significant_mode(const std::vector<char>& modes) const +{ + for (const char mode: modes) + { + if (this->modes.find(mode) != this->modes.end()) + return mode; + } + return 0; +} diff --git a/src/irc/irc_user.hpp b/src/irc/irc_user.hpp new file mode 100644 index 0000000..c84030e --- /dev/null +++ b/src/irc/irc_user.hpp @@ -0,0 +1,33 @@ +#pragma once + + +#include <vector> +#include <string> +#include <map> +#include <set> + +/** + * Keeps various information about one IRC channel user + */ +class IrcUser +{ +public: + explicit IrcUser(const std::string& name, + const std::map<char, char>& prefix_to_mode); + explicit IrcUser(const std::string& name); + + IrcUser(const IrcUser&) = delete; + IrcUser(IrcUser&&) = delete; + IrcUser& operator=(const IrcUser&) = delete; + IrcUser& operator=(IrcUser&&) = delete; + + void add_mode(const char mode); + void remove_mode(const char mode); + char get_most_significant_mode(const std::vector<char>& sorted_user_modes) const; + + std::string nick; + std::string host; + std::set<char> modes; +}; + + diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..53f3193 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,202 @@ +#include <xmpp/biboumi_component.hpp> +#include <utils/timed_events.hpp> +#include <network/poller.hpp> +#include <config/config.hpp> +#include <logger/logger.hpp> +#include <utils/xdg.hpp> +#include <utils/reload.hpp> + +#ifdef CARES_FOUND +# include <network/dns_handler.hpp> +#endif + +#include <atomic> +#include <signal.h> + +// A flag set by the SIGINT signal handler. +static volatile std::atomic<bool> stop(false); +// Flag set by the SIGUSR1/2 signal handler. +static volatile std::atomic<bool> reload(false); +// A flag indicating that we are wanting to exit the process. i.e: if this +// flag is set and all connections are closed, we can exit properly. +static bool exiting = false; + +/** + * Provide an helpful message to help the user write a minimal working + * configuration file. + */ +int config_help(const std::string& missing_option) +{ + if (!missing_option.empty()) + log_error("Configuration error: empty value for option ", missing_option, "."); + log_error("Please provide a configuration file filled like this:\n\n" + "hostname=irc.example.com\npassword=S3CR3T"); + return 1; +} + +int display_help() +{ + std::cout << "Usage: biboumi [configuration_file]" << std::endl; + return 0; +} + +static void sigint_handler(int sig, siginfo_t*, void*) +{ + // In 2 seconds, repeat the same signal, to force the exit + TimedEventsManager::instance().add_event(TimedEvent(std::chrono::steady_clock::now() + 2s, + [sig]() { raise(sig); })); + stop.store(true); +} + +static void sigusr_handler(int, siginfo_t*, void*) +{ + reload.store(true); +} + +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"); + + try { + open_database(); + } catch (...) { + return 1; + } + + // 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 + sigset_t mask; + sigemptyset(&mask); + sigaddset(&mask, SIGINT); + sigaddset(&mask, SIGTERM); + sigaddset(&mask, SIGUSR1); + sigaddset(&mask, SIGUSR2); + sigprocmask(SIG_BLOCK, &mask, nullptr); + + // Install the signals used to exit the process cleanly, or reload the + // config + struct sigaction on_sigint; + on_sigint.sa_sigaction = &sigint_handler; + // All signals must be blocked while a signal handler is running + 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; + sigaction(SIGINT, &on_sigint, nullptr); + sigaction(SIGTERM, &on_sigint, nullptr); + + // Install a signal to reload the config on SIGUSR1/2 + struct sigaction on_sigusr; + on_sigusr.sa_sigaction = &sigusr_handler; + sigfillset(&on_sigusr.sa_mask); + on_sigusr.sa_flags = 0; + sigaction(SIGUSR1, &on_sigusr, nullptr); + sigaction(SIGUSR2, &on_sigusr, nullptr); + + auto p = std::make_shared<Poller>(); + auto xmpp_component = + std::make_shared<BiboumiComponent>(p, hostname, password); + xmpp_component->start(); + +#ifdef CARES_FOUND + DNSHandler::instance.watch_dns_sockets(p); +#endif + auto timeout = TimedEventsManager::instance().get_timeout(); + while (p->poll(timeout) != -1) + { + TimedEventsManager::instance().execute_expired_events(); + // Check for empty irc_clients (not connected, or with no joined + // channel) and remove them + xmpp_component->clean(); + if (stop) + { + log_info("Signal received, exiting..."); +#ifdef SYSTEMD_FOUND + sd_notify(0, "STOPPING=1"); +#endif + exiting = true; + stop.store(false); + xmpp_component->shutdown(); + // Cancel the timer for a potential reconnection + TimedEventsManager::instance().cancel("XMPP reconnection"); + } + if (reload) + { + log_info("Signal received, reloading the config..."); + ::reload_process(); + reload.store(false); + } + // Reconnect to the XMPP server if this was not intended. This may have + // happened because we sent something invalid to it and it decided to + // close the connection. This is a bug that should be fixed, but we + // still reconnect automatically instead of dropping everything + if (!exiting && xmpp_component->ever_auth && + !xmpp_component->is_connected() && + !xmpp_component->is_connecting()) + { + if (xmpp_component->first_connection_try == true) + { // immediately re-try to connect + xmpp_component->reset(); + xmpp_component->start(); + } + else + { // Re-connecting failed, we now try only each few seconds + auto reconnect_later = [xmpp_component]() + { + xmpp_component->reset(); + xmpp_component->start(); + }; + TimedEvent event(std::chrono::steady_clock::now() + 2s, + reconnect_later, "XMPP reconnection"); + TimedEventsManager::instance().add_event(std::move(event)); + } + } + // If the only existing connection is the one to the XMPP component: + // close the XMPP stream. + if (exiting && xmpp_component->is_connecting()) + xmpp_component->close(); + if (exiting && p->size() == 1 && xmpp_component->is_document_open()) + xmpp_component->close_document(); +#ifdef CARES_FOUND + if (!exiting) + DNSHandler::instance.watch_dns_sockets(p); +#endif + if (exiting) // If we are exiting, do not wait for any timed event + timeout = utils::no_timeout; + else + timeout = TimedEventsManager::instance().get_timeout(); + } +#ifdef CARES_FOUND + DNSHandler::instance.destroy(); +#endif + if (!xmpp_component->ever_auth) + return 1; // To signal that the process did not properly start + log_info("All connections cleanly closed, have a nice day."); + return 0; +} diff --git a/src/utils/empty_if_fixed_server.hpp b/src/utils/empty_if_fixed_server.hpp new file mode 100644 index 0000000..9ccf5fd --- /dev/null +++ b/src/utils/empty_if_fixed_server.hpp @@ -0,0 +1,26 @@ +#pragma once + + +#include <string> + +#include <config/config.hpp> + +namespace utils +{ + inline std::string empty_if_fixed_server(std::string&& str) + { + if (!Config::get("fixed_irc_server", "").empty()) + return {}; + return str; + } + + inline std::string empty_if_fixed_server(const std::string& str) + { + if (!Config::get("fixed_irc_server", "").empty()) + return {}; + return str; + } + +} + + diff --git a/src/utils/reload.cpp b/src/utils/reload.cpp new file mode 100644 index 0000000..348c5b5 --- /dev/null +++ b/src/utils/reload.cpp @@ -0,0 +1,34 @@ +#include <utils/reload.hpp> +#include <database/database.hpp> +#include <config/config.hpp> +#include <utils/xdg.hpp> +#include <logger/logger.hpp> + +#include "biboumi.h" + +void open_database() +{ +#ifdef USE_DATABASE + const auto db_filename = Config::get("db_name", xdg_data_path("biboumi.sqlite")); + log_info("Opening database: ", db_filename); + Database::open(db_filename); + log_info("database successfully opened."); +#endif +} + +void reload_process() +{ + Config::read_conf(); + // Destroy the logger instance, to be recreated the next time a log + // line needs to be written + Logger::instance().reset(); + log_info("Configuration and logger reloaded."); +#ifdef USE_DATABASE + try { + open_database(); + } catch (const litesql::DatabaseError&) { + log_warning("Re-using the previous database."); + } +#endif +} + diff --git a/src/utils/reload.hpp b/src/utils/reload.hpp new file mode 100644 index 0000000..408426a --- /dev/null +++ b/src/utils/reload.hpp @@ -0,0 +1,4 @@ +#pragma once + +void open_database(); +void reload_process(); diff --git a/src/xmpp/biboumi_adhoc_commands.cpp b/src/xmpp/biboumi_adhoc_commands.cpp new file mode 100644 index 0000000..eec930d --- /dev/null +++ b/src/xmpp/biboumi_adhoc_commands.cpp @@ -0,0 +1,635 @@ +#include <xmpp/biboumi_adhoc_commands.hpp> +#include <xmpp/biboumi_component.hpp> +#include <config/config.hpp> +#include <utils/string.hpp> +#include <utils/split.hpp> +#include <xmpp/jid.hpp> + +#include <biboumi.h> + +#ifdef USE_DATABASE +#include <database/database.hpp> +#endif + +#include <louloulibs.h> + +#include <algorithm> + +using namespace std::string_literals; + +void DisconnectUserStep1(XmppComponent& xmpp_component, AdhocSession&, XmlNode& command_node) +{ + auto& biboumi_component = static_cast<BiboumiComponent&>(xmpp_component); + + XmlNode x("jabber:x:data:x"); + x["type"] = "form"; + XmlNode title("title"); + title.set_inner("Disconnect a user from the gateway"); + x.add_child(std::move(title)); + XmlNode instructions("instructions"); + instructions.set_inner("Choose a user JID and a quit message"); + x.add_child(std::move(instructions)); + XmlNode jids_field("field"); + jids_field["var"] = "jids"; + jids_field["type"] = "list-multi"; + jids_field["label"] = "The JIDs to disconnect"; + XmlNode required("required"); + jids_field.add_child(std::move(required)); + for (Bridge* bridge: biboumi_component.get_bridges()) + { + XmlNode option("option"); + option["label"] = bridge->get_jid(); + XmlNode value("value"); + value.set_inner(bridge->get_jid()); + option.add_child(std::move(value)); + jids_field.add_child(std::move(option)); + } + x.add_child(std::move(jids_field)); + + XmlNode message_field("field"); + message_field["var"] = "quit-message"; + message_field["type"] = "text-single"; + message_field["label"] = "Quit message"; + XmlNode message_value("value"); + message_value.set_inner("Disconnected by admin"); + message_field.add_child(std::move(message_value)); + x.add_child(std::move(message_field)); + command_node.add_child(std::move(x)); +} + +void DisconnectUserStep2(XmppComponent& xmpp_component, AdhocSession& session, XmlNode& command_node) +{ + auto& biboumi_component = static_cast<BiboumiComponent&>(xmpp_component); + + // Find out if the jids, and the quit message are provided in the form. + std::string quit_message; + const XmlNode* x = command_node.get_child("x", "jabber:x:data"); + if (x) + { + const XmlNode* message_field = nullptr; + const XmlNode* jids_field = nullptr; + for (const XmlNode* field: x->get_children("field", "jabber:x:data")) + if (field->get_tag("var") == "jids") + jids_field = field; + else if (field->get_tag("var") == "quit-message") + message_field = field; + if (message_field) + { + const XmlNode* value = message_field->get_child("value", "jabber:x:data"); + if (value) + quit_message = value->get_inner(); + } + if (jids_field) + { + std::size_t num = 0; + for (const XmlNode* value: jids_field->get_children("value", "jabber:x:data")) + { + Bridge* bridge = biboumi_component.find_user_bridge(value->get_inner()); + if (bridge) + { + bridge->shutdown(quit_message); + num++; + } + } + command_node.delete_all_children(); + + XmlNode note("note"); + note["type"] = "info"; + if (num == 0) + note.set_inner("No user were disconnected."); + else if (num == 1) + note.set_inner("1 user has been disconnected."); + else + note.set_inner(std::to_string(num) + " users have been disconnected."); + command_node.add_child(std::move(note)); + return; + } + } + XmlNode error(ADHOC_NS":error"); + error["type"] = "modify"; + XmlNode condition(STANZA_NS":bad-request"); + error.add_child(std::move(condition)); + command_node.add_child(std::move(error)); + session.terminate(); +} + +#ifdef USE_DATABASE +void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node) +{ + const Jid owner(session.get_owner_jid()); + const Jid target(session.get_target_jid()); + std::string server_domain; + if ((server_domain = Config::get("fixed_irc_server", "")).empty()) + server_domain = target.local; + auto options = Database::get_irc_server_options(owner.local + "@" + owner.domain, + server_domain); + + XmlNode x("jabber:x:data:x"); + x["type"] = "form"; + XmlNode title("title"); + title.set_inner("Configure the IRC server "s + server_domain); + x.add_child(std::move(title)); + XmlNode instructions("instructions"); + instructions.set_inner("Edit the form, to configure the settings of the IRC server "s + server_domain); + x.add_child(std::move(instructions)); + + XmlNode required("required"); + + XmlNode ports("field"); + ports["var"] = "ports"; + ports["type"] = "text-multi"; + ports["label"] = "Ports"; + ports["desc"] = "List of ports to try, without TLS. Defaults: 6667."; + auto vals = utils::split(options.ports.value(), ';', false); + for (const auto& val: vals) + { + XmlNode ports_value("value"); + ports_value.set_inner(val); + ports.add_child(std::move(ports_value)); + } + ports.add_child(required); + x.add_child(std::move(ports)); + +#ifdef BOTAN_FOUND + XmlNode tls_ports("field"); + 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."; + vals = utils::split(options.tlsPorts.value(), ';', false); + for (const auto& val: vals) + { + XmlNode tls_ports_value("value"); + tls_ports_value.set_inner(val); + tls_ports.add_child(std::move(tls_ports_value)); + } + tls_ports.add_child(required); + x.add_child(std::move(tls_ports)); + + XmlNode verify_cert("field"); + 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"; + XmlNode verify_cert_value("value"); + if (options.verifyCert.value()) + verify_cert_value.set_inner("true"); + else + verify_cert_value.set_inner("false"); + verify_cert.add_child(std::move(verify_cert_value)); + x.add_child(std::move(verify_cert)); + + XmlNode fingerprint("field"); + fingerprint["var"] = "fingerprint"; + fingerprint["type"] = "text-single"; + fingerprint["label"] = "SHA-1 fingerprint of the TLS certificate to trust."; + if (!options.trustedFingerprint.value().empty()) + { + XmlNode fingerprint_value("value"); + fingerprint_value.set_inner(options.trustedFingerprint.value()); + fingerprint.add_child(std::move(fingerprint_value)); + } + fingerprint.add_child(required); + x.add_child(std::move(fingerprint)); +#endif + + XmlNode pass("field"); + pass["var"] = "pass"; + pass["type"] = "text-private"; + pass["label"] = "Server password (to be used in a PASS command when connecting)"; + if (!options.pass.value().empty()) + { + XmlNode pass_value("value"); + pass_value.set_inner(options.pass.value()); + pass.add_child(std::move(pass_value)); + } + pass.add_child(required); + x.add_child(std::move(pass)); + + XmlNode after_cnt_cmd("field"); + after_cnt_cmd["var"] = "after_connect_command"; + after_cnt_cmd["type"] = "text-single"; + after_cnt_cmd["desc"] = "Custom IRC command sent after the connection is established with the server."; + after_cnt_cmd["label"] = "After-connection IRC command"; + if (!options.afterConnectionCommand.value().empty()) + { + XmlNode after_cnt_cmd_value("value"); + after_cnt_cmd_value.set_inner(options.afterConnectionCommand.value()); + after_cnt_cmd.add_child(std::move(after_cnt_cmd_value)); + } + after_cnt_cmd.add_child(required); + x.add_child(std::move(after_cnt_cmd)); + + if (Config::get("realname_customization", "true") == "true") + { + XmlNode username("field"); + username["var"] = "username"; + username["type"] = "text-single"; + username["label"] = "Username"; + if (!options.username.value().empty()) + { + XmlNode username_value("value"); + username_value.set_inner(options.username.value()); + username.add_child(std::move(username_value)); + } + username.add_child(required); + x.add_child(std::move(username)); + + XmlNode realname("field"); + realname["var"] = "realname"; + realname["type"] = "text-single"; + realname["label"] = "Realname"; + if (!options.realname.value().empty()) + { + XmlNode realname_value("value"); + realname_value.set_inner(options.realname.value()); + realname.add_child(std::move(realname_value)); + } + realname.add_child(required); + x.add_child(std::move(realname)); + } + + XmlNode encoding_out("field"); + encoding_out["var"] = "encoding_out"; + encoding_out["type"] = "text-single"; + encoding_out["desc"] = "The encoding used when sending messages to the IRC server."; + encoding_out["label"] = "Out encoding"; + if (!options.encodingOut.value().empty()) + { + XmlNode encoding_out_value("value"); + encoding_out_value.set_inner(options.encodingOut.value()); + encoding_out.add_child(std::move(encoding_out_value)); + } + encoding_out.add_child(required); + x.add_child(std::move(encoding_out)); + + XmlNode encoding_in("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."; + encoding_in["label"] = "In encoding"; + if (!options.encodingIn.value().empty()) + { + XmlNode encoding_in_value("value"); + encoding_in_value.set_inner(options.encodingIn.value()); + encoding_in.add_child(std::move(encoding_in_value)); + } + encoding_in.add_child(required); + x.add_child(std::move(encoding_in)); + + + command_node.add_child(std::move(x)); +} + +void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node) +{ + const XmlNode* x = command_node.get_child("x", "jabber:x:data"); + if (x) + { + const Jid owner(session.get_owner_jid()); + const Jid target(session.get_target_jid()); + std::string server_domain; + if ((server_domain = Config::get("fixed_irc_server", "")).empty()) + server_domain = target.local; + auto options = Database::get_irc_server_options(owner.local + "@" + owner.domain, + server_domain); + for (const XmlNode* field: x->get_children("field", "jabber:x:data")) + { + const XmlNode* value = field->get_child("value", "jabber:x:data"); + const std::vector<const XmlNode*> values = field->get_children("value", "jabber:x:data"); + if (field->get_tag("var") == "ports") + { + std::string ports; + for (const auto& val: values) + ports += val->get_inner() + ";"; + options.ports = ports; + } + +#ifdef BOTAN_FOUND + else if (field->get_tag("var") == "tls_ports") + { + std::string ports; + for (const auto& val: values) + ports += val->get_inner() + ";"; + options.tlsPorts = ports; + } + + else if (field->get_tag("var") == "verify_cert" && value + && !value->get_inner().empty()) + { + auto val = to_bool(value->get_inner()); + options.verifyCert = val; + } + + else if (field->get_tag("var") == "fingerprint" && value && + !value->get_inner().empty()) + { + options.trustedFingerprint = value->get_inner(); + } + +#endif // BOTAN_FOUND + + else if (field->get_tag("var") == "pass" && + value && !value->get_inner().empty()) + options.pass = value->get_inner(); + + else if (field->get_tag("var") == "after_connect_command" && + value && !value->get_inner().empty()) + options.afterConnectionCommand = value->get_inner(); + + else if (field->get_tag("var") == "username" && + value && !value->get_inner().empty()) + { + auto username = value->get_inner(); + // The username must not contain spaces + std::replace(username.begin(), username.end(), ' ', '_'); + options.username = username; + } + + else if (field->get_tag("var") == "realname" && + value && !value->get_inner().empty()) + options.realname = value->get_inner(); + + else if (field->get_tag("var") == "encoding_out" && + value && !value->get_inner().empty()) + options.encodingOut = value->get_inner(); + + else if (field->get_tag("var") == "encoding_in" && + value && !value->get_inner().empty()) + options.encodingIn = value->get_inner(); + + } + + options.update(); + + command_node.delete_all_children(); + XmlNode note("note"); + note["type"] = "info"; + note.set_inner("Configuration successfully applied."); + command_node.add_child(std::move(note)); + return; + } + XmlNode error(ADHOC_NS":error"); + error["type"] = "modify"; + XmlNode condition(STANZA_NS":bad-request"); + error.add_child(std::move(condition)); + command_node.add_child(std::move(error)); + session.terminate(); +} + +void ConfigureIrcChannelStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node) +{ + const Jid owner(session.get_owner_jid()); + const Jid target(session.get_target_jid()); + const Iid iid(target.local); + auto options = Database::get_irc_channel_options_with_server_default(owner.local + "@" + owner.domain, + iid.get_server(), iid.get_local()); + + XmlNode x("jabber:x:data:x"); + x["type"] = "form"; + XmlNode title("title"); + title.set_inner("Configure the IRC channel "s + iid.get_local() + " on server "s + iid.get_server()); + x.add_child(std::move(title)); + XmlNode instructions("instructions"); + instructions.set_inner("Edit the form, to configure the settings of the IRC channel "s + iid.get_local()); + x.add_child(std::move(instructions)); + + XmlNode required("required"); + + XmlNode encoding_out("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"; + encoding_out["label"] = "Out encoding"; + if (!options.encodingOut.value().empty()) + { + XmlNode encoding_out_value("value"); + encoding_out_value.set_inner(options.encodingOut.value()); + encoding_out.add_child(std::move(encoding_out_value)); + } + encoding_out.add_child(required); + x.add_child(std::move(encoding_out)); + + XmlNode encoding_in("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"; + encoding_in["label"] = "In encoding"; + if (!options.encodingIn.value().empty()) + { + XmlNode encoding_in_value("value"); + encoding_in_value.set_inner(options.encodingIn.value()); + encoding_in.add_child(std::move(encoding_in_value)); + } + encoding_in.add_child(required); + x.add_child(std::move(encoding_in)); + + command_node.add_child(std::move(x)); +} + +void ConfigureIrcChannelStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node) +{ + const XmlNode* x = command_node.get_child("x", "jabber:x:data"); + if (x) + { + const Jid owner(session.get_owner_jid()); + const Jid target(session.get_target_jid()); + const Iid iid(target.local); + auto options = Database::get_irc_channel_options(owner.local + "@" + owner.domain, + iid.get_server(), iid.get_local()); + for (const XmlNode* field: x->get_children("field", "jabber:x:data")) + { + const XmlNode* value = field->get_child("value", "jabber:x:data"); + + if (field->get_tag("var") == "encoding_out" && + value && !value->get_inner().empty()) + options.encodingOut = value->get_inner(); + + else if (field->get_tag("var") == "encoding_in" && + value && !value->get_inner().empty()) + options.encodingIn = value->get_inner(); + } + + options.update(); + + command_node.delete_all_children(); + XmlNode note("note"); + note["type"] = "info"; + note.set_inner("Configuration successfully applied."); + command_node.add_child(std::move(note)); + return; + } + XmlNode error(ADHOC_NS":error"); + error["type"] = "modify"; + XmlNode condition(STANZA_NS":bad-request"); + error.add_child(std::move(condition)); + command_node.add_child(std::move(error)); + session.terminate(); +} +#endif // USE_DATABASE + +void DisconnectUserFromServerStep1(XmppComponent& xmpp_component, AdhocSession& session, XmlNode& command_node) +{ + const Jid owner(session.get_owner_jid()); + if (owner.bare() != Config::get("admin", "")) + { // A non-admin is not allowed to disconnect other users, only + // him/herself, so we just skip this step + auto next_step = session.get_next_step(); + next_step(xmpp_component, session, command_node); + } + else + { // Send a form to select the user to disconnect + auto& biboumi_component = static_cast<BiboumiComponent&>(xmpp_component); + + XmlNode x("jabber:x:data:x"); + x["type"] = "form"; + XmlNode title("title"); + title.set_inner("Disconnect a user from selected IRC servers"); + x.add_child(std::move(title)); + XmlNode instructions("instructions"); + instructions.set_inner("Choose a user JID"); + x.add_child(std::move(instructions)); + XmlNode jids_field("field"); + jids_field["var"] = "jid"; + jids_field["type"] = "list-single"; + jids_field["label"] = "The JID to disconnect"; + XmlNode required("required"); + jids_field.add_child(std::move(required)); + for (Bridge* bridge: biboumi_component.get_bridges()) + { + XmlNode option("option"); + option["label"] = bridge->get_jid(); + XmlNode value("value"); + value.set_inner(bridge->get_jid()); + option.add_child(std::move(value)); + jids_field.add_child(std::move(option)); + } + x.add_child(std::move(jids_field)); + command_node.add_child(std::move(x)); + } +} + +void DisconnectUserFromServerStep2(XmppComponent& xmpp_component, AdhocSession& session, XmlNode& command_node) +{ + // If no JID is contained in the command node, it means we skipped the + // previous stage, and the jid to disconnect is the executor's jid + std::string jid_to_disconnect = session.get_owner_jid(); + + if (const XmlNode* x = command_node.get_child("x", "jabber:x:data")) + { + for (const XmlNode* field: x->get_children("field", "jabber:x:data")) + if (field->get_tag("var") == "jid") + { + if (const XmlNode* value = field->get_child("value", "jabber:x:data")) + jid_to_disconnect = value->get_inner(); + } + } + + // Save that JID for the last step + session.vars["jid"] = jid_to_disconnect; + + // Send a data form to let the user choose which server to disconnect the + // user from + command_node.delete_all_children(); + auto& biboumi_component = static_cast<BiboumiComponent&>(xmpp_component); + + XmlNode x("jabber:x:data:x"); + x["type"] = "form"; + XmlNode title("title"); + title.set_inner("Disconnect a user from selected IRC servers"); + x.add_child(std::move(title)); + XmlNode instructions("instructions"); + instructions.set_inner("Choose one or more servers to disconnect this JID from"); + x.add_child(std::move(instructions)); + XmlNode jids_field("field"); + jids_field["var"] = "irc-servers"; + jids_field["type"] = "list-multi"; + jids_field["label"] = "The servers to disconnect from"; + XmlNode required("required"); + jids_field.add_child(std::move(required)); + Bridge* bridge = biboumi_component.find_user_bridge(jid_to_disconnect); + + if (!bridge || bridge->get_irc_clients().empty()) + { + XmlNode note("note"); + note["type"] = "info"; + note.set_inner("User "s + jid_to_disconnect + " is not connected to any IRC server."); + command_node.add_child(std::move(note)); + session.terminate(); + return ; + } + + for (const auto& pair: bridge->get_irc_clients()) + { + XmlNode option("option"); + option["label"] = pair.first; + XmlNode value("value"); + value.set_inner(pair.first); + option.add_child(std::move(value)); + jids_field.add_child(std::move(option)); + } + x.add_child(std::move(jids_field)); + + XmlNode message_field("field"); + message_field["var"] = "quit-message"; + message_field["type"] = "text-single"; + message_field["label"] = "Quit message"; + XmlNode message_value("value"); + message_value.set_inner("Killed by admin"); + message_field.add_child(std::move(message_value)); + x.add_child(std::move(message_field)); + + command_node.add_child(std::move(x)); +} + +void DisconnectUserFromServerStep3(XmppComponent& xmpp_component, AdhocSession& session, XmlNode& command_node) +{ + const auto it = session.vars.find("jid"); + if (it == session.vars.end()) + return ; + const auto jid_to_disconnect = it->second; + + std::vector<std::string> servers; + std::string quit_message; + + if (const XmlNode* x = command_node.get_child("x", "jabber:x:data")) + { + for (const XmlNode* field: x->get_children("field", "jabber:x:data")) + { + if (field->get_tag("var") == "irc-servers") + { + for (const XmlNode* value: field->get_children("value", "jabber:x:data")) + servers.push_back(value->get_inner()); + } + else if (field->get_tag("var") == "quit-message") + if (const XmlNode* value = field->get_child("value", "jabber:x:data")) + quit_message = value->get_inner(); + } + } + + auto& biboumi_component = static_cast<BiboumiComponent&>(xmpp_component); + Bridge* bridge = biboumi_component.find_user_bridge(jid_to_disconnect); + auto& clients = bridge->get_irc_clients(); + + std::size_t number = 0; + + for (const auto& hostname: servers) + { + auto it = clients.find(hostname); + if (it != clients.end()) + { + it->second->on_error({"ERROR", {quit_message}}); + clients.erase(it); + number++; + } + } + command_node.delete_all_children(); + XmlNode note("note"); + note["type"] = "info"; + std::string msg = jid_to_disconnect + " was disconnected from " + std::to_string(number) + " IRC server"; + if (number > 1) + msg += "s"; + msg += "."; + note.set_inner(msg); + command_node.add_child(std::move(note)); +} diff --git a/src/xmpp/biboumi_adhoc_commands.hpp b/src/xmpp/biboumi_adhoc_commands.hpp new file mode 100644 index 0000000..2763a9f --- /dev/null +++ b/src/xmpp/biboumi_adhoc_commands.hpp @@ -0,0 +1,23 @@ +#pragma once + + +#include <xmpp/adhoc_command.hpp> +#include <xmpp/adhoc_session.hpp> +#include <xmpp/xmpp_stanza.hpp> + +class XmppComponent; + +void DisconnectUserStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node); +void DisconnectUserStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node); + +void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node); +void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node); + +void ConfigureIrcChannelStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node); +void ConfigureIrcChannelStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node); + +void DisconnectUserFromServerStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node); +void DisconnectUserFromServerStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node); +void DisconnectUserFromServerStep3(XmppComponent&, AdhocSession& session, XmlNode& command_node); + + diff --git a/src/xmpp/biboumi_component.cpp b/src/xmpp/biboumi_component.cpp new file mode 100644 index 0000000..a6aac21 --- /dev/null +++ b/src/xmpp/biboumi_component.cpp @@ -0,0 +1,632 @@ +#include <xmpp/biboumi_component.hpp> + +#include <utils/timed_events.hpp> +#include <utils/scopeguard.hpp> +#include <utils/tolower.hpp> +#include <logger/logger.hpp> +#include <xmpp/adhoc_command.hpp> +#include <xmpp/biboumi_adhoc_commands.hpp> +#include <bridge/list_element.hpp> +#include <config/config.hpp> +#include <xmpp/jid.hpp> +#include <utils/sha1.hpp> + +#include <stdexcept> +#include <iostream> + +#include <stdio.h> + +#include <louloulibs.h> +#include <biboumi.h> + +#include <uuid.h> + +#ifdef SYSTEMD_FOUND +# include <systemd/sd-daemon.h> +#endif + +using namespace std::string_literals; + +static std::set<std::string> kickable_errors{ + "gone", + "internal-server-error", + "item-not-found", + "jid-malformed", + "recipient-unavailable", + "redirect", + "remote-server-not-found", + "remote-server-timeout", + "service-unavailable", + "malformed-error" + }; + + +BiboumiComponent::BiboumiComponent(std::shared_ptr<Poller> poller, const std::string& hostname, const std::string& secret): + XmppComponent(poller, hostname, secret), + irc_server_adhoc_commands_handler(*this), + irc_channel_adhoc_commands_handler(*this) +{ + this->stanza_handlers.emplace("presence", + std::bind(&BiboumiComponent::handle_presence, this,std::placeholders::_1)); + this->stanza_handlers.emplace("message", + std::bind(&BiboumiComponent::handle_message, this,std::placeholders::_1)); + this->stanza_handlers.emplace("iq", + std::bind(&BiboumiComponent::handle_iq, this,std::placeholders::_1)); + + this->adhoc_commands_handler.get_commands() = { + {"ping", AdhocCommand({&PingStep1}, "Do a ping", false)}, + {"hello", AdhocCommand({&HelloStep1, &HelloStep2}, "Receive a custom greeting", false)}, + {"disconnect-user", AdhocCommand({&DisconnectUserStep1, &DisconnectUserStep2}, "Disconnect selected users from the gateway", true)}, + {"disconnect-from-irc-servers", AdhocCommand({&DisconnectUserFromServerStep1, &DisconnectUserFromServerStep2, &DisconnectUserFromServerStep3}, "Disconnect from the selected IRC servers", false)}, + {"reload", AdhocCommand({&Reload}, "Reload biboumi’s configuration", true)} + }; + +#ifdef USE_DATABASE + AdhocCommand configure_server_command({&ConfigureIrcServerStep1, &ConfigureIrcServerStep2}, "Configure a few settings for that IRC server", false); + if (!Config::get("fixed_irc_server", "").empty()) + { + this->adhoc_commands_handler.get_commands().emplace(std::make_pair("configure", + configure_server_command)); + } +#endif + + this->irc_server_adhoc_commands_handler.get_commands() = { +#ifdef USE_DATABASE + {"configure", configure_server_command}, +#endif + }; + this->irc_channel_adhoc_commands_handler.get_commands() = { +#ifdef USE_DATABASE + {"configure", AdhocCommand({&ConfigureIrcChannelStep1, &ConfigureIrcChannelStep2}, "Configure a few settings for that IRC channel", false)}, +#endif + }; +} + +void BiboumiComponent::shutdown() +{ + for (auto it = this->bridges.begin(); it != this->bridges.end(); ++it) + { + it->second->shutdown("Gateway shutdown"); + } +} + +void BiboumiComponent::clean() +{ + auto it = this->bridges.begin(); + while (it != this->bridges.end()) + { + it->second->clean(); + if (it->second->active_clients() == 0) + it = this->bridges.erase(it); + else + ++it; + } +} + +void BiboumiComponent::handle_presence(const Stanza& stanza) +{ + std::string from_str = stanza.get_tag("from"); + std::string id = stanza.get_tag("id"); + std::string to_str = stanza.get_tag("to"); + std::string type = stanza.get_tag("type"); + + // Check for mandatory tags + if (from_str.empty()) + { + log_warning("Received an invalid presence stanza: tag 'from' is missing."); + return; + } + if (to_str.empty()) + { + this->send_stanza_error("presence", from_str, this->served_hostname, id, + "modify", "bad-request", "Missing 'to' tag"); + return; + } + + Bridge* bridge = this->get_user_bridge(from_str); + Jid to(to_str); + Jid from(from_str); + Iid iid(to.local); + + // An error stanza is sent whenever we exit this function without + // disabling this scopeguard. If error_type and error_name are not + // changed, the error signaled is internal-server-error. Change their + // value to signal an other kind of error. For example + // feature-not-implemented, etc. Any non-error process should reach the + // stanza_error.disable() call at the end of the function. + std::string error_type("cancel"); + std::string error_name("internal-server-error"); + utils::ScopeGuard stanza_error([&](){ + this->send_stanza_error("presence", from_str, to_str, id, + error_type, error_name, ""); + }); + + try { + if (iid.is_channel && !iid.get_server().empty()) + { // presence toward a MUC that corresponds to an irc channel, or a + // dummy channel if iid.chan is empty + if (type.empty()) + { + const std::string own_nick = bridge->get_own_nick(iid); + if (!own_nick.empty() && own_nick != to.resource) + bridge->send_irc_nick_change(iid, to.resource); + const XmlNode* x = stanza.get_child("x", MUC_NS); + const XmlNode* password = x ? x->get_child("password", MUC_NS): nullptr; + bridge->join_irc_channel(iid, to.resource, password ? password->get_inner(): "", + from.resource); + } + else if (type == "unavailable") + { + const XmlNode* status = stanza.get_child("status", COMPONENT_NS); + bridge->leave_irc_channel(std::move(iid), status ? status->get_inner() : "", from.resource); + } + } + else + { + // An user wants to join an invalid IRC channel, return a presence error to him + if (type.empty()) + this->send_invalid_room_error(to.local, to.resource, from_str); + } + } + catch (const IRCNotConnected& ex) + { + this->send_stanza_error("presence", from_str, to_str, id, + "cancel", "remote-server-not-found", + "Not connected to IRC server "s + ex.hostname, + true); + } + stanza_error.disable(); +} + +void BiboumiComponent::handle_message(const Stanza& stanza) +{ + std::string from = stanza.get_tag("from"); + std::string id = stanza.get_tag("id"); + std::string to_str = stanza.get_tag("to"); + std::string type = stanza.get_tag("type"); + + if (from.empty()) + return; + if (type.empty()) + type = "normal"; + Bridge* bridge = this->get_user_bridge(from); + Jid to(to_str); + Iid iid(to.local); + + std::string error_type("cancel"); + std::string error_name("internal-server-error"); + utils::ScopeGuard stanza_error([&](){ + this->send_stanza_error("message", from, to_str, id, + error_type, error_name, ""); + }); + const XmlNode* body = stanza.get_child("body", COMPONENT_NS); + + try { // catch IRCNotConnected exceptions + if (type == "groupchat" && iid.is_channel) + { + if (body && !body->get_inner().empty()) + { + bridge->send_channel_message(iid, body->get_inner()); + } + const XmlNode* subject = stanza.get_child("subject", COMPONENT_NS); + if (subject) + bridge->set_channel_topic(iid, subject->get_inner()); + } + else if (type == "error") + { + const XmlNode* error = stanza.get_child("error", COMPONENT_NS); + // Only a set of errors are considered “fatal”. If we encounter one of + // them, we purge (we disconnect the user from all the IRC servers). + // We consider this to be true, unless the error condition is + // specified and is not in the kickable_errors set + bool kickable_error = true; + if (error && error->has_children()) + { + const XmlNode* condition = error->get_last_child(); + if (kickable_errors.find(condition->get_name()) == kickable_errors.end()) + kickable_error = false; + } + if (kickable_error) + bridge->shutdown("Error from remote client"); + } + else if (type == "chat") + { + if (body && !body->get_inner().empty()) + { + // a message for nick!server + if (iid.is_user && !iid.get_local().empty()) + { + bridge->send_private_message(iid, body->get_inner()); + bridge->remove_preferred_from_jid(iid.get_local()); + } + else if (!iid.is_user && !to.resource.empty()) + { // a message for chan%server@biboumi/Nick or + // server@biboumi/Nick + // Convert that into a message to nick!server + Iid user_iid(utils::tolower(to.resource) + "!" + iid.get_server()); + bridge->send_private_message(user_iid, body->get_inner()); + bridge->set_preferred_from_jid(user_iid.get_local(), to_str); + } + else if (!iid.is_user && !iid.is_channel) + { // Message sent to the server JID + // Convert the message body into a raw IRC message + bridge->send_raw_message(iid.get_server(), body->get_inner()); + } + } + } + else if (iid.is_user) + this->send_invalid_user_error(to.local, from); + } catch (const IRCNotConnected& ex) + { + this->send_stanza_error("message", from, to_str, id, + "cancel", "remote-server-not-found", + "Not connected to IRC server "s + ex.hostname, + true); + } + stanza_error.disable(); +} + +// We MUST return an iq, whatever happens, except if the type is +// "result". +// To do this, we use a scopeguard. If an exception is raised somewhere, an +// iq of type error "internal-server-error" is sent. If we handle the +// request properly (by calling a function that registers an iq to be sent +// later, or that directly sends an iq), we disable the ScopeGuard. If we +// reach the end of the function without having disabled the scopeguard, we +// send a "feature-not-implemented" iq as a result. If an other kind of +// error is found (for example the feature is implemented in biboumi, but +// the request is missing some attribute) we can just change the values of +// error_type and error_name and return from the function (without disabling +// the scopeguard); an iq error will be sent +void BiboumiComponent::handle_iq(const Stanza& stanza) +{ + std::string id = stanza.get_tag("id"); + std::string from = stanza.get_tag("from"); + std::string to_str = stanza.get_tag("to"); + std::string type = stanza.get_tag("type"); + + if (from.empty()) { + log_warning("Received an iq without a 'from'. Ignoring."); + return; + } + if (id.empty() || to_str.empty() || type.empty()) + { + this->send_stanza_error("iq", from, this->served_hostname, id, + "modify", "bad-request", ""); + return; + } + + Bridge* bridge = this->get_user_bridge(from); + Jid to(to_str); + + // These two values will be used in the error iq sent if we don't disable + // the scopeguard. + std::string error_type("cancel"); + std::string error_name("internal-server-error"); + utils::ScopeGuard stanza_error([&](){ + this->send_stanza_error("iq", from, to_str, id, + error_type, error_name, ""); + }); + try { + if (type == "set") + { + const XmlNode* query; + if ((query = stanza.get_child("query", MUC_ADMIN_NS))) + { + const XmlNode* child = query->get_child("item", MUC_ADMIN_NS); + if (child) + { + std::string nick = child->get_tag("nick"); + std::string role = child->get_tag("role"); + std::string affiliation = child->get_tag("affiliation"); + if (!nick.empty()) + { + Iid iid(to.local); + if (role == "none") + { // This is a kick + std::string reason; + const XmlNode* reason_el = child->get_child("reason", MUC_ADMIN_NS); + if (reason_el) + reason = reason_el->get_inner(); + bridge->send_irc_kick(iid, nick, reason, id, from); + } + else + bridge->forward_affiliation_role_change(iid, nick, affiliation, role); + stanza_error.disable(); + } + } + } + else if ((query = stanza.get_child("command", ADHOC_NS))) + { + Stanza response("iq"); + response["to"] = from; + response["from"] = to_str; + response["id"] = id; + + // Depending on the 'to' jid in the request, we use one adhoc + // command handler or an other + Iid iid(to.local); + AdhocCommandsHandler* adhoc_handler; + if (!to.local.empty() && !iid.is_user && !iid.is_channel) + adhoc_handler = &this->irc_server_adhoc_commands_handler; + else if (!to.local.empty() && iid.is_channel) + adhoc_handler = &this->irc_channel_adhoc_commands_handler; + else + adhoc_handler = &this->adhoc_commands_handler; + + // Execute the command, if any, and get a result XmlNode that we + // insert in our response + XmlNode inner_node = adhoc_handler->handle_request(from, to_str, *query); + if (inner_node.get_child("error", ADHOC_NS)) + response["type"] = "error"; + else + response["type"] = "result"; + response.add_child(std::move(inner_node)); + this->send_stanza(response); + stanza_error.disable(); + } + } + else if (type == "get") + { + const XmlNode* query; + if ((query = stanza.get_child("query", DISCO_INFO_NS))) + { // Disco info + if (to_str == this->served_hostname) + { + const std::string node = query->get_tag("node"); + if (node.empty()) + { + // On the gateway itself + this->send_self_disco_info(id, from); + stanza_error.disable(); + } + } + } + else if ((query = stanza.get_child("query", VERSION_NS))) + { + Iid iid(to.local); + if (iid.is_user || + (iid.is_channel && !to.resource.empty())) + { + // Get the IRC user version + std::string target; + if (iid.is_user) + target = iid.get_local(); + else + target = to.resource; + bridge->send_irc_version_request(iid.get_server(), target, id, + from, to_str); + } + else + { + // On the gateway itself or on a channel + this->send_version(id, from, to_str); + } + stanza_error.disable(); + } + else if ((query = stanza.get_child("query", DISCO_ITEMS_NS))) + { + Iid iid(to.local); + const std::string node = query->get_tag("node"); + if (node == ADHOC_NS) + { + Jid from_jid(from); + if (to.local.empty()) + { // Get biboumi's adhoc commands + this->send_adhoc_commands_list(id, from, this->served_hostname, + (Config::get("admin", "") == + from_jid.bare()), + this->adhoc_commands_handler); + stanza_error.disable(); + } + else if (!iid.is_user && !iid.is_channel) + { // Get the server's adhoc commands + this->send_adhoc_commands_list(id, from, to_str, + (Config::get("admin", "") == + from_jid.bare()), + this->irc_server_adhoc_commands_handler); + stanza_error.disable(); + } + else if (!iid.is_user && iid.is_channel) + { // Get the channel's adhoc commands + this->send_adhoc_commands_list(id, from, to_str, + (Config::get("admin", "") == + from_jid.bare()), + this->irc_channel_adhoc_commands_handler); + stanza_error.disable(); + } + } + else if (node.empty() && !iid.is_user && !iid.is_channel) + { // Disco on an IRC server: get the list of channels + bridge->send_irc_channel_list_request(iid, id, from); + stanza_error.disable(); + } + } + else if ((query = stanza.get_child("ping", PING_NS))) + { + Iid iid(to.local); + if (iid.is_user) + { // Ping any user (no check on the nick done ourself) + bridge->send_irc_user_ping_request(iid.get_server(), + iid.get_local(), id, from, to_str); + } + else if (iid.is_channel && !to.resource.empty()) + { // Ping a room participant (we check if the nick is in the room) + bridge->send_irc_participant_ping_request(iid, + to.resource, id, from, to_str); + } + else + { // Ping a channel, a server or the gateway itself + bridge->on_gateway_ping(iid.get_server(), + id, from, to_str); + } + stanza_error.disable(); + } + } + else if (type == "result") + { + stanza_error.disable(); + const XmlNode* query; + if ((query = stanza.get_child("query", VERSION_NS))) + { + const XmlNode* name_node = query->get_child("name", VERSION_NS); + const XmlNode* version_node = query->get_child("version", VERSION_NS); + const XmlNode* os_node = query->get_child("os", VERSION_NS); + std::string name; + std::string version; + std::string os; + if (name_node) + name = name_node->get_inner() + " (through the biboumi gateway)"; + if (version_node) + version = version_node->get_inner(); + if (os_node) + os = os_node->get_inner(); + const Iid iid(to.local); + bridge->send_xmpp_version_to_irc(iid, name, version, os); + } + else + { + const auto it = this->waiting_iq.find(id); + if (it != this->waiting_iq.end()) + { + it->second(bridge, stanza); + this->waiting_iq.erase(it); + } + } + } + } + catch (const IRCNotConnected& ex) + { + this->send_stanza_error("iq", from, to_str, id, + "cancel", "remote-server-not-found", + "Not connected to IRC server "s + ex.hostname, + true); + stanza_error.disable(); + return; + } + error_type = "cancel"; + error_name = "feature-not-implemented"; +} + +Bridge* BiboumiComponent::get_user_bridge(const std::string& user_jid) +{ + auto bare_jid = Jid{user_jid}.bare(); + try + { + return this->bridges.at(bare_jid).get(); + } + catch (const std::out_of_range& exception) + { + this->bridges.emplace(bare_jid, std::make_unique<Bridge>(bare_jid, *this, this->poller)); + return this->bridges.at(bare_jid).get(); + } +} + +Bridge* BiboumiComponent::find_user_bridge(const std::string& full_jid) +{ + auto bare_jid = Jid{full_jid}.bare(); + try + { + return this->bridges.at(bare_jid).get(); + } + catch (const std::out_of_range& exception) + { + return nullptr; + } +} + +std::vector<Bridge*> BiboumiComponent::get_bridges() const +{ + std::vector<Bridge*> res; + for (auto it = this->bridges.begin(); it != this->bridges.end(); ++it) + res.push_back(it->second.get()); + return res; +} + +void BiboumiComponent::send_self_disco_info(const std::string& id, const std::string& jid_to) +{ + Stanza iq("iq"); + iq["type"] = "result"; + iq["id"] = id; + iq["to"] = jid_to; + iq["from"] = this->served_hostname; + XmlNode query("query"); + query["xmlns"] = DISCO_INFO_NS; + XmlNode identity("identity"); + identity["category"] = "conference"; + identity["type"] = "irc"; + identity["name"] = "Biboumi XMPP-IRC gateway"; + query.add_child(std::move(identity)); + for (const char* ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS}) + { + XmlNode feature("feature"); + feature["var"] = ns; + query.add_child(std::move(feature)); + } + iq.add_child(std::move(query)); + this->send_stanza(iq); +} + +void BiboumiComponent::send_iq_version_request(const std::string& from, + const std::string& jid_to) +{ + Stanza iq("iq"); + iq["type"] = "get"; + iq["id"] = "version_"s + this->next_id(); + iq["from"] = from + "@" + this->served_hostname; + iq["to"] = jid_to; + XmlNode query("query"); + query["xmlns"] = VERSION_NS; + iq.add_child(std::move(query)); + this->send_stanza(iq); +} + +void BiboumiComponent::send_ping_request(const std::string& from, + const std::string& jid_to, + const std::string& id) +{ + Stanza iq("iq"); + iq["type"] = "get"; + iq["id"] = id; + iq["from"] = from + "@" + this->served_hostname; + iq["to"] = jid_to; + XmlNode ping("ping"); + ping["xmlns"] = PING_NS; + iq.add_child(std::move(ping)); + this->send_stanza(iq); + + auto result_cb = [from, id](Bridge* bridge, const Stanza& stanza) + { + Jid to(stanza.get_tag("to")); + if (to.local != from) + { + log_error("Received a corresponding ping result, but the 'to' from " + "the response mismatches the 'from' of the request"); + } + else + bridge->send_irc_ping_result(from, id); + }; + this->waiting_iq[id] = result_cb; +} + +void BiboumiComponent::send_iq_room_list_result(const std::string& id, + const std::string& to_jid, + const std::string& from, + const std::vector<ListElement>& rooms_list) +{ + Stanza iq("iq"); + iq["from"] = from + "@" + this->served_hostname; + iq["to"] = to_jid; + iq["id"] = id; + iq["type"] = "result"; + XmlNode query("query"); + query["xmlns"] = DISCO_ITEMS_NS; + for (const auto& room: rooms_list) + { + XmlNode item("item"); + item["jid"] = room.channel + "%" + from + "@" + this->served_hostname; + query.add_child(std::move(item)); + } + iq.add_child(std::move(query)); + this->send_stanza(iq); +} diff --git a/src/xmpp/biboumi_component.hpp b/src/xmpp/biboumi_component.hpp new file mode 100644 index 0000000..24d768a --- /dev/null +++ b/src/xmpp/biboumi_component.hpp @@ -0,0 +1,109 @@ +#pragma once + + +#include <xmpp/xmpp_component.hpp> + +#include <bridge/bridge.hpp> + +#include <memory> +#include <string> +#include <map> + +struct ListElement; + +/** + * A callback called when the waited iq result is received (it is matched + * against the iq id) + */ +using iq_responder_callback_t = std::function<void(Bridge* bridge, const Stanza& stanza)>; + +/** + * Interact with the Biboumi Bridge + */ +class BiboumiComponent: public XmppComponent +{ +public: + explicit BiboumiComponent(std::shared_ptr<Poller> poller, const std::string& hostname, const std::string& secret); + ~BiboumiComponent() = default; + + BiboumiComponent(const BiboumiComponent&) = delete; + BiboumiComponent(BiboumiComponent&&) = delete; + BiboumiComponent& operator=(const BiboumiComponent&) = delete; + BiboumiComponent& operator=(BiboumiComponent&&) = delete; + + /** + * Returns the bridge for the given user. If it does not exist, return + * nullptr. + */ + Bridge* find_user_bridge(const std::string& full_jid); + /** + * Return a list of all the managed bridges. + */ + std::vector<Bridge*> get_bridges() const; + + /** + * Send a "close" message to all our connected peers. That message + * depends on the protocol used (this may be a QUIT irc message, or a + * <stream/>, etc). We may also directly close the connection, or we may + * wait for the remote peer to acknowledge it before closing. + */ + void shutdown(); + /** + * Run a check on all bridges, to remove all disconnected (socket is + * closed, or no channel is joined) IrcClients. Some kind of garbage collector. + */ + void clean(); + /** + * Send a result IQ with the gateway disco informations. + */ + void send_self_disco_info(const std::string& id, const std::string& jid_to); + /** + * Send an iq version request + */ + void send_iq_version_request(const std::string& from, + const std::string& jid_to); + /** + * Send a ping request + */ + void send_ping_request(const std::string& from, + const std::string& jid_to, + const std::string& id); + /** + * Send the channels list in one big stanza + */ + void send_iq_room_list_result(const std::string& id, const std::string& to_jid, + const std::string& from, + const std::vector<ListElement>& rooms_list); + /** + * Handle the various stanza types + */ + void handle_presence(const Stanza& stanza); + void handle_message(const Stanza& stanza); + void handle_iq(const Stanza& stanza); + +private: + /** + * Return the bridge associated with the bare JID. Create a new one + * if none already exist. + */ + Bridge* get_user_bridge(const std::string& user_jid); + + /** + * A map of id -> callback. When we want to wait for an iq result, we add + * the callback to this map, with the iq id as the key. When an iq result + * is received, we look for a corresponding callback in this map. If + * found, we call it and remove it. + */ + std::map<std::string, iq_responder_callback_t> waiting_iq; + + /** + * One bridge for each user of the component. Indexed by the user's bare + * jid + */ + std::unordered_map<std::string, std::unique_ptr<Bridge>> bridges; + + AdhocCommandsHandler irc_server_adhoc_commands_handler; + AdhocCommandsHandler irc_channel_adhoc_commands_handler; +}; + + |