Refactoring louloulibs and cmake
Use OBJECT libraries Remove the louloulibs directory Write FOUND variables in the cache
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
#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)
|
||||
{
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* Describe an ad-hoc command.
|
||||
*
|
||||
* Can only have zero or one step for now. When execution is requested, it
|
||||
* can return a result immediately, or provide a form to be filled, and
|
||||
* provide a result once the filled form is received.
|
||||
*/
|
||||
|
||||
#include <xmpp/adhoc_session.hpp>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
class AdhocCommand
|
||||
{
|
||||
friend class AdhocSession;
|
||||
public:
|
||||
AdhocCommand(std::vector<AdhocStep>&& 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 <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);
|
||||
@@ -0,0 +1,111 @@
|
||||
#include <xmpp/adhoc_commands_handler.hpp>
|
||||
#include <xmpp/xmpp_component.hpp>
|
||||
|
||||
#include <utils/timed_events.hpp>
|
||||
#include <logger/logger.hpp>
|
||||
#include <config/config.hpp>
|
||||
#include <xmpp/jid.hpp>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
using namespace std::string_literals;
|
||||
|
||||
const std::map<const std::string, const AdhocCommand>& AdhocCommandsHandler::get_commands() const
|
||||
{
|
||||
return this->commands;
|
||||
}
|
||||
|
||||
void AdhocCommandsHandler::add_command(std::string name, AdhocCommand command)
|
||||
{
|
||||
const auto found = this->commands.find(name);
|
||||
if (found != this->commands.end())
|
||||
throw std::runtime_error("Trying to add an ad-hoc command that already exist: "s + name);
|
||||
this->commands.emplace(std::make_pair(std::move(name), std::move(command)));
|
||||
}
|
||||
|
||||
XmlNode AdhocCommandsHandler::handle_request(const std::string& executor_jid, const std::string& to, XmlNode command_node)
|
||||
{
|
||||
std::string action = command_node.get_tag("action");
|
||||
if (action.empty())
|
||||
action = "execute";
|
||||
command_node.del_tag("action");
|
||||
|
||||
Jid jid(executor_jid);
|
||||
|
||||
const std::string node = command_node.get_tag("node");
|
||||
auto command_it = this->commands.find(node);
|
||||
if (command_it == this->commands.end())
|
||||
{
|
||||
XmlSubNode error(command_node, ADHOC_NS":error");
|
||||
error["type"] = "cancel";
|
||||
XmlSubNode condition(error, STANZA_NS":item-not-found");
|
||||
}
|
||||
else if (command_it->second.is_admin_only() &&
|
||||
Config::get("admin", "") != jid.local + "@" + jid.domain)
|
||||
{
|
||||
XmlSubNode error(command_node, ADHOC_NS":error");
|
||||
error["type"] = "cancel";
|
||||
XmlSubNode condition(error, STANZA_NS":forbidden");
|
||||
}
|
||||
else
|
||||
{
|
||||
std::string sessionid = command_node.get_tag("sessionid");
|
||||
if (sessionid.empty())
|
||||
{ // create a new session, with a new id
|
||||
sessionid = XmppComponent::next_id();
|
||||
command_node["sessionid"] = sessionid;
|
||||
this->sessions.emplace(std::piecewise_construct,
|
||||
std::forward_as_tuple(sessionid, executor_jid),
|
||||
std::forward_as_tuple(command_it->second, executor_jid, to));
|
||||
TimedEventsManager::instance().add_event(TimedEvent(std::chrono::steady_clock::now() + 3600s,
|
||||
std::bind(&AdhocCommandsHandler::remove_session, this, sessionid, executor_jid),
|
||||
"adhocsession"s + sessionid + executor_jid));
|
||||
}
|
||||
auto session_it = this->sessions.find(std::make_pair(sessionid, executor_jid));
|
||||
if ((session_it != this->sessions.end()) &&
|
||||
(action == "execute" || action == "next" || action == "complete"))
|
||||
{
|
||||
// execute the step
|
||||
AdhocSession& session = session_it->second;
|
||||
const AdhocStep& step = session.get_next_step();
|
||||
step(this->xmpp_component, session, command_node);
|
||||
if (session.remaining_steps() == 0 ||
|
||||
session.is_terminated())
|
||||
{
|
||||
this->sessions.erase(session_it);
|
||||
command_node["status"] = "completed";
|
||||
TimedEventsManager::instance().cancel("adhocsession"s + sessionid + executor_jid);
|
||||
}
|
||||
else
|
||||
{
|
||||
command_node["status"] = "executing";
|
||||
XmlSubNode actions(command_node, "actions");
|
||||
XmlSubNode next(actions, "next");
|
||||
}
|
||||
}
|
||||
else if (action == "cancel")
|
||||
{
|
||||
this->sessions.erase(session_it);
|
||||
command_node["status"] = "canceled";
|
||||
TimedEventsManager::instance().cancel("adhocsession"s + sessionid + executor_jid);
|
||||
}
|
||||
else // unsupported action
|
||||
{
|
||||
XmlSubNode error(command_node, ADHOC_NS":error");
|
||||
error["type"] = "modify";
|
||||
XmlSubNode condition(error, STANZA_NS":bad-request");
|
||||
}
|
||||
}
|
||||
return command_node;
|
||||
}
|
||||
|
||||
void AdhocCommandsHandler::remove_session(const std::string& session_id, const std::string& initiator_jid)
|
||||
{
|
||||
auto session_it = this->sessions.find(std::make_pair(session_id, initiator_jid));
|
||||
if (session_it != this->sessions.end())
|
||||
{
|
||||
this->sessions.erase(session_it);
|
||||
return ;
|
||||
}
|
||||
log_error("Tried to remove ad-hoc session for [", session_id, ", ", initiator_jid, "] but none found");
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* Manage a list of available AdhocCommands and the list of ongoing
|
||||
* AdhocCommandSessions.
|
||||
*/
|
||||
|
||||
#include <xmpp/adhoc_command.hpp>
|
||||
#include <xmpp/xmpp_stanza.hpp>
|
||||
|
||||
#include <utility>
|
||||
#include <string>
|
||||
#include <map>
|
||||
|
||||
class AdhocCommandsHandler
|
||||
{
|
||||
public:
|
||||
explicit AdhocCommandsHandler(XmppComponent& xmpp_component):
|
||||
xmpp_component(xmpp_component),
|
||||
commands{}
|
||||
{ }
|
||||
~AdhocCommandsHandler() = default;
|
||||
|
||||
AdhocCommandsHandler(const AdhocCommandsHandler&) = delete;
|
||||
AdhocCommandsHandler(AdhocCommandsHandler&&) = delete;
|
||||
AdhocCommandsHandler& operator=(const AdhocCommandsHandler&) = delete;
|
||||
AdhocCommandsHandler& operator=(AdhocCommandsHandler&&) = delete;
|
||||
|
||||
/**
|
||||
* Returns the list of available commands.
|
||||
*/
|
||||
const std::map<const std::string, const AdhocCommand>& get_commands() const;
|
||||
/**
|
||||
* Add a command into the list, associated with the given name
|
||||
*/
|
||||
void add_command(std::string name, AdhocCommand command);
|
||||
/**
|
||||
* Find the requested command, create a new session or use an existing
|
||||
* one, and process the request (provide a new form, an error, or a
|
||||
* result).
|
||||
*
|
||||
* Returns a (moved) XmlNode that will be inserted in the iq response. It
|
||||
* should be a <command/> node containing one or more useful children. If
|
||||
* it contains an <error/> node, the iq response will have an error type.
|
||||
*
|
||||
* Takes a copy of the <command/> node so we can actually edit it and use
|
||||
* it as our return value.
|
||||
*/
|
||||
XmlNode handle_request(const std::string& executor_jid, const std::string& to, XmlNode command_node);
|
||||
/**
|
||||
* Remove the session from the list. This is done to avoid filling the
|
||||
* memory with waiting session (for example due to a client that starts
|
||||
* multi-steps command but never finishes them).
|
||||
*/
|
||||
void remove_session(const std::string& session_id, const std::string& initiator_jid);
|
||||
private:
|
||||
/**
|
||||
* To access basically anything in the gateway.
|
||||
*/
|
||||
XmppComponent& xmpp_component;
|
||||
/**
|
||||
* The list of all available commands.
|
||||
*/
|
||||
std::map<const std::string, const AdhocCommand> commands;
|
||||
/**
|
||||
* The list of all currently on-going commands.
|
||||
*
|
||||
* Of the form: {{session_id, owner_jid}, session}.
|
||||
*/
|
||||
std::map<std::pair<const std::string, const std::string>, AdhocSession> sessions;
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
#include <xmpp/adhoc_session.hpp>
|
||||
#include <xmpp/adhoc_command.hpp>
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
#pragma once
|
||||
|
||||
#include <xmpp/xmpp_stanza.hpp>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <map>
|
||||
|
||||
class XmppComponent;
|
||||
|
||||
class AdhocCommand;
|
||||
class AdhocSession;
|
||||
|
||||
/**
|
||||
* A function executed as an ad-hoc command step. It takes a <command/>
|
||||
* XmlNode and modifies it accordingly (inserting for example an <error/>
|
||||
* node, or a data form…).
|
||||
*/
|
||||
using AdhocStep = std::function<void(XmppComponent&, AdhocSession&, XmlNode&)>;
|
||||
|
||||
class AdhocSession
|
||||
{
|
||||
public:
|
||||
explicit AdhocSession(const AdhocCommand& command, const std::string& owner_jid,
|
||||
const std::string& to_jid);
|
||||
~AdhocSession() = default;
|
||||
|
||||
AdhocSession(const AdhocSession&) = delete;
|
||||
AdhocSession(AdhocSession&&) = delete;
|
||||
AdhocSession& operator=(const AdhocSession&) = delete;
|
||||
AdhocSession& operator=(AdhocSession&&) = delete;
|
||||
|
||||
/**
|
||||
* Return the function to be executed, found in our AdhocCommand, for the
|
||||
* current_step. And increment the current_step.
|
||||
*/
|
||||
const AdhocStep& get_next_step();
|
||||
/**
|
||||
* Return the number of remaining steps.
|
||||
*/
|
||||
size_t remaining_steps() const;
|
||||
/**
|
||||
* This may be modified by an AdhocStep, to indicate that this session
|
||||
* should no longer exist, because we encountered an error, and we can't
|
||||
* execute any more step of it.
|
||||
*/
|
||||
void terminate();
|
||||
bool is_terminated() const;
|
||||
std::string get_target_jid() const
|
||||
{
|
||||
return this->to_jid;
|
||||
}
|
||||
std::string get_owner_jid() const
|
||||
{
|
||||
return this->owner_jid;
|
||||
}
|
||||
|
||||
private:
|
||||
/**
|
||||
* A reference of the command concerned by this session. Used for example
|
||||
* to get the next step of that command, things like that.
|
||||
*/
|
||||
const AdhocCommand& command;
|
||||
/**
|
||||
* The full JID of the XMPP user that created this session by executing
|
||||
* the first step of a command. Only that JID must be allowed to access
|
||||
* this session.
|
||||
*/
|
||||
const std::string& owner_jid;
|
||||
/**
|
||||
* The 'to' attribute in the request stanza. This is the target of the current session.
|
||||
*/
|
||||
const std::string& to_jid;
|
||||
/**
|
||||
* The current step we are at. It starts at zero. It is used to index the
|
||||
* associated AdhocCommand::callbacks vector.
|
||||
*/
|
||||
size_t current_step;
|
||||
bool terminated;
|
||||
|
||||
public:
|
||||
/**
|
||||
* A map to store various things that we may want to remember between two
|
||||
* steps of the same session. A step can insert any value associated to
|
||||
* any key in there.
|
||||
*/
|
||||
std::map<std::string, std::string> vars;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
#include <xmpp/auth.hpp>
|
||||
|
||||
#include <utils/sha1.hpp>
|
||||
|
||||
std::string get_handshake_digest(const std::string& stream_id, const std::string& secret)
|
||||
{
|
||||
return sha1(stream_id + secret);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
std::string get_handshake_digest(const std::string& stream_id, const std::string& secret);
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
#include <louloulibs.h>
|
||||
#include <biboumi.h>
|
||||
|
||||
#include <uuid/uuid.h>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
|
||||
namespace Xmpp
|
||||
{
|
||||
// Contains:
|
||||
// - an XMPP-valid UTF-8 body
|
||||
// - an XML node representing the XHTML-IM body, or null
|
||||
using body = std::tuple<const std::string, std::unique_ptr<XmlNode>>;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
#include <xmpp/jid.hpp>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
|
||||
#include <biboumi.h>
|
||||
#ifdef LIBIDN_FOUND
|
||||
#include <stringprep.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netdb.h>
|
||||
#include <utils/scopeguard.hpp>
|
||||
#include <set>
|
||||
#endif
|
||||
|
||||
#include <logger/logger.hpp>
|
||||
|
||||
Jid::Jid(const std::string& jid)
|
||||
{
|
||||
std::string::size_type slash = jid.find('/');
|
||||
if (slash != std::string::npos)
|
||||
{
|
||||
this->resource = jid.substr(slash + 1);
|
||||
}
|
||||
|
||||
std::string::size_type at = jid.find('@');
|
||||
if (at != std::string::npos && at < slash)
|
||||
{
|
||||
this->local = jid.substr(0, at);
|
||||
at++;
|
||||
}
|
||||
else
|
||||
at = 0;
|
||||
|
||||
this->domain = jid.substr(at, slash - at);
|
||||
}
|
||||
|
||||
static constexpr size_t max_jid_part_len = 1023;
|
||||
|
||||
std::string jidprep(const std::string& original)
|
||||
{
|
||||
#ifdef LIBIDN_FOUND
|
||||
using CacheType = std::map<std::string, std::string>;
|
||||
static CacheType cache;
|
||||
std::pair<CacheType::iterator, bool> cached = cache.insert({original, {}});
|
||||
if (std::get<1>(cached) == false)
|
||||
{ // Insertion failed: the result is already in the cache, return it
|
||||
return std::get<0>(cached)->second;
|
||||
}
|
||||
|
||||
const std::string error_msg("Failed to convert " + original + " into a valid JID:");
|
||||
Jid jid(original);
|
||||
|
||||
char local[max_jid_part_len] = {};
|
||||
memcpy(local, jid.local.data(), std::min(max_jid_part_len, jid.local.size()));
|
||||
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(), 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_rc>(::stringprep(domain, max_jid_part_len,
|
||||
static_cast<Stringprep_profile_flags>(0), stringprep_nameprep));
|
||||
if (rc != STRINGPREP_OK)
|
||||
{
|
||||
log_error(error_msg + stringprep_strerror(rc));
|
||||
return "";
|
||||
}
|
||||
|
||||
// Make sure it contains only allowed characters
|
||||
using std::begin;
|
||||
using std::end;
|
||||
char* domain_end = domain + ::strlen(domain);
|
||||
std::replace_if(std::begin(domain), domain + ::strlen(domain),
|
||||
[](const char c) -> bool
|
||||
{
|
||||
return !((c >= 'a' && c <= 'z') || c == '-' ||
|
||||
(c >= '0' && c <= '9') || c == '.');
|
||||
}, '-');
|
||||
// Make sure there are no doubled - or .
|
||||
std::set<char> special_chars{'-', '.'};
|
||||
domain_end = std::unique(begin(domain), domain + ::strlen(domain), [&special_chars](const char& a, const char& b) -> bool
|
||||
{
|
||||
return special_chars.count(a) && special_chars.count(b);
|
||||
});
|
||||
// remove leading and trailing -. if any
|
||||
if (domain_end != domain && special_chars.count(*(domain_end - 1)))
|
||||
--domain_end;
|
||||
if (domain_end != domain && special_chars.count(domain[0]))
|
||||
{
|
||||
std::memmove(domain, domain + 1, domain_end - domain + 1);
|
||||
--domain_end;
|
||||
}
|
||||
// And if the final result is an empty string, return a dummy hostname
|
||||
if (domain_end == domain)
|
||||
::strcpy(domain, "empty");
|
||||
else
|
||||
*domain_end = '\0';
|
||||
}
|
||||
else if (addr_res->ai_family == AF_INET6)
|
||||
{ // IPv6, surround it with []. The length is always enough:
|
||||
// the longest possible IPv6 is way shorter than max_jid_part_len
|
||||
::memmove(domain + 1, domain, jid.domain.size());
|
||||
domain[0] = '[';
|
||||
domain[jid.domain.size() + 1] = ']';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If there is no resource, stop here
|
||||
if (jid.resource.empty())
|
||||
{
|
||||
std::get<0>(cached)->second = std::string(local) + "@" + domain;
|
||||
return std::get<0>(cached)->second;
|
||||
}
|
||||
|
||||
// Otherwise, also process the resource part
|
||||
char resource[max_jid_part_len] = {};
|
||||
memcpy(resource, jid.resource.data(), std::min(max_jid_part_len, jid.resource.size()));
|
||||
rc = static_cast<Stringprep_rc>(::stringprep(resource, max_jid_part_len,
|
||||
static_cast<Stringprep_profile_flags>(0), stringprep_xmpp_resourceprep));
|
||||
if (rc != STRINGPREP_OK)
|
||||
{
|
||||
log_error(error_msg + stringprep_strerror(rc));
|
||||
return "";
|
||||
}
|
||||
std::get<0>(cached)->second = std::string(local) + "@" + domain + "/" + resource;
|
||||
return std::get<0>(cached)->second;
|
||||
|
||||
#else
|
||||
(void)original;
|
||||
return "";
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* Parse a JID into its different subart
|
||||
*/
|
||||
class Jid
|
||||
{
|
||||
public:
|
||||
explicit Jid(const std::string& jid);
|
||||
|
||||
Jid(const Jid&) = delete;
|
||||
Jid(Jid&&) = delete;
|
||||
Jid& operator=(const Jid&) = delete;
|
||||
Jid& operator=(Jid&&) = delete;
|
||||
|
||||
std::string domain;
|
||||
std::string local;
|
||||
std::string resource;
|
||||
|
||||
std::string bare() const
|
||||
{
|
||||
return this->local + "@" + this->domain;
|
||||
}
|
||||
std::string full() const
|
||||
{
|
||||
std::string res = this->domain;
|
||||
if (!this->local.empty())
|
||||
res = this->local + "@" + this->domain;
|
||||
if (!this->resource.empty())
|
||||
res += "/" + this->resource;
|
||||
return res;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Prepare the given UTF-8 string according to the XMPP node stringprep
|
||||
* identifier profile. This is used to send properly-formed JID to the XMPP
|
||||
* server.
|
||||
*
|
||||
* If the stringprep library is not found, we return an empty string. When
|
||||
* this function is used, the result must always be checked for an empty
|
||||
* value, and if this is the case it must not be used as a JID.
|
||||
*/
|
||||
std::string jidprep(const std::string& original);
|
||||
|
||||
|
||||
@@ -0,0 +1,672 @@
|
||||
#include <utils/timed_events.hpp>
|
||||
#include <utils/scopeguard.hpp>
|
||||
#include <utils/tolower.hpp>
|
||||
#include <logger/logger.hpp>
|
||||
|
||||
#include <xmpp/xmpp_component.hpp>
|
||||
#include <config/config.hpp>
|
||||
#include <utils/system.hpp>
|
||||
#include <utils/time.hpp>
|
||||
#include <xmpp/auth.hpp>
|
||||
#include <xmpp/jid.hpp>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <iostream>
|
||||
#include <set>
|
||||
|
||||
#include <uuid/uuid.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <set>
|
||||
|
||||
#include <biboumi.h>
|
||||
#ifdef SYSTEMD_FOUND
|
||||
# include <systemd/sd-daemon.h>
|
||||
#endif
|
||||
|
||||
using namespace std::string_literals;
|
||||
|
||||
static std::set<std::string> kickable_errors{
|
||||
"gone",
|
||||
"internal-server-error",
|
||||
"item-not-found",
|
||||
"jid-malformed",
|
||||
"recipient-unavailable",
|
||||
"redirect",
|
||||
"remote-server-not-found",
|
||||
"remote-server-timeout",
|
||||
"service-unavailable",
|
||||
"malformed-error"
|
||||
};
|
||||
|
||||
XmppComponent::XmppComponent(std::shared_ptr<Poller>& poller, 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 = "<stream:stream to='"s + this->served_hostname + \
|
||||
"' xmlns:stream='http://etherx.jabber.org/streams' xmlns='" COMPONENT_NS "'>";
|
||||
log_debug("XMPP SENDING: ", data);
|
||||
this->send_data(std::move(data));
|
||||
this->doc_open = true;
|
||||
// We may have some pending data to send: this happens when we try to send
|
||||
// some data before we are actually connected. We send that data right now, if any
|
||||
this->send_pending_data();
|
||||
}
|
||||
|
||||
void XmppComponent::on_connection_close(const std::string& error)
|
||||
{
|
||||
if (error.empty())
|
||||
log_info("XMPP server closed connection");
|
||||
else
|
||||
log_info("XMPP server closed connection: ", error);
|
||||
}
|
||||
|
||||
void XmppComponent::parse_in_buffer(const size_t size)
|
||||
{
|
||||
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 = "<handshake xmlns='" COMPONENT_NS "'>"s + get_handshake_digest(this->stream_id, this->secret) + "</handshake>";
|
||||
log_debug("XMPP SENDING: ", data);
|
||||
this->send_data(std::move(data));
|
||||
}
|
||||
|
||||
void XmppComponent::on_remote_stream_close(const XmlNode& node)
|
||||
{
|
||||
log_debug("XMPP RECEIVING: ", node.to_string());
|
||||
this->doc_open = false;
|
||||
}
|
||||
|
||||
void XmppComponent::reset()
|
||||
{
|
||||
this->parser.reset();
|
||||
}
|
||||
|
||||
void XmppComponent::on_stanza(const Stanza& stanza)
|
||||
{
|
||||
log_debug("XMPP RECEIVING: ", stanza.to_string());
|
||||
std::function<void(const Stanza&)> handler;
|
||||
try
|
||||
{
|
||||
handler = this->stanza_handlers.at(stanza.get_name());
|
||||
}
|
||||
catch (const std::out_of_range& exception)
|
||||
{
|
||||
log_warning("No handler for stanza of type ", stanza.get_name());
|
||||
return;
|
||||
}
|
||||
handler(stanza);
|
||||
}
|
||||
|
||||
void XmppComponent::send_stream_error(const std::string& name, const std::string& explanation)
|
||||
{
|
||||
Stanza node("stream:error");
|
||||
{
|
||||
XmlSubNode error(node, name);
|
||||
error["xmlns"] = STREAM_NS;
|
||||
if (!explanation.empty())
|
||||
error.set_inner(explanation);
|
||||
}
|
||||
this->send_stanza(node);
|
||||
}
|
||||
|
||||
void XmppComponent::send_stanza_error(const std::string& kind, const std::string& to, const std::string& from,
|
||||
const std::string& id, const std::string& error_type,
|
||||
const std::string& defined_condition, const std::string& text,
|
||||
const bool fulljid)
|
||||
{
|
||||
Stanza node(kind);
|
||||
{
|
||||
if (!to.empty())
|
||||
node["to"] = to;
|
||||
if (!from.empty())
|
||||
{
|
||||
if (fulljid)
|
||||
node["from"] = from;
|
||||
else
|
||||
node["from"] = from + "@" + this->served_hostname;
|
||||
}
|
||||
if (!id.empty())
|
||||
node["id"] = id;
|
||||
node["type"] = "error";
|
||||
{
|
||||
XmlSubNode error(node, "error");
|
||||
error["type"] = error_type;
|
||||
{
|
||||
XmlSubNode inner_error(error, defined_condition);
|
||||
inner_error["xmlns"] = STANZA_NS;
|
||||
}
|
||||
if (!text.empty())
|
||||
{
|
||||
XmlSubNode text_node(error, "text");
|
||||
text_node["xmlns"] = STANZA_NS;
|
||||
text_node.set_inner(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
this->send_stanza(node);
|
||||
}
|
||||
|
||||
void XmppComponent::close_document()
|
||||
{
|
||||
log_debug("XMPP SENDING: </stream:stream>");
|
||||
this->send_data("</stream:stream>");
|
||||
this->doc_open = false;
|
||||
}
|
||||
|
||||
void XmppComponent::handle_handshake(const Stanza&)
|
||||
{
|
||||
this->authenticated = true;
|
||||
this->ever_auth = true;
|
||||
log_info("Authenticated with the XMPP server");
|
||||
#ifdef SYSTEMD_FOUND
|
||||
sd_notify(0, "READY=1");
|
||||
// Install an event that sends a keepalive to systemd. If biboumi crashes
|
||||
// or hangs for too long, systemd will restart it.
|
||||
uint64_t usec;
|
||||
if (sd_watchdog_enabled(0, &usec) > 0)
|
||||
{
|
||||
TimedEventsManager::instance().add_event(TimedEvent(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::microseconds(usec / 2)),
|
||||
[]() { sd_notify(0, "WATCHDOG=1"); }));
|
||||
}
|
||||
#endif
|
||||
this->after_handshake();
|
||||
}
|
||||
|
||||
void XmppComponent::handle_error(const Stanza& stanza)
|
||||
{
|
||||
const XmlNode* text = stanza.get_child("text", STREAMS_NS);
|
||||
std::string error_message("Unspecified error");
|
||||
if (text)
|
||||
error_message = text->get_inner();
|
||||
log_error("Stream error received from the XMPP server: ", error_message);
|
||||
#ifdef SYSTEMD_FOUND
|
||||
if (!this->ever_auth)
|
||||
sd_notifyf(0, "STATUS=Failed to authenticate to the XMPP server: %s", error_message.data());
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
void* XmppComponent::get_receive_buffer(const size_t size) const
|
||||
{
|
||||
return this->parser.get_buffer(size);
|
||||
}
|
||||
|
||||
void XmppComponent::send_message(const std::string& from, Xmpp::body&& body, const std::string& to,
|
||||
const std::string& type, const bool fulljid, const bool nocopy)
|
||||
{
|
||||
Stanza message("message");
|
||||
{
|
||||
message["to"] = to;
|
||||
if (fulljid)
|
||||
message["from"] = from;
|
||||
else
|
||||
message["from"] = from + "@" + this->served_hostname;
|
||||
if (!type.empty())
|
||||
message["type"] = type;
|
||||
XmlSubNode body_node(message, "body");
|
||||
body_node.set_inner(std::get<0>(body));
|
||||
if (std::get<1>(body))
|
||||
{
|
||||
XmlSubNode html(message, "html");
|
||||
html["xmlns"] = XHTMLIM_NS;
|
||||
// Pass the ownership of the pointer to this xmlnode
|
||||
html.add_child(std::move(std::get<1>(body)));
|
||||
}
|
||||
if (nocopy)
|
||||
{
|
||||
XmlSubNode private_node(message, "private");
|
||||
private_node["xmlns"] = "urn:xmpp:carbons:2";
|
||||
XmlSubNode nocopy(message, "no-copy");
|
||||
nocopy["xmlns"] = "urn:xmpp:hints";
|
||||
}
|
||||
}
|
||||
this->send_stanza(message);
|
||||
}
|
||||
|
||||
void XmppComponent::send_user_join(const std::string& from,
|
||||
const std::string& nick,
|
||||
const std::string& realjid,
|
||||
const std::string& affiliation,
|
||||
const std::string& role,
|
||||
const std::string& to,
|
||||
const bool self)
|
||||
{
|
||||
Stanza presence("presence");
|
||||
{
|
||||
presence["to"] = to;
|
||||
presence["from"] = from + "@" + this->served_hostname + "/" + nick;
|
||||
|
||||
XmlSubNode x(presence, "x");
|
||||
x["xmlns"] = MUC_USER_NS;
|
||||
|
||||
XmlSubNode item(x, "item");
|
||||
if (!affiliation.empty())
|
||||
item["affiliation"] = affiliation;
|
||||
if (!role.empty())
|
||||
item["role"] = role;
|
||||
if (!realjid.empty())
|
||||
{
|
||||
const std::string preped_jid = jidprep(realjid);
|
||||
if (!preped_jid.empty())
|
||||
item["jid"] = preped_jid;
|
||||
}
|
||||
|
||||
if (self)
|
||||
{
|
||||
XmlSubNode status(x, "status");
|
||||
status["code"] = "110";
|
||||
}
|
||||
}
|
||||
this->send_stanza(presence);
|
||||
}
|
||||
|
||||
void XmppComponent::send_invalid_room_error(const std::string& muc_name,
|
||||
const std::string& nick,
|
||||
const std::string& to)
|
||||
{
|
||||
Stanza presence("presence");
|
||||
{
|
||||
if (!muc_name.empty ())
|
||||
presence["from"] = muc_name + "@" + this->served_hostname + "/" + nick;
|
||||
else
|
||||
presence["from"] = this->served_hostname;
|
||||
presence["to"] = to;
|
||||
presence["type"] = "error";
|
||||
XmlSubNode x(presence, "x");
|
||||
x["xmlns"] = MUC_NS;
|
||||
XmlSubNode error(presence, "error");
|
||||
error["by"] = muc_name + "@" + this->served_hostname;
|
||||
error["type"] = "cancel";
|
||||
XmlSubNode item_not_found(error, "item-not-found");
|
||||
item_not_found["xmlns"] = STANZA_NS;
|
||||
XmlSubNode text(error, "text");
|
||||
text["xmlns"] = STANZA_NS;
|
||||
text["xml:lang"] = "en";
|
||||
text.set_inner(muc_name +
|
||||
" is not a valid IRC channel name. A correct room jid is of the form: #<chan>%<server>@" +
|
||||
this->served_hostname);
|
||||
}
|
||||
this->send_stanza(presence);
|
||||
}
|
||||
|
||||
void XmppComponent::send_topic(const std::string& from, Xmpp::body&& topic, const std::string& to, const std::string& who)
|
||||
{
|
||||
Stanza message("message");
|
||||
{
|
||||
message["to"] = to;
|
||||
if (who.empty())
|
||||
message["from"] = from + "@" + this->served_hostname;
|
||||
else
|
||||
message["from"] = from + "@" + this->served_hostname + "/" + who;
|
||||
message["type"] = "groupchat";
|
||||
XmlSubNode subject(message, "subject");
|
||||
subject.set_inner(std::get<0>(topic));
|
||||
}
|
||||
this->send_stanza(message);
|
||||
}
|
||||
|
||||
void XmppComponent::send_muc_message(const std::string& muc_name, const std::string& nick, Xmpp::body&& xmpp_body, const std::string& jid_to)
|
||||
{
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
#pragma once
|
||||
|
||||
|
||||
#include <xmpp/adhoc_commands_handler.hpp>
|
||||
#include <network/tcp_client_socket_handler.hpp>
|
||||
#include <xmpp/xmpp_parser.hpp>
|
||||
#include <xmpp/body.hpp>
|
||||
|
||||
#include <unordered_map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <ctime>
|
||||
#include <map>
|
||||
|
||||
#define STREAM_NS "http://etherx.jabber.org/streams"
|
||||
#define COMPONENT_NS "jabber:component:accept"
|
||||
#define MUC_NS "http://jabber.org/protocol/muc"
|
||||
#define MUC_USER_NS MUC_NS"#user"
|
||||
#define MUC_ADMIN_NS MUC_NS"#admin"
|
||||
#define 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>& 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 <delay/> element, part of a MUC history
|
||||
*/
|
||||
void send_history_message(const std::string& muc_name, const std::string& nick, const std::string& body,
|
||||
const std::string& jid_to, const std::time_t timestamp);
|
||||
/**
|
||||
* Send an unavailable presence for this nick
|
||||
*/
|
||||
void send_muc_leave(const std::string& muc_name, std::string&& nick, Xmpp::body&& message, const std::string& jid_to, const bool self);
|
||||
/**
|
||||
* Indicate that a participant changed his nick
|
||||
*/
|
||||
void send_nick_change(const std::string& muc_name,
|
||||
const std::string& old_nick,
|
||||
const std::string& new_nick,
|
||||
const std::string& affiliation,
|
||||
const std::string& role,
|
||||
const std::string& jid_to,
|
||||
const bool self);
|
||||
/**
|
||||
* An user is kicked from a room
|
||||
*/
|
||||
void kick_user(const std::string& muc_name, const std::string& target, const std::string& reason,
|
||||
const std::string& author, const std::string& jid_to, const bool self);
|
||||
/**
|
||||
* Send a generic presence error
|
||||
*/
|
||||
void send_presence_error(const std::string& muc_name,
|
||||
const std::string& nickname,
|
||||
const std::string& jid_to,
|
||||
const std::string& type,
|
||||
const std::string& condition,
|
||||
const std::string& error_code,
|
||||
const std::string& text);
|
||||
/**
|
||||
* Send a presence from the MUC indicating a change in the role and/or
|
||||
* affiliation of a participant
|
||||
*/
|
||||
void send_affiliation_role_change(const std::string& muc_name,
|
||||
const std::string& target,
|
||||
const std::string& affiliation,
|
||||
const std::string& role,
|
||||
const std::string& jid_to);
|
||||
/**
|
||||
* Send a result IQ with the given version, or the gateway version if the
|
||||
* passed string is empty.
|
||||
*/
|
||||
void send_version(const std::string& id, const std::string& jid_to, const std::string& jid_from,
|
||||
const std::string& version="");
|
||||
/**
|
||||
* Send the list of all available ad-hoc commands to that JID. The list is
|
||||
* different depending on what JID made the request.
|
||||
*/
|
||||
void send_adhoc_commands_list(const std::string& id, const std::string& requester_jid, const std::string& from_jid,
|
||||
const bool with_admin_only, const AdhocCommandsHandler& adhoc_handler);
|
||||
/**
|
||||
* Send an iq version request
|
||||
*/
|
||||
void send_iq_version_request(const std::string& from,
|
||||
const std::string& jid_to);
|
||||
/**
|
||||
* Send an empty iq of type result
|
||||
*/
|
||||
void send_iq_result(const std::string& id, const std::string& to_jid, const std::string& from);
|
||||
void send_iq_result_full_jid(const std::string& id, const std::string& to_jid,
|
||||
const std::string& from_full_jid);
|
||||
|
||||
void handle_handshake(const Stanza& stanza);
|
||||
void handle_error(const Stanza& stanza);
|
||||
|
||||
virtual void after_handshake() {}
|
||||
|
||||
const std::string& get_served_hostname() const
|
||||
{ return this->served_hostname; }
|
||||
|
||||
/**
|
||||
* Whether or not we ever succeeded our authentication to the XMPP server
|
||||
*/
|
||||
bool ever_auth;
|
||||
/**
|
||||
* Whether or not this is the first consecutive try on connecting to the
|
||||
* XMPP server. We use this to delay the connection attempt for a few
|
||||
* seconds, if it is not the first try.
|
||||
*/
|
||||
bool first_connection_try;
|
||||
|
||||
private:
|
||||
/**
|
||||
* Return a buffer provided by the XML parser, to read data directly into
|
||||
* it, and avoiding some unnecessary copy.
|
||||
*/
|
||||
void* get_receive_buffer(const size_t size) const override final;
|
||||
XmppParser parser;
|
||||
std::string stream_id;
|
||||
std::string secret;
|
||||
bool authenticated;
|
||||
/**
|
||||
* Whether or not OUR XMPP document is open
|
||||
*/
|
||||
bool doc_open;
|
||||
protected:
|
||||
std::string served_hostname;
|
||||
|
||||
std::unordered_map<std::string, std::function<void(const Stanza&)>> stanza_handlers;
|
||||
AdhocCommandsHandler adhoc_commands_handler;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
#include <xmpp/xmpp_parser.hpp>
|
||||
#include <xmpp/xmpp_stanza.hpp>
|
||||
|
||||
#include <logger/logger.hpp>
|
||||
|
||||
/**
|
||||
* Expat handlers. Called by the Expat library, never by ourself.
|
||||
* They just forward the call to the XmppParser corresponding methods.
|
||||
*/
|
||||
|
||||
static void start_element_handler(void* user_data, const XML_Char* name, const XML_Char** atts)
|
||||
{
|
||||
static_cast<XmppParser*>(user_data)->start_element(name, atts);
|
||||
}
|
||||
|
||||
static void end_element_handler(void* user_data, const XML_Char* name)
|
||||
{
|
||||
static_cast<XmppParser*>(user_data)->end_element(name);
|
||||
}
|
||||
|
||||
static void character_data_handler(void *user_data, const XML_Char *s, int len)
|
||||
{
|
||||
static_cast<XmppParser*>(user_data)->char_data(s, len);
|
||||
}
|
||||
|
||||
/**
|
||||
* XmppParser class
|
||||
*/
|
||||
|
||||
XmppParser::XmppParser():
|
||||
level(0),
|
||||
current_node(nullptr),
|
||||
root(nullptr)
|
||||
{
|
||||
this->init_xml_parser();
|
||||
}
|
||||
|
||||
void XmppParser::init_xml_parser()
|
||||
{
|
||||
// Create the expat parser
|
||||
this->parser = XML_ParserCreateNS("UTF-8", ':');
|
||||
XML_SetUserData(this->parser, static_cast<void*>(this));
|
||||
|
||||
// Install Expat handlers
|
||||
XML_SetElementHandler(this->parser, &start_element_handler, &end_element_handler);
|
||||
XML_SetCharacterDataHandler(this->parser, &character_data_handler);
|
||||
}
|
||||
|
||||
XmppParser::~XmppParser()
|
||||
{
|
||||
XML_ParserFree(this->parser);
|
||||
}
|
||||
|
||||
int XmppParser::feed(const char* data, const int len, const bool is_final)
|
||||
{
|
||||
int res = XML_Parse(this->parser, data, len, is_final);
|
||||
if (res == XML_STATUS_ERROR &&
|
||||
(XML_GetErrorCode(this->parser) != XML_ERROR_FINISHED))
|
||||
log_error("Xml_Parse encountered an error: ",
|
||||
XML_ErrorString(XML_GetErrorCode(this->parser)));
|
||||
return res;
|
||||
}
|
||||
|
||||
int XmppParser::parse(const int len, const bool is_final)
|
||||
{
|
||||
int res = XML_ParseBuffer(this->parser, len, is_final);
|
||||
if (res == XML_STATUS_ERROR)
|
||||
log_error("Xml_Parsebuffer encountered an error: ",
|
||||
XML_ErrorString(XML_GetErrorCode(this->parser)));
|
||||
return res;
|
||||
}
|
||||
|
||||
void XmppParser::reset()
|
||||
{
|
||||
XML_ParserFree(this->parser);
|
||||
this->init_xml_parser();
|
||||
this->current_node = nullptr;
|
||||
this->root.reset(nullptr);
|
||||
this->level = 0;
|
||||
}
|
||||
|
||||
void* XmppParser::get_buffer(const size_t size) const
|
||||
{
|
||||
return XML_GetBuffer(this->parser, static_cast<int>(size));
|
||||
}
|
||||
|
||||
void XmppParser::start_element(const XML_Char* name, const XML_Char** attribute)
|
||||
{
|
||||
this->level++;
|
||||
|
||||
auto new_node = std::make_unique<XmlNode>(name, this->current_node);
|
||||
auto new_node_ptr = new_node.get();
|
||||
if (this->current_node)
|
||||
this->current_node->add_child(std::move(new_node));
|
||||
else
|
||||
this->root = std::move(new_node);
|
||||
this->current_node = new_node_ptr;
|
||||
for (size_t i = 0; attribute[i]; i += 2)
|
||||
this->current_node->set_attribute(attribute[i], attribute[i+1]);
|
||||
if (this->level == 1)
|
||||
this->stream_open_event(*this->current_node);
|
||||
}
|
||||
|
||||
void XmppParser::end_element(const XML_Char*)
|
||||
{
|
||||
this->level--;
|
||||
if (this->level == 0)
|
||||
{ // End of the whole stream
|
||||
this->stream_close_event(*this->current_node);
|
||||
this->current_node = nullptr;
|
||||
this->root.reset();
|
||||
}
|
||||
else
|
||||
{
|
||||
auto parent = this->current_node->get_parent();
|
||||
if (this->level == 1)
|
||||
{ // End of a stanza
|
||||
this->stanza_event(*this->current_node);
|
||||
// Note: deleting all the children of our parent deletes ourself,
|
||||
// so current_node is an invalid pointer after this line
|
||||
parent->delete_all_children();
|
||||
}
|
||||
this->current_node = parent;
|
||||
}
|
||||
}
|
||||
|
||||
void XmppParser::char_data(const XML_Char* data, const size_t len)
|
||||
{
|
||||
if (this->current_node->has_children())
|
||||
this->current_node->get_last_child()->add_to_tail({data, len});
|
||||
else
|
||||
this->current_node->add_to_inner({data, len});
|
||||
}
|
||||
|
||||
void XmppParser::stanza_event(const Stanza& stanza) const
|
||||
{
|
||||
for (const auto& callback: this->stanza_callbacks)
|
||||
{
|
||||
try {
|
||||
callback(stanza);
|
||||
} catch (const std::exception& e) {
|
||||
log_error("Unhandled exception: ", e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void XmppParser::stream_open_event(const XmlNode& node) const
|
||||
{
|
||||
for (const auto& callback: this->stream_open_callbacks)
|
||||
callback(node);
|
||||
}
|
||||
|
||||
void XmppParser::stream_close_event(const XmlNode& node) const
|
||||
{
|
||||
for (const auto& callback: this->stream_close_callbacks)
|
||||
callback(node);
|
||||
}
|
||||
|
||||
void XmppParser::add_stanza_callback(std::function<void(const Stanza&)>&& callback)
|
||||
{
|
||||
this->stanza_callbacks.emplace_back(std::move(callback));
|
||||
}
|
||||
|
||||
void XmppParser::add_stream_open_callback(std::function<void(const XmlNode&)>&& callback)
|
||||
{
|
||||
this->stream_open_callbacks.emplace_back(std::move(callback));
|
||||
}
|
||||
|
||||
void XmppParser::add_stream_close_callback(std::function<void(const XmlNode&)>&& callback)
|
||||
{
|
||||
this->stream_close_callbacks.emplace_back(std::move(callback));
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
#pragma once
|
||||
|
||||
|
||||
#include <xmpp/xmpp_stanza.hpp>
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <expat.h>
|
||||
|
||||
/**
|
||||
* A SAX XML parser that builds XML nodes and spawns events when a complete
|
||||
* stanza is received (an element of level 2), or when the document is
|
||||
* opened/closed (an element of level 1)
|
||||
*
|
||||
* After a stanza_event has been spawned, we delete the whole stanza. This
|
||||
* means that even with a very long document (in XMPP the document is
|
||||
* potentially infinite), the memory is never exhausted as long as each
|
||||
* stanza is reasonnably short.
|
||||
*
|
||||
* The element names generated by expat contain the namespace of the
|
||||
* element, a colon (':') and then the actual name of the element. To get
|
||||
* an element "x" with a namespace of "http://jabber.org/protocol/muc", you
|
||||
* just look for an XmlNode named "http://jabber.org/protocol/muc:x"
|
||||
*
|
||||
* TODO: enforce the size-limit for the stanza (limit the number of childs
|
||||
* it can contain). For example forbid the parser going further than level
|
||||
* 20 (arbitrary number here), and each XML node to have more than 15 childs
|
||||
* (arbitrary number again).
|
||||
*/
|
||||
class XmppParser
|
||||
{
|
||||
public:
|
||||
explicit XmppParser();
|
||||
~XmppParser();
|
||||
XmppParser(const XmppParser&) = delete;
|
||||
XmppParser& operator=(const XmppParser&) = delete;
|
||||
XmppParser(XmppParser&&) = delete;
|
||||
XmppParser& operator=(XmppParser&&) = delete;
|
||||
|
||||
public:
|
||||
/**
|
||||
* Feed the parser with some XML data
|
||||
*/
|
||||
int feed(const char* data, const int len, const bool is_final);
|
||||
/**
|
||||
* Parse the data placed in the parser buffer
|
||||
*/
|
||||
int parse(const int size, const bool is_final);
|
||||
/**
|
||||
* Reset the parser, so it can be used from scratch afterward
|
||||
*/
|
||||
void reset();
|
||||
/**
|
||||
* Get a buffer provided by the xml parser.
|
||||
*/
|
||||
void* get_buffer(const size_t size) const;
|
||||
/**
|
||||
* Add one callback for the various events that this parser can spawn.
|
||||
*/
|
||||
void add_stanza_callback(std::function<void(const Stanza&)>&& callback);
|
||||
void add_stream_open_callback(std::function<void(const XmlNode&)>&& callback);
|
||||
void add_stream_close_callback(std::function<void(const XmlNode&)>&& callback);
|
||||
|
||||
/**
|
||||
* Called when a new XML element has been opened. We instanciate a new
|
||||
* XmlNode and set it as our current node. The parent of this new node is
|
||||
* the previous "current" node. We have all the element's attributes in
|
||||
* this event.
|
||||
*
|
||||
* We spawn a stream_event with this node if this is a level-1 element.
|
||||
*/
|
||||
void start_element(const XML_Char* name, const XML_Char** attribute);
|
||||
/**
|
||||
* Called when an XML element has been closed. We close the current_node,
|
||||
* set our current_node as the parent of the current_node, and if that was
|
||||
* a level-2 element we spawn a stanza_event with this node.
|
||||
*
|
||||
* And we then delete the stanza (and everything under it, its children,
|
||||
* attribute, etc).
|
||||
*/
|
||||
void end_element(const XML_Char* name);
|
||||
/**
|
||||
* Some inner or tail data has been parsed
|
||||
*/
|
||||
void char_data(const XML_Char* data, const size_t len);
|
||||
/**
|
||||
* Calls all the stanza_callbacks one by one.
|
||||
*/
|
||||
void stanza_event(const Stanza& stanza) const;
|
||||
/**
|
||||
* Calls all the stream_open_callbacks one by one. Note: the passed node is not
|
||||
* closed yet.
|
||||
*/
|
||||
void stream_open_event(const XmlNode& node) const;
|
||||
/**
|
||||
* Calls all the stream_close_callbacks one by one.
|
||||
*/
|
||||
void stream_close_event(const XmlNode& node) const;
|
||||
|
||||
private:
|
||||
/**
|
||||
* Init the XML parser and install the callbacks
|
||||
*/
|
||||
void init_xml_parser();
|
||||
|
||||
/**
|
||||
* Expat structure.
|
||||
*/
|
||||
XML_Parser parser;
|
||||
/**
|
||||
* The current depth in the XML document
|
||||
*/
|
||||
size_t level;
|
||||
/**
|
||||
* The deepest XML node opened but not yet closed (to which we are adding
|
||||
* new children, inner or tail)
|
||||
*/
|
||||
XmlNode* current_node;
|
||||
/**
|
||||
* The root node has no parent, so we keep it here: the XmppParser object
|
||||
* is its owner.
|
||||
*/
|
||||
std::unique_ptr<XmlNode> root;
|
||||
/**
|
||||
* A list of callbacks to be called on an *_event, receiving the
|
||||
* concerned Stanza/XmlNode.
|
||||
*/
|
||||
std::vector<std::function<void(const Stanza&)>> stanza_callbacks;
|
||||
std::vector<std::function<void(const XmlNode&)>> stream_open_callbacks;
|
||||
std::vector<std::function<void(const XmlNode&)>> stream_close_callbacks;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
#include <xmpp/xmpp_stanza.hpp>
|
||||
|
||||
#include <utils/encoding.hpp>
|
||||
#include <utils/split.hpp>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
|
||||
#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 += "&";
|
||||
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<const XmlNode*> XmlNode::get_children(const std::string& name, const std::string& xmlns) const
|
||||
{
|
||||
std::vector<const XmlNode*> res;
|
||||
for (const auto& child: this->children)
|
||||
{
|
||||
if (child->name == name && child->get_tag("xmlns") == xmlns)
|
||||
res.push_back(child.get());
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
XmlNode* XmlNode::add_child(std::unique_ptr<XmlNode> child)
|
||||
{
|
||||
child->parent = this;
|
||||
auto ret = child.get();
|
||||
this->children.push_back(std::move(child));
|
||||
return ret;
|
||||
}
|
||||
|
||||
XmlNode* XmlNode::add_child(XmlNode&& child)
|
||||
{
|
||||
auto new_node = std::make_unique<XmlNode>(std::move(child));
|
||||
return this->add_child(std::move(new_node));
|
||||
}
|
||||
|
||||
XmlNode* XmlNode::add_child(const XmlNode& child)
|
||||
{
|
||||
auto new_node = std::make_unique<XmlNode>(child);
|
||||
return this->add_child(std::move(new_node));
|
||||
}
|
||||
|
||||
XmlNode* XmlNode::get_last_child() const
|
||||
{
|
||||
return this->children.back().get();
|
||||
}
|
||||
|
||||
XmlNode* XmlNode::get_parent() const
|
||||
{
|
||||
return this->parent;
|
||||
}
|
||||
|
||||
void XmlNode::set_name(const std::string& name)
|
||||
{
|
||||
this->name = name;
|
||||
}
|
||||
|
||||
void XmlNode::set_name(std::string&& name)
|
||||
{
|
||||
this->name = std::move(name);
|
||||
}
|
||||
|
||||
const std::string XmlNode::get_name() const
|
||||
{
|
||||
return this->name;
|
||||
}
|
||||
|
||||
std::string XmlNode::to_string() const
|
||||
{
|
||||
std::ostringstream res;
|
||||
res << "<" << this->name;
|
||||
for (const auto& it: this->attributes)
|
||||
res << " " << it.first << "='" << sanitize(it.second) + "'";
|
||||
if (!this->has_children() && this->inner.empty())
|
||||
res << "/>";
|
||||
else
|
||||
{
|
||||
res << ">" + sanitize(this->inner);
|
||||
for (const auto& child: this->children)
|
||||
res << child->to_string();
|
||||
res << "</" << this->get_name() << ">";
|
||||
}
|
||||
res << sanitize(this->tail);
|
||||
return res.str();
|
||||
}
|
||||
|
||||
bool XmlNode::has_children() const
|
||||
{
|
||||
return !this->children.empty();
|
||||
}
|
||||
|
||||
const std::string& XmlNode::get_tag(const std::string& name) const
|
||||
{
|
||||
try
|
||||
{
|
||||
const auto& value = this->attributes.at(name);
|
||||
return value;
|
||||
}
|
||||
catch (const std::out_of_range& e)
|
||||
{
|
||||
static const std::string def{};
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
bool XmlNode::del_tag(const std::string& name)
|
||||
{
|
||||
if (this->attributes.erase(name) != 0)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string& XmlNode::operator[](const std::string& name)
|
||||
{
|
||||
return this->attributes[name];
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const XmlNode& node)
|
||||
{
|
||||
return os << node.to_string();
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
#pragma once
|
||||
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
std::string xml_escape(const std::string& data);
|
||||
std::string xml_unescape(const std::string& data);
|
||||
std::string sanitize(const std::string& data, const std::string& encoding = "ISO-8859-1");
|
||||
|
||||
/**
|
||||
* Represent an XML node. It has
|
||||
* - A parent XML node (in the case of the first-level nodes, the parent is
|
||||
nullptr)
|
||||
* - zero, one or more children XML nodes
|
||||
* - A name
|
||||
* - A map of attributes
|
||||
* - inner data (text inside the node)
|
||||
* - tail data (text just after the node)
|
||||
*/
|
||||
class XmlNode
|
||||
{
|
||||
public:
|
||||
explicit XmlNode(const std::string& name, XmlNode* parent);
|
||||
explicit XmlNode(const std::string& name);
|
||||
/**
|
||||
* The copy constructor does not copy the parent attribute. The children
|
||||
* nodes are all copied recursively.
|
||||
*/
|
||||
XmlNode(const XmlNode& node):
|
||||
name(node.name),
|
||||
parent(nullptr),
|
||||
attributes(node.attributes),
|
||||
children{},
|
||||
inner(node.inner),
|
||||
tail(node.tail)
|
||||
{
|
||||
for (const auto& child: node.children)
|
||||
this->add_child(std::make_unique<XmlNode>(*child));
|
||||
}
|
||||
|
||||
XmlNode(XmlNode&& node) = default;
|
||||
XmlNode& operator=(const XmlNode&) = delete;
|
||||
XmlNode& operator=(XmlNode&&) = delete;
|
||||
|
||||
~XmlNode() = default;
|
||||
|
||||
void delete_all_children();
|
||||
void set_attribute(const std::string& name, const std::string& value);
|
||||
/**
|
||||
* Set the content of the tail, that is the text just after this node
|
||||
*/
|
||||
void set_tail(const std::string& data);
|
||||
/**
|
||||
* Append the given data to the content of the tail. This exists because
|
||||
* the expat library may provide the complete text of an element in more
|
||||
* than one call
|
||||
*/
|
||||
void add_to_tail(const std::string& data);
|
||||
/**
|
||||
* Set the content of the inner, that is the text inside this node.
|
||||
*/
|
||||
void set_inner(const std::string& data);
|
||||
/**
|
||||
* Append the given data to the content of the inner. For the reason
|
||||
* described in add_to_tail comment.
|
||||
*/
|
||||
void add_to_inner(const std::string& data);
|
||||
/**
|
||||
* Get the content of inner
|
||||
*/
|
||||
std::string get_inner() const;
|
||||
/**
|
||||
* Get the content of the tail
|
||||
*/
|
||||
std::string get_tail() const;
|
||||
/**
|
||||
* Get a pointer to the first child element with that name and that xml namespace
|
||||
*/
|
||||
const XmlNode* get_child(const std::string& name, const std::string& xmlns) const;
|
||||
/**
|
||||
* Get a vector of all the children that have that name and that xml namespace.
|
||||
*/
|
||||
std::vector<const XmlNode*> get_children(const std::string& name, const std::string& xmlns) const;
|
||||
/**
|
||||
* Add a node child to this node. Assign this node to the child’s parent.
|
||||
* Returns a pointer to the newly added child.
|
||||
*/
|
||||
XmlNode* add_child(std::unique_ptr<XmlNode> child);
|
||||
XmlNode* add_child(XmlNode&& child);
|
||||
XmlNode* add_child(const XmlNode& child);
|
||||
/**
|
||||
* Returns the last of the children. If the node doesn't have any child,
|
||||
* the behaviour is undefined. The user should make sure this is the case
|
||||
* by calling has_children() for example.
|
||||
*/
|
||||
XmlNode* get_last_child() const;
|
||||
XmlNode* get_parent() const;
|
||||
void set_name(const std::string& name);
|
||||
void set_name(std::string&& name);
|
||||
const std::string get_name() const;
|
||||
/**
|
||||
* Serialize the stanza into a string
|
||||
*/
|
||||
std::string to_string() const;
|
||||
/**
|
||||
* Whether or not this node has at least one child (if not, this is a leaf
|
||||
* node)
|
||||
*/
|
||||
bool has_children() const;
|
||||
/**
|
||||
* Gets the value for the given attribute, returns an empty string if the
|
||||
* node as no such attribute.
|
||||
*/
|
||||
const std::string& get_tag(const std::string& name) const;
|
||||
/**
|
||||
* Remove the attribute of the node. Does nothing if that attribute is not
|
||||
* present. Returns true if the tag was removed, false if it was absent.
|
||||
*/
|
||||
bool del_tag(const std::string& name);
|
||||
/**
|
||||
* Use this to set an attribute's value, like node["id"] = "12";
|
||||
*/
|
||||
std::string& operator[](const std::string& name);
|
||||
|
||||
private:
|
||||
std::string name;
|
||||
XmlNode* parent;
|
||||
std::map<std::string, std::string> attributes;
|
||||
std::vector<std::unique_ptr<XmlNode>> children;
|
||||
std::string inner;
|
||||
std::string tail;
|
||||
};
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const XmlNode& node);
|
||||
|
||||
/**
|
||||
* An XMPP stanza is just an XML node of level 2 in the XMPP document (the
|
||||
* level 1 ones are the <stream::stream/>, and the ones above 2 are just the
|
||||
* content of the stanzas)
|
||||
*/
|
||||
using Stanza = XmlNode;
|
||||
|
||||
class XmlSubNode: public XmlNode
|
||||
{
|
||||
public:
|
||||
XmlSubNode(XmlNode& parent_ref, const std::string& name):
|
||||
XmlNode(name),
|
||||
parent_to_add(parent_ref)
|
||||
{}
|
||||
|
||||
~XmlSubNode()
|
||||
{
|
||||
this->parent_to_add.add_child(std::move(*this));
|
||||
}
|
||||
private:
|
||||
XmlNode& parent_to_add;
|
||||
};
|
||||
Reference in New Issue
Block a user