biboumi_component.cpp 22.5 KB
Newer Older
1 2 3 4 5 6 7
#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>
8
#include <xmpp/biboumi_adhoc_commands.hpp>
9 10 11 12 13 14 15 16 17 18 19
#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>
20
#include <biboumi.h>
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44

#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):
45
  XmppComponent(poller, hostname, secret),
46 47
  irc_server_adhoc_commands_handler(*this),
  irc_channel_adhoc_commands_handler(*this)
48 49 50 51 52 53 54 55
{
  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));

56
  this->adhoc_commands_handler.get_commands() = {
57 58
    {"ping", AdhocCommand({&PingStep1}, "Do a ping", false)},
    {"hello", AdhocCommand({&HelloStep1, &HelloStep2}, "Receive a custom greeting", false)},
59 60
    {"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)},
61 62
    {"reload", AdhocCommand({&Reload}, "Reload biboumi’s configuration", true)}
  };
63

64 65 66 67 68 69 70 71 72
#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

73 74
  this->irc_server_adhoc_commands_handler.get_commands() = {
#ifdef USE_DATABASE
75
    {"configure", configure_server_command},
76 77
#endif
  };
78
  this->irc_channel_adhoc_commands_handler.get_commands() = {
79
#ifdef USE_DATABASE
80
    {"configure", AdhocCommand({&ConfigureIrcChannelStep1, &ConfigureIrcChannelStep2}, "Configure a few settings for that IRC channel", false)},
81
#endif
82
  };
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
}

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)
{
108
  std::string from_str = stanza.get_tag("from");
109 110 111 112 113
  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
114
  if (from_str.empty())
115 116 117 118 119 120
    {
      log_warning("Received an invalid presence stanza: tag 'from' is missing.");
      return;
    }
  if (to_str.empty())
    {
121
      this->send_stanza_error("presence", from_str, this->served_hostname, id,
122 123 124 125
                              "modify", "bad-request", "Missing 'to' tag");
      return;
    }

126
  Bridge* bridge = this->get_user_bridge(from_str);
127
  Jid to(to_str);
128
  Jid from(from_str);
129
  Iid iid(to.local, bridge);
130 131 132 133 134 135 136 137 138 139

  // 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([&](){
140
      this->send_stanza_error("presence", from_str, to_str, id,
141 142 143
                              error_type, error_name, "");
        });

144
  try {
145
  if (iid.type == Iid::Type::Channel && !iid.get_server().empty())
146 147 148 149 150 151 152
    { // 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);
153 154
          const XmlNode* x = stanza.get_child("x", MUC_NS);
          const XmlNode* password = x ? x->get_child("password", MUC_NS): nullptr;
155 156
          bridge->join_irc_channel(iid, to.resource, password ? password->get_inner(): "",
                                   from.resource);
157 158 159
        }
      else if (type == "unavailable")
        {
160
          const XmlNode* status = stanza.get_child("status", COMPONENT_NS);
161
          bridge->leave_irc_channel(std::move(iid), status ? status->get_inner() : "", from.resource);
162 163 164 165 166 167
        }
    }
  else
    {
      // An user wants to join an invalid IRC channel, return a presence error to him
      if (type.empty())
168
        this->send_invalid_room_error(to.local, to.resource, from_str);
169
    }
170 171 172
  }
  catch (const IRCNotConnected& ex)
    {
173
      this->send_stanza_error("presence", from_str, to_str, id,
174 175 176 177
                              "cancel", "remote-server-not-found",
                              "Not connected to IRC server "s + ex.hostname,
                              true);
    }
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
  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);
194
  Iid iid(to.local, bridge);
195 196 197 198 199 200 201

  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, "");
    });
202
  const XmlNode* body = stanza.get_child("body", COMPONENT_NS);
203 204

  try {                         // catch IRCNotConnected exceptions
205
  if (type == "groupchat" && iid.type == Iid::Type::Channel)
206 207 208 209 210
    {
      if (body && !body->get_inner().empty())
        {
          bridge->send_channel_message(iid, body->get_inner());
        }
211
      const XmlNode* subject = stanza.get_child("subject", COMPONENT_NS);
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236
      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
237
          if (iid.type == Iid::Type::User && !iid.get_local().empty())
238 239 240 241
            {
              bridge->send_private_message(iid, body->get_inner());
              bridge->remove_preferred_from_jid(iid.get_local());
            }
242
          else if (iid.type != Iid::Type::User && !to.resource.empty())
243 244 245
            { // a message for chan%server@biboumi/Nick or
              // server@biboumi/Nick
              // Convert that into a message to nick!server
246
              Iid user_iid(utils::tolower(to.resource), iid.get_server(), Iid::Type::User);
247 248 249
              bridge->send_private_message(user_iid, body->get_inner());
              bridge->set_preferred_from_jid(user_iid.get_local(), to_str);
            }
250
          else if (iid.type == Iid::Type::Server)
louiz’'s avatar
louiz’ committed
251 252 253 254
            { // 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());
            }
255 256
        }
    }
257
  else if (iid.type == Iid::Type::User)
258
    this->send_invalid_user_error(to.local, from);
259 260 261 262 263 264 265
  } 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);
    }
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287
  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");

288 289
  if (from.empty()) {
    log_warning("Received an iq without a 'from'. Ignoring.");
290
    return;
291
  }
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
  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, "");
    });
310
  try {
311 312
  if (type == "set")
    {
313
      const XmlNode* query;
314 315 316 317 318 319 320 321 322 323
      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())
                {
324
                  Iid iid(to.local, {});
325 326 327
                  if (role == "none")
                    {               // This is a kick
                      std::string reason;
328
                      const XmlNode* reason_el = child->get_child("reason", MUC_ADMIN_NS);
329 330 331 332 333 334 335 336 337 338 339 340 341 342
                      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;
343
          response["from"] = to_str;
344
          response["id"] = id;
345 346 347

          // Depending on the 'to' jid in the request, we use one adhoc
          // command handler or an other
348
          Iid iid(to.local, {});
349
          AdhocCommandsHandler* adhoc_handler;
350
          if (to.local.empty())
351
            adhoc_handler = &this->adhoc_commands_handler;
352 353 354 355 356 357 358
          else
          {
            if (iid.type == Iid::Type::Server)
              adhoc_handler = &this->irc_server_adhoc_commands_handler;
            else
              adhoc_handler = &this->irc_channel_adhoc_commands_handler;
          }
359 360 361
          // 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);
362 363 364 365 366 367 368 369 370 371 372
          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")
    {
373
      const XmlNode* query;
374 375 376 377 378 379 380 381 382 383 384 385 386 387 388
      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)))
        {
389 390
          Iid iid(to.local, bridge);
          if (iid.type != Iid::Type::Server && !to.resource.empty())
391 392 393
            {
              // Get the IRC user version
              std::string target;
394
              if (iid.type == Iid::Type::User)
395 396 397 398 399 400 401 402 403 404 405 406 407 408 409
                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)))
        {
410
          Iid iid(to.local, bridge);
411 412 413
          const std::string node = query->get_tag("node");
          if (node == ADHOC_NS)
            {
414
              Jid from_jid(from);
415 416 417 418
              if (to.local.empty())
                {               // Get biboumi's adhoc commands
                  this->send_adhoc_commands_list(id, from, this->served_hostname,
                                                 (Config::get("admin", "") ==
419
                                                  from_jid.bare()),
420 421 422
                                                 this->adhoc_commands_handler);
                  stanza_error.disable();
                }
423
              else if (iid.type == Iid::Type::Server)
424 425 426
                {               // Get the server's adhoc commands
                  this->send_adhoc_commands_list(id, from, to_str,
                                                 (Config::get("admin", "") ==
427
                                                  from_jid.bare()),
428 429 430
                                                 this->irc_server_adhoc_commands_handler);
                  stanza_error.disable();
                }
431
              else if (iid.type == Iid::Type::Channel)
432 433 434 435 436 437 438
                {               // 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();
                }
439
            }
440
          else if (node.empty() && iid.type == Iid::Type::Server)
441 442 443 444 445 446 447
            { // 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)))
        {
448 449
          Iid iid(to.local, bridge);
          if (iid.type == Iid::Type::User)
450 451 452 453
            { // 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);
            }
454
          else if (iid.type == Iid::Type::Channel && !to.resource.empty())
455 456 457 458 459 460 461 462 463 464 465 466 467 468 469
            { // 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();
470
      const XmlNode* query;
471 472
      if ((query = stanza.get_child("query", VERSION_NS)))
        {
473 474 475
          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);
476 477 478 479 480 481 482 483 484
          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();
485
          const Iid iid(to.local, bridge);
486 487 488 489 490 491 492 493 494 495 496 497
          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);
            }
        }
    }
498 499 500 501 502 503 504 505 506 507
  }
  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;
    }
508 509 510 511 512 513
  error_type = "cancel";
  error_name = "feature-not-implemented";
}

Bridge* BiboumiComponent::get_user_bridge(const std::string& user_jid)
{
514
  auto bare_jid = Jid{user_jid}.bare();
515 516
  try
    {
517
      return this->bridges.at(bare_jid).get();
518 519 520
    }
  catch (const std::out_of_range& exception)
    {
521 522
      this->bridges.emplace(bare_jid, std::make_unique<Bridge>(bare_jid, *this, this->poller));
      return this->bridges.at(bare_jid).get();
523 524 525
    }
}

526
Bridge* BiboumiComponent::find_user_bridge(const std::string& full_jid)
527
{
528
  auto bare_jid = Jid{full_jid}.bare();
529 530
  try
    {
531
      return this->bridges.at(bare_jid).get();
532 533 534 535 536 537 538
    }
  catch (const std::out_of_range& exception)
    {
      return nullptr;
    }
}

louiz’'s avatar
louiz’ committed
539
std::vector<Bridge*> BiboumiComponent::get_bridges() const
540
{
louiz’'s avatar
louiz’ committed
541
  std::vector<Bridge*> res;
542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560
  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));
561
  for (const char* ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS})
562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585
    {
      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,
586 587
                                         const std::string& jid_to,
                                         const std::string& id)
588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607
{
  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
608
        bridge->send_irc_ping_result({from, bridge}, id);
609 610 611 612 613
    };
  this->waiting_iq[id] = result_cb;
}

void BiboumiComponent::send_iq_room_list_result(const std::string& id,
614
                                             const std::string& to_jid,
615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633
                                             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);
}