#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef SYSTEMD_FOUND # include #endif #include #include #include using namespace std::string_literals; static std::set 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, 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.add_command("ping", {{&PingStep1}, "Do a ping", false}); this->adhoc_commands_handler.add_command("hello", {{&HelloStep1, &HelloStep2}, "Receive a custom greeting", false}); this->adhoc_commands_handler.add_command("disconnect-user", {{&DisconnectUserStep1, &DisconnectUserStep2}, "Disconnect selected users from the gateway", true}); this->adhoc_commands_handler.add_command("disconnect-from-irc-server", {{&DisconnectUserFromServerStep1, &DisconnectUserFromServerStep2, &DisconnectUserFromServerStep3}, "Disconnect from the selected IRC servers", false}); this->adhoc_commands_handler.add_command("reload", {{&Reload}, "Reload biboumi’s configuration", true}); AdhocCommand get_irc_connection_info{{&GetIrcConnectionInfoStep1}, "Returns various information about your connection to this IRC server.", false}; if (!Config::get("fixed_irc_server", "").empty()) this->adhoc_commands_handler.add_command("get-irc-connection-info", get_irc_connection_info); else this->irc_server_adhoc_commands_handler.add_command("get-irc-connection-info", get_irc_connection_info); #ifdef USE_DATABASE AdhocCommand configure_server_command({&ConfigureIrcServerStep1, &ConfigureIrcServerStep2}, "Configure a few settings for that IRC server", false); AdhocCommand configure_global_command({&ConfigureGlobalStep1, &ConfigureGlobalStep2}, "Configure a few settings", false); if (!Config::get("fixed_irc_server", "").empty()) this->adhoc_commands_handler.add_command("configure", configure_server_command); else this->adhoc_commands_handler.add_command("configure", configure_global_command); this->irc_server_adhoc_commands_handler.add_command("configure", configure_server_command); this->irc_channel_adhoc_commands_handler.add_command("configure", {{&ConfigureIrcChannelStep1, &ConfigureIrcChannelStep2}, "Configure a few settings for that IRC channel", false}); #endif } void BiboumiComponent::shutdown() { for (auto& pair: this->bridges) pair.second->shutdown("Gateway shutdown"); #ifdef USE_DATABASE for (const Database::RosterItem& roster_item: Database::get_full_roster()) { this->send_presence_to_contact(roster_item.col(), roster_item.col(), "unavailable"); } #endif } 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, bridge); // 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, &from_str, &to_str, &id, &error_type, &error_name](){ this->send_stanza_error("presence", from_str, to_str, id, error_type, error_name, ""); }); try { if (iid.type == Iid::Type::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, from.resource); const XmlNode* x = stanza.get_child("x", MUC_NS); const XmlNode* password = x ? x->get_child("password", MUC_NS): nullptr; const XmlNode* history = x ? x->get_child("history", MUC_NS): nullptr; HistoryLimit history_limit; if (history) { const auto seconds = history->get_tag("seconds"); if (!seconds.empty()) { const auto now = std::chrono::system_clock::now(); std::time_t timestamp = std::chrono::system_clock::to_time_t(now); int int_seconds = std::atoi(seconds.data()); timestamp -= int_seconds; history_limit.since = utils::to_string(timestamp); } const auto since = history->get_tag("since"); if (!since.empty()) history_limit.since = since; const auto maxstanzas = history->get_tag("maxstanzas"); if (!maxstanzas.empty()) history_limit.stanzas = std::atoi(maxstanzas.data()); // Ignore any other value, because this is too complex to implement, // so I won’t do it. if (history->get_tag("maxchars") == "0") history_limit.stanzas = 0; } bridge->join_irc_channel(iid, to.resource, password ? password->get_inner(): "", from.resource, history_limit); } 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 if (iid.type == Iid::Type::Server || iid.type == Iid::Type::None) { if (type == "subscribe") { // Auto-accept any subscription request for an IRC server this->send_presence_to_contact(to_str, from.bare(), "subscribed", id); if (iid.type == Iid::Type::None || bridge->find_irc_client(iid.get_server())) this->send_presence_to_contact(to_str, from.bare(), ""); this->send_presence_to_contact(to_str, from.bare(), "subscribe"); #ifdef USE_DATABASE if (!Database::has_roster_item(to_str, from.bare())) Database::add_roster_item(to_str, from.bare()); #endif } else if (type == "unsubscribe") { this->send_presence_to_contact(to_str, from.bare(), "unavailable", id); this->send_presence_to_contact(to_str, from.bare(), "unsubscribed"); this->send_presence_to_contact(to_str, from.bare(), "unsubscribe"); #ifdef USE_DATABASE const bool res = Database::has_roster_item(to_str, from.bare()); if (res) Database::delete_roster_item(to_str, from.bare()); #endif } else if (type == "probe") { if ((iid.type == Iid::Type::Server && bridge->find_irc_client(iid.get_server())) || iid.type == Iid::Type::None) { #ifdef USE_DATABASE if (Database::has_roster_item(to_str, from.bare())) #endif this->send_presence_to_contact(to_str, from.bare(), ""); #ifdef USE_DATABASE else // rfc 6121 4.3.2.1 this->send_presence_to_contact(to_str, from.bare(), "unsubscribed"); #endif } } else if (type.empty()) { // We just receive a presence from someone (as the result of a probe, // or a directed presence, or a normal presence change) if (iid.type == Iid::Type::None) this->send_presence_to_contact(to_str, from.bare(), ""); } } else if (iid.type == Iid::Type::User) { // Do nothing yet } } catch (const IRCNotConnected& ex) { if (type != "unavailable") this->send_stanza_error("presence", from_str, to_str, id, "cancel", "remote-server-not-found", "Not connected to IRC server " + ex.hostname, true); } stanza_error.disable(); } void BiboumiComponent::handle_message(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"); if (from_str.empty()) return; if (type.empty()) type = "normal"; Bridge* bridge = this->get_user_bridge(from_str); Jid from(from_str); Jid to(to_str); Iid iid(to.local, bridge); std::string error_type("cancel"); std::string error_name("internal-server-error"); utils::ScopeGuard stanza_error([this, &from_str, &to_str, &id, &error_type, &error_name](){ this->send_stanza_error("message", from_str, to_str, id, error_type, error_name, ""); }); const XmlNode* body = stanza.get_child("body", COMPONENT_NS); try { // catch IRCNotConnected exceptions if (type == "groupchat" && iid.type == Iid::Type::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 that resource 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->remove_resource(from.resource, "Error from remote client"); } else if (type == "chat") { if (body && !body->get_inner().empty()) { const auto fixed_irc_server = Config::get("fixed_irc_server", ""); // a message for nick!server if (iid.type == Iid::Type::User && !iid.get_local().empty()) { bridge->send_private_message(iid, body->get_inner()); bridge->remove_preferred_from_jid(iid.get_local()); } else if (iid.type != Iid::Type::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(), Iid::Type::User); bridge->send_private_message(user_iid, body->get_inner()); bridge->set_preferred_from_jid(user_iid.get_local(), to_str); } else if (iid.type == Iid::Type::Server) bridge->send_raw_message(iid.get_server(), body->get_inner()); else if (iid.type == Iid::Type::None && !fixed_irc_server.empty()) { // Message sent to the server JID // Convert the message body into a raw IRC message bridge->send_raw_message(fixed_irc_server, body->get_inner()); } } } else if (type == "normal" && iid.type == Iid::Type::Channel) { if (const XmlNode* x = stanza.get_child("x", MUC_USER_NS)) if (const XmlNode* invite = x->get_child("invite", MUC_USER_NS)) { const auto invite_to = invite->get_tag("to"); if (!invite_to.empty()) { Jid invited_jid{invite_to}; if (invited_jid.domain == this->get_served_hostname() || invited_jid.local.empty()) bridge->send_irc_invitation(iid, invite_to); else this->send_invitation_from_fulljid(std::to_string(iid), invite_to, from_str); } } } } catch (const IRCNotConnected& ex) { this->send_stanza_error("message", from_str, to_str, id, "cancel", "remote-server-not-found", "Not connected to IRC server " + ex.hostname, true); } stanza_error.disable(); } // We MUST return an iq, whatever happens, except if the type is // "result" or "error". // 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, &from, &to_str, &id, &error_type, &error_name](){ 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, from, nick, affiliation, role, id); 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()) adhoc_handler = &this->adhoc_commands_handler; else { if (iid.type == Iid::Type::Server) adhoc_handler = &this->irc_server_adhoc_commands_handler; else if (iid.type == Iid::Type::Channel && to.resource.empty()) adhoc_handler = &this->irc_channel_adhoc_commands_handler; else { error_name = "feature-not-implemented"; return; } } // 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(); } #ifdef USE_DATABASE else if ((query = stanza.get_child("query", MAM_NS))) { try { if (this->handle_mam_request(stanza)) stanza_error.disable(); } catch (const Database::RecordNotFound& exc) { error_name = "item-not-found"; return; } } else if ((query = stanza.get_child("query", MUC_OWNER_NS))) { if (this->handle_room_configuration_form(*query, from, to, id)) stanza_error.disable(); } #endif } else if (type == "get") { const XmlNode* query; if ((query = stanza.get_child("query", DISCO_INFO_NS))) { // Disco info Iid iid(to.local, {'#', '&'}); const std::string node = query->get_tag("node"); if (to_str == this->served_hostname) { if (node.empty()) { // On the gateway itself this->send_self_disco_info(id, from); stanza_error.disable(); } } else if (iid.type == Iid::Type::Server) { if (node.empty()) { this->send_irc_server_disco_info(id, from, to_str); stanza_error.disable(); } } else if (iid.type == Iid::Type::Channel && to.resource.empty()) { if (node.empty()) { this->send_irc_channel_disco_info(id, from, to_str); stanza_error.disable(); } else if (node == MUC_TRAFFIC_NS) { this->send_irc_channel_muc_traffic_info(id, from, to_str); stanza_error.disable(); } } } else if ((query = stanza.get_child("query", VERSION_NS))) { Iid iid(to.local, bridge); if ((iid.type == Iid::Type::Channel && !to.resource.empty()) || (iid.type == Iid::Type::User)) { // Get the IRC user version std::string target; if (iid.type == Iid::Type::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, bridge); 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.type == Iid::Type::Server) { // 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.type == Iid::Type::Channel && to.resource.empty()) { // 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 // “to” is a MUC user, not the room itself error_name = "feature-not-implemented"; } else if (node.empty() && iid.type == Iid::Type::Server) { // Disco on an IRC server: get the list of channels ResultSetInfo rs_info; const XmlNode* set_node = query->get_child("set", RSM_NS); if (set_node) { const XmlNode* after = set_node->get_child("after", RSM_NS); if (after) rs_info.after = after->get_inner(); const XmlNode* before = set_node->get_child("before", RSM_NS); if (before) rs_info.before = before->get_inner(); const XmlNode* max = set_node->get_child("max", RSM_NS); if (max) rs_info.max = std::atoi(max->get_inner().data()); } if (rs_info.max == -1) rs_info.max = 100; bridge->send_irc_channel_list_request(iid, id, from, std::move(rs_info)); stanza_error.disable(); } } else if ((query = stanza.get_child("ping", PING_NS))) { Iid iid(to.local, bridge); if (iid.type == Iid::Type::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.type == Iid::Type::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(); } #ifdef USE_DATABASE else if ((query = stanza.get_child("query", MUC_OWNER_NS))) { if (this->handle_room_configuration_form_request(from, to, id)) stanza_error.disable(); } #endif } 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); 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); } } } else if (type == "error") { stanza_error.disable(); 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 " + ex.hostname, true); stanza_error.disable(); return; } error_type = "cancel"; error_name = "feature-not-implemented"; } #ifdef USE_DATABASE bool BiboumiComponent::handle_mam_request(const Stanza& stanza) { std::string id = stanza.get_tag("id"); Jid from(stanza.get_tag("from")); Jid to(stanza.get_tag("to")); const XmlNode* query = stanza.get_child("query", MAM_NS); Iid iid(to.local, {'#', '&'}); if (query && iid.type == Iid::Type::Channel && to.resource.empty()) { const std::string query_id = query->get_tag("queryid"); std::string start; std::string end; const XmlNode* x = query->get_child("x", DATAFORM_NS); if (x) { const XmlNode* value; const auto fields = x->get_children("field", DATAFORM_NS); for (const auto& field: fields) { if (field->get_tag("var") == "start") { value = field->get_child("value", DATAFORM_NS); if (value) start = value->get_inner(); } else if (field->get_tag("var") == "end") { value = field->get_child("value", DATAFORM_NS); if (value) end = value->get_inner(); } } } const XmlNode* set = query->get_child("set", RSM_NS); int limit = -1; Id::real_type after_id{Id::unset_value}; if (set) { const XmlNode* max = set->get_child("max", RSM_NS); if (max) limit = std::atoi(max->get_inner().data()); const XmlNode* after = set->get_child("after", RSM_NS); if (after) { auto after_record = Database::get_muc_log(from.bare(), iid.get_local(), iid.get_server(), after->get_inner(), start, end); after_id = after_record.col(); } } // Do not send more than 100 messages, even if the client asked for more, // or if it didn’t specify any limit. // 101 is just a trick to know if there are more available messages. // If our query returns 101 message, we know it’s incomplete, but we // still send only 100 if ((limit == -1 && start.empty() && end.empty()) || limit > 100) limit = 101; auto lines = Database::get_muc_logs(from.bare(), iid.get_local(), iid.get_server(), limit, start, end, after_id); bool complete = true; if (lines.size() > 100) { complete = false; lines.erase(lines.begin(), std::prev(lines.end(), 100)); } for (const Database::MucLogLine& line: lines) { if (!line.col().empty()) this->send_archived_message(line, to.full(), from.full(), query_id); } { auto fin_ptr = std::make_unique("fin"); { XmlNode& fin = *(fin_ptr.get()); fin["xmlns"] = MAM_NS; if (complete) fin["complete"] = "true"; XmlSubNode set(fin, "set"); set["xmlns"] = RSM_NS; if (!lines.empty()) { XmlSubNode first(set, "first"); first["index"] = "0"; first.set_inner(lines[0].col()); XmlSubNode last(set, "last"); last.set_inner(lines[lines.size() - 1].col()); } } this->send_iq_result_full_jid(id, from.full(), to.full(), std::move(fin_ptr)); } return true; } return false; } void BiboumiComponent::send_archived_message(const Database::MucLogLine& log_line, const std::string& from, const std::string& to, const std::string& queryid) { Stanza message("message"); { message["from"] = from; message["to"] = to; XmlSubNode result(message, "result"); result["xmlns"] = MAM_NS; if (!queryid.empty()) result["queryid"] = queryid; result["id"] = log_line.col(); XmlSubNode forwarded(result, "forwarded"); forwarded["xmlns"] = FORWARD_NS; XmlSubNode delay(forwarded, "delay"); delay["xmlns"] = DELAY_NS; delay["stamp"] = utils::to_string(log_line.col()); XmlSubNode submessage(forwarded, "message"); submessage["xmlns"] = CLIENT_NS; submessage["from"] = from + "/" + log_line.col(); submessage["type"] = "groupchat"; XmlSubNode body(submessage, "body"); body.set_inner(log_line.col()); } this->send_stanza(message); } bool BiboumiComponent::handle_room_configuration_form_request(const std::string& from, const Jid& to, const std::string& id) { Iid iid(to.local, {'#', '&'}); if (iid.type != Iid::Type::Channel || !to.resource.empty()) return false; Stanza iq("iq"); { iq["from"] = to.full(); iq["to"] = from; iq["id"] = id; iq["type"] = "result"; XmlSubNode query(iq, "query"); query["xmlns"] = MUC_OWNER_NS; Jid requester(from); insert_irc_channel_configuration_form(query, requester, to); } this->send_stanza(iq); return true; } bool BiboumiComponent::handle_room_configuration_form(const XmlNode& query, const std::string &from, const Jid &to, const std::string &id) { Iid iid(to.local, {'#', '&'}); if (iid.type != Iid::Type::Channel || !to.resource.empty()) return false; Jid requester(from); if (!handle_irc_channel_configuration_form(*this, query, requester, to)) return false; Stanza iq("iq"); iq["type"] = "result"; iq["from"] = to.full(); iq["to"] = from; iq["id"] = id; this->send_stanza(iq); return true; } #endif 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) { return this->bridges.emplace(bare_jid, std::make_unique(bare_jid, *this, this->poller)).first->second.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 BiboumiComponent::get_bridges() const { std::vector res; for (const auto& bridge: this->bridges) res.push_back(bridge.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; XmlSubNode query(iq, "query"); query["xmlns"] = DISCO_INFO_NS; XmlSubNode identity(query, "identity"); identity["category"] = "conference"; identity["type"] = "irc"; identity["name"] = "Biboumi XMPP-IRC gateway"; for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS}) { XmlSubNode feature(query, "feature"); feature["var"] = ns; } } this->send_stanza(iq); } void BiboumiComponent::send_irc_server_disco_info(const std::string& id, const std::string& jid_to, const std::string& jid_from) { Jid from(jid_from); Stanza iq("iq"); { iq["type"] = "result"; iq["id"] = id; iq["to"] = jid_to; iq["from"] = jid_from; XmlSubNode query(iq, "query"); query["xmlns"] = DISCO_INFO_NS; XmlSubNode identity(query, "identity"); identity["category"] = "conference"; identity["type"] = "irc"; identity["name"] = "IRC server " + from.local + " over Biboumi"; for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS}) { XmlSubNode feature(query, "feature"); feature["var"] = ns; } } this->send_stanza(iq); } void BiboumiComponent::send_irc_channel_muc_traffic_info(const std::string& id, const std::string& jid_to, const std::string& jid_from) { Stanza iq("iq"); { iq["type"] = "result"; iq["id"] = id; iq["from"] = jid_from; iq["to"] = jid_to; XmlSubNode query(iq, "query"); query["xmlns"] = DISCO_INFO_NS; query["node"] = MUC_TRAFFIC_NS; // We drop all “special” traffic (like xhtml-im, chatstates, etc), so // don’t include any } this->send_stanza(iq); } void BiboumiComponent::send_irc_channel_disco_info(const std::string& id, const std::string& jid_to, const std::string& jid_from) { Jid from(jid_from); Iid iid(from.local, {}); Stanza iq("iq"); { iq["type"] = "result"; iq["id"] = id; iq["to"] = jid_to; iq["from"] = jid_from; XmlSubNode query(iq, "query"); query["xmlns"] = DISCO_INFO_NS; XmlSubNode identity(query, "identity"); identity["category"] = "conference"; identity["type"] = "irc"; identity["name"] = "IRC channel " + iid.get_local() + " from server " + iid.get_server() + " over biboumi"; for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS}) { XmlSubNode feature(query, "feature"); feature["var"] = ns; } } 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; XmlSubNode ping(iq, "ping"); ping["xmlns"] = PING_NS; } 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"); return; } const std::string type = stanza.get_tag("type"); const XmlNode* error = stanza.get_child("error", COMPONENT_NS); // Check if what we receive is considered a valid response. And yes, those errors are valid responses if (type == "result" || (type == "error" && error && (error->get_child("feature-not-implemented", STANZA_NS) || error->get_child("service-unavailable", STANZA_NS)))) bridge->send_irc_ping_result({from, bridge}, 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 ChannelList& channel_list, std::vector::const_iterator begin, std::vector::const_iterator end, const ResultSetInfo& rs_info) { Stanza iq("iq"); { iq["from"] = from + "@" + this->served_hostname; iq["to"] = to_jid; iq["id"] = id; iq["type"] = "result"; XmlSubNode query(iq, "query"); query["xmlns"] = DISCO_ITEMS_NS; for (auto it = begin; it != end; ++it) { XmlSubNode item(query, "item"); std::string channel_name = it->channel; xep0106::encode(channel_name); item["jid"] = channel_name + "@" + this->served_hostname; } if ((rs_info.max >= 0 || !rs_info.after.empty() || !rs_info.before.empty())) { XmlSubNode set_node(query, "set"); set_node["xmlns"] = RSM_NS; if (begin != channel_list.channels.cend()) { XmlSubNode first_node(set_node, "first"); first_node["index"] = std::to_string(std::distance(channel_list.channels.cbegin(), begin)); first_node.set_inner(begin->channel + "@" + this->served_hostname); } if (end != channel_list.channels.cbegin()) { XmlSubNode last_node(set_node, "last"); last_node.set_inner(std::prev(end)->channel + "@" + this->served_hostname); } if (channel_list.complete) { XmlSubNode count_node(set_node, "count"); count_node.set_inner(std::to_string(channel_list.channels.size())); } } } this->send_stanza(iq); } void BiboumiComponent::send_invitation(const std::string& room_target, const std::string& jid_to, const std::string& author_nick) { if (author_nick.empty()) this->send_invitation_from_fulljid(room_target, jid_to, room_target + "@" + this->served_hostname); else this->send_invitation_from_fulljid(room_target, jid_to, room_target + "@" + this->served_hostname + "/" + author_nick); } void BiboumiComponent::send_invitation_from_fulljid(const std::string& room_target, const std::string& jid_to, const std::string& from) { Stanza message("message"); { message["from"] = room_target + "@" + this->served_hostname; message["to"] = jid_to; XmlSubNode x(message, "x"); x["xmlns"] = MUC_USER_NS; XmlSubNode invite(x, "invite"); invite["from"] = from; } this->send_stanza(message); } void BiboumiComponent::accept_subscription(const std::string& from, const std::string& to) { Stanza presence("presence"); presence["from"] = from; presence["to"] = to; presence["id"] = this->next_id(); presence["type"] = "subscribed"; this->send_stanza(presence); } void BiboumiComponent::ask_subscription(const std::string& from, const std::string& to) { Stanza presence("presence"); presence["from"] = from; presence["to"] = to; presence["id"] = this->next_id(); presence["type"] = "subscribe"; this->send_stanza(presence); } void BiboumiComponent::send_presence_to_contact(const std::string& from, const std::string& to, const std::string& type, const std::string& id) { Stanza presence("presence"); presence["from"] = from; presence["to"] = to; if (!type.empty()) presence["type"] = type; if (!id.empty()) presence["id"] = id; this->send_stanza(presence); } void BiboumiComponent::on_irc_client_connected(const std::string& irc_hostname, const std::string& jid) { #ifdef USE_DATABASE const auto local_jid = irc_hostname + "@" + this->served_hostname; if (Database::has_roster_item(local_jid, jid)) this->send_presence_to_contact(local_jid, jid, ""); #else (void)irc_hostname; (void)jid; #endif } void BiboumiComponent::on_irc_client_disconnected(const std::string& irc_hostname, const std::string& jid) { #ifdef USE_DATABASE const auto local_jid = irc_hostname + "@" + this->served_hostname; if (Database::has_roster_item(local_jid, jid)) this->send_presence_to_contact(irc_hostname + "@" + this->served_hostname, jid, "unavailable"); #else (void)irc_hostname; (void)jid; #endif } void BiboumiComponent::after_handshake() { XmppComponent::after_handshake(); #ifdef USE_DATABASE const auto contacts = Database::get_contact_list(this->get_served_hostname()); for (const Database::RosterItem& roster_item: contacts) { const auto remote_jid = roster_item.col(); // In response, we will receive a presence indicating the // contact is online, to which we will respond with our own // presence. // If the contact removed us from their roster while we were // offline, we will receive an unsubscribed presence, letting us // stay in sync. this->send_presence_to_contact(this->get_served_hostname(), remote_jid, "probe"); } #endif }