Mercurial > irccd
view lib/irccd/irccdctl.cpp @ 203:c26754e419c4
Irccd: rename RemoteCommand to Command
author | David Demelier <markand@malikania.fr> |
---|---|
date | Thu, 09 Jun 2016 13:50:55 +0200 |
parents | b87679fbf7d8 |
children | 6635b9187d71 |
line wrap: on
line source
/* * 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 <algorithm> #include <cassert> #include <iterator> #include <memory> #include <string> #include <unordered_map> #include <format.h> #include "command.hpp" #include "elapsed-timer.hpp" #include "fs.hpp" #include "ini.hpp" #include "irccdctl.hpp" #include "json.hpp" #include "net.hpp" #include "logger.hpp" #include "options.hpp" #include "path.hpp" #include "sysconfig.hpp" #include "system.hpp" #include "util.hpp" namespace irccd { using namespace fmt::literals; using namespace net; using namespace net::address; using namespace net::option; using namespace net::protocol; using namespace std::placeholders; using namespace std::chrono_literals; using namespace std::string_literals; /* * Config file format * ------------------------------------------------------------------ * * [connect] * type = "ip | unix" * * # if ip * host = "" * port = number * domain = "ipv4 | ipv6", default: ipv4 * * # if unix * path = "" * * [alias] * name = replacement */ /* * Initialize a connection from the configuration file * ------------------------------------------------------------------ */ void Irccdctl::usage() const { // TODO: CHANGE log::warning() << "usage: " << sys::programName() << " [options...] <command> [command-options...] [command-args...]\n\n"; log::warning() << "General options:\n"; log::warning() << "\tc, --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() << "General commands:\n"; log::warning() << "\thelp\t\t\tShow an help topic\n"; log::warning() << "\twatch\t\t\tStart listening to irccd\n\n"; log::warning() << "Plugin management:\n"; log::warning() << "\tplugin-config\t\tGet or set a plugin configuration option\n"; log::warning() << "\tplugin-info\t\tGet plugin information\n"; log::warning() << "\tplugin-list\t\tList all loaded plugins\n"; log::warning() << "\tplugin-load\t\tLoad a plugin\n"; log::warning() << "\tplugin-reload\t\tReload a plugin\n"; log::warning() << "\tplugin-unload\t\tUnload a plugin\n\n"; log::warning() << "Server management:\n"; log::warning() << "\tserver-cmode\t\tChange a channel mode\n"; log::warning() << "\tserver-cnotice\t\tSend a channel notice\n"; log::warning() << "\tserver-connect\t\tConnect to a server\n"; log::warning() << "\tserver-disconnect\tDisconnect from a server\n"; log::warning() << "\tserver-info\t\tGet server information\n"; log::warning() << "\tserver-invite\t\tInvite someone to a channel\n"; log::warning() << "\tserver-join\t\tJoin a channel\n"; log::warning() << "\tserver-kick\t\tKick someone from a channel\n"; log::warning() << "\tserver-list\t\tList all servers\n"; log::warning() << "\tserver-me\t\tSend a CTCP Action (same as /me)\n"; log::warning() << "\tserver-message\t\tSend a message to someone or a channel\n"; log::warning() << "\tserver-mode\t\tChange a user mode\n"; log::warning() << "\tserver-notice\t\tSend a private notice\n"; log::warning() << "\tserver-nick\t\tChange your nickname\n"; log::warning() << "\tserver-part\t\tLeave a channel\n"; log::warning() << "\tserver-reconnect\tReconnect one or all servers\n"; log::warning() << "\tserver-topic\t\tChange a channel topic\n"; log::warning() << "\nFor more information on a command, type " << sys::programName() << " help <command>" << std::endl; std::exit(1); } void Irccdctl::readConnectIp(const ini::Section &sc) { ini::Section::const_iterator it; // Host. std::string host; if ((it = sc.find("host")) == sc.end()) throw std::invalid_argument("missing host parameter"); host = it->value(); // Port. std::uint16_t port; if ((it = sc.find("port")) == sc.end()) throw std::invalid_argument("missing port parameter"); try { port = util::toNumber<std::uint16_t>(it->value()); } catch (...) { throw std::invalid_argument("invalid port number: " + it->value()); } // Domain. 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_connection = std::make_unique<ConnectionBase<Ip>>(Ip::resolve(host, std::to_string(port), domain)); } void Irccdctl::readConnectUnix(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_connection = std::make_unique<ConnectionBase<Local>>(Local{it->value(), false}); #else (void)sc; throw std::invalid_argument("unix connection not supported on Windows"); #endif } 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") readConnectUnix(sc); else throw std::invalid_argument("invalid type given: " + it->value()); } void Irccdctl::readGeneral(const ini::Section &sc) { auto verbose = sc.find("verbose"); if (verbose != sc.end()) log::setVerbose(util::isBoolean(verbose->value())); } void Irccdctl::readAliases(const ini::Section &sc) { for (const ini::Option &option : sc) { // This is the alias name. Alias alias(option.key()); if (m_commandService.contains(option.key())) throw std::invalid_argument("there is already a command named " + option.key()); // Iterate over the list of commands to execute for this alias. for (const std::string &repl : option) { // This is the alias split string. std::vector<std::string> 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. std::string command = list[0]; // This is the alias arguments. std::vector<AliasArg> args; for (auto it = list.begin() + 1; it != list.end(); ++it) args.push_back(std::move(*it)); alias.push_back({std::move(command), std::move(args)}); } // Show for debugging purpose. log::debug("alias {}:"_format(option.key())); for (const auto &cmd : alias) log::debug(" {} {}"_format(cmd.command(), util::join(cmd.args().begin(), cmd.args().end(), ' '))); m_aliases.emplace(option.key(), std::move(alias)); } } void Irccdctl::read(const std::string &path, const parser::Result &options) { ini::Document doc = ini::readFile(path); ini::Document::const_iterator it = doc.find("connect"); // Do not try to read [connect] if specified at command line. if (it != doc.end() && options.count("-t") == 0 && options.count("--type") == 0) readConnect(*it); if ((it = doc.find("general")) != doc.end()) readGeneral(*it); if ((it = doc.find("alias")) != doc.end()) readAliases(*it); } /* * Initialize a connection from the command line. * ------------------------------------------------------------------ */ void Irccdctl::parseConnectIp(const parser::Result &options, bool ipv6) { parser::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::uint16_t port; if ((it = options.find("-p")) == options.end() && (it = options.find("--port")) == options.end()) throw std::invalid_argument("missing port argument (-p or --port)"); try { port = util::toNumber<std::uint16_t>(it->second); } catch (...) { throw std::invalid_argument("invalid port number: " + it->second); } // Domain int domain = (ipv6) ? AF_INET6 : AF_INET; m_connection = std::make_unique<ConnectionBase<Ip>>(Ip::resolve(host, std::to_string(port), domain, SOCK_STREAM)); } void Irccdctl::parseConnectUnix(const parser::Result &options) { #if !defined(IRCCD_SYSTEM_WINDOWS) parser::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_connection = std::make_unique<ConnectionBase<Local>>(Local{it->second, false}); #else (void)options; throw std::invalid_argument("unix connection not supported on Windows"); #endif } void Irccdctl::parseConnect(const parser::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, it->second == "ipv6"); if (it->second == "unix") return parseConnectUnix(options); throw std::invalid_argument("invalid type given: " + it->second); } parser::Result Irccdctl::parse(int &argc, char **&argv) const { // 1. Parse command line options. parser::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 } }; parser::Result result; try { result = parser::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; } void Irccdctl::exec(const Command &cmd, std::vector<std::string> args) { // 1. Build options from command line arguments. parser::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 : parser::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"); if (args.size() > cmd.max()) throw std::runtime_error("too many arguments"); // 4. Construct the request, if the returned value is not an object, do not send anything (e.g. help). json::Value request = cmd.request(*this, CommandRequest(std::move(requestOptions), std::move(args))); if (!request.isObject()) return; request.insert("command", cmd.name()); // 5. Send the command. m_connection->send(request.toJson(0), 30000); // 6. Parse the result. cmd.result(*this, m_connection->next(cmd.name(), 30000)); } void Irccdctl::exec(const Alias &alias, std::vector<std::string> argsCopy) { 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 AliasArg &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. exec(cmdArgs); } } void 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()); if (alias != m_aliases.end()) exec(alias->second, args); else { auto cmd = m_commandService.find(name); if (cmd) exec(*cmd, args); else throw std::invalid_argument("no alias or command named " + name); } } void Irccdctl::connect() { log::info("{}: connecting to irccd..."_format(sys::programName())); // Try to connect. m_connection->connect(30000); // Get irccd information. json::Value object = m_connection->next(30000); if (!object.contains("program") || object.at("program").toString() != "irccd") throw std::runtime_error("not an irccd server"); // Get values. m_major = object.at("major").toInt(); m_minor = object.at("minor").toInt(); m_patch = object.at("patch").toInt(); m_javascript = object.valueOr("javascript", json::Type::Boolean, false).toBool(); m_ssl = object.valueOr("ssl", json::Type::Boolean, false).toBool(); log::info() << std::boolalpha; log::info("{}: connected to irccd {}.{}.{}"_format(sys::programName(), m_major, m_minor, m_patch)); log::info("{}: javascript: {}, ssl support: {}"_format(sys::programName(), m_javascript, m_ssl)); } void Irccdctl::run(int argc, char **argv) { // 1. Read command line arguments. parser::Result 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, result); else { for (const std::string &dir : path::list(path::PathConfig)) { std::string path = dir + "irccdctl.conf"; if (fs::exists(path)) read(path, result); } } } catch (const std::exception &ex) { log::warning("{}: {}"_format(sys::programName(), ex.what())); 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); } connect(); } // Build a vector of arguments. std::vector<std::string> args; for (int i = 0; i < argc; ++i) args.push_back(argv[i]); exec(args); } } // !irccd