changeset 293:7a82aae1ec36

Irccd: split lib into libirccdctl, #564
author David Demelier <markand@malikania.fr>
date Thu, 06 Oct 2016 12:36:13 +0200
parents 671612cbc721
children 55662f35a16b
files CMakeLists.txt libirccdctl/CMakeLists.txt libirccdctl/irccd/client.cpp libirccdctl/irccd/client.hpp libirccdctl/irccd/irccdctl.cpp libirccdctl/irccd/irccdctl.hpp
diffstat 6 files changed, 1712 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Wed Oct 05 20:32:27 2016 +0200
+++ b/CMakeLists.txt	Thu Oct 06 12:36:13 2016 +0200
@@ -80,6 +80,7 @@
 add_subdirectory(doc)
 add_subdirectory(libcommon)
 add_subdirectory(libirccd)
+add_subdirectory(libirccdctl)
 
 if (WITH_JS)
     add_subdirectory(libirccd-js)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccdctl/CMakeLists.txt	Thu Oct 06 12:36:13 2016 +0200
@@ -0,0 +1,22 @@
+project(libirccdctl)
+
+set(
+    HEADERS
+    ${libirccdctl_SOURCE_DIR}/irccd/client.hpp
+    ${libirccdctl_SOURCE_DIR}/irccd/irccdctl.hpp
+)
+
+set(
+    SOURCES
+    ${libirccdctl_SOURCE_DIR}/irccd/client.cpp
+    ${libirccdctl_SOURCE_DIR}/irccd/irccdctl.cpp
+)
+
+irccd_define_library(
+    TARGET libirccdctl
+    SOURCES
+        ${libirccdctl_SOURCE_DIR}/CMakeLists.txt
+        ${HEADERS}
+        ${SOURCES}
+    LIBRARIES libcommon
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccdctl/irccd/client.cpp	Thu Oct 06 12:36:13 2016 +0200
@@ -0,0 +1,550 @@
+/*
+ * client.cpp -- value wrapper for connecting to irccd
+ *
+ * Copyright (c) 2013-2016 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 Client WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <stdexcept>
+
+#include <format.h>
+
+#include "client.hpp"
+#include "util.hpp"
+
+using namespace fmt::literals;
+
+namespace irccd {
+
+/*
+ * Client::State.
+ * ------------------------------------------------------------------
+ */
+
+class Client::State {
+public:
+    State() = default;
+    virtual ~State() = default;
+    virtual Status status() const noexcept = 0;
+    virtual void prepare(Client &cnt, fd_set &in, fd_set &out) = 0;
+    virtual void sync(Client &cnt, fd_set &in, fd_set &out) = 0;
+};
+
+/*
+ * Client::DisconnectedState.
+ * ------------------------------------------------------------------
+ */
+
+class Client::DisconnectedState : public Client::State {
+public:
+    Client::Status status() const noexcept override
+    {
+        return Disconnected;
+    }
+
+    void prepare(Client &, fd_set &, fd_set &) override {}
+    void sync(Client &, fd_set &, fd_set &) override {}
+};
+
+/*
+ * Client::DisconnectedState.
+ * ------------------------------------------------------------------
+ */
+
+class Client::ReadyState : public Client::State {
+private:
+    void parse(Client &client, const std::string &message)
+    {
+        try {
+            auto json = nlohmann::json::parse(message);
+
+            if (!json.is_object())
+                return;
+
+            if (json.count("event") > 0)
+                client.onEvent(json);
+            else
+                client.onMessage(json);
+        } catch (const std::exception &) {
+        }
+    }
+public:
+    Client::Status status() const noexcept override
+    {
+        return Ready;
+    }
+
+    void prepare(Client &cnx, fd_set &in, fd_set &out) override
+    {
+        FD_SET(cnx.m_socket.handle(), &in);
+
+        if (!cnx.m_output.empty())
+            FD_SET(cnx.m_socket.handle(), &out);
+    }
+
+    void sync(Client &cnx, fd_set &in, fd_set &out) override
+    {
+        if (FD_ISSET(cnx.m_socket.handle(), &out))
+            cnx.send();
+
+        if (FD_ISSET(cnx.m_socket.handle(), &in))
+            cnx.recv();
+
+        std::string msg;
+
+        do {
+            msg = util::nextNetwork(cnx.m_input);
+
+            if (!msg.empty())
+                parse(cnx, msg);
+        } while (!msg.empty());
+    }
+};
+
+/*
+ * Client::AuthState.
+ * ------------------------------------------------------------------
+ */
+
+class Client::AuthState : public Client::State {
+private:
+    enum {
+        Created,
+        Sending,
+        Checking
+    } m_auth{Created};
+
+    std::string m_output;
+
+    void send(Client &cnt) noexcept
+    {
+        try {
+            auto n = cnt.send(m_output.data(), m_output.size());
+
+            if (n == 0) {
+                m_output.clear();
+                throw std::runtime_error("Client lost");
+            }
+
+            m_output.erase(0, n);
+
+            if (m_output.empty())
+                m_auth = Checking;
+        } catch (const std::exception &ex) {
+            cnt.m_state = std::make_unique<DisconnectedState>();
+            cnt.onDisconnect(ex.what());
+        }
+    }
+
+    void check(Client &cnt) noexcept
+    {
+        cnt.recv();
+
+        auto msg = util::nextNetwork(cnt.m_input);
+
+        if (msg.empty())
+            return;
+
+        try {
+            auto doc = nlohmann::json::parse(msg);
+
+            if (!doc.is_object())
+                throw std::invalid_argument("invalid argument");
+
+            auto cmd = doc.find("response");
+
+            if (cmd == doc.end() || !cmd->is_string() || *cmd != "auth")
+                throw std::invalid_argument("authentication result expected");
+
+            auto result = doc.find("result");
+
+            if (result == doc.end() || !result->is_boolean())
+                throw std::invalid_argument("bad protocol");
+
+            if (!*result)
+                throw std::runtime_error("authentication failed");
+
+            cnt.m_state = std::make_unique<ReadyState>();
+        } catch (const std::exception &ex) {
+            cnt.m_state = std::make_unique<DisconnectedState>();
+            cnt.onDisconnect(ex.what());
+        }
+    }
+
+public:
+    Client::Status status() const noexcept override
+    {
+        return Authenticating;
+    }
+
+    void prepare(Client &cnt, fd_set &in, fd_set &out) override
+    {
+        switch (m_auth) {
+        case Created:
+            m_auth = Sending;
+            m_output += nlohmann::json({
+                { "command", "auth" },
+                { "password", cnt.m_password }
+            }).dump();
+            m_output += "\r\n\r\n";
+
+            // FALLTHROUGH
+        case Sending:
+            FD_SET(cnt.m_socket.handle(), &out);
+            break;
+        case Checking:
+            FD_SET(cnt.m_socket.handle(), &in);
+            break;
+        default:
+            break;
+        }
+    }
+
+    void sync(Client &cnt, fd_set &in, fd_set &out) override
+    {
+        switch (m_auth) {
+        case Sending:
+            if (FD_ISSET(cnt.m_socket.handle(), &out))
+                send(cnt);
+            break;
+        case Checking:
+            if (FD_ISSET(cnt.m_socket.handle(), &in))
+                check(cnt);
+            break;
+        default:
+            break;
+        }
+    }
+};
+
+/*
+ * Client::CheckingState.
+ * ------------------------------------------------------------------
+ */
+
+class Client::CheckingState : public Client::State {
+private:
+    void verifyProgram(const nlohmann::json &json) const
+    {
+        auto prog = json.find("program");
+
+        if (prog == json.end() || !prog->is_string() || prog->get<std::string>() != "irccd")
+            throw std::runtime_error("not an irccd instance");
+    }
+
+    void verifyVersion(Client &cnx, const nlohmann::json &json) const
+    {
+        auto getVersionVar = [&] (auto key) {
+            auto it = json.find(key);
+
+            if (it == json.end() || !it->is_number_unsigned())
+                throw std::runtime_error("invalid irccd instance");
+
+            return *it;
+        };
+
+        Info info{
+            getVersionVar("major"),
+            getVersionVar("minor"),
+            getVersionVar("patch")
+        };
+
+        // Ensure compatibility.
+        if (info.major != IRCCD_VERSION_MAJOR || info.minor > IRCCD_VERSION_MINOR)
+            throw std::runtime_error("server version too recent {}.{}.{} vs {}.{}.{}"_format(
+                info.major, info.minor, info.patch,
+                IRCCD_VERSION_MAJOR, IRCCD_VERSION_MINOR, IRCCD_VERSION_PATCH));
+
+        // Successfully connected.
+        if (cnx.m_password.empty())
+            cnx.m_stateNext = std::make_unique<ReadyState>();
+        else
+            cnx.m_stateNext = std::make_unique<AuthState>();
+
+        cnx.onConnect(info);
+    }
+
+    void verify(Client &cnx) const
+    {
+        auto msg = util::nextNetwork(cnx.m_input);
+
+        if (msg.empty())
+            return;
+
+        try {
+            auto json = nlohmann::json::parse(msg);
+
+            verifyProgram(json);
+            verifyVersion(cnx, json);
+        } catch (const std::exception &ex) {
+            cnx.m_stateNext = std::make_unique<DisconnectedState>();
+            cnx.onDisconnect(ex.what());
+        }
+    }
+
+public:
+    Client::Status status() const noexcept override
+    {
+        return Checking;
+    }
+
+    void prepare(Client &cnx, fd_set &in, fd_set &) override
+    {
+        FD_SET(cnx.m_socket.handle(), &in);
+    }
+
+    void sync(Client &cnx, fd_set &, fd_set &) override
+    {
+        cnx.recv();
+
+        verify(cnx);
+    }
+};
+
+/*
+ * Client::ConnectingState.
+ * ------------------------------------------------------------------
+ */
+
+class Client::ConnectingState : public Client::State {
+public:
+    Client::Status status() const noexcept override
+    {
+        return Connecting;
+    }
+
+    void prepare(Client &cnx, fd_set &, fd_set &out) override
+    {
+        FD_SET(cnx.m_socket.handle(), &out);
+    }
+
+    void sync(Client &cnx, fd_set &, fd_set &out) override
+    {
+        if (!FD_ISSET(cnx.m_socket.handle(), &out))
+            return;
+
+        try {
+            auto errc = cnx.m_socket.get<int>(SOL_SOCKET, SO_ERROR);
+
+            if (errc != 0) {
+                cnx.m_stateNext = std::make_unique<DisconnectedState>();
+                cnx.onDisconnect(net::error(errc));
+            } else
+                cnx.m_stateNext = std::make_unique<CheckingState>();
+        } catch (const std::exception &ex) {
+            cnx.m_stateNext = std::make_unique<DisconnectedState>();
+            cnx.onDisconnect(ex.what());
+        }
+    }
+};
+
+/*
+ * Client.
+ * ------------------------------------------------------------------
+ */
+
+unsigned Client::recv(char *buffer, unsigned length)
+{
+    return m_socket.recv(buffer, length);
+}
+
+unsigned Client::send(const char *buffer, unsigned length)
+{
+    return m_socket.send(buffer, length);
+}
+
+void Client::recv()
+{
+    try {
+        std::string buffer;
+
+        buffer.resize(512);
+        buffer.resize(recv(&buffer[0], buffer.size()));
+
+        if (buffer.empty())
+            throw std::runtime_error("Client lost");
+
+        m_input += std::move(buffer);
+    } catch (const std::exception &ex) {
+        m_stateNext = std::make_unique<DisconnectedState>();
+        onDisconnect(ex.what());
+    }
+}
+
+void Client::send()
+{
+    try {
+        auto ns = send(m_output.data(), m_output.length());
+
+        if (ns > 0)
+            m_output.erase(0, ns);
+    } catch (const std::exception &ex) {
+        m_stateNext = std::make_unique<DisconnectedState>();
+        onDisconnect(ex.what());
+    }
+}
+
+Client::Client()
+    : m_state(std::make_unique<DisconnectedState>())
+{
+}
+
+Client::~Client() = default;
+
+Client::Status Client::status() const noexcept
+{
+    return m_state->status();
+}
+
+void Client::connect(const net::Address &address)
+{
+    assert(status() == Disconnected);
+
+    try {
+        m_socket = net::TcpSocket(address.domain(), 0);
+        m_socket.set(net::option::SockBlockMode(false));
+        m_socket.connect(address);
+        m_state = std::make_unique<CheckingState>();
+    } catch (const net::WouldBlockError &) {
+        m_state = std::make_unique<ConnectingState>();
+    } catch (const std::exception &ex) {
+        m_state = std::make_unique<DisconnectedState>();
+        onDisconnect(ex.what());
+    }
+}
+
+void Client::prepare(fd_set &in, fd_set &out, net::Handle &max)
+{
+    try {
+        m_state->prepare(*this, in, out);
+
+        if (m_socket.handle() > max)
+            max = m_socket.handle();
+    } catch (const std::exception &ex) {
+        m_state = std::make_unique<DisconnectedState>();
+        onDisconnect(ex.what());
+    }
+}
+
+void Client::sync(fd_set &in, fd_set &out)
+{
+    try {
+        m_state->sync(*this, in, out);
+
+        if (m_stateNext) {
+            m_state = std::move(m_stateNext);
+            m_stateNext = nullptr;
+        }
+    } catch (const std::exception &ex) {
+        m_state = std::make_unique<DisconnectedState>();
+        onDisconnect(ex.what());
+    }
+}
+
+/*
+ * TlsClient.
+ * ------------------------------------------------------------------
+ */
+
+void TlsClient::handshake()
+{
+    try {
+        m_ssl->handshake();
+        m_handshake = HandshakeReady;
+    } catch (const net::WantReadError &) {
+        m_handshake = HandshakeRead;
+    } catch (const net::WantWriteError &) {
+        m_handshake = HandshakeWrite;
+    } catch (const std::exception &ex) {
+        m_state = std::make_unique<DisconnectedState>();
+        onDisconnect(ex.what());
+    }
+}
+
+unsigned TlsClient::recv(char *buffer, unsigned length)
+{
+    unsigned nread = 0;
+
+    try {
+        nread = m_ssl->recv(buffer, length);
+    } catch (const net::WantReadError &) {
+        m_handshake = HandshakeRead;
+    } catch (const net::WantWriteError &) {
+        m_handshake = HandshakeWrite;
+    }
+
+    return nread;
+}
+
+unsigned TlsClient::send(const char *buffer, unsigned length)
+{
+    unsigned nsent = 0;
+
+    try {
+        nsent = m_ssl->send(buffer, length);
+    } catch (const net::WantReadError &) {
+        m_handshake = HandshakeRead;
+    } catch (const net::WantWriteError &) {
+        m_handshake = HandshakeWrite;
+    }
+
+    return nsent;
+}
+
+void TlsClient::connect(const net::Address &address)
+{
+    Client::connect(address);
+
+    m_ssl = std::make_unique<net::TlsSocket>(m_socket, net::TlsSocket::Client);
+}
+
+void TlsClient::prepare(fd_set &in, fd_set &out, net::Handle &max)
+{
+    if (m_state->status() == Connecting)
+        Client::prepare(in, out, max);
+    else {
+        if (m_socket.handle() > max)
+            max = m_socket.handle();
+
+        /*
+         * Attempt an immediate handshake immediately if Client succeeded
+         * in last iteration.
+         */
+        if (m_handshake == HandshakeUndone)
+            handshake();
+
+        switch (m_handshake) {
+        case HandshakeRead:
+            FD_SET(m_socket.handle(), &in);
+            break;
+        case HandshakeWrite:
+            FD_SET(m_socket.handle(), &out);
+            break;
+        default:
+            Client::prepare(in, out, max);
+        }
+    }
+}
+
+void TlsClient::sync(fd_set &in, fd_set &out)
+{
+    if (m_state->status() == Connecting)
+        Client::sync(in, out);
+    else if (m_handshake != HandshakeReady)
+        handshake();
+    else
+        Client::sync(in, out);
+}
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccdctl/irccd/client.hpp	Thu Oct 06 12:36:13 2016 +0200
@@ -0,0 +1,320 @@
+/*
+ * client.hpp -- value wrapper for connecting to irccd
+ *
+ * Copyright (c) 2013-2016 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.
+ */
+
+#ifndef IRCCD_CLIENT_HPP
+#define IRCCD_CLIENT_HPP
+
+/**
+ * \file client.hpp
+ * \brief Connection to irccd instance.
+ */
+
+#include <cassert>
+#include <memory>
+#include <string>
+
+#include <json.hpp>
+
+#include "net.hpp"
+#include "signals.hpp"
+
+namespace irccd {
+
+/**
+ * \brief Low level connection to irccd instance.
+ *
+ * This class is an event-based connection to an irccd instance. You can use
+ * it directly if you want to issue commands to irccd in an asynchronous way.
+ *
+ * Being asynchronous makes mixing the event loop with this connection easier.
+ *
+ * It is implemented as a finite state machine as it may requires several
+ * roundtrips between the controller and irccd.
+ *
+ * Be aware that there are no namespaces for commands, if you plan to use
+ * Irccdctl class and you also connect the onMessage signal, irccdctl will also
+ * use it. Do not use Irccdctl directly if this is a concern.
+ *
+ * The state may change and is currently implementing as following:
+ *
+ *   [o]
+ *    |       +----------------------------+
+ *    v       v                            |
+ * +--------------+   +----------+     +----------------+
+ * | Disconnected |-->| Checking |---->| Authenticating |
+ * +--------------+   +----------+     +----------------+
+ *     ^       |            ^              |
+ *     |       |            |              v
+ *     |       |      +------------+   +-------+
+ *     |       +----->| Connecting |<--| Ready |
+ *     |              +------------+   +-------+
+ *     |                                   |
+ *     ------------------------------------+
+ */
+class Client {
+public:
+    /**
+     * \brief The current connection state.
+     */
+    enum Status {
+        Disconnected,           //!< Socket is closed
+        Connecting,             //!< Connection is in progress
+        Checking,               //!< Connection is checking irccd daemon
+        Authenticating,         //!< Connection is authenticating
+        Ready                   //!< Socket is ready for I/O
+    };
+
+    /**
+     * \brief Irccd information.
+     */
+    class Info {
+    public:
+        unsigned short major;   //!< Major version number
+        unsigned short minor;   //!< Minor version number
+        unsigned short patch;   //!< Patch version
+    };
+
+    /**
+     * onConnect
+     * --------------------------------------------------------------
+     *
+     * Connection was successful.
+     */
+    Signal<const Info &> onConnect;
+
+    /**
+     * onEvent
+     * --------------------------------------------------------------
+     *
+     * An event has been received.
+     */
+    Signal<const nlohmann::json &> onEvent;
+
+    /**
+     * onMessage
+     * ---------------------------------------------------------------
+     *
+     * A message from irccd was received.
+     */
+    Signal<const nlohmann::json &> onMessage;
+
+    /**
+     * onDisconnect
+     * --------------------------------------------------------------
+     *
+     * A fatal error occured resulting in disconnection.
+     */
+    Signal<const std::string &> onDisconnect;
+
+private:
+    std::string m_input;
+    std::string m_output;
+    std::string m_password;
+
+public:
+    class State;
+    class AuthState;
+    class DisconnectedState;
+    class ConnectingState;
+    class CheckingState;
+    class ReadyState;
+
+protected:
+    std::unique_ptr<State> m_state;
+    std::unique_ptr<State> m_stateNext;
+    net::TcpSocket m_socket{net::Invalid};
+
+    /**
+     * Try to receive some data into the given buffer.
+     *
+     * \param buffer the destination buffer
+     * \param length the buffer length
+     * \return the number of bytes received
+     */
+    virtual unsigned recv(char *buffer, unsigned length);
+
+    /**
+     * Try to send some data into the given buffer.
+     *
+     * \param buffer the source buffer
+     * \param length the buffer length
+     * \return the number of bytes sent
+     */
+    virtual unsigned send(const char *buffer, unsigned length);
+
+    /**
+     * Convenient wrapper around recv().
+     *
+     * Must be used in sync() function.
+     */
+    void recv();
+
+    /**
+     * Convenient wrapper around send().
+     *
+     * Must be used in sync() function.
+     */
+    void send();
+
+public:
+    /**
+     * Default constructor.
+     */
+    Client();
+
+    /**
+     * Default destructor.
+     */
+    virtual ~Client();
+
+    /**
+     * Get the optional password.
+     *
+     * \return the password
+     */
+    inline const std::string &password() const noexcept
+    {
+        return m_password;
+    }
+
+    /**
+     * Set the optional password
+     *
+     * \param password the password
+     */
+    inline void setPassword(std::string password) noexcept
+    {
+        m_password = std::move(password);
+    }
+
+    /**
+     * Send an asynchronous request to irccd.
+     *
+     * \pre json.is_object
+     * \param json the JSON object
+     */
+    inline void request(const nlohmann::json &json)
+    {
+        assert(json.is_object());
+
+        m_output += json.dump();
+        m_output += "\r\n\r\n";
+    }
+
+    /**
+     * Get the underlying socket handle.
+     *
+     * \return the handle
+     */
+    inline net::Handle handle() const noexcept
+    {
+        return m_socket.handle();
+    }
+
+    /**
+     * Shorthand for state() != Disconnected.
+     *
+     * \return true if state() != Disconnected
+     */
+    inline bool isConnected() const noexcept
+    {
+        return status() != Disconnected;
+    }
+
+    /**
+     * Get the current state.
+     *
+     * \return the state
+     */
+    Status status() const noexcept;
+
+    /**
+     * Initiate connection to irccd.
+     *
+     * \pre state() == Disconnected
+     * \param address the address
+     */
+    virtual void connect(const net::Address &address);
+
+    /**
+     * Prepare the input and output set according to the current connection
+     * state.
+     *
+     * \param in the input set
+     * \param out the output set
+     * \param max the maximum file descriptor
+     */
+    virtual void prepare(fd_set &in, fd_set &out, net::Handle &max);
+
+    /**
+     * Do some I/O using the protected recv and send functions.
+     *
+     * \param in the input set
+     * \param out the output set
+     */
+    virtual void sync(fd_set &in, fd_set &out);
+};
+
+/**
+ * \brief TLS over IP connection.
+ */
+class TlsClient : public Client {
+private:
+    enum {
+        HandshakeUndone,
+        HandshakeRead,
+        HandshakeWrite,
+        HandshakeReady
+    } m_handshake{HandshakeUndone};
+
+private:
+    std::unique_ptr<net::TlsSocket> m_ssl;
+
+    void handshake();
+
+protected:
+    /**
+     * \copydoc Client::recv
+     */
+    virtual unsigned recv(char *buffer, unsigned length);
+
+    /**
+     * \copydoc Client::send
+     */
+    virtual unsigned send(const char *buffer, unsigned length);
+
+public:
+    /**
+     * \copydoc Client::connect
+     */
+    void connect(const net::Address &address) override;
+
+    /**
+     * \copydoc Service::prepare
+     */
+    void prepare(fd_set &in, fd_set &out, net::Handle &max) override;
+
+    /**
+     * \copydoc Service::sync
+     */
+    void sync(fd_set &in, fd_set &out) override;
+};
+
+} // !irccd
+
+#endif // !IRCCD_CLIENT_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccdctl/irccd/irccdctl.cpp	Thu Oct 06 12:36:13 2016 +0200
@@ -0,0 +1,646 @@
+/*
+ * irccdctl.cpp -- main irccdctl class
+ *
+ * Copyright (c) 2013-2016 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 <format.h>
+
+#include "command.hpp"
+#include "client.hpp"
+#include "elapsed-timer.hpp"
+#include "fs.hpp"
+#include "ini.hpp"
+#include "irccdctl.hpp"
+#include "logger.hpp"
+#include "options.hpp"
+#include "path.hpp"
+#include "system.hpp"
+#include "util.hpp"
+
+using namespace std::string_literals;
+
+using namespace fmt::literals;
+
+namespace irccd {
+
+void Irccdctl::usage() const
+{
+    bool first = true;
+
+    for (const auto &cmd : m_commandService.commands()) {
+        log::warning() << (first ? "usage: " : "       ") << sys::programName() << " "
+                       << cmd->usage() << std::endl;
+        first = false;
+    }
+
+    std::exit(1);
+}
+
+void Irccdctl::help() const
+{
+    log::warning() << "usage: " << sys::programName() << " [options...] <command> [command-options...] [command-args...]\n\n";
+    log::warning() << "General options:\n";
+    log::warning() << "\t-c, --config file\tspecify the configuration file\n";
+    log::warning() << "\t--help\t\t\tshow this help\n";
+    log::warning() << "\t-t, --type type\t\tspecify connection type\n";
+    log::warning() << "\t-v, --verbose\t\tbe verbose\n\n";
+    log::warning() << "Available options for type ip and ipv6 (-t, --type):\n";
+    log::warning() << "\t-h, --host address\tconnect to the specified address\n";
+    log::warning() << "\t-p, --port port\t\tuse the specified port number\n\n";
+    log::warning() << "Available options for type unix (-t, --type):\n";
+    log::warning() << "\t-P, --path file\t\tconnect to the specified socket file\n\n";
+    log::warning() << "Available commands:\n";
+
+    for (const auto &cmd : m_commandService.commands())
+        log::warning() << "\t" << std::left << std::setw(32)
+                       << cmd->name() << cmd->description() << std::endl;
+
+    log::warning() << "\nFor more information on a command, type " << sys::programName() << " help <command>" << std::endl;
+
+    std::exit(1);
+}
+
+/*
+ * Configuration file parsing.
+ * -------------------------------------------------------------------
+ */
+
+/*
+ * readConnectIp
+ * -------------------------------------------------------------------
+ *
+ * Extract IP connection information from the config file.
+ *
+ * [connect]
+ * type = "ip"
+ * host = "ip or hostname"
+ * port = "port number or service"
+ * domain = "ipv4 or ipv6" (Optional, default: ipv4)
+ * ssl = true | false
+ */
+void Irccdctl::readConnectIp(const ini::Section &sc)
+{
+    ini::Section::const_iterator it;
+
+    std::string host, port;
+
+    if ((it = sc.find("host")) == sc.end())
+        throw std::invalid_argument("missing host parameter");
+
+    host = it->value();
+
+    if ((it = sc.find("port")) == sc.end())
+        throw std::invalid_argument("missing port parameter");
+
+    port = it->value();
+
+    int domain = AF_INET;
+
+    if ((it = sc.find("domain")) != sc.end()) {
+        if (it->value() == "ipv6")
+            domain = AF_INET6;
+        else if (it->value() == "ipv4")
+            domain = AF_INET;
+        else
+            throw std::invalid_argument("invalid domain: " + it->value());
+    }
+
+    m_address = net::resolveOne(host, port, domain, SOCK_STREAM);
+
+    if ((it = sc.find("ssl")) != sc.end() && util::isBoolean(it->value()))
+        m_connection = std::make_unique<TlsClient>();
+    else
+        m_connection = std::make_unique<Client>();
+}
+
+/*
+ * readConnectLocal
+ * -------------------------------------------------------------------
+ *
+ * Extract local connection for Unix.
+ *
+ * [connect]
+ * type = "unix"
+ * path = "path to socket file"
+ */
+void Irccdctl::readConnectLocal(const ini::Section &sc)
+{
+#if !defined(IRCCD_SYSTEM_WINDOWS)
+    auto it = sc.find("path");
+
+    if (it == sc.end())
+        throw std::invalid_argument("missing path parameter");
+
+    m_address = net::local::create(it->value());
+    m_connection = std::make_unique<Client>();
+#else
+    (void)sc;
+
+    throw std::invalid_argument("unix connection not supported on Windows");
+#endif
+}
+
+/*
+ * readConnect
+ * -------------------------------------------------------------------
+ *
+ * Generic function for reading the [connect] section.
+ */
+void Irccdctl::readConnect(const ini::Section &sc)
+{
+    auto it = sc.find("type");
+
+    if (it == sc.end())
+        throw std::invalid_argument("missing type parameter");
+
+    if (it->value() == "ip")
+        readConnectIp(sc);
+    else if (it->value() == "unix")
+        readConnectLocal(sc);
+    else
+        throw std::invalid_argument("invalid type given: " + it->value());
+
+    auto password = sc.find("password");
+
+    if (password != sc.end())
+        m_connection->setPassword(password->value());
+}
+
+/*
+ * readGeneral
+ * -------------------------------------------------------------------
+ *
+ * Read the general section.
+ *
+ * [general]
+ * verbose = true
+ */
+void Irccdctl::readGeneral(const ini::Section &sc)
+{
+    auto verbose = sc.find("verbose");
+
+    if (verbose != sc.end())
+        log::setVerbose(util::isBoolean(verbose->value()));
+}
+
+/*
+ * readAliases
+ * -------------------------------------------------------------------
+ *
+ * Read aliases for irccdctl.
+ *
+ * [alias]
+ * name = ( "command", "arg1, "...", "argn" )
+ */
+void Irccdctl::readAliases(const ini::Section &sc)
+{
+    for (const auto &option : sc) {
+        // This is the alias name.
+        Alias alias(option.key());
+
+        // Iterate over the list of commands to execute for this alias.
+        for (const auto &repl : option) {
+            // This is the alias split string.
+            auto list = util::split(repl, " \t");
+
+            if (list.size() < 1)
+                throw std::invalid_argument("alias require at least one argument");
+
+            // First argument is the command/alias to execute.
+            auto command = list[0];
+
+            // Remove command name and puts arguments.
+            alias.push_back({std::move(command), std::vector<AliasArg>(list.begin() + 1, list.end())});
+        }
+
+        m_aliases.emplace(option.key(), std::move(alias));
+    }
+}
+
+void Irccdctl::read(const std::string &path)
+{
+    try {
+        ini::Document doc = ini::readFile(path);
+        ini::Document::const_iterator it;
+
+        if (!m_connection && (it = doc.find("connect")) != doc.end())
+            readConnect(*it);
+        if ((it = doc.find("general")) != doc.end())
+            readGeneral(*it);
+        if ((it = doc.find("alias")) != doc.end())
+            readAliases(*it);
+    } catch (const std::exception &ex) {
+        log::warning() << path << ": " << ex.what() << std::endl;
+    }
+}
+
+/*
+ * Command line parsing.
+ * -------------------------------------------------------------------
+ */
+
+/*
+ * parseConnectIp
+ * ------------------------------------------------------------------
+ *
+ * Parse internet connection from command line.
+ *
+ * -t ip | ipv6
+ * -h host or ip
+ * -p port
+ */
+void Irccdctl::parseConnectIp(const option::Result &options)
+{
+    option::Result::const_iterator it;
+
+    // Host (-h or --host).
+    std::string host;
+
+    if ((it = options.find("-h")) == options.end() && (it = options.find("--host")) == options.end())
+        throw std::invalid_argument("missing host argument (-h or --host)");
+
+    host = it->second;
+
+    // Port (-p or --port).
+    std::string port;
+
+    if ((it = options.find("-p")) == options.end() && (it = options.find("--port")) == options.end())
+        throw std::invalid_argument("missing port argument (-p or --port)");
+
+    port = it->second;
+
+    // Domain
+    int domain = AF_INET;
+
+    if ((it = options.find("-t")) != options.end())
+        domain = it->second == "ipv6" ? AF_INET6 : AF_INET;
+    else if ((it = options.find("--type")) != options.end())
+        domain = it->second == "ipv6" ? AF_INET6: AF_INET;
+
+    m_address = net::resolveOne(host, port, domain, SOCK_STREAM);
+    m_connection = std::make_unique<Client>();
+}
+
+/*
+ * parseConnectLocal
+ * ------------------------------------------------------------------
+ *
+ * Parse local connection.
+ *
+ * -P file
+ */
+void Irccdctl::parseConnectLocal(const option::Result &options)
+{
+#if !defined(IRCCD_SYSTEM_WINDOWS)
+    option::Result::const_iterator it;
+
+    if ((it = options.find("-P")) == options.end() && (it = options.find("--path")) == options.end())
+        throw std::invalid_argument("missing path parameter (-P or --path)");
+
+    m_address = net::local::create(it->second, false);
+    m_connection = std::make_unique<Client>();
+#else
+    (void)options;
+
+    throw std::invalid_argument("unix connection not supported on Windows");
+#endif
+}
+
+/*
+ * parseConnect
+ * ------------------------------------------------------------------
+ *
+ * Generic parsing of command line option for connection.
+ */
+void Irccdctl::parseConnect(const option::Result &options)
+{
+    assert(options.count("-t") > 0 || options.count("--type") > 0);
+
+    auto it = options.find("-t");
+
+    if (it == options.end())
+        it = options.find("--type");
+    if (it->second == "ip" || it->second == "ipv6")
+        return parseConnectIp(options);
+    if (it->second == "unix")
+        return parseConnectLocal(options);
+
+    throw std::invalid_argument("invalid type given: " + it->second);
+}
+
+option::Result Irccdctl::parse(int &argc, char **&argv)
+{
+    // 1. Parse command line options.
+    option::Options def{
+        { "-c",         true    },
+        { "--config",   true    },
+        { "-h",         true    },
+        { "--help",     false   },
+        { "--host",     true    },
+        { "-p",         true    },
+        { "--port",     true    },
+        { "-P",         true    },
+        { "--path",     true    },
+        { "-t",         true    },
+        { "--type",     true    },
+        { "-v",         false   },
+        { "--verbose",  false   }
+    };
+
+    option::Result result;
+
+    try {
+        result = option::read(argc, argv, def);
+
+        if (result.count("--help") != 0) {
+            usage();
+            // NOTREACHED
+        }
+
+        if (result.count("-v") != 0 || result.count("--verbose") != 0)
+            log::setVerbose(true);
+    } catch (const std::exception &ex) {
+        log::warning("{}: {}"_format(sys::programName(), ex.what()));
+        usage();
+    }
+
+    return result;
+}
+
+nlohmann::json Irccdctl::waitMessage(const std::string id)
+{
+    ElapsedTimer timer;
+
+    while (m_messages.empty() && m_connection->isConnected() && timer.elapsed() < m_timeout)
+        util::poller::poll(250, *m_connection);
+
+    if (m_messages.empty())
+        return nlohmann::json();
+
+    nlohmann::json value;
+
+    if (id == "") {
+        value = m_messages[0];
+        m_messages.erase(m_messages.begin());
+    } else {
+        auto it = std::find_if(m_messages.begin(), m_messages.end(), [&] (const auto &v) {
+            auto rt = v.find("response");
+
+            if (v.count("error") > 0 || (rt != v.end() && rt->is_string() && *rt == id))
+                return true;
+
+            return false;
+        });
+
+        // Remove the previous messages.
+        if (it != m_messages.end()) {
+            value = *it;
+            m_messages.erase(m_messages.begin(), it + 1);
+        }
+    }
+
+    auto error = value.find("error");
+
+    if (error != value.end() && error->is_string())
+        throw std::runtime_error(error->template get<std::string>());
+
+    return value;
+}
+
+nlohmann::json Irccdctl::waitEvent()
+{
+    ElapsedTimer timer;
+
+    while (m_events.empty() && m_connection->isConnected() && timer.elapsed() < m_timeout)
+        util::poller::poll(250, *m_connection);
+
+    if (m_events.empty())
+        return nullptr;
+
+    auto first = m_events.front();
+    m_events.erase(m_events.begin());
+
+    return first;
+}
+
+nlohmann::json Irccdctl::exec(const Command &cmd, std::vector<std::string> args)
+{
+    // 1. Build options from command line arguments.
+    option::Options def;
+
+    for (const auto &opt : cmd.options()) {
+        // parser::read needs '-' and '--' so add them.
+        if (!opt.simpleKey().empty())
+            def.emplace("-"s + opt.simpleKey(), !opt.arg().empty());
+        if (!opt.longKey().empty())
+            def.emplace("--"s + opt.longKey(), !opt.arg().empty());
+    }
+
+    // 2. Parse them, remove them from args (in parser::read) and build the map with id.
+    CommandRequest::Options requestOptions;
+
+    for (const auto &pair : option::read(args, def)) {
+        auto options = cmd.options();
+        auto it = std::find_if(options.begin(), options.end(), [&] (const auto &opt) {
+            return ("-"s + opt.simpleKey()) == pair.first || ("--"s + opt.longKey()) == pair.first;
+        });
+
+        requestOptions.emplace(it->id(), pair.second);
+    }
+
+    // 3. Check number of arguments.
+    if (args.size() < cmd.min())
+        throw std::runtime_error("too few arguments");
+
+    /*
+     * 4. Construct the request, if the returned value is not an object, do not
+     * send anything (e.g. help).
+     */
+    auto request = cmd.request(*this, CommandRequest(std::move(requestOptions), std::move(args)));
+
+    if (!request.is_object())
+        throw std::invalid_argument("command has returned invalid request");
+
+    request.push_back({"command", cmd.name()});
+
+    // 5. Send the command.
+    m_connection->request(request);
+
+    // 6. Returns the response.
+    return waitMessage(cmd.name());
+}
+
+std::vector<nlohmann::json> Irccdctl::exec(const Alias &alias, std::vector<std::string> argsCopy)
+{
+    std::vector<nlohmann::json> values;
+
+    for (const AliasCommand &cmd : alias) {
+        std::vector<std::string> args(argsCopy);
+        std::vector<std::string> cmdArgs;
+        std::vector<std::string>::size_type toremove = 0;
+
+        // 1. Append command name before.
+        cmdArgs.push_back(cmd.command());
+
+        for (const auto &arg : cmd.args()) {
+            if (arg.isPlaceholder()) {
+                if (args.size() < arg.index() + 1)
+                    throw std::invalid_argument("missing argument for placeholder %" + std::to_string(arg.index()));
+
+                cmdArgs.push_back(args[arg.index()]);
+
+                if (arg.index() + 1 > toremove)
+                    toremove = arg.index() + 1;
+            } else
+                cmdArgs.push_back(arg.value());
+        }
+
+        assert(toremove <= args.size());
+
+        // 2. Remove the arguments that been placed in placeholders.
+        args.erase(args.begin(), args.begin() + toremove);
+
+        // 3. Now append the rest of arguments.
+        std::copy(args.begin(), args.end(), std::back_inserter(cmdArgs));
+
+        // 4. Finally try to execute.
+        auto response = exec(cmdArgs);
+
+        values.insert(values.end(), response.begin(), response.end());
+    }
+
+    return values;
+}
+
+std::vector<nlohmann::json> Irccdctl::exec(std::vector<std::string> args)
+{
+    assert(args.size() > 0);
+
+    auto name = args[0];
+    auto alias = m_aliases.find(name);
+
+    // Remove name.
+    args.erase(args.begin());
+
+    std::vector<nlohmann::json> values;
+
+    if (alias != m_aliases.end()) {
+        auto response = exec(alias->second, args);
+
+        values.insert(values.end(), response.begin(), response.end());
+    } else {
+        auto cmd = m_commandService.find(name);
+
+        if (cmd)
+            values.push_back(exec(*cmd, args));
+        else
+            throw std::invalid_argument("no alias or command named " + name);
+    }
+
+    return values;
+}
+
+void Irccdctl::run(int argc, char **argv)
+{
+    // 1. Read command line arguments.
+    auto result = parse(argc, argv);
+
+    /*
+     * 2. Open optional config by command line or by searching it
+     *
+     * The connection to irccd is searched in the following order :
+     *
+     * 1. From the command line if specified
+     * 2. From the configuration file specified by -c
+     * 3. From the configuration file searched through directories
+     */
+    try {
+        if (result.count("-t") > 0 || result.count("--type") > 0)
+            parseConnect(result);
+
+        auto it = result.find("-c");
+
+        if (it != result.end() || (it = result.find("--config")) != result.end())
+            read(it->second);
+        else {
+            for (const std::string &dir : path::list(path::PathConfig)) {
+                std::string path = dir + "irccdctl.conf";
+
+                if (fs::exists(path)) {
+                    read(path);
+                    break;
+                }
+            }
+        }
+    } catch (const std::exception &ex) {
+        log::warning() << sys::programName() << ": " << ex.what() << std::endl;
+        std::exit(1);
+    }
+
+    if (argc <= 0) {
+        usage();
+        // NOTREACHED
+    }
+
+    // Help does not require connection.
+    if (std::strcmp(argv[0], "help") != 0) {
+        if (!m_connection) {
+            log::warning("{}: no connection specified"_format(sys::programName()));
+            std::exit(1);
+        }
+
+        m_connection->onDisconnect.connect([this] (auto reason) {
+            log::warning() << "connection lost to irccd: " << reason << std::endl;
+        });
+        m_connection->onConnect.connect([this] (auto info) {
+            log::info() << "connected to irccd "
+                        << info.major << "."
+                        << info.minor << "."
+                        << info.patch << std::endl;
+        });
+        m_connection->onEvent.connect([this] (auto msg) {
+            m_events.push_back(std::move(msg));
+        });
+        m_connection->onMessage.connect([this] (auto msg) {
+            m_messages.push_back(std::move(msg));
+        });
+
+        m_connection->connect(m_address);
+    } else if (argc == 1)
+        help();
+        // NOTREACHED
+
+    // Build a vector of arguments.
+    std::vector<std::string> args;
+
+    for (int i = 0; i < argc; ++i)
+        args.push_back(argv[i]);
+
+    auto commands = exec(args);
+
+    for (const auto &r : commands) {
+        auto name = r.find("response");
+
+        if (name == r.end() || !name->is_string())
+            log::warning() << "unknown irccd response with no response" << std::endl;
+
+        auto it = m_commandService.find(*name);
+
+        it->result(*this, r);
+    }
+}
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libirccdctl/irccd/irccdctl.hpp	Thu Oct 06 12:36:13 2016 +0200
@@ -0,0 +1,173 @@
+/*
+ * irccdctl.hpp -- main irccdctl class
+ *
+ * Copyright (c) 2013-2016 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.
+ */
+
+#ifndef IRCCD_IRCCDCTL_HPP
+#define IRCCD_IRCCDCTL_HPP
+
+/**
+ * \file irccdctl.hpp
+ * \brief Base class for irccdctl front end.
+ */
+
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "client.hpp"
+#include "alias.hpp"
+#include "options.hpp"
+#include "service-command.hpp"
+
+#include <json.hpp>
+
+namespace irccd {
+
+class Client;
+
+namespace ini {
+
+class Document;
+class Section;
+
+} // !ini
+
+/**
+ * \brief Main irccdctl class.
+ */
+class Irccdctl {
+private:
+    // Commands.
+    CommandService m_commandService;
+
+    // Connection handler.
+    std::unique_ptr<Client> m_connection;
+    std::uint32_t m_timeout{30000};
+    net::Address m_address;
+
+    // Aliases.
+    std::map<std::string, Alias> m_aliases;
+
+    // Incoming data.
+    std::vector<nlohmann::json> m_events;
+    std::vector<nlohmann::json> m_messages;
+
+    void usage() const;
+    void help() const;
+
+    // Parse configuration file.
+    void readConnectIp(const ini::Section &sc);
+    void readConnectLocal(const ini::Section &sc);
+    void readConnect(const ini::Section &sc);
+    void readGeneral(const ini::Section &sc);
+    void readAliases(const ini::Section &sc);
+    void read(const std::string &path);
+
+    // Parse command line options.
+    void parseConnectIp(const option::Result &options);
+    void parseConnectLocal(const option::Result &options);
+    void parseConnect(const option::Result &options);
+    option::Result parse(int &argc, char **&argv);
+
+public:
+    /**
+     * Get the command service.
+     *
+     * \return the command service
+     */
+    inline CommandService &commandService() noexcept
+    {
+        return m_commandService;
+    }
+
+    /**
+     * Get the client connection to irccd.
+     *
+     * \return the connection
+     */
+    inline const Client &client() const noexcept
+    {
+        return *m_connection;
+    }
+
+    /**
+     * Get the client connection to irccd.
+     *
+     * \return the connection
+     */
+    inline Client &client() noexcept
+    {
+        return *m_connection;
+    }
+
+    /**
+     * Get the next message response with the given id.
+     *
+     * If the response id is not provided, get the next incoming message.
+     *
+     * Otherwise, if the id is provided, all other previous messages will be
+     * discarded.
+     *
+     * \param id the response id (e.g. server-message)
+     * \return the next message
+     * \warning this may skip previous events
+     */
+    IRCCD_EXPORT nlohmann::json waitMessage(const std::string id = "");
+
+    /**
+     * Get the next pending even within the internal timeout.
+     *
+     * \return the next event or empty if not available
+     */
+    IRCCD_EXPORT nlohmann::json waitEvent();
+
+    /**
+     * Execute the given command and wait for its result.
+     *
+     * \param cmd the command
+     * \param args the arguments
+     */
+    IRCCD_EXPORT nlohmann::json exec(const Command &cmd, std::vector<std::string> args);
+
+    /**
+     * Execute the given alias.
+     *
+     * \param alias the alias
+     * \param args the arguments
+     */
+    IRCCD_EXPORT std::vector<nlohmann::json> exec(const Alias &alias, std::vector<std::string> args);
+
+    /**
+     * Resolve the command line arguments.
+     *
+     * \param args the main arguments
+     */
+    IRCCD_EXPORT std::vector<nlohmann::json> exec(std::vector<std::string> args);
+
+    /**
+     * Run the irccdctl front end.
+     *
+     * \param argc the number of arguments
+     * \param argv the arguments
+     */
+    IRCCD_EXPORT void run(int argc, char **argv);
+};
+
+} // !irccd
+
+#endif // !IRCCD_IRCCDCTL_HPP