Files
biboumi/src/xmpp/biboumi_component.cpp
T

603 lines
21 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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)
{
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)}
};
this->irc_server_adhoc_commands_handler.get_commands() = {
#ifdef USE_DATABASE
{"configure", AdhocCommand({&ConfigureIrcServerStep1, &ConfigureIrcServerStep2}, "Configure a few settings for that IRC server", 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 = 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.empty())
{
log_warning("Received an invalid presence stanza: tag 'from' is missing.");
return;
}
if (to_str.empty())
{
this->send_stanza_error("presence", from, this->served_hostname, id,
"modify", "bad-request", "Missing 'to' tag");
return;
}
Bridge* bridge = this->get_user_bridge(from);
Jid to(to_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, 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() : "");
}
else if (type == "unavailable")
{
const XmlNode* status = stanza.get_child("status", COMPONENT_NS);
bridge->leave_irc_channel(std::move(iid), status ? status->get_inner() : "");
}
}
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);
}
}
catch (const IRCNotConnected& ex)
{
this->send_stanza_error("presence", from, 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())
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
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 (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)
{
try
{
return this->bridges.at(user_jid).get();
}
catch (const std::out_of_range& exception)
{
this->bridges.emplace(user_jid, std::make_unique<Bridge>(user_jid, *this, this->poller));
return this->bridges.at(user_jid).get();
}
}
Bridge* BiboumiComponent::find_user_bridge(const std::string& user_jid)
{
try
{
return this->bridges.at(user_jid).get();
}
catch (const std::out_of_range& exception)
{
return nullptr;
}
}
std::list<Bridge*> BiboumiComponent::get_bridges() const
{
std::list<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 std::string& 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);
}