#include #include #include #include #include #include #include #include #include #include #include #include #include #include "utils.h" #include "minecctl.h" #include "minecctl-commands.h" #include "minecctl-rcon.h" #include "config-parser.h" #include "server-config-options.h" #include "misc.h" #include "config.h" static struct cfg *cfg = NULL; static void dump_config() { /* FIXME: return unless debugging enabled */ struct server *server; struct saddr *saddr; info("Configuration"); info("============="); info("cfgdir : %s", cfg->cfgdir); info("rcon_password : %s", cfg->rcon_password); info("rcon_addrstr : %s", cfg->rcon_addrstr); info("mc_addrstr : %s", cfg->mc_addrstr); info("cmdstr : %s", cfg->cmdstr); info("cmd : %p", cfg->cmd); info("force stop : %s", cfg->force_stop ? "yes" : "no"); list_for_each_entry(server, &cfg->servers, list) { info(" * server"); info(" name : %s", server->name); info(" filename : %s", server->filename); info(" rcon_password : %s", server->rcon_password); info(" file_read : %s", server->file_read ? "yes" : "no"); list_for_each_entry(saddr, &server->rcon_addrs, list) info(" * rcon addr : %s", saddr->addrstr); list_for_each_entry(saddr, &server->mc_addrs, list) info(" * mc addr : %s", saddr->addrstr); } info("============="); } void read_server_config(struct server *server) { char buf[4096]; size_t off = 0; ssize_t r; int dfd; int fd; char *pos = buf; if (!server || !server->filename || server->file_read) return; server->file_read = true; dfd = open(cfg->cfgdir, O_DIRECTORY | O_PATH | O_CLOEXEC); if (dfd < 0) die("Failed to open %s: %m", cfg->cfgdir); fd = openat(dfd, server->filename, O_RDONLY | O_CLOEXEC); if (fd < 0) die("Failed to open %s: %m", server->filename); close(dfd); while (true) { r = read(fd, buf + off, sizeof(buf) - off - 1); if (r < 0) die("Failed to read %s: %m", server->filename); else if (r == 0) break; off += r; if (off == sizeof(buf) - 1) die("Failed to read %s: file too large", server->filename); } buf[off] = '\0'; close(fd); if (!config_parse_header(SERVER_CFG_HEADER, &pos)) die("Unable to parse %s: invalid/missing header", server->filename); /* FIXME: this will cause superflous DNS lookups of other cfg entries */ while (true) { int key; const char *keyname; struct cfg_value value; if (!config_parse_line(server->filename, &pos, scfg_key_map, &key, &keyname, &value, false)) break; switch (key) { case SCFG_KEY_RCON: if (!list_empty(&server->rcon_addrs)) die("rcon address defined twice in %s", server->filename); list_replace(&value.saddrs, &server->rcon_addrs); break; case SCFG_KEY_RCON_PASSWORD: if (server->rcon_password) die("rcon password defined twice in %s", server->filename); server->rcon_password = xstrdup(value.str); break; case SCFG_KEY_REMOTE: if (!list_empty(&server->mc_addrs)) die("rcon address defined twice in %s", server->filename); list_replace(&value.saddrs, &server->mc_addrs); default: continue; } } if (!server->rcon_password) verbose("rcon password not found in %s", server->filename); if (list_empty(&server->rcon_addrs)) verbose("rcon address not found in %s", server->filename); if (list_empty(&server->mc_addrs)) verbose("mc server address not found in %s", server->filename); } _noreturn_ static void usage(bool no_error) { info("Usage: %s [OPTION...] COMMAND\n" "\n" "Valid options:\n" " -p, --rcon-password=PWD use PWD when connecting via rcon\n" " (or use environment variable RCON_PASSWORD)\n" " -r, --rcon-address=ADDR connect to rcon at ADDR\n" " (or use environment variable RCON_ADDRESS)\n" " -m, --mc-address=ADDR connect to Minecraft server at ADDR\n" " (only relevant for some commands, can also\n" " use environment variable MC_ADDRESS)\n" " -c, --cfgdir=DIR look for server configurations in DIR\n" " (default: %s)\n" " -f, --force stop server even if it has players\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 (including ADDR)\n" " pcount [SERVER] get player count for SERVER\n" " console [SERVER] 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", program_invocation_short_name, DEFAULT_CFG_DIR); exit(no_error ? EXIT_FAILURE : EXIT_SUCCESS); } static struct server * server_new() { struct server *server; server = zmalloc(sizeof(*server)); INIT_LIST_HEAD(&server->rcon_addrs); INIT_LIST_HEAD(&server->mc_addrs); INIT_LIST_HEAD(&server->list); return server; } static void server_free(struct server *server) { struct saddr *saddr, *tmp; xfree(server->name); xfree(server->filename); free_password(&server->rcon_password); list_for_each_entry_safe(saddr, tmp, &server->rcon_addrs, list) { list_del(&saddr->list); xfree(saddr); } list_for_each_entry_safe(saddr, tmp, &server->mc_addrs, list) { list_del(&saddr->list); xfree(saddr); } xfree(server); } static void get_servers() { struct dirent *dent; DIR *dir; 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 = server_new(); server->filename = xstrdup(dent->d_name); suffix = strrchr(dent->d_name, '.'); assert_die(suffix, "Error parsing filename"); *suffix = '\0'; server->name = xstrdup(dent->d_name); list_add(&server->list, &cfg->servers); } closedir(dir); } static bool str_to_addrs(const char *str, struct list_head *list) { struct cfg_value value; char *tmp = NULL; bool rv = false; /* strtosockaddrs mangles the input string */ tmp = xstrdup(str); if (!strtosockaddrs(tmp, &value, false)) { error("Unable to parse address: %s", str); goto out; } if (value.type != CFG_VAL_TYPE_ADDRS) { error("Unexpected return value from strtosockaddrs"); goto out; } if (list_empty(&value.saddrs)) { error("Found no valid addresses for %s", str); goto out; } list_replace(&value.saddrs, list); rv = true; out: xfree(tmp); return rv; } static bool create_server_from_cmdline_args() { struct server *server; if (!cfg->rcon_addrstr && !cfg->mc_addrstr) return false; server = server_new(); if (cfg->rcon_addrstr) { if (!str_to_addrs(cfg->rcon_addrstr, &server->rcon_addrs)) goto error; server->name = cfg->rcon_addrstr; cfg->rcon_addrstr = NULL; } if (cfg->mc_addrstr) { if (!str_to_addrs(cfg->mc_addrstr, &server->mc_addrs)) goto error; if (!server->name) server->name = cfg->mc_addrstr; else xfree(cfg->mc_addrstr); cfg->mc_addrstr = NULL; } if (cfg->rcon_password) { server->rcon_password = cfg->rcon_password; cfg->rcon_password = NULL; } list_add(&server->list, &cfg->servers); return true; error: server_free(server); return false; } static void do_list(struct cfg *cfg) { struct server *server; /* server->filename check excludes servers created from cmdline */ list_for_each_entry(server, &cfg->servers, list) if (server->filename) info("%s", server->name); } struct server * get_default_server(struct cfg *cfg) { struct server *server; server = list_first_entry_or_null(&cfg->servers, struct server, list); if (!server) die("No servers defined"); read_server_config(server); return server; } static void set_default_server(const char *name) { struct server *server; assert_die(cfg, "invalid arguments"); list_for_each_entry(server, &cfg->servers, list) { if (streq(name, server->name)) { list_rotate_to_front(&server->list, &cfg->servers); return; } } error("\"%s\" is not a known server or command", name); usage(false); } static inline void get_optional_server_arg(char * const **argv, bool more) { if (!cfg->rcon_addrstr) { if (!**argv) { error("Missing arguments"); usage(false); } set_default_server(**argv); (*argv)++; } if (!more && **argv) { error("Too many arguments"); usage(false); } } static void parse_command(char * const *argv) { enum commands cmd = CMD_INVALID; assert_die(argv, "invalid arguments"); /* Shorthand for console if address is set */ if (!*argv) { if (!cfg->rcon_addrstr) { error("Missing arguments"); usage(false); } cfg->cmd = do_console; return; } for (unsigned i = 0; command_list[i].name; i++) { if (streq(*argv, command_list[i].name)) { cmd = command_list[i].cmd; argv++; break; } } switch (cmd) { case CMD_LIST: if (*argv) { error("Too many arguments"); usage(false); } cfg->cmd = do_list; break; case CMD_STOPALL: if (*argv) { error("Too many arguments"); usage(false); } cfg->cmd = do_stop_all; break; case CMD_STATUS: get_optional_server_arg(&argv, false); cfg->cmd = do_status; break; case CMD_PING: get_optional_server_arg(&argv, false); cfg->cmd = do_ping; break; case CMD_STOP: get_optional_server_arg(&argv, false); cfg->cmd = do_stop; break; case CMD_PCOUNT: get_optional_server_arg(&argv, false); cfg->cmd = do_pcount; break; case CMD_CONSOLE: get_optional_server_arg(&argv, false); cfg->cmd = do_console; break; case CMD_COMMAND: get_optional_server_arg(&argv, true); if (!*argv) { error("Missing arguments"); usage(false); } cfg->cmdstr = strv_join(argv); cfg->cmd = do_command; break; case CMD_INVALID: /* shorthand notation */ get_optional_server_arg(&argv, true); if (!*argv) { /* !CMD = console */ cfg->cmd = do_console; } else { /* CMD... */ cfg->cmdstr = strv_join(argv); cfg->cmd = do_command; } break; default: die("Unreachable"); } } static void parse_cmdline(int argc, char * const *argv) { int c; char *e; assert_die(argc && argv && cfg, "invalid arguments"); if (argc < 2) { error("Missing options"); usage(false); } cfg->cfgdir = DEFAULT_CFG_DIR; /* FIXME: add lint and debug options */ while (true) { int option_index = 0; static struct option long_options[] = { { "rcon-password", required_argument, 0, 'p' }, { "rcon-address", required_argument, 0, 'r' }, { "mc-address", required_argument, 0, 'm' }, { "cfgdir", required_argument, 0, 'c' }, { "verbose", no_argument, 0, 'v' }, { "force", no_argument, 0, 'f' }, { "help", no_argument, 0, 'h' }, { 0, 0, 0, 0 } }; c = getopt_long(argc, argv, ":p:r:m:c:vfh", long_options, &option_index); if (c == -1) break; switch (c) { case 'p': cfg->rcon_password = xstrdup(optarg); break; case 'r': cfg->rcon_addrstr = xstrdup(optarg); break; case 'm': cfg->mc_addrstr = xstrdup(optarg); break; case 'c': cfg->cfgdir = optarg; break; case 'v': debug_mask |= DBG_VERBOSE; break; case 'f': cfg->force_stop = true; break; case 'h': usage(true); default: error("Invalid options"); usage(false); } } if (!cfg->rcon_password) { e = getenv("RCON_PASSWORD"); if (e) cfg->rcon_password = xstrdup(e); } if (!cfg->rcon_addrstr) { e = getenv("RCON_ADDRESS"); if (e) cfg->rcon_addrstr = xstrdup(e); } if (!cfg->mc_addrstr) { e = getenv("MC_ADDRESS"); if (e) cfg->mc_addrstr = xstrdup(e); } } int main(int argc, char **argv) { int rv = EXIT_FAILURE; struct server *server, *tmp; debug_mask = DBG_ERROR | DBG_INFO; set_use_colors(); cfg = zmalloc(sizeof(*cfg)); INIT_LIST_HEAD(&cfg->servers); parse_cmdline(argc, argv); get_servers(); parse_command(&argv[optind]); if (!cfg->cmd) { error("Failed to parse command"); goto out; } if (cfg->rcon_addrstr || cfg->mc_addrstr) if (!create_server_from_cmdline_args()) goto out; dump_config(); /* FIXME: Should return bool */ cfg->cmd(cfg); rv = EXIT_SUCCESS; out: list_for_each_entry_safe(server, tmp, &cfg->servers, list) server_free(server); free_password(&cfg->rcon_password); xfree(cfg->rcon_addrstr); xfree(cfg->mc_addrstr); xfree(cfg->cmdstr); xfree(cfg); exit(rv); }