summaryrefslogtreecommitdiff
path: root/louloulibs/xmpp
diff options
context:
space:
mode:
authorFlorent Le Coz <louiz@louiz.org>2015-05-28 23:42:52 +0200
committerFlorent Le Coz <louiz@louiz.org>2015-05-28 23:46:24 +0200
commite1a7114c8daa10589c830ce972cf461c3540111b (patch)
tree3b9fc79a881b252248d30c8e797bed13f131e90b /louloulibs/xmpp
parent897b281e67dc82700db9fd9c2dedc5e01e5871ee (diff)
downloadbiboumi-e1a7114c8daa10589c830ce972cf461c3540111b.tar.gz
biboumi-e1a7114c8daa10589c830ce972cf461c3540111b.tar.bz2
biboumi-e1a7114c8daa10589c830ce972cf461c3540111b.tar.xz
biboumi-e1a7114c8daa10589c830ce972cf461c3540111b.zip
louloulibs is directly included, instead of being a submodule
Because this is a nightmare to manage
Diffstat (limited to 'louloulibs/xmpp')
m---------louloulibs0
-rw-r--r--louloulibs/xmpp/adhoc_command.cpp105
-rw-r--r--louloulibs/xmpp/adhoc_command.hpp43
-rw-r--r--louloulibs/xmpp/adhoc_commands_handler.cpp133
-rw-r--r--louloulibs/xmpp/adhoc_commands_handler.hpp74
-rw-r--r--louloulibs/xmpp/adhoc_session.cpp37
-rw-r--r--louloulibs/xmpp/adhoc_session.hpp70
-rw-r--r--louloulibs/xmpp/body.hpp12
-rw-r--r--louloulibs/xmpp/jid.cpp101
-rw-r--r--louloulibs/xmpp/jid.hpp36
-rw-r--r--louloulibs/xmpp/roster.cpp21
-rw-r--r--louloulibs/xmpp/roster.hpp71
-rw-r--r--louloulibs/xmpp/xmpp_component.cpp716
-rw-r--r--louloulibs/xmpp/xmpp_component.hpp240
-rw-r--r--louloulibs/xmpp/xmpp_parser.cpp169
-rw-r--r--louloulibs/xmpp/xmpp_parser.hpp126
-rw-r--r--louloulibs/xmpp/xmpp_stanza.cpp274
-rw-r--r--louloulibs/xmpp/xmpp_stanza.hpp160
18 files changed, 2388 insertions, 0 deletions
diff --git a/louloulibs b/louloulibs
deleted file mode 160000
-Subproject 0f3c1183e2bf0941ae2bffd3f31577bce4f3001
diff --git a/louloulibs/xmpp/adhoc_command.cpp b/louloulibs/xmpp/adhoc_command.cpp
new file mode 100644
index 0000000..24145f5
--- /dev/null
+++ b/louloulibs/xmpp/adhoc_command.cpp
@@ -0,0 +1,105 @@
+#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, const std::string& name, const bool admin_only):
+ name(name),
+ callbacks(std::move(callbacks)),
+ admin_only(admin_only)
+{
+}
+
+AdhocCommand::~AdhocCommand()
+{
+}
+
+bool AdhocCommand::is_admin_only() const
+{
+ return this->admin_only;
+}
+
+void PingStep1(XmppComponent*, AdhocSession&, XmlNode& command_node)
+{
+ XmlNode note("note");
+ note["type"] = "info";
+ note.set_inner("Pong");
+ note.close();
+ command_node.add_child(std::move(note));
+}
+
+void HelloStep1(XmppComponent*, AdhocSession&, XmlNode& command_node)
+{
+ XmlNode x("jabber:x:data:x");
+ x["type"] = "form";
+ XmlNode title("title");
+ title.set_inner("Configure your name.");
+ title.close();
+ x.add_child(std::move(title));
+ XmlNode instructions("instructions");
+ instructions.set_inner("Please provide your name.");
+ instructions.close();
+ x.add_child(std::move(instructions));
+ XmlNode name_field("field");
+ name_field["var"] = "name";
+ name_field["type"] = "text-single";
+ name_field["label"] = "Your name";
+ XmlNode required("required");
+ required.close();
+ name_field.add_child(std::move(required));
+ name_field.close();
+ x.add_child(std::move(name_field));
+ x.close();
+ command_node.add_child(std::move(x));
+}
+
+void HelloStep2(XmppComponent*, AdhocSession& session, XmlNode& command_node)
+{
+ // Find out if the name was provided in the form.
+ XmlNode* x = command_node.get_child("x", "jabber:x:data");
+ if (x)
+ {
+ XmlNode* name_field = nullptr;
+ for (XmlNode* field: x->get_children("field", "jabber:x:data"))
+ if (field->get_tag("var") == "name")
+ {
+ name_field = field;
+ break;
+ }
+ if (name_field)
+ {
+ XmlNode* value = name_field->get_child("value", "jabber:x:data");
+ if (value)
+ {
+ XmlNode note("note");
+ note["type"] = "info";
+ note.set_inner("Hello "s + value->get_inner() + "!"s);
+ note.close();
+ command_node.delete_all_children();
+ command_node.add_child(std::move(note));
+ return;
+ }
+ }
+ }
+ command_node.delete_all_children();
+ XmlNode error(ADHOC_NS":error");
+ error["type"] = "modify";
+ XmlNode condition(STANZA_NS":bad-request");
+ condition.close();
+ error.add_child(std::move(condition));
+ error.close();
+ command_node.add_child(std::move(error));
+ session.terminate();
+}
+
+void Reload(XmppComponent*, AdhocSession&, XmlNode& command_node)
+{
+ ::reload_process();
+ command_node.delete_all_children();
+ XmlNode note("note");
+ note["type"] = "info";
+ note.set_inner("Configuration reloaded.");
+ note.close();
+ command_node.add_child(std::move(note));
+}
diff --git a/louloulibs/xmpp/adhoc_command.hpp b/louloulibs/xmpp/adhoc_command.hpp
new file mode 100644
index 0000000..1ff2bcf
--- /dev/null
+++ b/louloulibs/xmpp/adhoc_command.hpp
@@ -0,0 +1,43 @@
+#ifndef ADHOC_COMMAND_HPP
+# define ADHOC_COMMAND_HPP
+
+/**
+ * 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>&& callback, const std::string& name, const bool admin_only);
+ ~AdhocCommand();
+
+ 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);
+
+#endif // ADHOC_COMMAND_HPP
diff --git a/louloulibs/xmpp/adhoc_commands_handler.cpp b/louloulibs/xmpp/adhoc_commands_handler.cpp
new file mode 100644
index 0000000..46c8a32
--- /dev/null
+++ b/louloulibs/xmpp/adhoc_commands_handler.cpp
@@ -0,0 +1,133 @@
+#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;
+}
+
+std::map<const std::string, const AdhocCommand>& AdhocCommandsHandler::get_commands()
+{
+ return this->commands;
+}
+
+XmlNode AdhocCommandsHandler::handle_request(const std::string& executor_jid, 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())
+ {
+ XmlNode error(ADHOC_NS":error");
+ error["type"] = "cancel";
+ XmlNode condition(STANZA_NS":item-not-found");
+ condition.close();
+ error.add_child(std::move(condition));
+ error.close();
+ command_node.add_child(std::move(error));
+ }
+ else if (command_it->second.is_admin_only() &&
+ Config::get("admin", "") != jid.local + "@" + jid.domain)
+ {
+ XmlNode error(ADHOC_NS":error");
+ error["type"] = "cancel";
+ XmlNode condition(STANZA_NS":forbidden");
+ condition.close();
+ error.add_child(std::move(condition));
+ error.close();
+ command_node.add_child(std::move(error));
+ }
+ 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));
+ 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())
+ {
+ XmlNode error(ADHOC_NS":error");
+ error["type"] = "modify";
+ XmlNode condition(STANZA_NS":bad-request");
+ condition.close();
+ error.add_child(std::move(condition));
+ error.close();
+ command_node.add_child(std::move(error));
+ }
+ else if (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";
+ XmlNode actions("actions");
+ XmlNode next("next");
+ next.close();
+ actions.add_child(std::move(next));
+ actions.close();
+ command_node.add_child(std::move(actions));
+ }
+ }
+ else if (action == "cancel")
+ {
+ this->sessions.erase(session_it);
+ command_node["status"] = "canceled";
+ TimedEventsManager::instance().cancel("adhocsession"s + sessionid + executor_jid);
+ }
+ else // unsupported action
+ {
+ XmlNode error(ADHOC_NS":error");
+ error["type"] = "modify";
+ XmlNode condition(STANZA_NS":bad-request");
+ condition.close();
+ error.add_child(std::move(condition));
+ error.close();
+ command_node.add_child(std::move(error));
+ }
+ }
+ 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/louloulibs/xmpp/adhoc_commands_handler.hpp b/louloulibs/xmpp/adhoc_commands_handler.hpp
new file mode 100644
index 0000000..1083c44
--- /dev/null
+++ b/louloulibs/xmpp/adhoc_commands_handler.hpp
@@ -0,0 +1,74 @@
+#ifndef ADHOC_COMMANDS_HANDLER_HPP
+# define ADHOC_COMMANDS_HANDLER_HPP
+
+/**
+ * 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;
+ /**
+ * Returns the list of available commands.
+ */
+ const std::map<const std::string, const AdhocCommand>& get_commands() const;
+ /**
+ * This one can be used to add new commands.
+ */
+ std::map<const std::string, const AdhocCommand>& get_commands();
+ /**
+ * 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, 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:
+ /**
+ * A pointer to the XmppComponent, to access to 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;
+
+ AdhocCommandsHandler(const AdhocCommandsHandler&) = delete;
+ AdhocCommandsHandler(AdhocCommandsHandler&&) = delete;
+ AdhocCommandsHandler& operator=(const AdhocCommandsHandler&) = delete;
+ AdhocCommandsHandler& operator=(AdhocCommandsHandler&&) = delete;
+};
+
+#endif // ADHOC_COMMANDS_HANDLER_HPP
diff --git a/louloulibs/xmpp/adhoc_session.cpp b/louloulibs/xmpp/adhoc_session.cpp
new file mode 100644
index 0000000..fc60bb7
--- /dev/null
+++ b/louloulibs/xmpp/adhoc_session.cpp
@@ -0,0 +1,37 @@
+#include <xmpp/adhoc_session.hpp>
+#include <xmpp/adhoc_command.hpp>
+
+#include <assert.h>
+
+AdhocSession::AdhocSession(const AdhocCommand& command, const std::string& jid):
+ command(command),
+ owner_jid(jid),
+ current_step(0),
+ terminated(false)
+{
+}
+
+AdhocSession::~AdhocSession()
+{
+}
+
+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/louloulibs/xmpp/adhoc_session.hpp b/louloulibs/xmpp/adhoc_session.hpp
new file mode 100644
index 0000000..ddfb2fe
--- /dev/null
+++ b/louloulibs/xmpp/adhoc_session.hpp
@@ -0,0 +1,70 @@
+#ifndef ADHOC_SESSION_HPP
+# define ADHOC_SESSION_HPP
+
+#include <xmpp/xmpp_stanza.hpp>
+
+#include <functional>
+#include <string>
+
+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…).
+ * TODO fix this:
+ * It also must call one of step_passed(), cancel() etc on the AdhocSession object.
+ */
+typedef std::function<void(XmppComponent*, AdhocSession&, XmlNode&)> AdhocStep;
+
+class AdhocSession
+{
+public:
+ explicit AdhocSession(const AdhocCommand& command, const std::string& jid);
+ ~AdhocSession();
+ /**
+ * 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;
+
+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 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;
+
+ AdhocSession(const AdhocSession&) = delete;
+ AdhocSession(AdhocSession&&) = delete;
+ AdhocSession& operator=(const AdhocSession&) = delete;
+ AdhocSession& operator=(AdhocSession&&) = delete;
+};
+
+#endif // ADHOC_SESSION_HPP
diff --git a/louloulibs/xmpp/body.hpp b/louloulibs/xmpp/body.hpp
new file mode 100644
index 0000000..6ac678e
--- /dev/null
+++ b/louloulibs/xmpp/body.hpp
@@ -0,0 +1,12 @@
+#ifndef XMPP_BODY_HPP_INCLUDED
+#define XMPP_BODY_HPP_INCLUDED
+
+namespace Xmpp
+{
+// Contains:
+// - an XMPP-valid UTF-8 body
+// - an XML node representing the XHTML-IM body, or null
+ typedef std::tuple<const std::string, std::unique_ptr<XmlNode>> body;
+}
+
+#endif /* XMPP_BODY_HPP_INCLUDED */
diff --git a/louloulibs/xmpp/jid.cpp b/louloulibs/xmpp/jid.cpp
new file mode 100644
index 0000000..e6fee45
--- /dev/null
+++ b/louloulibs/xmpp/jid.cpp
@@ -0,0 +1,101 @@
+#include <xmpp/jid.hpp>
+#include <algorithm>
+#include <cstring>
+#include <map>
+
+#include <louloulibs.h>
+#ifdef LIBIDN_FOUND
+ #include <stringprep.h>
+#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);
+}
+
+#include <iostream>
+
+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(), jid.local.size());
+ Stringprep_rc 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(), jid.domain.size());
+ 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 "";
+ }
+ std::replace_if(std::begin(domain), domain + ::strlen(domain),
+ [](const char c) -> bool
+ {
+ return !((c >= 'a' && c <= 'z') || c == '-' ||
+ (c >= '0' && c <= '9') || c == '.');
+ }, '-');
+
+ // 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(), 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/louloulibs/xmpp/jid.hpp b/louloulibs/xmpp/jid.hpp
new file mode 100644
index 0000000..b6975a2
--- /dev/null
+++ b/louloulibs/xmpp/jid.hpp
@@ -0,0 +1,36 @@
+#ifndef JID_INCLUDED
+# define JID_INCLUDED
+
+#include <string>
+
+/**
+ * Parse a JID into its different subart
+ */
+class Jid
+{
+public:
+ explicit Jid(const std::string& jid);
+
+ std::string domain;
+ std::string local;
+ std::string resource;
+
+private:
+ Jid(const Jid&) = delete;
+ Jid(Jid&&) = delete;
+ Jid& operator=(const Jid&) = delete;
+ Jid& operator=(Jid&&) = delete;
+};
+
+/**
+ * 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);
+
+#endif // JID_INCLUDED
diff --git a/louloulibs/xmpp/roster.cpp b/louloulibs/xmpp/roster.cpp
new file mode 100644
index 0000000..a14a384
--- /dev/null
+++ b/louloulibs/xmpp/roster.cpp
@@ -0,0 +1,21 @@
+#include <xmpp/roster.hpp>
+
+RosterItem::RosterItem(const std::string& jid, const std::string& name,
+ std::vector<std::string>& groups):
+ jid(jid),
+ name(name),
+ groups(groups)
+{
+}
+
+RosterItem::RosterItem(const std::string& jid, const std::string& name):
+ jid(jid),
+ name(name),
+ groups{}
+{
+}
+
+void Roster::clear()
+{
+ this->items.clear();
+}
diff --git a/louloulibs/xmpp/roster.hpp b/louloulibs/xmpp/roster.hpp
new file mode 100644
index 0000000..0aebca5
--- /dev/null
+++ b/louloulibs/xmpp/roster.hpp
@@ -0,0 +1,71 @@
+#ifndef ROSTER_HPP_INCLUDED
+#define ROSTER_HPP_INCLUDED
+
+#include <algorithm>
+#include <string>
+#include <vector>
+
+class RosterItem
+{
+public:
+ RosterItem(const std::string& jid, const std::string& name,
+ std::vector<std::string>& groups);
+ RosterItem(const std::string& jid, const std::string& name);
+ RosterItem() = default;
+ ~RosterItem() = default;
+ RosterItem(const RosterItem&) = default;
+ RosterItem(RosterItem&&) = default;
+ RosterItem& operator=(const RosterItem&) = default;
+ RosterItem& operator=(RosterItem&&) = default;
+
+ std::string jid;
+ std::string name;
+ std::vector<std::string> groups;
+
+private:
+};
+
+/**
+ * Keep track of the last known stat of a JID's roster
+ */
+class Roster
+{
+public:
+ Roster() = default;
+ ~Roster() = default;
+
+ void clear();
+
+ template <typename... ArgsType>
+ RosterItem* add_item(ArgsType&&... args)
+ {
+ this->items.emplace_back(std::forward<ArgsType>(args)...);
+ auto it = this->items.end() - 1;
+ return &*it;
+ }
+ RosterItem* get_item(const std::string& jid)
+ {
+ auto it = std::find_if(this->items.begin(), this->items.end(),
+ [this, &jid](const auto& item)
+ {
+ return item.jid == jid;
+ });
+ if (it != this->items.end())
+ return &*it;
+ return nullptr;
+ }
+ const std::vector<RosterItem>& get_items() const
+ {
+ return this->items;
+ }
+
+private:
+ std::vector<RosterItem> items;
+
+ Roster(const Roster&) = delete;
+ Roster(Roster&&) = delete;
+ Roster& operator=(const Roster&) = delete;
+ Roster& operator=(Roster&&) = delete;
+};
+
+#endif /* ROSTER_HPP_INCLUDED */
diff --git a/louloulibs/xmpp/xmpp_component.cpp b/louloulibs/xmpp/xmpp_component.cpp
new file mode 100644
index 0000000..1048f86
--- /dev/null
+++ b/louloulibs/xmpp/xmpp_component.cpp
@@ -0,0 +1,716 @@
+#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 <xmpp/jid.hpp>
+#include <utils/sha1.hpp>
+
+#include <stdexcept>
+#include <iostream>
+#include <set>
+
+#include <stdio.h>
+
+#include <uuid.h>
+
+#include <louloulibs.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, const std::string& hostname, const std::string& secret):
+ TCPSocketHandler(poller),
+ ever_auth(false),
+ first_connection_try(true),
+ secret(secret),
+ authenticated(false),
+ doc_open(false),
+ served_hostname(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("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;
+ XmlNode node("", nullptr);
+ node.set_name("stream:stream");
+ node["xmlns"] = COMPONENT_NS;
+ node["xmlns:stream"] = STREAM_NS;
+ node["to"] = this->served_hostname;
+ this->send_stanza(node);
+ 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)
+{
+ 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(), 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(size, false);
+ }
+}
+
+void XmppComponent::on_remote_stream_open(const XmlNode& node)
+{
+ log_debug("XMPP DOCUMENT OPEN: " << 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
+ char digest[HASH_LENGTH * 2 + 1];
+ sha1nfo sha1;
+ sha1_init(&sha1);
+ sha1_write(&sha1, this->stream_id.data(), this->stream_id.size());
+ sha1_write(&sha1, this->secret.data(), this->secret.size());
+ const uint8_t* result = sha1_result(&sha1);
+ for (int i=0; i < HASH_LENGTH; i++)
+ sprintf(digest + (i*2), "%02x", result[i]);
+ digest[HASH_LENGTH * 2] = '\0';
+
+ Stanza handshake(COMPONENT_NS":handshake");
+ handshake.set_inner(digest);
+ handshake.close();
+ this->send_stanza(handshake);
+}
+
+void XmppComponent::on_remote_stream_close(const XmlNode& node)
+{
+ log_debug("XMPP DOCUMENT CLOSE " << 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)
+{
+ XmlNode node("stream:error", nullptr);
+ XmlNode error(name, nullptr);
+ error["xmlns"] = STREAM_NS;
+ if (!explanation.empty())
+ error.set_inner(explanation);
+ error.close();
+ node.add_child(std::move(error));
+ node.close();
+ 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";
+ XmlNode error("error");
+ error["type"] = error_type;
+ XmlNode inner_error(defined_condition);
+ inner_error["xmlns"] = STANZA_NS;
+ inner_error.close();
+ error.add_child(std::move(inner_error));
+ if (!text.empty())
+ {
+ XmlNode text_node("text");
+ text_node["xmlns"] = STANZA_NS;
+ text_node.set_inner(text);
+ text_node.close();
+ error.add_child(std::move(text_node));
+ }
+ error.close();
+ node.add_child(std::move(error));
+ node.close();
+ 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& stanza)
+{
+ (void)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)
+{
+ 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)
+{
+ XmlNode node("message");
+ node["to"] = to;
+ if (fulljid)
+ node["from"] = from;
+ else
+ node["from"] = from + "@" + this->served_hostname;
+ if (!type.empty())
+ node["type"] = type;
+ XmlNode body_node("body");
+ body_node.set_inner(std::get<0>(body));
+ body_node.close();
+ node.add_child(std::move(body_node));
+ if (std::get<1>(body))
+ {
+ XmlNode html("html");
+ html["xmlns"] = XHTMLIM_NS;
+ // Pass the ownership of the pointer to this xmlnode
+ html.add_child(std::get<1>(body).release());
+ html.close();
+ node.add_child(std::move(html));
+ }
+ node.close();
+ this->send_stanza(node);
+}
+
+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)
+{
+ XmlNode node("presence");
+ node["to"] = to;
+ node["from"] = from + "@" + this->served_hostname + "/" + nick;
+
+ XmlNode x("x");
+ x["xmlns"] = MUC_USER_NS;
+
+ XmlNode item("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;
+ }
+ item.close();
+ x.add_child(std::move(item));
+
+ if (self)
+ {
+ XmlNode status("status");
+ status["code"] = "110";
+ status.close();
+ x.add_child(std::move(status));
+ }
+ x.close();
+ node.add_child(std::move(x));
+ node.close();
+ this->send_stanza(node);
+}
+
+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";
+ XmlNode x("x");
+ x["xmlns"] = MUC_NS;
+ x.close();
+ presence.add_child(std::move(x));
+ XmlNode error("error");
+ error["by"] = muc_name + "@" + this->served_hostname;
+ error["type"] = "cancel";
+ XmlNode item_not_found("item-not-found");
+ item_not_found["xmlns"] = STANZA_NS;
+ item_not_found.close();
+ error.add_child(std::move(item_not_found));
+ XmlNode text("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);
+ text.close();
+ error.add_child(std::move(text));
+ error.close();
+ presence.add_child(std::move(error));
+ presence.close();
+ this->send_stanza(presence);
+}
+
+void XmppComponent::send_invalid_user_error(const std::string& user_name, const std::string& to)
+{
+ Stanza message("message");
+ message["from"] = user_name + "@" + this->served_hostname;
+ message["to"] = to;
+ message["type"] = "error";
+ XmlNode x("x");
+ x["xmlns"] = MUC_NS;
+ x.close();
+ message.add_child(std::move(x));
+ XmlNode error("error");
+ error["type"] = "cancel";
+ XmlNode item_not_found("item-not-found");
+ item_not_found["xmlns"] = STANZA_NS;
+ item_not_found.close();
+ error.add_child(std::move(item_not_found));
+ XmlNode text("text");
+ text["xmlns"] = STANZA_NS;
+ text["xml:lang"] = "en";
+ text.set_inner(user_name +
+ " is not a valid IRC user name. A correct user jid is of the form: <nick>!<server>@" +
+ this->served_hostname);
+ text.close();
+ error.add_child(std::move(text));
+ error.close();
+ message.add_child(std::move(error));
+ message.close();
+ this->send_stanza(message);
+}
+
+void XmppComponent::send_topic(const std::string& from, Xmpp::body&& topic, const std::string& to)
+{
+ XmlNode message("message");
+ message["to"] = to;
+ message["from"] = from + "@" + this->served_hostname;
+ message["type"] = "groupchat";
+ XmlNode subject("subject");
+ subject.set_inner(std::get<0>(topic));
+ subject.close();
+ message.add_child(std::move(subject));
+ message.close();
+ 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)
+{
+ 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";
+ XmlNode body("body");
+ body.set_inner(std::get<0>(xmpp_body));
+ body.close();
+ message.add_child(std::move(body));
+ if (std::get<1>(xmpp_body))
+ {
+ XmlNode html("html");
+ html["xmlns"] = XHTMLIM_NS;
+ // Pass the ownership of the pointer to this xmlnode
+ html.add_child(std::get<1>(xmpp_body).release());
+ html.close();
+ message.add_child(std::move(html));
+ }
+ message.close();
+ this->send_stanza(message);
+}
+
+void XmppComponent::send_muc_leave(const std::string& muc_name, 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);
+ XmlNode x("x");
+ x["xmlns"] = MUC_USER_NS;
+ if (self)
+ {
+ XmlNode status("status");
+ status["code"] = "110";
+ status.close();
+ x.add_child(std::move(status));
+ }
+ x.close();
+ presence.add_child(std::move(x));
+ if (!message_str.empty())
+ {
+ XmlNode status("status");
+ status.set_inner(message_str);
+ status.close();
+ presence.add_child(std::move(status));
+ }
+ presence.close();
+ 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";
+ XmlNode x("x");
+ x["xmlns"] = MUC_USER_NS;
+ XmlNode item("item");
+ item["nick"] = new_nick;
+ item.close();
+ x.add_child(std::move(item));
+ XmlNode status("status");
+ status["code"] = "303";
+ status.close();
+ x.add_child(std::move(status));
+ if (self)
+ {
+ XmlNode status2("status");
+ status2["code"] = "110";
+ status2.close();
+ x.add_child(std::move(status2));
+ }
+ x.close();
+ presence.add_child(std::move(x));
+ presence.close();
+ 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)
+{
+ Stanza presence("presence");
+ presence["from"] = muc_name + "@" + this->served_hostname + "/" + target;
+ presence["to"] = jid_to;
+ presence["type"] = "unavailable";
+ XmlNode x("x");
+ x["xmlns"] = MUC_USER_NS;
+ XmlNode item("item");
+ item["affiliation"] = "none";
+ item["role"] = "none";
+ XmlNode actor("actor");
+ actor["nick"] = author;
+ actor["jid"] = author; // backward compatibility with old clients
+ actor.close();
+ item.add_child(std::move(actor));
+ XmlNode reason("reason");
+ reason.set_inner(txt);
+ reason.close();
+ item.add_child(std::move(reason));
+ item.close();
+ x.add_child(std::move(item));
+ XmlNode status("status");
+ status["code"] = "307";
+ status.close();
+ x.add_child(std::move(status));
+ x.close();
+ presence.add_child(std::move(x));
+ presence.close();
+ 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";
+ XmlNode x("x");
+ x["xmlns"] = MUC_NS;
+ x.close();
+ presence.add_child(std::move(x));
+ XmlNode error("error");
+ error["by"] = muc_name + "@" + this->served_hostname;
+ error["type"] = type;
+ if (!error_code.empty())
+ error["code"] = error_code;
+ XmlNode subnode(condition);
+ subnode["xmlns"] = STANZA_NS;
+ subnode.close();
+ error.add_child(std::move(subnode));
+ error.close();
+ presence.add_child(std::move(error));
+ presence.close();
+ 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;
+ XmlNode x("x");
+ x["xmlns"] = MUC_USER_NS;
+ XmlNode item("item");
+ item["affiliation"] = affiliation;
+ item["role"] = role;
+ item.close();
+ x.add_child(std::move(item));
+ x.close();
+ presence.add_child(std::move(x));
+ presence.close();
+ 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;
+ XmlNode query("query");
+ query["xmlns"] = VERSION_NS;
+ if (version.empty())
+ {
+ XmlNode name("name");
+ name.set_inner("biboumi");
+ name.close();
+ query.add_child(std::move(name));
+ XmlNode version("version");
+ version.set_inner(SOFTWARE_VERSION);
+ version.close();
+ query.add_child(std::move(version));
+ XmlNode os("os");
+ os.set_inner(SYSTEM_NAME);
+ os.close();
+ query.add_child(std::move(os));
+ }
+ else
+ {
+ XmlNode name("name");
+ name.set_inner(version);
+ name.close();
+ query.add_child(std::move(name));
+ }
+ query.close();
+ iq.add_child(std::move(query));
+ iq.close();
+ this->send_stanza(iq);
+}
+
+void XmppComponent::send_adhoc_commands_list(const std::string& id, const std::string& requester_jid)
+{
+ Stanza iq("iq");
+ iq["type"] = "result";
+ iq["id"] = id;
+ iq["to"] = requester_jid;
+ iq["from"] = this->served_hostname;
+ XmlNode query("query");
+ query["xmlns"] = DISCO_ITEMS_NS;
+ query["node"] = ADHOC_NS;
+ for (const auto& kv: this->adhoc_commands_handler.get_commands())
+ {
+ XmlNode item("item");
+ item["jid"] = this->served_hostname;
+ item["node"] = kv.first;
+ item["name"] = kv.second.name;
+ item.close();
+ query.add_child(std::move(item));
+ }
+ query.close();
+ iq.add_child(std::move(query));
+ iq.close();
+ 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;
+ XmlNode query("query");
+ query["xmlns"] = VERSION_NS;
+ query.close();
+ iq.add_child(std::move(query));
+ iq.close();
+ 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";
+ iq.close();
+ 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/louloulibs/xmpp/xmpp_component.hpp b/louloulibs/xmpp/xmpp_component.hpp
new file mode 100644
index 0000000..1bea54e
--- /dev/null
+++ b/louloulibs/xmpp/xmpp_component.hpp
@@ -0,0 +1,240 @@
+#ifndef XMPP_COMPONENT_INCLUDED
+# define XMPP_COMPONENT_INCLUDED
+
+#include <xmpp/adhoc_commands_handler.hpp>
+#include <network/tcp_socket_handler.hpp>
+#include <xmpp/xmpp_parser.hpp>
+#include <xmpp/body.hpp>
+
+#include <unordered_map>
+#include <memory>
+#include <string>
+#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 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"
+
+/**
+ * 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 TCPSocketHandler
+{
+public:
+ explicit XmppComponent(std::shared_ptr<Poller> poller, const std::string& hostname, const std::string& secret);
+ virtual ~XmppComponent() = default;
+
+ 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& message, 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=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 an error to indicate that the user tried to send a message to an
+ * invalid user.
+ */
+ void send_invalid_user_error(const std::string& user_name,
+ 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);
+ /**
+ * 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);
+ /**
+ * Send an unavailable presence for this nick
+ */
+ void send_muc_leave(const std::string& muc_name, 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);
+ /**
+ * 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 gateway disco informations.
+ */
+ void send_self_disco_info(const std::string& id, 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);
+ /**
+ * 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 handle_handshake(const Stanza& stanza);
+ void handle_error(const Stanza& stanza);
+
+ virtual void after_handshake() {}
+
+ /**
+ * 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;
+
+ XmppComponent(const XmppComponent&) = delete;
+ XmppComponent(XmppComponent&&) = delete;
+ XmppComponent& operator=(const XmppComponent&) = delete;
+ XmppComponent& operator=(XmppComponent&&) = delete;
+};
+
+#endif // XMPP_COMPONENT_INCLUDED
diff --git a/louloulibs/xmpp/xmpp_parser.cpp b/louloulibs/xmpp/xmpp_parser.cpp
new file mode 100644
index 0000000..6bb0d28
--- /dev/null
+++ b/louloulibs/xmpp/xmpp_parser.cpp
@@ -0,0 +1,169 @@
+#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)
+{
+ 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()
+{
+ if (this->current_node)
+ delete this->current_node;
+ 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();
+ if (this->current_node)
+ delete this->current_node;
+ this->current_node = 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)
+{
+ level++;
+
+ XmlNode* new_node = new XmlNode(name, this->current_node);
+ if (this->current_node)
+ this->current_node->add_child(new_node);
+ this->current_node = new_node;
+ 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* name)
+{
+ (void)name;
+ level--;
+ this->current_node->close();
+ if (level == 1)
+ {
+ this->stanza_event(*this->current_node);
+ }
+ if (level == 0)
+ {
+ this->stream_close_event(*this->current_node);
+ delete this->current_node;
+ this->current_node = nullptr;
+ }
+ else
+ this->current_node = this->current_node->get_parent();
+ if (level == 1)
+ this->current_node->delete_all_children();
+}
+
+void XmppParser::char_data(const XML_Char* data, int len)
+{
+ if (this->current_node->has_children())
+ this->current_node->get_last_child()->add_to_tail(std::string(data, len));
+ else
+ this->current_node->add_to_inner(std::string(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_debug("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/louloulibs/xmpp/xmpp_parser.hpp b/louloulibs/xmpp/xmpp_parser.hpp
new file mode 100644
index 0000000..79c9f8f
--- /dev/null
+++ b/louloulibs/xmpp/xmpp_parser.hpp
@@ -0,0 +1,126 @@
+#ifndef XMPP_PARSER_INCLUDED
+# define XMPP_PARSER_INCLUDED
+
+#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();
+
+public:
+ /**
+ * Init the XML parser and install the callbacks
+ */
+ void init_xml_parser();
+ /**
+ * 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, int 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:
+ /**
+ * 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;
+ /**
+ * 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;
+
+ XmppParser(const XmppParser&) = delete;
+ XmppParser& operator=(const XmppParser&) = delete;
+};
+
+#endif // XMPP_PARSER_INCLUDED
diff --git a/louloulibs/xmpp/xmpp_stanza.cpp b/louloulibs/xmpp/xmpp_stanza.cpp
new file mode 100644
index 0000000..01d1d2e
--- /dev/null
+++ b/louloulibs/xmpp/xmpp_stanza.cpp
@@ -0,0 +1,274 @@
+#include <xmpp/xmpp_stanza.hpp>
+
+#include <utils/encoding.hpp>
+#include <utils/split.hpp>
+
+#include <stdexcept>
+#include <iostream>
+
+#include <string.h>
+
+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 xml_unescape(const std::string& data)
+{
+ std::string res;
+ res.reserve(data.size());
+ const char* str = data.c_str();
+ while (str && *str && static_cast<size_t>(str - data.c_str()) < data.size())
+ {
+ if (*str == '&')
+ {
+ if (strncmp(str+1, "amp;", 4) == 0)
+ {
+ res += "&";
+ str += 4;
+ }
+ else if (strncmp(str+1, "lt;", 3) == 0)
+ {
+ res += "<";
+ str += 3;
+ }
+ else if (strncmp(str+1, "gt;", 3) == 0)
+ {
+ res += ">";
+ str += 3;
+ }
+ else if (strncmp(str+1, "quot;", 5) == 0)
+ {
+ res += "\"";
+ str += 5;
+ }
+ else if (strncmp(str+1, "apos;", 5) == 0)
+ {
+ res += "'";
+ str += 5;
+ }
+ else
+ res += "&";
+ }
+ else
+ res += *str;
+ str++;
+ }
+ return res;
+}
+
+XmlNode::XmlNode(const std::string& name, XmlNode* parent):
+ parent(parent),
+ closed(false)
+{
+ // 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)
+{
+}
+
+XmlNode::~XmlNode()
+{
+ this->delete_all_children();
+}
+
+void XmlNode::delete_all_children()
+{
+ for (auto& child: this->children)
+ {
+ delete child;
+ }
+ 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;
+}
+
+XmlNode* XmlNode::get_child(const std::string& name, const std::string& xmlns) const
+{
+ for (auto& child: this->children)
+ {
+ if (child->name == name && child->get_tag("xmlns") == xmlns)
+ return child;
+ }
+ return nullptr;
+}
+
+std::vector<XmlNode*> XmlNode::get_children(const std::string& name, const std::string& xmlns) const
+{
+ std::vector<XmlNode*> res;
+ for (auto& child: this->children)
+ {
+ if (child->name == name && child->get_tag("xmlns") == xmlns)
+ res.push_back(child);
+ }
+ return res;
+}
+
+XmlNode* XmlNode::add_child(XmlNode* child)
+{
+ child->parent = this;
+ this->children.push_back(child);
+ return child;
+}
+
+XmlNode* XmlNode::add_child(XmlNode&& child)
+{
+ XmlNode* new_node = new XmlNode(std::move(child));
+ return this->add_child(new_node);
+}
+
+XmlNode* XmlNode::get_last_child() const
+{
+ return this->children.back();
+}
+
+void XmlNode::close()
+{
+ if (this->closed)
+ throw std::runtime_error("Closing an already closed XmlNode");
+ this->closed = true;
+}
+
+XmlNode* XmlNode::get_parent() const
+{
+ return this->parent;
+}
+
+void XmlNode::set_name(const std::string& name)
+{
+ this->name = name;
+}
+
+const std::string XmlNode::get_name() const
+{
+ return this->name;
+}
+
+std::string XmlNode::to_string() const
+{
+ std::string res("<");
+ res += this->name;
+ for (const auto& it: this->attributes)
+ res += " " + it.first + "='" + sanitize(it.second) + "'";
+ if (this->closed && !this->has_children() && this->inner.empty())
+ res += "/>";
+ else
+ {
+ res += ">" + sanitize(this->inner);
+ for (const auto& child: this->children)
+ res += child->to_string();
+ if (this->closed)
+ {
+ res += "</" + this->get_name() + ">";
+ }
+ }
+ res += sanitize(this->tail);
+ return res;
+}
+
+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)
+ {
+ return "";
+ }
+}
+
+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::string sanitize(const std::string& data)
+{
+ 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, "ISO-8859-1")));
+}
diff --git a/louloulibs/xmpp/xmpp_stanza.hpp b/louloulibs/xmpp/xmpp_stanza.hpp
new file mode 100644
index 0000000..f1a6a0f
--- /dev/null
+++ b/louloulibs/xmpp/xmpp_stanza.hpp
@@ -0,0 +1,160 @@
+#ifndef XMPP_STANZA_INCLUDED
+# define XMPP_STANZA_INCLUDED
+
+#include <unordered_map>
+#include <string>
+#include <vector>
+
+std::string xml_escape(const std::string& data);
+std::string xml_unescape(const std::string& data);
+std::string sanitize(const std::string& data);
+
+/**
+ * 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);
+ XmlNode(XmlNode&& node):
+ name(std::move(node.name)),
+ parent(node.parent),
+ closed(node.closed),
+ attributes(std::move(node.attributes)),
+ children(std::move(node.children)),
+ inner(std::move(node.inner)),
+ tail(std::move(node.tail))
+ {
+ node.parent = nullptr;
+ }
+ /**
+ * The copy constructor do not copy the parent attribute. The children
+ * nodes are all copied recursively.
+ */
+ XmlNode(const XmlNode& node):
+ name(node.name),
+ parent(nullptr),
+ closed(node.closed),
+ attributes(node.attributes),
+ children{},
+ inner(node.inner),
+ tail(node.tail)
+ {
+ for (XmlNode* child: node.children)
+ {
+ XmlNode* child_copy = new XmlNode(*child);
+ this->add_child(child_copy);
+ }
+ }
+
+ ~XmlNode();
+
+ 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
+ */
+ 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<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(XmlNode* child);
+ XmlNode* add_child(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;
+ /**
+ * Mark this node as closed, nothing else
+ */
+ void close();
+ XmlNode* get_parent() const;
+ void set_name(const 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;
+ bool closed;
+ std::unordered_map<std::string, std::string> attributes;
+ std::vector<XmlNode*> children;
+ std::string inner;
+ std::string tail;
+
+ XmlNode& operator=(const XmlNode&) = delete;
+ XmlNode& operator=(XmlNode&&) = delete;
+};
+
+/**
+ * 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)
+ */
+typedef XmlNode Stanza;
+
+#endif // XMPP_STANZA_INCLUDED