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