diff options
Diffstat (limited to 'src/xmpp')
-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 |
4 files changed, 1399 insertions, 0 deletions
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; +}; + + |