view libirccd/irccd/server.cpp @ 489:349fe29d86d5

Tests: switch to Boost, closes #680
author David Demelier <markand@malikania.fr>
date Sun, 20 Aug 2017 08:16:39 +0200
parents 7e273b7f4f92
children 9fcdd3c9cd33
line wrap: on
line source

/*
 * server.cpp -- an IRC server
 *
 * Copyright (c) 2013-2017 David Demelier <markand@malikania.fr>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <algorithm>
#include <cerrno>
#include <cstring>
#include <stdexcept>

#include <format.h>

#include <libircclient.h>
#include <libirc_rfcnumeric.h>

#include "sysconfig.hpp"

#if !defined(IRCCD_SYSTEM_WINDOWS)
#  include <sys/types.h>
#  include <netinet/in.h>
#  include <arpa/nameser.h>
#  include <resolv.h>
#endif

#include "logger.hpp"
#include "util.hpp"
#include "server.hpp"

using namespace fmt::literals;

namespace irccd {

/*
 * server::session declaration.
 * ------------------------------------------------------------------
 */

class server::session {
public:
    std::unique_ptr<irc_session_t, void (*)(irc_session_t *)> handle_{nullptr, nullptr};

    inline operator const irc_session_t*() const noexcept
    {
        return handle_.get();
    }

    inline operator irc_session_t*() noexcept
    {
        return handle_.get();
    }

    inline bool is_connected() const noexcept
    {
        return irc_is_connected(handle_.get());
    }
};

/*
 * server::state declaration.
 * ------------------------------------------------------------------
 */

class server::state {
public:
    state() = default;
    virtual ~state() = default;
    virtual void prepare(server&, fd_set&, fd_set&, net::Handle&) = 0;
    virtual std::string ident() const = 0;
};

/*
 * server::disconnected_state declaration.
 * ------------------------------------------------------------------
 */

class server::disconnected_state : public server::state {
private:
    boost::timer::cpu_timer timer_;

public:
    void prepare(server&, fd_set&, fd_set&, net::Handle&) override;
    std::string ident() const override;
};

/*
 * server::connecting_state declaration.
 * ------------------------------------------------------------------
 */

class server::connecting_state : public state {
private:
    enum {
        disconnected,
        connecting
    } state_{disconnected};

    boost::timer::cpu_timer timer_;

    bool connect(server& server);

public:
    void prepare(server&, fd_set&, fd_set&, net::Handle&) override;
    std::string ident() const override;
};

/*
 * server::connected_state declaration.
 * ------------------------------------------------------------------
 */

class server::connected_state : public state {
public:
    void prepare(server&, fd_set&, fd_set&, net::Handle&) override;
    std::string ident() const override;
};

namespace {

/*
 * strify
 * ------------------------------------------------------------------
 *
 * Make sure to build a C++ string with a not-null C string.
 */
inline std::string strify(const char* s)
{
    return (s == nullptr) ? "" : std::string(s);
}

/*
 * clean_prefix
 * ------------------------------------------------------------------
 *
 * Remove the user prefix only if it is present in the mode table, for example
 * removes @ from @irccd if and only if @ is a character mode (e.g. operator).
 */
std::string clean_prefix(const std::map<channel_mode, char>& modes, std::string nickname)
{
    if (nickname.length() == 0)
        return nickname;

    for (const auto& pair : modes)
        if (nickname[0] == pair.second)
            nickname.erase(0, 1);

    return nickname;
}

/*
 * extract_prefixes
 * ------------------------------------------------------------------
 *
 * Read modes from the IRC event numeric.
 */
std::map<channel_mode, char> extract_prefixes(const std::string& line)
{
    // FIXME: what if line has different size?
    std::pair<char, char> table[16];
    std::string buf = line.substr(7);
    std::map<channel_mode, char> modes;

    for (int i = 0; i < 16; ++i)
        table[i] = std::make_pair(-1, -1);

    int j = 0;
    bool read_modes = true;
    for (size_t i = 0; i < buf.size(); ++i) {
        if (buf[i] == '(')
            continue;
        if (buf[i] == ')') {
            j = 0;
            read_modes = false;
            continue;
        }

        if (read_modes)
            table[j++].first = buf[i];
        else
            table[j++].second = buf[i];
    }

    // Put these as a map of mode to prefix.
    for (int i = 0; i < 16; ++i) {
        auto key = static_cast<channel_mode>(table[i].first);
        auto value = table[i].second;

        modes.emplace(key, value);
    }

    return modes;
}

} // !namespace

void server::remove_joined_channel(const std::string& channel)
{
    jchannels_.erase(std::remove(jchannels_.begin(), jchannels_.end(), channel), jchannels_.end());
}

void server::handle_connect(const char*, const char**) noexcept
{
    // Reset the number of tried reconnection.
    recocur_ = 1;

    // Reset the timer.
    timer_.start();

    // Reset joined channels.
    jchannels_.clear();

    // Don't forget to change state and notify.
    next(std::make_unique<connected_state>());
    on_connect(connect_event{shared_from_this()});

    // Auto join listed channels.
    for (const auto& channel : rchannels_) {
        log::info() << "server " << name_ << ": auto joining " << channel.name << std::endl;
        join(channel.name, channel.password);
    }
}

void server::handle_channel(const char* orig, const char** params) noexcept
{
    on_message({shared_from_this(), strify(orig), strify(params[0]), strify(params[1])});
}

void server::handle_channel_mode(const char* orig, const char** params) noexcept
{
    on_channel_mode({
        shared_from_this(),
        strify(orig),
        strify(params[0]),
        strify(params[1]),
        strify(params[2])
    });
}

void server::handle_channel_notice(const char* orig, const char** params) noexcept
{
    on_channel_notice({
        shared_from_this(),
        strify(orig),
        strify(params[0]),
        strify(params[1])
    });
}

void server::handle_ctcp_action(const char* orig, const char** params) noexcept
{
    on_me({shared_from_this(), strify(orig), strify(params[0]), strify(params[1])});
}

void server::handle_invite(const char* orig, const char** params) noexcept
{
    // If joininvite is set, join the channel.
    if ((flags_ & join_invite) && is_self(strify(params[0])))
        join(strify(params[1]));

    /*
     * The libircclient says that invite contains the target nickname, it's
     * quit uncommon to need it so it is passed as the last argument to be
     * optional in the plugin.
     */
    on_invite({shared_from_this(), strify(orig), strify(params[1]), strify(params[0])});
}

void server::handle_join(const char* orig, const char** params) noexcept
{
    if (is_self(strify(orig)))
        jchannels_.push_back(strify(params[0]));

    on_join({shared_from_this(), strify(orig), strify(params[0])});
}

void server::handle_kick(const char* orig, const char** params) noexcept
{
    if (is_self(strify(params[1]))) {
        // Remove the channel from the joined list.
        remove_joined_channel(strify(params[0]));

        // Rejoin the channel if the option has been set and I was kicked.
        if (flags_ & auto_rejoin)
            join(strify(params[0]));
    }

    on_kick({
        shared_from_this(),
        strify(orig),
        strify(params[0]),
        strify(params[1]),
        strify(params[2])
    });
}

void server::handle_mode(const char* orig, const char** params) noexcept
{
    on_mode({shared_from_this(), strify(orig), strify(params[1])});
}

void server::handle_nick(const char* orig, const char** params) noexcept
{
    // Update our nickname.
    if (is_self(strify(orig)))
        nickname_ = strify(params[0]);

    on_nick({shared_from_this(), strify(orig), strify(params[0])});
}

void server::handle_notice(const char* orig, const char** params) noexcept
{
    /*
     * Like handleInvite, the notice provides the target nickname, we discard
     * it.
     */
    on_notice({shared_from_this(), strify(orig), strify(params[1])});
}

void server::handle_numeric(unsigned int event, const char** params, unsigned int c) noexcept
{
    if (event == LIBIRC_RFC_RPL_NAMREPLY) {
        /*
         * Called multiple times to list clients on a channel.
         *
         * params[0] == originator
         * params[1] == '='
         * params[2] == channel
         * params[3] == list of users with their prefixes
         *
         * IDEA for the future: maybe give the appropriate mode as a second
         * parameter in onNames.
         */
        if (c < 4 || params[2] == nullptr || params[3] == nullptr)
            return;

        auto users = util::split(params[3], " \t");

        // The listing may add some prefixes, remove them if needed.
        for (auto u : users)
            names_map_[params[2]].insert(clean_prefix(modes_, u));
    } else if (event == LIBIRC_RFC_RPL_ENDOFNAMES) {
        /*
         * Called when end of name listing has finished on a channel.
         *
         * params[0] == originator
         * params[1] == channel
         * params[2] == End of NAMES list
         */
        if (c < 3 || params[1] == nullptr)
            return;

        auto it = names_map_.find(params[1]);
        if (it != names_map_.end()) {
            on_names({
                shared_from_this(),
                params[1],
                std::vector<std::string>(it->second.begin(), it->second.end())
            });

            // Don't forget to remove the list.
            names_map_.erase(it);
        }
    } else if (event == LIBIRC_RFC_RPL_WHOISUSER) {
        /*
         * Called when whois information has been partially received.
         *
         * params[0] == originator
         * params[1] == nickname
         * params[2] == username
         * params[3] == host
         * params[4] == * (no idea what is that)
         * params[5] == realname
         */
        if (c < 6 || !params[1] || !params[2] || !params[3] || !params[5])
            return;

        class whois info;

        info.nick = strify(params[1]);
        info.user = strify(params[2]);
        info.host = strify(params[3]);
        info.realname = strify(params[5]);

        whois_map_.emplace(info.nick, info);
    } else if (event == LIBIRC_RFC_RPL_WHOISCHANNELS) {
        /*
         * Called when we have received channels for one user.
         *
         * params[0] == originator
         * params[1] == nickname
         * params[2] == list of channels with their prefixes
         */
        if (c < 3 || !params[1] || !params[2])
            return;

        auto it = whois_map_.find(params[1]);
        if (it != whois_map_.end()) {
            auto channels = util::split(params[2], " \t");

            // Clean their prefixes.
            for (auto &s : channels)
                s = clean_prefix(modes_, s);

            it->second.channels = std::move(channels);
        }
    } else if (event == LIBIRC_RFC_RPL_ENDOFWHOIS) {
        /*
         * Called when whois is finished.
         *
         * params[0] == originator
         * params[1] == nickname
         * params[2] == End of WHOIS list
         */
        auto it = whois_map_.find(params[1]);
        if (it != whois_map_.end()) {
            on_whois({shared_from_this(), it->second});

            // Don't forget to remove.
            whois_map_.erase(it);
        }
    } else if (event == /* RPL_BOUNCE */ 5) {
        /*
         * The event 5 is usually RPL_BOUNCE, but we always see it as ISUPPORT.
         */
        for (unsigned int i = 0; i < c; ++i) {
            if (strncmp(params[i], "PREFIX", 6) == 0) {
                modes_ = extract_prefixes(params[i]);
                break;
            }
        }
    }
}

void server::handle_part(const char* orig, const char** params) noexcept
{
    // Remove the channel from the joined list if I left a channel.
    if (is_self(strify(orig)))
        remove_joined_channel(strify(params[0]));

    on_part({shared_from_this(), strify(orig), strify(params[0]), strify(params[1])});
}

void server::handle_ping(const char*, const char**) noexcept
{
    // Reset the timer to detect disconnection.
    timer_.start();
}

void server::handle_query(const char* orig, const char** params) noexcept
{
    on_query({shared_from_this(), strify(orig), strify(params[1])});
}

void server::handle_topic(const char* orig, const char** params) noexcept
{
    on_topic({shared_from_this(), strify(orig), strify(params[0]), strify(params[1])});
}

std::shared_ptr<server> server::from_json(const nlohmann::json& object)
{
    auto sv = std::make_shared<server>(util::json::require_identifier(object, "name"));

    sv->set_host(util::json::require_string(object, "host"));
    sv->set_password(util::json::get_string(object, "password"));
    sv->set_nickname(util::json::get_string(object, "nickname", sv->nickname()));
    sv->set_realname(util::json::get_string(object, "realname", sv->realname()));
    sv->set_username(util::json::get_string(object, "username", sv->username()));
    sv->set_ctcp_version(util::json::get_string(object, "ctcpVersion", sv->ctcp_version()));
    sv->set_command_char(util::json::get_string(object, "commandChar", sv->command_char()));

    if (object.find("port") != object.end())
        sv->set_port(util::json::get_uint_range<std::uint16_t>(object, "port"));
    if (util::json::get_bool(object, "ipv6"))
        sv->set_flags(sv->flags() | server::ipv6);
    if (util::json::get_bool(object, "ssl"))
        sv->set_flags(sv->flags() | server::ssl);
    if (util::json::get_bool(object, "sslVerify"))
        sv->set_flags(sv->flags() | server::ssl_verify);
    if (util::json::get_bool(object, "autoRejoin"))
        sv->set_flags(sv->flags() | server::auto_rejoin);
    if (util::json::get_bool(object, "joinInvite"))
        sv->set_flags(sv->flags() | server::join_invite);

    return sv;
}

channel server::split_channel(const std::string& value)
{
    auto pos = value.find(':');

    if (pos != std::string::npos)
        return {value.substr(0, pos), value.substr(pos + 1)};

    return {value, ""};
}

server::server(std::string name)
    : name_(std::move(name))
    , session_(std::make_unique<session>())
    , state_(std::make_unique<connecting_state>())
{
    irc_callbacks_t callbacks;

    /*
     * GCC 4.9.2 triggers some missing-field-initializers warnings when
     * using uniform initialization so use a std::memset as a workaround.
     */
    std::memset(&callbacks, 0, sizeof (irc_callbacks_t));

    /*
     * Convert the raw pointer functions from libircclient to Server member
     * function.
     *
     * While doing this, discard useless arguments.
     */
    callbacks.event_channel = [] (auto session, auto, auto orig, auto params, auto) {
        static_cast<server*>(irc_get_ctx(session))->handle_channel(orig, params);
    };
    callbacks.event_channel_notice = [] (auto session, auto, auto orig, auto params, auto) {
        static_cast<server*>(irc_get_ctx(session))->handle_channel_notice(orig, params);
    };
    callbacks.event_connect = [] (auto session, auto, auto orig, auto params, auto) {
        static_cast<server*>(irc_get_ctx(session))->handle_connect(orig, params);
    };
    callbacks.event_ctcp_action = [] (auto session, auto, auto orig, auto params, auto) {
        static_cast<server*>(irc_get_ctx(session))->handle_ctcp_action(orig, params);
    };
    callbacks.event_invite = [] (auto session, auto, auto orig, auto params, auto) {
        static_cast<server*>(irc_get_ctx(session))->handle_invite(orig, params);
    };
    callbacks.event_join = [] (auto session, auto, auto orig, auto params, auto) {
        static_cast<server*>(irc_get_ctx(session))->handle_join(orig, params);
    };
    callbacks.event_kick = [] (auto session, auto, auto orig, auto params, auto) {
        static_cast<server*>(irc_get_ctx(session))->handle_kick(orig, params);
    };
    callbacks.event_mode = [] (auto session, auto, auto orig, auto params, auto) {
        static_cast<server*>(irc_get_ctx(session))->handle_channel_mode(orig, params);
    };
    callbacks.event_nick = [] (auto session, auto, auto orig, auto params, auto) {
        static_cast<server*>(irc_get_ctx(session))->handle_nick(orig, params);
    };
    callbacks.event_notice = [] (auto session, auto, auto orig, auto params, auto) {
        static_cast<server*>(irc_get_ctx(session))->handle_notice(orig, params);
    };
    callbacks.event_numeric = [] (auto session, auto event, auto, auto params, auto count) {
        static_cast<server*>(irc_get_ctx(session))->handle_numeric(event, params, count);
    };
    callbacks.event_part = [] (auto session, auto, auto orig, auto params, auto) {
        static_cast<server*>(irc_get_ctx(session))->handle_part(orig, params);
    };
    callbacks.event_ping = [] (auto session, auto, auto orig, auto params, auto) {
        static_cast<server*>(irc_get_ctx(session))->handle_ping(orig, params);
    };
    callbacks.event_privmsg = [] (auto session, auto, auto orig, auto params, auto) {
        static_cast<server*>(irc_get_ctx(session))->handle_query(orig, params);
    };
    callbacks.event_topic = [] (auto session, auto, auto orig, auto params, auto) {
        static_cast<server*>(irc_get_ctx(session))->handle_topic(orig, params);
    };
    callbacks.event_umode = [] (auto session, auto, auto orig, auto params, auto) {
        static_cast<server*>(irc_get_ctx(session))->handle_mode(orig, params);
    };

    session_->handle_ = {irc_create_session(&callbacks), irc_destroy_session};

    // Save this to the session.
    irc_set_ctx(*session_, this);
    irc_set_ctcp_version(*session_, ctcpversion_.c_str());
}

server::~server()
{
    irc_disconnect(*session_);
}

void server::set_nickname(std::string nickname)
{
    if (session_->is_connected())
        queue_.push([=] () {
            return irc_cmd_nick(*session_, nickname.c_str()) == 0;
        });
    else
        nickname_ = std::move(nickname);
}

void server::set_ctcp_version(std::string ctcpversion)
{
    ctcpversion_ = std::move(ctcpversion);
    irc_set_ctcp_version(*session_, ctcpversion_.c_str());
}

void server::next(std::unique_ptr<state> state) noexcept
{
    state_next_ = std::move(state);
}

std::string server::status() const noexcept
{
    return state_ ? state_->ident() : "null";
}

void server::update() noexcept
{
    if (state_next_) {
        log::debug("server {}: switch state {} -> {}"_format(name_, state_->ident(), state_next_->ident()));

        state_ = std::move(state_next_);
        state_next_ = nullptr;

        // Reset channels.
        jchannels_.clear();
    }
}

void server::disconnect() noexcept
{
    using namespace std::placeholders;

    irc_disconnect(*session_);
    on_die();
}

void server::reconnect() noexcept
{
    irc_disconnect(*session_);
    next(std::make_unique<connecting_state>());
}

void server::prepare(fd_set& setinput, fd_set& setoutput, net::Handle& maxfd) noexcept
{
    state_->prepare(*this, setinput, setoutput, maxfd);
}

void server::sync(fd_set &setinput, fd_set &setoutput)
{
    /*
     * 1. Send maximum of command possible if available for write
     *
     * Break on the first failure to avoid changing the order of the
     * commands if any of them fails.
     */
    bool done = false;

    while (!queue_.empty() && !done) {
        if (queue_.front()())
            queue_.pop();
        else
            done = true;
    }

    // 2. Read data.
    irc_process_select_descriptors(*session_, &setinput, &setoutput);
}

bool server::is_self(const std::string& nick) const noexcept
{
    char target[32]{0};

    irc_target_get_nick(nick.c_str(), target, sizeof (target));

    return nickname_ == target;
}

void server::cmode(std::string channel, std::string mode)
{
    queue_.push([=] () {
        return irc_cmd_channel_mode(*session_, channel.c_str(), mode.c_str()) == 0;
    });
}

void server::cnotice(std::string channel, std::string message)
{
    queue_.push([=] () {
        return irc_cmd_notice(*session_, channel.c_str(), message.c_str()) == 0;
    });
}

void server::invite(std::string target, std::string channel)
{
    queue_.push([=] () {
        return irc_cmd_invite(*session_, target.c_str(), channel.c_str()) == 0;
    });
}

void server::join(std::string channel, std::string password)
{
    // 1. Add the channel or update it to the requested channels.
    auto it = std::find_if(rchannels_.begin(), rchannels_.end(), [&] (const auto& c) {
        return c.name == channel;
    });

    if (it == rchannels_.end())
        rchannels_.push_back({ channel, password });
    else
        *it = { channel, password };

    // 2. Join if not found and connected.
    if (session_->is_connected())
        irc_cmd_join(*session_, channel.c_str(), password.empty() ? nullptr : password.c_str());
}

void server::kick(std::string target, std::string channel, std::string reason)
{
    queue_.push([=] () {
        return irc_cmd_kick(*session_, target.c_str(), channel.c_str(), reason.c_str()) == 0;
    });
}

void server::me(std::string target, std::string message)
{
    queue_.push([=] () {
        return irc_cmd_me(*session_, target.c_str(), message.c_str()) == 0;
    });
}

void server::message(std::string target, std::string message)
{
    queue_.push([=] () {
        return irc_cmd_msg(*session_, target.c_str(), message.c_str()) == 0;
    });
}

void server::mode(std::string mode)
{
    queue_.push([=] () {
        return irc_cmd_user_mode(*session_, mode.c_str()) == 0;
    });
}

void server::names(std::string channel)
{
    queue_.push([=] () {
        return irc_cmd_names(*session_, channel.c_str()) == 0;
    });
}

void server::notice(std::string target, std::string message)
{
    queue_.push([=] () {
        return irc_cmd_notice(*session_, target.c_str(), message.c_str()) == 0;
    });
}

void server::part(std::string channel, std::string reason)
{
    queue_.push([=] () -> bool {
        if (reason.empty())
            return irc_cmd_part(*session_, channel.c_str()) == 0;

        return irc_send_raw(*session_, "PART %s :%s", channel.c_str(), reason.c_str());
    });
}

void server::send(std::string raw)
{
    queue_.push([=] () {
        return irc_send_raw(*session_, raw.c_str()) == 0;
    });
}

void server::topic(std::string channel, std::string topic)
{
    queue_.push([=] () {
        return irc_cmd_topic(*session_, channel.c_str(), topic.c_str()) == 0;
    });
}

void server::whois(std::string target)
{
    queue_.push([=] () {
        return irc_cmd_whois(*session_, target.c_str()) == 0;
    });
}

/*
 * server::disconnected_state implementation
 * ------------------------------------------------------------------
 */

void server::disconnected_state::prepare(server& server, fd_set&, fd_set&, net::Handle&)
{
    if (server.recotries_ == 0) {
        log::warning() << "server " << server.name_ << ": reconnection disabled, skipping" << std::endl;
        server.on_die();
    } else if (server.recotries_ > 0 && server.recocur_ > server.recotries_) {
        log::warning() << "server " << server.name_ << ": giving up" << std::endl;
        server.on_die();
    } else {
        if (timer_.elapsed().wall / 1000000LL > static_cast<unsigned>(server.recodelay_ * 1000)) {
            irc_disconnect(*server.session_);

            server.recocur_ ++;
            server.next(std::make_unique<connecting_state>());
        }
    }
}

std::string server::disconnected_state::ident() const
{
    return "Disconnected";
}

/*
 * server::connecting_state implementation
 * ------------------------------------------------------------------
 */

bool server::connecting_state::connect(server& server)
{
    auto password = server.password_.empty() ? nullptr : server.password_.c_str();
    auto host = server.host_;

    // libircclient requires # for SSL connection.
#if defined(WITH_SSL)
    if (server.flags_ & server::ssl)
        host.insert(0, 1, '#');
    if (!(server.flags_ & server::ssl_verify))
        irc_option_set(*server.session_, LIBIRC_OPTION_SSL_NO_VERIFY);
#endif

    int code;
    if (server.flags() & server::ipv6) {
        code = irc_connect6(*server.session_, host.c_str(), server.port_, password,
                            server.nickname_.c_str(),
                            server.username_.c_str(),
                            server.realname_.c_str());
    } else {
        code = irc_connect(*server.session_, host.c_str(), server.port_, password,
                           server.nickname_.c_str(),
                           server.username_.c_str(),
                           server.realname_.c_str());
    }

    return code == 0;
}

void server::connecting_state::prepare(server& server, fd_set& setinput, fd_set& setoutput, net::Handle& maxfd)
{
    /*
     * The connect function will either fail if the hostname wasn't resolved or
     * if any of the internal functions fail.
     *
     * It returns success if the connection was successful but it does not mean
     * that connection is established.
     *
     * Because this function will be called repeatidly, the connection was
     * started and we're still not connected in the specified timeout time, we
     * mark the server as disconnected.
     *
     * Otherwise, the libircclient event_connect will change the state.
     */
    if (state_ == connecting) {
        if (timer_.elapsed().wall / 1000000LL > static_cast<unsigned>(server.recodelay_ * 1000)) {
            log::warning() << "server " << server.name() << ": timeout while connecting" << std::endl;
            server.next(std::make_unique<disconnected_state>());
        } else if (!irc_is_connected(*server.session_)) {
            log::warning() << "server " << server.name_ << ": error while connecting: ";
            log::warning() << irc_strerror(irc_errno(*server.session_)) << std::endl;

            if (server.recotries_ != 0)
                log::warning("server {}: retrying in {} seconds"_format(server.name_, server.recodelay_));

            server.next(std::make_unique<disconnected_state>());
        } else
            irc_add_select_descriptors(*server.session_, &setinput, &setoutput, reinterpret_cast<int*>(&maxfd));
    } else {
        /*
         * This is needed if irccd is started before DHCP or if DNS cache is
         * outdated.
         */
#if !defined(IRCCD_SYSTEM_WINDOWS)
        (void)res_init();
#endif
        log::info("server {}: trying to connect to {}, port {}"_format(server.name_, server.host_, server.port_));

        if (!connect(server)) {
            log::warning() << "server " << server.name_ << ": disconnected while connecting: ";
            log::warning() << irc_strerror(irc_errno(*server.session_)) << std::endl;
            server.next(std::make_unique<disconnected_state>());
        } else {
            state_ = connecting;

            if (irc_is_connected(*server.session_))
                irc_add_select_descriptors(*server.session_, &setinput, &setoutput, reinterpret_cast<int*>(&maxfd));
        }
    }
}

std::string server::connecting_state::ident() const
{
    return "Connecting";
}

/*
 * server::connected_state implementation
 * ------------------------------------------------------------------
 */

void server::connected_state::prepare(server& server, fd_set& setinput, fd_set& setoutput, net::Handle& maxfd)
{
    if (!irc_is_connected(*server.session_)) {
        log::warning() << "server " << server.name_ << ": disconnected" << std::endl;

        if (server.recodelay_ > 0)
            log::warning("server {}: retrying in {} seconds"_format(server.name_, server.recodelay_));

        server.next(std::make_unique<disconnected_state>());
    } else if (server.timer_.elapsed().wall / 1000000LL >= server.timeout_ * 1000) {
        log::warning() << "server " << server.name_ << ": ping timeout after "
                       << (server.timer_.elapsed().wall / 1000000000LL) << " seconds" << std::endl;
        server.next(std::make_unique<disconnected_state>());
    } else
        irc_add_select_descriptors(*server.session_, &setinput, &setoutput, reinterpret_cast<int*>(&maxfd));
}

std::string server::connected_state::ident() const
{
    return "Connected";
}

} // !irccd