biboumi_component.cpp 37.6 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
#include <bridge/list_element.hpp>
#include <config/config.hpp>
11 12
#include <utils/time.hpp>
#include <xmpp/jid.hpp>
13 14 15 16

#include <stdexcept>
#include <iostream>

17
#include <cstdlib>
18

19
#include <biboumi.h>
20 21 22 23 24

#ifdef SYSTEMD_FOUND
# include <systemd/sd-daemon.h>
#endif

25
#include <database/database.hpp>
26
#include <bridge/result_set_management.hpp>
27

28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
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"
    };


44
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 57 58
  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});
59
  this->adhoc_commands_handler.add_command("disconnect-from-irc-server", {{&DisconnectUserFromServerStep1, &DisconnectUserFromServerStep2, &DisconnectUserFromServerStep3}, "Disconnect from the selected IRC servers", false});
60
  this->adhoc_commands_handler.add_command("reload", {{&Reload}, "Reload biboumi’s configuration", true});
61

62 63 64 65 66 67
  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);

68 69
#ifdef USE_DATABASE
  AdhocCommand configure_server_command({&ConfigureIrcServerStep1, &ConfigureIrcServerStep2}, "Configure a few settings for that IRC server", false);
70
  AdhocCommand configure_global_command({&ConfigureGlobalStep1, &ConfigureGlobalStep2}, "Configure a few settings", false);
71

72
  if (!Config::get("fixed_irc_server", "").empty())
73
    this->adhoc_commands_handler.add_command("configure", configure_server_command);
74
  else
75
    this->adhoc_commands_handler.add_command("configure", configure_global_command);
76

77 78
  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});
79
#endif
80 81 82 83
}

void BiboumiComponent::shutdown()
{
84 85
  for (auto& pair: this->bridges)
    pair.second->shutdown("Gateway shutdown");
86
#ifdef USE_DATABASE
87
  for (const Database::RosterItem& roster_item: Database::get_full_roster())
88 89 90 91 92 93
    {
      this->send_presence_to_contact(roster_item.col<Database::LocalJid>(),
                                     roster_item.col<Database::RemoteJid>(),
                                     "unavailable");
    }
#endif
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
}

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

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

  // 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");
louiz’'s avatar
louiz’ committed
142
  utils::ScopeGuard stanza_error([this, &from_str, &to_str, &id, &error_type, &error_name](){
143
      this->send_stanza_error("presence", from_str, to_str, id,
144 145 146
                              error_type, error_name, "");
        });

147
  try {
148
  if (iid.type == Iid::Type::Channel && !iid.get_server().empty())
149 150 151 152 153 154
    { // 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)
155
            bridge->send_irc_nick_change(iid, to.resource, from.resource);
156 157
          const XmlNode* x = stanza.get_child("x", MUC_NS);
          const XmlNode* password = x ? x->get_child("password", MUC_NS): nullptr;
158 159
          bridge->join_irc_channel(iid, to.resource, password ? password->get_inner(): "",
                                   from.resource);
160 161 162
        }
      else if (type == "unavailable")
        {
163
          const XmlNode* status = stanza.get_child("status", COMPONENT_NS);
164
          bridge->leave_irc_channel(std::move(iid), status ? status->get_inner() : "", from.resource);
165 166
        }
    }
louiz’'s avatar
louiz’ committed
167 168 169 170
  else if (iid.type == Iid::Type::Server || iid.type == Iid::Type::None)
    {
      if (type == "subscribe")
        { // Auto-accept any subscription request for an IRC server
171
          this->send_presence_to_contact(to_str, from.bare(), "subscribed", id);
172
          if (iid.type == Iid::Type::None || bridge->find_irc_client(iid.get_server()))
173 174 175 176 177 178 179 180 181
            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")
        {
182
          this->send_presence_to_contact(to_str, from.bare(), "unavailable", id);
183
          this->send_presence_to_contact(to_str, from.bare(), "unsubscribed");
184
          this->send_presence_to_contact(to_str, from.bare(), "unsubscribe");
185 186 187 188 189 190 191 192 193
#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.empty())
        { // We just receive a presence from someone (as the result of a probe,
          // or a directed presence, or a normal presence change)
194 195
          if (iid.type == Iid::Type::None)
            this->send_presence_to_contact(to_str, from.bare(), "");
louiz’'s avatar
louiz’ committed
196 197
        }
    }
198 199
  else
    {
louiz’'s avatar
louiz’ committed
200
      // A user wants to join an invalid IRC channel, return a presence error to him/her
201
      if (type.empty())
202
        this->send_invalid_room_error(to.local, to.resource, from_str);
203
    }
204 205 206
  }
  catch (const IRCNotConnected& ex)
    {
louiz’'s avatar
louiz’ committed
207
      if (type != "unavailable")
208 209
        this->send_stanza_error("presence", from_str, to_str, id,
                                "cancel", "remote-server-not-found",
210
                                "Not connected to IRC server " + ex.hostname,
211
                                true);
212
    }
213 214 215 216 217
  stanza_error.disable();
}

void BiboumiComponent::handle_message(const Stanza& stanza)
{
218
  std::string from_str = stanza.get_tag("from");
219 220 221 222
  std::string id = stanza.get_tag("id");
  std::string to_str = stanza.get_tag("to");
  std::string type = stanza.get_tag("type");

223
  if (from_str.empty())
224 225 226
    return;
  if (type.empty())
    type = "normal";
227 228
  Bridge* bridge = this->get_user_bridge(from_str);
  Jid from(from_str);
229
  Jid to(to_str);
230
  Iid iid(to.local, bridge);
231 232 233

  std::string error_type("cancel");
  std::string error_name("internal-server-error");
louiz’'s avatar
louiz’ committed
234
  utils::ScopeGuard stanza_error([this, &from_str, &to_str, &id, &error_type, &error_name](){
235
      this->send_stanza_error("message", from_str, to_str, id,
236 237
                              error_type, error_name, "");
    });
238
  const XmlNode* body = stanza.get_child("body", COMPONENT_NS);
239 240

  try {                         // catch IRCNotConnected exceptions
241
  if (type == "groupchat" && iid.type == Iid::Type::Channel)
242 243 244 245 246
    {
      if (body && !body->get_inner().empty())
        {
          bridge->send_channel_message(iid, body->get_inner());
        }
247
      const XmlNode* subject = stanza.get_child("subject", COMPONENT_NS);
248 249 250 251 252 253 254
      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
255
      // them, we purge (we disconnect that resource from all the IRC servers)
256 257 258 259 260 261 262 263 264 265
      // 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)
266
        bridge->remove_resource(from.resource, "Error from remote client");
267 268 269 270 271 272
    }
  else if (type == "chat")
    {
      if (body && !body->get_inner().empty())
        {
          // a message for nick!server
273
          if (iid.type == Iid::Type::User && !iid.get_local().empty())
274 275 276 277
            {
              bridge->send_private_message(iid, body->get_inner());
              bridge->remove_preferred_from_jid(iid.get_local());
            }
278
          else if (iid.type != Iid::Type::User && !to.resource.empty())
279 280 281
            { // a message for chan%server@biboumi/Nick or
              // server@biboumi/Nick
              // Convert that into a message to nick!server
282
              Iid user_iid(utils::tolower(to.resource), iid.get_server(), Iid::Type::User);
283 284 285
              bridge->send_private_message(user_iid, body->get_inner());
              bridge->set_preferred_from_jid(user_iid.get_local(), to_str);
            }
286
          else if (iid.type == Iid::Type::Server)
louiz’'s avatar
louiz’ committed
287 288 289 290
            { // 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());
            }
291 292
        }
    }
293 294 295 296 297 298 299 300 301 302 303 304 305
  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())
              {
                bridge->send_irc_invitation(iid, invite_to);
              }
          }

    }
306 307
  } catch (const IRCNotConnected& ex)
    {
308
      this->send_stanza_error("message", from_str, to_str, id,
309
                              "cancel", "remote-server-not-found",
310
                              "Not connected to IRC server " + ex.hostname,
311 312
                              true);
    }
313 314 315 316
  stanza_error.disable();
}

// We MUST return an iq, whatever happens, except if the type is
317
// "result" or "error".
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
// 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");

335 336
  if (from.empty()) {
    log_warning("Received an iq without a 'from'. Ignoring.");
337
    return;
338
  }
339 340 341 342 343 344 345 346 347 348 349 350 351 352
  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");
louiz’'s avatar
louiz’ committed
353
  utils::ScopeGuard stanza_error([this, &from, &to_str, &id, &error_type, &error_name](){
354 355 356
      this->send_stanza_error("iq", from, to_str, id,
                              error_type, error_name, "");
    });
357
  try {
358 359
  if (type == "set")
    {
360
      const XmlNode* query;
361 362 363 364 365 366 367 368 369 370
      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())
                {
371
                  Iid iid(to.local, {});
372 373 374
                  if (role == "none")
                    {               // This is a kick
                      std::string reason;
375
                      const XmlNode* reason_el = child->get_child("reason", MUC_ADMIN_NS);
376 377 378 379 380
                      if (reason_el)
                        reason = reason_el->get_inner();
                      bridge->send_irc_kick(iid, nick, reason, id, from);
                    }
                  else
381
                    bridge->forward_affiliation_role_change(iid, from, nick, affiliation, role, id);
382 383 384 385 386 387 388 389
                  stanza_error.disable();
                }
            }
        }
      else if ((query = stanza.get_child("command", ADHOC_NS)))
        {
          Stanza response("iq");
          response["to"] = from;
390
          response["from"] = to_str;
391
          response["id"] = id;
392 393 394

          // Depending on the 'to' jid in the request, we use one adhoc
          // command handler or an other
395
          Iid iid(to.local, {});
396
          AdhocCommandsHandler* adhoc_handler;
397
          if (to.local.empty())
398
            adhoc_handler = &this->adhoc_commands_handler;
399 400 401 402 403 404 405
          else
          {
            if (iid.type == Iid::Type::Server)
              adhoc_handler = &this->irc_server_adhoc_commands_handler;
            else
              adhoc_handler = &this->irc_channel_adhoc_commands_handler;
          }
406 407 408
          // 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);
409 410 411 412 413 414 415 416
          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();
        }
417 418 419 420 421 422
#ifdef USE_DATABASE
      else if ((query = stanza.get_child("query", MAM_NS)))
        {
          if (this->handle_mam_request(stanza))
            stanza_error.disable();
        }
423 424 425 426 427
      else if ((query = stanza.get_child("query", MUC_OWNER_NS)))
        {
          if (this->handle_room_configuration_form(*query, from, to, id))
            stanza_error.disable();
        }
428
#endif
429 430 431
    }
  else if (type == "get")
    {
432
      const XmlNode* query;
433 434
      if ((query = stanza.get_child("query", DISCO_INFO_NS)))
        { // Disco info
louiz’'s avatar
louiz’ committed
435
          Iid iid(to.local, {'#', '&'});
436
          const std::string node = query->get_tag("node");
437 438 439 440 441 442 443 444 445
          if (to_str == this->served_hostname)
            {
              if (node.empty())
                {
                  // On the gateway itself
                  this->send_self_disco_info(id, from);
                  stanza_error.disable();
                }
            }
446 447 448 449 450 451 452 453
          else if (iid.type == Iid::Type::Server)
            {
                if (node.empty())
                {
                    this->send_irc_server_disco_info(id, from, to_str);
                    stanza_error.disable();
                }
            }
louiz’'s avatar
louiz’ committed
454 455
          else if (iid.type == Iid::Type::Channel)
            {
louiz’'s avatar
louiz’ committed
456 457 458 459 460 461
              if (node.empty())
                {
                  this->send_irc_channel_disco_info(id, from, to_str);
                  stanza_error.disable();
                }
              else if (node == MUC_TRAFFIC_NS)
louiz’'s avatar
louiz’ committed
462 463
                {
                  this->send_irc_channel_muc_traffic_info(id, from, to_str);
louiz’'s avatar
louiz’ committed
464
                  stanza_error.disable();
louiz’'s avatar
louiz’ committed
465 466
                }
            }
467 468 469
        }
      else if ((query = stanza.get_child("query", VERSION_NS)))
        {
470
          Iid iid(to.local, bridge);
471 472
          if ((iid.type == Iid::Type::Channel && !to.resource.empty()) ||
              (iid.type == Iid::Type::User))
473 474 475
            {
              // Get the IRC user version
              std::string target;
476
              if (iid.type == Iid::Type::User)
477 478 479 480 481 482 483 484 485 486 487 488 489 490 491
                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)))
        {
492
          Iid iid(to.local, bridge);
493 494 495
          const std::string node = query->get_tag("node");
          if (node == ADHOC_NS)
            {
496
              Jid from_jid(from);
497 498 499 500
              if (to.local.empty())
                {               // Get biboumi's adhoc commands
                  this->send_adhoc_commands_list(id, from, this->served_hostname,
                                                 (Config::get("admin", "") ==
501
                                                  from_jid.bare()),
502 503 504
                                                 this->adhoc_commands_handler);
                  stanza_error.disable();
                }
505
              else if (iid.type == Iid::Type::Server)
506 507 508
                {               // Get the server's adhoc commands
                  this->send_adhoc_commands_list(id, from, to_str,
                                                 (Config::get("admin", "") ==
509
                                                  from_jid.bare()),
510 511 512
                                                 this->irc_server_adhoc_commands_handler);
                  stanza_error.disable();
                }
513
              else if (iid.type == Iid::Type::Channel)
514 515 516 517 518 519 520
                {               // 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();
                }
521
            }
522
          else if (node.empty() && iid.type == Iid::Type::Server)
523
            { // Disco on an IRC server: get the list of channels
524 525 526 527 528 529 530 531 532 533 534 535 536 537 538
              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());

                }
539 540
              if (rs_info.max == -1)
                rs_info.max = 100;
541
              bridge->send_irc_channel_list_request(iid, id, from, std::move(rs_info));
542 543 544 545 546
              stanza_error.disable();
            }
        }
      else if ((query = stanza.get_child("ping", PING_NS)))
        {
547 548
          Iid iid(to.local, bridge);
          if (iid.type == Iid::Type::User)
549 550 551 552
            { // 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);
            }
553
          else if (iid.type == Iid::Type::Channel && !to.resource.empty())
554 555 556 557 558 559 560 561 562 563 564
            { // 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();
        }
565 566 567 568 569 570 571
#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
572 573 574 575
    }
  else if (type == "result")
    {
      stanza_error.disable();
576
      const XmlNode* query;
577 578
      if ((query = stanza.get_child("query", VERSION_NS)))
        {
579 580 581
          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);
582 583 584 585 586 587 588 589 590
          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();
591
          const Iid iid(to.local, bridge);
592 593 594 595 596 597 598 599 600 601 602 603
          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);
            }
        }
    }
604 605 606
  else if (type == "error")
    {
      stanza_error.disable();
607 608 609 610 611 612
      const auto it = this->waiting_iq.find(id);
      if (it != this->waiting_iq.end())
        {
          it->second(bridge, stanza);
          this->waiting_iq.erase(it);
        }
613
    }
614 615 616 617 618
  }
  catch (const IRCNotConnected& ex)
    {
      this->send_stanza_error("iq", from, to_str, id,
                              "cancel", "remote-server-not-found",
619
                              "Not connected to IRC server " + ex.hostname,
620 621 622 623
                              true);
      stanza_error.disable();
      return;
    }
624 625 626 627
  error_type = "cancel";
  error_name = "feature-not-implemented";
}

628 629 630 631 632 633 634 635 636 637
#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, {'#', '&'});
638
    if (query && iid.type == Iid::Type::Channel && to.resource.empty())
639
      {
640
        const std::string query_id = query->get_tag("queryid");
641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663
        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();
                  }
              }
          }
664 665 666 667 668 669 670 671
        const XmlNode* set = query->get_child("set", RSM_NS);
        int limit = -1;
        if (set)
          {
            const XmlNode* max = set->get_child("max", RSM_NS);
            if (max)
              limit = std::atoi(max->get_inner().data());
          }
672 673 674 675 676 677
        // If the archive is really big, and the client didn’t specify any
        // limit, we avoid flooding it: we set an arbitrary max limit.
        if (limit == -1 && start.empty() && end.empty())
          {
            limit = 100;
          }
678
        const auto lines = Database::get_muc_logs(from.bare(), iid.get_local(), iid.get_server(), limit, start, end);
679
        for (const Database::MucLogLine& line: lines)
680
        {
681
          if (!line.col<Database::Nick>().empty())
682
            this->send_archived_message(line, to.full(), from.full(), query_id);
683
        }
684 685 686 687 688 689
        this->send_iq_result_full_jid(id, from.full(), to.full());
        return true;
      }
  return false;
}

690
void BiboumiComponent::send_archived_message(const Database::MucLogLine& log_line, const std::string& from, const std::string& to,
691 692
                                             const std::string& queryid)
{
693 694
  Stanza message("message");
  {
695 696 697
    message["from"] = from;
    message["to"] = to;

698
    XmlSubNode result(message, "result");
699
    result["xmlns"] = MAM_NS;
700 701
    if (!queryid.empty())
      result["queryid"] = queryid;
702
    result["id"] = log_line.col<Database::Uuid>();
703

704
    XmlSubNode forwarded(result, "forwarded");
705 706
    forwarded["xmlns"] = FORWARD_NS;

707
    XmlSubNode delay(forwarded, "delay");
708
    delay["xmlns"] = DELAY_NS;
709
    delay["stamp"] = utils::to_string(log_line.col<Database::Date>());
710

711
    XmlSubNode submessage(forwarded, "message");
712
    submessage["xmlns"] = CLIENT_NS;
713
    submessage["from"] = from + "/" + log_line.col<Database::Nick>();
714 715
    submessage["type"] = "groupchat";

716
    XmlSubNode body(submessage, "body");
717
    body.set_inner(log_line.col<Database::Body>());
718 719
  }
  this->send_stanza(message);
720 721
}

722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751
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)
    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)
    return false;

  Jid requester(from);
752
  if (!handle_irc_channel_configuration_form(*this, query, requester, to))
753 754 755 756 757 758 759 760 761 762 763 764 765
    return false;

  Stanza iq("iq");
  iq["type"] = "result";
  iq["from"] = to.full();
  iq["to"] = from;
  iq["id"] = id;

  this->send_stanza(iq);

  return true;
}

766 767
#endif

768 769
Bridge* BiboumiComponent::get_user_bridge(const std::string& user_jid)
{
770
  auto bare_jid = Jid{user_jid}.bare();
771 772
  try
    {
773
      return this->bridges.at(bare_jid).get();
774 775 776
    }
  catch (const std::out_of_range& exception)
    {
777
      return this->bridges.emplace(bare_jid, std::make_unique<Bridge>(bare_jid, *this, this->poller)).first->second.get();
778 779 780
    }
}

781
Bridge* BiboumiComponent::find_user_bridge(const std::string& full_jid)
782
{
783
  auto bare_jid = Jid{full_jid}.bare();
784 785
  try
    {
786
      return this->bridges.at(bare_jid).get();
787 788 789 790 791 792 793
    }
  catch (const std::out_of_range& exception)
    {
      return nullptr;
    }
}

louiz’'s avatar
louiz’ committed
794
std::vector<Bridge*> BiboumiComponent::get_bridges() const
795
{
louiz’'s avatar
louiz’ committed
796
  std::vector<Bridge*> res;
797 798
  for (const auto& bridge: this->bridges)
    res.push_back(bridge.second.get());
799 800 801 802 803 804
  return res;
}

void BiboumiComponent::send_self_disco_info(const std::string& id, const std::string& jid_to)
{
  Stanza iq("iq");
805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821
  {
    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;
      }
  }
822 823 824
  this->send_stanza(iq);
}

825 826 827 828
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");
829 830 831 832 833 834 835 836 837 838
  {
    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";
839
    identity["name"] = "IRC server " + from.local + " over Biboumi";
840
    for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS})
841 842 843 844 845
      {
        XmlSubNode feature(query, "feature");
        feature["var"] = ns;
      }
  }
846 847 848
  this->send_stanza(iq);
}

849
void BiboumiComponent::send_irc_channel_muc_traffic_info(const std::string& id, const std::string& jid_to, const std::string& jid_from)
louiz’'s avatar
louiz’ committed
850 851
{
  Stanza iq("iq");
852 853 854 855 856 857 858 859 860 861 862 863
  {
    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 <feature/>
  }
louiz’'s avatar
louiz’ committed
864 865 866
  this->send_stanza(iq);
}

louiz’'s avatar
louiz’ committed
867 868 869 870 871 872 873 874 875 876 877 878 879 880 881
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";
882
    identity["name"] = "IRC channel " + iid.get_local() + " from server " + iid.get_server() + " over biboumi";
louiz’'s avatar
louiz’ committed
883 884 885 886 887 888 889 890 891
    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);
}

892
void BiboumiComponent::send_ping_request(const std::string& from,
893 894
                                         const std::string& jid_to,
                                         const std::string& id)
895 896
{
  Stanza iq("iq");
897 898 899 900 901 902 903 904
  {
    iq["type"] = "get";
    iq["id"] = id;
    iq["from"] = from + "@" + this->served_hostname;
    iq["to"] = jid_to;
    XmlSubNode ping(iq, "ping");
    ping["xmlns"] = PING_NS;
  }
905 906 907 908 909 910 911 912 913
  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");
914
          return;
915
        }
916 917 918 919 920 921
      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))))
922
        bridge->send_irc_ping_result({from, bridge}, id);
923 924 925 926
    };
  this->waiting_iq[id] = result_cb;
}

927 928 929 930 931
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<ListElement>::const_iterator begin,
                                                std::vector<ListElement>::const_iterator end,
                                                const ResultSetInfo& rs_info)
932 933
{
  Stanza iq("iq");
934 935 936 937 938 939 940
  {
    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;
941 942

    for (auto it = begin; it != end; ++it)
943 944
      {
        XmlSubNode item(query, "item");
945
        item["jid"] = it->channel + "@" + this->served_hostname;
946
      }
947

948 949 950 951
    if ((rs_info.max >= 0 || !rs_info.after.empty() || !rs_info.before.empty()))
      {
        XmlSubNode set_node(query, "set");
        set_node["xmlns"] = RSM_NS;
952

953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970
        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()));
          }
      }
  }
971 972
  this->send_stanza(iq);
}
louiz’'s avatar
louiz’ committed
973 974 975 976 977 978

void BiboumiComponent::send_invitation(const std::string& room_target,
                                       const std::string& jid_to,
                                       const std::string& author_nick)
{
  Stanza message("message");
979 980 981 982 983 984 985 986 987 988 989
  {
    message["from"] = room_target + "@" + this->served_hostname;
    message["to"] = jid_to;
    XmlSubNode x(message, "x");
    x["xmlns"] = MUC_USER_NS;
    XmlSubNode invite(x, "invite");
    if (author_nick.empty())
      invite["from"] = room_target + "@" + this->served_hostname;
    else
      invite["from"] = room_target + "@" + this->served_hostname + "/" + author_nick;
  }
louiz’'s avatar
louiz’ committed
990 991
  this->send_stanza(message);
}
louiz’'s avatar
louiz’ committed
992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004

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)
{
1005
  Stanza presence("presence");
louiz’'s avatar
louiz’ committed
1006 1007 1008 1009 1010 1011
  presence["from"] = from;
  presence["to"] = to;
  presence["id"] = this->next_id();
  presence["type"] = "subscribe";
  this->send_stanza(presence);
}
1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025

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);
}

1026 1027
void BiboumiComponent::on_irc_client_connected(const std::string& irc_hostname, const std::string& jid)
{
1028 1029 1030 1031 1032
#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, "");
#endif
1033 1034 1035 1036
}

void BiboumiComponent::on_irc_client_disconnected(const std::string& irc_hostname, const std::string& jid)
{
1037 1038 1039 1040 1041
#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");
#endif
1042 1043
}

1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057
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<Database::RemoteJid>();
      this->send_presence_to_contact(this->get_served_hostname(), remote_jid, "probe");
    }
#endif
}