Mercurial > irccd
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