From 0ab40dc1ab4e689921da54080b135e1d22b1c586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?louiz=E2=80=99?= Date: Tue, 14 Mar 2017 21:45:23 +0100 Subject: Refactoring louloulibs and cmake Use OBJECT libraries Remove the louloulibs directory Write FOUND variables in the cache --- src/xmpp/adhoc_command.cpp | 80 +++++ src/xmpp/adhoc_command.hpp | 44 +++ src/xmpp/adhoc_commands_handler.cpp | 111 ++++++ src/xmpp/adhoc_commands_handler.hpp | 71 ++++ src/xmpp/adhoc_session.cpp | 35 ++ src/xmpp/adhoc_session.hpp | 88 +++++ src/xmpp/auth.cpp | 8 + src/xmpp/auth.hpp | 6 + src/xmpp/biboumi_component.cpp | 1 - src/xmpp/body.hpp | 12 + src/xmpp/jid.cpp | 153 ++++++++ src/xmpp/jid.hpp | 49 +++ src/xmpp/xmpp_component.cpp | 672 ++++++++++++++++++++++++++++++++++++ src/xmpp/xmpp_component.hpp | 245 +++++++++++++ src/xmpp/xmpp_parser.cpp | 172 +++++++++ src/xmpp/xmpp_parser.hpp | 133 +++++++ src/xmpp/xmpp_stanza.cpp | 229 ++++++++++++ src/xmpp/xmpp_stanza.hpp | 160 +++++++++ 18 files changed, 2268 insertions(+), 1 deletion(-) create mode 100644 src/xmpp/adhoc_command.cpp create mode 100644 src/xmpp/adhoc_command.hpp create mode 100644 src/xmpp/adhoc_commands_handler.cpp create mode 100644 src/xmpp/adhoc_commands_handler.hpp create mode 100644 src/xmpp/adhoc_session.cpp create mode 100644 src/xmpp/adhoc_session.hpp create mode 100644 src/xmpp/auth.cpp create mode 100644 src/xmpp/auth.hpp create mode 100644 src/xmpp/body.hpp create mode 100644 src/xmpp/jid.cpp create mode 100644 src/xmpp/jid.hpp create mode 100644 src/xmpp/xmpp_component.cpp create mode 100644 src/xmpp/xmpp_component.hpp create mode 100644 src/xmpp/xmpp_parser.cpp create mode 100644 src/xmpp/xmpp_parser.hpp create mode 100644 src/xmpp/xmpp_stanza.cpp create mode 100644 src/xmpp/xmpp_stanza.hpp (limited to 'src/xmpp') diff --git a/src/xmpp/adhoc_command.cpp b/src/xmpp/adhoc_command.cpp new file mode 100644 index 0000000..825cc92 --- /dev/null +++ b/src/xmpp/adhoc_command.cpp @@ -0,0 +1,80 @@ +#include +#include +#include + +using namespace std::string_literals; + +AdhocCommand::AdhocCommand(std::vector&& callbacks, const std::string& name, const bool admin_only): + name(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..7c4de47 --- /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 + +#include +#include + +class AdhocCommand +{ + friend class AdhocSession; +public: + AdhocCommand(std::vector&& callback, const 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 XmlNode and calling + * methods of an AdhocSession. + */ + std::vector 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 +#include + +#include +#include +#include +#include + +#include + +using namespace std::string_literals; + +const std::map& 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 +#include + +#include +#include +#include + +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& 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 node containing one or more useful children. If + * it contains an node, the iq response will have an error type. + * + * Takes a copy of the 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 commands; + /** + * The list of all currently on-going commands. + * + * Of the form: {{session_id, owner_jid}, session}. + */ + std::map, AdhocSession> sessions; +}; diff --git a/src/xmpp/adhoc_session.cpp b/src/xmpp/adhoc_session.cpp new file mode 100644 index 0000000..dda4bea --- /dev/null +++ b/src/xmpp/adhoc_session.cpp @@ -0,0 +1,35 @@ +#include +#include + +#include + +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 + +#include +#include +#include + +class XmppComponent; + +class AdhocCommand; +class AdhocSession; + +/** + * A function executed as an ad-hoc command step. It takes a + * XmlNode and modifies it accordingly (inserting for example an + * node, or a data form…). + */ +using AdhocStep = std::function; + +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 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 + +#include + +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 + +std::string get_handshake_digest(const std::string& stream_id, const std::string& secret); + diff --git a/src/xmpp/biboumi_component.cpp b/src/xmpp/biboumi_component.cpp index 4ba5e65..1c0f65c 100644 --- a/src/xmpp/biboumi_component.cpp +++ b/src/xmpp/biboumi_component.cpp @@ -16,7 +16,6 @@ #include -#include #include #include 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>; +} + + diff --git a/src/xmpp/jid.cpp b/src/xmpp/jid.cpp new file mode 100644 index 0000000..0751387 --- /dev/null +++ b/src/xmpp/jid.cpp @@ -0,0 +1,153 @@ +#include +#include +#include +#include + +#include +#ifdef LIBIDN_FOUND + #include + #include + #include + #include + #include + #include +#endif + +#include + +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; + static CacheType cache; + std::pair 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())); + Stringprep_rc rc = static_cast(::stringprep(local, max_jid_part_len, + static_cast(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; + memset(&hints, 0, sizeof(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(domain, max_jid_part_len, + static_cast(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 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(resource, max_jid_part_len, + static_cast(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 + +/** + * 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..1453b18 --- /dev/null +++ b/src/xmpp/xmpp_component.cpp @@ -0,0 +1,672 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include +#include + +#include +#ifdef SYSTEMD_FOUND +# include +#endif + +using namespace std::string_literals; + +static std::set 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, const std::string& hostname, const std::string& secret): + TCPClientSocketHandler(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(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 = ""; + 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) +{ + 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 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 = ""s + get_handshake_digest(this->stream_id, this->secret) + ""; + 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 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: "); + this->send_data(""); + 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::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: #%@" + + 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) +{ + 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))); + } + 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, 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..16d7480 --- /dev/null +++ b/src/xmpp/xmpp_component.hpp @@ -0,0 +1,245 @@ +#pragma once + + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#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" +#define DELAY_NS "urn:xmpp:delay" +#define MAM_NS "urn:xmpp:mam:1" +#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" + +/** + * 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, const std::string& hostname, const 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& 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, 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); + /** + * Send a message, with a 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, 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> 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 +#include + +#include + +/** + * 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(user_data)->start_element(name, atts); +} + +static void end_element_handler(void* user_data, const XML_Char* name) +{ + static_cast(user_data)->end_element(name); +} + +static void character_data_handler(void *user_data, const XML_Char *s, int len) +{ + static_cast(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(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(size)); +} + +void XmppParser::start_element(const XML_Char* name, const XML_Char** attribute) +{ + this->level++; + + auto new_node = std::make_unique(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&& callback) +{ + this->stanza_callbacks.emplace_back(std::move(callback)); +} + +void XmppParser::add_stream_open_callback(std::function&& callback) +{ + this->stream_open_callbacks.emplace_back(std::move(callback)); +} + +void XmppParser::add_stream_close_callback(std::function&& 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..9d67228 --- /dev/null +++ b/src/xmpp/xmpp_parser.hpp @@ -0,0 +1,133 @@ +#pragma once + + +#include + +#include + +#include + +/** + * 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&& callback); + void add_stream_open_callback(std::function&& callback); + void add_stream_close_callback(std::function&& 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 root; + /** + * A list of callbacks to be called on an *_event, receiving the + * concerned Stanza/XmlNode. + */ + std::vector> stanza_callbacks; + std::vector> stream_open_callbacks; + std::vector> stream_close_callbacks; +}; + + diff --git a/src/xmpp/xmpp_stanza.cpp b/src/xmpp/xmpp_stanza.cpp new file mode 100644 index 0000000..ac6ce9b --- /dev/null +++ b/src/xmpp/xmpp_stanza.cpp @@ -0,0 +1,229 @@ +#include + +#include +#include + +#include +#include +#include + +#include + +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 += "&"; + break; + case '<': + res += "<"; + break; + case '>': + res += ">"; + break; + case '\"': + res += """; + break; + case '\'': + res += "'"; + 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 XmlNode::get_children(const std::string& name, const std::string& xmlns) const +{ + std::vector 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 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(std::move(child)); + return this->add_child(std::move(new_node)); +} + +XmlNode* XmlNode::add_child(const XmlNode& child) +{ + auto new_node = std::make_unique(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 << "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 +#include +#include +#include + +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(*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 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 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 attributes; + std::vector> 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 , 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 -- cgit v1.2.3