diff options
Diffstat (limited to 'src/network')
-rw-r--r-- | src/network/credentials_manager.cpp | 136 | ||||
-rw-r--r-- | src/network/credentials_manager.hpp | 55 | ||||
-rw-r--r-- | src/network/dns_handler.cpp | 46 | ||||
-rw-r--r-- | src/network/dns_handler.hpp | 37 | ||||
-rw-r--r-- | src/network/dns_socket_handler.cpp | 43 | ||||
-rw-r--r-- | src/network/dns_socket_handler.hpp | 33 | ||||
-rw-r--r-- | src/network/poller.cpp | 238 | ||||
-rw-r--r-- | src/network/poller.hpp | 98 | ||||
-rw-r--r-- | src/network/resolver.cpp | 280 | ||||
-rw-r--r-- | src/network/resolver.hpp | 122 | ||||
-rw-r--r-- | src/network/socket_handler.hpp | 42 | ||||
-rw-r--r-- | src/network/tcp_client_socket_handler.cpp | 261 | ||||
-rw-r--r-- | src/network/tcp_client_socket_handler.hpp | 82 | ||||
-rw-r--r-- | src/network/tcp_server_socket.hpp | 69 | ||||
-rw-r--r-- | src/network/tcp_socket_handler.cpp | 360 | ||||
-rw-r--r-- | src/network/tcp_socket_handler.hpp | 238 | ||||
-rw-r--r-- | src/network/tls_policy.cpp | 48 | ||||
-rw-r--r-- | src/network/tls_policy.hpp | 28 |
18 files changed, 2216 insertions, 0 deletions
diff --git a/src/network/credentials_manager.cpp b/src/network/credentials_manager.cpp new file mode 100644 index 0000000..f93a366 --- /dev/null +++ b/src/network/credentials_manager.cpp @@ -0,0 +1,136 @@ +#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> + +/** + * 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, + const 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(cert); + } catch (const Botan::Decoding_Error& error) { + continue; + } + } + // Only use the first file that can successfully be read. + return true; + } + catch (const Botan::Stream_IO_Error& e) + { + log_debug(e.what()); + } + } + return false; +} + +void BasicCredentialsManager::load_certs() +{ + // Only load the certificates the first time + if (BasicCredentialsManager::certs_loaded) + return; + const std::string conf_path = Config::get("ca_file", ""); + std::vector<std::string> paths; + if (conf_path.empty()) + paths = default_cert_files; + else + paths.push_back(conf_path); + + if (BasicCredentialsManager::try_to_open_one_ca_bundle(paths)) + BasicCredentialsManager::certs_loaded = true; + else + log_warning("The CA could not be loaded, TLS negociation will probably fail."); +} + +std::vector<Botan::Certificate_Store*> BasicCredentialsManager::trusted_certificate_authorities(const std::string&, const std::string&) +{ + return {&this->certificate_store}; +} + +#endif diff --git a/src/network/credentials_manager.hpp b/src/network/credentials_manager.hpp new file mode 100644 index 0000000..e7c247d --- /dev/null +++ b/src/network/credentials_manager.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include "biboumi.h" + +#ifdef BOTAN_FOUND + +#include <botan/botan.h> +#include <botan/tls_client.h> + +class TCPSocketHandler; + +/** + * If the given cert isn’t valid, based on the given hostname + * and fingerprint, then throws the exception if it’s non-empty. + * + * Must be called after the standard (from Botan) way of + * checking the certificate, if we want to also accept certificates based + * on a trusted fingerprint. + */ +void check_tls_certificate(const std::vector<Botan::X509_Certificate>& certs, + const std::string& hostname, const std::string& trusted_fingerprint, + const std::exception_ptr& exc); + +class BasicCredentialsManager: public Botan::Credentials_Manager +{ +public: + BasicCredentialsManager(const TCPSocketHandler* const socket_handler); + + BasicCredentialsManager(BasicCredentialsManager&&) = delete; + BasicCredentialsManager(const BasicCredentialsManager&) = delete; + BasicCredentialsManager& operator=(const BasicCredentialsManager&) = delete; + BasicCredentialsManager& operator=(BasicCredentialsManager&&) = delete; + +#if BOTAN_VERSION_CODE < BOTAN_VERSION_CODE_FOR(1,11,34) + void verify_certificate_chain(const std::string& type, + const std::string& purported_hostname, + const std::vector<Botan::X509_Certificate>&) override final; +#endif + std::vector<Botan::Certificate_Store*> trusted_certificate_authorities(const std::string& type, + const std::string& context) override final; + void set_trusted_fingerprint(const std::string& fingerprint); + const std::string& get_trusted_fingerprint() const; + +private: + const TCPSocketHandler* const socket_handler; + + static bool try_to_open_one_ca_bundle(const std::vector<std::string>& paths); + static void load_certs(); + static Botan::Certificate_Store_In_Memory certificate_store; + static bool certs_loaded; + std::string trusted_fingerprint; +}; + +#endif //BOTAN_FOUND + diff --git a/src/network/dns_handler.cpp b/src/network/dns_handler.cpp new file mode 100644 index 0000000..7f0c96a --- /dev/null +++ b/src/network/dns_handler.cpp @@ -0,0 +1,46 @@ +#include <biboumi.h> +#ifdef UDNS_FOUND + +#include <network/dns_socket_handler.hpp> +#include <network/dns_handler.hpp> +#include <network/poller.hpp> + +#include <utils/timed_events.hpp> + +#include <udns.h> +#include <cerrno> +#include <cstring> + +class Resolver; + +using namespace std::string_literals; + +std::unique_ptr<DNSSocketHandler> DNSHandler::socket_handler{}; + +DNSHandler::DNSHandler(std::shared_ptr<Poller>& poller) +{ + dns_init(nullptr, 0); + const auto socket = dns_open(nullptr); + if (socket == -1) + throw std::runtime_error("Failed to initialize udns socket: "s + strerror(errno)); + + DNSHandler::socket_handler = std::make_unique<DNSSocketHandler>(poller, socket); +} + +void DNSHandler::destroy() +{ + DNSHandler::socket_handler.reset(nullptr); + dns_close(nullptr); +} + +void DNSHandler::watch() +{ + DNSHandler::socket_handler->watch(); +} + +void DNSHandler::unwatch() +{ + DNSHandler::socket_handler->unwatch(); +} + +#endif /* UDNS_FOUND */ diff --git a/src/network/dns_handler.hpp b/src/network/dns_handler.hpp new file mode 100644 index 0000000..c694452 --- /dev/null +++ b/src/network/dns_handler.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include <biboumi.h> +#ifdef UDNS_FOUND + +class Poller; + +#include <network/dns_socket_handler.hpp> + +#include <string> +#include <vector> +#include <memory> + +class DNSHandler +{ +public: + explicit DNSHandler(std::shared_ptr<Poller>& poller); + ~DNSHandler() = default; + + DNSHandler(const DNSHandler&) = delete; + DNSHandler(DNSHandler&&) = delete; + DNSHandler& operator=(const DNSHandler&) = delete; + DNSHandler& operator=(DNSHandler&&) = delete; + + void destroy(); + + static void watch(); + static void unwatch(); + +private: + /** + * Manager for the socket returned by udns, that we need to watch with the poller + */ + static std::unique_ptr<DNSSocketHandler> socket_handler; +}; + +#endif /* UDNS_FOUND */ diff --git a/src/network/dns_socket_handler.cpp b/src/network/dns_socket_handler.cpp new file mode 100644 index 0000000..5c286c4 --- /dev/null +++ b/src/network/dns_socket_handler.cpp @@ -0,0 +1,43 @@ +#include <biboumi.h> +#ifdef UDNS_FOUND + +#include <network/dns_socket_handler.hpp> +#include <network/dns_handler.hpp> +#include <network/poller.hpp> + +#include <udns.h> + +DNSSocketHandler::DNSSocketHandler(std::shared_ptr<Poller>& poller, + const socket_t socket): + SocketHandler(poller, socket) +{ + poller->add_socket_handler(this); +} + +DNSSocketHandler::~DNSSocketHandler() +{ + this->unwatch(); +} + +void DNSSocketHandler::on_recv() +{ + dns_ioevent(nullptr, 0); +} + +bool DNSSocketHandler::is_connected() const +{ + return true; +} + +void DNSSocketHandler::unwatch() +{ + if (this->poller->is_managing_socket(this->socket)) + this->poller->remove_socket_handler(this->socket); +} + +void DNSSocketHandler::watch() +{ + this->poller->add_socket_handler(this); +} + +#endif /* UDNS_FOUND */ diff --git a/src/network/dns_socket_handler.hpp b/src/network/dns_socket_handler.hpp new file mode 100644 index 0000000..6e83e87 --- /dev/null +++ b/src/network/dns_socket_handler.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include <biboumi.h> +#ifdef UDNS_FOUND + +#include <network/socket_handler.hpp> + +/** + * Manage the UDP socket provided by udns, we do not create, open or close the + * socket ourself: this is done by udns. We only watch it for readability + */ +class DNSSocketHandler: public SocketHandler +{ +public: + explicit DNSSocketHandler(std::shared_ptr<Poller>& poller, const socket_t socket); + ~DNSSocketHandler(); + DNSSocketHandler(const DNSSocketHandler&) = delete; + DNSSocketHandler(DNSSocketHandler&&) = delete; + DNSSocketHandler& operator=(const DNSSocketHandler&) = delete; + DNSSocketHandler& operator=(DNSSocketHandler&&) = delete; + + void on_recv() override final; + + /** + * Always true, see the comment for connect() + */ + bool is_connected() const override final; + + void watch(); + void unwatch(); +}; + +#endif // UDNS_FOUND diff --git a/src/network/poller.cpp b/src/network/poller.cpp new file mode 100644 index 0000000..0f02cc5 --- /dev/null +++ b/src/network/poller.cpp @@ -0,0 +1,238 @@ +#include <network/poller.hpp> +#include <logger/logger.hpp> +#include <utils/timed_events.hpp> + +#include <cassert> +#include <cerrno> +#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); + + int real_timeout = std::numeric_limits<int>::max(); + if (timeout.count() < real_timeout) // Just avoid any potential int overflow + real_timeout = static_cast<int>(timeout.count()); + const int nb_events = ::epoll_pwait(this->epfd, revents, max_events, real_timeout, + &empty_signal_set); + if (nb_events == -1) + { + if (errno == EINTR) + return 0; + log_error("epoll wait: ", strerror(errno)); + throw std::runtime_error("Epoll_wait failed"); + } + for (int i = 0; i < nb_events; ++i) + { + auto socket_handler = static_cast<SocketHandler*>(revents[i].data.ptr); + if (revents[i].events & EPOLLIN && socket_handler->is_connected()) + socket_handler->on_recv(); + else if (revents[i].events & EPOLLOUT && socket_handler->is_connected()) + socket_handler->on_send(); + else if (revents[i].events & EPOLLOUT) + socket_handler->connect(); + } + return nb_events; +#endif +} + +size_t Poller::size() const +{ + return this->socket_handlers.size(); +} + +bool Poller::is_managing_socket(const socket_t socket) const +{ + return (this->socket_handlers.find(socket) != this->socket_handlers.end()); +} diff --git a/src/network/poller.hpp b/src/network/poller.hpp new file mode 100644 index 0000000..3cc2710 --- /dev/null +++ b/src/network/poller.hpp @@ -0,0 +1,98 @@ +#pragma once + + +#include <network/socket_handler.hpp> + +#include <unordered_map> +#include <memory> +#include <chrono> + +#define POLL 1 +#define EPOLL 2 +#define KQUEUE 3 +#include <biboumi.h> +#ifndef POLLER + #define POLLER POLL +#endif + +#if POLLER == POLL + #include <poll.h> + #define MAX_POLL_FD_NUMBER 4096 +#elif POLLER == EPOLL + #include <sys/epoll.h> +#else + #error Invalid POLLER value +#endif + +/** + * We pass some SocketHandlers to this Poller, which uses + * poll/epoll/kqueue/select etc to wait for events on these SocketHandlers, + * and call the callbacks when event occurs. + * + * TODO: support these pollers: + * - kqueue(2) + */ + +class Poller +{ +public: + explicit Poller(); + ~Poller(); + Poller(const Poller&) = delete; + Poller(Poller&&) = delete; + Poller& operator=(const Poller&) = delete; + Poller& operator=(Poller&&) = delete; + /** + * Add a SocketHandler to be monitored by this Poller. All receive events + * are always automatically watched. + */ + void add_socket_handler(SocketHandler* socket_handler); + /** + * Remove (and stop managing) a SocketHandler, designated by the given socket_t. + */ + void remove_socket_handler(const socket_t socket); + /** + * Signal the poller that he needs to watch for send events for the given + * SocketHandler. + */ + void watch_send_events(SocketHandler* socket_handler); + /** + * Signal the poller that he needs to stop watching for send events for + * this SocketHandler. + */ + void stop_watching_send_events(SocketHandler* socket_handler); + /** + * Wait for all watched events, and call the SocketHandlers' callbacks + * when one is ready. Returns if nothing happened before the provided + * timeout. If the timeout is 0, it waits forever. If there is no + * watched event, returns -1 immediately, ignoring the timeout value. + * Otherwise, returns the number of event handled. If 0 is returned this + * means that we were interrupted by a signal, or the timeout occured. + */ + int poll(const std::chrono::milliseconds& timeout); + /** + * Returns the number of SocketHandlers managed by the poller. + */ + size_t size() const; + /** + * Whether the given socket is managed by the poller + */ + bool is_managing_socket(const socket_t socket) const; + +private: + /** + * A "list" of all the SocketHandlers that we manage, indexed by socket, + * because that's what is returned by select/poll/etc when an event + * occures. + */ + std::unordered_map<socket_t, SocketHandler*> socket_handlers; + +#if POLLER == POLL + struct pollfd fds[MAX_POLL_FD_NUMBER]; + nfds_t nfds; +#elif POLLER == EPOLL + int epfd; +#endif +}; + + diff --git a/src/network/resolver.cpp b/src/network/resolver.cpp new file mode 100644 index 0000000..ef54ba2 --- /dev/null +++ b/src/network/resolver.cpp @@ -0,0 +1,280 @@ +#include <network/dns_handler.hpp> +#include <utils/timed_events.hpp> +#include <network/resolver.hpp> +#include <cstring> +#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 = std::move(error_cb); + this->success_cb = std::move(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{}; + 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) + { + auto 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) + { + auto resolver = static_cast<Resolver*>(data); + resolver->on_hostname4_resolved(result); + resolver->after_resolved(); + std::free(result); + }; + + DNSHandler::watch(); + auto res = dns_submit_a4(nullptr, hostname.data(), 0, hostname4_resolved, this); + if (!res) + this->on_hostname4_resolved(nullptr); + res = dns_submit_a6(nullptr, hostname.data(), 0, hostname6_resolved, this); + if (!res) + this->on_hostname6_resolved(nullptr); + + this->start_timer(); +} + +void Resolver::start_timer() +{ + const auto timeout = dns_timeouts(nullptr, -1, 0); + if (timeout < 0) + return; + TimedEvent event(std::chrono::steady_clock::now() + std::chrono::seconds(timeout), [this]() { this->start_timer(); }, "DNS"); + TimedEventsManager::instance().add_event(std::move(event)); +} + +std::vector<std::string> Resolver::look_in_etc_hosts(const std::string &hostname) +{ + std::ifstream hosts("/etc/hosts"); + std::string line; + + std::vector<std::string> results; + while (std::getline(hosts, line)) + { + if (line.empty()) + continue; + + std::string ip; + std::istringstream line_stream(line); + line_stream >> ip; + if (ip.empty() || ip[0] == '#') + continue; + + std::string host; + while (line_stream >> host && !host.empty() && host[0] != '#') + { + if (hostname == host) + { + results.push_back(ip); + break; + } + } + } + return results; +} + +void Resolver::on_hostname4_resolved(dns_rr_a4 *result) +{ + this->resolved4 = true; + + const auto status = dns_status(nullptr); + + if (status >= 0 && result) + { + char buf[INET6_ADDRSTRLEN]; + + for (auto i = 0; i < result->dnsa4_nrr; ++i) + { + inet_ntop(AF_INET, &result->dnsa4_addr[i], buf, sizeof(buf)); + this->call_getaddrinfo(buf, this->port.data(), AI_NUMERICHOST); + } + } + else + { + const auto error = dns_error_messages.find(status); + if (error != end(dns_error_messages)) + this->error_msg = error->second; + } +} + +void Resolver::on_hostname6_resolved(dns_rr_a6 *result) +{ + this->resolved6 = true; + + const auto status = dns_status(nullptr); + + if (status >= 0 && result) + { + char buf[INET6_ADDRSTRLEN]; + for (auto i = 0; i < result->dnsa6_nrr; ++i) + { + inet_ntop(AF_INET6, &result->dnsa6_addr[i], buf, sizeof(buf)); + this->call_getaddrinfo(buf, this->port.data(), AI_NUMERICHOST); + } + } +} + +void Resolver::after_resolved() +{ + if (dns_active(nullptr) == 0) + DNSHandler::unwatch(); + + if (this->resolved6 && this->resolved4) + this->on_resolved(); +} + +void Resolver::on_resolved() +{ + this->resolved = true; + this->resolving = false; + if (!this->addr) + { + if (this->error_cb) + this->error_cb(this->error_msg.data()); + } + else + { + if (this->success_cb) + this->success_cb(this->addr.get()); + } +} + +#else // ifdef UDNS_FOUND + +void Resolver::start_resolving(const std::string& hostname, const std::string& port) +{ + // If the resolution fails, the addr will be unset + this->addr.reset(nullptr); + + const auto res = this->call_getaddrinfo(hostname.data(), port.data(), 0); + + this->resolved = true; + + if (res != 0) + { + this->error_msg = gai_strerror(res); + if (this->error_cb) + this->error_cb(this->error_msg.data()); + } + else + { + if (this->success_cb) + this->success_cb(this->addr.get()); + } +} +#endif // ifdef UDNS_FOUND + +std::string addr_to_string(const struct addrinfo* rp) +{ + char buf[INET6_ADDRSTRLEN]; + if (rp->ai_family == AF_INET) + return ::inet_ntop(rp->ai_family, + &reinterpret_cast<sockaddr_in*>(rp->ai_addr)->sin_addr, + buf, sizeof(buf)); + else if (rp->ai_family == AF_INET6) + return ::inet_ntop(rp->ai_family, + &reinterpret_cast<sockaddr_in6*>(rp->ai_addr)->sin6_addr, + buf, sizeof(buf)); + return {}; +} diff --git a/src/network/resolver.hpp b/src/network/resolver.hpp new file mode 100644 index 0000000..f65ff86 --- /dev/null +++ b/src/network/resolver.hpp @@ -0,0 +1,122 @@ +#pragma once + +#include "biboumi.h" + +#include <functional> +#include <vector> +#include <memory> +#include <string> + +#include <sys/types.h> +#include <sys/socket.h> +#include <netdb.h> +#ifdef UDNS_FOUND +# include <udns.h> +#endif + +class AddrinfoDeleter +{ + public: + void operator()(struct addrinfo* addr) + { + freeaddrinfo(addr); + } +}; + + +class Resolver +{ +public: + + using ErrorCallbackType = std::function<void(const char*)>; + using SuccessCallbackType = std::function<void(const struct addrinfo*)>; + + Resolver(); + ~Resolver() = default; + Resolver(const Resolver&) = delete; + Resolver(Resolver&&) = delete; + Resolver& operator=(const Resolver&) = delete; + Resolver& operator=(Resolver&&) = delete; + + bool is_resolving() const + { +#ifdef UDNS_FOUND + return this->resolving; +#else + return false; +#endif + } + + bool is_resolved() const + { + return this->resolved; + } + + const auto& get_result() const + { + return this->addr; + } + std::string get_error_message() const + { + return this->error_msg; + } + + void clear() + { +#ifdef UDNS_FOUND + this->resolved6 = false; + this->resolved4 = false; + this->resolving = false; + this->port.clear(); +#endif + this->resolved = false; + this->addr.reset(); + this->error_msg.clear(); + } + + void resolve(const std::string& hostname, const std::string& port, + SuccessCallbackType success_cb, ErrorCallbackType error_cb); + +private: + void start_resolving(const std::string& hostname, const std::string& port); + std::vector<std::string> look_in_etc_hosts(const std::string& hostname); + /** + * Call getaddrinfo() on the given hostname or IP, and append the result + * to our internal addrinfo list. Return getaddrinfo()’s return value. + */ + int call_getaddrinfo(const char* name, const char* port, int flags); + +#ifdef UDNS_FOUND + void on_hostname4_resolved(dns_rr_a4 *result); + void on_hostname6_resolved(dns_rr_a6 *result); + /** + * Called after one record (4 or 6) has been resolved. + */ + void after_resolved(); + + void start_timer(); + + void on_resolved(); + + bool resolved4; + bool resolved6; + + bool resolving; + + std::string port; + +#endif + /** + * Tells if we finished the resolution process. It doesn't indicate if it + * was successful (it is true even if the result is an error). + */ + bool resolved; + std::string error_msg; + + std::unique_ptr<struct addrinfo, AddrinfoDeleter> addr; + + ErrorCallbackType error_cb; + SuccessCallbackType success_cb; +}; + +std::string addr_to_string(const struct addrinfo* rp); diff --git a/src/network/socket_handler.hpp b/src/network/socket_handler.hpp new file mode 100644 index 0000000..181a6c0 --- /dev/null +++ b/src/network/socket_handler.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include <biboumi.h> +#include <memory> + +class Poller; + +using socket_t = int; + +class SocketHandler +{ +public: + explicit SocketHandler(std::shared_ptr<Poller>& poller, const socket_t socket): + poller(poller), + socket(socket) + {} + virtual ~SocketHandler() = default; + SocketHandler(const SocketHandler&) = delete; + SocketHandler(SocketHandler&&) = delete; + SocketHandler& operator=(const SocketHandler&) = delete; + SocketHandler& operator=(SocketHandler&&) = delete; + + virtual void on_recv() {} + virtual void on_send() {} + virtual void connect() {} + virtual bool is_connected() const = 0; + + socket_t get_socket() const + { return this->socket; } + +protected: + /** + * A pointer to the poller that manages us, because we need to communicate + * with it. + */ + std::shared_ptr<Poller> poller; + /** + * The handled socket. + */ + socket_t socket; +}; + diff --git a/src/network/tcp_client_socket_handler.cpp b/src/network/tcp_client_socket_handler.cpp new file mode 100644 index 0000000..35f2446 --- /dev/null +++ b/src/network/tcp_client_socket_handler.cpp @@ -0,0 +1,261 @@ +#include <network/tcp_client_socket_handler.hpp> +#include <utils/timed_events.hpp> +#include <utils/scopeguard.hpp> +#include <network/poller.hpp> + +#include <logger/logger.hpp> + +#include <cstring> +#include <unistd.h> +#include <fcntl.h> + +using namespace std::string_literals; + +TCPClientSocketHandler::TCPClientSocketHandler(std::shared_ptr<Poller>& poller): + TCPSocketHandler(poller), + hostname_resolution_failed(false), + connected(false), + connecting(false) +{} + +TCPClientSocketHandler::~TCPClientSocketHandler() +{ + this->close(); +} + +void TCPClientSocketHandler::init_socket(const struct addrinfo* rp) +{ + if (this->socket != -1) + ::close(this->socket); + if ((this->socket = ::socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol)) == -1) + throw std::runtime_error("Could not create socket: "s + std::strerror(errno)); + // Bind the socket to a specific address, if specified + if (!this->bind_addr.empty()) + { + // Convert the address from string format to a sockaddr that can be + // used in bind() + struct addrinfo* result; + struct addrinfo hints{}; + memset(&hints, 0, sizeof(hints)); + hints.ai_flags = AI_NUMERICHOST; + hints.ai_family = AF_UNSPEC; + int err = ::getaddrinfo(this->bind_addr.data(), nullptr, &hints, &result); + if (err != 0 || !result) + log_error("Failed to bind socket to ", this->bind_addr, ": ", + gai_strerror(err)); + else + { + utils::ScopeGuard sg([result](){ freeaddrinfo(result); }); + struct addrinfo* rp; + for (rp = result; rp; rp = rp->ai_next) + { + if ((::bind(this->socket, + reinterpret_cast<const struct sockaddr*>(rp->ai_addr), + rp->ai_addrlen)) == 0) + break; + } + if (!rp) + log_error("Failed to bind socket to ", this->bind_addr, ": ", + strerror(errno)); + else + log_info("Socket successfully bound to ", this->bind_addr); + } + } + int optval = 1; + if (::setsockopt(this->socket, SOL_SOCKET, SO_KEEPALIVE, &optval, sizeof(optval)) == -1) + log_warning("Failed to enable TCP keepalive on socket: ", strerror(errno)); + // Set the socket on non-blocking mode. This is useful to receive a EAGAIN + // error when connect() would block, to not block the whole process if a + // remote is not responsive. + const int existing_flags = ::fcntl(this->socket, F_GETFL, 0); + if ((existing_flags == -1) || + (::fcntl(this->socket, F_SETFL, existing_flags | O_NONBLOCK) == -1)) + throw std::runtime_error("Could not initialize socket: "s + std::strerror(errno)); +} + +void TCPClientSocketHandler::connect(const std::string& address, const std::string& port, const bool tls) +{ + this->address = address; + this->port = port; + this->use_tls = tls; + + struct addrinfo* addr_res; + + if (!this->connecting) + { + // Get the addrinfo from getaddrinfo (or using udns), only if + // this is the first call of this function. + if (!this->resolver.is_resolved()) + { + log_info("Trying to connect to ", address, ":", port); + // Start the asynchronous process of resolving the hostname. Once + // the addresses have been found and `resolved` has been set to true + // (but connecting will still be false), TCPClientSocketHandler::connect() + // needs to be called, again. + this->resolver.resolve(address, port, + [this](const struct addrinfo*) + { + log_debug("Resolution success, calling connect() again"); + this->connect(); + }, + [this](const char*) + { + log_debug("Resolution failed, calling connect() again"); + this->connect(); + }); + return; + } + else + { + // The DNS resolver resolved the hostname and the available addresses + // where saved in the addrinfo linked list. Now, just use + // this list to try to connect. + addr_res = this->resolver.get_result().get(); + if (!addr_res) + { + this->hostname_resolution_failed = true; + const auto msg = this->resolver.get_error_message(); + this->close(); + this->on_connection_failed(msg); + return ; + } + } + } + else + { // This function is called again, use the saved addrinfo structure, + // instead of re-doing the whole getaddrinfo process. + addr_res = &this->addrinfo; + } + + for (struct addrinfo* rp = addr_res; rp; rp = rp->ai_next) + { + if (!this->connecting) + { + try { + this->init_socket(rp); + } + catch (const std::runtime_error& error) { + log_error("Failed to init socket: ", error.what()); + break; + } + } + + this->display_resolved_ip(rp); + + if (::connect(this->socket, rp->ai_addr, rp->ai_addrlen) == 0 + || errno == EISCONN) + { + log_info("Connection success."); + TimedEventsManager::instance().cancel("connection_timeout"s + + std::to_string(this->socket)); + this->poller->add_socket_handler(this); + this->connected = true; + this->connecting = false; +#ifdef BOTAN_FOUND + if (this->use_tls) + this->start_tls(this->address, this->port); +#endif + this->connection_date = std::chrono::system_clock::now(); + + // Get our local TCP port and store it + this->local_port = static_cast<uint16_t>(-1); + if (rp->ai_family == AF_INET6) + { + struct sockaddr_in6 a{}; + socklen_t l = sizeof(a); + if (::getsockname(this->socket, (struct sockaddr*)&a, &l) != -1) + this->local_port = ntohs(a.sin6_port); + } + else if (rp->ai_family == AF_INET) + { + struct sockaddr_in a{}; + socklen_t l = sizeof(a); + if (::getsockname(this->socket, (struct sockaddr*)&a, &l) != -1) + this->local_port = ntohs(a.sin_port); + } + + log_debug("Local port: ", this->local_port, ", and remote port: ", this->port); + + this->on_connected(); + return ; + } + else if (errno == EINPROGRESS || errno == EALREADY) + { // retry this process later, when the socket + // is ready to be written on. + this->connecting = true; + this->poller->add_socket_handler(this); + this->poller->watch_send_events(this); + // Save the addrinfo structure, to use it on the next call + this->ai_addrlen = rp->ai_addrlen; + memcpy(&this->ai_addr, rp->ai_addr, this->ai_addrlen); + memcpy(&this->addrinfo, rp, sizeof(struct addrinfo)); + this->addrinfo.ai_addr = reinterpret_cast<struct sockaddr*>(&this->ai_addr); + this->addrinfo.ai_next = nullptr; + // If the connection has not succeeded or failed in 5s, we consider + // it to have failed + TimedEventsManager::instance().add_event( + TimedEvent(std::chrono::steady_clock::now() + 5s, + std::bind(&TCPClientSocketHandler::on_connection_timeout, this), + "connection_timeout"s + std::to_string(this->socket))); + return ; + } + log_info("Connection failed:", std::strerror(errno)); + } + log_error("All connection attempts failed."); + this->close(); + this->on_connection_failed(std::strerror(errno)); + return ; +} + +void TCPClientSocketHandler::on_connection_timeout() +{ + this->close(); + this->on_connection_failed("connection timed out"); +} + +void TCPClientSocketHandler::connect() +{ + this->connect(this->address, this->port, this->use_tls); +} + +void TCPClientSocketHandler::close() +{ + TimedEventsManager::instance().cancel("connection_timeout"s + + std::to_string(this->socket)); + + TCPSocketHandler::close(); + + this->connected = false; + this->connecting = false; + this->port.clear(); + this->resolver.clear(); +} + +void TCPClientSocketHandler::display_resolved_ip(struct addrinfo* rp) const +{ + if (rp->ai_family == AF_INET) + log_debug("Trying IPv4 address ", addr_to_string(rp)); + else if (rp->ai_family == AF_INET6) + log_debug("Trying IPv6 address ", addr_to_string(rp)); +} + +bool TCPClientSocketHandler::is_connected() const +{ + return this->connected; +} + +bool TCPClientSocketHandler::is_connecting() const +{ + return this->connecting || this->resolver.is_resolving(); +} + +std::string TCPClientSocketHandler::get_port() const +{ + return this->port; +} + +bool TCPClientSocketHandler::match_port_pairt(const uint16_t local, const uint16_t remote) const +{ + const auto remote_port = static_cast<uint16_t>(std::stoi(this->port)); + return this->is_connected() && local == this->local_port && remote == remote_port; +} diff --git a/src/network/tcp_client_socket_handler.hpp b/src/network/tcp_client_socket_handler.hpp new file mode 100644 index 0000000..74caca9 --- /dev/null +++ b/src/network/tcp_client_socket_handler.hpp @@ -0,0 +1,82 @@ +#pragma once + +#include <network/tcp_socket_handler.hpp> + +class TCPClientSocketHandler: public TCPSocketHandler +{ + public: + TCPClientSocketHandler(std::shared_ptr<Poller>& poller); + ~TCPClientSocketHandler(); + /** + * Connect to the remote server, and call on_connected() if this + * succeeds. If tls is true, we set use_tls to true and will also call + * start_tls() when the connection succeeds. + */ + void connect(const std::string& address, const std::string& port, const bool tls); + void connect() override final; + /** + * Called by a TimedEvent, when the connection did not succeed or fail + * after a given time. + */ + void on_connection_timeout(); + /** + * Called when the connection is successful. + */ + virtual void on_connected() = 0; + bool is_connected() const override; + bool is_connecting() const override; + + std::string get_port() const; + + void close() override final; + std::chrono::system_clock::time_point connection_date; + + /** + * Whether or not this connection is using the two given TCP ports. + */ + bool match_port_pairt(const uint16_t local, const uint16_t remote) const; + + protected: + bool hostname_resolution_failed; + /** + * Address to bind the socket to, before calling connect(). + * If empty, it’s equivalent to binding to INADDR_ANY. + */ + std::string bind_addr; + /** + * Display the resolved IP, just for information purpose. + */ + void display_resolved_ip(struct addrinfo* rp) const; + private: + /** + * Initialize the socket with the parameters contained in the given + * addrinfo structure. + */ + void init_socket(const struct addrinfo* rp); + /** + * DNS resolver + */ + Resolver resolver; + /** + * Keep the details of the addrinfo returned by the resolver that + * triggered a EINPROGRESS error when connect()ing to it, to reuse it + * directly when connect() is called again. + */ + struct addrinfo addrinfo{}; + struct sockaddr_in6 ai_addr{}; + socklen_t ai_addrlen{}; + + /** + * Hostname we are connected/connecting to + */ + std::string address; + /** + * Port we are connected/connecting to + */ + std::string port; + + uint16_t local_port{}; + + bool connected; + bool connecting; +}; diff --git a/src/network/tcp_server_socket.hpp b/src/network/tcp_server_socket.hpp new file mode 100644 index 0000000..652b773 --- /dev/null +++ b/src/network/tcp_server_socket.hpp @@ -0,0 +1,69 @@ +#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> + +template <typename RemoteSocketType> +class TcpSocketServer: public SocketHandler +{ + public: + TcpSocketServer(std::shared_ptr<Poller>& poller, const uint16_t port): + SocketHandler(poller, -1) + { + if ((this->socket = ::socket(AF_INET6, SOCK_STREAM, 0)) == -1) + throw std::runtime_error(std::string{"Could not create socket: "} + std::strerror(errno)); + + int opt = 1; + if (::setsockopt(this->socket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) + throw std::runtime_error(std::string{"Failed to set socket option: "} + std::strerror(errno)); + + struct sockaddr_in6 addr{}; + addr.sin6_family = AF_INET6; + addr.sin6_port = htons(port); + addr.sin6_addr = IN6ADDR_ANY_INIT; + if ((::bind(this->socket, (const struct sockaddr*)&addr, sizeof(addr))) == -1) + { // If we can’t listen on this port, we just give up, but this is not fatal. + log_warning("Failed to bind on port ", std::to_string(port), ": ", std::strerror(errno)); + return; + } + + if ((::listen(this->socket, 10)) == -1) + throw std::runtime_error("listen() failed"); + + this->accept(); + } + ~TcpSocketServer() = default; + + void on_recv() override + { + // Accept a RemoteSocketType + int socket = ::accept(this->socket, nullptr, nullptr); + + auto client = std::make_unique<RemoteSocketType>(poller, socket, *this); + this->poller->add_socket_handler(client.get()); + this->sockets.push_back(std::move(client)); + } + + protected: + std::vector<std::unique_ptr<RemoteSocketType>> sockets; + + private: + void accept() + { + this->poller->add_socket_handler(this); + } + bool is_connected() const override + { + return true; + } +}; diff --git a/src/network/tcp_socket_handler.cpp b/src/network/tcp_socket_handler.cpp new file mode 100644 index 0000000..1049375 --- /dev/null +++ b/src/network/tcp_socket_handler.cpp @@ -0,0 +1,360 @@ +#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 <cerrno> +#include <cstring> + +#ifdef BOTAN_FOUND +# include <botan/hex.h> +# include <botan/tls_exceptn.h> +# include <config/config.hpp> +# include <utils/dirname.hpp> + +namespace +{ + Botan::AutoSeeded_RNG& get_rng() + { + static Botan::AutoSeeded_RNG rng{}; + return rng; + } + 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; + + +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{}; + msg.msg_iov = msg_iov; + msg.msg_iovlen = 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_string) +{ + auto port = std::min(std::stoul(port_string), static_cast<unsigned long>(std::numeric_limits<uint16_t>::max())); + Botan::TLS::Server_Information server_info(address, "irc", static_cast<uint16_t>(port)); + auto policy_directory = Config::get("policy_directory", utils::dirname(Config::get_filename())); + if (!policy_directory.empty() && policy_directory[policy_directory.size()-1] != '/') + policy_directory += '/'; + this->policy.load(policy_directory + "policy.txt"); + this->policy.load(policy_directory + address + ".policy.txt"); + 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, this->policy, + get_rng(), server_info, Botan::TLS::Protocol_Version::latest_tls_version()); +} + +void TCPSocketHandler::tls_recv() +{ + static constexpr size_t buf_size = 4096; + Botan::byte recv_buf[buf_size]; + + const ssize_t size = this->do_recv(recv_buf, buf_size); + if (size > 0) + { + const bool was_active = this->tls->is_active(); + try { + this->tls->received_data(recv_buf, static_cast<size_t>(size)); + } catch (const Botan::TLS::TLS_Exception& e) { + // May happen if the server sends malformed TLS data (buggy server, + // or more probably we are just connected to a server that sends + // plain-text) + this->on_connection_close("TLS error: "s + e.what()); + this->close(); + return ; + } + if (!was_active && this->tls->is_active()) + this->on_tls_activated(); + } +} + +void TCPSocketHandler::tls_send(std::string&& data) +{ + // We may not be connected yet, or the tls session has + // not yet been negociated + if (this->tls && this->tls->is_active()) + { + const bool was_active = this->tls->is_active(); + if (!this->pre_buf.empty()) + { + this->tls->send(this->pre_buf.data(), this->pre_buf.size()); + this->pre_buf.clear(); + } + if (!data.empty()) + this->tls->send(reinterpret_cast<const Botan::byte*>(data.data()), + data.size()); + if (!was_active && this->tls->is_active()) + this->on_tls_activated(); + } + else + this->pre_buf.insert(this->pre_buf.end(), + std::make_move_iterator(data.begin()), + std::make_move_iterator(data.end())); +} + +void TCPSocketHandler::tls_record_received(uint64_t, const Botan::byte *data, size_t size) +{ + this->in_buf += std::string(reinterpret_cast<const char*>(data), + size); + if (!this->in_buf.empty()) + this->parse_in_buffer(size); +} + +void TCPSocketHandler::tls_emit_data(const Botan::byte *data, size_t size) +{ + this->raw_send(std::string(reinterpret_cast<const char*>(data), size)); +} + +void TCPSocketHandler::tls_alert(Botan::TLS::Alert alert) +{ + log_debug("tls_alert: ", alert.type_string()); +} + +bool TCPSocketHandler::tls_session_established(const Botan::TLS::Session& session) +{ + log_debug("Handshake with ", session.server_info().hostname(), " complete.", + " Version: ", session.version().to_string(), + " using ", session.ciphersuite().to_string()); + if (!session.session_id().empty()) + log_debug("Session ID ", Botan::hex_encode(session.session_id())); + if (!session.session_ticket().empty()) + log_debug("Session ticket ", Botan::hex_encode(session.session_ticket())); + return true; +} + +#if BOTAN_VERSION_CODE >= BOTAN_VERSION_CODE_FOR(1,11,34) +void TCPSocketHandler::tls_verify_cert_chain(const std::vector<Botan::X509_Certificate>& cert_chain, + const std::vector<std::shared_ptr<const Botan::OCSP::Response>>& ocsp_responses, + const std::vector<Botan::Certificate_Store*>& trusted_roots, + Botan::Usage_Type usage, const std::string& hostname, + const Botan::TLS::Policy& policy) +{ + log_debug("Checking remote certificate for hostname ", hostname); + try + { + Botan::TLS::Callbacks::tls_verify_cert_chain(cert_chain, ocsp_responses, trusted_roots, usage, hostname, policy); + log_debug("Certificate is valid"); + } + catch (const std::exception& tls_exception) + { + log_warning("TLS certificate check failed: ", tls_exception.what()); + std::exception_ptr exception_ptr{}; + if (this->abort_on_invalid_cert()) + exception_ptr = std::current_exception(); + + check_tls_certificate(cert_chain, hostname, this->credential_manager.get_trusted_fingerprint(), exception_ptr); + } +} +#endif + +void TCPSocketHandler::on_tls_activated() +{ + this->send_data({}); +} + +#endif // BOTAN_FOUND diff --git a/src/network/tcp_socket_handler.hpp b/src/network/tcp_socket_handler.hpp new file mode 100644 index 0000000..f68698e --- /dev/null +++ b/src/network/tcp_socket_handler.hpp @@ -0,0 +1,238 @@ +#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> +# include <network/tls_policy.hpp> + +# 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: + BiboumiTLSPolicy policy; + /** + * We use a unique_ptr because we may not want to create the object at + * all. The Botan::TLS::Client object generates a handshake message and + * calls the output_fn callback with it as soon as it is created. + * Therefore, we do not want to create it if we do not intend to send any + * TLS-encrypted message. We create the object only when needed (for + * example after we have negociated a TLS session using a STARTTLS + * message, or stuf like that). + * + * See start_tls for the method where this object is created. + */ + std::unique_ptr<Botan::TLS::Client> tls; + /** + * An additional buffer to keep data that the user wants to send, but + * cannot because the handshake is not done. + */ + std::vector<Botan::byte> pre_buf; +#endif // BOTAN_FOUND +}; diff --git a/src/network/tls_policy.cpp b/src/network/tls_policy.cpp new file mode 100644 index 0000000..5439397 --- /dev/null +++ b/src/network/tls_policy.cpp @@ -0,0 +1,48 @@ +#include "biboumi.h" + +#ifdef BOTAN_FOUND + +#include <fstream> + +#include <utils/tolower.hpp> + +#include <network/tls_policy.hpp> +#include <logger/logger.hpp> + +bool BiboumiTLSPolicy::load(const std::string& filename) +{ + std::ifstream is(filename.data()); + if (is) + { + try { + this->load(is); + log_info("Successfully loaded policy file: ", filename); + return true; + } catch (const Botan::Exception& e) { + log_error("Failed to parse policy_file ", filename, ": ", e.what()); + return false; + } + } + log_info("Could not open policy file: ", filename); + return false; +} + +void BiboumiTLSPolicy::load(std::istream& is) +{ + const auto dict = Botan::read_cfg(is); + for (const auto& pair: dict) + { + // Workaround for options that are not overridden in Botan::TLS::Text_Policy + if (pair.first == "require_cert_revocation_info") + this->req_cert_revocation_info = !(pair.second == "0" || utils::tolower(pair.second) == "false"); + else + this->set(pair.first, pair.second); + } +} + +bool BiboumiTLSPolicy::require_cert_revocation_info() const +{ + return this->req_cert_revocation_info; +} + +#endif diff --git a/src/network/tls_policy.hpp b/src/network/tls_policy.hpp new file mode 100644 index 0000000..29fd2b3 --- /dev/null +++ b/src/network/tls_policy.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "biboumi.h" + +#ifdef BOTAN_FOUND + +#include <botan/tls_policy.h> + +class BiboumiTLSPolicy: public Botan::TLS::Text_Policy +{ +public: + BiboumiTLSPolicy(): + Botan::TLS::Text_Policy({}) + {} + bool load(const std::string& filename); + void load(std::istream& iss); + + BiboumiTLSPolicy(const BiboumiTLSPolicy &) = delete; + BiboumiTLSPolicy(BiboumiTLSPolicy &&) = delete; + BiboumiTLSPolicy &operator=(const BiboumiTLSPolicy &) = delete; + BiboumiTLSPolicy &operator=(BiboumiTLSPolicy &&) = delete; + + bool require_cert_revocation_info() const override; +protected: + bool req_cert_revocation_info{true}; +}; + +#endif |