summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorlouiz’ <louiz@louiz.org>2017-03-14 21:45:23 +0100
committerlouiz’ <louiz@louiz.org>2017-03-14 21:45:23 +0100
commit0ab40dc1ab4e689921da54080b135e1d22b1c586 (patch)
tree238ac477aa6b29a8d1e187a8d97ecbcbbbc663ef /src
parent2d3806152e854ace7533fc3ad34d4ac1c44e9687 (diff)
downloadbiboumi-0ab40dc1ab4e689921da54080b135e1d22b1c586.tar.gz
biboumi-0ab40dc1ab4e689921da54080b135e1d22b1c586.tar.bz2
biboumi-0ab40dc1ab4e689921da54080b135e1d22b1c586.tar.xz
biboumi-0ab40dc1ab4e689921da54080b135e1d22b1c586.zip
Refactoring louloulibs and cmake
Use OBJECT libraries Remove the louloulibs directory Write FOUND variables in the cache
Diffstat (limited to 'src')
-rw-r--r--src/config/config.cpp103
-rw-r--r--src/config/config.hpp93
-rw-r--r--src/irc/irc_client.cpp1
-rw-r--r--src/logger/logger.cpp42
-rw-r--r--src/logger/logger.hpp128
-rw-r--r--src/network/credentials_manager.cpp140
-rw-r--r--src/network/credentials_manager.hpp55
-rw-r--r--src/network/dns_handler.cpp46
-rw-r--r--src/network/dns_handler.hpp37
-rw-r--r--src/network/dns_socket_handler.cpp43
-rw-r--r--src/network/dns_socket_handler.hpp33
-rw-r--r--src/network/poller.cpp234
-rw-r--r--src/network/poller.hpp98
-rw-r--r--src/network/resolver.cpp281
-rw-r--r--src/network/resolver.hpp122
-rw-r--r--src/network/socket_handler.hpp42
-rw-r--r--src/network/tcp_client_socket_handler.cpp261
-rw-r--r--src/network/tcp_client_socket_handler.hpp82
-rw-r--r--src/network/tcp_server_socket.hpp70
-rw-r--r--src/network/tcp_socket_handler.cpp358
-rw-r--r--src/network/tcp_socket_handler.hpp251
-rw-r--r--src/utils/encoding.cpp254
-rw-r--r--src/utils/encoding.hpp43
-rw-r--r--src/utils/get_first_non_empty.cpp11
-rw-r--r--src/utils/get_first_non_empty.hpp20
-rw-r--r--src/utils/revstr.cpp9
-rw-r--r--src/utils/revstr.hpp11
-rw-r--r--src/utils/scopeguard.hpp98
-rw-r--r--src/utils/sha1.cpp32
-rw-r--r--src/utils/sha1.hpp5
-rw-r--r--src/utils/split.cpp19
-rw-r--r--src/utils/split.hpp12
-rw-r--r--src/utils/string.cpp28
-rw-r--r--src/utils/string.hpp10
-rw-r--r--src/utils/system.cpp21
-rw-r--r--src/utils/system.hpp8
-rw-r--r--src/utils/time.cpp70
-rw-r--r--src/utils/time.hpp10
-rw-r--r--src/utils/timed_events.cpp47
-rw-r--r--src/utils/timed_events.hpp132
-rw-r--r--src/utils/timed_events_manager.cpp73
-rw-r--r--src/utils/tolower.cpp13
-rw-r--r--src/utils/tolower.hpp11
-rw-r--r--src/utils/xdg.cpp29
-rw-r--r--src/utils/xdg.hpp14
-rw-r--r--src/xmpp/adhoc_command.cpp80
-rw-r--r--src/xmpp/adhoc_command.hpp44
-rw-r--r--src/xmpp/adhoc_commands_handler.cpp111
-rw-r--r--src/xmpp/adhoc_commands_handler.hpp71
-rw-r--r--src/xmpp/adhoc_session.cpp35
-rw-r--r--src/xmpp/adhoc_session.hpp88
-rw-r--r--src/xmpp/auth.cpp8
-rw-r--r--src/xmpp/auth.hpp6
-rw-r--r--src/xmpp/biboumi_component.cpp1
-rw-r--r--src/xmpp/body.hpp12
-rw-r--r--src/xmpp/jid.cpp153
-rw-r--r--src/xmpp/jid.hpp49
-rw-r--r--src/xmpp/xmpp_component.cpp672
-rw-r--r--src/xmpp/xmpp_component.hpp245
-rw-r--r--src/xmpp/xmpp_parser.cpp172
-rw-r--r--src/xmpp/xmpp_parser.hpp133
-rw-r--r--src/xmpp/xmpp_stanza.cpp229
-rw-r--r--src/xmpp/xmpp_stanza.hpp160
63 files changed, 5767 insertions, 2 deletions
diff --git a/src/config/config.cpp b/src/config/config.cpp
new file mode 100644
index 0000000..24a1c87
--- /dev/null
+++ b/src/config/config.cpp
@@ -0,0 +1,103 @@
+#include <config/config.hpp>
+
+#include <iostream>
+#include <cstring>
+
+#include <cstdlib>
+
+std::string Config::filename{};
+std::map<std::string, std::string> Config::values{};
+std::vector<t_config_changed_callback> Config::callbacks{};
+
+std::string Config::get(const std::string& option, const std::string& def)
+{
+ auto it = Config::values.find(option);
+
+ if (it == Config::values.end())
+ return def;
+ return it->second;
+}
+
+int Config::get_int(const std::string& option, const int& def)
+{
+ std::string res = Config::get(option, "");
+ if (!res.empty())
+ return std::atoi(res.c_str());
+ else
+ return def;
+}
+
+void Config::set(const std::string& option, const std::string& value, bool save)
+{
+ Config::values[option] = value;
+ if (save)
+ {
+ Config::save_to_file();
+ Config::trigger_configuration_change();
+ }
+}
+
+void Config::connect(t_config_changed_callback callback)
+{
+ Config::callbacks.push_back(callback);
+}
+
+void Config::clear()
+{
+ Config::values.clear();
+}
+
+/**
+ * Private methods
+ */
+void Config::trigger_configuration_change()
+{
+ std::vector<t_config_changed_callback>::iterator it;
+ for (it = Config::callbacks.begin(); it < Config::callbacks.end(); ++it)
+ (*it)();
+}
+
+bool Config::read_conf(const std::string& name)
+{
+ if (!name.empty())
+ Config::filename = name;
+
+ std::ifstream file(Config::filename.data());
+ if (!file.is_open())
+ {
+ std::cerr << "Error while opening file " << filename << " for reading: " << strerror(errno) << std::endl;
+ return false;
+ }
+
+ Config::clear();
+
+ std::string line;
+ size_t pos;
+ std::string option;
+ std::string value;
+ while (file.good())
+ {
+ std::getline(file, line);
+ if (line == "" || line[0] == '#')
+ continue ;
+ pos = line.find('=');
+ if (pos == std::string::npos)
+ continue ;
+ option = line.substr(0, pos);
+ value = line.substr(pos+1);
+ Config::values[option] = value;
+ }
+ return true;
+}
+
+void Config::save_to_file()
+{
+ std::ofstream file(Config::filename.data());
+ if (file.fail())
+ {
+ std::cerr << "Could not save config file." << std::endl;
+ return ;
+ }
+ for (const auto& it: Config::values)
+ file << it.first << "=" << it.second << '\n';
+}
diff --git a/src/config/config.hpp b/src/config/config.hpp
new file mode 100644
index 0000000..4e01281
--- /dev/null
+++ b/src/config/config.hpp
@@ -0,0 +1,93 @@
+/**
+ * Read the config file and save all the values in a map.
+ * Also, a singleton.
+ *
+ * Use Config::filename = "bla" to set the filename you want to use.
+ *
+ * If you want to exit if the file does not exist when it is open for
+ * reading, set Config::file_must_exist = true.
+ *
+ * Config::get() can then be used to access the values in the conf.
+ *
+ * Use Config::close() when you're done getting/setting value. This will
+ * save the config into the file.
+ */
+
+#pragma once
+
+#include <functional>
+#include <fstream>
+#include <memory>
+#include <vector>
+#include <string>
+#include <map>
+
+typedef std::function<void()> t_config_changed_callback;
+
+class Config
+{
+public:
+ Config() = default;
+ ~Config() = default;
+ Config(const Config&) = delete;
+ Config& operator=(const Config&) = delete;
+ Config(Config&&) = delete;
+ Config& operator=(Config&&) = delete;
+
+ /**
+ * returns a value from the config. If it doesn’t exist, use
+ * the second argument as the default.
+ */
+ static std::string get(const std::string&, const std::string&);
+ /**
+ * returns a value from the config. If it doesn’t exist, use
+ * the second argument as the default.
+ */
+ static int get_int(const std::string&, const int&);
+ /**
+ * Set a value for the given option. And write all the config
+ * in the file from which it was read if save is true.
+ */
+ static void set(const std::string&, const std::string&, bool save = false);
+ /**
+ * Adds a function to a list. This function will be called whenever a
+ * configuration change occurs (when set() is called, or when the initial
+ * conf is read)
+ */
+ static void connect(t_config_changed_callback);
+ /**
+ * Destroy the instance, forcing it to be recreated (with potentially
+ * different parameters) the next time it’s needed.
+ */
+ static void clear();
+ /**
+ * Read the configuration file at the given path.
+ */
+ static bool read_conf(const std::string& name="");
+ /**
+ * Get the filename
+ */
+ static const std::string& get_filename()
+ { return Config::filename; }
+
+private:
+ /**
+ * Set the value of the filename to use, before calling any method.
+ */
+ static std::string filename;
+ /**
+ * Write all the config values into the configuration file
+ */
+ static void save_to_file();
+ /**
+ * Call all the callbacks previously registered using connect().
+ * This is used to notify any class that a configuration change occured.
+ */
+ static void trigger_configuration_change();
+
+ static std::map<std::string, std::string> values;
+ static std::vector<t_config_changed_callback> callbacks;
+
+};
+
+
diff --git a/src/irc/irc_client.cpp b/src/irc/irc_client.cpp
index d0970c1..93e463b 100644
--- a/src/irc/irc_client.cpp
+++ b/src/irc/irc_client.cpp
@@ -21,7 +21,6 @@
#include <string>
#include "biboumi.h"
-#include "louloulibs.h"
using namespace std::string_literals;
using namespace std::chrono_literals;
diff --git a/src/logger/logger.cpp b/src/logger/logger.cpp
new file mode 100644
index 0000000..92a3d9b
--- /dev/null
+++ b/src/logger/logger.cpp
@@ -0,0 +1,42 @@
+#include <logger/logger.hpp>
+#include <config/config.hpp>
+
+Logger::Logger(const int log_level):
+ log_level(log_level),
+ stream(std::cout.rdbuf()),
+ null_buffer{},
+ null_stream{&null_buffer}
+{
+}
+
+Logger::Logger(const int log_level, const std::string& log_file):
+ log_level(log_level),
+ ofstream(log_file.data(), std::ios_base::app),
+ stream(ofstream.rdbuf()),
+ null_buffer{},
+ null_stream{&null_buffer}
+{
+}
+
+std::unique_ptr<Logger>& Logger::instance()
+{
+ static std::unique_ptr<Logger> instance;
+
+ if (!instance)
+ {
+ const std::string log_file = Config::get("log_file", "");
+ const int log_level = Config::get_int("log_level", 0);
+ if (log_file.empty())
+ instance = std::make_unique<Logger>(log_level);
+ else
+ instance = std::make_unique<Logger>(log_level, log_file);
+ }
+ return instance;
+}
+
+std::ostream& Logger::get_stream(const int lvl)
+{
+ if (lvl >= this->log_level)
+ return this->stream;
+ return this->null_stream;
+}
diff --git a/src/logger/logger.hpp b/src/logger/logger.hpp
new file mode 100644
index 0000000..ff6a82b
--- /dev/null
+++ b/src/logger/logger.hpp
@@ -0,0 +1,128 @@
+#pragma once
+
+
+/**
+ * Singleton used in logger macros to write into files or stdout, with
+ * various levels of severity.
+ * Only the macros should be used.
+ * @class Logger
+ */
+
+#include <memory>
+#include <iostream>
+#include <fstream>
+
+#define debug_lvl 0
+#define info_lvl 1
+#define warning_lvl 2
+#define error_lvl 3
+
+#include "biboumi.h"
+#ifdef SYSTEMD_FOUND
+# include <systemd/sd-daemon.h>
+#else
+# define SD_DEBUG "[DEBUG]: "
+# define SD_INFO "[INFO]: "
+# define SD_WARNING "[WARNING]: "
+# define SD_ERR "[ERROR]: "
+#endif
+
+// Macro defined to get the filename instead of the full path. But if it is
+// not properly defined by the build system, we fallback to __FILE__
+#ifndef __FILENAME__
+# define __FILENAME__ __FILE__
+#endif
+
+
+/**
+ * A buffer, used to construct an ostream that does nothing
+ * when we output data in it
+ */
+class NullBuffer: public std::streambuf
+{
+ public:
+ int overflow(int c) { return c; }
+};
+
+class Logger
+{
+public:
+ static std::unique_ptr<Logger>& instance();
+ std::ostream& get_stream(const int);
+ Logger(const int log_level, const std::string& log_file);
+ Logger(const int log_level);
+
+ Logger(const Logger&) = delete;
+ Logger& operator=(const Logger&) = delete;
+ Logger(Logger&&) = delete;
+ Logger& operator=(Logger&&) = delete;
+
+private:
+ const int log_level;
+ std::ofstream ofstream{};
+ std::ostream stream;
+
+ NullBuffer null_buffer;
+ std::ostream null_stream;
+};
+
+#define WHERE __FILENAME__, ":", __LINE__, ":\t"
+
+namespace logging_details
+{
+ template <typename T>
+ void log(std::ostream& os, const T& arg)
+ {
+ os << arg << std::endl;
+ }
+
+ template <typename T, typename... U>
+ void log(std::ostream& os, const T& first, U&&... rest)
+ {
+ os << first;
+ log(os, std::forward<U>(rest)...);
+ }
+
+ template <typename... U>
+ void log_debug(U&&... args)
+ {
+ auto& os = Logger::instance()->get_stream(debug_lvl);
+ os << SD_DEBUG;
+ log(os, std::forward<U>(args)...);
+ }
+
+ template <typename... U>
+ void log_info(U&&... args)
+ {
+ auto& os = Logger::instance()->get_stream(info_lvl);
+ os << SD_INFO;
+ log(os, std::forward<U>(args)...);
+ }
+
+ template <typename... U>
+ void log_warning(U&&... args)
+ {
+ auto& os = Logger::instance()->get_stream(warning_lvl);
+ os << SD_WARNING;
+ log(os, std::forward<U>(args)...);
+ }
+
+ template <typename... U>
+ void log_error(U&&... args)
+ {
+ auto& os = Logger::instance()->get_stream(error_lvl);
+ os << SD_ERR;
+ log(os, std::forward<U>(args)...);
+ }
+}
+
+#define log_info(...) logging_details::log_info(WHERE, __VA_ARGS__)
+
+#define log_warning(...) logging_details::log_warning(WHERE, __VA_ARGS__)
+
+#define log_error(...) logging_details::log_error(WHERE, __VA_ARGS__)
+
+#define log_debug(...) logging_details::log_debug(WHERE, __VA_ARGS__)
+
+
+
diff --git a/src/network/credentials_manager.cpp b/src/network/credentials_manager.cpp
new file mode 100644
index 0000000..f9f8c94
--- /dev/null
+++ b/src/network/credentials_manager.cpp
@@ -0,0 +1,140 @@
+#include "biboumi.h"
+
+#ifdef BOTAN_FOUND
+#include <network/tcp_socket_handler.hpp>
+#include <network/credentials_manager.hpp>
+#include <logger/logger.hpp>
+#include <botan/tls_exceptn.h>
+#include <config/config.hpp>
+
+#ifdef USE_DATABASE
+# include <database/database.hpp>
+#endif
+
+/**
+ * TODO find a standard way to find that out.
+ */
+static const std::vector<std::string> default_cert_files = {
+ "/etc/ssl/certs/ca-bundle.crt",
+ "/etc/pki/tls/certs/ca-bundle.crt",
+ "/etc/ssl/certs/ca-certificates.crt",
+ "/etc/ca-certificates/extracted/tls-ca-bundle.pem"
+};
+
+Botan::Certificate_Store_In_Memory BasicCredentialsManager::certificate_store;
+bool BasicCredentialsManager::certs_loaded = false;
+
+BasicCredentialsManager::BasicCredentialsManager(const TCPSocketHandler* const socket_handler):
+ Botan::Credentials_Manager(),
+ socket_handler(socket_handler),
+ trusted_fingerprint{}
+{
+ BasicCredentialsManager::load_certs();
+}
+
+void BasicCredentialsManager::set_trusted_fingerprint(const std::string& fingerprint)
+{
+ this->trusted_fingerprint = fingerprint;
+}
+
+const std::string& BasicCredentialsManager::get_trusted_fingerprint() const
+{
+ return this->trusted_fingerprint;
+}
+
+void check_tls_certificate(const std::vector<Botan::X509_Certificate>& certs,
+ const std::string& hostname, const std::string& trusted_fingerprint,
+ std::exception_ptr exc)
+{
+
+ if (!trusted_fingerprint.empty() && !certs.empty() &&
+ trusted_fingerprint == certs[0].fingerprint() &&
+ certs[0].matches_dns_name(hostname))
+ // We trust the certificate, based on the trusted fingerprint and
+ // the fact that the hostname matches
+ return;
+
+ if (exc)
+ std::rethrow_exception(exc);
+}
+
+#if BOTAN_VERSION_CODE < BOTAN_VERSION_CODE_FOR(1,11,34)
+void BasicCredentialsManager::verify_certificate_chain(const std::string& type,
+ const std::string& purported_hostname,
+ const std::vector<Botan::X509_Certificate>& certs)
+{
+ log_debug("Checking remote certificate (", type, ") for hostname ", purported_hostname);
+ try
+ {
+ Botan::Credentials_Manager::verify_certificate_chain(type, purported_hostname, certs);
+ log_debug("Certificate is valid");
+ }
+ catch (const std::exception& tls_exception)
+ {
+ log_warning("TLS certificate check failed: ", tls_exception.what());
+ std::exception_ptr exception_ptr{};
+ if (this->socket_handler->abort_on_invalid_cert())
+ exception_ptr = std::current_exception();
+
+ check_tls_certificate(certs, purported_hostname, this->trusted_fingerprint, exception_ptr);
+ }
+}
+#endif
+
+bool BasicCredentialsManager::try_to_open_one_ca_bundle(const std::vector<std::string>& paths)
+{
+ for (const auto& path: paths)
+ {
+ try
+ {
+ Botan::DataSource_Stream bundle(path);
+ log_debug("Using ca bundle: ", path);
+ while (!bundle.end_of_data() && bundle.check_available(27))
+ {
+ // TODO: remove this work-around for Botan 1.11.29
+ // https://github.com/randombit/botan/issues/438#issuecomment-192866796
+ // Note that every certificate that fails to be transcoded into latin-1
+ // will be ignored. As a result, some TLS connection may be refused
+ // because the certificate is signed by an issuer that was ignored.
+ try {
+ Botan::X509_Certificate cert(bundle);
+ BasicCredentialsManager::certificate_store.add_certificate(std::move(cert));
+ } catch (const Botan::Decoding_Error& error) {
+ continue;
+ }
+ }
+ // Only use the first file that can successfully be read.
+ return true;
+ }
+ catch (const Botan::Stream_IO_Error& e)
+ {
+ log_debug(e.what());
+ }
+ }
+ return false;
+}
+
+void BasicCredentialsManager::load_certs()
+{
+ // Only load the certificates the first time
+ if (BasicCredentialsManager::certs_loaded)
+ return;
+ const std::string conf_path = Config::get("ca_file", "");
+ std::vector<std::string> paths;
+ if (conf_path.empty())
+ paths = default_cert_files;
+ else
+ paths.push_back(conf_path);
+
+ if (BasicCredentialsManager::try_to_open_one_ca_bundle(paths))
+ BasicCredentialsManager::certs_loaded = true;
+ else
+ log_warning("The CA could not be loaded, TLS negociation will probably fail.");
+}
+
+std::vector<Botan::Certificate_Store*> BasicCredentialsManager::trusted_certificate_authorities(const std::string&, const std::string&)
+{
+ return {&this->certificate_store};
+}
+
+#endif
diff --git a/src/network/credentials_manager.hpp b/src/network/credentials_manager.hpp
new file mode 100644
index 0000000..c463ad4
--- /dev/null
+++ b/src/network/credentials_manager.hpp
@@ -0,0 +1,55 @@
+#pragma once
+
+#include "biboumi.h"
+
+#ifdef BOTAN_FOUND
+
+#include <botan/botan.h>
+#include <botan/tls_client.h>
+
+class TCPSocketHandler;
+
+/**
+ * If the given cert isn’t valid, based on the given hostname
+ * and fingerprint, then throws the exception if it’s non-empty.
+ *
+ * Must be called after the standard (from Botan) way of
+ * checking the certificate, if we want to also accept certificates based
+ * on a trusted fingerprint.
+ */
+void check_tls_certificate(const std::vector<Botan::X509_Certificate>& certs,
+ const std::string& hostname, const std::string& trusted_fingerprint,
+ std::exception_ptr exc);
+
+class BasicCredentialsManager: public Botan::Credentials_Manager
+{
+public:
+ BasicCredentialsManager(const TCPSocketHandler* const socket_handler);
+
+ BasicCredentialsManager(BasicCredentialsManager&&) = delete;
+ BasicCredentialsManager(const BasicCredentialsManager&) = delete;
+ BasicCredentialsManager& operator=(const BasicCredentialsManager&) = delete;
+ BasicCredentialsManager& operator=(BasicCredentialsManager&&) = delete;
+
+#if BOTAN_VERSION_CODE < BOTAN_VERSION_CODE_FOR(1,11,34)
+ void verify_certificate_chain(const std::string& type,
+ const std::string& purported_hostname,
+ const std::vector<Botan::X509_Certificate>&) override final;
+#endif
+ std::vector<Botan::Certificate_Store*> trusted_certificate_authorities(const std::string& type,
+ const std::string& context) override final;
+ void set_trusted_fingerprint(const std::string& fingerprint);
+ const std::string& get_trusted_fingerprint() const;
+
+private:
+ const TCPSocketHandler* const socket_handler;
+
+ static bool try_to_open_one_ca_bundle(const std::vector<std::string>& paths);
+ static void load_certs();
+ static Botan::Certificate_Store_In_Memory certificate_store;
+ static bool certs_loaded;
+ std::string trusted_fingerprint;
+};
+
+#endif //BOTAN_FOUND
+
diff --git a/src/network/dns_handler.cpp b/src/network/dns_handler.cpp
new file mode 100644
index 0000000..7f0c96a
--- /dev/null
+++ b/src/network/dns_handler.cpp
@@ -0,0 +1,46 @@
+#include <biboumi.h>
+#ifdef UDNS_FOUND
+
+#include <network/dns_socket_handler.hpp>
+#include <network/dns_handler.hpp>
+#include <network/poller.hpp>
+
+#include <utils/timed_events.hpp>
+
+#include <udns.h>
+#include <cerrno>
+#include <cstring>
+
+class Resolver;
+
+using namespace std::string_literals;
+
+std::unique_ptr<DNSSocketHandler> DNSHandler::socket_handler{};
+
+DNSHandler::DNSHandler(std::shared_ptr<Poller>& poller)
+{
+ dns_init(nullptr, 0);
+ const auto socket = dns_open(nullptr);
+ if (socket == -1)
+ throw std::runtime_error("Failed to initialize udns socket: "s + strerror(errno));
+
+ DNSHandler::socket_handler = std::make_unique<DNSSocketHandler>(poller, socket);
+}
+
+void DNSHandler::destroy()
+{
+ DNSHandler::socket_handler.reset(nullptr);
+ dns_close(nullptr);
+}
+
+void DNSHandler::watch()
+{
+ DNSHandler::socket_handler->watch();
+}
+
+void DNSHandler::unwatch()
+{
+ DNSHandler::socket_handler->unwatch();
+}
+
+#endif /* UDNS_FOUND */
diff --git a/src/network/dns_handler.hpp b/src/network/dns_handler.hpp
new file mode 100644
index 0000000..c694452
--- /dev/null
+++ b/src/network/dns_handler.hpp
@@ -0,0 +1,37 @@
+#pragma once
+
+#include <biboumi.h>
+#ifdef UDNS_FOUND
+
+class Poller;
+
+#include <network/dns_socket_handler.hpp>
+
+#include <string>
+#include <vector>
+#include <memory>
+
+class DNSHandler
+{
+public:
+ explicit DNSHandler(std::shared_ptr<Poller>& poller);
+ ~DNSHandler() = default;
+
+ DNSHandler(const DNSHandler&) = delete;
+ DNSHandler(DNSHandler&&) = delete;
+ DNSHandler& operator=(const DNSHandler&) = delete;
+ DNSHandler& operator=(DNSHandler&&) = delete;
+
+ void destroy();
+
+ static void watch();
+ static void unwatch();
+
+private:
+ /**
+ * Manager for the socket returned by udns, that we need to watch with the poller
+ */
+ static std::unique_ptr<DNSSocketHandler> socket_handler;
+};
+
+#endif /* UDNS_FOUND */
diff --git a/src/network/dns_socket_handler.cpp b/src/network/dns_socket_handler.cpp
new file mode 100644
index 0000000..5c286c4
--- /dev/null
+++ b/src/network/dns_socket_handler.cpp
@@ -0,0 +1,43 @@
+#include <biboumi.h>
+#ifdef UDNS_FOUND
+
+#include <network/dns_socket_handler.hpp>
+#include <network/dns_handler.hpp>
+#include <network/poller.hpp>
+
+#include <udns.h>
+
+DNSSocketHandler::DNSSocketHandler(std::shared_ptr<Poller>& poller,
+ const socket_t socket):
+ SocketHandler(poller, socket)
+{
+ poller->add_socket_handler(this);
+}
+
+DNSSocketHandler::~DNSSocketHandler()
+{
+ this->unwatch();
+}
+
+void DNSSocketHandler::on_recv()
+{
+ dns_ioevent(nullptr, 0);
+}
+
+bool DNSSocketHandler::is_connected() const
+{
+ return true;
+}
+
+void DNSSocketHandler::unwatch()
+{
+ if (this->poller->is_managing_socket(this->socket))
+ this->poller->remove_socket_handler(this->socket);
+}
+
+void DNSSocketHandler::watch()
+{
+ this->poller->add_socket_handler(this);
+}
+
+#endif /* UDNS_FOUND */
diff --git a/src/network/dns_socket_handler.hpp b/src/network/dns_socket_handler.hpp
new file mode 100644
index 0000000..6e83e87
--- /dev/null
+++ b/src/network/dns_socket_handler.hpp
@@ -0,0 +1,33 @@
+#pragma once
+
+#include <biboumi.h>
+#ifdef UDNS_FOUND
+
+#include <network/socket_handler.hpp>
+
+/**
+ * Manage the UDP socket provided by udns, we do not create, open or close the
+ * socket ourself: this is done by udns. We only watch it for readability
+ */
+class DNSSocketHandler: public SocketHandler
+{
+public:
+ explicit DNSSocketHandler(std::shared_ptr<Poller>& poller, const socket_t socket);
+ ~DNSSocketHandler();
+ DNSSocketHandler(const DNSSocketHandler&) = delete;
+ DNSSocketHandler(DNSSocketHandler&&) = delete;
+ DNSSocketHandler& operator=(const DNSSocketHandler&) = delete;
+ DNSSocketHandler& operator=(DNSSocketHandler&&) = delete;
+
+ void on_recv() override final;
+
+ /**
+ * Always true, see the comment for connect()
+ */
+ bool is_connected() const override final;
+
+ void watch();
+ void unwatch();
+};
+
+#endif // UDNS_FOUND
diff --git a/src/network/poller.cpp b/src/network/poller.cpp
new file mode 100644
index 0000000..9f5bcfb
--- /dev/null
+++ b/src/network/poller.cpp
@@ -0,0 +1,234 @@
+#include <network/poller.hpp>
+#include <logger/logger.hpp>
+#include <utils/timed_events.hpp>
+
+#include <assert.h>
+#include <errno.h>
+#include <stdio.h>
+#include <signal.h>
+#include <unistd.h>
+
+#include <cstring>
+#include <iostream>
+#include <stdexcept>
+
+Poller::Poller()
+{
+#if POLLER == POLL
+ this->nfds = 0;
+#elif POLLER == EPOLL
+ this->epfd = ::epoll_create1(0);
+ if (this->epfd == -1)
+ {
+ log_error("epoll failed: ", strerror(errno));
+ throw std::runtime_error("Could not create epoll instance");
+ }
+#endif
+}
+
+Poller::~Poller()
+{
+#if POLLER == EPOLL
+ if (this->epfd > 0)
+ ::close(this->epfd);
+#endif
+}
+
+void Poller::add_socket_handler(SocketHandler* socket_handler)
+{
+ // Don't do anything if the socket is already managed
+ const auto it = this->socket_handlers.find(socket_handler->get_socket());
+ if (it != this->socket_handlers.end())
+ return ;
+
+ this->socket_handlers.emplace(socket_handler->get_socket(), socket_handler);
+
+ // We always watch all sockets for receive events
+#if POLLER == POLL
+ this->fds[this->nfds].fd = socket_handler->get_socket();
+ this->fds[this->nfds].events = POLLIN;
+ this->nfds++;
+#endif
+#if POLLER == EPOLL
+ struct epoll_event event = {EPOLLIN, {socket_handler}};
+ const int res = ::epoll_ctl(this->epfd, EPOLL_CTL_ADD, socket_handler->get_socket(), &event);
+ if (res == -1)
+ {
+ log_error("epoll_ctl failed: ", strerror(errno));
+ throw std::runtime_error("Could not add socket to epoll");
+ }
+#endif
+}
+
+void Poller::remove_socket_handler(const socket_t socket)
+{
+ const auto it = this->socket_handlers.find(socket);
+ if (it == this->socket_handlers.end())
+ throw std::runtime_error("Trying to remove a SocketHandler that is not managed");
+ this->socket_handlers.erase(it);
+
+#if POLLER == POLL
+ for (size_t i = 0; i < this->nfds; i++)
+ {
+ if (this->fds[i].fd == socket)
+ {
+ // Move all subsequent pollfd by one on the left, erasing the
+ // value of the one we remove
+ for (size_t j = i; j < this->nfds - 1; ++j)
+ {
+ this->fds[j].fd = this->fds[j+1].fd;
+ this->fds[j].events= this->fds[j+1].events;
+ }
+ this->nfds--;
+ }
+ }
+#elif POLLER == EPOLL
+ const int res = ::epoll_ctl(this->epfd, EPOLL_CTL_DEL, socket, nullptr);
+ if (res == -1)
+ {
+ log_error("epoll_ctl failed: ", strerror(errno));
+ throw std::runtime_error("Could not remove socket from epoll");
+ }
+#endif
+}
+
+void Poller::watch_send_events(SocketHandler* socket_handler)
+{
+#if POLLER == POLL
+ for (size_t i = 0; i < this->nfds; ++i)
+ {
+ if (this->fds[i].fd == socket_handler->get_socket())
+ {
+ this->fds[i].events = POLLIN|POLLOUT;
+ return;
+ }
+ }
+ throw std::runtime_error("Cannot watch a non-registered socket for send events");
+#elif POLLER == EPOLL
+ struct epoll_event event = {EPOLLIN|EPOLLOUT, {socket_handler}};
+ const int res = ::epoll_ctl(this->epfd, EPOLL_CTL_MOD, socket_handler->get_socket(), &event);
+ if (res == -1)
+ {
+ log_error("epoll_ctl failed: ", strerror(errno));
+ throw std::runtime_error("Could not modify socket flags in epoll");
+ }
+#endif
+}
+
+void Poller::stop_watching_send_events(SocketHandler* socket_handler)
+{
+#if POLLER == POLL
+ for (size_t i = 0; i <= this->nfds; ++i)
+ {
+ if (this->fds[i].fd == socket_handler->get_socket())
+ {
+ this->fds[i].events = POLLIN;
+ return;
+ }
+ }
+ throw std::runtime_error("Cannot watch a non-registered socket for send events");
+#elif POLLER == EPOLL
+ struct epoll_event event = {EPOLLIN, {socket_handler}};
+ const int res = ::epoll_ctl(this->epfd, EPOLL_CTL_MOD, socket_handler->get_socket(), &event);
+ if (res == -1)
+ {
+ log_error("epoll_ctl failed: ", strerror(errno));
+ throw std::runtime_error("Could not modify socket flags in epoll");
+ }
+#endif
+}
+
+int Poller::poll(const std::chrono::milliseconds& timeout)
+{
+ if (this->socket_handlers.empty() && timeout == utils::no_timeout)
+ return -1;
+#if POLLER == POLL
+ // Convert our nice timeout into this ugly struct
+ struct timespec timeout_ts;
+ struct timespec* timeout_tsp;
+ if (timeout > 0s)
+ {
+ auto seconds = std::chrono::duration_cast<std::chrono::seconds>(timeout);
+ timeout_ts.tv_sec = seconds.count();
+ timeout_ts.tv_nsec = std::chrono::duration_cast<std::chrono::nanoseconds>(timeout - seconds).count();
+ timeout_tsp = &timeout_ts;
+ }
+ else
+ timeout_tsp = nullptr;
+
+ // Unblock all signals, only during the ppoll call
+ sigset_t empty_signal_set;
+ sigemptyset(&empty_signal_set);
+ int nb_events = ::ppoll(this->fds, this->nfds, timeout_tsp,
+ &empty_signal_set);
+ if (nb_events < 0)
+ {
+ if (errno == EINTR)
+ return true;
+ log_error("poll failed: ", strerror(errno));
+ throw std::runtime_error("Poll failed");
+ }
+ // We cannot possibly have more ready events than the number of fds we are
+ // watching
+ assert(static_cast<unsigned int>(nb_events) <= this->nfds);
+ for (size_t i = 0; i < this->nfds && nb_events != 0; ++i)
+ {
+ auto socket_handler = this->socket_handlers.at(this->fds[i].fd);
+ if (this->fds[i].revents == 0)
+ continue;
+ else if (this->fds[i].revents & POLLIN && socket_handler->is_connected())
+ {
+ socket_handler->on_recv();
+ nb_events--;
+ }
+ else if (this->fds[i].revents & POLLOUT && socket_handler->is_connected())
+ {
+ socket_handler->on_send();
+ nb_events--;
+ }
+ else if (this->fds[i].revents & POLLOUT ||
+ this->fds[i].revents & POLLIN)
+ {
+ socket_handler->connect();
+ nb_events--;
+ }
+ }
+ return 1;
+#elif POLLER == EPOLL
+ static const size_t max_events = 12;
+ struct epoll_event revents[max_events];
+ // Unblock all signals, only during the epoll_pwait call
+ sigset_t empty_signal_set;
+ sigemptyset(&empty_signal_set);
+ const int nb_events = ::epoll_pwait(this->epfd, revents, max_events, timeout.count(),
+ &empty_signal_set);
+ if (nb_events == -1)
+ {
+ if (errno == EINTR)
+ return 0;
+ log_error("epoll wait: ", strerror(errno));
+ throw std::runtime_error("Epoll_wait failed");
+ }
+ for (int i = 0; i < nb_events; ++i)
+ {
+ auto socket_handler = static_cast<SocketHandler*>(revents[i].data.ptr);
+ if (revents[i].events & EPOLLIN && socket_handler->is_connected())
+ socket_handler->on_recv();
+ else if (revents[i].events & EPOLLOUT && socket_handler->is_connected())
+ socket_handler->on_send();
+ else if (revents[i].events & EPOLLOUT)
+ socket_handler->connect();
+ }
+ return nb_events;
+#endif
+}
+
+size_t Poller::size() const
+{
+ return this->socket_handlers.size();
+}
+
+bool Poller::is_managing_socket(const socket_t socket) const
+{
+ return (this->socket_handlers.find(socket) != this->socket_handlers.end());
+}
diff --git a/src/network/poller.hpp b/src/network/poller.hpp
new file mode 100644
index 0000000..3cc2710
--- /dev/null
+++ b/src/network/poller.hpp
@@ -0,0 +1,98 @@
+#pragma once
+
+
+#include <network/socket_handler.hpp>
+
+#include <unordered_map>
+#include <memory>
+#include <chrono>
+
+#define POLL 1
+#define EPOLL 2
+#define KQUEUE 3
+#include <biboumi.h>
+#ifndef POLLER
+ #define POLLER POLL
+#endif
+
+#if POLLER == POLL
+ #include <poll.h>
+ #define MAX_POLL_FD_NUMBER 4096
+#elif POLLER == EPOLL
+ #include <sys/epoll.h>
+#else
+ #error Invalid POLLER value
+#endif
+
+/**
+ * We pass some SocketHandlers to this Poller, which uses
+ * poll/epoll/kqueue/select etc to wait for events on these SocketHandlers,
+ * and call the callbacks when event occurs.
+ *
+ * TODO: support these pollers:
+ * - kqueue(2)
+ */
+
+class Poller
+{
+public:
+ explicit Poller();
+ ~Poller();
+ Poller(const Poller&) = delete;
+ Poller(Poller&&) = delete;
+ Poller& operator=(const Poller&) = delete;
+ Poller& operator=(Poller&&) = delete;
+ /**
+ * Add a SocketHandler to be monitored by this Poller. All receive events
+ * are always automatically watched.
+ */
+ void add_socket_handler(SocketHandler* socket_handler);
+ /**
+ * Remove (and stop managing) a SocketHandler, designated by the given socket_t.
+ */
+ void remove_socket_handler(const socket_t socket);
+ /**
+ * Signal the poller that he needs to watch for send events for the given
+ * SocketHandler.
+ */
+ void watch_send_events(SocketHandler* socket_handler);
+ /**
+ * Signal the poller that he needs to stop watching for send events for
+ * this SocketHandler.
+ */
+ void stop_watching_send_events(SocketHandler* socket_handler);
+ /**
+ * Wait for all watched events, and call the SocketHandlers' callbacks
+ * when one is ready. Returns if nothing happened before the provided
+ * timeout. If the timeout is 0, it waits forever. If there is no
+ * watched event, returns -1 immediately, ignoring the timeout value.
+ * Otherwise, returns the number of event handled. If 0 is returned this
+ * means that we were interrupted by a signal, or the timeout occured.
+ */
+ int poll(const std::chrono::milliseconds& timeout);
+ /**
+ * Returns the number of SocketHandlers managed by the poller.
+ */
+ size_t size() const;
+ /**
+ * Whether the given socket is managed by the poller
+ */
+ bool is_managing_socket(const socket_t socket) const;
+
+private:
+ /**
+ * A "list" of all the SocketHandlers that we manage, indexed by socket,
+ * because that's what is returned by select/poll/etc when an event
+ * occures.
+ */
+ std::unordered_map<socket_t, SocketHandler*> socket_handlers;
+
+#if POLLER == POLL
+ struct pollfd fds[MAX_POLL_FD_NUMBER];
+ nfds_t nfds;
+#elif POLLER == EPOLL
+ int epfd;
+#endif
+};
+
+
diff --git a/src/network/resolver.cpp b/src/network/resolver.cpp
new file mode 100644
index 0000000..db7fb32
--- /dev/null
+++ b/src/network/resolver.cpp
@@ -0,0 +1,281 @@
+#include <network/dns_handler.hpp>
+#include <utils/timed_events.hpp>
+#include <network/resolver.hpp>
+#include <string.h>
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#ifdef UDNS_FOUND
+# include <udns.h>
+#endif
+
+#include <fstream>
+#include <cstdlib>
+#include <sstream>
+#include <chrono>
+#include <map>
+
+using namespace std::string_literals;
+
+#ifdef UDNS_FOUND
+static std::map<int, std::string> dns_error_messages {
+ {DNS_E_TEMPFAIL, "Timeout while contacting DNS servers"},
+ {DNS_E_PROTOCOL, "Misformatted DNS reply"},
+ {DNS_E_NXDOMAIN, "Domain name not found"},
+ {DNS_E_NOMEM, "Out of memory"},
+ {DNS_E_BADQUERY, "Misformatted domain name"}
+};
+#endif
+
+Resolver::Resolver():
+#ifdef UDNS_FOUND
+ resolved4(false),
+ resolved6(false),
+ resolving(false),
+ port{},
+#endif
+ resolved(false),
+ error_msg{}
+{
+}
+
+void Resolver::resolve(const std::string& hostname, const std::string& port,
+ SuccessCallbackType success_cb, ErrorCallbackType error_cb)
+{
+ this->error_cb = error_cb;
+ this->success_cb = success_cb;
+#ifdef UDNS_FOUND
+ this->port = port;
+#endif
+
+ this->start_resolving(hostname, port);
+}
+
+int Resolver::call_getaddrinfo(const char *name, const char* port, int flags)
+{
+ struct addrinfo hints;
+ memset(&hints, 0, sizeof(struct addrinfo));
+ hints.ai_flags = flags;
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_protocol = 0;
+
+ struct addrinfo* addr_res = nullptr;
+ const int res = ::getaddrinfo(name, port,
+ &hints, &addr_res);
+
+ if (res == 0 && addr_res)
+ {
+ if (!this->addr)
+ this->addr.reset(addr_res);
+ else
+ { // Append this result at the end of the linked list
+ struct addrinfo *rp = this->addr.get();
+ while (rp->ai_next)
+ rp = rp->ai_next;
+ rp->ai_next = addr_res;
+ }
+ }
+
+ return res;
+}
+
+#ifdef UDNS_FOUND
+void Resolver::start_resolving(const std::string& hostname, const std::string& port)
+{
+ this->resolving = true;
+ this->resolved = false;
+ this->resolved4 = false;
+ this->resolved6 = false;
+
+ this->error_msg.clear();
+ this->addr.reset(nullptr);
+
+ // We first try to use it as an IP address directly. We tell getaddrinfo
+ // to NOT use any DNS resolution.
+ if (this->call_getaddrinfo(hostname.data(), port.data(), AI_NUMERICHOST) == 0)
+ {
+ this->on_resolved();
+ return;
+ }
+
+ // Then we look into /etc/hosts to translate the given hostname
+ const auto hosts = this->look_in_etc_hosts(hostname);
+ if (!hosts.empty())
+ {
+ for (const auto &host: hosts)
+ this->call_getaddrinfo(host.data(), port.data(), AI_NUMERICHOST);
+ this->on_resolved();
+ return;
+ }
+
+ // And finally, we try a DNS resolution
+ auto hostname6_resolved = [](dns_ctx*, dns_rr_a6* result, void* data)
+ {
+ Resolver* resolver = static_cast<Resolver*>(data);
+ resolver->on_hostname6_resolved(result);
+ resolver->after_resolved();
+ std::free(result);
+ };
+
+ auto hostname4_resolved = [](dns_ctx*, dns_rr_a4* result, void* data)
+ {
+ Resolver* resolver = static_cast<Resolver*>(data);
+ resolver->on_hostname4_resolved(result);
+ resolver->after_resolved();
+ std::free(result);
+ };
+
+ DNSHandler::watch();
+ auto res = dns_submit_a4(nullptr, hostname.data(), 0, hostname4_resolved, this);
+ if (!res)
+ this->on_hostname4_resolved(nullptr);
+ res = dns_submit_a6(nullptr, hostname.data(), 0, hostname6_resolved, this);
+ if (!res)
+ this->on_hostname6_resolved(nullptr);
+
+ this->start_timer();
+}
+
+void Resolver::start_timer()
+{
+ const auto timeout = dns_timeouts(nullptr, -1, 0);
+ if (timeout < 0)
+ return;
+ TimedEvent event(std::chrono::steady_clock::now() + std::chrono::seconds(timeout), [this]() { this->start_timer(); }, "DNS");
+ TimedEventsManager::instance().add_event(std::move(event));
+}
+
+std::vector<std::string> Resolver::look_in_etc_hosts(const std::string &hostname)
+{
+ std::ifstream hosts("/etc/hosts");
+ std::string line;
+
+ std::vector<std::string> results;
+ while (std::getline(hosts, line))
+ {
+ if (line.empty())
+ continue;
+
+ std::string ip;
+ std::istringstream line_stream(line);
+ line_stream >> ip;
+ if (ip.empty() || ip[0] == '#')
+ continue;
+
+ std::string host;
+ while (line_stream >> host && !host.empty() && host[0] != '#')
+ {
+ if (hostname == host)
+ {
+ results.push_back(ip);
+ break;
+ }
+ }
+ }
+ return results;
+}
+
+void Resolver::on_hostname4_resolved(dns_rr_a4 *result)
+{
+ this->resolved4 = true;
+
+ const auto status = dns_status(nullptr);
+
+ if (status >= 0 && result)
+ {
+ char buf[INET6_ADDRSTRLEN];
+
+ for (auto i = 0; i < result->dnsa4_nrr; ++i)
+ {
+ inet_ntop(AF_INET, &result->dnsa4_addr[i], buf, sizeof(buf));
+ this->call_getaddrinfo(buf, this->port.data(), AI_NUMERICHOST);
+ }
+ }
+ else
+ {
+ const auto error = dns_error_messages.find(status);
+ if (error != end(dns_error_messages))
+ this->error_msg = error->second;
+ }
+}
+
+void Resolver::on_hostname6_resolved(dns_rr_a6 *result)
+{
+ this->resolved6 = true;
+
+ const auto status = dns_status(nullptr);
+
+ if (status >= 0 && result)
+ {
+ char buf[INET6_ADDRSTRLEN];
+ for (auto i = 0; i < result->dnsa6_nrr; ++i)
+ {
+ inet_ntop(AF_INET6, &result->dnsa6_addr[i], buf, sizeof(buf));
+ this->call_getaddrinfo(buf, this->port.data(), AI_NUMERICHOST);
+ }
+ }
+}
+
+void Resolver::after_resolved()
+{
+ if (dns_active(nullptr) == 0)
+ DNSHandler::unwatch();
+
+ if (this->resolved6 && this->resolved4)
+ this->on_resolved();
+}
+
+void Resolver::on_resolved()
+{
+ this->resolved = true;
+ this->resolving = false;
+ if (!this->addr)
+ {
+ if (this->error_cb)
+ this->error_cb(this->error_msg.data());
+ }
+ else
+ {
+ if (this->success_cb)
+ this->success_cb(this->addr.get());
+ }
+}
+
+#else // ifdef UDNS_FOUND
+
+void Resolver::start_resolving(const std::string& hostname, const std::string& port)
+{
+ // If the resolution fails, the addr will be unset
+ this->addr.reset(nullptr);
+
+ const auto res = this->call_getaddrinfo(hostname.data(), port.data(), 0);
+
+ this->resolved = true;
+
+ if (res != 0)
+ {
+ this->error_msg = gai_strerror(res);
+ if (this->error_cb)
+ this->error_cb(this->error_msg.data());
+ }
+ else
+ {
+ if (this->success_cb)
+ this->success_cb(this->addr.get());
+ }
+}
+#endif // ifdef UDNS_FOUND
+
+std::string addr_to_string(const struct addrinfo* rp)
+{
+ char buf[INET6_ADDRSTRLEN];
+ if (rp->ai_family == AF_INET)
+ return ::inet_ntop(rp->ai_family,
+ &reinterpret_cast<sockaddr_in*>(rp->ai_addr)->sin_addr,
+ buf, sizeof(buf));
+ else if (rp->ai_family == AF_INET6)
+ return ::inet_ntop(rp->ai_family,
+ &reinterpret_cast<sockaddr_in6*>(rp->ai_addr)->sin6_addr,
+ buf, sizeof(buf));
+ return {};
+}
diff --git a/src/network/resolver.hpp b/src/network/resolver.hpp
new file mode 100644
index 0000000..f65ff86
--- /dev/null
+++ b/src/network/resolver.hpp
@@ -0,0 +1,122 @@
+#pragma once
+
+#include "biboumi.h"
+
+#include <functional>
+#include <vector>
+#include <memory>
+#include <string>
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netdb.h>
+#ifdef UDNS_FOUND
+# include <udns.h>
+#endif
+
+class AddrinfoDeleter
+{
+ public:
+ void operator()(struct addrinfo* addr)
+ {
+ freeaddrinfo(addr);
+ }
+};
+
+
+class Resolver
+{
+public:
+
+ using ErrorCallbackType = std::function<void(const char*)>;
+ using SuccessCallbackType = std::function<void(const struct addrinfo*)>;
+
+ Resolver();
+ ~Resolver() = default;
+ Resolver(const Resolver&) = delete;
+ Resolver(Resolver&&) = delete;
+ Resolver& operator=(const Resolver&) = delete;
+ Resolver& operator=(Resolver&&) = delete;
+
+ bool is_resolving() const
+ {
+#ifdef UDNS_FOUND
+ return this->resolving;
+#else
+ return false;
+#endif
+ }
+
+ bool is_resolved() const
+ {
+ return this->resolved;
+ }
+
+ const auto& get_result() const
+ {
+ return this->addr;
+ }
+ std::string get_error_message() const
+ {
+ return this->error_msg;
+ }
+
+ void clear()
+ {
+#ifdef UDNS_FOUND
+ this->resolved6 = false;
+ this->resolved4 = false;
+ this->resolving = false;
+ this->port.clear();
+#endif
+ this->resolved = false;
+ this->addr.reset();
+ this->error_msg.clear();
+ }
+
+ void resolve(const std::string& hostname, const std::string& port,
+ SuccessCallbackType success_cb, ErrorCallbackType error_cb);
+
+private:
+ void start_resolving(const std::string& hostname, const std::string& port);
+ std::vector<std::string> look_in_etc_hosts(const std::string& hostname);
+ /**
+ * Call getaddrinfo() on the given hostname or IP, and append the result
+ * to our internal addrinfo list. Return getaddrinfo()’s return value.
+ */
+ int call_getaddrinfo(const char* name, const char* port, int flags);
+
+#ifdef UDNS_FOUND
+ void on_hostname4_resolved(dns_rr_a4 *result);
+ void on_hostname6_resolved(dns_rr_a6 *result);
+ /**
+ * Called after one record (4 or 6) has been resolved.
+ */
+ void after_resolved();
+
+ void start_timer();
+
+ void on_resolved();
+
+ bool resolved4;
+ bool resolved6;
+
+ bool resolving;
+
+ std::string port;
+
+#endif
+ /**
+ * Tells if we finished the resolution process. It doesn't indicate if it
+ * was successful (it is true even if the result is an error).
+ */
+ bool resolved;
+ std::string error_msg;
+
+ std::unique_ptr<struct addrinfo, AddrinfoDeleter> addr;
+
+ ErrorCallbackType error_cb;
+ SuccessCallbackType success_cb;
+};
+
+std::string addr_to_string(const struct addrinfo* rp);
diff --git a/src/network/socket_handler.hpp b/src/network/socket_handler.hpp
new file mode 100644
index 0000000..181a6c0
--- /dev/null
+++ b/src/network/socket_handler.hpp
@@ -0,0 +1,42 @@
+#pragma once
+
+#include <biboumi.h>
+#include <memory>
+
+class Poller;
+
+using socket_t = int;
+
+class SocketHandler
+{
+public:
+ explicit SocketHandler(std::shared_ptr<Poller>& poller, const socket_t socket):
+ poller(poller),
+ socket(socket)
+ {}
+ virtual ~SocketHandler() = default;
+ SocketHandler(const SocketHandler&) = delete;
+ SocketHandler(SocketHandler&&) = delete;
+ SocketHandler& operator=(const SocketHandler&) = delete;
+ SocketHandler& operator=(SocketHandler&&) = delete;
+
+ virtual void on_recv() {}
+ virtual void on_send() {}
+ virtual void connect() {}
+ virtual bool is_connected() const = 0;
+
+ socket_t get_socket() const
+ { return this->socket; }
+
+protected:
+ /**
+ * A pointer to the poller that manages us, because we need to communicate
+ * with it.
+ */
+ std::shared_ptr<Poller> poller;
+ /**
+ * The handled socket.
+ */
+ socket_t socket;
+};
+
diff --git a/src/network/tcp_client_socket_handler.cpp b/src/network/tcp_client_socket_handler.cpp
new file mode 100644
index 0000000..4628703
--- /dev/null
+++ b/src/network/tcp_client_socket_handler.cpp
@@ -0,0 +1,261 @@
+#include <network/tcp_client_socket_handler.hpp>
+#include <utils/timed_events.hpp>
+#include <utils/scopeguard.hpp>
+#include <network/poller.hpp>
+
+#include <logger/logger.hpp>
+
+#include <cstring>
+#include <unistd.h>
+#include <fcntl.h>
+
+using namespace std::string_literals;
+
+TCPClientSocketHandler::TCPClientSocketHandler(std::shared_ptr<Poller>& poller):
+ TCPSocketHandler(poller),
+ hostname_resolution_failed(false),
+ connected(false),
+ connecting(false)
+{}
+
+TCPClientSocketHandler::~TCPClientSocketHandler()
+{
+ this->close();
+}
+
+void TCPClientSocketHandler::init_socket(const struct addrinfo* rp)
+{
+ if (this->socket != -1)
+ ::close(this->socket);
+ if ((this->socket = ::socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol)) == -1)
+ throw std::runtime_error("Could not create socket: "s + std::strerror(errno));
+ // Bind the socket to a specific address, if specified
+ if (!this->bind_addr.empty())
+ {
+ // Convert the address from string format to a sockaddr that can be
+ // used in bind()
+ struct addrinfo* result;
+ struct addrinfo hints;
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_flags = AI_NUMERICHOST;
+ hints.ai_family = AF_UNSPEC;
+ int err = ::getaddrinfo(this->bind_addr.data(), nullptr, &hints, &result);
+ if (err != 0 || !result)
+ log_error("Failed to bind socket to ", this->bind_addr, ": ",
+ gai_strerror(err));
+ else
+ {
+ utils::ScopeGuard sg([result](){ freeaddrinfo(result); });
+ struct addrinfo* rp;
+ for (rp = result; rp; rp = rp->ai_next)
+ {
+ if ((::bind(this->socket,
+ reinterpret_cast<const struct sockaddr*>(rp->ai_addr),
+ rp->ai_addrlen)) == 0)
+ break;
+ }
+ if (!rp)
+ log_error("Failed to bind socket to ", this->bind_addr, ": ",
+ strerror(errno));
+ else
+ log_info("Socket successfully bound to ", this->bind_addr);
+ }
+ }
+ int optval = 1;
+ if (::setsockopt(this->socket, SOL_SOCKET, SO_KEEPALIVE, &optval, sizeof(optval)) == -1)
+ log_warning("Failed to enable TCP keepalive on socket: ", strerror(errno));
+ // Set the socket on non-blocking mode. This is useful to receive a EAGAIN
+ // error when connect() would block, to not block the whole process if a
+ // remote is not responsive.
+ const int existing_flags = ::fcntl(this->socket, F_GETFL, 0);
+ if ((existing_flags == -1) ||
+ (::fcntl(this->socket, F_SETFL, existing_flags | O_NONBLOCK) == -1))
+ throw std::runtime_error("Could not initialize socket: "s + std::strerror(errno));
+}
+
+void TCPClientSocketHandler::connect(const std::string& address, const std::string& port, const bool tls)
+{
+ this->address = address;
+ this->port = port;
+ this->use_tls = tls;
+
+ struct addrinfo* addr_res;
+
+ if (!this->connecting)
+ {
+ // Get the addrinfo from getaddrinfo (or using udns), only if
+ // this is the first call of this function.
+ if (!this->resolver.is_resolved())
+ {
+ log_info("Trying to connect to ", address, ":", port);
+ // Start the asynchronous process of resolving the hostname. Once
+ // the addresses have been found and `resolved` has been set to true
+ // (but connecting will still be false), TCPClientSocketHandler::connect()
+ // needs to be called, again.
+ this->resolver.resolve(address, port,
+ [this](const struct addrinfo*)
+ {
+ log_debug("Resolution success, calling connect() again");
+ this->connect();
+ },
+ [this](const char*)
+ {
+ log_debug("Resolution failed, calling connect() again");
+ this->connect();
+ });
+ return;
+ }
+ else
+ {
+ // The DNS resolver resolved the hostname and the available addresses
+ // where saved in the addrinfo linked list. Now, just use
+ // this list to try to connect.
+ addr_res = this->resolver.get_result().get();
+ if (!addr_res)
+ {
+ this->hostname_resolution_failed = true;
+ const auto msg = this->resolver.get_error_message();
+ this->close();
+ this->on_connection_failed(msg);
+ return ;
+ }
+ }
+ }
+ else
+ { // This function is called again, use the saved addrinfo structure,
+ // instead of re-doing the whole getaddrinfo process.
+ addr_res = &this->addrinfo;
+ }
+
+ for (struct addrinfo* rp = addr_res; rp; rp = rp->ai_next)
+ {
+ if (!this->connecting)
+ {
+ try {
+ this->init_socket(rp);
+ }
+ catch (const std::runtime_error& error) {
+ log_error("Failed to init socket: ", error.what());
+ break;
+ }
+ }
+
+ this->display_resolved_ip(rp);
+
+ if (::connect(this->socket, rp->ai_addr, rp->ai_addrlen) == 0
+ || errno == EISCONN)
+ {
+ log_info("Connection success.");
+ TimedEventsManager::instance().cancel("connection_timeout"s +
+ std::to_string(this->socket));
+ this->poller->add_socket_handler(this);
+ this->connected = true;
+ this->connecting = false;
+#ifdef BOTAN_FOUND
+ if (this->use_tls)
+ this->start_tls(this->address, this->port);
+#endif
+ this->connection_date = std::chrono::system_clock::now();
+
+ // Get our local TCP port and store it
+ this->local_port = static_cast<uint16_t>(-1);
+ if (rp->ai_family == AF_INET6)
+ {
+ struct sockaddr_in6 a;
+ socklen_t l = sizeof(a);
+ if (::getsockname(this->socket, (struct sockaddr*)&a, &l) != -1)
+ this->local_port = ntohs(a.sin6_port);
+ }
+ else if (rp->ai_family == AF_INET)
+ {
+ struct sockaddr_in a;
+ socklen_t l = sizeof(a);
+ if (::getsockname(this->socket, (struct sockaddr*)&a, &l) != -1)
+ this->local_port = ntohs(a.sin_port);
+ }
+
+ log_debug("Local port: ", this->local_port, ", and remote port: ", this->port);
+
+ this->on_connected();
+ return ;
+ }
+ else if (errno == EINPROGRESS || errno == EALREADY)
+ { // retry this process later, when the socket
+ // is ready to be written on.
+ this->connecting = true;
+ this->poller->add_socket_handler(this);
+ this->poller->watch_send_events(this);
+ // Save the addrinfo structure, to use it on the next call
+ this->ai_addrlen = rp->ai_addrlen;
+ memcpy(&this->ai_addr, rp->ai_addr, this->ai_addrlen);
+ memcpy(&this->addrinfo, rp, sizeof(struct addrinfo));
+ this->addrinfo.ai_addr = reinterpret_cast<struct sockaddr*>(&this->ai_addr);
+ this->addrinfo.ai_next = nullptr;
+ // If the connection has not succeeded or failed in 5s, we consider
+ // it to have failed
+ TimedEventsManager::instance().add_event(
+ TimedEvent(std::chrono::steady_clock::now() + 5s,
+ std::bind(&TCPClientSocketHandler::on_connection_timeout, this),
+ "connection_timeout"s + std::to_string(this->socket)));
+ return ;
+ }
+ log_info("Connection failed:", std::strerror(errno));
+ }
+ log_error("All connection attempts failed.");
+ this->close();
+ this->on_connection_failed(std::strerror(errno));
+ return ;
+}
+
+void TCPClientSocketHandler::on_connection_timeout()
+{
+ this->close();
+ this->on_connection_failed("connection timed out");
+}
+
+void TCPClientSocketHandler::connect()
+{
+ this->connect(this->address, this->port, this->use_tls);
+}
+
+void TCPClientSocketHandler::close()
+{
+ TimedEventsManager::instance().cancel("connection_timeout"s +
+ std::to_string(this->socket));
+
+ TCPSocketHandler::close();
+
+ this->connected = false;
+ this->connecting = false;
+ this->port.clear();
+ this->resolver.clear();
+}
+
+void TCPClientSocketHandler::display_resolved_ip(struct addrinfo* rp) const
+{
+ if (rp->ai_family == AF_INET)
+ log_debug("Trying IPv4 address ", addr_to_string(rp));
+ else if (rp->ai_family == AF_INET6)
+ log_debug("Trying IPv6 address ", addr_to_string(rp));
+}
+
+bool TCPClientSocketHandler::is_connected() const
+{
+ return this->connected;
+}
+
+bool TCPClientSocketHandler::is_connecting() const
+{
+ return this->connecting || this->resolver.is_resolving();
+}
+
+std::string TCPClientSocketHandler::get_port() const
+{
+ return this->port;
+}
+
+bool TCPClientSocketHandler::match_port_pairt(const uint16_t local, const uint16_t remote) const
+{
+ const uint16_t remote_port = static_cast<uint16_t>(std::stoi(this->port));
+ return this->is_connected() && local == this->local_port && remote == remote_port;
+}
diff --git a/src/network/tcp_client_socket_handler.hpp b/src/network/tcp_client_socket_handler.hpp
new file mode 100644
index 0000000..74caca9
--- /dev/null
+++ b/src/network/tcp_client_socket_handler.hpp
@@ -0,0 +1,82 @@
+#pragma once
+
+#include <network/tcp_socket_handler.hpp>
+
+class TCPClientSocketHandler: public TCPSocketHandler
+{
+ public:
+ TCPClientSocketHandler(std::shared_ptr<Poller>& poller);
+ ~TCPClientSocketHandler();
+ /**
+ * Connect to the remote server, and call on_connected() if this
+ * succeeds. If tls is true, we set use_tls to true and will also call
+ * start_tls() when the connection succeeds.
+ */
+ void connect(const std::string& address, const std::string& port, const bool tls);
+ void connect() override final;
+ /**
+ * Called by a TimedEvent, when the connection did not succeed or fail
+ * after a given time.
+ */
+ void on_connection_timeout();
+ /**
+ * Called when the connection is successful.
+ */
+ virtual void on_connected() = 0;
+ bool is_connected() const override;
+ bool is_connecting() const override;
+
+ std::string get_port() const;
+
+ void close() override final;
+ std::chrono::system_clock::time_point connection_date;
+
+ /**
+ * Whether or not this connection is using the two given TCP ports.
+ */
+ bool match_port_pairt(const uint16_t local, const uint16_t remote) const;
+
+ protected:
+ bool hostname_resolution_failed;
+ /**
+ * Address to bind the socket to, before calling connect().
+ * If empty, it’s equivalent to binding to INADDR_ANY.
+ */
+ std::string bind_addr;
+ /**
+ * Display the resolved IP, just for information purpose.
+ */
+ void display_resolved_ip(struct addrinfo* rp) const;
+ private:
+ /**
+ * Initialize the socket with the parameters contained in the given
+ * addrinfo structure.
+ */
+ void init_socket(const struct addrinfo* rp);
+ /**
+ * DNS resolver
+ */
+ Resolver resolver;
+ /**
+ * Keep the details of the addrinfo returned by the resolver that
+ * triggered a EINPROGRESS error when connect()ing to it, to reuse it
+ * directly when connect() is called again.
+ */
+ struct addrinfo addrinfo{};
+ struct sockaddr_in6 ai_addr{};
+ socklen_t ai_addrlen{};
+
+ /**
+ * Hostname we are connected/connecting to
+ */
+ std::string address;
+ /**
+ * Port we are connected/connecting to
+ */
+ std::string port;
+
+ uint16_t local_port{};
+
+ bool connected;
+ bool connecting;
+};
diff --git a/src/network/tcp_server_socket.hpp b/src/network/tcp_server_socket.hpp
new file mode 100644
index 0000000..c511962
--- /dev/null
+++ b/src/network/tcp_server_socket.hpp
@@ -0,0 +1,70 @@
+#pragma once
+
+#include <network/socket_handler.hpp>
+#include <network/poller.hpp>
+#include <logger/logger.hpp>
+
+#include <string>
+
+#include <arpa/inet.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/ip.h>
+
+#include <cstring>
+#include <cassert>
+
+template <typename RemoteSocketType>
+class TcpSocketServer: public SocketHandler
+{
+ public:
+ TcpSocketServer(std::shared_ptr<Poller>& poller, const uint16_t port):
+ SocketHandler(poller, -1)
+ {
+ if ((this->socket = ::socket(AF_INET6, SOCK_STREAM, 0)) == -1)
+ throw std::runtime_error(std::string{"Could not create socket: "} + std::strerror(errno));
+
+ int opt = 1;
+ if (::setsockopt(this->socket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1)
+ throw std::runtime_error(std::string{"Failed to set socket option: "} + std::strerror(errno));
+
+ struct sockaddr_in6 addr{};
+ addr.sin6_family = AF_INET6;
+ addr.sin6_port = htons(port);
+ addr.sin6_addr = IN6ADDR_ANY_INIT;
+ if ((::bind(this->socket, (const struct sockaddr*)&addr, sizeof(addr))) == -1)
+ { // If we can’t listen on this port, we just give up, but this is not fatal.
+ log_warning("Failed to bind on port ", std::to_string(port), ": ", std::strerror(errno));
+ return;
+ }
+
+ if ((::listen(this->socket, 10)) == -1)
+ throw std::runtime_error("listen() failed");
+
+ this->accept();
+ }
+ ~TcpSocketServer() = default;
+
+ void on_recv() override
+ {
+ // Accept a RemoteSocketType
+ int socket = ::accept(this->socket, nullptr, nullptr);
+
+ auto client = std::make_unique<RemoteSocketType>(poller, socket, *this);
+ this->poller->add_socket_handler(client.get());
+ this->sockets.push_back(std::move(client));
+ }
+
+ protected:
+ std::vector<std::unique_ptr<RemoteSocketType>> sockets;
+
+ private:
+ void accept()
+ {
+ this->poller->add_socket_handler(this);
+ }
+ bool is_connected() const override
+ {
+ return true;
+ }
+};
diff --git a/src/network/tcp_socket_handler.cpp b/src/network/tcp_socket_handler.cpp
new file mode 100644
index 0000000..7eebae0
--- /dev/null
+++ b/src/network/tcp_socket_handler.cpp
@@ -0,0 +1,358 @@
+#include <network/tcp_socket_handler.hpp>
+#include <network/dns_handler.hpp>
+
+#include <network/poller.hpp>
+
+#include <logger/logger.hpp>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <stdexcept>
+#include <unistd.h>
+#include <errno.h>
+#include <cstring>
+
+#ifdef BOTAN_FOUND
+# include <botan/hex.h>
+# include <botan/tls_exceptn.h>
+
+namespace
+{
+ Botan::AutoSeeded_RNG& get_rng()
+ {
+ static Botan::AutoSeeded_RNG rng{};
+ return rng;
+ }
+ BiboumiTLSPolicy& get_policy()
+ {
+ static BiboumiTLSPolicy policy{};
+ return policy;
+ }
+ Botan::TLS::Session_Manager_In_Memory& get_session_manager()
+ {
+ static Botan::TLS::Session_Manager_In_Memory session_manager{get_rng()};
+ return session_manager;
+ }
+}
+#endif
+
+#ifndef UIO_FASTIOV
+# define UIO_FASTIOV 8
+#endif
+
+using namespace std::string_literals;
+using namespace std::chrono_literals;
+
+namespace ph = std::placeholders;
+
+TCPSocketHandler::TCPSocketHandler(std::shared_ptr<Poller>& poller):
+ SocketHandler(poller, -1),
+ use_tls(false)
+#ifdef BOTAN_FOUND
+ ,credential_manager(this)
+#endif
+{}
+
+TCPSocketHandler::~TCPSocketHandler()
+{
+ if (this->poller->is_managing_socket(this->get_socket()))
+ this->poller->remove_socket_handler(this->get_socket());
+ if (this->socket != -1)
+ {
+ ::close(this->socket);
+ this->socket = -1;
+ }
+}
+
+void TCPSocketHandler::on_recv()
+{
+#ifdef BOTAN_FOUND
+ if (this->use_tls)
+ this->tls_recv();
+ else
+#endif
+ this->plain_recv();
+}
+
+void TCPSocketHandler::plain_recv()
+{
+ static constexpr size_t buf_size = 4096;
+ char buf[buf_size];
+ void* recv_buf = this->get_receive_buffer(buf_size);
+
+ if (recv_buf == nullptr)
+ recv_buf = buf;
+
+ const ssize_t size = this->do_recv(recv_buf, buf_size);
+
+ if (size > 0)
+ {
+ if (buf == recv_buf)
+ {
+ // data needs to be placed in the in_buf string, because no buffer
+ // was provided to receive that data directly. The in_buf buffer
+ // will be handled in parse_in_buffer()
+ this->in_buf += std::string(buf, size);
+ }
+ this->parse_in_buffer(size);
+ }
+}
+
+ssize_t TCPSocketHandler::do_recv(void* recv_buf, const size_t buf_size)
+{
+ ssize_t size = ::recv(this->socket, recv_buf, buf_size, 0);
+ if (0 == size)
+ {
+ this->on_connection_close("");
+ this->close();
+ }
+ else if (-1 == size)
+ {
+ if (this->is_connecting())
+ log_warning("Error connecting: ", strerror(errno));
+ else
+ log_warning("Error while reading from socket: ", strerror(errno));
+ // Remember if we were connecting, or already connected when this
+ // happened, because close() sets this->connecting to false
+ const auto were_connecting = this->is_connecting();
+ this->close();
+ if (were_connecting)
+ this->on_connection_failed(strerror(errno));
+ else
+ this->on_connection_close(strerror(errno));
+ }
+ return size;
+}
+
+void TCPSocketHandler::on_send()
+{
+ struct iovec msg_iov[UIO_FASTIOV] = {};
+ struct msghdr msg{nullptr, 0,
+ msg_iov,
+ 0, nullptr, 0, 0};
+ for (const std::string& s: this->out_buf)
+ {
+ // unconsting the content of s is ok, sendmsg will never modify it
+ msg_iov[msg.msg_iovlen].iov_base = const_cast<char*>(s.data());
+ msg_iov[msg.msg_iovlen].iov_len = s.size();
+ msg.msg_iovlen++;
+ if (msg.msg_iovlen == UIO_FASTIOV)
+ break;
+ }
+ ssize_t res = ::sendmsg(this->socket, &msg, MSG_NOSIGNAL);
+ if (res < 0)
+ {
+ log_error("sendmsg failed: ", strerror(errno));
+ this->on_connection_close(strerror(errno));
+ this->close();
+ }
+ else
+ {
+ // remove all the strings that were successfully sent.
+ auto it = this->out_buf.begin();
+ while (it != this->out_buf.end())
+ {
+ if (static_cast<size_t>(res) >= it->size())
+ {
+ res -= it->size();
+ ++it;
+ }
+ else
+ {
+ // If one string has partially been sent, we use substr to
+ // crop it
+ if (res > 0)
+ *it = it->substr(res, std::string::npos);
+ break;
+ }
+ }
+ this->out_buf.erase(this->out_buf.begin(), it);
+ if (this->out_buf.empty())
+ this->poller->stop_watching_send_events(this);
+ }
+}
+
+void TCPSocketHandler::close()
+{
+ if (this->is_connected() || this->is_connecting())
+ this->poller->remove_socket_handler(this->get_socket());
+ if (this->socket != -1)
+ {
+ ::close(this->socket);
+ this->socket = -1;
+ }
+ this->in_buf.clear();
+ this->out_buf.clear();
+}
+
+void TCPSocketHandler::send_data(std::string&& data)
+{
+#ifdef BOTAN_FOUND
+ if (this->use_tls)
+ try {
+ this->tls_send(std::move(data));
+ } catch (const Botan::TLS::TLS_Exception& e) {
+ this->on_connection_close("TLS error: "s + e.what());
+ this->close();
+ return ;
+ }
+ else
+#endif
+ this->raw_send(std::move(data));
+}
+
+void TCPSocketHandler::raw_send(std::string&& data)
+{
+ if (data.empty())
+ return ;
+ this->out_buf.emplace_back(std::move(data));
+ if (this->is_connected())
+ this->poller->watch_send_events(this);
+}
+
+void TCPSocketHandler::send_pending_data()
+{
+ if (this->is_connected() && !this->out_buf.empty())
+ this->poller->watch_send_events(this);
+}
+
+bool TCPSocketHandler::is_using_tls() const
+{
+ return this->use_tls;
+}
+
+void* TCPSocketHandler::get_receive_buffer(const size_t) const
+{
+ return nullptr;
+}
+
+void TCPSocketHandler::consume_in_buffer(const std::size_t size)
+{
+ this->in_buf = this->in_buf.substr(size, std::string::npos);
+}
+
+#ifdef BOTAN_FOUND
+void TCPSocketHandler::start_tls(const std::string& address, const std::string& port)
+{
+ Botan::TLS::Server_Information server_info(address, "irc", std::stoul(port));
+ this->tls = std::make_unique<Botan::TLS::Client>(
+# if BOTAN_VERSION_CODE >= BOTAN_VERSION_CODE_FOR(1,11,32)
+ *this,
+# else
+ [this](const Botan::byte* data, size_t size) { this->tls_emit_data(data, size); },
+ [this](const Botan::byte* data, size_t size) { this->tls_record_received(0, data, size); },
+ [this](Botan::TLS::Alert alert, const Botan::byte*, size_t) { this->tls_alert(alert); },
+ [this](const Botan::TLS::Session& session) { return this->tls_session_established(session); },
+# endif
+ get_session_manager(), this->credential_manager, get_policy(),
+ get_rng(), server_info, Botan::TLS::Protocol_Version::latest_tls_version());
+}
+
+void TCPSocketHandler::tls_recv()
+{
+ static constexpr size_t buf_size = 4096;
+ Botan::byte recv_buf[buf_size];
+
+ const ssize_t size = this->do_recv(recv_buf, buf_size);
+ if (size > 0)
+ {
+ const bool was_active = this->tls->is_active();
+ try {
+ this->tls->received_data(recv_buf, static_cast<size_t>(size));
+ } catch (const Botan::TLS::TLS_Exception& e) {
+ // May happen if the server sends malformed TLS data (buggy server,
+ // or more probably we are just connected to a server that sends
+ // plain-text)
+ this->on_connection_close("TLS error: "s + e.what());
+ this->close();
+ return ;
+ }
+ if (!was_active && this->tls->is_active())
+ this->on_tls_activated();
+ }
+}
+
+void TCPSocketHandler::tls_send(std::string&& data)
+{
+ // We may not be connected yet, or the tls session has
+ // not yet been negociated
+ if (this->tls && this->tls->is_active())
+ {
+ const bool was_active = this->tls->is_active();
+ if (!this->pre_buf.empty())
+ {
+ this->tls->send(this->pre_buf.data(), this->pre_buf.size());
+ this->pre_buf.clear();
+ }
+ if (!data.empty())
+ this->tls->send(reinterpret_cast<const Botan::byte*>(data.data()),
+ data.size());
+ if (!was_active && this->tls->is_active())
+ this->on_tls_activated();
+ }
+ else
+ this->pre_buf.insert(this->pre_buf.end(),
+ std::make_move_iterator(data.begin()),
+ std::make_move_iterator(data.end()));
+}
+
+void TCPSocketHandler::tls_record_received(uint64_t, const Botan::byte *data, size_t size)
+{
+ this->in_buf += std::string(reinterpret_cast<const char*>(data),
+ size);
+ if (!this->in_buf.empty())
+ this->parse_in_buffer(size);
+}
+
+void TCPSocketHandler::tls_emit_data(const Botan::byte *data, size_t size)
+{
+ this->raw_send(std::string(reinterpret_cast<const char*>(data), size));
+}
+
+void TCPSocketHandler::tls_alert(Botan::TLS::Alert alert)
+{
+ log_debug("tls_alert: ", alert.type_string());
+}
+
+bool TCPSocketHandler::tls_session_established(const Botan::TLS::Session& session)
+{
+ log_debug("Handshake with ", session.server_info().hostname(), " complete.",
+ " Version: ", session.version().to_string(),
+ " using ", session.ciphersuite().to_string());
+ if (!session.session_id().empty())
+ log_debug("Session ID ", Botan::hex_encode(session.session_id()));
+ if (!session.session_ticket().empty())
+ log_debug("Session ticket ", Botan::hex_encode(session.session_ticket()));
+ return true;
+}
+
+#if BOTAN_VERSION_CODE >= BOTAN_VERSION_CODE_FOR(1,11,34)
+void TCPSocketHandler::tls_verify_cert_chain(const std::vector<Botan::X509_Certificate>& cert_chain,
+ const std::vector<std::shared_ptr<const Botan::OCSP::Response>>& ocsp_responses,
+ const std::vector<Botan::Certificate_Store*>& trusted_roots,
+ Botan::Usage_Type usage, const std::string& hostname,
+ const Botan::TLS::Policy& policy)
+{
+ log_debug("Checking remote certificate for hostname ", hostname);
+ try
+ {
+ Botan::TLS::Callbacks::tls_verify_cert_chain(cert_chain, ocsp_responses, trusted_roots, usage, hostname, policy);
+ log_debug("Certificate is valid");
+ }
+ catch (const std::exception& tls_exception)
+ {
+ log_warning("TLS certificate check failed: ", tls_exception.what());
+ std::exception_ptr exception_ptr{};
+ if (this->abort_on_invalid_cert())
+ exception_ptr = std::current_exception();
+
+ check_tls_certificate(cert_chain, hostname, this->credential_manager.get_trusted_fingerprint(), exception_ptr);
+ }
+}
+#endif
+
+void TCPSocketHandler::on_tls_activated()
+{
+ this->send_data({});
+}
+
+#endif // BOTAN_FOUND
diff --git a/src/network/tcp_socket_handler.hpp b/src/network/tcp_socket_handler.hpp
new file mode 100644
index 0000000..ba23861
--- /dev/null
+++ b/src/network/tcp_socket_handler.hpp
@@ -0,0 +1,251 @@
+#pragma once
+
+#include "biboumi.h"
+
+#include <network/socket_handler.hpp>
+#include <network/resolver.hpp>
+
+#include <network/credentials_manager.hpp>
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <netdb.h>
+
+#include <chrono>
+#include <vector>
+#include <memory>
+#include <string>
+#include <list>
+
+#ifdef BOTAN_FOUND
+
+# include <botan/types.h>
+# include <botan/botan.h>
+# include <botan/tls_session_manager.h>
+
+class BiboumiTLSPolicy: public Botan::TLS::Policy
+{
+public:
+# if BOTAN_VERSION_CODE >= BOTAN_VERSION_CODE_FOR(1,11,33)
+ bool use_ecc_point_compression() const override
+ {
+ return true;
+ }
+ bool require_cert_revocation_info() const override
+ {
+ return false;
+ }
+# endif
+};
+
+# if BOTAN_VERSION_CODE >= BOTAN_VERSION_CODE_FOR(1,11,32)
+# define BOTAN_TLS_CALLBACKS_OVERRIDE override final
+# else
+# define BOTAN_TLS_CALLBACKS_OVERRIDE
+# endif
+#endif
+
+/**
+ * Does all the read/write, buffering etc. With optional tls.
+ * But doesn’t do any connect() or accept() or anything else.
+ */
+class TCPSocketHandler: public SocketHandler
+#ifdef BOTAN_FOUND
+# if BOTAN_VERSION_CODE >= BOTAN_VERSION_CODE_FOR(1,11,32)
+ ,public Botan::TLS::Callbacks
+# endif
+#endif
+{
+protected:
+ ~TCPSocketHandler();
+public:
+ explicit TCPSocketHandler(std::shared_ptr<Poller>& poller);
+ TCPSocketHandler(const TCPSocketHandler&) = delete;
+ TCPSocketHandler(TCPSocketHandler&&) = delete;
+ TCPSocketHandler& operator=(const TCPSocketHandler&) = delete;
+ TCPSocketHandler& operator=(TCPSocketHandler&&) = delete;
+
+ /**
+ * Reads raw data from the socket. And pass it to parse_in_buffer()
+ * If we are using TLS on this connection, we call tls_recv()
+ */
+ void on_recv() override final;
+ /**
+ * Write as much data from out_buf as possible, in the socket.
+ */
+ void on_send() override final;
+ /**
+ * Add the given data to out_buf and tell our poller that we want to be
+ * notified when a send event is ready.
+ *
+ * This can be overriden if we want to modify the data before sending
+ * it. For example if we want to encrypt it.
+ */
+ void send_data(std::string&& data);
+ /**
+ * Watch the socket for send events, if our out buffer is not empty.
+ */
+ void send_pending_data();
+ /**
+ * Close the connection, remove us from the poller
+ */
+ virtual void close();
+ /**
+ * Handle/consume (some of) the data received so far. The data to handle
+ * may be in the in_buf buffer, or somewhere else, depending on what
+ * get_receive_buffer() returned. If some data is used from in_buf, it
+ * should be truncated, only the unused data should be left untouched.
+ *
+ * The size argument is the size of the last chunk of data that was added to the buffer.
+ *
+ * The function should call consume_in_buffer, with the size that was consumed by the
+ * “parsing”, and thus to be removed from the input buffer.
+ */
+ virtual void parse_in_buffer(const size_t size) = 0;
+#ifdef BOTAN_FOUND
+ /**
+ * Tell whether the credential manager should cancel the connection when the
+ * certificate is invalid.
+ */
+ virtual bool abort_on_invalid_cert() const
+ {
+ return true;
+ }
+#endif
+ bool is_using_tls() const;
+
+private:
+ /**
+ * Reads from the socket into the provided buffer. If an error occurs
+ * (read returns <= 0), the handling of the error is done here (close the
+ * connection, log a message, etc).
+ *
+ * Returns the value returned by ::recv(), so the buffer should not be
+ * used if it’s not positive.
+ */
+ ssize_t do_recv(void* recv_buf, const size_t buf_size);
+ /**
+ * Reads data from the socket and calls parse_in_buffer with it.
+ */
+ void plain_recv();
+ /**
+ * Mark the given data as ready to be sent, as-is, on the socket, as soon
+ * as we can.
+ */
+ void raw_send(std::string&& data);
+
+ protected:
+ virtual bool is_connecting() const = 0;
+#ifdef BOTAN_FOUND
+ /**
+ * Create the TLS::Client object, with all the callbacks etc. This must be
+ * called only when we know we are able to send TLS-encrypted data over
+ * the socket.
+ */
+ void start_tls(const std::string& address, const std::string& port);
+ private:
+ /**
+ * An additional step to pass the data into our tls object to decrypt it
+ * before passing it to parse_in_buffer.
+ */
+ void tls_recv();
+ /**
+ * Pass the data to the tls object in order to encrypt it. The tls object
+ * will then call raw_send as a callback whenever data as been encrypted
+ * and can be sent on the socket.
+ */
+ void tls_send(std::string&& data);
+ /**
+ * Called by the tls object that some data has been decrypt. We call
+ * parse_in_buffer() to handle that unencrypted data.
+ */
+ void tls_record_received(uint64_t rec_no, const Botan::byte* data, size_t size) BOTAN_TLS_CALLBACKS_OVERRIDE;
+ /**
+ * Called by the tls object to indicate that some data has been encrypted
+ * and is now ready to be sent on the socket as is.
+ */
+ void tls_emit_data(const Botan::byte* data, size_t size) BOTAN_TLS_CALLBACKS_OVERRIDE;
+ /**
+ * Called by the tls object to indicate that a TLS alert has been
+ * received. We don’t use it, we just log some message, at the moment.
+ */
+ void tls_alert(Botan::TLS::Alert alert) BOTAN_TLS_CALLBACKS_OVERRIDE;
+ /**
+ * Called by the tls object at the end of the TLS handshake. We don't do
+ * anything here appart from logging the TLS session information.
+ */
+ bool tls_session_established(const Botan::TLS::Session& session) BOTAN_TLS_CALLBACKS_OVERRIDE;
+
+#if BOTAN_VERSION_CODE >= BOTAN_VERSION_CODE_FOR(1,11,34)
+ void tls_verify_cert_chain(const std::vector<Botan::X509_Certificate>& cert_chain,
+ const std::vector<std::shared_ptr<const Botan::OCSP::Response>>& ocsp_responses,
+ const std::vector<Botan::Certificate_Store*>& trusted_roots,
+ Botan::Usage_Type usage,
+ const std::string& hostname,
+ const Botan::TLS::Policy& policy) BOTAN_TLS_CALLBACKS_OVERRIDE;
+#endif
+ /**
+ * Called whenever the tls session goes from inactive to active. This
+ * means that the handshake has just been successfully done, and we can
+ * now proceed to send any available data into our tls object.
+ */
+ void on_tls_activated();
+#endif // BOTAN_FOUND
+ /**
+ * Where data is added, when we want to send something to the client.
+ */
+ std::vector<std::string> out_buf;
+protected:
+ /**
+ * Whether we are using TLS on this connection or not.
+ */
+ bool use_tls;
+ /**
+ * Where data read from the socket is added until we can extract a full
+ * and meaningful “message” from it.
+ *
+ * TODO: something more efficient than a string.
+ */
+ std::string in_buf;
+ /**
+ * Remove the given “size” first bytes from our in_buf.
+ */
+ void consume_in_buffer(const std::size_t size);
+ /**
+ * Provide a buffer in which data can be directly received. This can be
+ * used to avoid copying data into in_buf before using it. If no buffer
+ * needs to be provided, nullptr is returned (the default implementation
+ * does that), in that case our internal in_buf will be used to save the
+ * data until it can be used by parse_in_buffer().
+ */
+ virtual void* get_receive_buffer(const size_t size) const;
+ /**
+ * Called when we detect a disconnection from the remote host.
+ */
+ virtual void on_connection_close(const std::string&) {}
+ virtual void on_connection_failed(const std::string&) {}
+
+#ifdef BOTAN_FOUND
+protected:
+ BasicCredentialsManager credential_manager;
+private:
+ /**
+ * We use a unique_ptr because we may not want to create the object at
+ * all. The Botan::TLS::Client object generates a handshake message and
+ * calls the output_fn callback with it as soon as it is created.
+ * Therefore, we do not want to create it if we do not intend to send any
+ * TLS-encrypted message. We create the object only when needed (for
+ * example after we have negociated a TLS session using a STARTTLS
+ * message, or stuf like that).
+ *
+ * See start_tls for the method where this object is created.
+ */
+ std::unique_ptr<Botan::TLS::Client> tls;
+ /**
+ * An additional buffer to keep data that the user wants to send, but
+ * cannot because the handshake is not done.
+ */
+ std::vector<Botan::byte> pre_buf;
+#endif // BOTAN_FOUND
+};
diff --git a/src/utils/encoding.cpp b/src/utils/encoding.cpp
new file mode 100644
index 0000000..aa91dac
--- /dev/null
+++ b/src/utils/encoding.cpp
@@ -0,0 +1,254 @@
+#include <utils/encoding.hpp>
+
+#include <utils/scopeguard.hpp>
+
+#include <stdexcept>
+
+#include <assert.h>
+#include <string.h>
+#include <iconv.h>
+#include <cerrno>
+
+#include <map>
+#include <bitset>
+
+/**
+ * The UTF-8-encoded character used as a place holder when a character conversion fails.
+ * This is U+FFFD � "replacement character"
+ */
+static const char* invalid_char = "\xef\xbf\xbd";
+static const size_t invalid_char_len = 3;
+
+namespace utils
+{
+ /**
+ * Based on http://en.wikipedia.org/wiki/UTF-8#Description
+ */
+ std::size_t get_next_codepoint_size(const unsigned char c)
+ {
+ if ((c & 0b11111000) == 0b11110000) // 4 bytes: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
+ return 4;
+ else if ((c & 0b11110000) == 0b11100000) // 3 bytes: 1110xxx 10xxxxxx 10xxxxxx
+ return 3;
+ else if ((c & 0b11100000) == 0b11000000) // 2 bytes: 110xxxxx 10xxxxxx
+ return 2;
+ return 1; // 1 byte: 0xxxxxxx
+ }
+
+ bool is_valid_utf8(const char* s)
+ {
+ if (!s)
+ return false;
+
+ const unsigned char* str = reinterpret_cast<const unsigned char*>(s);
+
+ while (*str)
+ {
+ const auto codepoint_size = get_next_codepoint_size(str[0]);
+ if (codepoint_size == 4)
+ {
+ if (!str[1] || !str[2] || !str[3]
+ || ((str[1] & 0b11000000) != 0b10000000)
+ || ((str[2] & 0b11000000) != 0b10000000)
+ || ((str[3] & 0b11000000) != 0b10000000))
+ return false;
+ }
+ else if (codepoint_size == 3)
+ {
+ if (!str[1] || !str[2]
+ || ((str[1] & 0b11000000) != 0b10000000)
+ || ((str[2] & 0b11000000) != 0b10000000))
+ return false;
+ }
+ else if (codepoint_size == 2)
+ {
+ if (!str[1] ||
+ ((str[1] & 0b11000000) != 0b10000000))
+ return false;
+ }
+ else if ((str[0] & 0b10000000) != 0)
+ return false;
+ str += codepoint_size;
+ }
+ return true;
+ }
+
+ std::string remove_invalid_xml_chars(const std::string& original)
+ {
+ // The given string MUST be a valid utf-8 string
+ std::vector<char> res(original.size(), '\0');
+
+ // pointer where we write valid chars
+ char* r = res.data();
+
+ const char* str = original.c_str();
+ std::bitset<20> codepoint;
+
+ while (*str)
+ {
+ // 4 bytes: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
+ if ((str[0] & 0b11111000) == 0b11110000)
+ {
+ codepoint = ((str[0] & 0b00000111) << 18);
+ codepoint |= ((str[1] & 0b00111111) << 12);
+ codepoint |= ((str[2] & 0b00111111) << 6 );
+ codepoint |= ((str[3] & 0b00111111) << 0 );
+ if (codepoint.to_ulong() <= 0x10FFFF)
+ {
+ ::memcpy(r, str, 4);
+ r += 4;
+ }
+ str += 4;
+ }
+ // 3 bytes: 1110xxx 10xxxxxx 10xxxxxx
+ else if ((str[0] & 0b11110000) == 0b11100000)
+ {
+ codepoint = ((str[0] & 0b00001111) << 12);
+ codepoint |= ((str[1] & 0b00111111) << 6);
+ codepoint |= ((str[2] & 0b00111111) << 0 );
+ if (codepoint.to_ulong() <= 0xD7FF ||
+ (codepoint.to_ulong() >= 0xE000 && codepoint.to_ulong() <= 0xFFFD))
+ {
+ ::memcpy(r, str, 3);
+ r += 3;
+ }
+ str += 3;
+ }
+ // 2 bytes: 110xxxxx 10xxxxxx
+ else if (((str[0]) & 0b11100000) == 0b11000000)
+ {
+ // All 2 bytes char are valid, don't even bother calculating
+ // the codepoint
+ ::memcpy(r, str, 2);
+ r += 2;
+ str += 2;
+ }
+ // 1 byte: 0xxxxxxx
+ else if ((str[0] & 0b10000000) == 0)
+ {
+ codepoint = ((str[0] & 0b01111111));
+ if (codepoint.to_ulong() == 0x09 ||
+ codepoint.to_ulong() == 0x0A ||
+ codepoint.to_ulong() == 0x0D ||
+ codepoint.to_ulong() >= 0x20)
+ {
+ ::memcpy(r, str, 1);
+ r += 1;
+ }
+ str += 1;
+ }
+ else
+ throw std::runtime_error("Invalid UTF-8 passed to remove_invalid_xml_chars");
+ }
+ return {res.data(), static_cast<size_t>(r - res.data())};
+ }
+
+ std::string convert_to_utf8(const std::string& str, const char* charset)
+ {
+ std::string res;
+
+ const iconv_t cd = iconv_open("UTF-8", charset);
+ if (cd == (iconv_t)-1)
+ throw std::runtime_error("Cannot convert into UTF-8");
+
+ // Make sure cd is always closed when we leave this function
+ const auto sg = utils::make_scope_guard([&cd](){ iconv_close(cd); });
+
+ size_t inbytesleft = str.size();
+
+ // iconv will not attempt to modify this buffer, but some plateform
+ // require a char** anyway
+#ifdef ICONV_SECOND_ARGUMENT_IS_CONST
+ const char* inbuf_ptr = str.c_str();
+#else
+ char* inbuf_ptr = const_cast<char*>(str.c_str());
+#endif
+
+ size_t outbytesleft = str.size() * 4;
+ char* outbuf = new char[outbytesleft];
+ char* outbuf_ptr = outbuf;
+
+ // Make sure outbuf is always deleted when we leave this function
+ const auto sg2 = utils::make_scope_guard([outbuf](){ delete[] outbuf; });
+
+ bool done = false;
+ while (done == false)
+ {
+ size_t error = iconv(cd, &inbuf_ptr, &inbytesleft, &outbuf_ptr, &outbytesleft);
+ if ((size_t)-1 == error)
+ {
+ switch (errno)
+ {
+ case EILSEQ:
+ // Invalid byte found. Insert a placeholder instead of the
+ // converted character, jump one byte and continue
+ memcpy(outbuf_ptr, invalid_char, invalid_char_len);
+ outbuf_ptr += invalid_char_len;
+ inbytesleft--;
+ inbuf_ptr++;
+ break;
+ case EINVAL:
+ // A multibyte sequence is not terminated, but we can't
+ // provide any more data, so we just add a placeholder to
+ // indicate that the character is not properly converted,
+ // and we stop the conversion
+ memcpy(outbuf_ptr, invalid_char, invalid_char_len);
+ outbuf_ptr += invalid_char_len;
+ outbuf_ptr++;
+ done = true;
+ break;
+ case E2BIG: // This should never happen
+ default: // This should happen even neverer
+ done = true;
+ break;
+ }
+ }
+ else
+ {
+ // The conversion finished without any error, stop converting
+ done = true;
+ }
+ }
+ // Terminate the converted buffer, and copy that buffer it into the
+ // string we return
+ *outbuf_ptr = '\0';
+ res = outbuf;
+ return res;
+ }
+
+}
+
+namespace xep0106
+{
+ static const std::map<const char, const std::string> encode_map = {
+ {' ', "\\20"},
+ {'"', "\\22"},
+ {'&', "\\26"},
+ {'\'',"\\27"},
+ {'/', "\\2f"},
+ {':', "\\3a"},
+ {'<', "\\3c"},
+ {'>', "\\3e"},
+ {'@', "\\40"},
+ };
+
+ void decode(std::string& s)
+ {
+ std::string::size_type pos;
+ for (const auto& pair: encode_map)
+ while ((pos = s.find(pair.second)) != std::string::npos)
+ s.replace(pos, pair.second.size(),
+ 1, pair.first);
+ }
+
+ void encode(std::string& s)
+ {
+ std::string::size_type pos;
+ while ((pos = s.find_first_of(" \"&'/:<>@")) != std::string::npos)
+ {
+ auto it = encode_map.find(s[pos]);
+ assert(it != encode_map.end());
+ s.replace(pos, 1, it->second);
+ }
+ }
+}
diff --git a/src/utils/encoding.hpp b/src/utils/encoding.hpp
new file mode 100644
index 0000000..586edd8
--- /dev/null
+++ b/src/utils/encoding.hpp
@@ -0,0 +1,43 @@
+#pragma once
+
+
+#include <string>
+
+namespace utils
+{
+ /**
+ * Return the size, in bytes, of the next UTF-8 codepoint, based on
+ * the given char.
+ */
+ std::size_t get_next_codepoint_size(const unsigned char c);
+ /**
+ * Returns true if the given null-terminated string is valid utf-8.
+ *
+ * Based on http://en.wikipedia.org/wiki/UTF-8#Description
+ */
+ bool is_valid_utf8(const char* s);
+ /**
+ * Remove all invalid codepoints from the given utf-8-encoded string.
+ * The value returned is a copy of the string, without the removed chars.
+ *
+ * See http://www.w3.org/TR/xml/#charsets for the list of valid characters
+ * in XML.
+ */
+ std::string remove_invalid_xml_chars(const std::string& original);
+ /**
+ * Convert the given string (encoded is "encoding") into valid utf-8.
+ * If some decoding fails, insert an utf-8 placeholder character instead.
+ */
+ std::string convert_to_utf8(const std::string& str, const char* encoding);
+}
+
+namespace xep0106
+{
+ /**
+ * Decode and encode inplace.
+ */
+ void decode(std::string&);
+ void encode(std::string&);
+}
+
+
diff --git a/src/utils/get_first_non_empty.cpp b/src/utils/get_first_non_empty.cpp
new file mode 100644
index 0000000..5b3bedb
--- /dev/null
+++ b/src/utils/get_first_non_empty.cpp
@@ -0,0 +1,11 @@
+#include <utils/get_first_non_empty.hpp>
+
+bool is_empty(const std::string& val)
+{
+ return val.empty();
+}
+
+bool is_empty(const int& val)
+{
+ return val == 0;
+}
diff --git a/src/utils/get_first_non_empty.hpp b/src/utils/get_first_non_empty.hpp
new file mode 100644
index 0000000..a38f5fb
--- /dev/null
+++ b/src/utils/get_first_non_empty.hpp
@@ -0,0 +1,20 @@
+#pragma once
+
+#include <string>
+
+bool is_empty(const std::string& val);
+bool is_empty(const int& val);
+
+template <typename T>
+T get_first_non_empty(T&& last)
+{
+ return last;
+}
+
+template <typename T, typename... Args>
+T get_first_non_empty(T&& first, Args&&... args)
+{
+ if (!is_empty(first))
+ return first;
+ return get_first_non_empty(std::forward<Args>(args)...);
+}
diff --git a/src/utils/revstr.cpp b/src/utils/revstr.cpp
new file mode 100644
index 0000000..87fd801
--- /dev/null
+++ b/src/utils/revstr.cpp
@@ -0,0 +1,9 @@
+#include <utils/revstr.hpp>
+
+namespace utils
+{
+ std::string revstr(const std::string& original)
+ {
+ return {original.rbegin(), original.rend()};
+ }
+}
diff --git a/src/utils/revstr.hpp b/src/utils/revstr.hpp
new file mode 100644
index 0000000..8e521ea
--- /dev/null
+++ b/src/utils/revstr.hpp
@@ -0,0 +1,11 @@
+#pragma once
+
+
+#include <string>
+
+namespace utils
+{
+ std::string revstr(const std::string& original);
+}
+
+
diff --git a/src/utils/scopeguard.hpp b/src/utils/scopeguard.hpp
new file mode 100644
index 0000000..e697fc3
--- /dev/null
+++ b/src/utils/scopeguard.hpp
@@ -0,0 +1,98 @@
+#pragma once
+
+#include <functional>
+#include <memory>
+#include <vector>
+
+/**
+ * A class to be used to make sure some functions are called when the scope
+ * is left, because they will be called in the ScopeGuard's destructor. It
+ * can for example be used to delete some pointer whenever any exception is
+ * called. Example:
+
+ * {
+ * ScopeGuard scope;
+ * int* number = new int(2);
+ * scope.add_callback([number]() { delete number; });
+ * // Do some other stuff with the number. But these stuff might throw an exception:
+ * throw std::runtime_error("Some error not caught here, but in our caller");
+ * return true;
+ * }
+
+ * In this example, our pointer will always be deleted, even when the
+ * exception is thrown. If we want the functions to be called only when the
+ * scope is left because of an unexpected exception, we can use
+ * ScopeGuard::disable();
+ */
+
+namespace utils
+{
+
+class ScopeGuard
+{
+public:
+ /**
+ * The constructor can take a callback. But additional callbacks can be
+ * added later with add_callback()
+ */
+ explicit ScopeGuard(std::function<void()>&& func):
+ enabled(true)
+ {
+ this->add_callback(std::move(func));
+ }
+
+ ScopeGuard(const ScopeGuard&) = delete;
+ ScopeGuard& operator=(ScopeGuard&&) = delete;
+ ScopeGuard(ScopeGuard&&) = delete;
+ ScopeGuard& operator=(const ScopeGuard&) = delete;
+
+ /**
+ * default constructor, the scope guard is enabled but empty, use
+ * add_callback()
+ */
+ explicit ScopeGuard():
+ enabled(true)
+ {
+ }
+ /**
+ * Call all callbacks in the desctructor, unless it has been disabled.
+ */
+ ~ScopeGuard()
+ {
+ if (this->enabled)
+ for (auto& func: this->callbacks)
+ func();
+ }
+ /**
+ * Add a callback to be called in our destructor, one scope guard can be
+ * used for more than one task, if needed.
+ */
+ void add_callback(std::function<void()>&& func)
+ {
+ this->callbacks.emplace_back(std::move(func));
+ }
+ /**
+ * Disable that scope guard, nothing will be done when the scope is
+ * exited.
+ */
+ void disable()
+ {
+ this->enabled = false;
+ }
+
+private:
+ bool enabled;
+ std::vector<std::function<void()>> callbacks;
+
+};
+
+template<typename F>
+auto make_scope_guard(F f)
+{
+ static struct Empty {} empty;
+ auto deleter = [f = std::move(f)](Empty*) { f(); };
+ return std::unique_ptr<Empty, decltype(deleter)>{&empty, std::move(deleter)};
+}
+
+}
+
diff --git a/src/utils/sha1.cpp b/src/utils/sha1.cpp
new file mode 100644
index 0000000..b77446e
--- /dev/null
+++ b/src/utils/sha1.cpp
@@ -0,0 +1,32 @@
+#include <utils/sha1.hpp>
+
+#include <biboumi.h>
+
+#ifdef BOTAN_FOUND
+# include <botan/hash.h>
+# include <botan/hex.h>
+#endif
+#ifdef GCRYPT_FOUND
+# include <gcrypt.h>
+# include <vector>
+# include <iomanip>
+# include <sstream>
+#endif
+
+std::string sha1(const std::string& input)
+{
+#ifdef BOTAN_FOUND
+ auto sha1 = Botan::HashFunction::create_or_throw("SHA-1");
+ sha1->update(input);
+ return Botan::hex_encode(sha1->final(), false);
+#endif
+#ifdef GCRYPT_FOUND
+ const auto hash_length = gcry_md_get_algo_dlen(GCRY_MD_SHA1);
+ std::vector<uint8_t> output(hash_length, {});
+ gcry_md_hash_buffer(GCRY_MD_SHA1, output.data(), input.data(), input.size());
+ std::ostringstream digest;
+ for (std::size_t i = 0; i < hash_length; i++)
+ digest << std::hex << std::setfill('0') << std::setw(2) << static_cast<int>(output[i]);
+ return digest.str();
+#endif
+}
diff --git a/src/utils/sha1.hpp b/src/utils/sha1.hpp
new file mode 100644
index 0000000..6c551ac
--- /dev/null
+++ b/src/utils/sha1.hpp
@@ -0,0 +1,5 @@
+#pragma once
+
+#include <string>
+
+std::string sha1(const std::string& input);
diff --git a/src/utils/split.cpp b/src/utils/split.cpp
new file mode 100644
index 0000000..80f8dae
--- /dev/null
+++ b/src/utils/split.cpp
@@ -0,0 +1,19 @@
+#include <utils/split.hpp>
+#include <sstream>
+
+namespace utils
+{
+ std::vector<std::string> split(const std::string& s, const char delim, const bool allow_empty)
+ {
+ std::vector<std::string> ret;
+ std::stringstream ss(s);
+ std::string item;
+ while (std::getline(ss, item, delim))
+ {
+ if (item.empty() && !allow_empty)
+ continue ;
+ ret.emplace_back(std::move(item));
+ }
+ return ret;
+ }
+}
diff --git a/src/utils/split.hpp b/src/utils/split.hpp
new file mode 100644
index 0000000..3755ef8
--- /dev/null
+++ b/src/utils/split.hpp
@@ -0,0 +1,12 @@
+#pragma once
+
+
+#include <string>
+#include <vector>
+
+namespace utils
+{
+ std::vector<std::string> split(const std::string &s, const char delim, const bool allow_empty=true);
+}
+
+
diff --git a/src/utils/string.cpp b/src/utils/string.cpp
new file mode 100644
index 0000000..635e71a
--- /dev/null
+++ b/src/utils/string.cpp
@@ -0,0 +1,28 @@
+#include <utils/string.hpp>
+#include <utils/encoding.hpp>
+
+bool to_bool(const std::string& val)
+{
+ return (val == "1" || val == "true");
+}
+
+std::vector<std::string> cut(const std::string& val, const std::size_t size)
+{
+ std::vector<std::string> res;
+ std::string::size_type pos = 0;
+ while (pos < val.size())
+ {
+ // Get the number of chars, <= size, that contain only whole
+ // UTF-8 codepoints.
+ std::size_t s = 0;
+ auto codepoint_size = utils::get_next_codepoint_size(val[pos + s]);
+ while (s + codepoint_size <= size && pos + s < val.size())
+ {
+ s += codepoint_size;
+ codepoint_size = utils::get_next_codepoint_size(val[pos + s]);
+ }
+ res.emplace_back(val.substr(pos, s));
+ pos += s;
+ }
+ return res;
+}
diff --git a/src/utils/string.hpp b/src/utils/string.hpp
new file mode 100644
index 0000000..84ba101
--- /dev/null
+++ b/src/utils/string.hpp
@@ -0,0 +1,10 @@
+#pragma once
+
+
+#include <vector>
+#include <string>
+
+bool to_bool(const std::string& val);
+std::vector<std::string> cut(const std::string& val, const std::size_t size);
+
+
diff --git a/src/utils/system.cpp b/src/utils/system.cpp
new file mode 100644
index 0000000..c0bee11
--- /dev/null
+++ b/src/utils/system.cpp
@@ -0,0 +1,21 @@
+#include <logger/logger.hpp>
+#include <utils/system.hpp>
+#include <sys/utsname.h>
+#include <cstring>
+
+using namespace std::string_literals;
+
+namespace utils
+{
+std::string get_system_name()
+{
+ struct utsname uts;
+ const int res = ::uname(&uts);
+ if (res == -1)
+ {
+ log_error("uname failed: ", std::strerror(errno));
+ return "Unknown";
+ }
+ return uts.sysname + " "s + uts.release;
+}
+} \ No newline at end of file
diff --git a/src/utils/system.hpp b/src/utils/system.hpp
new file mode 100644
index 0000000..7ea1677
--- /dev/null
+++ b/src/utils/system.hpp
@@ -0,0 +1,8 @@
+#pragma once
+
+#include <string>
+
+namespace utils
+{
+std::string get_system_name();
+} \ No newline at end of file
diff --git a/src/utils/time.cpp b/src/utils/time.cpp
new file mode 100644
index 0000000..8fa3fcd
--- /dev/null
+++ b/src/utils/time.cpp
@@ -0,0 +1,70 @@
+#include <utils/time.hpp>
+#include <time.h>
+
+#include <sstream>
+#include <iomanip>
+#include <locale>
+
+#include "biboumi.h"
+
+namespace utils
+{
+std::string to_string(const std::time_t& timestamp)
+{
+ constexpr std::size_t stamp_size = 21;
+ char date_buf[stamp_size];
+ if (std::strftime(date_buf, stamp_size, "%FT%TZ", std::gmtime(&timestamp)) != stamp_size - 1)
+ return "";
+ return {std::begin(date_buf), std::end(date_buf) - 1};
+}
+
+std::time_t parse_datetime(const std::string& stamp)
+{
+ static const char* format = "%Y-%m-%dT%H:%M:%S";
+ std::tm t = {};
+#ifdef HAS_GET_TIME
+ std::istringstream ss(stamp);
+ ss.imbue(std::locale("C"));
+
+ std::string timezone;
+ ss >> std::get_time(&t, format) >> timezone;
+ if (ss.fail())
+ return -1;
+#else
+ /* Y - m - d T H : M : S */
+ constexpr std::size_t stamp_size_without_tz = 4 + 1 + 2 + 1 + 2 + 1 + 2 + 1 + 2 + 1 + 2;
+ if (!strptime(stamp.data(), format, &t)) {
+ return -1;
+ }
+ const std::string timezone(stamp.data() + stamp_size_without_tz);
+#endif
+
+ if (timezone.empty())
+ return -1;
+
+ if (timezone.compare(0, 1, "Z") != 0)
+ {
+ std::stringstream tz_ss;
+ tz_ss << timezone;
+ int multiplier = -1;
+ char prefix;
+ int hours;
+ char sep;
+ int minutes;
+ tz_ss >> prefix >> hours >> sep >> minutes;
+ if (tz_ss.fail())
+ return -1;
+ if (prefix == '-')
+ multiplier = +1;
+ else if (prefix != '+')
+ return -1;
+
+ t.tm_hour += multiplier * hours;
+ t.tm_min += multiplier * minutes;
+ }
+ return ::timegm(&t);
+}
+
+}
+
+
diff --git a/src/utils/time.hpp b/src/utils/time.hpp
new file mode 100644
index 0000000..c71cd9c
--- /dev/null
+++ b/src/utils/time.hpp
@@ -0,0 +1,10 @@
+#pragma once
+
+#include <ctime>
+#include <string>
+
+namespace utils
+{
+std::string to_string(const std::time_t& timestamp);
+std::time_t parse_datetime(const std::string& stamp);
+} \ No newline at end of file
diff --git a/src/utils/timed_events.cpp b/src/utils/timed_events.cpp
new file mode 100644
index 0000000..5077199
--- /dev/null
+++ b/src/utils/timed_events.cpp
@@ -0,0 +1,47 @@
+#include <utils/timed_events.hpp>
+
+TimedEvent::TimedEvent(std::chrono::steady_clock::time_point&& time_point,
+ std::function<void()> callback, const std::string& name):
+ time_point(std::move(time_point)),
+ callback(callback),
+ repeat(false),
+ repeat_delay(0),
+ name(name)
+{
+}
+
+TimedEvent::TimedEvent(std::chrono::milliseconds&& duration,
+ std::function<void()> callback, const std::string& name):
+ time_point(std::chrono::steady_clock::now() + duration),
+ callback(callback),
+ repeat(true),
+ repeat_delay(std::move(duration)),
+ name(name)
+{
+}
+
+bool TimedEvent::is_after(const TimedEvent& other) const
+{
+ return this->is_after(other.time_point);
+}
+
+bool TimedEvent::is_after(const std::chrono::steady_clock::time_point& time_point) const
+{
+ return this->time_point > time_point;
+}
+
+std::chrono::milliseconds TimedEvent::get_timeout() const
+{
+ auto diff = std::chrono::duration_cast<std::chrono::milliseconds>(this->time_point - std::chrono::steady_clock::now());
+ return std::max(diff, 0ms);
+}
+
+void TimedEvent::execute() const
+{
+ this->callback();
+}
+
+const std::string& TimedEvent::get_name() const
+{
+ return this->name;
+}
diff --git a/src/utils/timed_events.hpp b/src/utils/timed_events.hpp
new file mode 100644
index 0000000..6e28206
--- /dev/null
+++ b/src/utils/timed_events.hpp
@@ -0,0 +1,132 @@
+#pragma once
+
+#include <functional>
+#include <string>
+#include <chrono>
+#include <vector>
+
+using namespace std::literals::chrono_literals;
+
+namespace utils {
+static constexpr std::chrono::milliseconds no_timeout = std::chrono::milliseconds(-1);
+}
+
+class TimedEventsManager;
+
+/**
+ * A callback with an associated date.
+ */
+
+class TimedEvent
+{
+ friend class TimedEventsManager;
+public:
+ /**
+ * An event the occurs only once, at the given time_point
+ */
+ explicit TimedEvent(std::chrono::steady_clock::time_point&& time_point,
+ std::function<void()> callback, const std::string& name="");
+ explicit TimedEvent(std::chrono::milliseconds&& duration,
+ std::function<void()> callback, const std::string& name="");
+
+ explicit TimedEvent(TimedEvent&&) = default;
+ TimedEvent& operator=(TimedEvent&&) = default;
+ ~TimedEvent() = default;
+
+ TimedEvent(const TimedEvent&) = delete;
+ TimedEvent& operator=(const TimedEvent&) = delete;
+
+ /**
+ * Whether or not this event happens after the other one.
+ */
+ bool is_after(const TimedEvent& other) const;
+ bool is_after(const std::chrono::steady_clock::time_point& time_point) const;
+ /**
+ * Return the duration difference between now and the event time point.
+ * If the difference would be negative (i.e. the event is expired), the
+ * returned value is 0 instead. The value cannot then be negative.
+ */
+ std::chrono::milliseconds get_timeout() const;
+ void execute() const;
+ const std::string& get_name() const;
+
+private:
+ /**
+ * The next time point at which the event is executed.
+ */
+ std::chrono::steady_clock::time_point time_point;
+ /**
+ * The function to execute.
+ */
+ std::function<void()> callback;
+ /**
+ * Whether or not this events repeats itself until it is destroyed.
+ */
+ bool repeat;
+ /**
+ * This value is added to the time_point each time the event is executed,
+ * if repeat is true. Otherwise it is ignored.
+ */
+ std::chrono::milliseconds repeat_delay;
+ /**
+ * A name that is used to identify that event. If you want to find your
+ * event (for example if you want to cancel it), the name should be
+ * unique.
+ */
+ std::string name;
+};
+
+/**
+ * A class managing a list of TimedEvents.
+ * They are sorted, new events can be added, removed, fetch, etc.
+ */
+
+class TimedEventsManager
+{
+public:
+ ~TimedEventsManager() = default;
+
+ TimedEventsManager(const TimedEventsManager&) = delete;
+ TimedEventsManager(TimedEventsManager&&) = delete;
+ TimedEventsManager& operator=(const TimedEventsManager&) = delete;
+ TimedEventsManager& operator=(TimedEventsManager&&) = delete;
+
+ /**
+ * Return the unique instance of this class
+ */
+ static TimedEventsManager& instance();
+ /**
+ * Add an event to the list of managed events. The list is sorted after
+ * this call.
+ */
+ void add_event(TimedEvent&& event);
+ /**
+ * Returns the duration, in milliseconds, between now and the next
+ * available event. If the event is already expired (the duration is
+ * negative), 0 is returned instead (as in “it's not too late, execute it
+ * now”)
+ * Returns a negative value if no event is available.
+ */
+ std::chrono::milliseconds get_timeout() const;
+ /**
+ * Execute all the expired events (if their expiration time is exactly
+ * now, or before now). The event is then removed from the list. If the
+ * event does repeat, its expiration time is updated and it is reinserted
+ * in the list at the correct position.
+ * Returns the number of executed events.
+ */
+ std::size_t execute_expired_events();
+ /**
+ * Remove (and thus cancel) all the timed events with the given name.
+ * Returns the number of canceled events.
+ */
+ std::size_t cancel(const std::string& name);
+ /**
+ * Return the number of managed events.
+ */
+ std::size_t size() const;
+
+private:
+ std::vector<TimedEvent> events;
+ explicit TimedEventsManager() = default;
+};
diff --git a/src/utils/timed_events_manager.cpp b/src/utils/timed_events_manager.cpp
new file mode 100644
index 0000000..67d61fe
--- /dev/null
+++ b/src/utils/timed_events_manager.cpp
@@ -0,0 +1,73 @@
+#include <utils/timed_events.hpp>
+
+TimedEventsManager& TimedEventsManager::instance()
+{
+ static TimedEventsManager inst;
+ return inst;
+}
+
+void TimedEventsManager::add_event(TimedEvent&& event)
+{
+ for (auto it = this->events.begin(); it != this->events.end(); ++it)
+ {
+ if (it->is_after(event))
+ {
+ this->events.emplace(it, std::move(event));
+ return;
+ }
+ }
+ this->events.emplace_back(std::move(event));
+}
+
+std::chrono::milliseconds TimedEventsManager::get_timeout() const
+{
+ if (this->events.empty())
+ return utils::no_timeout;
+ return this->events.front().get_timeout();
+}
+
+std::size_t TimedEventsManager::execute_expired_events()
+{
+ std::size_t count = 0;
+ const auto now = std::chrono::steady_clock::now();
+ for (auto it = this->events.begin(); it != this->events.end();)
+ {
+ if (!it->is_after(now))
+ {
+ TimedEvent copy(std::move(*it));
+ it = this->events.erase(it);
+ ++count;
+ copy.execute();
+ if (copy.repeat)
+ {
+ copy.time_point += copy.repeat_delay;
+ this->add_event(std::move(copy));
+ }
+ continue;
+ }
+ else
+ break;
+ }
+ return count;
+}
+
+std::size_t TimedEventsManager::cancel(const std::string& name)
+{
+ std::size_t res = 0;
+ for (auto it = this->events.begin(); it != this->events.end();)
+ {
+ if (it->get_name() == name)
+ {
+ it = this->events.erase(it);
+ res++;
+ }
+ else
+ ++it;
+ }
+ return res;
+}
+
+std::size_t TimedEventsManager::size() const
+{
+ return this->events.size();
+}
diff --git a/src/utils/tolower.cpp b/src/utils/tolower.cpp
new file mode 100644
index 0000000..3e518bd
--- /dev/null
+++ b/src/utils/tolower.cpp
@@ -0,0 +1,13 @@
+#include <utils/tolower.hpp>
+
+namespace utils
+{
+ std::string tolower(const std::string& original)
+ {
+ std::string res;
+ res.reserve(original.size());
+ for (const char c: original)
+ res += static_cast<char>(std::tolower(c));
+ return res;
+ }
+}
diff --git a/src/utils/tolower.hpp b/src/utils/tolower.hpp
new file mode 100644
index 0000000..650e05d
--- /dev/null
+++ b/src/utils/tolower.hpp
@@ -0,0 +1,11 @@
+#pragma once
+
+
+#include <string>
+
+namespace utils
+{
+ std::string tolower(const std::string& original);
+}
+
+
diff --git a/src/utils/xdg.cpp b/src/utils/xdg.cpp
new file mode 100644
index 0000000..b0fa7be
--- /dev/null
+++ b/src/utils/xdg.cpp
@@ -0,0 +1,29 @@
+#include <utils/xdg.hpp>
+#include <cstdlib>
+
+#include "biboumi.h"
+
+std::string xdg_path(const std::string& filename, const char* env_var)
+{
+ const char* xdg_home = ::getenv(env_var);
+ if (xdg_home && xdg_home[0] == '/')
+ return std::string{xdg_home} + "/" PROJECT_NAME "/" + filename;
+ else
+ {
+ const char* home = ::getenv("HOME");
+ if (home)
+ return std::string{home} + "/" ".config" "/" PROJECT_NAME "/" + filename;
+ else
+ return filename;
+ }
+}
+
+std::string xdg_config_path(const std::string& filename)
+{
+ return xdg_path(filename, "XDG_CONFIG_HOME");
+}
+
+std::string xdg_data_path(const std::string& filename)
+{
+ return xdg_path(filename, "XDG_DATA_HOME");
+}
diff --git a/src/utils/xdg.hpp b/src/utils/xdg.hpp
new file mode 100644
index 0000000..56e11da
--- /dev/null
+++ b/src/utils/xdg.hpp
@@ -0,0 +1,14 @@
+#pragma once
+
+
+#include <string>
+
+/**
+ * Returns a path for the given filename, according to the XDG base
+ * directory specification, see
+ * http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
+ */
+std::string xdg_config_path(const std::string& filename);
+std::string xdg_data_path(const std::string& filename);
+
+
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 <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.");
+}
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 <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);
diff --git a/src/xmpp/adhoc_commands_handler.cpp b/src/xmpp/adhoc_commands_handler.cpp
new file mode 100644
index 0000000..040d0ff
--- /dev/null
+++ b/src/xmpp/adhoc_commands_handler.cpp
@@ -0,0 +1,111 @@
+#include <xmpp/adhoc_commands_handler.hpp>
+#include <xmpp/xmpp_component.hpp>
+
+#include <utils/timed_events.hpp>
+#include <logger/logger.hpp>
+#include <config/config.hpp>
+#include <xmpp/jid.hpp>
+
+#include <iostream>
+
+using namespace std::string_literals;
+
+const std::map<const std::string, const AdhocCommand>& AdhocCommandsHandler::get_commands() const
+{
+ return this->commands;
+}
+
+void AdhocCommandsHandler::add_command(std::string name, AdhocCommand command)
+{
+ const auto found = this->commands.find(name);
+ if (found != this->commands.end())
+ throw std::runtime_error("Trying to add an ad-hoc command that already exist: "s + name);
+ this->commands.emplace(std::make_pair(std::move(name), std::move(command)));
+}
+
+XmlNode AdhocCommandsHandler::handle_request(const std::string& executor_jid, const std::string& to, XmlNode command_node)
+{
+ std::string action = command_node.get_tag("action");
+ if (action.empty())
+ action = "execute";
+ command_node.del_tag("action");
+
+ Jid jid(executor_jid);
+
+ const std::string node = command_node.get_tag("node");
+ auto command_it = this->commands.find(node);
+ if (command_it == this->commands.end())
+ {
+ XmlSubNode error(command_node, ADHOC_NS":error");
+ error["type"] = "cancel";
+ XmlSubNode condition(error, STANZA_NS":item-not-found");
+ }
+ else if (command_it->second.is_admin_only() &&
+ Config::get("admin", "") != jid.local + "@" + jid.domain)
+ {
+ XmlSubNode error(command_node, ADHOC_NS":error");
+ error["type"] = "cancel";
+ XmlSubNode condition(error, STANZA_NS":forbidden");
+ }
+ else
+ {
+ std::string sessionid = command_node.get_tag("sessionid");
+ if (sessionid.empty())
+ { // create a new session, with a new id
+ sessionid = XmppComponent::next_id();
+ command_node["sessionid"] = sessionid;
+ this->sessions.emplace(std::piecewise_construct,
+ std::forward_as_tuple(sessionid, executor_jid),
+ std::forward_as_tuple(command_it->second, executor_jid, to));
+ TimedEventsManager::instance().add_event(TimedEvent(std::chrono::steady_clock::now() + 3600s,
+ std::bind(&AdhocCommandsHandler::remove_session, this, sessionid, executor_jid),
+ "adhocsession"s + sessionid + executor_jid));
+ }
+ auto session_it = this->sessions.find(std::make_pair(sessionid, executor_jid));
+ if ((session_it != this->sessions.end()) &&
+ (action == "execute" || action == "next" || action == "complete"))
+ {
+ // execute the step
+ AdhocSession& session = session_it->second;
+ const AdhocStep& step = session.get_next_step();
+ step(this->xmpp_component, session, command_node);
+ if (session.remaining_steps() == 0 ||
+ session.is_terminated())
+ {
+ this->sessions.erase(session_it);
+ command_node["status"] = "completed";
+ TimedEventsManager::instance().cancel("adhocsession"s + sessionid + executor_jid);
+ }
+ else
+ {
+ command_node["status"] = "executing";
+ XmlSubNode actions(command_node, "actions");
+ XmlSubNode next(actions, "next");
+ }
+ }
+ else if (action == "cancel")
+ {
+ this->sessions.erase(session_it);
+ command_node["status"] = "canceled";
+ TimedEventsManager::instance().cancel("adhocsession"s + sessionid + executor_jid);
+ }
+ else // unsupported action
+ {
+ XmlSubNode error(command_node, ADHOC_NS":error");
+ error["type"] = "modify";
+ XmlSubNode condition(error, STANZA_NS":bad-request");
+ }
+ }
+ return command_node;
+}
+
+void AdhocCommandsHandler::remove_session(const std::string& session_id, const std::string& initiator_jid)
+{
+ auto session_it = this->sessions.find(std::make_pair(session_id, initiator_jid));
+ if (session_it != this->sessions.end())
+ {
+ this->sessions.erase(session_it);
+ return ;
+ }
+ log_error("Tried to remove ad-hoc session for [", session_id, ", ", initiator_jid, "] but none found");
+}
diff --git a/src/xmpp/adhoc_commands_handler.hpp b/src/xmpp/adhoc_commands_handler.hpp
new file mode 100644
index 0000000..e37d913
--- /dev/null
+++ b/src/xmpp/adhoc_commands_handler.hpp
@@ -0,0 +1,71 @@
+#pragma once
+
+/**
+ * Manage a list of available AdhocCommands and the list of ongoing
+ * AdhocCommandSessions.
+ */
+
+#include <xmpp/adhoc_command.hpp>
+#include <xmpp/xmpp_stanza.hpp>
+
+#include <utility>
+#include <string>
+#include <map>
+
+class AdhocCommandsHandler
+{
+public:
+ explicit AdhocCommandsHandler(XmppComponent& xmpp_component):
+ xmpp_component(xmpp_component),
+ commands{}
+ { }
+ ~AdhocCommandsHandler() = default;
+
+ AdhocCommandsHandler(const AdhocCommandsHandler&) = delete;
+ AdhocCommandsHandler(AdhocCommandsHandler&&) = delete;
+ AdhocCommandsHandler& operator=(const AdhocCommandsHandler&) = delete;
+ AdhocCommandsHandler& operator=(AdhocCommandsHandler&&) = delete;
+
+ /**
+ * Returns the list of available commands.
+ */
+ const std::map<const std::string, const AdhocCommand>& get_commands() const;
+ /**
+ * Add a command into the list, associated with the given name
+ */
+ void add_command(std::string name, AdhocCommand command);
+ /**
+ * Find the requested command, create a new session or use an existing
+ * one, and process the request (provide a new form, an error, or a
+ * result).
+ *
+ * Returns a (moved) XmlNode that will be inserted in the iq response. It
+ * should be a <command/> node containing one or more useful children. If
+ * it contains an <error/> node, the iq response will have an error type.
+ *
+ * Takes a copy of the <command/> node so we can actually edit it and use
+ * it as our return value.
+ */
+ XmlNode handle_request(const std::string& executor_jid, const std::string& to, XmlNode command_node);
+ /**
+ * Remove the session from the list. This is done to avoid filling the
+ * memory with waiting session (for example due to a client that starts
+ * multi-steps command but never finishes them).
+ */
+ void remove_session(const std::string& session_id, const std::string& initiator_jid);
+private:
+ /**
+ * To access basically anything in the gateway.
+ */
+ XmppComponent& xmpp_component;
+ /**
+ * The list of all available commands.
+ */
+ std::map<const std::string, const AdhocCommand> commands;
+ /**
+ * The list of all currently on-going commands.
+ *
+ * Of the form: {{session_id, owner_jid}, session}.
+ */
+ std::map<std::pair<const std::string, const std::string>, AdhocSession> sessions;
+};
diff --git a/src/xmpp/adhoc_session.cpp b/src/xmpp/adhoc_session.cpp
new file mode 100644
index 0000000..dda4bea
--- /dev/null
+++ b/src/xmpp/adhoc_session.cpp
@@ -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;
+}
diff --git a/src/xmpp/adhoc_session.hpp b/src/xmpp/adhoc_session.hpp
new file mode 100644
index 0000000..0de8d13
--- /dev/null
+++ b/src/xmpp/adhoc_session.hpp
@@ -0,0 +1,88 @@
+#pragma once
+
+#include <xmpp/xmpp_stanza.hpp>
+
+#include <functional>
+#include <string>
+#include <map>
+
+class XmppComponent;
+
+class AdhocCommand;
+class AdhocSession;
+
+/**
+ * A function executed as an ad-hoc command step. It takes a <command/>
+ * XmlNode and modifies it accordingly (inserting for example an <error/>
+ * node, or a data form…).
+ */
+using AdhocStep = std::function<void(XmppComponent&, AdhocSession&, XmlNode&)>;
+
+class AdhocSession
+{
+public:
+ explicit AdhocSession(const AdhocCommand& command, const std::string& owner_jid,
+ const std::string& to_jid);
+ ~AdhocSession() = default;
+
+ AdhocSession(const AdhocSession&) = delete;
+ AdhocSession(AdhocSession&&) = delete;
+ AdhocSession& operator=(const AdhocSession&) = delete;
+ AdhocSession& operator=(AdhocSession&&) = delete;
+
+ /**
+ * Return the function to be executed, found in our AdhocCommand, for the
+ * current_step. And increment the current_step.
+ */
+ const AdhocStep& get_next_step();
+ /**
+ * Return the number of remaining steps.
+ */
+ size_t remaining_steps() const;
+ /**
+ * This may be modified by an AdhocStep, to indicate that this session
+ * should no longer exist, because we encountered an error, and we can't
+ * execute any more step of it.
+ */
+ void terminate();
+ bool is_terminated() const;
+ std::string get_target_jid() const
+ {
+ return this->to_jid;
+ }
+ std::string get_owner_jid() const
+ {
+ return this->owner_jid;
+ }
+
+private:
+ /**
+ * A reference of the command concerned by this session. Used for example
+ * to get the next step of that command, things like that.
+ */
+ const AdhocCommand& command;
+ /**
+ * The full JID of the XMPP user that created this session by executing
+ * the first step of a command. Only that JID must be allowed to access
+ * this session.
+ */
+ const std::string& owner_jid;
+ /**
+ * The 'to' attribute in the request stanza. This is the target of the current session.
+ */
+ const std::string& to_jid;
+ /**
+ * The current step we are at. It starts at zero. It is used to index the
+ * associated AdhocCommand::callbacks vector.
+ */
+ size_t current_step;
+ bool terminated;
+
+public:
+ /**
+ * A map to store various things that we may want to remember between two
+ * steps of the same session. A step can insert any value associated to
+ * any key in there.
+ */
+ std::map<std::string, std::string> vars;
+};
diff --git a/src/xmpp/auth.cpp b/src/xmpp/auth.cpp
new file mode 100644
index 0000000..8a34a4e
--- /dev/null
+++ b/src/xmpp/auth.cpp
@@ -0,0 +1,8 @@
+#include <xmpp/auth.hpp>
+
+#include <utils/sha1.hpp>
+
+std::string get_handshake_digest(const std::string& stream_id, const std::string& secret)
+{
+ return sha1(stream_id + secret);
+}
diff --git a/src/xmpp/auth.hpp b/src/xmpp/auth.hpp
new file mode 100644
index 0000000..34a2116
--- /dev/null
+++ b/src/xmpp/auth.hpp
@@ -0,0 +1,6 @@
+#pragma once
+
+#include <string>
+
+std::string get_handshake_digest(const std::string& stream_id, const std::string& secret);
+
diff --git a/src/xmpp/biboumi_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 <cstdlib>
-#include <louloulibs.h>
#include <biboumi.h>
#include <uuid/uuid.h>
diff --git a/src/xmpp/body.hpp b/src/xmpp/body.hpp
new file mode 100644
index 0000000..068d1a4
--- /dev/null
+++ b/src/xmpp/body.hpp
@@ -0,0 +1,12 @@
+#pragma once
+
+
+namespace Xmpp
+{
+// Contains:
+// - an XMPP-valid UTF-8 body
+// - an XML node representing the XHTML-IM body, or null
+ using body = std::tuple<const std::string, std::unique_ptr<XmlNode>>;
+}
+
+
diff --git a/src/xmpp/jid.cpp b/src/xmpp/jid.cpp
new file mode 100644
index 0000000..0751387
--- /dev/null
+++ b/src/xmpp/jid.cpp
@@ -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
+}
diff --git a/src/xmpp/jid.hpp b/src/xmpp/jid.hpp
new file mode 100644
index 0000000..85e835c
--- /dev/null
+++ b/src/xmpp/jid.hpp
@@ -0,0 +1,49 @@
+#pragma once
+
+
+#include <string>
+
+/**
+ * Parse a JID into its different subart
+ */
+class Jid
+{
+public:
+ explicit Jid(const std::string& jid);
+
+ Jid(const Jid&) = delete;
+ Jid(Jid&&) = delete;
+ Jid& operator=(const Jid&) = delete;
+ Jid& operator=(Jid&&) = delete;
+
+ std::string domain;
+ std::string local;
+ std::string resource;
+
+ std::string bare() const
+ {
+ return this->local + "@" + this->domain;
+ }
+ std::string full() const
+ {
+ std::string res = this->domain;
+ if (!this->local.empty())
+ res = this->local + "@" + this->domain;
+ if (!this->resource.empty())
+ res += "/" + this->resource;
+ return res;
+ }
+};
+
+/**
+ * Prepare the given UTF-8 string according to the XMPP node stringprep
+ * identifier profile. This is used to send properly-formed JID to the XMPP
+ * server.
+ *
+ * If the stringprep library is not found, we return an empty string. When
+ * this function is used, the result must always be checked for an empty
+ * value, and if this is the case it must not be used as a JID.
+ */
+std::string jidprep(const std::string& original);
+
+
diff --git a/src/xmpp/xmpp_component.cpp b/src/xmpp/xmpp_component.cpp
new file mode 100644
index 0000000..1453b18
--- /dev/null
+++ b/src/xmpp/xmpp_component.cpp
@@ -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;
+}
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 <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;
+};
+
+
diff --git a/src/xmpp/xmpp_parser.cpp b/src/xmpp/xmpp_parser.cpp
new file mode 100644
index 0000000..0488be9
--- /dev/null
+++ b/src/xmpp/xmpp_parser.cpp
@@ -0,0 +1,172 @@
+#include <xmpp/xmpp_parser.hpp>
+#include <xmpp/xmpp_stanza.hpp>
+
+#include <logger/logger.hpp>
+
+/**
+ * Expat handlers. Called by the Expat library, never by ourself.
+ * They just forward the call to the XmppParser corresponding methods.
+ */
+
+static void start_element_handler(void* user_data, const XML_Char* name, const XML_Char** atts)
+{
+ static_cast<XmppParser*>(user_data)->start_element(name, atts);
+}
+
+static void end_element_handler(void* user_data, const XML_Char* name)
+{
+ static_cast<XmppParser*>(user_data)->end_element(name);
+}
+
+static void character_data_handler(void *user_data, const XML_Char *s, int len)
+{
+ static_cast<XmppParser*>(user_data)->char_data(s, len);
+}
+
+/**
+ * XmppParser class
+ */
+
+XmppParser::XmppParser():
+ level(0),
+ current_node(nullptr),
+ root(nullptr)
+{
+ this->init_xml_parser();
+}
+
+void XmppParser::init_xml_parser()
+{
+ // Create the expat parser
+ this->parser = XML_ParserCreateNS("UTF-8", ':');
+ XML_SetUserData(this->parser, static_cast<void*>(this));
+
+ // Install Expat handlers
+ XML_SetElementHandler(this->parser, &start_element_handler, &end_element_handler);
+ XML_SetCharacterDataHandler(this->parser, &character_data_handler);
+}
+
+XmppParser::~XmppParser()
+{
+ XML_ParserFree(this->parser);
+}
+
+int XmppParser::feed(const char* data, const int len, const bool is_final)
+{
+ int res = XML_Parse(this->parser, data, len, is_final);
+ if (res == XML_STATUS_ERROR &&
+ (XML_GetErrorCode(this->parser) != XML_ERROR_FINISHED))
+ log_error("Xml_Parse encountered an error: ",
+ XML_ErrorString(XML_GetErrorCode(this->parser)));
+ return res;
+}
+
+int XmppParser::parse(const int len, const bool is_final)
+{
+ int res = XML_ParseBuffer(this->parser, len, is_final);
+ if (res == XML_STATUS_ERROR)
+ log_error("Xml_Parsebuffer encountered an error: ",
+ XML_ErrorString(XML_GetErrorCode(this->parser)));
+ return res;
+}
+
+void XmppParser::reset()
+{
+ XML_ParserFree(this->parser);
+ this->init_xml_parser();
+ this->current_node = nullptr;
+ this->root.reset(nullptr);
+ this->level = 0;
+}
+
+void* XmppParser::get_buffer(const size_t size) const
+{
+ return XML_GetBuffer(this->parser, static_cast<int>(size));
+}
+
+void XmppParser::start_element(const XML_Char* name, const XML_Char** attribute)
+{
+ this->level++;
+
+ auto new_node = std::make_unique<XmlNode>(name, this->current_node);
+ auto new_node_ptr = new_node.get();
+ if (this->current_node)
+ this->current_node->add_child(std::move(new_node));
+ else
+ this->root = std::move(new_node);
+ this->current_node = new_node_ptr;
+ for (size_t i = 0; attribute[i]; i += 2)
+ this->current_node->set_attribute(attribute[i], attribute[i+1]);
+ if (this->level == 1)
+ this->stream_open_event(*this->current_node);
+}
+
+void XmppParser::end_element(const XML_Char*)
+{
+ this->level--;
+ if (this->level == 0)
+ { // End of the whole stream
+ this->stream_close_event(*this->current_node);
+ this->current_node = nullptr;
+ this->root.reset();
+ }
+ else
+ {
+ auto parent = this->current_node->get_parent();
+ if (this->level == 1)
+ { // End of a stanza
+ this->stanza_event(*this->current_node);
+ // Note: deleting all the children of our parent deletes ourself,
+ // so current_node is an invalid pointer after this line
+ parent->delete_all_children();
+ }
+ this->current_node = parent;
+ }
+}
+
+void XmppParser::char_data(const XML_Char* data, const size_t len)
+{
+ if (this->current_node->has_children())
+ this->current_node->get_last_child()->add_to_tail({data, len});
+ else
+ this->current_node->add_to_inner({data, len});
+}
+
+void XmppParser::stanza_event(const Stanza& stanza) const
+{
+ for (const auto& callback: this->stanza_callbacks)
+ {
+ try {
+ callback(stanza);
+ } catch (const std::exception& e) {
+ log_error("Unhandled exception: ", e.what());
+ }
+ }
+}
+
+void XmppParser::stream_open_event(const XmlNode& node) const
+{
+ for (const auto& callback: this->stream_open_callbacks)
+ callback(node);
+}
+
+void XmppParser::stream_close_event(const XmlNode& node) const
+{
+ for (const auto& callback: this->stream_close_callbacks)
+ callback(node);
+}
+
+void XmppParser::add_stanza_callback(std::function<void(const Stanza&)>&& callback)
+{
+ this->stanza_callbacks.emplace_back(std::move(callback));
+}
+
+void XmppParser::add_stream_open_callback(std::function<void(const XmlNode&)>&& callback)
+{
+ this->stream_open_callbacks.emplace_back(std::move(callback));
+}
+
+void XmppParser::add_stream_close_callback(std::function<void(const XmlNode&)>&& callback)
+{
+ this->stream_close_callbacks.emplace_back(std::move(callback));
+}
diff --git a/src/xmpp/xmpp_parser.hpp b/src/xmpp/xmpp_parser.hpp
new file mode 100644
index 0000000..9d67228
--- /dev/null
+++ b/src/xmpp/xmpp_parser.hpp
@@ -0,0 +1,133 @@
+#pragma once
+
+
+#include <xmpp/xmpp_stanza.hpp>
+
+#include <functional>
+
+#include <expat.h>
+
+/**
+ * A SAX XML parser that builds XML nodes and spawns events when a complete
+ * stanza is received (an element of level 2), or when the document is
+ * opened/closed (an element of level 1)
+ *
+ * After a stanza_event has been spawned, we delete the whole stanza. This
+ * means that even with a very long document (in XMPP the document is
+ * potentially infinite), the memory is never exhausted as long as each
+ * stanza is reasonnably short.
+ *
+ * The element names generated by expat contain the namespace of the
+ * element, a colon (':') and then the actual name of the element. To get
+ * an element "x" with a namespace of "http://jabber.org/protocol/muc", you
+ * just look for an XmlNode named "http://jabber.org/protocol/muc:x"
+ *
+ * TODO: enforce the size-limit for the stanza (limit the number of childs
+ * it can contain). For example forbid the parser going further than level
+ * 20 (arbitrary number here), and each XML node to have more than 15 childs
+ * (arbitrary number again).
+ */
+class XmppParser
+{
+public:
+ explicit XmppParser();
+ ~XmppParser();
+ XmppParser(const XmppParser&) = delete;
+ XmppParser& operator=(const XmppParser&) = delete;
+ XmppParser(XmppParser&&) = delete;
+ XmppParser& operator=(XmppParser&&) = delete;
+
+public:
+ /**
+ * Feed the parser with some XML data
+ */
+ int feed(const char* data, const int len, const bool is_final);
+ /**
+ * Parse the data placed in the parser buffer
+ */
+ int parse(const int size, const bool is_final);
+ /**
+ * Reset the parser, so it can be used from scratch afterward
+ */
+ void reset();
+ /**
+ * Get a buffer provided by the xml parser.
+ */
+ void* get_buffer(const size_t size) const;
+ /**
+ * Add one callback for the various events that this parser can spawn.
+ */
+ void add_stanza_callback(std::function<void(const Stanza&)>&& callback);
+ void add_stream_open_callback(std::function<void(const XmlNode&)>&& callback);
+ void add_stream_close_callback(std::function<void(const XmlNode&)>&& callback);
+
+ /**
+ * Called when a new XML element has been opened. We instanciate a new
+ * XmlNode and set it as our current node. The parent of this new node is
+ * the previous "current" node. We have all the element's attributes in
+ * this event.
+ *
+ * We spawn a stream_event with this node if this is a level-1 element.
+ */
+ void start_element(const XML_Char* name, const XML_Char** attribute);
+ /**
+ * Called when an XML element has been closed. We close the current_node,
+ * set our current_node as the parent of the current_node, and if that was
+ * a level-2 element we spawn a stanza_event with this node.
+ *
+ * And we then delete the stanza (and everything under it, its children,
+ * attribute, etc).
+ */
+ void end_element(const XML_Char* name);
+ /**
+ * Some inner or tail data has been parsed
+ */
+ void char_data(const XML_Char* data, const size_t len);
+ /**
+ * Calls all the stanza_callbacks one by one.
+ */
+ void stanza_event(const Stanza& stanza) const;
+ /**
+ * Calls all the stream_open_callbacks one by one. Note: the passed node is not
+ * closed yet.
+ */
+ void stream_open_event(const XmlNode& node) const;
+ /**
+ * Calls all the stream_close_callbacks one by one.
+ */
+ void stream_close_event(const XmlNode& node) const;
+
+private:
+ /**
+ * Init the XML parser and install the callbacks
+ */
+ void init_xml_parser();
+
+ /**
+ * Expat structure.
+ */
+ XML_Parser parser;
+ /**
+ * The current depth in the XML document
+ */
+ size_t level;
+ /**
+ * The deepest XML node opened but not yet closed (to which we are adding
+ * new children, inner or tail)
+ */
+ XmlNode* current_node;
+ /**
+ * The root node has no parent, so we keep it here: the XmppParser object
+ * is its owner.
+ */
+ std::unique_ptr<XmlNode> root;
+ /**
+ * A list of callbacks to be called on an *_event, receiving the
+ * concerned Stanza/XmlNode.
+ */
+ std::vector<std::function<void(const Stanza&)>> stanza_callbacks;
+ std::vector<std::function<void(const XmlNode&)>> stream_open_callbacks;
+ std::vector<std::function<void(const XmlNode&)>> stream_close_callbacks;
+};
+
+
diff --git a/src/xmpp/xmpp_stanza.cpp b/src/xmpp/xmpp_stanza.cpp
new file mode 100644
index 0000000..ac6ce9b
--- /dev/null
+++ b/src/xmpp/xmpp_stanza.cpp
@@ -0,0 +1,229 @@
+#include <xmpp/xmpp_stanza.hpp>
+
+#include <utils/encoding.hpp>
+#include <utils/split.hpp>
+
+#include <stdexcept>
+#include <iostream>
+#include <sstream>
+
+#include <string.h>
+
+std::string xml_escape(const std::string& data)
+{
+ std::string res;
+ res.reserve(data.size());
+ for (size_t pos = 0; pos != data.size(); ++pos)
+ {
+ switch(data[pos])
+ {
+ case '&':
+ res += "&amp;";
+ break;
+ case '<':
+ res += "&lt;";
+ break;
+ case '>':
+ res += "&gt;";
+ break;
+ case '\"':
+ res += "&quot;";
+ break;
+ case '\'':
+ res += "&apos;";
+ break;
+ default:
+ res += data[pos];
+ break;
+ }
+ }
+ return res;
+}
+
+std::string sanitize(const std::string& data, const std::string& encoding)
+{
+ if (utils::is_valid_utf8(data.data()))
+ return xml_escape(utils::remove_invalid_xml_chars(data));
+ else
+ return xml_escape(utils::remove_invalid_xml_chars(utils::convert_to_utf8(data, encoding.data())));
+}
+
+XmlNode::XmlNode(const std::string& name, XmlNode* parent):
+ parent(parent)
+{
+ // split the namespace and the name
+ auto n = name.rfind(":");
+ if (n == std::string::npos)
+ this->name = name;
+ else
+ {
+ this->name = name.substr(n+1);
+ this->attributes["xmlns"] = name.substr(0, n);
+ }
+}
+
+XmlNode::XmlNode(const std::string& name):
+ XmlNode(name, nullptr)
+{
+}
+
+void XmlNode::delete_all_children()
+{
+ this->children.clear();
+}
+
+void XmlNode::set_attribute(const std::string& name, const std::string& value)
+{
+ this->attributes[name] = value;
+}
+
+void XmlNode::set_tail(const std::string& data)
+{
+ this->tail = data;
+}
+
+void XmlNode::add_to_tail(const std::string& data)
+{
+ this->tail += data;
+}
+
+void XmlNode::set_inner(const std::string& data)
+{
+ this->inner = data;
+}
+
+void XmlNode::add_to_inner(const std::string& data)
+{
+ this->inner += data;
+}
+
+std::string XmlNode::get_inner() const
+{
+ return this->inner;
+}
+
+std::string XmlNode::get_tail() const
+{
+ return this->tail;
+}
+
+const XmlNode* XmlNode::get_child(const std::string& name, const std::string& xmlns) const
+{
+ for (const auto& child: this->children)
+ {
+ if (child->name == name && child->get_tag("xmlns") == xmlns)
+ return child.get();
+ }
+ return nullptr;
+}
+
+std::vector<const XmlNode*> XmlNode::get_children(const std::string& name, const std::string& xmlns) const
+{
+ std::vector<const XmlNode*> res;
+ for (const auto& child: this->children)
+ {
+ if (child->name == name && child->get_tag("xmlns") == xmlns)
+ res.push_back(child.get());
+ }
+ return res;
+}
+
+XmlNode* XmlNode::add_child(std::unique_ptr<XmlNode> child)
+{
+ child->parent = this;
+ auto ret = child.get();
+ this->children.push_back(std::move(child));
+ return ret;
+}
+
+XmlNode* XmlNode::add_child(XmlNode&& child)
+{
+ auto new_node = std::make_unique<XmlNode>(std::move(child));
+ return this->add_child(std::move(new_node));
+}
+
+XmlNode* XmlNode::add_child(const XmlNode& child)
+{
+ auto new_node = std::make_unique<XmlNode>(child);
+ return this->add_child(std::move(new_node));
+}
+
+XmlNode* XmlNode::get_last_child() const
+{
+ return this->children.back().get();
+}
+
+XmlNode* XmlNode::get_parent() const
+{
+ return this->parent;
+}
+
+void XmlNode::set_name(const std::string& name)
+{
+ this->name = name;
+}
+
+void XmlNode::set_name(std::string&& name)
+{
+ this->name = std::move(name);
+}
+
+const std::string XmlNode::get_name() const
+{
+ return this->name;
+}
+
+std::string XmlNode::to_string() const
+{
+ std::ostringstream res;
+ res << "<" << this->name;
+ for (const auto& it: this->attributes)
+ res << " " << it.first << "='" << sanitize(it.second) + "'";
+ if (!this->has_children() && this->inner.empty())
+ res << "/>";
+ else
+ {
+ res << ">" + sanitize(this->inner);
+ for (const auto& child: this->children)
+ res << child->to_string();
+ res << "</" << this->get_name() << ">";
+ }
+ res << sanitize(this->tail);
+ return res.str();
+}
+
+bool XmlNode::has_children() const
+{
+ return !this->children.empty();
+}
+
+const std::string& XmlNode::get_tag(const std::string& name) const
+{
+ try
+ {
+ const auto& value = this->attributes.at(name);
+ return value;
+ }
+ catch (const std::out_of_range& e)
+ {
+ static const std::string def{};
+ return def;
+ }
+}
+
+bool XmlNode::del_tag(const std::string& name)
+{
+ if (this->attributes.erase(name) != 0)
+ return true;
+ return false;
+}
+
+std::string& XmlNode::operator[](const std::string& name)
+{
+ return this->attributes[name];
+}
+
+std::ostream& operator<<(std::ostream& os, const XmlNode& node)
+{
+ return os << node.to_string();
+}
diff --git a/src/xmpp/xmpp_stanza.hpp b/src/xmpp/xmpp_stanza.hpp
new file mode 100644
index 0000000..f4b3948
--- /dev/null
+++ b/src/xmpp/xmpp_stanza.hpp
@@ -0,0 +1,160 @@
+#pragma once
+
+
+#include <map>
+#include <string>
+#include <vector>
+#include <memory>
+
+std::string xml_escape(const std::string& data);
+std::string xml_unescape(const std::string& data);
+std::string sanitize(const std::string& data, const std::string& encoding = "ISO-8859-1");
+
+/**
+ * Represent an XML node. It has
+ * - A parent XML node (in the case of the first-level nodes, the parent is
+ nullptr)
+ * - zero, one or more children XML nodes
+ * - A name
+ * - A map of attributes
+ * - inner data (text inside the node)
+ * - tail data (text just after the node)
+ */
+class XmlNode
+{
+public:
+ explicit XmlNode(const std::string& name, XmlNode* parent);
+ explicit XmlNode(const std::string& name);
+ /**
+ * The copy constructor does not copy the parent attribute. The children
+ * nodes are all copied recursively.
+ */
+ XmlNode(const XmlNode& node):
+ name(node.name),
+ parent(nullptr),
+ attributes(node.attributes),
+ children{},
+ inner(node.inner),
+ tail(node.tail)
+ {
+ for (const auto& child: node.children)
+ this->add_child(std::make_unique<XmlNode>(*child));
+ }
+
+ XmlNode(XmlNode&& node) = default;
+ XmlNode& operator=(const XmlNode&) = delete;
+ XmlNode& operator=(XmlNode&&) = delete;
+
+ ~XmlNode() = default;
+
+ void delete_all_children();
+ void set_attribute(const std::string& name, const std::string& value);
+ /**
+ * Set the content of the tail, that is the text just after this node
+ */
+ void set_tail(const std::string& data);
+ /**
+ * Append the given data to the content of the tail. This exists because
+ * the expat library may provide the complete text of an element in more
+ * than one call
+ */
+ void add_to_tail(const std::string& data);
+ /**
+ * Set the content of the inner, that is the text inside this node.
+ */
+ void set_inner(const std::string& data);
+ /**
+ * Append the given data to the content of the inner. For the reason
+ * described in add_to_tail comment.
+ */
+ void add_to_inner(const std::string& data);
+ /**
+ * Get the content of inner
+ */
+ std::string get_inner() const;
+ /**
+ * Get the content of the tail
+ */
+ std::string get_tail() const;
+ /**
+ * Get a pointer to the first child element with that name and that xml namespace
+ */
+ const XmlNode* get_child(const std::string& name, const std::string& xmlns) const;
+ /**
+ * Get a vector of all the children that have that name and that xml namespace.
+ */
+ std::vector<const XmlNode*> get_children(const std::string& name, const std::string& xmlns) const;
+ /**
+ * Add a node child to this node. Assign this node to the child’s parent.
+ * Returns a pointer to the newly added child.
+ */
+ XmlNode* add_child(std::unique_ptr<XmlNode> child);
+ XmlNode* add_child(XmlNode&& child);
+ XmlNode* add_child(const XmlNode& child);
+ /**
+ * Returns the last of the children. If the node doesn't have any child,
+ * the behaviour is undefined. The user should make sure this is the case
+ * by calling has_children() for example.
+ */
+ XmlNode* get_last_child() const;
+ XmlNode* get_parent() const;
+ void set_name(const std::string& name);
+ void set_name(std::string&& name);
+ const std::string get_name() const;
+ /**
+ * Serialize the stanza into a string
+ */
+ std::string to_string() const;
+ /**
+ * Whether or not this node has at least one child (if not, this is a leaf
+ * node)
+ */
+ bool has_children() const;
+ /**
+ * Gets the value for the given attribute, returns an empty string if the
+ * node as no such attribute.
+ */
+ const std::string& get_tag(const std::string& name) const;
+ /**
+ * Remove the attribute of the node. Does nothing if that attribute is not
+ * present. Returns true if the tag was removed, false if it was absent.
+ */
+ bool del_tag(const std::string& name);
+ /**
+ * Use this to set an attribute's value, like node["id"] = "12";
+ */
+ std::string& operator[](const std::string& name);
+
+private:
+ std::string name;
+ XmlNode* parent;
+ std::map<std::string, std::string> attributes;
+ std::vector<std::unique_ptr<XmlNode>> children;
+ std::string inner;
+ std::string tail;
+};
+
+std::ostream& operator<<(std::ostream& os, const XmlNode& node);
+
+/**
+ * An XMPP stanza is just an XML node of level 2 in the XMPP document (the
+ * level 1 ones are the <stream::stream/>, and the ones above 2 are just the
+ * content of the stanzas)
+ */
+using Stanza = XmlNode;
+
+class XmlSubNode: public XmlNode
+{
+public:
+ XmlSubNode(XmlNode& parent_ref, const std::string& name):
+ XmlNode(name),
+ parent_to_add(parent_ref)
+ {}
+
+ ~XmlSubNode()
+ {
+ this->parent_to_add.add_child(std::move(*this));
+ }
+private:
+ XmlNode& parent_to_add;
+}; \ No newline at end of file