From 4d0fcab10e91ad5962837f7dd428f5bca1c8c980 Mon Sep 17 00:00:00 2001 From: David Härdeman Date: Thu, 25 Jun 2020 17:01:24 +0200 Subject: Flesh out minecctl some more --- minecctl/minecctl.c | 668 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 533 insertions(+), 135 deletions(-) (limited to 'minecctl/minecctl.c') diff --git a/minecctl/minecctl.c b/minecctl/minecctl.c index e233528..e71a1cb 100644 --- a/minecctl/minecctl.c +++ b/minecctl/minecctl.c @@ -8,12 +8,50 @@ #include #include #include +#include +#include +#include +#include -#include "rcon-protocol.h" #include "utils.h" +#include "minecctl.h" +#include "minecctl-commands.h" +#include "minecctl-rcon.h" #include "config-parser.h" #include "server-config-options.h" +#include "config.h" + +static struct cfg *cfg = NULL; + +bool use_colors = false; + +/* FIXME: Can be shared */ +static void +set_use_colors() +{ + int fd; + const char *e; + if (getenv("NO_COLOR")) + return; + + fd = fileno(stderr); + if (fd < 0) + return; + + if (!isatty(fd)) + return; + + e = getenv("TERM"); + if (!e) + return; + + if (streq(e, "dumb")) + return; + + use_colors = true; +} + void __debug(_unused_ enum debug_lvl lvl, const char *fmt, ...) { @@ -84,108 +122,19 @@ __xfree(const char *fn, int line, void *ptr) } static void -send_packet(int sfd, const char *buf, size_t len) -{ - size_t off = 0; - ssize_t r; - - while (true) { - r = write(sfd, buf + off, len - off); - if (r < 0) { - if (errno == EINTR) - continue; - else - die("Failed to write packet: %m"); - } - - off += r; - if (off == len) - break; - } -} - -static void -read_packet(int sfd, char *buf, size_t len, int32_t *id, int32_t *type, const char **msg) -{ - size_t off = 0; - ssize_t r; - const char *error; - - while (true) { - r = read(sfd, buf + off, len - off); - if (r < 0) { - if (errno == EINTR) - continue; - else - die("Failed to read reply: %m"); - } - - if (r == 0) - die("Failed, connection closed"); - - off += r; - if (rcon_protocol_packet_complete(buf, off)) - break; - - if (off >= len) - die("Reply too large %zu and %zu", off, len); - } - - if (!rcon_protocol_read_packet(buf, off, id, type, msg, &error)) - die("Failed to parse response: %s", error); -} - -static void -send_msg(int sfd, char *buf, size_t len, enum rcon_packet_type type, - const char *msg, enum rcon_packet_type *rtype, const char **reply) -{ - static uint32_t rcon_packet_id = 1; - size_t plen; - int32_t id; - - if (!rcon_protocol_create_packet(buf, len, &plen, - rcon_packet_id, type, msg)) - die("Failed to create rcon packet"); - - send_packet(sfd, buf, plen); - read_packet(sfd, buf, len, &id, rtype, reply); - - if (id != rcon_packet_id) - die("Invalid reply id"); - - rcon_packet_id++; -} - -static void -send_login(int sfd, const char *password) -{ - char buf[4096]; - int32_t rtype; - const char *reply; - - send_msg(sfd, buf, sizeof(buf), RCON_PACKET_LOGIN, password, &rtype, &reply); - - if (rtype == RCON_PACKET_LOGIN_OK) - info("Login ok"); - else if (rtype == RCON_PACKET_LOGIN_FAIL) - die("Login failure, invalid password?"); - else - die("Invalid return code: %" PRIi32, rtype); -} - -static void -send_cmd(int sfd, const char *cmd) +dump_config() { - char buf[4096]; - int32_t rtype; - const char *reply; - - send_msg(sfd, buf, sizeof(buf), RCON_PACKET_COMMAND, "stop", &rtype, &reply); - - if (rtype == RCON_PACKET_RESPONSE) - info("Command (%s) sent, reply: %s", cmd, reply); - else - die("Invalid return code: %" PRIi32, rtype); + assert_die(cfg, "cfg not set"); + + info("Configuration:"); + info("pwd : %s", cfg->password); + info("cfgdir : %s", cfg->cfgdir); + info("addrstr : %s", cfg->addrstr); + info("cmdstr : %s", cfg->cmdstr); + info("server : %p", cfg->server); + info("cmd : %p", cfg->cmd); + info("addrs : %sempty", list_empty(&cfg->addrs) ? "" : "not "); + info("known_servers : %sempty", list_empty(&cfg->addrs) ? "" : "not "); } static int @@ -218,12 +167,13 @@ connect_any(struct list_head *addrs) } static void -parse_config(char *buf, const char *filename, - const char **password, struct list_head *addrs) +parse_server_config(char *buf, const char *filename, + struct list_head *addrs) { - *password = NULL; list_init(addrs); + assert_die(buf && filename && addrs && cfg, "invalid arguments"); + if (!config_parse_header(SERVER_CFG_HEADER, &buf)) die("Unable to parse %s: invalid/missing header", filename); @@ -242,81 +192,529 @@ parse_config(char *buf, const char *filename, list_replace(&value.saddrs, addrs); break; case SCFG_KEY_RCON_PASSWORD: - *password = value.str; + if (!cfg->password) + cfg->password = xstrdup(value.str); break; default: continue; } - if (*password && !list_empty(addrs)) + if (cfg->password && !list_empty(addrs)) break; } - if (!*password) - die("rcon password not found in %s", filename); + if (!cfg->password) + verbose("rcon password not found in %s", filename); if (list_empty(addrs)) die("rcon address not found in %s", filename); } static void -read_file(const char *filename, char *buf, size_t len) +read_server_config() { - int fd; + char buf[4096]; size_t off = 0; ssize_t r; + int dfd; + int fd; - fd = open(filename, O_RDONLY | O_CLOEXEC); + assert_die(cfg && cfg->server && cfg->cfgdir, "invalid arguments"); + + dfd = open(cfg->cfgdir, O_DIRECTORY | O_PATH | O_CLOEXEC); + if (dfd < 0) + die("Failed to open %s: %m", cfg->cfgdir); + + fd = openat(dfd, cfg->server->filename, O_RDONLY | O_CLOEXEC); if (fd < 0) - die("Failed to open %s: %m", filename); + die("Failed to open %s: %m", cfg->server->filename); + + close(dfd); while (true) { - r = read(fd, buf + off, len - off - 1); + r = read(fd, buf + off, sizeof(buf) - off - 1); if (r < 0) - die("Failed to read %s: %m", filename); + die("Failed to read %s: %m", cfg->server->filename); else if (r == 0) break; off += r; - if (off == len) - die("Failed to read %s: file too large", filename); + if (off == sizeof(buf) - 1) + die("Failed to read %s: file too large", cfg->server->filename); } buf[off] = '\0'; close(fd); + + parse_server_config(buf, cfg->server->filename, &cfg->addrs); +} + +_noreturn_ static void +usage(bool invalid) +{ + if (invalid) + info("Invalid option(s)"); + + info("Usage: %s [OPTION...] COMMAND\n" + "\n" + "Valid options:\n" + " -p, --password=PWD use PWD as the rcon password\n" + " (or use environment variable RCON_PASSWORD)\n" + " -a, --address=ADDR connect to rcon at ADDR\n" + " (or use environment variable RCON_ADDRESS)\n" + " -c, --cfgdir=DIR look for server configurations in DIR\n" + " (default: %s)\n" + " -v, --verbose enable extra logging\n" + " -h, --help print this information\n" + "\n" + "Valid commands:\n" + " list list known servers\n" + " status [SERVER] show status of SERVER (or all known servers)\n" + " ping [SERVER] check if SERVER is running\n" + " stop [SERVER] stop SERVER\n" + " stopall stop all known servers\n" + " pcount [SERVER] get player count for SERVER\n" + " console [SERVER] provide an interactive command line for SERVER\n" + " cmd [SERVER] CMD send CMD to SERVER\n" + " [SERVER] CMD shorthand for \"cmd [SERVER] CMD\"\n" + " [SERVER] shorthand for \"console [SERVER]\"\n" + "\n" + "Note: if ADDR is given as an option, SERVER must be omitted from\n" + " the command and vice versa.\n" + "\n", + program_invocation_short_name, DEFAULT_CFG_DIR); + + exit(invalid ? EXIT_FAILURE : EXIT_SUCCESS); +} + +static char * +combine_args(int index, int remain, char **argv) +{ + size_t len = 0; + char *result, *pos; + + if (index < 0 || remain < 0) + die("Internal error parsing arguments"); + + for (int i = index; i < index + remain; i++) + len += strlen(argv[i]) + 1; + + if (len == 0) + die("Internal error parsing arguments"); + + len++; + result = zmalloc(len); + pos = result; + for (int i = index; i < index + remain; i++) + pos += sprintf(pos, "%s ", argv[i]); + + pos--; + *pos = '\0'; + + return result; +} + +static void +get_servers() +{ + static bool first = true; + struct dirent *dent; + DIR *dir; + + if (!first) + return; + + first = false; + + dir = opendir(cfg->cfgdir); + if (!dir) { + info("Can't open config directory %s: %m", cfg->cfgdir); + return; + } + + while ((dent = readdir(dir))) { + struct server *server; + char *suffix; + + if (!is_valid_server_config_filename(dent, NULL)) + continue; + + server = zmalloc(sizeof(*server)); + server->filename = xstrdup(dent->d_name); + + suffix = strrchr(dent->d_name, '.'); + assert_die(suffix, "Error parsing filename"); + *suffix = '\0'; + server->shortname = xstrdup(dent->d_name); + + list_add(&server->list, &cfg->known_servers); + } + + closedir(dir); +} + +static void +do_list(struct cfg *cfg) +{ + struct server *server; + + get_servers(); + + list_for_each_entry(server, &cfg->known_servers, list) + info("%s", server->shortname); +} + +static bool +is_known_server(const char *name) +{ + struct server *server; + + assert_die(cfg && name, "invalid arguments"); + + get_servers(); + list_for_each_entry(server, &cfg->known_servers, list) { + if (streq(name, server->shortname)) + return true; + } + + return false; +} + +static void +set_server(const char *name) +{ + struct server *server; + + assert_die(cfg, "invalid arguments"); + assert_die(!cfg->server, "can't set server twice"); + + get_servers(); + list_for_each_entry(server, &cfg->known_servers, list) { + if (streq(name, server->shortname)) { + cfg->server = server; + return; + } + } + + error("%s is not a known server", name); + usage(true); +} + +static char * +prompt_password() +{ + struct termios old, new; + char *password = NULL; + size_t len = 0; + ssize_t r; + + assert_die(cfg, "invalid arguments"); + + if (!isatty(STDIN_FILENO)) + return NULL; + + if (tcgetattr(STDIN_FILENO, &old) < 0) + return NULL; + + new = old; + new.c_lflag &= ~ECHO; + new.c_lflag |= ICANON; + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &new) < 0) + return NULL; + + fprintf(stderr, "Password: "); + r = getline(&password, &len, stdin); + + tcsetattr(STDIN_FILENO, TCSAFLUSH, &old); + + if (r < 0) { + info("Error in getline: %m"); + clearerr(stdin); + free(password); + return NULL; + } + + while (r > 0 && password[r - 1] == '\n') + password[--r] = '\0'; + + return password; +} + +static void +parse_verb(int index, int remain, char **argv) +{ + enum command_args args; + enum commands cmd; + + assert_die(index >= 0 && remain >= 0 && argv && cfg, "invalid arguments"); + + cmd = CMD_INVALID; + + info("start verb, remain %i, index %i", remain, index); + for (int i = index; i < index + remain; i++) + info("arg[%i]: %s", i, argv[i]); + + if (remain == 0) { + /* shorthand for console if address is set */ + if (!cfg->addrstr) + usage(true); + + cmd = CMD_CONSOLE; + goto out; + } + + args = CMD_ARG_INVALID; + for (int i = 0; command_list[i].name; i++) { + if (streq(argv[index], command_list[i].name)) { + cmd = command_list[i].cmd; + args = command_list[i].args; + break; + } + } + + if (cmd == CMD_INVALID) { + /* maybe shorthand for console: [SERVER] CMD */ + if (cfg->addrstr && is_known_server(argv[index])) { + error("Ambigous command, address set and server specified"); + usage(true); + } else if (cfg->addrstr) { + /* CMD */ + cmd = CMD_COMMAND; + cfg->cmdstr = combine_args(index, remain, argv); + goto out; + } else { + /* SERVER [CMD] */ + set_server(argv[index]); + index++; + remain--; + + if (remain < 1) + cmd = CMD_CONSOLE; + else { + cmd = CMD_COMMAND; + cfg->cmdstr = combine_args(index, remain, argv); + } + goto out; + } + } + + index++; + remain--; + + info("here: index %i, remain %i, args %i", index, remain, args); + + switch (args) { + case CMD_ARG_NONE: + info("here: index %i, remain %i, args %i", index, remain, args); + if (remain != 0) + usage(true); + info("here: index %i, remain %i, args %i", index, remain, args); + break; + + case CMD_ARG_ONE_OPTIONAL: + info("hereY: index %i, remain %i, args %i", index, remain, args); + if (remain == 0 && cfg->addrstr) + break; + else if (remain == 1 && !cfg->addrstr) { + set_server(argv[index]); + index++; + remain--; + break; + } + usage(true); + + case CMD_ARG_AT_LEAST_ONE: + info("hereX: index %i, remain %i, args %i", index, remain, args); + if (remain > 0 && cfg->addrstr) + break; + else if (remain > 1 && !cfg->addrstr) { + set_server(argv[index]); + index++; + remain--; + break; + } + usage(true); + + case CMD_ARG_INVALID: + info("hereZ: index %i, remain %i, args %i", index, remain, args); + _fallthrough_; + default: + info("hereT: index %i, remain %i, args %i", index, remain, args); + die("Internal cmd parsing error"); + } + +out: + switch (cmd) { + case CMD_LIST: + cfg->cmd = do_list; + break; + case CMD_STATUS: + cfg->cmd = do_status; + break; + case CMD_PING: + cfg->cmd = do_ping; + break; + case CMD_STOP: + cfg->cmd = do_stop; + break; + case CMD_STOPALL: + cfg->cmd = do_stop_all; + break; + case CMD_PCOUNT: + cfg->cmd = do_pcount; + break; + case CMD_CONSOLE: + cfg->cmd = do_console; + break; + case CMD_COMMAND: + cfg->cmd = do_command; + cfg->cmdstr = combine_args(index, remain, argv); + remain = 0; + break; + default: + die("Internal cmd parsing error"); + } + + if (args == CMD_ARG_NONE && !cfg->server && !cfg->cmdstr) + return; + + if (cfg->addrstr && cfg->server) + usage(true); + + dump_config(); + + info("here2: index %i, remain %i, args %i", index, remain, args); + if ((!cfg->addrstr && !cfg->server) || + (cfg->cmdstr && cmd != CMD_COMMAND) || + (!cfg->cmdstr && cmd == CMD_COMMAND) || + (remain != 0) || (cmd == CMD_INVALID)) + die("Internal cmd parsing error"); +} + +static void +parse_cmdline(int argc, char **argv) +{ + int c; + + assert_die(argc && argv && cfg, "invalid arguments"); + + if (argc < 2) + usage(true); + + list_init(&cfg->addrs); + cfg->cfgdir = DEFAULT_CFG_DIR; + + while (true) { + int option_index = 0; + static struct option long_options[] = { + { "password", required_argument, 0, 'p' }, + { "address", required_argument, 0, 'a' }, + { "cfgdir", required_argument, 0, 'c' }, + { "verbose", no_argument, 0, 'v' }, + { "help", no_argument, 0, 'h' }, + { 0, 0, 0, 0 } + }; + + c = getopt_long(argc, argv, ":p:a:c:vh", + long_options, &option_index); + + if (c == -1) + break; + + switch (c) { + case 'p': + cfg->password = xstrdup(optarg); + break; + case 'a': + cfg->addrstr = xstrdup(optarg); + break; + case 'c': + cfg->cfgdir = optarg; + break; + case 'v': + debug_mask |= DBG_VERBOSE; + break; + case 'h': + usage(false); + default: + usage(true); + } + } + + if (!cfg->password) { + char *e; + + e = getenv("RCON_PASSWORD"); + if (e) + cfg->password = xstrdup(e); + } + + if (!cfg->addrstr) { + char *e; + + e = getenv("RCON_ADDRESS"); + if (e) + cfg->addrstr = xstrdup(e); + } + + parse_verb(optind, argc - optind, argv); + } int main(int argc, char **argv) { - char buf[4096]; - const char *password; - const char *filename; - struct list_head addrs; - struct saddr *saddr; - int fd; - debug_mask = DBG_ERROR | DBG_INFO; - if (argc != 2) - die("Usage: minecctl CFGFILE"); + set_use_colors(); + + cfg = zmalloc(sizeof(*cfg)); + cfg->fd = -1; + list_init(&cfg->addrs); + list_init(&cfg->known_servers); + + parse_cmdline(argc, argv); + + dump_config(); + + if (!cfg->cmd) + die("Command not parsed correctly"); + + if (cfg->server) { + info("Would read file %s", cfg->server->filename); - filename = argv[1]; + read_server_config(); - read_file(filename, buf, sizeof(buf)); + } else if (cfg->addrstr) { + struct cfg_value value; + + if (!strtosockaddrs(cfg->addrstr, &value, false)) + die("Unable to connect"); + + if (value.type != CFG_VAL_TYPE_ADDRS) + die("Unexpected return value from strtosockaddrs"); + + if (list_empty(&value.saddrs)) + die("Found no valid addresses for %s", cfg->addrstr); + + list_replace(&value.saddrs, &cfg->addrs); + } - parse_config(buf, filename, &password, &addrs); + info("here %p", cfg->password); + if (!cfg->password) + cfg->password = prompt_password(); + info("here %p", cfg->password); - info("Password: %s", password); - list_for_each_entry(saddr, &addrs, list) - info("Address: %s", saddr->addrstr); + dump_config(); - fd = connect_any(&addrs); + if (list_empty(&cfg->addrs)) + die("Remote address not found"); - send_login(fd, password); + cfg->fd = connect_any(&cfg->addrs); - send_cmd(fd, "stop"); + cfg->cmd(cfg); + xfree(cfg); exit(EXIT_SUCCESS); } -- cgit v1.2.3