1 #include <xmpp/biboumi_component.hpp>
2
3 #include <utils/timed_events.hpp>
4 #include <utils/scopeguard.hpp>
5 #include <utils/tolower.hpp>
6 #include <logger/logger.hpp>
7 #include <xmpp/adhoc_command.hpp>
8 #include <xmpp/biboumi_adhoc_commands.hpp>
9 #include <bridge/list_element.hpp>
10 #include <utils/encoding.hpp>
11 #include <config/config.hpp>
12 #include <utils/time.hpp>
13 #include <xmpp/jid.hpp>
14
15 #include <stdexcept>
16 #include <iostream>
17
18 #include <cstdlib>
19
20 #include <biboumi.h>
21
22 #ifdef SYSTEMD_FOUND
23 # include <systemd/sd-daemon.h>
24 #endif
25
26 #include <database/database.hpp>
27 #include <bridge/result_set_management.hpp>
28 #include <bridge/history_limit.hpp>
29
30 using namespace std::string_literals;
31
32 static std::set<std::string> kickable_errors{
33 "gone",
34 "internal-server-error",
35 "item-not-found",
36 "jid-malformed",
37 "recipient-unavailable",
38 "redirect",
39 "remote-server-not-found",
40 "remote-server-timeout",
41 "service-unavailable",
42 "malformed-error"
43 };
44
45
BiboumiComponent(std::shared_ptr<Poller> & poller,const std::string & hostname,const std::string & secret)46 BiboumiComponent::BiboumiComponent(std::shared_ptr<Poller>& poller, const std::string& hostname, const std::string& secret):
47 XmppComponent(poller, hostname, secret),
48 irc_server_adhoc_commands_handler(*this),
49 irc_channel_adhoc_commands_handler(*this)
50 {
51 this->stanza_handlers.emplace("presence",
52 std::bind(&BiboumiComponent::handle_presence, this,std::placeholders::_1));
53 this->stanza_handlers.emplace("message",
54 std::bind(&BiboumiComponent::handle_message, this,std::placeholders::_1));
55 this->stanza_handlers.emplace("iq",
56 std::bind(&BiboumiComponent::handle_iq, this,std::placeholders::_1));
57
58 this->adhoc_commands_handler.add_command("ping", {{&PingStep1}, "Do a ping", false});
59 this->adhoc_commands_handler.add_command("hello", {{&HelloStep1, &HelloStep2}, "Receive a custom greeting", false});
60 this->adhoc_commands_handler.add_command("disconnect-user", {{&DisconnectUserStep1, &DisconnectUserStep2}, "Disconnect selected users from the gateway", true});
61 this->adhoc_commands_handler.add_command("disconnect-from-irc-server", {{&DisconnectUserFromServerStep1, &DisconnectUserFromServerStep2, &DisconnectUserFromServerStep3}, "Disconnect from the selected IRC servers", false});
62 this->adhoc_commands_handler.add_command("reload", {{&Reload}, "Reload biboumi’s configuration", true});
63
64 AdhocCommand get_irc_connection_info{{&GetIrcConnectionInfoStep1}, "Returns various information about your connection to this IRC server.", false};
65 if (!Config::get("fixed_irc_server", "").empty())
66 this->adhoc_commands_handler.add_command("get-irc-connection-info", get_irc_connection_info);
67 else
68 this->irc_server_adhoc_commands_handler.add_command("get-irc-connection-info", get_irc_connection_info);
69
70 #ifdef USE_DATABASE
71 AdhocCommand configure_server_command({&ConfigureIrcServerStep1, &ConfigureIrcServerStep2}, "Configure a few settings for that IRC server", false);
72 AdhocCommand configure_global_command({&ConfigureGlobalStep1, &ConfigureGlobalStep2}, "Configure a few settings", false);
73
74 if (!Config::get("fixed_irc_server", "").empty())
75 {
76 this->adhoc_commands_handler.add_command("server-configure", configure_server_command);
77 this->adhoc_commands_handler.add_command("global-configure", configure_global_command);
78 }
79 else
80 {
81 this->adhoc_commands_handler.add_command("configure", configure_global_command);
82 this->irc_server_adhoc_commands_handler.add_command("configure", configure_server_command);
83 }
84
85 this->irc_channel_adhoc_commands_handler.add_command("configure", {{&ConfigureIrcChannelStep1, &ConfigureIrcChannelStep2}, "Configure a few settings for that IRC channel", false});
86 #endif
87 }
88
shutdown()89 void BiboumiComponent::shutdown()
90 {
91 for (auto& pair: this->bridges)
92 pair.second->shutdown("Gateway shutdown");
93 #ifdef USE_DATABASE
94 for (const Database::RosterItem& roster_item: Database::get_full_roster())
95 {
96 this->send_presence_to_contact(roster_item.col<Database::LocalJid>(),
97 roster_item.col<Database::RemoteJid>(),
98 "unavailable");
99 }
100 #endif
101 }
102
clean()103 void BiboumiComponent::clean()
104 {
105 auto it = std::begin(this->bridges);
106 while (it != std::end(this->bridges))
107 {
108 it->second->clean();
109 if (it->second->active_clients() == 0)
110 it = this->bridges.erase(it);
111 else
112 ++it;
113 }
114 }
115
handle_presence(const Stanza & stanza)116 void BiboumiComponent::handle_presence(const Stanza& stanza)
117 {
118 std::string from_str = stanza.get_tag("from");
119 std::string id = stanza.get_tag("id");
120 std::string to_str = stanza.get_tag("to");
121 std::string type = stanza.get_tag("type");
122
123 // Check for mandatory tags
124 if (from_str.empty())
125 {
126 log_warning("Received an invalid presence stanza: tag 'from' is missing.");
127 return;
128 }
129 if (to_str.empty())
130 {
131 this->send_stanza_error("presence", from_str, this->served_hostname, id,
132 "modify", "bad-request", "Missing 'to' tag");
133 return;
134 }
135
136 Bridge* bridge = this->get_user_bridge(from_str);
137 Jid to(to_str);
138 Jid from(from_str);
139 Iid iid(to.local, bridge);
140
141 // An error stanza is sent whenever we exit this function without
142 // disabling this scopeguard. If error_type and error_name are not
143 // changed, the error signaled is internal-server-error. Change their
144 // value to signal an other kind of error. For example
145 // feature-not-implemented, etc. Any non-error process should reach the
146 // stanza_error.disable() call at the end of the function.
147 std::string error_type("cancel");
148 std::string error_name("internal-server-error");
149 utils::ScopeGuard stanza_error([this, &from_str, &to_str, &id, &error_type, &error_name](){
150 this->send_stanza_error("presence", from_str, to_str, id,
151 error_type, error_name, "");
152 });
153
154 try {
155 if (iid.type == Iid::Type::Channel && !iid.get_server().empty())
156 { // presence toward a MUC that corresponds to an irc channel
157 if (type.empty())
158 {
159 const std::string own_nick = bridge->get_own_nick(iid);
160 const XmlNode* x = stanza.get_child("x", MUC_NS);
161 const IrcClient* irc = bridge->find_irc_client(iid.get_server());
162 // if there is no <x/>, this is a presence status update, we don’t care about those
163 if (x)
164 {
165 const XmlNode* password = x->get_child("password", MUC_NS);
166 const XmlNode* history = x->get_child("history", MUC_NS);
167 HistoryLimit history_limit;
168 if (history)
169 {
170 const auto seconds = history->get_tag("seconds");
171 if (!seconds.empty())
172 {
173 const auto now = std::chrono::system_clock::now();
174 std::time_t timestamp = std::chrono::system_clock::to_time_t(now);
175 int int_seconds = std::atoi(seconds.data());
176 timestamp -= int_seconds;
177 history_limit.since = utils::to_string(timestamp);
178 }
179 const auto since = history->get_tag("since");
180 if (!since.empty())
181 history_limit.since = since;
182 const auto maxstanzas = history->get_tag("maxstanzas");
183 if (!maxstanzas.empty())
184 history_limit.stanzas = std::atoi(maxstanzas.data());
185 // Ignore any other value, because this is too complex to implement,
186 // so I won’t do it.
187 if (history->get_tag("maxchars") == "0")
188 history_limit.stanzas = 0;
189 }
190 bridge->join_irc_channel(iid, to.resource, password ? password->get_inner(): "",
191 from.resource, history_limit);
192 }
193 else
194 {
195 if (irc)
196 {
197 const auto chan = irc->find_channel(iid.get_local());
198 if (chan && chan->joined)
199 bridge->send_irc_nick_change(iid, to.resource, from.resource);
200 else
201 { // send an error if we are not joined yet, instead of treating it as a join
202 this->send_stanza_error("presence", from_str, to_str, id, "modify", "not-acceptable", "You are not joined to this MUC.");
203 }
204 }
205 }
206 }
207 else if (type == "unavailable")
208 {
209 const XmlNode* status = stanza.get_child("status", COMPONENT_NS);
210 bridge->leave_irc_channel(std::move(iid), status ? status->get_inner() : "", from.resource);
211 }
212 }
213 else if (iid.type == Iid::Type::Server || iid.type == Iid::Type::None)
214 {
215 if (type == "subscribe")
216 { // Auto-accept any subscription request for an IRC server
217 this->send_presence_to_contact(to_str, from.bare(), "subscribed", id);
218 if (iid.type == Iid::Type::None || bridge->find_irc_client(iid.get_server()))
219 this->send_presence_to_contact(to_str, from.bare(), "");
220 this->send_presence_to_contact(to_str, from.bare(), "subscribe");
221 #ifdef USE_DATABASE
222 if (!Database::has_roster_item(to_str, from.bare()))
223 Database::add_roster_item(to_str, from.bare());
224 #endif
225 }
226 else if (type == "unsubscribe")
227 {
228 this->send_presence_to_contact(to_str, from.bare(), "unavailable", id);
229 this->send_presence_to_contact(to_str, from.bare(), "unsubscribed");
230 this->send_presence_to_contact(to_str, from.bare(), "unsubscribe");
231 #ifdef USE_DATABASE
232 const bool res = Database::has_roster_item(to_str, from.bare());
233 if (res)
234 Database::delete_roster_item(to_str, from.bare());
235 #endif
236 }
237 else if (type == "probe")
238 {
239 if ((iid.type == Iid::Type::Server && bridge->find_irc_client(iid.get_server()))
240 || iid.type == Iid::Type::None)
241 {
242 #ifdef USE_DATABASE
243 if (Database::has_roster_item(to_str, from.bare()))
244 #endif
245 this->send_presence_to_contact(to_str, from.bare(), "");
246 #ifdef USE_DATABASE
247 else // rfc 6121 4.3.2.1
248 this->send_presence_to_contact(to_str, from.bare(), "unsubscribed");
249 #endif
250 }
251 }
252 else if (type.empty())
253 { // We just receive a presence from someone (as the result of a probe,
254 // or a directed presence, or a normal presence change)
255 if (iid.type == Iid::Type::None)
256 this->send_presence_to_contact(to_str, from.bare(), "");
257 }
258 }
259 else if (iid.type == Iid::Type::User)
260 { // Do nothing yet
261 }
262 }
263 catch (const IRCNotConnected& ex)
264 {
265 if (type != "unavailable")
266 this->send_stanza_error("presence", from_str, to_str, id,
267 "cancel", "remote-server-not-found",
268 "Not connected to IRC server " + ex.hostname,
269 true);
270 }
271 stanza_error.disable();
272 }
273
handle_message(const Stanza & stanza)274 void BiboumiComponent::handle_message(const Stanza& stanza)
275 {
276 std::string from_str = stanza.get_tag("from");
277 std::string id = stanza.get_tag("id");
278 std::string to_str = stanza.get_tag("to");
279 std::string type = stanza.get_tag("type");
280
281 if (from_str.empty())
282 return;
283 if (type.empty())
284 type = "normal";
285 Bridge* bridge = this->get_user_bridge(from_str);
286 Jid from(from_str);
287 Jid to(to_str);
288 Iid iid(to.local, bridge);
289
290 std::string error_type("cancel");
291 std::string error_name("internal-server-error");
292 std::string error_text{};
293 utils::ScopeGuard stanza_error([this, &from_str, &to_str, &id, &error_type, &error_name, &error_text](){
294 this->send_stanza_error("message", from_str, to_str, id,
295 error_type, error_name, error_text);
296 });
297 const XmlNode* body = stanza.get_child("body", COMPONENT_NS);
298
299 try { // catch IRCNotConnected exceptions
300 if (type == "groupchat" && iid.type == Iid::Type::Channel)
301 {
302 if (body && !body->get_inner().empty())
303 {
304 if (bridge->is_resource_in_chan(iid.to_tuple(), from.resource))
305 {
306 // Extract some XML nodes that we must include in the
307 // reflection (if any), because XMPP says so
308 std::vector<XmlNode> nodes_to_reflect;
309 const XmlNode* origin_id = stanza.get_child("origin-id", STABLE_ID_NS);
310 if (origin_id)
311 nodes_to_reflect.push_back(*origin_id);
312 const auto own_address = std::to_string(iid) + '@' + this->served_hostname;
313 for (const XmlNode* stanza_id: stanza.get_children("stanza-id", STABLE_ID_NS))
314 {
315 // Stanza ID generating entities, which encounter a
316 // <stanza-id/> element where the 'by' attribute matches
317 // the 'by' attribute they would otherwise set, MUST
318 // delete that element even if they are not adding their
319 // own stanza ID.
320 if (stanza_id->get_tag("by") != own_address)
321 nodes_to_reflect.push_back(*stanza_id);
322 }
323 bridge->send_channel_message(iid, body->get_inner(), id, std::move(nodes_to_reflect));
324 }
325 else
326 {
327 error_type = "modify";
328 error_name = "not-acceptable";
329 error_text = "You are not a participant in this room.";
330 return;
331 }
332 }
333 const XmlNode* subject = stanza.get_child("subject", COMPONENT_NS);
334 if (subject)
335 bridge->set_channel_topic(iid, subject->get_inner());
336 }
337 else if (type == "error")
338 {
339 const XmlNode* error = stanza.get_child("error", COMPONENT_NS);
340 // Only a set of errors are considered “fatal”. If we encounter one of
341 // them, we purge (we disconnect that resource from all the IRC servers)
342 // We consider this to be true, unless the error condition is
343 // specified and is not in the kickable_errors set
344 bool kickable_error = true;
345 if (error && error->has_children())
346 {
347 const XmlNode* condition = error->get_last_child();
348 if (kickable_errors.find(condition->get_name()) == kickable_errors.end())
349 kickable_error = false;
350 }
351 if (kickable_error)
352 bridge->remove_resource(from.resource, "Error from remote client");
353 }
354 else if (type == "chat")
355 {
356 if (body && !body->get_inner().empty())
357 {
358 const auto fixed_irc_server = Config::get("fixed_irc_server", "");
359 // a message for nick!server
360 if (iid.type == Iid::Type::User && !iid.get_local().empty())
361 {
362 bridge->send_private_message(iid, body->get_inner());
363 bridge->remove_preferred_from_jid(iid.get_local());
364 }
365 else if (iid.type != Iid::Type::User && !to.resource.empty())
366 { // a message for chan%server@biboumi/Nick or
367 // server@biboumi/Nick
368 // Convert that into a message to nick!server
369 Iid user_iid(utils::tolower(to.resource), iid.get_server(), Iid::Type::User);
370 bridge->send_private_message(user_iid, body->get_inner());
371 bridge->set_preferred_from_jid(user_iid.get_local(), to_str);
372 }
373 else if (iid.type == Iid::Type::Server)
374 bridge->send_raw_message(iid.get_server(), body->get_inner());
375 else if (iid.type == Iid::Type::None && !fixed_irc_server.empty())
376 { // Message sent to the server JID
377 // Convert the message body into a raw IRC message
378 bridge->send_raw_message(fixed_irc_server, body->get_inner());
379 }
380 }
381 }
382 else if (type == "normal" && iid.type == Iid::Type::Channel)
383 {
384 if (const XmlNode* x = stanza.get_child("x", MUC_USER_NS))
385 if (const XmlNode* invite = x->get_child("invite", MUC_USER_NS))
386 {
387 const auto invite_to = invite->get_tag("to");
388 if (!invite_to.empty())
389 {
390 Jid invited_jid{invite_to};
391 if (invited_jid.domain == this->get_served_hostname() || invited_jid.local.empty())
392 bridge->send_irc_invitation(iid, invite_to);
393 else
394 this->send_invitation_from_fulljid(std::to_string(iid), invite_to, from_str);
395 }
396 }
397 }
398 } catch (const IRCNotConnected& ex)
399 {
400 this->send_stanza_error("message", from_str, to_str, id,
401 "cancel", "remote-server-not-found",
402 "Not connected to IRC server " + ex.hostname,
403 true);
404 }
405 stanza_error.disable();
406 }
407
408 // We MUST return an iq, whatever happens, except if the type is
409 // "result" or "error".
410 // To do this, we use a scopeguard. If an exception is raised somewhere, an
411 // iq of type error "internal-server-error" is sent. If we handle the
412 // request properly (by calling a function that registers an iq to be sent
413 // later, or that directly sends an iq), we disable the ScopeGuard. If we
414 // reach the end of the function without having disabled the scopeguard, we
415 // send a "feature-not-implemented" iq as a result. If an other kind of
416 // error is found (for example the feature is implemented in biboumi, but
417 // the request is missing some attribute) we can just change the values of
418 // error_type and error_name and return from the function (without disabling
419 // the scopeguard); an iq error will be sent
handle_iq(const Stanza & stanza)420 void BiboumiComponent::handle_iq(const Stanza& stanza)
421 {
422 std::string id = stanza.get_tag("id");
423 std::string from = stanza.get_tag("from");
424 std::string to_str = stanza.get_tag("to");
425 std::string type = stanza.get_tag("type");
426
427 if (from.empty()) {
428 log_warning("Received an iq without a 'from'. Ignoring.");
429 return;
430 }
431 if (id.empty() || to_str.empty() || type.empty())
432 {
433 this->send_stanza_error("iq", from, this->served_hostname, id,
434 "modify", "bad-request", "");
435 return;
436 }
437
438 Bridge* bridge = this->get_user_bridge(from);
439 Jid to(to_str);
440
441 // These two values will be used in the error iq sent if we don't disable
442 // the scopeguard.
443 std::string error_type("cancel");
444 std::string error_name("internal-server-error");
445 utils::ScopeGuard stanza_error([this, &from, &to_str, &id, &error_type, &error_name](){
446 this->send_stanza_error("iq", from, to_str, id,
447 error_type, error_name, "");
448 });
449 try {
450 if (type == "set")
451 {
452 const XmlNode* query;
453 if ((query = stanza.get_child("query", MUC_ADMIN_NS)))
454 {
455 const XmlNode* child = query->get_child("item", MUC_ADMIN_NS);
456 if (child)
457 {
458 std::string nick = child->get_tag("nick");
459 std::string role = child->get_tag("role");
460 std::string affiliation = child->get_tag("affiliation");
461 if (!nick.empty())
462 {
463 Iid iid(to.local, {});
464 if (role == "none")
465 { // This is a kick
466 std::string reason;
467 const XmlNode* reason_el = child->get_child("reason", MUC_ADMIN_NS);
468 if (reason_el)
469 reason = reason_el->get_inner();
470 bridge->send_irc_kick(iid, nick, reason, id, from);
471 }
472 else
473 bridge->forward_affiliation_role_change(iid, from, nick, affiliation, role, id);
474 stanza_error.disable();
475 }
476 }
477 }
478 else if ((query = stanza.get_child("command", ADHOC_NS)))
479 {
480 Stanza response("iq");
481 response["to"] = from;
482 response["from"] = to_str;
483 response["id"] = id;
484
485 // Depending on the 'to' jid in the request, we use one adhoc
486 // command handler or an other
487 Iid iid(to.local, {'#', '&'});
488 AdhocCommandsHandler* adhoc_handler;
489 if (to.local.empty())
490 adhoc_handler = &this->adhoc_commands_handler;
491 else
492 {
493 if (iid.type == Iid::Type::Server)
494 adhoc_handler = &this->irc_server_adhoc_commands_handler;
495 else if (iid.type == Iid::Type::Channel && to.resource.empty())
496 adhoc_handler = &this->irc_channel_adhoc_commands_handler;
497 else
498 {
499 error_name = "feature-not-implemented";
500 return;
501 }
502 }
503 // Execute the command, if any, and get a result XmlNode that we
504 // insert in our response
505 XmlNode inner_node = adhoc_handler->handle_request(from, to_str, *query);
506 if (inner_node.get_child("error", ADHOC_NS))
507 response["type"] = "error";
508 else
509 response["type"] = "result";
510 response.add_child(std::move(inner_node));
511 this->send_stanza(response);
512 stanza_error.disable();
513 }
514 #ifdef USE_DATABASE
515 else if ((query = stanza.get_child("query", MAM_NS)))
516 {
517 try {
518 if (this->handle_mam_request(stanza))
519 stanza_error.disable();
520 } catch (const Database::RecordNotFound& exc) {
521 error_name = "item-not-found";
522 return;
523 }
524 }
525 else if ((query = stanza.get_child("query", MUC_OWNER_NS)))
526 {
527 if (this->handle_room_configuration_form(*query, from, to, id))
528 stanza_error.disable();
529 }
530 #endif
531 }
532 else if (type == "get")
533 {
534 const XmlNode* query;
535 if ((query = stanza.get_child("query", DISCO_INFO_NS)))
536 { // Disco info
537 Iid iid(to.local, {'#', '&'});
538 const std::string node = query->get_tag("node");
539 if (to_str == this->served_hostname)
540 {
541 if (node.empty())
542 {
543 // On the gateway itself
544 this->send_self_disco_info(id, from);
545 stanza_error.disable();
546 }
547 }
548 else if (iid.type == Iid::Type::Server)
549 {
550 if (node.empty())
551 {
552 this->send_irc_server_disco_info(id, from, to_str);
553 stanza_error.disable();
554 }
555 }
556 else if (iid.type == Iid::Type::Channel && to.resource.empty())
557 {
558 if (node.empty())
559 {
560 const IrcClient* irc_client = bridge->find_irc_client(iid.get_server());
561 const IrcChannel* irc_channel{};
562 if (irc_client)
563 irc_channel = irc_client->find_channel(iid.get_local());
564 this->send_irc_channel_disco_info(id, from, to_str, irc_channel);
565 stanza_error.disable();
566 }
567 else if (node == MUC_TRAFFIC_NS)
568 {
569 this->send_irc_channel_muc_traffic_info(id, from, to_str);
570 stanza_error.disable();
571 }
572 }
573 }
574 else if ((query = stanza.get_child("query", VERSION_NS)))
575 {
576 Iid iid(to.local, bridge);
577 if ((iid.type == Iid::Type::Channel && !to.resource.empty()) ||
578 (iid.type == Iid::Type::User))
579 {
580 // Get the IRC user version
581 std::string target;
582 if (iid.type == Iid::Type::User)
583 target = iid.get_local();
584 else
585 target = to.resource;
586 bridge->send_irc_version_request(iid.get_server(), target, id,
587 from, to_str);
588 }
589 else
590 {
591 // On the gateway itself or on a channel
592 this->send_version(id, from, to_str);
593 }
594 stanza_error.disable();
595 }
596 else if ((query = stanza.get_child("query", DISCO_ITEMS_NS)))
597 {
598 Iid iid(to.local, bridge);
599 const std::string node = query->get_tag("node");
600 if (node == ADHOC_NS)
601 {
602 Jid from_jid(from);
603 if (to.local.empty())
604 { // Get biboumi's adhoc commands
605 this->send_adhoc_commands_list(id, from, this->served_hostname,
606 Config::is_in_list("admin", from_jid.bare()),
607 this->adhoc_commands_handler);
608 stanza_error.disable();
609 }
610 else if (iid.type == Iid::Type::Server)
611 { // Get the server's adhoc commands
612 this->send_adhoc_commands_list(id, from, to_str,
613 Config::is_in_list("admin", from_jid.bare()),
614 this->irc_server_adhoc_commands_handler);
615 stanza_error.disable();
616 }
617 else if (iid.type == Iid::Type::Channel && to.resource.empty())
618 { // Get the channel's adhoc commands
619 this->send_adhoc_commands_list(id, from, to_str,
620 Config::is_in_list("admin", from_jid.bare()),
621 this->irc_channel_adhoc_commands_handler);
622 stanza_error.disable();
623 }
624 else // “to” is a MUC user, not the room itself
625 error_name = "feature-not-implemented";
626 }
627 else if (node.empty() && iid.type == Iid::Type::Server)
628 { // Disco on an IRC server: get the list of channels
629 ResultSetInfo rs_info;
630 const XmlNode* set_node = query->get_child("set", RSM_NS);
631 if (set_node)
632 {
633 const XmlNode* after = set_node->get_child("after", RSM_NS);
634 if (after)
635 rs_info.after = after->get_inner();
636 const XmlNode* before = set_node->get_child("before", RSM_NS);
637 if (before)
638 rs_info.before = before->get_inner();
639 const XmlNode* max = set_node->get_child("max", RSM_NS);
640 if (max)
641 rs_info.max = std::atoi(max->get_inner().data());
642 }
643 if (rs_info.max == -1)
644 rs_info.max = 100;
645 bridge->send_irc_channel_list_request(iid, id, from, std::move(rs_info));
646 stanza_error.disable();
647 }
648 }
649 else if ((query = stanza.get_child("ping", PING_NS)))
650 {
651 Iid iid(to.local, bridge);
652 if (iid.type == Iid::Type::User)
653 { // Ping any user (no check on the nick done ourself)
654 bridge->send_irc_user_ping_request(iid.get_server(),
655 iid.get_local(), id, from, to_str);
656 }
657 else if (iid.type == Iid::Type::Channel && !to.resource.empty())
658 { // Ping a room participant (we check if the nick is in the room)
659 bridge->send_irc_participant_ping_request(iid,
660 to.resource, id, from, to_str);
661 }
662 else
663 { // Ping a channel, a server or the gateway itself
664 bridge->on_gateway_ping(iid.get_server(),
665 id, from, to_str);
666 }
667 stanza_error.disable();
668 }
669 #ifdef USE_DATABASE
670 else if ((query = stanza.get_child("query", MUC_OWNER_NS)))
671 {
672 if (this->handle_room_configuration_form_request(from, to, id))
673 stanza_error.disable();
674 }
675 #endif
676 }
677 else if (type == "result")
678 {
679 stanza_error.disable();
680 const XmlNode* query;
681 if ((query = stanza.get_child("query", VERSION_NS)))
682 {
683 const XmlNode* name_node = query->get_child("name", VERSION_NS);
684 const XmlNode* version_node = query->get_child("version", VERSION_NS);
685 const XmlNode* os_node = query->get_child("os", VERSION_NS);
686 std::string name;
687 std::string version;
688 std::string os;
689 if (name_node)
690 name = name_node->get_inner() + " (through the biboumi gateway)";
691 if (version_node)
692 version = version_node->get_inner();
693 if (os_node)
694 os = os_node->get_inner();
695 const Iid iid(to.local, bridge);
696 bridge->send_xmpp_version_to_irc(iid, name, version, os);
697 }
698 else
699 {
700 const auto it = this->waiting_iq.find(id);
701 if (it != this->waiting_iq.end())
702 {
703 it->second(bridge, stanza);
704 this->waiting_iq.erase(it);
705 }
706 }
707 }
708 else if (type == "error")
709 {
710 stanza_error.disable();
711 const auto it = this->waiting_iq.find(id);
712 if (it != this->waiting_iq.end())
713 {
714 it->second(bridge, stanza);
715 this->waiting_iq.erase(it);
716 }
717 }
718 }
719 catch (const IRCNotConnected& ex)
720 {
721 this->send_stanza_error("iq", from, to_str, id,
722 "cancel", "remote-server-not-found",
723 "Not connected to IRC server " + ex.hostname,
724 true);
725 stanza_error.disable();
726 return;
727 }
728 error_type = "cancel";
729 error_name = "feature-not-implemented";
730 }
731
732 #ifdef USE_DATABASE
handle_mam_request(const Stanza & stanza)733 bool BiboumiComponent::handle_mam_request(const Stanza& stanza)
734 {
735 std::string id = stanza.get_tag("id");
736 Jid from(stanza.get_tag("from"));
737 Jid to(stanza.get_tag("to"));
738
739 const XmlNode* query = stanza.get_child("query", MAM_NS);
740
741 Iid iid(to.local, {'#', '&'});
742 if (query && iid.type == Iid::Type::Channel && to.resource.empty())
743 {
744 const std::string query_id = query->get_tag("queryid");
745 std::string start;
746 std::string end;
747 const XmlNode* x = query->get_child("x", DATAFORM_NS);
748 if (x)
749 {
750 const XmlNode* value;
751 const auto fields = x->get_children("field", DATAFORM_NS);
752 for (const auto& field: fields)
753 {
754 if (field->get_tag("var") == "start")
755 {
756 value = field->get_child("value", DATAFORM_NS);
757 if (value)
758 start = value->get_inner();
759 }
760 else if (field->get_tag("var") == "end")
761 {
762 value = field->get_child("value", DATAFORM_NS);
763 if (value)
764 end = value->get_inner();
765 }
766 }
767 }
768 const XmlNode* set = query->get_child("set", RSM_NS);
769 int limit = -1;
770 Id::real_type reference_record_id{Id::unset_value};
771 Database::Paging paging_order{Database::Paging::first};
772 if (set)
773 {
774 const XmlNode* max = set->get_child("max", RSM_NS);
775 if (max)
776 limit = std::atoi(max->get_inner().data());
777 const XmlNode* after = set->get_child("after", RSM_NS);
778 if (after)
779 {
780 auto after_record = Database::get_muc_log(from.bare(), iid.get_local(), iid.get_server(),
781 after->get_inner(), start, end);
782 reference_record_id = after_record.col<Id>();
783 }
784 const XmlNode* before = set->get_child("before", RSM_NS);
785 if (before)
786 {
787 paging_order = Database::Paging::last;
788 if (!before->get_inner().empty())
789 {
790 auto before_record = Database::get_muc_log(from.bare(), iid.get_local(), iid.get_server(), before->get_inner(), start, end);
791 reference_record_id = before_record.col<Id>();
792 }
793 }
794 }
795 // Do not send more than 100 messages, even if the client asked for more,
796 // or if it didn’t specify any limit.
797 if (limit < 0 || limit > 100)
798 limit = 100;
799 auto result = Database::get_muc_logs(from.bare(), iid.get_local(), iid.get_server(),
800 static_cast<std::size_t>(limit),
801 start, end,
802 reference_record_id, paging_order);
803 bool complete = std::get<bool>(result);
804 auto& lines = std::get<1>(result);
805
806 for (const Database::MucLogLine& line: lines)
807 {
808 if (!line.col<Database::Nick>().empty())
809 this->send_archived_message(line, to.full(), from.full(), query_id);
810 }
811 {
812 auto fin_ptr = std::make_unique<XmlNode>("fin");
813 {
814 XmlNode& fin = *(fin_ptr.get());
815 fin["xmlns"] = MAM_NS;
816 if (complete)
817 fin["complete"] = "true";
818 XmlSubNode set(fin, "set");
819 set["xmlns"] = RSM_NS;
820 if (!lines.empty())
821 {
822 XmlSubNode first(set, "first");
823 first["index"] = "0";
824 first.set_inner(lines[0].col<Database::Uuid>());
825 XmlSubNode last(set, "last");
826 last.set_inner(lines[lines.size() - 1].col<Database::Uuid>());
827 }
828 }
829 this->send_iq_result_full_jid(id, from.full(), to.full(), std::move(fin_ptr));
830 }
831 return true;
832 }
833 return false;
834 }
835
send_archived_message(const Database::MucLogLine & log_line,const std::string & from,const std::string & to,const std::string & queryid)836 void BiboumiComponent::send_archived_message(const Database::MucLogLine& log_line, const std::string& from, const std::string& to,
837 const std::string& queryid)
838 {
839 Stanza message("message");
840 {
841 message["from"] = from;
842 message["to"] = to;
843
844 XmlSubNode result(message, "result");
845 result["xmlns"] = MAM_NS;
846 if (!queryid.empty())
847 result["queryid"] = queryid;
848 result["id"] = log_line.col<Database::Uuid>();
849
850 XmlSubNode forwarded(result, "forwarded");
851 forwarded["xmlns"] = FORWARD_NS;
852
853 XmlSubNode delay(forwarded, "delay");
854 delay["xmlns"] = DELAY_NS;
855 delay["stamp"] = utils::to_string(log_line.col<Database::Date>());
856
857 XmlSubNode submessage(forwarded, "message");
858 submessage["xmlns"] = CLIENT_NS;
859 submessage["from"] = from + "/" + log_line.col<Database::Nick>();
860 submessage["type"] = "groupchat";
861
862 XmlSubNode body(submessage, "body");
863 body.set_inner(log_line.col<Database::Body>());
864 }
865 this->send_stanza(message);
866 }
867
handle_room_configuration_form_request(const std::string & from,const Jid & to,const std::string & id)868 bool BiboumiComponent::handle_room_configuration_form_request(const std::string& from, const Jid& to, const std::string& id)
869 {
870 Iid iid(to.local, {'#', '&'});
871
872 if (iid.type != Iid::Type::Channel || !to.resource.empty())
873 return false;
874
875 Stanza iq("iq");
876 {
877 iq["from"] = to.full();
878 iq["to"] = from;
879 iq["id"] = id;
880 iq["type"] = "result";
881 XmlSubNode query(iq, "query");
882 query["xmlns"] = MUC_OWNER_NS;
883 Jid requester(from);
884 insert_irc_channel_configuration_form(query, requester, to);
885 }
886 this->send_stanza(iq);
887 return true;
888 }
889
handle_room_configuration_form(const XmlNode & query,const std::string & from,const Jid & to,const std::string & id)890 bool BiboumiComponent::handle_room_configuration_form(const XmlNode& query, const std::string &from, const Jid &to, const std::string &id)
891 {
892 Iid iid(to.local, {'#', '&'});
893
894 if (iid.type != Iid::Type::Channel || !to.resource.empty())
895 return false;
896
897 Jid requester(from);
898 if (!handle_irc_channel_configuration_form(*this, query, requester, to))
899 return false;
900
901 Stanza iq("iq");
902 iq["type"] = "result";
903 iq["from"] = to.full();
904 iq["to"] = from;
905 iq["id"] = id;
906
907 this->send_stanza(iq);
908
909 return true;
910 }
911
912 #endif
913
get_user_bridge(const std::string & user_jid)914 Bridge* BiboumiComponent::get_user_bridge(const std::string& user_jid)
915 {
916 auto bare_jid = Jid{user_jid}.bare();
917 try
918 {
919 return this->bridges.at(bare_jid).get();
920 }
921 catch (const std::out_of_range& exception)
922 {
923 return this->bridges.emplace(bare_jid, std::make_unique<Bridge>(bare_jid, *this, this->poller)).first->second.get();
924 }
925 }
926
find_user_bridge(const std::string & full_jid)927 Bridge* BiboumiComponent::find_user_bridge(const std::string& full_jid)
928 {
929 auto bare_jid = Jid{full_jid}.bare();
930 try
931 {
932 return this->bridges.at(bare_jid).get();
933 }
934 catch (const std::out_of_range& exception)
935 {
936 return nullptr;
937 }
938 }
939
get_bridges() const940 std::vector<Bridge*> BiboumiComponent::get_bridges() const
941 {
942 std::vector<Bridge*> res;
943 for (const auto& bridge: this->bridges)
944 res.push_back(bridge.second.get());
945 return res;
946 }
947
send_self_disco_info(const std::string & id,const std::string & jid_to)948 void BiboumiComponent::send_self_disco_info(const std::string& id, const std::string& jid_to)
949 {
950 Stanza iq("iq");
951 {
952 iq["type"] = "result";
953 iq["id"] = id;
954 iq["to"] = jid_to;
955 iq["from"] = this->served_hostname;
956 XmlSubNode query(iq, "query");
957 query["xmlns"] = DISCO_INFO_NS;
958 XmlSubNode identity(query, "identity");
959 identity["category"] = "conference";
960 identity["type"] = "irc";
961 identity["name"] = "Biboumi XMPP-IRC gateway";
962 for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS, STABLE_MUC_ID_NS})
963 {
964 XmlSubNode feature(query, "feature");
965 feature["var"] = ns;
966 }
967 }
968 this->send_stanza(iq);
969 }
970
send_irc_server_disco_info(const std::string & id,const std::string & jid_to,const std::string & jid_from)971 void BiboumiComponent::send_irc_server_disco_info(const std::string& id, const std::string& jid_to, const std::string& jid_from)
972 {
973 Jid from(jid_from);
974 Stanza iq("iq");
975 {
976 iq["type"] = "result";
977 iq["id"] = id;
978 iq["to"] = jid_to;
979 iq["from"] = jid_from;
980 XmlSubNode query(iq, "query");
981 query["xmlns"] = DISCO_INFO_NS;
982 XmlSubNode identity(query, "identity");
983 identity["category"] = "conference";
984 identity["type"] = "irc";
985 identity["name"] = "IRC server " + from.local + " over Biboumi";
986 for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS, STABLE_MUC_ID_NS})
987 {
988 XmlSubNode feature(query, "feature");
989 feature["var"] = ns;
990 }
991 }
992 this->send_stanza(iq);
993 }
994
send_irc_channel_muc_traffic_info(const std::string & id,const std::string & jid_to,const std::string & jid_from)995 void BiboumiComponent::send_irc_channel_muc_traffic_info(const std::string& id, const std::string& jid_to, const std::string& jid_from)
996 {
997 Stanza iq("iq");
998 {
999 iq["type"] = "result";
1000 iq["id"] = id;
1001 iq["from"] = jid_from;
1002 iq["to"] = jid_to;
1003
1004 XmlSubNode query(iq, "query");
1005 query["xmlns"] = DISCO_INFO_NS;
1006 query["node"] = MUC_TRAFFIC_NS;
1007 // We drop all “special” traffic (like xhtml-im, chatstates, etc), so
1008 // don’t include any <feature/>
1009 }
1010 this->send_stanza(iq);
1011 }
1012
send_irc_channel_disco_info(const std::string & id,const std::string & jid_to,const std::string & jid_from,const IrcChannel * irc_channel)1013 void BiboumiComponent::send_irc_channel_disco_info(const std::string& id, const std::string& jid_to,
1014 const std::string& jid_from, const IrcChannel* irc_channel)
1015 {
1016 Jid from(jid_from);
1017 Iid iid(from.local, {});
1018 Stanza iq("iq");
1019 {
1020 iq["type"] = "result";
1021 iq["id"] = id;
1022 iq["to"] = jid_to;
1023 iq["from"] = jid_from;
1024 XmlSubNode query(iq, "query");
1025 query["xmlns"] = DISCO_INFO_NS;
1026 XmlSubNode identity(query, "identity");
1027 identity["category"] = "conference";
1028 identity["type"] = "irc";
1029 identity["name"] = ""s + iid.get_local() + " on " + iid.get_server();
1030 for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS, STABLE_MUC_ID_NS, SELF_PING_FLAG, "muc_nonanonymous", STABLE_ID_NS})
1031 {
1032 XmlSubNode feature(query, "feature");
1033 feature["var"] = ns;
1034 }
1035
1036 XmlSubNode x(query, "x");
1037 x["xmlns"] = DATAFORM_NS;
1038 x["type"] = "result";
1039 {
1040 XmlSubNode field(x, "field");
1041 field["var"] = "FORM_TYPE";
1042 field["type"] = "hidden";
1043 XmlSubNode value(field, "value");
1044 value.set_inner("http://jabber.org/protocol/muc#roominfo");
1045 }
1046
1047 if (irc_channel && irc_channel->joined)
1048 {
1049 XmlSubNode field(x, "field");
1050 field["var"] = "muc#roominfo_occupants";
1051 field["label"] = "Number of occupants";
1052 XmlSubNode value(field, "value");
1053 value.set_inner(std::to_string(irc_channel->get_users().size()));
1054 }
1055 }
1056 this->send_stanza(iq);
1057 }
1058
send_ping_request(const std::string & from,const std::string & jid_to,const std::string & id)1059 void BiboumiComponent::send_ping_request(const std::string& from,
1060 const std::string& jid_to,
1061 const std::string& id)
1062 {
1063 Stanza iq("iq");
1064 {
1065 iq["type"] = "get";
1066 iq["id"] = id;
1067 iq["from"] = from + "@" + this->served_hostname;
1068 iq["to"] = jid_to;
1069 XmlSubNode ping(iq, "ping");
1070 ping["xmlns"] = PING_NS;
1071 }
1072 this->send_stanza(iq);
1073
1074 auto result_cb = [from, id](Bridge* bridge, const Stanza& stanza)
1075 {
1076 Jid to(stanza.get_tag("to"));
1077 if (to.local != from)
1078 {
1079 log_error("Received a corresponding ping result, but the 'to' from "
1080 "the response mismatches the 'from' of the request");
1081 return;
1082 }
1083 const std::string type = stanza.get_tag("type");
1084 const XmlNode* error = stanza.get_child("error", COMPONENT_NS);
1085 // Check if what we receive is considered a valid response. And yes, those errors are valid responses
1086 if (type == "result" ||
1087 (type == "error" && error && (error->get_child("feature-not-implemented", STANZA_NS) ||
1088 error->get_child("service-unavailable", STANZA_NS))))
1089 bridge->send_irc_ping_result({from, bridge}, id);
1090 };
1091 this->waiting_iq[id] = result_cb;
1092 }
1093
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)1094 void BiboumiComponent::send_iq_room_list_result(const std::string& id, const std::string& to_jid,
1095 const std::string& from, const ChannelList& channel_list,
1096 std::vector<ListElement>::const_iterator begin,
1097 std::vector<ListElement>::const_iterator end,
1098 const ResultSetInfo& rs_info)
1099 {
1100 Stanza iq("iq");
1101 {
1102 iq["from"] = from + "@" + this->served_hostname;
1103 iq["to"] = to_jid;
1104 iq["id"] = id;
1105 iq["type"] = "result";
1106 XmlSubNode query(iq, "query");
1107 query["xmlns"] = DISCO_ITEMS_NS;
1108
1109 for (auto it = begin; it != end; ++it)
1110 {
1111 XmlSubNode item(query, "item");
1112 std::string channel_name = it->channel;
1113 xep0106::encode(channel_name);
1114 item["jid"] = channel_name + "@" + this->served_hostname;
1115 }
1116
1117 if ((rs_info.max >= 0 || !rs_info.after.empty() || !rs_info.before.empty()))
1118 {
1119 XmlSubNode set_node(query, "set");
1120 set_node["xmlns"] = RSM_NS;
1121
1122 if (begin != channel_list.channels.cend())
1123 {
1124 XmlSubNode first_node(set_node, "first");
1125 first_node["index"] = std::to_string(std::distance(channel_list.channels.cbegin(), begin));
1126 first_node.set_inner(begin->channel + "@" + this->served_hostname);
1127 }
1128 if (end != channel_list.channels.cbegin())
1129 {
1130 XmlSubNode last_node(set_node, "last");
1131 last_node.set_inner(std::prev(end)->channel + "@" + this->served_hostname);
1132 }
1133 if (channel_list.complete)
1134 {
1135 XmlSubNode count_node(set_node, "count");
1136 count_node.set_inner(std::to_string(channel_list.channels.size()));
1137 }
1138 }
1139 }
1140 this->send_stanza(iq);
1141 }
1142
send_invitation(const std::string & room_target,const std::string & jid_to,const std::string & author_nick)1143 void BiboumiComponent::send_invitation(const std::string& room_target,
1144 const std::string& jid_to,
1145 const std::string& author_nick)
1146 {
1147 if (author_nick.empty())
1148 this->send_invitation_from_fulljid(room_target, jid_to, room_target + "@" + this->served_hostname);
1149 else
1150 this->send_invitation_from_fulljid(room_target, jid_to, room_target + "@" + this->served_hostname + "/" + author_nick);
1151 }
1152
send_invitation_from_fulljid(const std::string & room_target,const std::string & jid_to,const std::string & from)1153 void BiboumiComponent::send_invitation_from_fulljid(const std::string& room_target,
1154 const std::string& jid_to,
1155 const std::string& from)
1156 {
1157 Stanza message("message");
1158 {
1159 message["from"] = room_target + "@" + this->served_hostname;
1160 message["to"] = jid_to;
1161 XmlSubNode x(message, "x");
1162 x["xmlns"] = MUC_USER_NS;
1163 XmlSubNode invite(x, "invite");
1164 invite["from"] = from;
1165 }
1166 this->send_stanza(message);
1167 }
1168
accept_subscription(const std::string & from,const std::string & to)1169 void BiboumiComponent::accept_subscription(const std::string& from, const std::string& to)
1170 {
1171 Stanza presence("presence");
1172 presence["from"] = from;
1173 presence["to"] = to;
1174 presence["id"] = this->next_id();
1175 presence["type"] = "subscribed";
1176 this->send_stanza(presence);
1177 }
1178
ask_subscription(const std::string & from,const std::string & to)1179 void BiboumiComponent::ask_subscription(const std::string& from, const std::string& to)
1180 {
1181 Stanza presence("presence");
1182 presence["from"] = from;
1183 presence["to"] = to;
1184 presence["id"] = this->next_id();
1185 presence["type"] = "subscribe";
1186 this->send_stanza(presence);
1187 }
1188
send_presence_to_contact(const std::string & from,const std::string & to,const std::string & type,const std::string & id)1189 void BiboumiComponent::send_presence_to_contact(const std::string& from, const std::string& to,
1190 const std::string& type, const std::string& id)
1191 {
1192 Stanza presence("presence");
1193 presence["from"] = from;
1194 presence["to"] = to;
1195 if (!type.empty())
1196 presence["type"] = type;
1197 if (!id.empty())
1198 presence["id"] = id;
1199 this->send_stanza(presence);
1200 }
1201
on_irc_client_connected(const std::string & irc_hostname,const std::string & jid)1202 void BiboumiComponent::on_irc_client_connected(const std::string& irc_hostname, const std::string& jid)
1203 {
1204 #ifdef USE_DATABASE
1205 const auto local_jid = irc_hostname + "@" + this->served_hostname;
1206 if (Database::has_roster_item(local_jid, jid))
1207 this->send_presence_to_contact(local_jid, jid, "");
1208 #else
1209 (void)irc_hostname;
1210 (void)jid;
1211 #endif
1212 }
1213
on_irc_client_disconnected(const std::string & irc_hostname,const std::string & jid)1214 void BiboumiComponent::on_irc_client_disconnected(const std::string& irc_hostname, const std::string& jid)
1215 {
1216 #ifdef USE_DATABASE
1217 const auto local_jid = irc_hostname + "@" + this->served_hostname;
1218 if (Database::has_roster_item(local_jid, jid))
1219 this->send_presence_to_contact(irc_hostname + "@" + this->served_hostname, jid, "unavailable");
1220 #else
1221 (void)irc_hostname;
1222 (void)jid;
1223 #endif
1224 }
1225
after_handshake()1226 void BiboumiComponent::after_handshake()
1227 {
1228 XmppComponent::after_handshake();
1229
1230 #ifdef USE_DATABASE
1231 const auto contacts = Database::get_contact_list(this->get_served_hostname());
1232
1233 for (const Database::RosterItem& roster_item: contacts)
1234 {
1235 const auto remote_jid = roster_item.col<Database::RemoteJid>();
1236 // In response, we will receive a presence indicating the
1237 // contact is online, to which we will respond with our own
1238 // presence.
1239 // If the contact removed us from their roster while we were
1240 // offline, we will receive an unsubscribed presence, letting us
1241 // stay in sync.
1242 this->send_presence_to_contact(this->get_served_hostname(), remote_jid, "probe");
1243 }
1244 #endif
1245 }
1246