summaryrefslogtreecommitdiff
path: root/src/xmpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/xmpp')
-rw-r--r--src/xmpp/adhoc_command.cpp81
-rw-r--r--src/xmpp/adhoc_command.hpp44
-rw-r--r--src/xmpp/adhoc_commands_handler.cpp111
-rw-r--r--src/xmpp/adhoc_commands_handler.hpp71
-rw-r--r--src/xmpp/adhoc_session.cpp35
-rw-r--r--src/xmpp/adhoc_session.hpp88
-rw-r--r--src/xmpp/auth.cpp8
-rw-r--r--src/xmpp/auth.hpp6
-rw-r--r--src/xmpp/biboumi_adhoc_commands.cpp694
-rw-r--r--src/xmpp/biboumi_adhoc_commands.hpp3
-rw-r--r--src/xmpp/biboumi_component.cpp437
-rw-r--r--src/xmpp/biboumi_component.hpp19
-rw-r--r--src/xmpp/body.hpp12
-rw-r--r--src/xmpp/jid.cpp152
-rw-r--r--src/xmpp/jid.hpp49
-rw-r--r--src/xmpp/xmpp_component.cpp684
-rw-r--r--src/xmpp/xmpp_component.hpp248
-rw-r--r--src/xmpp/xmpp_parser.cpp172
-rw-r--r--src/xmpp/xmpp_parser.hpp133
-rw-r--r--src/xmpp/xmpp_stanza.cpp229
-rw-r--r--src/xmpp/xmpp_stanza.hpp160
21 files changed, 2936 insertions, 500 deletions
diff --git a/src/xmpp/adhoc_command.cpp b/src/xmpp/adhoc_command.cpp
new file mode 100644
index 0000000..e02bf35
--- /dev/null
+++ b/src/xmpp/adhoc_command.cpp
@@ -0,0 +1,81 @@
+#include <utility>
+#include <xmpp/adhoc_command.hpp>
+#include <xmpp/xmpp_component.hpp>
+#include <utils/reload.hpp>
+
+using namespace std::string_literals;
+
+AdhocCommand::AdhocCommand(std::vector<AdhocStep>&& callbacks, std::string name, const bool admin_only):
+ name(std::move(name)),
+ callbacks(std::move(callbacks)),
+ admin_only(admin_only)
+{
+}
+
+bool AdhocCommand::is_admin_only() const
+{
+ return this->admin_only;
+}
+
+void PingStep1(XmppComponent&, AdhocSession&, XmlNode& command_node)
+{
+ XmlSubNode note(command_node, "note");
+ note["type"] = "info";
+ note.set_inner("Pong");
+}
+
+void HelloStep1(XmppComponent&, AdhocSession&, XmlNode& command_node)
+{
+ XmlSubNode x(command_node, "jabber:x:data:x");
+ x["type"] = "form";
+ XmlSubNode title(x, "title");
+ title.set_inner("Configure your name.");
+ XmlSubNode instructions(x, "instructions");
+ instructions.set_inner("Please provide your name.");
+ XmlSubNode name_field(x, "field");
+ name_field["var"] = "name";
+ name_field["type"] = "text-single";
+ name_field["label"] = "Your name";
+ XmlSubNode required(name_field, "required");
+}
+
+void HelloStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node)
+{
+ // Find out if the name was provided in the form.
+ if (const XmlNode* x = command_node.get_child("x", "jabber:x:data"))
+ {
+ const XmlNode* name_field = nullptr;
+ for (const XmlNode* field: x->get_children("field", "jabber:x:data"))
+ if (field->get_tag("var") == "name")
+ {
+ name_field = field;
+ break;
+ }
+ if (name_field)
+ {
+ if (const XmlNode* value = name_field->get_child("value", "jabber:x:data"))
+ {
+ const std::string value_str = value->get_inner();
+ command_node.delete_all_children();
+ XmlSubNode note(command_node, "note");
+ note["type"] = "info";
+ note.set_inner("Hello "s + value_str + "!"s);
+ return;
+ }
+ }
+ }
+ command_node.delete_all_children();
+ XmlSubNode error(command_node, ADHOC_NS":error");
+ error["type"] = "modify";
+ XmlSubNode condition(error, STANZA_NS":bad-request");
+ session.terminate();
+}
+
+void Reload(XmppComponent&, AdhocSession&, XmlNode& command_node)
+{
+ ::reload_process();
+ command_node.delete_all_children();
+ XmlSubNode note(command_node, "note");
+ note["type"] = "info";
+ note.set_inner("Configuration reloaded.");
+}
diff --git a/src/xmpp/adhoc_command.hpp b/src/xmpp/adhoc_command.hpp
new file mode 100644
index 0000000..c00d9e6
--- /dev/null
+++ b/src/xmpp/adhoc_command.hpp
@@ -0,0 +1,44 @@
+#pragma once
+
+/**
+ * Describe an ad-hoc command.
+ *
+ * Can only have zero or one step for now. When execution is requested, it
+ * can return a result immediately, or provide a form to be filled, and
+ * provide a result once the filled form is received.
+ */
+
+#include <xmpp/adhoc_session.hpp>
+
+#include <functional>
+#include <string>
+
+class AdhocCommand
+{
+ friend class AdhocSession;
+public:
+ AdhocCommand(std::vector<AdhocStep>&& callbacks, std::string name, const bool admin_only);
+ ~AdhocCommand() = default;
+ AdhocCommand(const AdhocCommand&) = default;
+ AdhocCommand(AdhocCommand&&) = default;
+ AdhocCommand& operator=(AdhocCommand&&) = delete;
+ AdhocCommand& operator=(const AdhocCommand&) = delete;
+
+ const std::string name;
+
+ bool is_admin_only() const;
+
+private:
+ /**
+ * A command may have one or more steps. Each step is a different
+ * callback, inserting things into a <command/> XmlNode and calling
+ * methods of an AdhocSession.
+ */
+ std::vector<AdhocStep> callbacks;
+ const bool admin_only;
+};
+
+void PingStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node);
+void HelloStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node);
+void HelloStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node);
+void Reload(XmppComponent&, AdhocSession& session, XmlNode& command_node);
diff --git a/src/xmpp/adhoc_commands_handler.cpp b/src/xmpp/adhoc_commands_handler.cpp
new file mode 100644
index 0000000..040d0ff
--- /dev/null
+++ b/src/xmpp/adhoc_commands_handler.cpp
@@ -0,0 +1,111 @@
+#include <xmpp/adhoc_commands_handler.hpp>
+#include <xmpp/xmpp_component.hpp>
+
+#include <utils/timed_events.hpp>
+#include <logger/logger.hpp>
+#include <config/config.hpp>
+#include <xmpp/jid.hpp>
+
+#include <iostream>
+
+using namespace std::string_literals;
+
+const std::map<const std::string, const AdhocCommand>& AdhocCommandsHandler::get_commands() const
+{
+ return this->commands;
+}
+
+void AdhocCommandsHandler::add_command(std::string name, AdhocCommand command)
+{
+ const auto found = this->commands.find(name);
+ if (found != this->commands.end())
+ throw std::runtime_error("Trying to add an ad-hoc command that already exist: "s + name);
+ this->commands.emplace(std::make_pair(std::move(name), std::move(command)));
+}
+
+XmlNode AdhocCommandsHandler::handle_request(const std::string& executor_jid, const std::string& to, XmlNode command_node)
+{
+ std::string action = command_node.get_tag("action");
+ if (action.empty())
+ action = "execute";
+ command_node.del_tag("action");
+
+ Jid jid(executor_jid);
+
+ const std::string node = command_node.get_tag("node");
+ auto command_it = this->commands.find(node);
+ if (command_it == this->commands.end())
+ {
+ XmlSubNode error(command_node, ADHOC_NS":error");
+ error["type"] = "cancel";
+ XmlSubNode condition(error, STANZA_NS":item-not-found");
+ }
+ else if (command_it->second.is_admin_only() &&
+ Config::get("admin", "") != jid.local + "@" + jid.domain)
+ {
+ XmlSubNode error(command_node, ADHOC_NS":error");
+ error["type"] = "cancel";
+ XmlSubNode condition(error, STANZA_NS":forbidden");
+ }
+ else
+ {
+ std::string sessionid = command_node.get_tag("sessionid");
+ if (sessionid.empty())
+ { // create a new session, with a new id
+ sessionid = XmppComponent::next_id();
+ command_node["sessionid"] = sessionid;
+ this->sessions.emplace(std::piecewise_construct,
+ std::forward_as_tuple(sessionid, executor_jid),
+ std::forward_as_tuple(command_it->second, executor_jid, to));
+ TimedEventsManager::instance().add_event(TimedEvent(std::chrono::steady_clock::now() + 3600s,
+ std::bind(&AdhocCommandsHandler::remove_session, this, sessionid, executor_jid),
+ "adhocsession"s + sessionid + executor_jid));
+ }
+ auto session_it = this->sessions.find(std::make_pair(sessionid, executor_jid));
+ if ((session_it != this->sessions.end()) &&
+ (action == "execute" || action == "next" || action == "complete"))
+ {
+ // execute the step
+ AdhocSession& session = session_it->second;
+ const AdhocStep& step = session.get_next_step();
+ step(this->xmpp_component, session, command_node);
+ if (session.remaining_steps() == 0 ||
+ session.is_terminated())
+ {
+ this->sessions.erase(session_it);
+ command_node["status"] = "completed";
+ TimedEventsManager::instance().cancel("adhocsession"s + sessionid + executor_jid);
+ }
+ else
+ {
+ command_node["status"] = "executing";
+ XmlSubNode actions(command_node, "actions");
+ XmlSubNode next(actions, "next");
+ }
+ }
+ else if (action == "cancel")
+ {
+ this->sessions.erase(session_it);
+ command_node["status"] = "canceled";
+ TimedEventsManager::instance().cancel("adhocsession"s + sessionid + executor_jid);
+ }
+ else // unsupported action
+ {
+ XmlSubNode error(command_node, ADHOC_NS":error");
+ error["type"] = "modify";
+ XmlSubNode condition(error, STANZA_NS":bad-request");
+ }
+ }
+ return command_node;
+}
+
+void AdhocCommandsHandler::remove_session(const std::string& session_id, const std::string& initiator_jid)
+{
+ auto session_it = this->sessions.find(std::make_pair(session_id, initiator_jid));
+ if (session_it != this->sessions.end())
+ {
+ this->sessions.erase(session_it);
+ return ;
+ }
+ log_error("Tried to remove ad-hoc session for [", session_id, ", ", initiator_jid, "] but none found");
+}
diff --git a/src/xmpp/adhoc_commands_handler.hpp b/src/xmpp/adhoc_commands_handler.hpp
new file mode 100644
index 0000000..e37d913
--- /dev/null
+++ b/src/xmpp/adhoc_commands_handler.hpp
@@ -0,0 +1,71 @@
+#pragma once
+
+/**
+ * Manage a list of available AdhocCommands and the list of ongoing
+ * AdhocCommandSessions.
+ */
+
+#include <xmpp/adhoc_command.hpp>
+#include <xmpp/xmpp_stanza.hpp>
+
+#include <utility>
+#include <string>
+#include <map>
+
+class AdhocCommandsHandler
+{
+public:
+ explicit AdhocCommandsHandler(XmppComponent& xmpp_component):
+ xmpp_component(xmpp_component),
+ commands{}
+ { }
+ ~AdhocCommandsHandler() = default;
+
+ AdhocCommandsHandler(const AdhocCommandsHandler&) = delete;
+ AdhocCommandsHandler(AdhocCommandsHandler&&) = delete;
+ AdhocCommandsHandler& operator=(const AdhocCommandsHandler&) = delete;
+ AdhocCommandsHandler& operator=(AdhocCommandsHandler&&) = delete;
+
+ /**
+ * Returns the list of available commands.
+ */
+ const std::map<const std::string, const AdhocCommand>& get_commands() const;
+ /**
+ * Add a command into the list, associated with the given name
+ */
+ void add_command(std::string name, AdhocCommand command);
+ /**
+ * Find the requested command, create a new session or use an existing
+ * one, and process the request (provide a new form, an error, or a
+ * result).
+ *
+ * Returns a (moved) XmlNode that will be inserted in the iq response. It
+ * should be a <command/> node containing one or more useful children. If
+ * it contains an <error/> node, the iq response will have an error type.
+ *
+ * Takes a copy of the <command/> node so we can actually edit it and use
+ * it as our return value.
+ */
+ XmlNode handle_request(const std::string& executor_jid, const std::string& to, XmlNode command_node);
+ /**
+ * Remove the session from the list. This is done to avoid filling the
+ * memory with waiting session (for example due to a client that starts
+ * multi-steps command but never finishes them).
+ */
+ void remove_session(const std::string& session_id, const std::string& initiator_jid);
+private:
+ /**
+ * To access basically anything in the gateway.
+ */
+ XmppComponent& xmpp_component;
+ /**
+ * The list of all available commands.
+ */
+ std::map<const std::string, const AdhocCommand> commands;
+ /**
+ * The list of all currently on-going commands.
+ *
+ * Of the form: {{session_id, owner_jid}, session}.
+ */
+ std::map<std::pair<const std::string, const std::string>, AdhocSession> sessions;
+};
diff --git a/src/xmpp/adhoc_session.cpp b/src/xmpp/adhoc_session.cpp
new file mode 100644
index 0000000..e2d6c0e
--- /dev/null
+++ b/src/xmpp/adhoc_session.cpp
@@ -0,0 +1,35 @@
+#include <xmpp/adhoc_session.hpp>
+#include <xmpp/adhoc_command.hpp>
+
+#include <cassert>
+
+AdhocSession::AdhocSession(const AdhocCommand& command, const std::string& owner_jid,
+ const std::string& to_jid):
+ command(command),
+ owner_jid(owner_jid),
+ to_jid(to_jid),
+ current_step(0),
+ terminated(false)
+{
+}
+
+const AdhocStep& AdhocSession::get_next_step()
+{
+ assert(this->current_step < this->command.callbacks.size());
+ return this->command.callbacks[this->current_step++];
+}
+
+size_t AdhocSession::remaining_steps() const
+{
+ return this->command.callbacks.size() - this->current_step;
+}
+
+bool AdhocSession::is_terminated() const
+{
+ return this->terminated;
+}
+
+void AdhocSession::terminate()
+{
+ this->terminated = true;
+}
diff --git a/src/xmpp/adhoc_session.hpp b/src/xmpp/adhoc_session.hpp
new file mode 100644
index 0000000..0de8d13
--- /dev/null
+++ b/src/xmpp/adhoc_session.hpp
@@ -0,0 +1,88 @@
+#pragma once
+
+#include <xmpp/xmpp_stanza.hpp>
+
+#include <functional>
+#include <string>
+#include <map>
+
+class XmppComponent;
+
+class AdhocCommand;
+class AdhocSession;
+
+/**
+ * A function executed as an ad-hoc command step. It takes a <command/>
+ * XmlNode and modifies it accordingly (inserting for example an <error/>
+ * node, or a data form…).
+ */
+using AdhocStep = std::function<void(XmppComponent&, AdhocSession&, XmlNode&)>;
+
+class AdhocSession
+{
+public:
+ explicit AdhocSession(const AdhocCommand& command, const std::string& owner_jid,
+ const std::string& to_jid);
+ ~AdhocSession() = default;
+
+ AdhocSession(const AdhocSession&) = delete;
+ AdhocSession(AdhocSession&&) = delete;
+ AdhocSession& operator=(const AdhocSession&) = delete;
+ AdhocSession& operator=(AdhocSession&&) = delete;
+
+ /**
+ * Return the function to be executed, found in our AdhocCommand, for the
+ * current_step. And increment the current_step.
+ */
+ const AdhocStep& get_next_step();
+ /**
+ * Return the number of remaining steps.
+ */
+ size_t remaining_steps() const;
+ /**
+ * This may be modified by an AdhocStep, to indicate that this session
+ * should no longer exist, because we encountered an error, and we can't
+ * execute any more step of it.
+ */
+ void terminate();
+ bool is_terminated() const;
+ std::string get_target_jid() const
+ {
+ return this->to_jid;
+ }
+ std::string get_owner_jid() const
+ {
+ return this->owner_jid;
+ }
+
+private:
+ /**
+ * A reference of the command concerned by this session. Used for example
+ * to get the next step of that command, things like that.
+ */
+ const AdhocCommand& command;
+ /**
+ * The full JID of the XMPP user that created this session by executing
+ * the first step of a command. Only that JID must be allowed to access
+ * this session.
+ */
+ const std::string& owner_jid;
+ /**
+ * The 'to' attribute in the request stanza. This is the target of the current session.
+ */
+ const std::string& to_jid;
+ /**
+ * The current step we are at. It starts at zero. It is used to index the
+ * associated AdhocCommand::callbacks vector.
+ */
+ size_t current_step;
+ bool terminated;
+
+public:
+ /**
+ * A map to store various things that we may want to remember between two
+ * steps of the same session. A step can insert any value associated to
+ * any key in there.
+ */
+ std::map<std::string, std::string> vars;
+};
diff --git a/src/xmpp/auth.cpp b/src/xmpp/auth.cpp
new file mode 100644
index 0000000..8a34a4e
--- /dev/null
+++ b/src/xmpp/auth.cpp
@@ -0,0 +1,8 @@
+#include <xmpp/auth.hpp>
+
+#include <utils/sha1.hpp>
+
+std::string get_handshake_digest(const std::string& stream_id, const std::string& secret)
+{
+ return sha1(stream_id + secret);
+}
diff --git a/src/xmpp/auth.hpp b/src/xmpp/auth.hpp
new file mode 100644
index 0000000..34a2116
--- /dev/null
+++ b/src/xmpp/auth.hpp
@@ -0,0 +1,6 @@
+#pragma once
+
+#include <string>
+
+std::string get_handshake_digest(const std::string& stream_id, const std::string& secret);
+
diff --git a/src/xmpp/biboumi_adhoc_commands.cpp b/src/xmpp/biboumi_adhoc_commands.cpp
index 003b901..4129517 100644
--- a/src/xmpp/biboumi_adhoc_commands.cpp
+++ b/src/xmpp/biboumi_adhoc_commands.cpp
@@ -7,6 +7,7 @@
#include <utils/split.hpp>
#include <xmpp/jid.hpp>
#include <algorithm>
+#include <sstream>
#include <iomanip>
#include <biboumi.h>
@@ -23,47 +24,38 @@ using namespace std::string_literals;
void DisconnectUserStep1(XmppComponent& xmpp_component, AdhocSession&, XmlNode& command_node)
{
- auto& biboumi_component = static_cast<BiboumiComponent&>(xmpp_component);
+ auto& biboumi_component = dynamic_cast<BiboumiComponent&>(xmpp_component);
- XmlNode x("jabber:x:data:x");
+ XmlSubNode x(command_node, "jabber:x:data:x");
x["type"] = "form";
- XmlNode title("title");
+ XmlSubNode title(x, "title");
title.set_inner("Disconnect a user from the gateway");
- x.add_child(std::move(title));
- XmlNode instructions("instructions");
+ XmlSubNode instructions(x, "instructions");
instructions.set_inner("Choose a user JID and a quit message");
- x.add_child(std::move(instructions));
- XmlNode jids_field("field");
+ XmlSubNode jids_field(x, "field");
jids_field["var"] = "jids";
jids_field["type"] = "list-multi";
jids_field["label"] = "The JIDs to disconnect";
- XmlNode required("required");
- jids_field.add_child(std::move(required));
+ XmlSubNode required(jids_field, "required");
for (Bridge* bridge: biboumi_component.get_bridges())
{
- XmlNode option("option");
+ XmlSubNode option(jids_field, "option");
option["label"] = bridge->get_jid();
- XmlNode value("value");
+ XmlSubNode value(option, "value");
value.set_inner(bridge->get_jid());
- option.add_child(std::move(value));
- jids_field.add_child(std::move(option));
}
- x.add_child(std::move(jids_field));
- XmlNode message_field("field");
+ XmlSubNode message_field(x, "field");
message_field["var"] = "quit-message";
message_field["type"] = "text-single";
message_field["label"] = "Quit message";
- XmlNode message_value("value");
+ XmlSubNode message_value(message_field, "value");
message_value.set_inner("Disconnected by admin");
- message_field.add_child(std::move(message_value));
- x.add_child(std::move(message_field));
- command_node.add_child(std::move(x));
}
void DisconnectUserStep2(XmppComponent& xmpp_component, AdhocSession& session, XmlNode& command_node)
{
- auto& biboumi_component = static_cast<BiboumiComponent&>(xmpp_component);
+ auto& biboumi_component = dynamic_cast<BiboumiComponent&>(xmpp_component);
// Find out if the jids, and the quit message are provided in the form.
std::string quit_message;
@@ -97,7 +89,7 @@ void DisconnectUserStep2(XmppComponent& xmpp_component, AdhocSession& session, X
}
command_node.delete_all_children();
- XmlNode note("note");
+ XmlSubNode note(command_node, "note");
note["type"] = "info";
if (num == 0)
note.set_inner("No user were disconnected.");
@@ -105,15 +97,12 @@ void DisconnectUserStep2(XmppComponent& xmpp_component, AdhocSession& session, X
note.set_inner("1 user has been disconnected.");
else
note.set_inner(std::to_string(num) + " users have been disconnected.");
- command_node.add_child(std::move(note));
return;
}
}
- XmlNode error(ADHOC_NS":error");
+ XmlSubNode error(command_node, ADHOC_NS":error");
error["type"] = "modify";
- XmlNode condition(STANZA_NS":bad-request");
- error.add_child(std::move(condition));
- command_node.add_child(std::move(error));
+ XmlSubNode condition(error, STANZA_NS":bad-request");
session.terminate();
}
@@ -126,48 +115,61 @@ void ConfigureGlobalStep1(XmppComponent&, AdhocSession& session, XmlNode& comman
auto options = Database::get_global_options(owner.bare());
- XmlNode x("jabber:x:data:x");
+ XmlSubNode x(command_node, "jabber:x:data:x");
x["type"] = "form";
- XmlNode title("title");
+ XmlSubNode title(x, "title");
title.set_inner("Configure some global default settings.");
- x.add_child(std::move(title));
- XmlNode instructions("instructions");
+ XmlSubNode instructions(x, "instructions");
instructions.set_inner("Edit the form, to configure your global settings for the component.");
- x.add_child(std::move(instructions));
-
- XmlNode required("required");
-
- XmlNode max_histo_length("field");
- max_histo_length["var"] = "max_history_length";
- max_histo_length["type"] = "text-single";
- max_histo_length["label"] = "Max history length";
- max_histo_length["desc"] = "The maximum number of lines in the history that the server sends when joining a channel";
-
- XmlNode value("value");
- value.set_inner(std::to_string(options.maxHistoryLength.value()));
- max_histo_length.add_child(std::move(value));
- x.add_child(std::move(max_histo_length));
-
- XmlNode record_history("field");
- record_history["var"] = "record_history";
- record_history["type"] = "boolean";
- record_history["label"] = "Record history";
- record_history["desc"] = "Whether to save the messages into the database, or not";
-
- value.set_name("value");
- if (options.recordHistory.value())
- value.set_inner("true");
- else
- value.set_inner("false");
- record_history.add_child(std::move(value));
- x.add_child(std::move(record_history));
- command_node.add_child(std::move(x));
+ {
+ XmlSubNode max_histo_length(x, "field");
+ max_histo_length["var"] = "max_history_length";
+ max_histo_length["type"] = "text-single";
+ max_histo_length["label"] = "Max history length";
+ max_histo_length["desc"] = "The maximum number of lines in the history that the server sends when joining a channel";
+ {
+ XmlSubNode value(max_histo_length, "value");
+ value.set_inner(std::to_string(options.col<Database::MaxHistoryLength>()));
+ }
+ }
+
+ {
+ XmlSubNode record_history(x, "field");
+ record_history["var"] = "record_history";
+ record_history["type"] = "boolean";
+ record_history["label"] = "Record history";
+ record_history["desc"] = "Whether to save the messages into the database, or not";
+ {
+ XmlSubNode value(record_history, "value");
+ value.set_name("value");
+ if (options.col<Database::RecordHistory>())
+ value.set_inner("true");
+ else
+ value.set_inner("false");
+ }
+ }
+
+ {
+ XmlSubNode persistent(x, "field");
+ persistent["var"] = "persistent";
+ persistent["type"] = "boolean";
+ persistent["label"] = "Make all channels persistent";
+ persistent["desc"] = "If true, all channels will be persistent";
+ {
+ XmlSubNode value(persistent, "value");
+ value.set_name("value");
+ if (options.col<Database::Persistent>())
+ value.set_inner("true");
+ else
+ value.set_inner("false");
+ }
+ }
}
void ConfigureGlobalStep2(XmppComponent& xmpp_component, AdhocSession& session, XmlNode& command_node)
{
- BiboumiComponent& biboumi_component = static_cast<BiboumiComponent&>(xmpp_component);
+ auto& biboumi_component = dynamic_cast<BiboumiComponent&>(xmpp_component);
const XmlNode* x = command_node.get_child("x", "jabber:x:data");
if (x)
@@ -180,31 +182,31 @@ void ConfigureGlobalStep2(XmppComponent& xmpp_component, AdhocSession& session,
if (field->get_tag("var") == "max_history_length" &&
value && !value->get_inner().empty())
- options.maxHistoryLength = value->get_inner();
+ options.col<Database::MaxHistoryLength>() = atoi(value->get_inner().data());
else if (field->get_tag("var") == "record_history" &&
value && !value->get_inner().empty())
{
- options.recordHistory = to_bool(value->get_inner());
+ options.col<Database::RecordHistory>() = to_bool(value->get_inner());
Bridge* bridge = biboumi_component.find_user_bridge(owner.bare());
if (bridge)
- bridge->set_record_history(options.recordHistory.value());
+ bridge->set_record_history(options.col<Database::RecordHistory>());
}
+ else if (field->get_tag("var") == "persistent" &&
+ value)
+ options.col<Database::Persistent>() = to_bool(value->get_inner());
}
- options.update();
+ options.save(Database::db);
command_node.delete_all_children();
- XmlNode note("note");
+ XmlSubNode note(command_node, "note");
note["type"] = "info";
note.set_inner("Configuration successfully applied.");
- command_node.add_child(std::move(note));
return;
}
- XmlNode error(ADHOC_NS":error");
+ XmlSubNode error(command_node, ADHOC_NS":error");
error["type"] = "modify";
- XmlNode condition(STANZA_NS":bad-request");
- error.add_child(std::move(condition));
- command_node.add_child(std::move(error));
+ XmlSubNode condition(error, STANZA_NS":bad-request");
session.terminate();
}
@@ -218,161 +220,143 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
auto options = Database::get_irc_server_options(owner.local + "@" + owner.domain,
server_domain);
- XmlNode x("jabber:x:data:x");
+ XmlSubNode x(command_node, "jabber:x:data:x");
x["type"] = "form";
- XmlNode title("title");
+ XmlSubNode title(x, "title");
title.set_inner("Configure the IRC server "s + server_domain);
- x.add_child(std::move(title));
- XmlNode instructions("instructions");
+ XmlSubNode instructions(x, "instructions");
instructions.set_inner("Edit the form, to configure the settings of the IRC server "s + server_domain);
- x.add_child(std::move(instructions));
-
- XmlNode required("required");
- XmlNode ports("field");
- ports["var"] = "ports";
- ports["type"] = "text-multi";
- ports["label"] = "Ports";
- ports["desc"] = "List of ports to try, without TLS. Defaults: 6667.";
- auto vals = utils::split(options.ports.value(), ';', false);
- for (const auto& val: vals)
- {
- XmlNode ports_value("value");
- ports_value.set_inner(val);
- ports.add_child(std::move(ports_value));
- }
- ports.add_child(required);
- x.add_child(std::move(ports));
+ {
+ XmlSubNode ports(x, "field");
+ ports["var"] = "ports";
+ ports["type"] = "text-multi";
+ ports["label"] = "Ports";
+ ports["desc"] = "List of ports to try, without TLS. Defaults: 6667.";
+ for (const auto& val: utils::split(options.col<Database::Ports>(), ';', false))
+ {
+ XmlSubNode ports_value(ports, "value");
+ ports_value.set_inner(val);
+ }
+ }
#ifdef BOTAN_FOUND
- XmlNode tls_ports("field");
- tls_ports["var"] = "tls_ports";
- tls_ports["type"] = "text-multi";
- tls_ports["label"] = "TLS ports";
- tls_ports["desc"] = "List of ports to try, with TLS. Defaults: 6697, 6670.";
- vals = utils::split(options.tlsPorts.value(), ';', false);
- for (const auto& val: vals)
- {
- XmlNode tls_ports_value("value");
- tls_ports_value.set_inner(val);
- tls_ports.add_child(std::move(tls_ports_value));
- }
- tls_ports.add_child(required);
- x.add_child(std::move(tls_ports));
-
- XmlNode verify_cert("field");
- verify_cert["var"] = "verify_cert";
- verify_cert["type"] = "boolean";
- verify_cert["label"] = "Verify certificate";
- verify_cert["desc"] = "Whether or not to abort the connection if the server’s TLS certificate is invalid";
- XmlNode verify_cert_value("value");
- if (options.verifyCert.value())
- verify_cert_value.set_inner("true");
- else
- verify_cert_value.set_inner("false");
- verify_cert.add_child(std::move(verify_cert_value));
- x.add_child(std::move(verify_cert));
-
- XmlNode fingerprint("field");
- fingerprint["var"] = "fingerprint";
- fingerprint["type"] = "text-single";
- fingerprint["label"] = "SHA-1 fingerprint of the TLS certificate to trust.";
- if (!options.trustedFingerprint.value().empty())
- {
- XmlNode fingerprint_value("value");
- fingerprint_value.set_inner(options.trustedFingerprint.value());
- fingerprint.add_child(std::move(fingerprint_value));
- }
- fingerprint.add_child(required);
- x.add_child(std::move(fingerprint));
+ {
+ XmlSubNode tls_ports(x, "field");
+ tls_ports["var"] = "tls_ports";
+ tls_ports["type"] = "text-multi";
+ tls_ports["label"] = "TLS ports";
+ tls_ports["desc"] = "List of ports to try, with TLS. Defaults: 6697, 6670.";
+ for (const auto& val: utils::split(options.col<Database::TlsPorts>(), ';', false))
+ {
+ XmlSubNode tls_ports_value(tls_ports, "value");
+ tls_ports_value.set_inner(val);
+ }
+ }
+
+ {
+ XmlSubNode verify_cert(x, "field");
+ verify_cert["var"] = "verify_cert";
+ verify_cert["type"] = "boolean";
+ verify_cert["label"] = "Verify certificate";
+ verify_cert["desc"] = "Whether or not to abort the connection if the server’s TLS certificate is invalid";
+ XmlSubNode verify_cert_value(verify_cert, "value");
+ if (options.col<Database::VerifyCert>())
+ verify_cert_value.set_inner("true");
+ else
+ verify_cert_value.set_inner("false");
+ }
+
+ {
+ XmlSubNode fingerprint(x, "field");
+ fingerprint["var"] = "fingerprint";
+ fingerprint["type"] = "text-single";
+ fingerprint["label"] = "SHA-1 fingerprint of the TLS certificate to trust.";
+ if (!options.col<Database::TrustedFingerprint>().empty())
+ {
+ XmlSubNode fingerprint_value(fingerprint, "value");
+ fingerprint_value.set_inner(options.col<Database::TrustedFingerprint>());
+ }
+ }
#endif
-
- XmlNode pass("field");
- pass["var"] = "pass";
- pass["type"] = "text-private";
- pass["label"] = "Server password (to be used in a PASS command when connecting)";
- if (!options.pass.value().empty())
- {
- XmlNode pass_value("value");
- pass_value.set_inner(options.pass.value());
- pass.add_child(std::move(pass_value));
- }
- pass.add_child(required);
- x.add_child(std::move(pass));
-
- XmlNode after_cnt_cmd("field");
- after_cnt_cmd["var"] = "after_connect_command";
- after_cnt_cmd["type"] = "text-single";
- after_cnt_cmd["desc"] = "Custom IRC command sent after the connection is established with the server.";
- after_cnt_cmd["label"] = "After-connection IRC command";
- if (!options.afterConnectionCommand.value().empty())
- {
- XmlNode after_cnt_cmd_value("value");
- after_cnt_cmd_value.set_inner(options.afterConnectionCommand.value());
- after_cnt_cmd.add_child(std::move(after_cnt_cmd_value));
- }
- after_cnt_cmd.add_child(required);
- x.add_child(std::move(after_cnt_cmd));
+ {
+ XmlSubNode pass(x, "field");
+ pass["var"] = "pass";
+ pass["type"] = "text-private";
+ pass["label"] = "Server password";
+ pass["desc"] = "Will be used in a PASS command when connecting";
+ if (!options.col<Database::Pass>().empty())
+ {
+ XmlSubNode pass_value(pass, "value");
+ pass_value.set_inner(options.col<Database::Pass>());
+ }
+ }
+
+ {
+ XmlSubNode after_cnt_cmd(x, "field");
+ after_cnt_cmd["var"] = "after_connect_command";
+ after_cnt_cmd["type"] = "text-single";
+ after_cnt_cmd["desc"] = "Custom IRC command sent after the connection is established with the server.";
+ after_cnt_cmd["label"] = "After-connection IRC command";
+ if (!options.col<Database::AfterConnectionCommand>().empty())
+ {
+ XmlSubNode after_cnt_cmd_value(after_cnt_cmd, "value");
+ after_cnt_cmd_value.set_inner(options.col<Database::AfterConnectionCommand>());
+ }
+ }
if (Config::get("realname_customization", "true") == "true")
{
- XmlNode username("field");
- username["var"] = "username";
- username["type"] = "text-single";
- username["label"] = "Username";
- if (!options.username.value().empty())
- {
- XmlNode username_value("value");
- username_value.set_inner(options.username.value());
- username.add_child(std::move(username_value));
- }
- username.add_child(required);
- x.add_child(std::move(username));
-
- XmlNode realname("field");
- realname["var"] = "realname";
- realname["type"] = "text-single";
- realname["label"] = "Realname";
- if (!options.realname.value().empty())
- {
- XmlNode realname_value("value");
- realname_value.set_inner(options.realname.value());
- realname.add_child(std::move(realname_value));
- }
- realname.add_child(required);
- x.add_child(std::move(realname));
+ {
+ XmlSubNode username(x, "field");
+ username["var"] = "username";
+ username["type"] = "text-single";
+ username["label"] = "Username";
+ if (!options.col<Database::Username>().empty())
+ {
+ XmlSubNode username_value(username, "value");
+ username_value.set_inner(options.col<Database::Username>());
+ }
+ }
+
+ {
+ XmlSubNode realname(x, "field");
+ realname["var"] = "realname";
+ realname["type"] = "text-single";
+ realname["label"] = "Realname";
+ if (!options.col<Database::Realname>().empty())
+ {
+ XmlSubNode realname_value(realname, "value");
+ realname_value.set_inner(options.col<Database::Realname>());
+ }
+ }
}
- XmlNode encoding_out("field");
+ {
+ XmlSubNode encoding_out(x, "field");
encoding_out["var"] = "encoding_out";
encoding_out["type"] = "text-single";
encoding_out["desc"] = "The encoding used when sending messages to the IRC server.";
encoding_out["label"] = "Out encoding";
- if (!options.encodingOut.value().empty())
- {
- XmlNode encoding_out_value("value");
- encoding_out_value.set_inner(options.encodingOut.value());
- encoding_out.add_child(std::move(encoding_out_value));
- }
- encoding_out.add_child(required);
- x.add_child(std::move(encoding_out));
-
- XmlNode encoding_in("field");
- encoding_in["var"] = "encoding_in";
- encoding_in["type"] = "text-single";
- encoding_in["desc"] = "The encoding used to decode message received from the IRC server.";
- encoding_in["label"] = "In encoding";
- if (!options.encodingIn.value().empty())
+ if (!options.col<Database::EncodingOut>().empty())
{
- XmlNode encoding_in_value("value");
- encoding_in_value.set_inner(options.encodingIn.value());
- encoding_in.add_child(std::move(encoding_in_value));
+ XmlSubNode encoding_out_value(encoding_out, "value");
+ encoding_out_value.set_inner(options.col<Database::EncodingOut>());
}
- encoding_in.add_child(required);
- x.add_child(std::move(encoding_in));
-
-
- command_node.add_child(std::move(x));
+ }
+
+ {
+ XmlSubNode encoding_in(x, "field");
+ encoding_in["var"] = "encoding_in";
+ encoding_in["type"] = "text-single";
+ encoding_in["desc"] = "The encoding used to decode message received from the IRC server.";
+ encoding_in["label"] = "In encoding";
+ if (!options.col<Database::EncodingIn>().empty())
+ {
+ XmlSubNode encoding_in_value(encoding_in, "value");
+ encoding_in_value.set_inner(options.col<Database::EncodingIn>());
+ }
+ }
}
void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node)
@@ -396,7 +380,7 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com
std::string ports;
for (const auto& val: values)
ports += val->get_inner() + ";";
- options.ports = ports;
+ options.col<Database::Ports>() = ports;
}
#ifdef BOTAN_FOUND
@@ -405,31 +389,31 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com
std::string ports;
for (const auto& val: values)
ports += val->get_inner() + ";";
- options.tlsPorts = ports;
+ options.col<Database::TlsPorts>() = ports;
}
else if (field->get_tag("var") == "verify_cert" && value
&& !value->get_inner().empty())
{
auto val = to_bool(value->get_inner());
- options.verifyCert = val;
+ options.col<Database::VerifyCert>() = val;
}
else if (field->get_tag("var") == "fingerprint" && value &&
!value->get_inner().empty())
{
- options.trustedFingerprint = value->get_inner();
+ options.col<Database::TrustedFingerprint>() = value->get_inner();
}
#endif // BOTAN_FOUND
else if (field->get_tag("var") == "pass" &&
value && !value->get_inner().empty())
- options.pass = value->get_inner();
+ options.col<Database::Pass>() = value->get_inner();
else if (field->get_tag("var") == "after_connect_command" &&
value && !value->get_inner().empty())
- options.afterConnectionCommand = value->get_inner();
+ options.col<Database::AfterConnectionCommand>() = value->get_inner();
else if (field->get_tag("var") == "username" &&
value && !value->get_inner().empty())
@@ -437,37 +421,34 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com
auto username = value->get_inner();
// The username must not contain spaces
std::replace(username.begin(), username.end(), ' ', '_');
- options.username = username;
+ options.col<Database::Username>() = username;
}
else if (field->get_tag("var") == "realname" &&
value && !value->get_inner().empty())
- options.realname = value->get_inner();
+ options.col<Database::Realname>() = value->get_inner();
else if (field->get_tag("var") == "encoding_out" &&
value && !value->get_inner().empty())
- options.encodingOut = value->get_inner();
+ options.col<Database::EncodingOut>() = value->get_inner();
else if (field->get_tag("var") == "encoding_in" &&
value && !value->get_inner().empty())
- options.encodingIn = value->get_inner();
+ options.col<Database::EncodingIn>() = value->get_inner();
}
- options.update();
+ options.save(Database::db);
command_node.delete_all_children();
- XmlNode note("note");
+ XmlSubNode note(command_node, "note");
note["type"] = "info";
note.set_inner("Configuration successfully applied.");
- command_node.add_child(std::move(note));
return;
}
- XmlNode error(ADHOC_NS":error");
+ XmlSubNode error(command_node, ADHOC_NS":error");
error["type"] = "modify";
- XmlNode condition(STANZA_NS":bad-request");
- error.add_child(std::move(condition));
- command_node.add_child(std::move(error));
+ XmlSubNode condition(error, STANZA_NS":bad-request");
session.terminate();
}
@@ -475,90 +456,165 @@ void ConfigureIrcChannelStep1(XmppComponent&, AdhocSession& session, XmlNode& co
{
const Jid owner(session.get_owner_jid());
const Jid target(session.get_target_jid());
+
+ insert_irc_channel_configuration_form(command_node, owner, target);
+}
+
+void insert_irc_channel_configuration_form(XmlNode& node, const Jid& requester, const Jid& target)
+{
const Iid iid(target.local, {});
- auto options = Database::get_irc_channel_options_with_server_default(owner.local + "@" + owner.domain,
- iid.get_server(), iid.get_local());
- XmlNode x("jabber:x:data:x");
+ auto options = Database::get_irc_channel_options_with_server_default(requester.local + "@" + requester.domain,
+ iid.get_server(), iid.get_local());
+ XmlSubNode x(node, "jabber:x:data:x");
x["type"] = "form";
- XmlNode title("title");
+ XmlSubNode title(x, "title");
title.set_inner("Configure the IRC channel "s + iid.get_local() + " on server "s + iid.get_server());
- x.add_child(std::move(title));
- XmlNode instructions("instructions");
+ XmlSubNode instructions(x, "instructions");
instructions.set_inner("Edit the form, to configure the settings of the IRC channel "s + iid.get_local());
- x.add_child(std::move(instructions));
-
- XmlNode required("required");
- XmlNode encoding_out("field");
- encoding_out["var"] = "encoding_out";
- encoding_out["type"] = "text-single";
- encoding_out["desc"] = "The encoding used when sending messages to the IRC server. Defaults to the server's “out encoding” if unset for the channel";
- encoding_out["label"] = "Out encoding";
- if (!options.encodingOut.value().empty())
+ {
+ XmlSubNode record_history(x, "field");
+ record_history["var"] = "record_history";
+ record_history["type"] = "list-single";
+ record_history["label"] = "Record history for this channel";
+ record_history["desc"] = "If unset, the value is the one configured globally";
{
- XmlNode encoding_out_value("value");
- encoding_out_value.set_inner(options.encodingOut.value());
- encoding_out.add_child(std::move(encoding_out_value));
+ // Value selected by default
+ XmlSubNode value(record_history, "value");
+ value.set_inner(options.col<Database::RecordHistoryOptional>().to_string());
}
- encoding_out.add_child(required);
- x.add_child(std::move(encoding_out));
-
- XmlNode encoding_in("field");
- encoding_in["var"] = "encoding_in";
- encoding_in["type"] = "text-single";
- encoding_in["desc"] = "The encoding used to decode message received from the IRC server. Defaults to the server's “in encoding” if unset for the channel";
- encoding_in["label"] = "In encoding";
- if (!options.encodingIn.value().empty())
+ // All three possible values
+ for (const auto& val: {"unset", "true", "false"})
+ {
+ XmlSubNode option(record_history, "option");
+ option["label"] = val;
+ XmlSubNode value(option, "value");
+ value.set_inner(val);
+ }
+ }
+
+ {
+ XmlSubNode encoding_out(x, "field");
+ encoding_out["var"] = "encoding_out";
+ encoding_out["type"] = "text-single";
+ encoding_out["desc"] = "The encoding used when sending messages to the IRC server. Defaults to the server's “out encoding” if unset for the channel";
+ encoding_out["label"] = "Out encoding";
+ if (!options.col<Database::EncodingOut>().empty())
+ {
+ XmlSubNode encoding_out_value(encoding_out, "value");
+ encoding_out_value.set_inner(options.col<Database::EncodingOut>());
+ }
+ }
+
+ {
+ XmlSubNode encoding_in(x, "field");
+ encoding_in["var"] = "encoding_in";
+ encoding_in["type"] = "text-single";
+ encoding_in["desc"] = "The encoding used to decode message received from the IRC server. Defaults to the server's “in encoding” if unset for the channel";
+ encoding_in["label"] = "In encoding";
+ if (!options.col<Database::EncodingIn>().empty())
+ {
+ XmlSubNode encoding_in_value(encoding_in, "value");
+ encoding_in_value.set_inner(options.col<Database::EncodingIn>());
+ }
+ }
+
+ {
+ XmlSubNode persistent(x, "field");
+ persistent["var"] = "persistent";
+ persistent["type"] = "boolean";
+ persistent["desc"] = "If set to true, when all XMPP clients have left this channel, biboumi will stay idle in it, without sending a PART command.";
+ persistent["label"] = "Persistent";
{
- XmlNode encoding_in_value("value");
- encoding_in_value.set_inner(options.encodingIn.value());
- encoding_in.add_child(std::move(encoding_in_value));
+ XmlSubNode value(persistent, "value");
+ value.set_name("value");
+ if (options.col<Database::Persistent>())
+ value.set_inner("true");
+ else
+ value.set_inner("false");
}
- encoding_in.add_child(required);
- x.add_child(std::move(encoding_in));
+ }
+}
+
+void ConfigureIrcChannelStep2(XmppComponent& xmpp_component, AdhocSession& session, XmlNode& command_node)
+{
+ const Jid owner(session.get_owner_jid());
+ const Jid target(session.get_target_jid());
- command_node.add_child(std::move(x));
+ if (handle_irc_channel_configuration_form(xmpp_component, command_node, owner, target))
+ {
+ command_node.delete_all_children();
+ XmlSubNode note(command_node, "note");
+ note["type"] = "info";
+ note.set_inner("Configuration successfully applied.");
+ }
+ else
+ {
+ XmlSubNode error(command_node, ADHOC_NS":error");
+ error["type"] = "modify";
+ XmlSubNode condition(error, STANZA_NS":bad-request");
+ session.terminate();
+ }
}
-void ConfigureIrcChannelStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node)
+bool handle_irc_channel_configuration_form(XmppComponent& xmpp_component, const XmlNode& node, const Jid& requester, const Jid& target)
{
- const XmlNode* x = command_node.get_child("x", "jabber:x:data");
+ const XmlNode* x = node.get_child("x", "jabber:x:data");
if (x)
{
- const Jid owner(session.get_owner_jid());
- const Jid target(session.get_target_jid());
- const Iid iid(target.local, {});
- auto options = Database::get_irc_channel_options(owner.local + "@" + owner.domain,
- iid.get_server(), iid.get_local());
- for (const XmlNode* field: x->get_children("field", "jabber:x:data"))
+ if (x->get_tag("type") == "submit")
{
- const XmlNode* value = field->get_child("value", "jabber:x:data");
+ const Iid iid(target.local, {});
+ auto options = Database::get_irc_channel_options(requester.bare(),
+ iid.get_server(), iid.get_local());
+ for (const XmlNode *field: x->get_children("field", "jabber:x:data"))
+ {
+ const XmlNode *value = field->get_child("value", "jabber:x:data");
- if (field->get_tag("var") == "encoding_out" &&
- value && !value->get_inner().empty())
- options.encodingOut = value->get_inner();
+ if (field->get_tag("var") == "encoding_out" &&
+ value && !value->get_inner().empty())
+ options.col<Database::EncodingOut>() = value->get_inner();
- else if (field->get_tag("var") == "encoding_in" &&
- value && !value->get_inner().empty())
- options.encodingIn = value->get_inner();
- }
+ else if (field->get_tag("var") == "encoding_in" &&
+ value && !value->get_inner().empty())
+ options.col<Database::EncodingIn>() = value->get_inner();
- options.update();
+ else if (field->get_tag("var") == "persistent" &&
+ value)
+ options.col<Database::Persistent>() = to_bool(value->get_inner());
+ else if (field->get_tag("var") == "record_history" &&
+ value && !value->get_inner().empty())
+ {
+ OptionalBool& database_value = options.col<Database::RecordHistoryOptional>();
+ if (value->get_inner() == "true")
+ database_value.set_value(true);
+ else if (value->get_inner() == "false")
+ database_value.set_value(false);
+ else
+ database_value.unset();
+ auto& biboumi_component = dynamic_cast<BiboumiComponent&>(xmpp_component);
+ Bridge* bridge = biboumi_component.find_user_bridge(requester.bare());
+ if (bridge)
+ {
+ if (database_value.is_set)
+ bridge->set_record_history(database_value.value);
+ else
+ { // It is unset, we need to fetch the Global option, to
+ // know if it’s enabled or not
+ auto g_options = Database::get_global_options(requester.bare());
+ bridge->set_record_history(g_options.col<Database::RecordHistory>());
+ }
+ }
+ }
- command_node.delete_all_children();
- XmlNode note("note");
- note["type"] = "info";
- note.set_inner("Configuration successfully applied.");
- command_node.add_child(std::move(note));
- return;
+ }
+
+ options.save(Database::db);
+ }
+ return true;
}
- XmlNode error(ADHOC_NS":error");
- error["type"] = "modify";
- XmlNode condition(STANZA_NS":bad-request");
- error.add_child(std::move(condition));
- command_node.add_child(std::move(error));
- session.terminate();
+ return false;
}
#endif // USE_DATABASE
@@ -573,33 +629,26 @@ void DisconnectUserFromServerStep1(XmppComponent& xmpp_component, AdhocSession&
}
else
{ // Send a form to select the user to disconnect
- auto& biboumi_component = static_cast<BiboumiComponent&>(xmpp_component);
+ auto& biboumi_component = dynamic_cast<BiboumiComponent&>(xmpp_component);
- XmlNode x("jabber:x:data:x");
+ XmlSubNode x(command_node, "jabber:x:data:x");
x["type"] = "form";
- XmlNode title("title");
+ XmlSubNode title(x, "title");
title.set_inner("Disconnect a user from selected IRC servers");
- x.add_child(std::move(title));
- XmlNode instructions("instructions");
+ XmlSubNode instructions(x, "instructions");
instructions.set_inner("Choose a user JID");
- x.add_child(std::move(instructions));
- XmlNode jids_field("field");
+ XmlSubNode jids_field(x, "field");
jids_field["var"] = "jid";
jids_field["type"] = "list-single";
jids_field["label"] = "The JID to disconnect";
- XmlNode required("required");
- jids_field.add_child(std::move(required));
+ XmlSubNode required(jids_field, "required");
for (Bridge* bridge: biboumi_component.get_bridges())
{
- XmlNode option("option");
+ XmlSubNode option(jids_field, "option");
option["label"] = bridge->get_jid();
- XmlNode value("value");
+ XmlSubNode value(option, "value");
value.set_inner(bridge->get_jid());
- option.add_child(std::move(value));
- jids_field.add_child(std::move(option));
}
- x.add_child(std::move(jids_field));
- command_node.add_child(std::move(x));
}
}
@@ -625,55 +674,44 @@ void DisconnectUserFromServerStep2(XmppComponent& xmpp_component, AdhocSession&
// Send a data form to let the user choose which server to disconnect the
// user from
command_node.delete_all_children();
- auto& biboumi_component = static_cast<BiboumiComponent&>(xmpp_component);
+ auto& biboumi_component = dynamic_cast<BiboumiComponent&>(xmpp_component);
- XmlNode x("jabber:x:data:x");
+ XmlSubNode x(command_node, "jabber:x:data:x");
x["type"] = "form";
- XmlNode title("title");
+ XmlSubNode title(x, "title");
title.set_inner("Disconnect a user from selected IRC servers");
- x.add_child(std::move(title));
- XmlNode instructions("instructions");
+ XmlSubNode instructions(x, "instructions");
instructions.set_inner("Choose one or more servers to disconnect this JID from");
- x.add_child(std::move(instructions));
- XmlNode jids_field("field");
+ XmlSubNode jids_field(x, "field");
jids_field["var"] = "irc-servers";
jids_field["type"] = "list-multi";
jids_field["label"] = "The servers to disconnect from";
- XmlNode required("required");
- jids_field.add_child(std::move(required));
+ XmlSubNode required(jids_field, "required");
Bridge* bridge = biboumi_component.find_user_bridge(jid_to_disconnect);
if (!bridge || bridge->get_irc_clients().empty())
{
- XmlNode note("note");
+ XmlSubNode note(command_node, "note");
note["type"] = "info";
note.set_inner("User "s + jid_to_disconnect + " is not connected to any IRC server.");
- command_node.add_child(std::move(note));
session.terminate();
return ;
}
for (const auto& pair: bridge->get_irc_clients())
{
- XmlNode option("option");
+ XmlSubNode option(jids_field, "option");
option["label"] = pair.first;
- XmlNode value("value");
+ XmlSubNode value(option, "value");
value.set_inner(pair.first);
- option.add_child(std::move(value));
- jids_field.add_child(std::move(option));
}
- x.add_child(std::move(jids_field));
- XmlNode message_field("field");
+ XmlSubNode message_field(x, "field");
message_field["var"] = "quit-message";
message_field["type"] = "text-single";
message_field["label"] = "Quit message";
- XmlNode message_value("value");
+ XmlSubNode message_value(message_field, "value");
message_value.set_inner("Killed by admin");
- message_field.add_child(std::move(message_value));
- x.add_child(std::move(message_field));
-
- command_node.add_child(std::move(x));
}
void DisconnectUserFromServerStep3(XmppComponent& xmpp_component, AdhocSession& session, XmlNode& command_node)
@@ -701,7 +739,7 @@ void DisconnectUserFromServerStep3(XmppComponent& xmpp_component, AdhocSession&
}
}
- auto& biboumi_component = static_cast<BiboumiComponent&>(xmpp_component);
+ auto& biboumi_component = dynamic_cast<BiboumiComponent&>(xmpp_component);
Bridge* bridge = biboumi_component.find_user_bridge(jid_to_disconnect);
auto& clients = bridge->get_irc_clients();
@@ -718,19 +756,18 @@ void DisconnectUserFromServerStep3(XmppComponent& xmpp_component, AdhocSession&
}
}
command_node.delete_all_children();
- XmlNode note("note");
+ XmlSubNode note(command_node, "note");
note["type"] = "info";
std::string msg = jid_to_disconnect + " was disconnected from " + std::to_string(number) + " IRC server";
if (number > 1)
msg += "s";
msg += ".";
note.set_inner(msg);
- command_node.add_child(std::move(note));
}
void GetIrcConnectionInfoStep1(XmppComponent& component, AdhocSession& session, XmlNode& command_node)
{
- BiboumiComponent& biboumi_component = static_cast<BiboumiComponent&>(component);
+ auto& biboumi_component = dynamic_cast<BiboumiComponent&>(component);
const Jid owner(session.get_owner_jid());
const Jid target(session.get_target_jid());
@@ -741,10 +778,9 @@ void GetIrcConnectionInfoStep1(XmppComponent& component, AdhocSession& session,
utils::ScopeGuard sg([&message, &command_node]()
{
command_node.delete_all_children();
- XmlNode note("note");
+ XmlSubNode note(command_node, "note");
note["type"] = "info";
note.set_inner(message);
- command_node.add_child(std::move(note));
});
Bridge* bridge = biboumi_component.get_user_bridge(owner.bare());
diff --git a/src/xmpp/biboumi_adhoc_commands.hpp b/src/xmpp/biboumi_adhoc_commands.hpp
index b5fce61..cb6acb9 100644
--- a/src/xmpp/biboumi_adhoc_commands.hpp
+++ b/src/xmpp/biboumi_adhoc_commands.hpp
@@ -4,6 +4,7 @@
#include <xmpp/adhoc_command.hpp>
#include <xmpp/adhoc_session.hpp>
#include <xmpp/xmpp_stanza.hpp>
+#include <xmpp/jid.hpp>
class XmppComponent;
@@ -17,7 +18,9 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node);
void ConfigureIrcChannelStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node);
+void insert_irc_channel_configuration_form(XmlNode& node, const Jid& requester, const Jid& target);
void ConfigureIrcChannelStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node);
+bool handle_irc_channel_configuration_form(XmppComponent&, const XmlNode& node, const Jid& requester, const Jid& target);
void DisconnectUserFromServerStep1(XmppComponent&, AdhocSession& session, XmlNode& command_node);
void DisconnectUserFromServerStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node);
diff --git a/src/xmpp/biboumi_component.cpp b/src/xmpp/biboumi_component.cpp
index d6782e2..32f3968 100644
--- a/src/xmpp/biboumi_component.cpp
+++ b/src/xmpp/biboumi_component.cpp
@@ -8,7 +8,6 @@
#include <xmpp/biboumi_adhoc_commands.hpp>
#include <bridge/list_element.hpp>
#include <config/config.hpp>
-#include <utils/sha1.hpp>
#include <utils/time.hpp>
#include <xmpp/jid.hpp>
@@ -17,11 +16,8 @@
#include <cstdlib>
-#include <louloulibs.h>
#include <biboumi.h>
-#include <uuid/uuid.h>
-
#ifdef SYSTEMD_FOUND
# include <systemd/sd-daemon.h>
#endif
@@ -45,7 +41,7 @@ static std::set<std::string> kickable_errors{
};
-BiboumiComponent::BiboumiComponent(std::shared_ptr<Poller> poller, const std::string& hostname, const std::string& secret):
+BiboumiComponent::BiboumiComponent(std::shared_ptr<Poller>& poller, const std::string& hostname, const std::string& secret):
XmppComponent(poller, hostname, secret),
irc_server_adhoc_commands_handler(*this),
irc_channel_adhoc_commands_handler(*this)
@@ -85,10 +81,8 @@ BiboumiComponent::BiboumiComponent(std::shared_ptr<Poller> poller, const std::st
void BiboumiComponent::shutdown()
{
- for (auto it = this->bridges.begin(); it != this->bridges.end(); ++it)
- {
- it->second->shutdown("Gateway shutdown");
- }
+ for (auto& pair: this->bridges)
+ pair.second->shutdown("Gateway shutdown");
}
void BiboumiComponent::clean()
@@ -137,7 +131,7 @@ void BiboumiComponent::handle_presence(const Stanza& stanza)
// 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([&](){
+ utils::ScopeGuard stanza_error([this, &from_str, &to_str, &id, &error_type, &error_name](){
this->send_stanza_error("presence", from_str, to_str, id,
error_type, error_name, "");
});
@@ -150,7 +144,7 @@ void BiboumiComponent::handle_presence(const Stanza& stanza)
{
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);
+ bridge->send_irc_nick_change(iid, to.resource, from.resource);
const XmlNode* x = stanza.get_child("x", MUC_NS);
const XmlNode* password = x ? x->get_child("password", MUC_NS): nullptr;
bridge->join_irc_channel(iid, to.resource, password ? password->get_inner(): "",
@@ -162,6 +156,15 @@ void BiboumiComponent::handle_presence(const Stanza& stanza)
bridge->leave_irc_channel(std::move(iid), status ? status->get_inner() : "", from.resource);
}
}
+ else if (iid.type == Iid::Type::Server || iid.type == Iid::Type::None)
+ {
+ if (type == "subscribe")
+ { // Auto-accept any subscription request for an IRC server
+ this->accept_subscription(to_str, from.bare());
+ this->ask_subscription(to_str, from.bare());
+ }
+
+ }
else
{
// A user wants to join an invalid IRC channel, return a presence error to him/her
@@ -171,10 +174,11 @@ void BiboumiComponent::handle_presence(const Stanza& stanza)
}
catch (const IRCNotConnected& ex)
{
- this->send_stanza_error("presence", from_str, to_str, id,
- "cancel", "remote-server-not-found",
- "Not connected to IRC server "s + ex.hostname,
- true);
+ if (type != "unavailable")
+ this->send_stanza_error("presence", from_str, to_str, id,
+ "cancel", "remote-server-not-found",
+ "Not connected to IRC server "s + ex.hostname,
+ true);
}
stanza_error.disable();
}
@@ -197,7 +201,7 @@ void BiboumiComponent::handle_message(const Stanza& stanza)
std::string error_type("cancel");
std::string error_name("internal-server-error");
- utils::ScopeGuard stanza_error([&](){
+ utils::ScopeGuard stanza_error([this, &from_str, &to_str, &id, &error_type, &error_name](){
this->send_stanza_error("message", from_str, to_str, id,
error_type, error_name, "");
});
@@ -280,7 +284,7 @@ void BiboumiComponent::handle_message(const Stanza& stanza)
}
// We MUST return an iq, whatever happens, except if the type is
-// "result".
+// "result" or "error".
// To do this, we use a scopeguard. If an exception is raised somewhere, an
// iq of type error "internal-server-error" is sent. If we handle the
// request properly (by calling a function that registers an iq to be sent
@@ -316,7 +320,7 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
// the scopeguard.
std::string error_type("cancel");
std::string error_name("internal-server-error");
- utils::ScopeGuard stanza_error([&](){
+ utils::ScopeGuard stanza_error([this, &from, &to_str, &id, &error_type, &error_name](){
this->send_stanza_error("iq", from, to_str, id,
error_type, error_name, "");
});
@@ -344,7 +348,7 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
bridge->send_irc_kick(iid, nick, reason, id, from);
}
else
- bridge->forward_affiliation_role_change(iid, nick, affiliation, role);
+ bridge->forward_affiliation_role_change(iid, from, nick, affiliation, role, id);
stanza_error.disable();
}
}
@@ -386,6 +390,11 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
if (this->handle_mam_request(stanza))
stanza_error.disable();
}
+ else if ((query = stanza.get_child("query", MUC_OWNER_NS)))
+ {
+ if (this->handle_room_configuration_form(*query, from, to, id))
+ stanza_error.disable();
+ }
#endif
}
else if (type == "get")
@@ -414,7 +423,12 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
}
else if (iid.type == Iid::Type::Channel)
{
- if (node == MUC_TRAFFIC_NS)
+ if (node.empty())
+ {
+ this->send_irc_channel_disco_info(id, from, to_str);
+ stanza_error.disable();
+ }
+ else if (node == MUC_TRAFFIC_NS)
{
this->send_irc_channel_muc_traffic_info(id, from, to_str);
stanza_error.disable();
@@ -492,6 +506,8 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
rs_info.max = std::atoi(max->get_inner().data());
}
+ if (rs_info.max == -1)
+ rs_info.max = 100;
bridge->send_irc_channel_list_request(iid, id, from, std::move(rs_info));
stanza_error.disable();
}
@@ -516,6 +532,13 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
}
stanza_error.disable();
}
+#ifdef USE_DATABASE
+ else if ((query = stanza.get_child("query", MUC_OWNER_NS)))
+ {
+ if (this->handle_room_configuration_form_request(from, to, id))
+ stanza_error.disable();
+ }
+#endif
}
else if (type == "result")
{
@@ -548,6 +571,16 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
}
}
}
+ else if (type == "error")
+ {
+ stanza_error.disable();
+ const auto it = this->waiting_iq.find(id);
+ if (it != this->waiting_iq.end())
+ {
+ it->second(bridge, stanza);
+ this->waiting_iq.erase(it);
+ }
+ }
}
catch (const IRCNotConnected& ex)
{
@@ -570,13 +603,11 @@ bool BiboumiComponent::handle_mam_request(const Stanza& stanza)
Jid to(stanza.get_tag("to"));
const XmlNode* query = stanza.get_child("query", MAM_NS);
- std::string query_id;
- if (query)
- query_id = query->get_tag("queryid");
Iid iid(to.local, {'#', '&'});
- if (iid.type == Iid::Type::Channel && to.resource.empty())
+ if (query && iid.type == Iid::Type::Channel && to.resource.empty())
{
+ const std::string query_id = query->get_tag("queryid");
std::string start;
std::string end;
const XmlNode* x = query->get_child("x", DATAFORM_NS);
@@ -600,10 +631,24 @@ bool BiboumiComponent::handle_mam_request(const Stanza& stanza)
}
}
}
- const auto lines = Database::get_muc_logs(from.bare(), iid.get_local(), iid.get_server(), -1, start, end);
- for (const db::MucLogLine& line: lines)
+ 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());
+ }
+ // 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;
+ }
+ const auto lines = Database::get_muc_logs(from.bare(), iid.get_local(), iid.get_server(), limit, start, end);
+ for (const Database::MucLogLine& line: lines)
{
- if (!line.nick.value().empty())
+ if (!line.col<Database::Nick>().empty())
this->send_archived_message(line, to.full(), from.full(), query_id);
}
this->send_iq_result_full_jid(id, from.full(), to.full());
@@ -612,42 +657,80 @@ bool BiboumiComponent::handle_mam_request(const Stanza& stanza)
return false;
}
-void BiboumiComponent::send_archived_message(const db::MucLogLine& log_line, const std::string& from, const std::string& to,
+void BiboumiComponent::send_archived_message(const Database::MucLogLine& log_line, const std::string& from, const std::string& to,
const std::string& queryid)
{
- Stanza message("message");
+ Stanza message("message");
+ {
message["from"] = from;
message["to"] = to;
- XmlNode result("result");
+ XmlSubNode result(message, "result");
result["xmlns"] = MAM_NS;
if (!queryid.empty())
result["queryid"] = queryid;
- result["id"] = log_line.uuid.value();
+ result["id"] = log_line.col<Database::Uuid>();
- XmlNode forwarded("forwarded");
+ XmlSubNode forwarded(result, "forwarded");
forwarded["xmlns"] = FORWARD_NS;
- XmlNode delay("delay");
+ XmlSubNode delay(forwarded, "delay");
delay["xmlns"] = DELAY_NS;
- delay["stamp"] = utils::to_string(log_line.date.value().timeStamp());
-
- forwarded.add_child(std::move(delay));
+ delay["stamp"] = utils::to_string(log_line.col<Database::Date>());
- XmlNode submessage("message");
+ XmlSubNode submessage(forwarded, "message");
submessage["xmlns"] = CLIENT_NS;
- submessage["from"] = from + "/" + log_line.nick.value();
+ submessage["from"] = from + "/" + log_line.col<Database::Nick>();
submessage["type"] = "groupchat";
- XmlNode body("body");
- body.set_inner(log_line.body.value());
- submessage.add_child(std::move(body));
+ XmlSubNode body(submessage, "body");
+ body.set_inner(log_line.col<Database::Body>());
+ }
+ this->send_stanza(message);
+}
+
+bool BiboumiComponent::handle_room_configuration_form_request(const std::string& from, const Jid& to, const std::string& id)
+{
+ Iid iid(to.local, {'#', '&'});
- forwarded.add_child(std::move(submessage));
- result.add_child(std::move(forwarded));
- message.add_child(std::move(result));
+ if (iid.type != Iid::Type::Channel)
+ return false;
- this->send_stanza(message);
+ 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);
+ if (!handle_irc_channel_configuration_form(*this, query, requester, to))
+ return false;
+
+ Stanza iq("iq");
+ iq["type"] = "result";
+ iq["from"] = to.full();
+ iq["to"] = from;
+ iq["id"] = id;
+
+ this->send_stanza(iq);
+
+ return true;
}
#endif
@@ -681,32 +764,31 @@ Bridge* BiboumiComponent::find_user_bridge(const std::string& full_jid)
std::vector<Bridge*> BiboumiComponent::get_bridges() const
{
std::vector<Bridge*> res;
- for (auto it = this->bridges.begin(); it != this->bridges.end(); ++it)
- res.push_back(it->second.get());
+ for (const auto& bridge: this->bridges)
+ res.push_back(bridge.second.get());
return res;
}
void BiboumiComponent::send_self_disco_info(const std::string& id, const std::string& jid_to)
{
Stanza iq("iq");
- iq["type"] = "result";
- iq["id"] = id;
- iq["to"] = jid_to;
- iq["from"] = this->served_hostname;
- XmlNode query("query");
- query["xmlns"] = DISCO_INFO_NS;
- XmlNode identity("identity");
- identity["category"] = "conference";
- identity["type"] = "irc";
- identity["name"] = "Biboumi XMPP-IRC gateway";
- query.add_child(std::move(identity));
- for (const char* ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS})
- {
- XmlNode feature("feature");
- feature["var"] = ns;
- query.add_child(std::move(feature));
- }
- iq.add_child(std::move(query));
+ {
+ iq["type"] = "result";
+ iq["id"] = id;
+ iq["to"] = jid_to;
+ iq["from"] = this->served_hostname;
+ XmlSubNode query(iq, "query");
+ query["xmlns"] = DISCO_INFO_NS;
+ XmlSubNode identity(query, "identity");
+ identity["category"] = "conference";
+ identity["type"] = "irc";
+ identity["name"] = "Biboumi XMPP-IRC gateway";
+ for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS})
+ {
+ XmlSubNode feature(query, "feature");
+ feature["var"] = ns;
+ }
+ }
this->send_stanza(iq);
}
@@ -714,57 +796,66 @@ void BiboumiComponent::send_irc_server_disco_info(const std::string& id, const s
{
Jid from(jid_from);
Stanza iq("iq");
- iq["type"] = "result";
- iq["id"] = id;
- iq["to"] = jid_to;
- iq["from"] = jid_from;
- XmlNode query("query");
- query["xmlns"] = DISCO_INFO_NS;
- XmlNode identity("identity");
- identity["category"] = "conference";
- identity["type"] = "irc";
- identity["name"] = "IRC server "s + from.local + " over Biboumi";
- query.add_child(std::move(identity));
- for (const char* ns: {DISCO_INFO_NS, ADHOC_NS, PING_NS, VERSION_NS})
- {
- XmlNode feature("feature");
- feature["var"] = ns;
- query.add_child(std::move(feature));
- }
- iq.add_child(std::move(query));
+ {
+ iq["type"] = "result";
+ iq["id"] = id;
+ iq["to"] = jid_to;
+ iq["from"] = jid_from;
+ XmlSubNode query(iq, "query");
+ query["xmlns"] = DISCO_INFO_NS;
+ XmlSubNode identity(query, "identity");
+ identity["category"] = "conference";
+ identity["type"] = "irc";
+ identity["name"] = "IRC server "s + from.local + " over Biboumi";
+ for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS})
+ {
+ XmlSubNode feature(query, "feature");
+ feature["var"] = ns;
+ }
+ }
this->send_stanza(iq);
}
-void BiboumiComponent::send_irc_channel_muc_traffic_info(const std::string id, const std::string& jid_from, const std::string& jid_to)
+void BiboumiComponent::send_irc_channel_muc_traffic_info(const std::string& id, const std::string& jid_to, const std::string& jid_from)
{
Stanza iq("iq");
- iq["type"] = "result";
- iq["id"] = id;
- iq["from"] = jid_from;
- iq["to"] = jid_to;
-
- XmlNode query("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/>
- iq.add_child(std::move(query));
-
+ {
+ 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/>
+ }
this->send_stanza(iq);
-
}
-void BiboumiComponent::send_iq_version_request(const std::string& from,
- const std::string& jid_to)
+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"] = "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));
+ {
+ iq["type"] = "result";
+ iq["id"] = id;
+ iq["to"] = jid_to;
+ iq["from"] = jid_from;
+ XmlSubNode query(iq, "query");
+ query["xmlns"] = DISCO_INFO_NS;
+ XmlSubNode identity(query, "identity");
+ identity["category"] = "conference";
+ identity["type"] = "irc";
+ identity["name"] = "IRC channel "s + iid.get_local() + " from server " + iid.get_server() + " over biboumi";
+ for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS})
+ {
+ XmlSubNode feature(query, "feature");
+ feature["var"] = ns;
+ }
+ }
this->send_stanza(iq);
}
@@ -773,13 +864,14 @@ void BiboumiComponent::send_ping_request(const std::string& from,
const std::string& id)
{
Stanza iq("iq");
- iq["type"] = "get";
- iq["id"] = id;
- iq["from"] = from + "@" + this->served_hostname;
- iq["to"] = jid_to;
- XmlNode ping("ping");
- ping["xmlns"] = PING_NS;
- iq.add_child(std::move(ping));
+ {
+ iq["type"] = "get";
+ iq["id"] = id;
+ iq["from"] = from + "@" + this->served_hostname;
+ iq["to"] = jid_to;
+ XmlSubNode ping(iq, "ping");
+ ping["xmlns"] = PING_NS;
+ }
this->send_stanza(iq);
auto result_cb = [from, id](Bridge* bridge, const Stanza& stanza)
@@ -789,8 +881,14 @@ void BiboumiComponent::send_ping_request(const std::string& from,
{
log_error("Received a corresponding ping result, but the 'to' from "
"the response mismatches the 'from' of the request");
+ return;
}
- else
+ const std::string type = stanza.get_tag("type");
+ const XmlNode* error = stanza.get_child("error", COMPONENT_NS);
+ // Check if what we receive is considered a valid response. And yes, those errors are valid responses
+ if (type == "result" ||
+ (type == "error" && error && (error->get_child("feature-not-implemented", STANZA_NS) ||
+ error->get_child("service-unavailable", STANZA_NS))))
bridge->send_irc_ping_result({from, bridge}, id);
};
this->waiting_iq[id] = result_cb;
@@ -803,48 +901,43 @@ void BiboumiComponent::send_iq_room_list_result(const std::string& id, const std
const ResultSetInfo& rs_info)
{
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;
+ {
+ iq["from"] = from + "@" + this->served_hostname;
+ iq["to"] = to_jid;
+ iq["id"] = id;
+ iq["type"] = "result";
+ XmlSubNode query(iq, "query");
+ query["xmlns"] = DISCO_ITEMS_NS;
for (auto it = begin; it != end; ++it)
- {
- XmlNode item("item");
+ {
+ XmlSubNode item(query, "item");
item["jid"] = it->channel + "@" + this->served_hostname;
- query.add_child(std::move(item));
- }
-
- if ((rs_info.max >= 0 || !rs_info.after.empty() || !rs_info.before.empty()))
- {
- XmlNode set_node("set");
- set_node["xmlns"] = RSM_NS;
+ }
- if (begin != channel_list.channels.cend())
- {
- XmlNode first_node("first");
- first_node["index"] = std::to_string(std::distance(channel_list.channels.cbegin(), begin));
- first_node.set_inner(begin->channel + "@" + this->served_hostname);
- set_node.add_child(std::move(first_node));
- }
- if (end != channel_list.channels.cbegin())
- {
- XmlNode last_node("last");
- last_node.set_inner(std::prev(end)->channel + "@" + this->served_hostname);
- set_node.add_child(std::move(last_node));
- }
- if (channel_list.complete)
- {
- XmlNode count_node("count");
- count_node.set_inner(std::to_string(channel_list.channels.size()));
- set_node.add_child(std::move(count_node));
- }
- query.add_child(std::move(set_node));
- }
+ if ((rs_info.max >= 0 || !rs_info.after.empty() || !rs_info.before.empty()))
+ {
+ XmlSubNode set_node(query, "set");
+ set_node["xmlns"] = RSM_NS;
- iq.add_child(std::move(query));
+ if (begin != channel_list.channels.cend())
+ {
+ XmlSubNode first_node(set_node, "first");
+ first_node["index"] = std::to_string(std::distance(channel_list.channels.cbegin(), begin));
+ first_node.set_inner(begin->channel + "@" + this->served_hostname);
+ }
+ if (end != channel_list.channels.cbegin())
+ {
+ XmlSubNode last_node(set_node, "last");
+ last_node.set_inner(std::prev(end)->channel + "@" + this->served_hostname);
+ }
+ if (channel_list.complete)
+ {
+ XmlSubNode count_node(set_node, "count");
+ count_node.set_inner(std::to_string(channel_list.channels.size()));
+ }
+ }
+ }
this->send_stanza(iq);
}
@@ -853,16 +946,36 @@ void BiboumiComponent::send_invitation(const std::string& room_target,
const std::string& author_nick)
{
Stanza message("message");
- message["from"] = room_target + "@" + this->served_hostname;
- message["to"] = jid_to;
- XmlNode x("x");
- x["xmlns"] = MUC_USER_NS;
- XmlNode invite("invite");
- if (author_nick.empty())
- invite["from"] = room_target + "@" + this->served_hostname;
- else
- invite["from"] = room_target + "@" + this->served_hostname + "/" + author_nick;
- x.add_child(std::move(invite));
- message.add_child(std::move(x));
+ {
+ 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;
+ }
this->send_stanza(message);
}
+
+void BiboumiComponent::accept_subscription(const std::string& from, const std::string& to)
+{
+ Stanza presence("presence");
+ presence["from"] = from;
+ presence["to"] = to;
+ presence["id"] = this->next_id();
+ presence["type"] = "subscribed";
+ this->send_stanza(presence);
+}
+
+void BiboumiComponent::ask_subscription(const std::string& from, const std::string& to)
+{
+ Stanza presence("presence");
+ presence["from"] = from;
+ presence["to"] = to;
+ presence["id"] = this->next_id();
+ presence["type"] = "subscribe";
+ this->send_stanza(presence);
+}
diff --git a/src/xmpp/biboumi_component.hpp b/src/xmpp/biboumi_component.hpp
index 999001f..87311f9 100644
--- a/src/xmpp/biboumi_component.hpp
+++ b/src/xmpp/biboumi_component.hpp
@@ -1,7 +1,8 @@
#pragma once
-
+#include <database/database.hpp>
#include <xmpp/xmpp_component.hpp>
+#include <xmpp/jid.hpp>
#include <bridge/bridge.hpp>
@@ -27,7 +28,7 @@ using iq_responder_callback_t = std::function<void(Bridge* bridge, const Stanza&
class BiboumiComponent: public XmppComponent
{
public:
- explicit BiboumiComponent(std::shared_ptr<Poller> poller, const std::string& hostname, const std::string& secret);
+ explicit BiboumiComponent(std::shared_ptr<Poller>& poller, const std::string& hostname, const std::string& secret);
~BiboumiComponent() = default;
BiboumiComponent(const BiboumiComponent&) = delete;
@@ -69,12 +70,8 @@ public:
* Sends the allowed namespaces in MUC message, according to
* http://xmpp.org/extensions/xep-0045.html#impl-service-traffic
*/
- void send_irc_channel_muc_traffic_info(const std::string id, const std::string& jid_from, const std::string& jid_to);
- /**
- * Send an iq version request
- */
- void send_iq_version_request(const std::string& from,
- const std::string& jid_to);
+ void send_irc_channel_muc_traffic_info(const std::string& id, const std::string& jid_to, const std::string& jid_from);
+ void send_irc_channel_disco_info(const std::string& id, const std::string& jid_to, const std::string& jid_from);
/**
* Send a ping request
*/
@@ -88,6 +85,8 @@ public:
const ChannelList& channel_list, std::vector<ListElement>::const_iterator begin,
std::vector<ListElement>::const_iterator end, const ResultSetInfo& rs_info);
void send_invitation(const std::string& room_target, const std::string& jid_to, const std::string& author_nick);
+ void accept_subscription(const std::string& from, const std::string& to);
+ void ask_subscription(const std::string& from, const std::string& to);
/**
* Handle the various stanza types
*/
@@ -97,8 +96,10 @@ public:
#ifdef USE_DATABASE
bool handle_mam_request(const Stanza& stanza);
- void send_archived_message(const db::MucLogLine& log_line, const std::string& from, const std::string& to,
+ void send_archived_message(const Database::MucLogLine& log_line, const std::string& from, const std::string& to,
const std::string& queryid);
+ bool handle_room_configuration_form_request(const std::string& from, const Jid& to, const std::string& id);
+ bool handle_room_configuration_form(const XmlNode& query, const std::string& from, const Jid& to, const std::string& id);
#endif
/**
diff --git a/src/xmpp/body.hpp b/src/xmpp/body.hpp
new file mode 100644
index 0000000..068d1a4
--- /dev/null
+++ b/src/xmpp/body.hpp
@@ -0,0 +1,12 @@
+#pragma once
+
+
+namespace Xmpp
+{
+// Contains:
+// - an XMPP-valid UTF-8 body
+// - an XML node representing the XHTML-IM body, or null
+ using body = std::tuple<const std::string, std::unique_ptr<XmlNode>>;
+}
+
+
diff --git a/src/xmpp/jid.cpp b/src/xmpp/jid.cpp
new file mode 100644
index 0000000..19d1b55
--- /dev/null
+++ b/src/xmpp/jid.cpp
@@ -0,0 +1,152 @@
+#include <xmpp/jid.hpp>
+#include <algorithm>
+#include <cstring>
+#include <map>
+
+#include <biboumi.h>
+#ifdef LIBIDN_FOUND
+ #include <stringprep.h>
+ #include <sys/types.h>
+ #include <sys/socket.h>
+ #include <netdb.h>
+ #include <utils/scopeguard.hpp>
+ #include <set>
+#endif
+
+#include <logger/logger.hpp>
+
+Jid::Jid(const std::string& jid)
+{
+ std::string::size_type slash = jid.find('/');
+ if (slash != std::string::npos)
+ {
+ this->resource = jid.substr(slash + 1);
+ }
+
+ std::string::size_type at = jid.find('@');
+ if (at != std::string::npos && at < slash)
+ {
+ this->local = jid.substr(0, at);
+ at++;
+ }
+ else
+ at = 0;
+
+ this->domain = jid.substr(at, slash - at);
+}
+
+static constexpr size_t max_jid_part_len = 1023;
+
+std::string jidprep(const std::string& original)
+{
+#ifdef LIBIDN_FOUND
+ using CacheType = std::map<std::string, std::string>;
+ static CacheType cache;
+ std::pair<CacheType::iterator, bool> cached = cache.insert({original, {}});
+ if (std::get<1>(cached) == false)
+ { // Insertion failed: the result is already in the cache, return it
+ return std::get<0>(cached)->second;
+ }
+
+ const std::string error_msg("Failed to convert " + original + " into a valid JID:");
+ Jid jid(original);
+
+ char local[max_jid_part_len] = {};
+ memcpy(local, jid.local.data(), std::min(max_jid_part_len, jid.local.size()));
+ auto rc = static_cast<Stringprep_rc>(::stringprep(local, max_jid_part_len,
+ static_cast<Stringprep_profile_flags>(0), stringprep_xmpp_nodeprep));
+ if (rc != STRINGPREP_OK)
+ {
+ log_error(error_msg + stringprep_strerror(rc));
+ return "";
+ }
+
+ char domain[max_jid_part_len] = {};
+ memcpy(domain, jid.domain.data(), std::min(max_jid_part_len, jid.domain.size()));
+
+ {
+ // Using getaddrinfo, check if the domain part is a valid IPv4 (then use
+ // it as is), or IPv6 (surround it with []), or a domain name (run
+ // nameprep)
+ struct addrinfo hints{};
+ hints.ai_flags = AI_NUMERICHOST;
+ hints.ai_family = AF_UNSPEC;
+
+ struct addrinfo* addr_res = nullptr;
+ const auto ret = ::getaddrinfo(domain, nullptr, &hints, &addr_res);
+ auto addrinfo_deleter = utils::make_scope_guard([addr_res] { if (addr_res) freeaddrinfo(addr_res); });
+ if (ret || !addr_res || (addr_res->ai_family != AF_INET && addr_res->ai_family != AF_INET6))
+ { // Not an IP, run nameprep on it
+ rc = static_cast<Stringprep_rc>(::stringprep(domain, max_jid_part_len,
+ static_cast<Stringprep_profile_flags>(0), stringprep_nameprep));
+ if (rc != STRINGPREP_OK)
+ {
+ log_error(error_msg + stringprep_strerror(rc));
+ return "";
+ }
+
+ // Make sure it contains only allowed characters
+ using std::begin;
+ using std::end;
+ char* domain_end = domain + ::strlen(domain);
+ std::replace_if(std::begin(domain), domain + ::strlen(domain),
+ [](const char c) -> bool
+ {
+ return !((c >= 'a' && c <= 'z') || c == '-' ||
+ (c >= '0' && c <= '9') || c == '.');
+ }, '-');
+ // Make sure there are no doubled - or .
+ std::set<char> special_chars{'-', '.'};
+ domain_end = std::unique(begin(domain), domain + ::strlen(domain), [&special_chars](const char& a, const char& b) -> bool
+ {
+ return special_chars.count(a) && special_chars.count(b);
+ });
+ // remove leading and trailing -. if any
+ if (domain_end != domain && special_chars.count(*(domain_end - 1)))
+ --domain_end;
+ if (domain_end != domain && special_chars.count(domain[0]))
+ {
+ std::memmove(domain, domain + 1, domain_end - domain + 1);
+ --domain_end;
+ }
+ // And if the final result is an empty string, return a dummy hostname
+ if (domain_end == domain)
+ ::strcpy(domain, "empty");
+ else
+ *domain_end = '\0';
+ }
+ else if (addr_res->ai_family == AF_INET6)
+ { // IPv6, surround it with []. The length is always enough:
+ // the longest possible IPv6 is way shorter than max_jid_part_len
+ ::memmove(domain + 1, domain, jid.domain.size());
+ domain[0] = '[';
+ domain[jid.domain.size() + 1] = ']';
+ }
+ }
+
+
+ // If there is no resource, stop here
+ if (jid.resource.empty())
+ {
+ std::get<0>(cached)->second = std::string(local) + "@" + domain;
+ return std::get<0>(cached)->second;
+ }
+
+ // Otherwise, also process the resource part
+ char resource[max_jid_part_len] = {};
+ memcpy(resource, jid.resource.data(), std::min(max_jid_part_len, jid.resource.size()));
+ rc = static_cast<Stringprep_rc>(::stringprep(resource, max_jid_part_len,
+ static_cast<Stringprep_profile_flags>(0), stringprep_xmpp_resourceprep));
+ if (rc != STRINGPREP_OK)
+ {
+ log_error(error_msg + stringprep_strerror(rc));
+ return "";
+ }
+ std::get<0>(cached)->second = std::string(local) + "@" + domain + "/" + resource;
+ return std::get<0>(cached)->second;
+
+#else
+ (void)original;
+ return "";
+#endif
+}
diff --git a/src/xmpp/jid.hpp b/src/xmpp/jid.hpp
new file mode 100644
index 0000000..85e835c
--- /dev/null
+++ b/src/xmpp/jid.hpp
@@ -0,0 +1,49 @@
+#pragma once
+
+
+#include <string>
+
+/**
+ * Parse a JID into its different subart
+ */
+class Jid
+{
+public:
+ explicit Jid(const std::string& jid);
+
+ Jid(const Jid&) = delete;
+ Jid(Jid&&) = delete;
+ Jid& operator=(const Jid&) = delete;
+ Jid& operator=(Jid&&) = delete;
+
+ std::string domain;
+ std::string local;
+ std::string resource;
+
+ std::string bare() const
+ {
+ return this->local + "@" + this->domain;
+ }
+ std::string full() const
+ {
+ std::string res = this->domain;
+ if (!this->local.empty())
+ res = this->local + "@" + this->domain;
+ if (!this->resource.empty())
+ res += "/" + this->resource;
+ return res;
+ }
+};
+
+/**
+ * Prepare the given UTF-8 string according to the XMPP node stringprep
+ * identifier profile. This is used to send properly-formed JID to the XMPP
+ * server.
+ *
+ * If the stringprep library is not found, we return an empty string. When
+ * this function is used, the result must always be checked for an empty
+ * value, and if this is the case it must not be used as a JID.
+ */
+std::string jidprep(const std::string& original);
+
+
diff --git a/src/xmpp/xmpp_component.cpp b/src/xmpp/xmpp_component.cpp
new file mode 100644
index 0000000..b138ed9
--- /dev/null
+++ b/src/xmpp/xmpp_component.cpp
@@ -0,0 +1,684 @@
+#include <utils/timed_events.hpp>
+#include <utils/scopeguard.hpp>
+#include <utils/tolower.hpp>
+#include <logger/logger.hpp>
+
+#include <xmpp/xmpp_component.hpp>
+#include <config/config.hpp>
+#include <utils/system.hpp>
+#include <utils/time.hpp>
+#include <xmpp/auth.hpp>
+#include <xmpp/jid.hpp>
+
+#include <stdexcept>
+#include <iostream>
+#include <set>
+
+#include <uuid/uuid.h>
+
+#include <cstdlib>
+#include <set>
+
+#include <biboumi.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"
+ };
+
+XmppComponent::XmppComponent(std::shared_ptr<Poller>& poller, std::string hostname, std::string secret):
+ TCPClientSocketHandler(poller),
+ ever_auth(false),
+ first_connection_try(true),
+ secret(std::move(secret)),
+ authenticated(false),
+ doc_open(false),
+ served_hostname(std::move(hostname)),
+ stanza_handlers{},
+ adhoc_commands_handler(*this)
+{
+ this->parser.add_stream_open_callback(std::bind(&XmppComponent::on_remote_stream_open, this,
+ std::placeholders::_1));
+ this->parser.add_stanza_callback(std::bind(&XmppComponent::on_stanza, this,
+ std::placeholders::_1));
+ this->parser.add_stream_close_callback(std::bind(&XmppComponent::on_remote_stream_close, this,
+ std::placeholders::_1));
+ this->stanza_handlers.emplace("handshake",
+ std::bind(&XmppComponent::handle_handshake, this,std::placeholders::_1));
+ this->stanza_handlers.emplace("error",
+ std::bind(&XmppComponent::handle_error, this,std::placeholders::_1));
+}
+
+void XmppComponent::start()
+{
+ this->connect(Config::get("xmpp_server_ip", "127.0.0.1"), Config::get("port", "5347"), false);
+}
+
+bool XmppComponent::is_document_open() const
+{
+ return this->doc_open;
+}
+
+void XmppComponent::send_stanza(const Stanza& stanza)
+{
+ std::string str = stanza.to_string();
+ log_debug("XMPP SENDING: ", str);
+ this->send_data(std::move(str));
+}
+
+void XmppComponent::on_connection_failed(const std::string& reason)
+{
+ this->first_connection_try = false;
+ log_error("Failed to connect to the XMPP server: ", reason);
+#ifdef SYSTEMD_FOUND
+ sd_notifyf(0, "STATUS=Failed to connect to the XMPP server: %s", reason.data());
+#endif
+}
+
+void XmppComponent::on_connected()
+{
+ log_info("connected to XMPP server");
+ this->first_connection_try = true;
+ auto data = "<stream:stream to='"s + this->served_hostname + \
+ "' xmlns:stream='http://etherx.jabber.org/streams' xmlns='" COMPONENT_NS "'>";
+ log_debug("XMPP SENDING: ", data);
+ this->send_data(std::move(data));
+ this->doc_open = true;
+ // We may have some pending data to send: this happens when we try to send
+ // some data before we are actually connected. We send that data right now, if any
+ this->send_pending_data();
+}
+
+void XmppComponent::on_connection_close(const std::string& error)
+{
+ if (error.empty())
+ log_info("XMPP server closed connection");
+ else
+ log_info("XMPP server closed connection: ", error);
+}
+
+void XmppComponent::parse_in_buffer(const size_t size)
+{
+ // in_buf.size, or size, cannot be bigger than our read-size (4096) so it’s safe
+ // to cast.
+
+ if (!this->in_buf.empty())
+ { // This may happen if the parser could not allocate enough space for
+ // us. We try to feed it the data that was read into our in_buf
+ // instead. If this fails again we are in trouble.
+ this->parser.feed(this->in_buf.data(), static_cast<int>(this->in_buf.size()), false);
+ this->in_buf.clear();
+ }
+ else
+ { // Just tell the parser to parse the data that was placed into the
+ // buffer it provided to us with GetBuffer
+ this->parser.parse(static_cast<int>(size), false);
+ }
+}
+
+void XmppComponent::on_remote_stream_open(const XmlNode& node)
+{
+ log_debug("XMPP RECEIVING: ", node.to_string());
+ this->stream_id = node.get_tag("id");
+ if (this->stream_id.empty())
+ {
+ log_error("Error: no attribute 'id' found");
+ this->send_stream_error("bad-format", "missing 'id' attribute");
+ this->close_document();
+ return ;
+ }
+
+ // Try to authenticate
+ auto data = "<handshake xmlns='" COMPONENT_NS "'>"s + get_handshake_digest(this->stream_id, this->secret) + "</handshake>";
+ log_debug("XMPP SENDING: ", data);
+ this->send_data(std::move(data));
+}
+
+void XmppComponent::on_remote_stream_close(const XmlNode& node)
+{
+ log_debug("XMPP RECEIVING: ", node.to_string());
+ this->doc_open = false;
+}
+
+void XmppComponent::reset()
+{
+ this->parser.reset();
+}
+
+void XmppComponent::on_stanza(const Stanza& stanza)
+{
+ log_debug("XMPP RECEIVING: ", stanza.to_string());
+ std::function<void(const Stanza&)> handler;
+ try
+ {
+ handler = this->stanza_handlers.at(stanza.get_name());
+ }
+ catch (const std::out_of_range& exception)
+ {
+ log_warning("No handler for stanza of type ", stanza.get_name());
+ return;
+ }
+ handler(stanza);
+}
+
+void XmppComponent::send_stream_error(const std::string& name, const std::string& explanation)
+{
+ Stanza node("stream:error");
+ {
+ XmlSubNode error(node, name);
+ error["xmlns"] = STREAM_NS;
+ if (!explanation.empty())
+ error.set_inner(explanation);
+ }
+ this->send_stanza(node);
+}
+
+void XmppComponent::send_stanza_error(const std::string& kind, const std::string& to, const std::string& from,
+ const std::string& id, const std::string& error_type,
+ const std::string& defined_condition, const std::string& text,
+ const bool fulljid)
+{
+ Stanza node(kind);
+ {
+ if (!to.empty())
+ node["to"] = to;
+ if (!from.empty())
+ {
+ if (fulljid)
+ node["from"] = from;
+ else
+ node["from"] = from + "@" + this->served_hostname;
+ }
+ if (!id.empty())
+ node["id"] = id;
+ node["type"] = "error";
+ {
+ XmlSubNode error(node, "error");
+ error["type"] = error_type;
+ {
+ XmlSubNode inner_error(error, defined_condition);
+ inner_error["xmlns"] = STANZA_NS;
+ }
+ if (!text.empty())
+ {
+ XmlSubNode text_node(error, "text");
+ text_node["xmlns"] = STANZA_NS;
+ text_node.set_inner(text);
+ }
+ }
+ }
+ this->send_stanza(node);
+}
+
+void XmppComponent::close_document()
+{
+ log_debug("XMPP SENDING: </stream:stream>");
+ this->send_data("</stream:stream>");
+ this->doc_open = false;
+}
+
+void XmppComponent::handle_handshake(const Stanza&)
+{
+ this->authenticated = true;
+ this->ever_auth = true;
+ log_info("Authenticated with the XMPP server");
+#ifdef SYSTEMD_FOUND
+ sd_notify(0, "READY=1");
+ // Install an event that sends a keepalive to systemd. If biboumi crashes
+ // or hangs for too long, systemd will restart it.
+ uint64_t usec;
+ if (sd_watchdog_enabled(0, &usec) > 0)
+ {
+ TimedEventsManager::instance().add_event(TimedEvent(
+ std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::microseconds(usec / 2)),
+ []() { sd_notify(0, "WATCHDOG=1"); }));
+ }
+#endif
+ this->after_handshake();
+}
+
+void XmppComponent::handle_error(const Stanza& stanza)
+{
+ const XmlNode* text = stanza.get_child("text", STREAMS_NS);
+ std::string error_message("Unspecified error");
+ if (text)
+ error_message = text->get_inner();
+ log_error("Stream error received from the XMPP server: ", error_message);
+#ifdef SYSTEMD_FOUND
+ if (!this->ever_auth)
+ sd_notifyf(0, "STATUS=Failed to authenticate to the XMPP server: %s", error_message.data());
+#endif
+
+}
+
+void* XmppComponent::get_receive_buffer(const size_t size) const
+{
+ return this->parser.get_buffer(size);
+}
+
+void XmppComponent::send_message(const std::string& from, Xmpp::body&& body, const std::string& to,
+ const std::string& type, const bool fulljid, const bool nocopy)
+{
+ Stanza message("message");
+ {
+ message["to"] = to;
+ if (fulljid)
+ message["from"] = from;
+ else
+ message["from"] = from + "@" + this->served_hostname;
+ if (!type.empty())
+ message["type"] = type;
+ XmlSubNode body_node(message, "body");
+ body_node.set_inner(std::get<0>(body));
+ if (std::get<1>(body))
+ {
+ XmlSubNode html(message, "html");
+ html["xmlns"] = XHTMLIM_NS;
+ // Pass the ownership of the pointer to this xmlnode
+ html.add_child(std::move(std::get<1>(body)));
+ }
+ if (nocopy)
+ {
+ XmlSubNode private_node(message, "private");
+ private_node["xmlns"] = "urn:xmpp:carbons:2";
+ XmlSubNode nocopy(message, "no-copy");
+ nocopy["xmlns"] = "urn:xmpp:hints";
+ }
+ }
+ this->send_stanza(message);
+}
+
+void XmppComponent::send_user_join(const std::string& from,
+ const std::string& nick,
+ const std::string& realjid,
+ const std::string& affiliation,
+ const std::string& role,
+ const std::string& to,
+ const bool self)
+{
+ Stanza presence("presence");
+ {
+ presence["to"] = to;
+ presence["from"] = from + "@" + this->served_hostname + "/" + nick;
+
+ XmlSubNode x(presence, "x");
+ x["xmlns"] = MUC_USER_NS;
+
+ XmlSubNode item(x, "item");
+ if (!affiliation.empty())
+ item["affiliation"] = affiliation;
+ if (!role.empty())
+ item["role"] = role;
+ if (!realjid.empty())
+ {
+ const std::string preped_jid = jidprep(realjid);
+ if (!preped_jid.empty())
+ item["jid"] = preped_jid;
+ }
+
+ if (self)
+ {
+ XmlSubNode status(x, "status");
+ status["code"] = "110";
+ }
+ }
+ this->send_stanza(presence);
+}
+
+void XmppComponent::send_invalid_room_error(const std::string& muc_name,
+ const std::string& nick,
+ const std::string& to)
+{
+ Stanza presence("presence");
+ {
+ if (!muc_name.empty ())
+ presence["from"] = muc_name + "@" + this->served_hostname + "/" + nick;
+ else
+ presence["from"] = this->served_hostname;
+ presence["to"] = to;
+ presence["type"] = "error";
+ XmlSubNode x(presence, "x");
+ x["xmlns"] = MUC_NS;
+ XmlSubNode error(presence, "error");
+ error["by"] = muc_name + "@" + this->served_hostname;
+ error["type"] = "cancel";
+ XmlSubNode item_not_found(error, "item-not-found");
+ item_not_found["xmlns"] = STANZA_NS;
+ XmlSubNode text(error, "text");
+ text["xmlns"] = STANZA_NS;
+ text["xml:lang"] = "en";
+ text.set_inner(muc_name +
+ " is not a valid IRC channel name. A correct room jid is of the form: #<chan>%<server>@" +
+ this->served_hostname);
+ }
+ this->send_stanza(presence);
+}
+
+void XmppComponent::send_topic(const std::string& from, Xmpp::body&& topic, const std::string& to, const std::string& who)
+{
+ Stanza message("message");
+ {
+ message["to"] = to;
+ if (who.empty())
+ message["from"] = from + "@" + this->served_hostname;
+ else
+ message["from"] = from + "@" + this->served_hostname + "/" + who;
+ message["type"] = "groupchat";
+ XmlSubNode subject(message, "subject");
+ subject.set_inner(std::get<0>(topic));
+ }
+ this->send_stanza(message);
+}
+
+void XmppComponent::send_muc_message(const std::string& muc_name, const std::string& nick, Xmpp::body&& xmpp_body, const std::string& jid_to, std::string uuid)
+{
+ Stanza message("message");
+ message["to"] = jid_to;
+ if (!nick.empty())
+ message["from"] = muc_name + "@" + this->served_hostname + "/" + nick;
+ else // Message from the room itself
+ message["from"] = muc_name + "@" + this->served_hostname;
+ message["type"] = "groupchat";
+
+ {
+ XmlSubNode body(message, "body");
+ body.set_inner(std::get<0>(xmpp_body));
+ }
+
+ if (std::get<1>(xmpp_body))
+ {
+ XmlSubNode html(message, "html");
+ html["xmlns"] = XHTMLIM_NS;
+ // Pass the ownership of the pointer to this xmlnode
+ html.add_child(std::move(std::get<1>(xmpp_body)));
+ }
+
+ if (!uuid.empty())
+ {
+ XmlSubNode stanza_id(message, "stanza-id");
+ stanza_id["xmlns"] = STABLE_ID_NS;
+ stanza_id["by"] = muc_name + "@" + this->served_hostname;
+ stanza_id["id"] = std::move(uuid);
+ }
+
+ this->send_stanza(message);
+}
+
+void XmppComponent::send_history_message(const std::string& muc_name, const std::string& nick, const std::string& body_txt, const std::string& jid_to, std::time_t timestamp)
+{
+ Stanza message("message");
+ message["to"] = jid_to;
+ if (!nick.empty())
+ message["from"] = muc_name + "@" + this->served_hostname + "/" + nick;
+ else
+ message["from"] = muc_name + "@" + this->served_hostname;
+ message["type"] = "groupchat";
+
+ {
+ XmlSubNode body(message, "body");
+ body.set_inner(body_txt);
+ }
+ {
+ XmlSubNode delay(message, "delay");
+ delay["xmlns"] = DELAY_NS;
+ delay["from"] = muc_name + "@" + this->served_hostname;
+ delay["stamp"] = utils::to_string(timestamp);
+ }
+
+ this->send_stanza(message);
+}
+
+void XmppComponent::send_muc_leave(const std::string& muc_name, const std::string& nick, Xmpp::body&& message, const std::string& jid_to, const bool self)
+{
+ Stanza presence("presence");
+ {
+ presence["to"] = jid_to;
+ presence["from"] = muc_name + "@" + this->served_hostname + "/" + nick;
+ presence["type"] = "unavailable";
+ const std::string& message_str = std::get<0>(message);
+ XmlSubNode x(presence, "x");
+ x["xmlns"] = MUC_USER_NS;
+ if (self)
+ {
+ XmlSubNode status(x, "status");
+ status["code"] = "110";
+ }
+ if (!message_str.empty())
+ {
+ XmlSubNode status(presence, "status");
+ status.set_inner(message_str);
+ }
+ }
+ this->send_stanza(presence);
+}
+
+void XmppComponent::send_nick_change(const std::string& muc_name,
+ const std::string& old_nick,
+ const std::string& new_nick,
+ const std::string& affiliation,
+ const std::string& role,
+ const std::string& jid_to,
+ const bool self)
+{
+ Stanza presence("presence");
+ {
+ presence["to"] = jid_to;
+ presence["from"] = muc_name + "@" + this->served_hostname + "/" + old_nick;
+ presence["type"] = "unavailable";
+ XmlSubNode x(presence, "x");
+ x["xmlns"] = MUC_USER_NS;
+ XmlSubNode item(x, "item");
+ item["nick"] = new_nick;
+ XmlSubNode status(x, "status");
+ status["code"] = "303";
+ if (self)
+ {
+ XmlSubNode status(x, "status");
+ status["code"] = "110";
+ }
+ }
+ this->send_stanza(presence);
+
+ this->send_user_join(muc_name, new_nick, "", affiliation, role, jid_to, self);
+}
+
+void XmppComponent::kick_user(const std::string& muc_name, const std::string& target, const std::string& txt,
+ const std::string& author, const std::string& jid_to, const bool self)
+{
+ Stanza presence("presence");
+ {
+ presence["from"] = muc_name + "@" + this->served_hostname + "/" + target;
+ presence["to"] = jid_to;
+ presence["type"] = "unavailable";
+ XmlSubNode x(presence, "x");
+ x["xmlns"] = MUC_USER_NS;
+ XmlSubNode item(x, "item");
+ item["affiliation"] = "none";
+ item["role"] = "none";
+ XmlSubNode actor(item, "actor");
+ actor["nick"] = author;
+ actor["jid"] = author; // backward compatibility with old clients
+ XmlSubNode reason(item, "reason");
+ reason.set_inner(txt);
+ XmlSubNode status(x, "status");
+ status["code"] = "307";
+ if (self)
+ {
+ XmlSubNode status(x, "status");
+ status["code"] = "110";
+ }
+ }
+ this->send_stanza(presence);
+}
+
+void XmppComponent::send_presence_error(const std::string& muc_name,
+ const std::string& nickname,
+ const std::string& jid_to,
+ const std::string& type,
+ const std::string& condition,
+ const std::string& error_code,
+ const std::string& text)
+{
+ Stanza presence("presence");
+ {
+ presence["from"] = muc_name + "@" + this->served_hostname + "/" + nickname;
+ presence["to"] = jid_to;
+ presence["type"] = "error";
+ XmlSubNode x(presence, "x");
+ x["xmlns"] = MUC_NS;
+ XmlSubNode error(presence, "error");
+ error["by"] = muc_name + "@" + this->served_hostname;
+ error["type"] = type;
+ if (!text.empty())
+ {
+ XmlSubNode text_node(error, "text");
+ text_node["xmlns"] = STANZA_NS;
+ text_node.set_inner(text);
+ }
+ if (!error_code.empty())
+ error["code"] = error_code;
+ XmlSubNode subnode(error, condition);
+ subnode["xmlns"] = STANZA_NS;
+ }
+ this->send_stanza(presence);
+}
+
+void XmppComponent::send_affiliation_role_change(const std::string& muc_name,
+ const std::string& target,
+ const std::string& affiliation,
+ const std::string& role,
+ const std::string& jid_to)
+{
+ Stanza presence("presence");
+ {
+ presence["from"] = muc_name + "@" + this->served_hostname + "/" + target;
+ presence["to"] = jid_to;
+ XmlSubNode x(presence, "x");
+ x["xmlns"] = MUC_USER_NS;
+ XmlSubNode item(x, "item");
+ item["affiliation"] = affiliation;
+ item["role"] = role;
+ }
+ this->send_stanza(presence);
+}
+
+void XmppComponent::send_version(const std::string& id, const std::string& jid_to, const std::string& jid_from,
+ const std::string& version)
+{
+ Stanza iq("iq");
+ iq["type"] = "result";
+ iq["id"] = id;
+ iq["to"] = jid_to;
+ iq["from"] = jid_from;
+ {
+ XmlSubNode query(iq, "query");
+ query["xmlns"] = VERSION_NS;
+ if (version.empty())
+ {
+ {
+ XmlSubNode name(query, "name");
+ name.set_inner("biboumi");
+ }
+ {
+ XmlSubNode version(query, "version");
+ version.set_inner(SOFTWARE_VERSION);
+ }
+ {
+ XmlSubNode os(query, "os");
+ os.set_inner(utils::get_system_name());
+ }
+ }
+ else
+ {
+ XmlSubNode name(query, "name");
+ name.set_inner(version);
+ }
+ }
+ this->send_stanza(iq);
+}
+
+void XmppComponent::send_adhoc_commands_list(const std::string& id, const std::string& requester_jid,
+ const std::string& from_jid,
+ const bool with_admin_only, const AdhocCommandsHandler& adhoc_handler)
+{
+ Stanza iq("iq");
+ {
+ iq["type"] = "result";
+ iq["id"] = id;
+ iq["to"] = requester_jid;
+ iq["from"] = from_jid;
+ XmlSubNode query(iq, "query");
+ query["xmlns"] = DISCO_ITEMS_NS;
+ query["node"] = ADHOC_NS;
+ for (const auto &kv: adhoc_handler.get_commands())
+ {
+ if (kv.second.is_admin_only() && !with_admin_only)
+ continue;
+ XmlSubNode item(query, "item");
+ item["jid"] = from_jid;
+ item["node"] = kv.first;
+ item["name"] = kv.second.name;
+ }
+ }
+ this->send_stanza(iq);
+}
+
+void XmppComponent::send_iq_version_request(const std::string& from,
+ const std::string& jid_to)
+{
+ Stanza iq("iq");
+ {
+ iq["type"] = "get";
+ iq["id"] = "version_"s + XmppComponent::next_id();
+ iq["from"] = from + "@" + this->served_hostname;
+ iq["to"] = jid_to;
+ XmlSubNode query(iq, "query");
+ query["xmlns"] = VERSION_NS;
+ }
+ this->send_stanza(iq);
+}
+
+void XmppComponent::send_iq_result_full_jid(const std::string& id, const std::string& to_jid, const std::string& from_full_jid)
+{
+ Stanza iq("iq");
+ iq["from"] = from_full_jid;
+ iq["to"] = to_jid;
+ iq["id"] = id;
+ iq["type"] = "result";
+ this->send_stanza(iq);
+}
+
+void XmppComponent::send_iq_result(const std::string& id, const std::string& to_jid, const std::string& from_local_part)
+{
+ Stanza iq("iq");
+ if (!from_local_part.empty())
+ iq["from"] = from_local_part + "@" + this->served_hostname;
+ else
+ iq["from"] = this->served_hostname;
+ iq["to"] = to_jid;
+ iq["id"] = id;
+ iq["type"] = "result";
+ this->send_stanza(iq);
+}
+
+std::string XmppComponent::next_id()
+{
+ char uuid_str[37];
+ uuid_t uuid;
+ uuid_generate(uuid);
+ uuid_unparse(uuid, uuid_str);
+ return uuid_str;
+}
diff --git a/src/xmpp/xmpp_component.hpp b/src/xmpp/xmpp_component.hpp
new file mode 100644
index 0000000..ebe3ec8
--- /dev/null
+++ b/src/xmpp/xmpp_component.hpp
@@ -0,0 +1,248 @@
+#pragma once
+
+
+#include <xmpp/adhoc_commands_handler.hpp>
+#include <network/tcp_client_socket_handler.hpp>
+#include <xmpp/xmpp_parser.hpp>
+#include <xmpp/body.hpp>
+
+#include <unordered_map>
+#include <memory>
+#include <string>
+#include <ctime>
+#include <map>
+
+#define STREAM_NS "http://etherx.jabber.org/streams"
+#define COMPONENT_NS "jabber:component:accept"
+#define MUC_NS "http://jabber.org/protocol/muc"
+#define MUC_USER_NS MUC_NS"#user"
+#define MUC_ADMIN_NS MUC_NS"#admin"
+#define MUC_OWNER_NS MUC_NS"#owner"
+#define DISCO_NS "http://jabber.org/protocol/disco"
+#define DISCO_ITEMS_NS DISCO_NS"#items"
+#define DISCO_INFO_NS DISCO_NS"#info"
+#define XHTMLIM_NS "http://jabber.org/protocol/xhtml-im"
+#define STANZA_NS "urn:ietf:params:xml:ns:xmpp-stanzas"
+#define STREAMS_NS "urn:ietf:params:xml:ns:xmpp-streams"
+#define VERSION_NS "jabber:iq:version"
+#define ADHOC_NS "http://jabber.org/protocol/commands"
+#define PING_NS "urn:xmpp:ping"
+#define DELAY_NS "urn:xmpp:delay"
+#define MAM_NS "urn:xmpp:mam:2"
+#define FORWARD_NS "urn:xmpp:forward:0"
+#define CLIENT_NS "jabber:client"
+#define DATAFORM_NS "jabber:x:data"
+#define RSM_NS "http://jabber.org/protocol/rsm"
+#define MUC_TRAFFIC_NS "http://jabber.org/protocol/muc#traffic"
+#define STABLE_ID_NS "urn:xmpp:sid:0"
+
+/**
+ * An XMPP component, communicating with an XMPP server using the protocole
+ * described in XEP-0114: Jabber Component Protocol
+ *
+ * TODO: implement XEP-0225: Component Connections
+ */
+class XmppComponent: public TCPClientSocketHandler
+{
+public:
+ explicit XmppComponent(std::shared_ptr<Poller>& poller, std::string hostname, std::string secret);
+ virtual ~XmppComponent() = default;
+
+ XmppComponent(const XmppComponent&) = delete;
+ XmppComponent(XmppComponent&&) = delete;
+ XmppComponent& operator=(const XmppComponent&) = delete;
+ XmppComponent& operator=(XmppComponent&&) = delete;
+
+ void on_connection_failed(const std::string& reason) override final;
+ void on_connected() override final;
+ void on_connection_close(const std::string& error) override final;
+ void parse_in_buffer(const size_t size) override final;
+
+ /**
+ * Returns a unique id, to be used in the 'id' element of our iq stanzas.
+ */
+ static std::string next_id();
+ bool is_document_open() const;
+ /**
+ * Connect to the XMPP server.
+ */
+ void start();
+ /**
+ * Reset the component so we can use the component on a new XMPP stream
+ */
+ void reset();
+ /**
+ * Serialize the stanza and add it to the out_buf to be sent to the
+ * server.
+ */
+ void send_stanza(const Stanza& stanza);
+ /**
+ * Handle the opening of the remote stream
+ */
+ void on_remote_stream_open(const XmlNode& node);
+ /**
+ * Handle the closing of the remote stream
+ */
+ void on_remote_stream_close(const XmlNode& node);
+ /**
+ * Handle received stanzas
+ */
+ void on_stanza(const Stanza& stanza);
+ /**
+ * Send an error stanza. Message being the name of the element inside the
+ * stanza, and explanation being a short human-readable sentence
+ * describing the error.
+ */
+ void send_stream_error(const std::string& name, const std::string& explanation);
+ /**
+ * Send error stanza, described in http://xmpp.org/rfcs/rfc6120.html#stanzas-error
+ */
+ void send_stanza_error(const std::string& kind, const std::string& to, const std::string& from,
+ const std::string& id, const std::string& error_type,
+ const std::string& defined_condition, const std::string& text,
+ const bool fulljid=true);
+ /**
+ * Send the closing signal for our document (not closing the connection though).
+ */
+ void close_document();
+ /**
+ * Send a message from from@served_hostname, with the given body
+ *
+ * If fulljid is false, the provided 'from' doesn't contain the
+ * server-part of the JID and must be added.
+ */
+ void send_message(const std::string& from, Xmpp::body&& body, const std::string& to,
+ const std::string& type, const bool fulljid, const bool nocopy=false);
+ /**
+ * Send a join from a new participant
+ */
+ void send_user_join(const std::string& from,
+ const std::string& nick,
+ const std::string& realjid,
+ const std::string& affiliation,
+ const std::string& role,
+ const std::string& to,
+ const bool self);
+ /**
+ * Send an error to indicate that the user tried to join an invalid room
+ */
+ void send_invalid_room_error(const std::string& muc_jid,
+ const std::string& nick,
+ const std::string& to);
+ /**
+ * Send the MUC topic to the user
+ */
+ void send_topic(const std::string& from, Xmpp::body&& xmpp_topic, const std::string& to, const std::string& who);
+ /**
+ * Send a (non-private) message to the MUC
+ */
+ void send_muc_message(const std::string& muc_name, const std::string& nick, Xmpp::body&& body, const std::string& jid_to,
+ std::string uuid);
+ /**
+ * Send a message, with a <delay/> element, part of a MUC history
+ */
+ void send_history_message(const std::string& muc_name, const std::string& nick, const std::string& body,
+ const std::string& jid_to, const std::time_t timestamp);
+ /**
+ * Send an unavailable presence for this nick
+ */
+ void send_muc_leave(const std::string& muc_name, const std::string& nick, Xmpp::body&& message, const std::string& jid_to, const bool self);
+ /**
+ * Indicate that a participant changed his nick
+ */
+ void send_nick_change(const std::string& muc_name,
+ const std::string& old_nick,
+ const std::string& new_nick,
+ const std::string& affiliation,
+ const std::string& role,
+ const std::string& jid_to,
+ const bool self);
+ /**
+ * An user is kicked from a room
+ */
+ void kick_user(const std::string& muc_name, const std::string& target, const std::string& reason,
+ const std::string& author, const std::string& jid_to, const bool self);
+ /**
+ * Send a generic presence error
+ */
+ void send_presence_error(const std::string& muc_name,
+ const std::string& nickname,
+ const std::string& jid_to,
+ const std::string& type,
+ const std::string& condition,
+ const std::string& error_code,
+ const std::string& text);
+ /**
+ * Send a presence from the MUC indicating a change in the role and/or
+ * affiliation of a participant
+ */
+ void send_affiliation_role_change(const std::string& muc_name,
+ const std::string& target,
+ const std::string& affiliation,
+ const std::string& role,
+ const std::string& jid_to);
+ /**
+ * Send a result IQ with the given version, or the gateway version if the
+ * passed string is empty.
+ */
+ void send_version(const std::string& id, const std::string& jid_to, const std::string& jid_from,
+ const std::string& version="");
+ /**
+ * Send the list of all available ad-hoc commands to that JID. The list is
+ * different depending on what JID made the request.
+ */
+ void send_adhoc_commands_list(const std::string& id, const std::string& requester_jid, const std::string& from_jid,
+ const bool with_admin_only, const AdhocCommandsHandler& adhoc_handler);
+ /**
+ * Send an iq version request
+ */
+ void send_iq_version_request(const std::string& from,
+ const std::string& jid_to);
+ /**
+ * Send an empty iq of type result
+ */
+ void send_iq_result(const std::string& id, const std::string& to_jid, const std::string& from);
+ void send_iq_result_full_jid(const std::string& id, const std::string& to_jid,
+ const std::string& from_full_jid);
+
+ void handle_handshake(const Stanza& stanza);
+ void handle_error(const Stanza& stanza);
+
+ virtual void after_handshake() {}
+
+ const std::string& get_served_hostname() const
+ { return this->served_hostname; }
+
+ /**
+ * Whether or not we ever succeeded our authentication to the XMPP server
+ */
+ bool ever_auth;
+ /**
+ * Whether or not this is the first consecutive try on connecting to the
+ * XMPP server. We use this to delay the connection attempt for a few
+ * seconds, if it is not the first try.
+ */
+ bool first_connection_try;
+
+private:
+ /**
+ * Return a buffer provided by the XML parser, to read data directly into
+ * it, and avoiding some unnecessary copy.
+ */
+ void* get_receive_buffer(const size_t size) const override final;
+ XmppParser parser;
+ std::string stream_id;
+ std::string secret;
+ bool authenticated;
+ /**
+ * Whether or not OUR XMPP document is open
+ */
+ bool doc_open;
+protected:
+ std::string served_hostname;
+
+ std::unordered_map<std::string, std::function<void(const Stanza&)>> stanza_handlers;
+ AdhocCommandsHandler adhoc_commands_handler;
+};
+
+
diff --git a/src/xmpp/xmpp_parser.cpp b/src/xmpp/xmpp_parser.cpp
new file mode 100644
index 0000000..0488be9
--- /dev/null
+++ b/src/xmpp/xmpp_parser.cpp
@@ -0,0 +1,172 @@
+#include <xmpp/xmpp_parser.hpp>
+#include <xmpp/xmpp_stanza.hpp>
+
+#include <logger/logger.hpp>
+
+/**
+ * Expat handlers. Called by the Expat library, never by ourself.
+ * They just forward the call to the XmppParser corresponding methods.
+ */
+
+static void start_element_handler(void* user_data, const XML_Char* name, const XML_Char** atts)
+{
+ static_cast<XmppParser*>(user_data)->start_element(name, atts);
+}
+
+static void end_element_handler(void* user_data, const XML_Char* name)
+{
+ static_cast<XmppParser*>(user_data)->end_element(name);
+}
+
+static void character_data_handler(void *user_data, const XML_Char *s, int len)
+{
+ static_cast<XmppParser*>(user_data)->char_data(s, len);
+}
+
+/**
+ * XmppParser class
+ */
+
+XmppParser::XmppParser():
+ level(0),
+ current_node(nullptr),
+ root(nullptr)
+{
+ this->init_xml_parser();
+}
+
+void XmppParser::init_xml_parser()
+{
+ // Create the expat parser
+ this->parser = XML_ParserCreateNS("UTF-8", ':');
+ XML_SetUserData(this->parser, static_cast<void*>(this));
+
+ // Install Expat handlers
+ XML_SetElementHandler(this->parser, &start_element_handler, &end_element_handler);
+ XML_SetCharacterDataHandler(this->parser, &character_data_handler);
+}
+
+XmppParser::~XmppParser()
+{
+ XML_ParserFree(this->parser);
+}
+
+int XmppParser::feed(const char* data, const int len, const bool is_final)
+{
+ int res = XML_Parse(this->parser, data, len, is_final);
+ if (res == XML_STATUS_ERROR &&
+ (XML_GetErrorCode(this->parser) != XML_ERROR_FINISHED))
+ log_error("Xml_Parse encountered an error: ",
+ XML_ErrorString(XML_GetErrorCode(this->parser)));
+ return res;
+}
+
+int XmppParser::parse(const int len, const bool is_final)
+{
+ int res = XML_ParseBuffer(this->parser, len, is_final);
+ if (res == XML_STATUS_ERROR)
+ log_error("Xml_Parsebuffer encountered an error: ",
+ XML_ErrorString(XML_GetErrorCode(this->parser)));
+ return res;
+}
+
+void XmppParser::reset()
+{
+ XML_ParserFree(this->parser);
+ this->init_xml_parser();
+ this->current_node = nullptr;
+ this->root.reset(nullptr);
+ this->level = 0;
+}
+
+void* XmppParser::get_buffer(const size_t size) const
+{
+ return XML_GetBuffer(this->parser, static_cast<int>(size));
+}
+
+void XmppParser::start_element(const XML_Char* name, const XML_Char** attribute)
+{
+ this->level++;
+
+ auto new_node = std::make_unique<XmlNode>(name, this->current_node);
+ auto new_node_ptr = new_node.get();
+ if (this->current_node)
+ this->current_node->add_child(std::move(new_node));
+ else
+ this->root = std::move(new_node);
+ this->current_node = new_node_ptr;
+ for (size_t i = 0; attribute[i]; i += 2)
+ this->current_node->set_attribute(attribute[i], attribute[i+1]);
+ if (this->level == 1)
+ this->stream_open_event(*this->current_node);
+}
+
+void XmppParser::end_element(const XML_Char*)
+{
+ this->level--;
+ if (this->level == 0)
+ { // End of the whole stream
+ this->stream_close_event(*this->current_node);
+ this->current_node = nullptr;
+ this->root.reset();
+ }
+ else
+ {
+ auto parent = this->current_node->get_parent();
+ if (this->level == 1)
+ { // End of a stanza
+ this->stanza_event(*this->current_node);
+ // Note: deleting all the children of our parent deletes ourself,
+ // so current_node is an invalid pointer after this line
+ parent->delete_all_children();
+ }
+ this->current_node = parent;
+ }
+}
+
+void XmppParser::char_data(const XML_Char* data, const size_t len)
+{
+ if (this->current_node->has_children())
+ this->current_node->get_last_child()->add_to_tail({data, len});
+ else
+ this->current_node->add_to_inner({data, len});
+}
+
+void XmppParser::stanza_event(const Stanza& stanza) const
+{
+ for (const auto& callback: this->stanza_callbacks)
+ {
+ try {
+ callback(stanza);
+ } catch (const std::exception& e) {
+ log_error("Unhandled exception: ", e.what());
+ }
+ }
+}
+
+void XmppParser::stream_open_event(const XmlNode& node) const
+{
+ for (const auto& callback: this->stream_open_callbacks)
+ callback(node);
+}
+
+void XmppParser::stream_close_event(const XmlNode& node) const
+{
+ for (const auto& callback: this->stream_close_callbacks)
+ callback(node);
+}
+
+void XmppParser::add_stanza_callback(std::function<void(const Stanza&)>&& callback)
+{
+ this->stanza_callbacks.emplace_back(std::move(callback));
+}
+
+void XmppParser::add_stream_open_callback(std::function<void(const XmlNode&)>&& callback)
+{
+ this->stream_open_callbacks.emplace_back(std::move(callback));
+}
+
+void XmppParser::add_stream_close_callback(std::function<void(const XmlNode&)>&& callback)
+{
+ this->stream_close_callbacks.emplace_back(std::move(callback));
+}
diff --git a/src/xmpp/xmpp_parser.hpp b/src/xmpp/xmpp_parser.hpp
new file mode 100644
index 0000000..ec42f9a
--- /dev/null
+++ b/src/xmpp/xmpp_parser.hpp
@@ -0,0 +1,133 @@
+#pragma once
+
+
+#include <xmpp/xmpp_stanza.hpp>
+
+#include <functional>
+
+#include <expat.h>
+
+/**
+ * A SAX XML parser that builds XML nodes and spawns events when a complete
+ * stanza is received (an element of level 2), or when the document is
+ * opened/closed (an element of level 1)
+ *
+ * After a stanza_event has been spawned, we delete the whole stanza. This
+ * means that even with a very long document (in XMPP the document is
+ * potentially infinite), the memory is never exhausted as long as each
+ * stanza is reasonnably short.
+ *
+ * The element names generated by expat contain the namespace of the
+ * element, a colon (':') and then the actual name of the element. To get
+ * an element "x" with a namespace of "http://jabber.org/protocol/muc", you
+ * just look for an XmlNode named "http://jabber.org/protocol/muc:x"
+ *
+ * TODO: enforce the size-limit for the stanza (limit the number of childs
+ * it can contain). For example forbid the parser going further than level
+ * 20 (arbitrary number here), and each XML node to have more than 15 childs
+ * (arbitrary number again).
+ */
+class XmppParser
+{
+public:
+ explicit XmppParser();
+ ~XmppParser();
+ XmppParser(const XmppParser&) = delete;
+ XmppParser& operator=(const XmppParser&) = delete;
+ XmppParser(XmppParser&&) = delete;
+ XmppParser& operator=(XmppParser&&) = delete;
+
+public:
+ /**
+ * Feed the parser with some XML data
+ */
+ int feed(const char* data, const int len, const bool is_final);
+ /**
+ * Parse the data placed in the parser buffer
+ */
+ int parse(const int size, const bool is_final);
+ /**
+ * Reset the parser, so it can be used from scratch afterward
+ */
+ void reset();
+ /**
+ * Get a buffer provided by the xml parser.
+ */
+ void* get_buffer(const size_t size) const;
+ /**
+ * Add one callback for the various events that this parser can spawn.
+ */
+ void add_stanza_callback(std::function<void(const Stanza&)>&& callback);
+ void add_stream_open_callback(std::function<void(const XmlNode&)>&& callback);
+ void add_stream_close_callback(std::function<void(const XmlNode&)>&& callback);
+
+ /**
+ * Called when a new XML element has been opened. We instanciate a new
+ * XmlNode and set it as our current node. The parent of this new node is
+ * the previous "current" node. We have all the element's attributes in
+ * this event.
+ *
+ * We spawn a stream_event with this node if this is a level-1 element.
+ */
+ void start_element(const XML_Char* name, const XML_Char** attribute);
+ /**
+ * Called when an XML element has been closed. We close the current_node,
+ * set our current_node as the parent of the current_node, and if that was
+ * a level-2 element we spawn a stanza_event with this node.
+ *
+ * And we then delete the stanza (and everything under it, its children,
+ * attribute, etc).
+ */
+ void end_element(const XML_Char* name);
+ /**
+ * Some inner or tail data has been parsed
+ */
+ void char_data(const XML_Char* data, const size_t len);
+ /**
+ * Calls all the stanza_callbacks one by one.
+ */
+ void stanza_event(const Stanza& stanza) const;
+ /**
+ * Calls all the stream_open_callbacks one by one. Note: the passed node is not
+ * closed yet.
+ */
+ void stream_open_event(const XmlNode& node) const;
+ /**
+ * Calls all the stream_close_callbacks one by one.
+ */
+ void stream_close_event(const XmlNode& node) const;
+
+private:
+ /**
+ * Init the XML parser and install the callbacks
+ */
+ void init_xml_parser();
+
+ /**
+ * Expat structure.
+ */
+ XML_Parser parser{};
+ /**
+ * The current depth in the XML document
+ */
+ size_t level;
+ /**
+ * The deepest XML node opened but not yet closed (to which we are adding
+ * new children, inner or tail)
+ */
+ XmlNode* current_node;
+ /**
+ * The root node has no parent, so we keep it here: the XmppParser object
+ * is its owner.
+ */
+ std::unique_ptr<XmlNode> root;
+ /**
+ * A list of callbacks to be called on an *_event, receiving the
+ * concerned Stanza/XmlNode.
+ */
+ std::vector<std::function<void(const Stanza&)>> stanza_callbacks;
+ std::vector<std::function<void(const XmlNode&)>> stream_open_callbacks;
+ std::vector<std::function<void(const XmlNode&)>> stream_close_callbacks;
+};
+
+
diff --git a/src/xmpp/xmpp_stanza.cpp b/src/xmpp/xmpp_stanza.cpp
new file mode 100644
index 0000000..435f333
--- /dev/null
+++ b/src/xmpp/xmpp_stanza.cpp
@@ -0,0 +1,229 @@
+#include <xmpp/xmpp_stanza.hpp>
+
+#include <utils/encoding.hpp>
+#include <utils/split.hpp>
+
+#include <stdexcept>
+#include <iostream>
+#include <sstream>
+
+#include <cstring>
+
+std::string xml_escape(const std::string& data)
+{
+ std::string res;
+ res.reserve(data.size());
+ for (size_t pos = 0; pos != data.size(); ++pos)
+ {
+ switch(data[pos])
+ {
+ case '&':
+ res += "&amp;";
+ break;
+ case '<':
+ res += "&lt;";
+ break;
+ case '>':
+ res += "&gt;";
+ break;
+ case '\"':
+ res += "&quot;";
+ break;
+ case '\'':
+ res += "&apos;";
+ break;
+ default:
+ res += data[pos];
+ break;
+ }
+ }
+ return res;
+}
+
+std::string sanitize(const std::string& data, const std::string& encoding)
+{
+ if (utils::is_valid_utf8(data.data()))
+ return xml_escape(utils::remove_invalid_xml_chars(data));
+ else
+ return xml_escape(utils::remove_invalid_xml_chars(utils::convert_to_utf8(data, encoding.data())));
+}
+
+XmlNode::XmlNode(const std::string& name, XmlNode* parent):
+ parent(parent)
+{
+ // split the namespace and the name
+ auto n = name.rfind(':');
+ if (n == std::string::npos)
+ this->name = name;
+ else
+ {
+ this->name = name.substr(n+1);
+ this->attributes["xmlns"] = name.substr(0, n);
+ }
+}
+
+XmlNode::XmlNode(const std::string& name):
+ XmlNode(name, nullptr)
+{
+}
+
+void XmlNode::delete_all_children()
+{
+ this->children.clear();
+}
+
+void XmlNode::set_attribute(const std::string& name, const std::string& value)
+{
+ this->attributes[name] = value;
+}
+
+void XmlNode::set_tail(const std::string& data)
+{
+ this->tail = data;
+}
+
+void XmlNode::add_to_tail(const std::string& data)
+{
+ this->tail += data;
+}
+
+void XmlNode::set_inner(const std::string& data)
+{
+ this->inner = data;
+}
+
+void XmlNode::add_to_inner(const std::string& data)
+{
+ this->inner += data;
+}
+
+std::string XmlNode::get_inner() const
+{
+ return this->inner;
+}
+
+std::string XmlNode::get_tail() const
+{
+ return this->tail;
+}
+
+const XmlNode* XmlNode::get_child(const std::string& name, const std::string& xmlns) const
+{
+ for (const auto& child: this->children)
+ {
+ if (child->name == name && child->get_tag("xmlns") == xmlns)
+ return child.get();
+ }
+ return nullptr;
+}
+
+std::vector<const XmlNode*> XmlNode::get_children(const std::string& name, const std::string& xmlns) const
+{
+ std::vector<const XmlNode*> res;
+ for (const auto& child: this->children)
+ {
+ if (child->name == name && child->get_tag("xmlns") == xmlns)
+ res.push_back(child.get());
+ }
+ return res;
+}
+
+XmlNode* XmlNode::add_child(std::unique_ptr<XmlNode> child)
+{
+ child->parent = this;
+ auto ret = child.get();
+ this->children.push_back(std::move(child));
+ return ret;
+}
+
+XmlNode* XmlNode::add_child(XmlNode&& child)
+{
+ auto new_node = std::make_unique<XmlNode>(std::move(child));
+ return this->add_child(std::move(new_node));
+}
+
+XmlNode* XmlNode::add_child(const XmlNode& child)
+{
+ auto new_node = std::make_unique<XmlNode>(child);
+ return this->add_child(std::move(new_node));
+}
+
+XmlNode* XmlNode::get_last_child() const
+{
+ return this->children.back().get();
+}
+
+XmlNode* XmlNode::get_parent() const
+{
+ return this->parent;
+}
+
+void XmlNode::set_name(const std::string& name)
+{
+ this->name = name;
+}
+
+void XmlNode::set_name(std::string&& name)
+{
+ this->name = std::move(name);
+}
+
+const std::string XmlNode::get_name() const
+{
+ return this->name;
+}
+
+std::string XmlNode::to_string() const
+{
+ std::ostringstream res;
+ res << "<" << this->name;
+ for (const auto& it: this->attributes)
+ res << " " << it.first << "='" << sanitize(it.second) + "'";
+ if (!this->has_children() && this->inner.empty())
+ res << "/>";
+ else
+ {
+ res << ">" + sanitize(this->inner);
+ for (const auto& child: this->children)
+ res << child->to_string();
+ res << "</" << this->get_name() << ">";
+ }
+ res << sanitize(this->tail);
+ return res.str();
+}
+
+bool XmlNode::has_children() const
+{
+ return !this->children.empty();
+}
+
+const std::string& XmlNode::get_tag(const std::string& name) const
+{
+ try
+ {
+ const auto& value = this->attributes.at(name);
+ return value;
+ }
+ catch (const std::out_of_range& e)
+ {
+ static const std::string def{};
+ return def;
+ }
+}
+
+bool XmlNode::del_tag(const std::string& name)
+{
+ if (this->attributes.erase(name) != 0)
+ return true;
+ return false;
+}
+
+std::string& XmlNode::operator[](const std::string& name)
+{
+ return this->attributes[name];
+}
+
+std::ostream& operator<<(std::ostream& os, const XmlNode& node)
+{
+ return os << node.to_string();
+}
diff --git a/src/xmpp/xmpp_stanza.hpp b/src/xmpp/xmpp_stanza.hpp
new file mode 100644
index 0000000..f4b3948
--- /dev/null
+++ b/src/xmpp/xmpp_stanza.hpp
@@ -0,0 +1,160 @@
+#pragma once
+
+
+#include <map>
+#include <string>
+#include <vector>
+#include <memory>
+
+std::string xml_escape(const std::string& data);
+std::string xml_unescape(const std::string& data);
+std::string sanitize(const std::string& data, const std::string& encoding = "ISO-8859-1");
+
+/**
+ * Represent an XML node. It has
+ * - A parent XML node (in the case of the first-level nodes, the parent is
+ nullptr)
+ * - zero, one or more children XML nodes
+ * - A name
+ * - A map of attributes
+ * - inner data (text inside the node)
+ * - tail data (text just after the node)
+ */
+class XmlNode
+{
+public:
+ explicit XmlNode(const std::string& name, XmlNode* parent);
+ explicit XmlNode(const std::string& name);
+ /**
+ * The copy constructor does not copy the parent attribute. The children
+ * nodes are all copied recursively.
+ */
+ XmlNode(const XmlNode& node):
+ name(node.name),
+ parent(nullptr),
+ attributes(node.attributes),
+ children{},
+ inner(node.inner),
+ tail(node.tail)
+ {
+ for (const auto& child: node.children)
+ this->add_child(std::make_unique<XmlNode>(*child));
+ }
+
+ XmlNode(XmlNode&& node) = default;
+ XmlNode& operator=(const XmlNode&) = delete;
+ XmlNode& operator=(XmlNode&&) = delete;
+
+ ~XmlNode() = default;
+
+ void delete_all_children();
+ void set_attribute(const std::string& name, const std::string& value);
+ /**
+ * Set the content of the tail, that is the text just after this node
+ */
+ void set_tail(const std::string& data);
+ /**
+ * Append the given data to the content of the tail. This exists because
+ * the expat library may provide the complete text of an element in more
+ * than one call
+ */
+ void add_to_tail(const std::string& data);
+ /**
+ * Set the content of the inner, that is the text inside this node.
+ */
+ void set_inner(const std::string& data);
+ /**
+ * Append the given data to the content of the inner. For the reason
+ * described in add_to_tail comment.
+ */
+ void add_to_inner(const std::string& data);
+ /**
+ * Get the content of inner
+ */
+ std::string get_inner() const;
+ /**
+ * Get the content of the tail
+ */
+ std::string get_tail() const;
+ /**
+ * Get a pointer to the first child element with that name and that xml namespace
+ */
+ const XmlNode* get_child(const std::string& name, const std::string& xmlns) const;
+ /**
+ * Get a vector of all the children that have that name and that xml namespace.
+ */
+ std::vector<const XmlNode*> get_children(const std::string& name, const std::string& xmlns) const;
+ /**
+ * Add a node child to this node. Assign this node to the child’s parent.
+ * Returns a pointer to the newly added child.
+ */
+ XmlNode* add_child(std::unique_ptr<XmlNode> child);
+ XmlNode* add_child(XmlNode&& child);
+ XmlNode* add_child(const XmlNode& child);
+ /**
+ * Returns the last of the children. If the node doesn't have any child,
+ * the behaviour is undefined. The user should make sure this is the case
+ * by calling has_children() for example.
+ */
+ XmlNode* get_last_child() const;
+ XmlNode* get_parent() const;
+ void set_name(const std::string& name);
+ void set_name(std::string&& name);
+ const std::string get_name() const;
+ /**
+ * Serialize the stanza into a string
+ */
+ std::string to_string() const;
+ /**
+ * Whether or not this node has at least one child (if not, this is a leaf
+ * node)
+ */
+ bool has_children() const;
+ /**
+ * Gets the value for the given attribute, returns an empty string if the
+ * node as no such attribute.
+ */
+ const std::string& get_tag(const std::string& name) const;
+ /**
+ * Remove the attribute of the node. Does nothing if that attribute is not
+ * present. Returns true if the tag was removed, false if it was absent.
+ */
+ bool del_tag(const std::string& name);
+ /**
+ * Use this to set an attribute's value, like node["id"] = "12";
+ */
+ std::string& operator[](const std::string& name);
+
+private:
+ std::string name;
+ XmlNode* parent;
+ std::map<std::string, std::string> attributes;
+ std::vector<std::unique_ptr<XmlNode>> children;
+ std::string inner;
+ std::string tail;
+};
+
+std::ostream& operator<<(std::ostream& os, const XmlNode& node);
+
+/**
+ * An XMPP stanza is just an XML node of level 2 in the XMPP document (the
+ * level 1 ones are the <stream::stream/>, and the ones above 2 are just the
+ * content of the stanzas)
+ */
+using Stanza = XmlNode;
+
+class XmlSubNode: public XmlNode
+{
+public:
+ XmlSubNode(XmlNode& parent_ref, const std::string& name):
+ XmlNode(name),
+ parent_to_add(parent_ref)
+ {}
+
+ ~XmlSubNode()
+ {
+ this->parent_to_add.add_child(std::move(*this));
+ }
+private:
+ XmlNode& parent_to_add;
+}; \ No newline at end of file