~singpolyma/biboumi

23f32ba39ebe5e9bbdfc4dd00d9914c0f0447ef4 — Florent Le Coz 8 years ago fa07130
Implement TLS support using Botan

For now, it tries two TLS ports and then connects to the non-tls port.  In
the future we would like the user to be able to configure that.

fix #2435
M CMakeLists.txt => CMakeLists.txt +8 -0
@@ 21,6 21,7 @@ find_package(Iconv REQUIRED)
find_package(Libuuid REQUIRED)
find_package(Libidn)
find_package(SystemdDaemon)
find_package(Botan)

# To be able to include the config.h file generated by cmake
include_directories("${CMAKE_CURRENT_SOURCE_DIR}/src/")


@@ 37,6 38,10 @@ if(SYSTEMDDAEMON_FOUND)
  include_directories(${SYSTEMDDAEMON_INCLUDE_DIRS})
endif()

if(BOTAN_FOUND)
  include_directories(${BOTAN_INCLUDE_DIRS})
endif()

set(POLLER_DOCSTRING "Choose the poller between POLL and EPOLL (Linux-only)")
if(${CMAKE_SYSTEM_NAME} MATCHES "Linux")
 set(POLLER "EPOLL" CACHE STRING ${POLLER_DOCSTRING})


@@ 95,6 100,9 @@ file(GLOB source_network
  src/network/*.[hc]pp)
add_library(network STATIC ${source_network})
target_link_libraries(network logger)
if(BOTAN_FOUND)
  target_link_libraries(network ${BOTAN_LIBRARIES})
endif()

#
## irclib

A cmake/Modules/FindBotan.cmake => cmake/Modules/FindBotan.cmake +34 -0
@@ 0,0 1,34 @@
# - Find botan
# Find the botan cryptographic library
#
# This module defines the following variables:
#   BOTAN_FOUND  -  True if library and include directory are found
# If set to TRUE, the following are also defined:
#   BOTAN_INCLUDE_DIRS  -  The directory where to find the header file
#   BOTAN_LIBRARIES  -  Where to find the library file
#
# For conveniance, these variables are also set. They have the same values
# than the variables above.  The user can thus choose his/her prefered way
# to write them.
#   BOTAN_LIBRARY
#   BOTAN_INCLUDE_DIR
#
# This file is in the public domain

find_path(BOTAN_INCLUDE_DIRS NAMES botan/botan.h
  DOC "The botan include directory")

find_library(BOTAN_LIBRARIES NAMES botan
  DOC "The botan library")

# Use some standard module to handle the QUIETLY and REQUIRED arguments, and
# set BOTAN_FOUND to TRUE if these two variables are set.
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Botan REQUIRED_VARS BOTAN_LIBRARIES BOTAN_INCLUDE_DIRS)

if(BOTAN_FOUND)
  set(BOTAN_LIBRARY ${BOTAN_LIBRARIES})
  set(BOTAN_INCLUDE_DIR ${BOTAN_INCLUDE_DIRS})
endif()

mark_as_advanced(BOTAN_INCLUDE_DIRS BOTAN_LIBRARIES)

M src/config.h.cmake => src/config.h.cmake +1 -0
@@ 2,3 2,4 @@
#cmakedefine LIBIDN_FOUND
#cmakedefine SYSTEMDDAEMON_FOUND
#cmakedefine POLLER ${POLLER}
#cmakedefine BOTAN_FOUND
\ No newline at end of file

M src/irc/irc_client.cpp => src/irc/irc_client.cpp +27 -9
@@ 14,6 14,8 @@
#include <chrono>
#include <string>

#include "config.h"

using namespace std::string_literals;
using namespace std::chrono_literals;



@@ 35,6 37,13 @@ IrcClient::IrcClient(std::shared_ptr<Poller> poller, const std::string& hostname
                              "alive without having to join a real channel of that server. "
                              "To disconnect from the IRC server, leave this room and all "
                              "other IRC channels of that server.";
  // TODO: get the values from the preferences of the user, and only use the
  // list of default ports if the user didn't specify anything
  this->ports_to_try.emplace("6667", false); // standard non-encrypted port
#ifdef BOTAN_FOUND
  this->ports_to_try.emplace("6670", true);  // non-standard but I want it for some servers
  this->ports_to_try.emplace("6697", true);  // standard encrypted port
#endif // BOTAN_FOUND
}

IrcClient::~IrcClient()


@@ 48,29 57,39 @@ void IrcClient::start()
{
  if (this->connected || this->connecting)
    return ;
  std::string port;
  bool tls;
  std::tie(port, tls) = this->ports_to_try.top();
  this->ports_to_try.pop();
  this->bridge->send_xmpp_message(this->hostname, "", "Connecting to "s +
                                  this->hostname + ":" + "6667");
  this->connect(this->hostname, "6667");
                                  this->hostname + ":" + port + " (" +
                                  (tls ? "encrypted" : "not encrypted") + ")");
  this->connect(this->hostname, port, tls);
}

void IrcClient::on_connection_failed(const std::string& reason)
{
  this->bridge->send_xmpp_message(this->hostname, "",
                                  "Connection failed: "s + reason);
  // Send an error message for all room that the user wanted to join
  for (const std::string& channel: this->channels_to_join)
  if (this->ports_to_try.empty())
    {
      Iid iid(channel + "%" + this->hostname);
      this->bridge->send_join_failed(iid, this->current_nick,
                                     "cancel", "item-not-found", reason);
      // Send an error message for all room that the user wanted to join
      for (const std::string& channel: this->channels_to_join)
        {
          Iid iid(channel + "%" + this->hostname);
          this->bridge->send_join_failed(iid, this->current_nick,
                                         "cancel", "item-not-found", reason);
        }
    }
  else                          // try the next port
    this->start();
}

void IrcClient::on_connected()
{
  this->send_nick_command(this->username);
  this->send_user_command(this->username, this->username);
  this->send_gateway_message("Connected to IRC server.");
  this->send_gateway_message("Connected to IRC server"s + (this->use_tls ? " (encrypted)": "") + ".");
  this->send_pending_data();
}



@@ 326,7 345,6 @@ void IrcClient::set_and_forward_user_list(const IrcMessage& message)
      const IrcUser* user = channel->add_user(nick, this->prefix_to_mode);
      if (user->nick != channel->get_self()->nick)
        {
          log_debug("Adding user [" << nick << "] to chan " << chan_name);
          this->bridge->send_user_join(this->hostname, chan_name, user,
                                       user->get_most_significant_mode(this->sorted_user_modes),
                                       false);

M src/irc/irc_client.hpp => src/irc/irc_client.hpp +7 -2
@@ 11,6 11,7 @@
#include <memory>
#include <vector>
#include <string>
#include <stack>
#include <map>
#include <set>



@@ 19,8 20,6 @@ class Bridge;
/**
 * Represent one IRC client, i.e. an endpoint connected to a single IRC
 * server, through a TCP socket, receiving and sending commands to it.
 *
 * TODO: TLS support, maybe, but that's not high priority
 */
class IrcClient: public SocketHandler
{


@@ 280,6 279,12 @@ private:
   * (for example 'ahov' is a common order).
   */
  std::vector<char> sorted_user_modes;
  /**
   * A list of ports to which we will try to connect, in reverse. Each port
   * is associated with a boolean telling if we should use TLS or not if the
   * connection succeeds on that port.
   */
  std::stack<std::pair<std::string, bool>> ports_to_try;

  IrcClient(const IrcClient&) = delete;
  IrcClient(IrcClient&&) = delete;

M src/network/socket_handler.cpp => src/network/socket_handler.cpp +154 -18
@@ 10,26 10,39 @@
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <netdb.h>
#include <cstring>
#include <fcntl.h>
#include <netdb.h>
#include <stdio.h>

#include <iostream>

using namespace std::string_literals;
#ifdef BOTAN_FOUND
# include <botan/hex.h>
#endif

#ifndef UIO_FASTIOV
# define UIO_FASTIOV 8
#endif

using namespace std::string_literals;

namespace ph = std::placeholders;

SocketHandler::SocketHandler(std::shared_ptr<Poller> poller):
  socket(-1),
  poller(poller),
  use_tls(false),
  connected(false),
  connecting(false)
{
}
#ifdef BOTAN_FOUND
  ,
  rng(),
  credential_manager(),
  policy(),
  session_manager(rng)
#endif
{}

void SocketHandler::init_socket(const struct addrinfo* rp)
{


@@ 47,10 60,11 @@ void SocketHandler::init_socket(const struct addrinfo* rp)
    throw std::runtime_error("Could not initialize socket: "s + strerror(errno));
}

void SocketHandler::connect(const std::string& address, const std::string& port)
void SocketHandler::connect(const std::string& address, const std::string& port, const bool tls)
{
  this->address = address;
  this->port = port;
  this->use_tls = tls;

  utils::ScopeGuard sg;



@@ 106,6 120,10 @@ void SocketHandler::connect(const std::string& address, const std::string& port)
          this->poller->add_socket_handler(this);
          this->connected = true;
          this->connecting = false;
#ifdef BOTAN_FOUND
          if (this->use_tls)
            this->start_tls();
#endif
          this->on_connected();
          return ;
        }


@@ 133,11 151,21 @@ void SocketHandler::connect(const std::string& address, const std::string& port)

void SocketHandler::connect()
{
  this->connect(this->address, this->port);
  this->connect(this->address, this->port, this->use_tls);
}

void SocketHandler::on_recv()
{
#ifdef BOTAN_FOUND
  if (this->use_tls)
    this->tls_recv();
  else
#endif
    this->plain_recv();
}

void SocketHandler::plain_recv()
{
  static constexpr size_t buf_size = 4096;
  char buf[buf_size];
  void* recv_buf = this->get_receive_buffer(buf_size);


@@ 145,6 173,23 @@ void SocketHandler::on_recv()
  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 SocketHandler::do_recv(void* recv_buf, const size_t buf_size)
{
  ssize_t size = ::recv(this->socket, recv_buf, buf_size, 0);
  if (0 == size)
    {


@@ 155,22 200,17 @@ void SocketHandler::on_recv()
    {
      log_warning("Error while reading from socket: " << strerror(errno));
      if (this->connecting)
        this->on_connection_failed(strerror(errno));
        {
          this->close();
          this->on_connection_failed(strerror(errno));
        }
      else
        this->on_connection_close();
      this->close();
    }
  else
    {
      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->close();
          this->on_connection_close();
        }
      this->parse_in_buffer(size);
    }
  return size;
}

void SocketHandler::on_send()


@@ 242,6 282,16 @@ socket_t SocketHandler::get_socket() const

void SocketHandler::send_data(std::string&& data)
{
#ifdef BOTAN_FOUND
  if (this->use_tls)
    this->tls_send(std::move(data));
  else
#endif
    this->raw_send(std::move(data));
}

void SocketHandler::raw_send(std::string&& data)
{
  if (data.empty())
    return ;
  this->out_buf.emplace_back(std::move(data));


@@ 269,3 319,89 @@ void* SocketHandler::get_receive_buffer(const size_t) const
{
  return nullptr;
}

#ifdef BOTAN_FOUND
void SocketHandler::start_tls()
{
  Botan::TLS::Server_Information server_info(this->address, "irc", std::stoul(this->port));
  this->tls = std::make_unique<Botan::TLS::Client>(
      std::bind(&SocketHandler::tls_output_fn, this, ph::_1, ph::_2),
      std::bind(&SocketHandler::tls_data_cb, this, ph::_1, ph::_2),
      std::bind(&SocketHandler::tls_alert_cb, this, ph::_1, ph::_2, ph::_3),
      std::bind(&SocketHandler::tls_handshake_cb, this, ph::_1),
      session_manager, credential_manager, policy,
      rng, server_info, Botan::TLS::Protocol_Version::latest_tls_version());
}

void SocketHandler::tls_recv()
{
  static constexpr size_t buf_size = 4096;
  char 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();
      this->tls->received_data(reinterpret_cast<const Botan::byte*>(recv_buf),
                              static_cast<size_t>(size));
      if (!was_active && this->tls->is_active())
        this->on_tls_activated();
    }
}

void SocketHandler::tls_send(std::string&& data)
{
  if (this->tls->is_active())
    {
      const bool was_active = this->tls->is_active();
      if (!this->pre_buf.empty())
        {
          this->tls->send(reinterpret_cast<const Botan::byte*>(this->pre_buf.data()),
                         this->pre_buf.size());
          this->pre_buf = "";
        }
      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 += data;
}

void SocketHandler::tls_data_cb(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 SocketHandler::tls_output_fn(const Botan::byte* data, size_t size)
{
  this->raw_send(std::string(reinterpret_cast<const char*>(data), size));
}

void SocketHandler::tls_alert_cb(Botan::TLS::Alert alert, const Botan::byte*, size_t)
{
  log_debug("tls_alert: " << alert.type_string());
}

bool SocketHandler::tls_handshake_cb(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;
}

void SocketHandler::on_tls_activated()
{
  this->send_data("");
}
#endif // BOTAN_FOUND

M src/network/socket_handler.hpp => src/network/socket_handler.hpp +156 -30
@@ 1,6 1,8 @@
#ifndef SOCKET_HANDLER_INCLUDED
# define SOCKET_HANDLER_INCLUDED

#include <logger/logger.hpp>

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>


@@ 10,6 12,26 @@
#include <string>
#include <list>

#include "config.h"

#ifdef BOTAN_FOUND
# include <botan/botan.h>
# include <botan/tls_client.h>

/**
 * A very simple credential manager that accepts any certificate.
 */
class Permissive_Credentials_Manager: public Botan::Credentials_Manager
{
public:
  void verify_certificate_chain(const std::string& type, const std::string& purported_hostname, const std::vector<Botan::X509_Certificate>&)
  { // TODO: Offer the admin to disallow connection on untrusted
    // certificates
    log_debug("Checking remote certificate (" << type << ") for hostname " << purported_hostname);
  }
};
#endif // BOTAN_FOUND

typedef int socket_t;

class Poller;


@@ 24,21 46,19 @@ class SocketHandler
{
protected:
  ~SocketHandler() {}

public:
  explicit SocketHandler(std::shared_ptr<Poller> poller);
  /**
   * Initialize the socket with the parameters contained in the given
   * addrinfo structure.
   */
  void init_socket(const struct addrinfo* rp);
  /**
   * Connect to the remote server, and call on_connected() if this succeeds
   * 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);
  void connect(const std::string& address, const std::string& port, const bool tls);
  void connect();
  /**
   * Reads data in our in_buf and the call parse_in_buf, for the implementor
   * to handle the data received so far.
   * 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();
  /**


@@ 48,6 68,9 @@ public:
  /**
   * 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);
  /**


@@ 87,30 110,95 @@ public:
  bool is_connected() const;
  bool is_connecting() const;

protected:
private:
  /**
   * 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
   * can provided, nullptr is returned (the default implementation does
   * that).
   * Initialize the socket with the parameters contained in the given
   * addrinfo structure.
   */
  virtual void* get_receive_buffer(const size_t size) const;
  void init_socket(const struct addrinfo* rp);
  /**
   * The handled socket.
   * 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.
   */
  socket_t socket;
  ssize_t do_recv(void* recv_buf, const size_t buf_size);
  /**
   * 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.
   * Reads data from the socket and calls parse_in_buffer with it.
   */
  std::string in_buf;
  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);

#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();
  /**
   * 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_data_cb(const Botan::byte* data, size_t size);
  /**
   * 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_output_fn(const Botan::byte* data, size_t size);
  /**
   * 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_cb(Botan::TLS::Alert alert, const Botan::byte*, size_t);
  /**
   * 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_handshake_cb(const Botan::TLS::Session& session);
  /**
   * 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
  /**
   * The handled socket.
   */
  socket_t socket;
  /**
   * Where data is added, when we want to send something to the client.
   */
  std::list<std::string> out_buf;
  /**
   * Keep the details of the addrinfo the triggered a EINPROGRESS error when
   * connect()ing to it, to reuse it directly when connect() is called
   * again.
   */
  struct addrinfo addrinfo;
  struct sockaddr ai_addr;
  socklen_t ai_addrlen;

protected:
  /**
   * A pointer to the poller that manages us, because we need to communicate
   * with it, sometimes (for example to tell it that he now needs to watch
   * write events for us). Do not ever try to delete it.


@@ 120,6 208,25 @@ protected:
   */
  std::shared_ptr<Poller> poller;
  /**
   * 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;
  /**
   * Whether we are using TLS on this connection or not.
   */
  bool use_tls;
  /**
   * 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;
  /**
   * Hostname we are connected/connecting to
   */
  std::string address;


@@ 127,14 234,6 @@ protected:
   * Port we are connected/connecting to
   */
  std::string port;
  /**
   * Keep the details of the addrinfo the triggered a EINPROGRESS error when
   * connect()ing to it, to reuse it directly when connect() is called
   * again.
   */
  struct addrinfo addrinfo;
  struct sockaddr ai_addr;
  socklen_t ai_addrlen;

  bool connected;
  bool connecting;


@@ 144,6 243,33 @@ private:
  SocketHandler(SocketHandler&&) = delete;
  SocketHandler& operator=(const SocketHandler&) = delete;
  SocketHandler& operator=(SocketHandler&&) = delete;

#ifdef BOTAN_FOUND
  /**
   * Botan stuff to manipulate a TLS session.
   */
  Botan::AutoSeeded_RNG rng;
  Permissive_Credentials_Manager credential_manager;
  Botan::TLS::Policy policy;
  Botan::TLS::Session_Manager_In_Memory session_manager;
  /**
   * 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 as
   * soon and calls the output_fn callback with it as soon as it is
   * created. Therefore, we do not want to create it if do not intend to do
   * 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::string pre_buf;
#endif // BOTAN_FOUND
};

#endif // SOCKET_HANDLER_INCLUDED

M src/xmpp/xmpp_component.cpp => src/xmpp/xmpp_component.cpp +1 -1
@@ 68,7 68,7 @@ XmppComponent::~XmppComponent()

void XmppComponent::start()
{
  this->connect("127.0.0.1", "5347");
  this->connect("127.0.0.1", "5347", false);
}

bool XmppComponent::is_document_open() const