#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() { info("Configuration:"); info("password : %s", cfg->password); info("cfgdir : %s", cfg->cfgdir); info("addrstr : %s", cfg->addrstr); info("cmdstr : %s", cfg->cmdstr); info("cmd : %p", cfg->cmd); info("force stop : %s", cfg->force_stop ? "yes" : "no"); info("servers : %sempty", list_empty(&cfg->servers) ? "" : "not "); /* FIXME: dump servers */ } 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, --password=PWD use PWD as the rcon password\n" " (or use environment variable RCON_PASSWORD)\n" " -a, --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\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 * allocate_server() { 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 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 = allocate_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->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->addrstr && !cfg->mcaddrstr) return false; server = allocate_server(); if (cfg->addrstr) { if (!str_to_addrs(cfg->addrstr, &server->rcon_addrs)) goto error; server->shortname = cfg->addrstr; cfg->addrstr = NULL; } if (cfg->mcaddrstr) { if (!str_to_addrs(cfg->mcaddrstr, &server->mc_addrs)) goto error; if (!server->shortname) server->shortname = cfg->mcaddrstr; else xfree(cfg->mcaddrstr); cfg->mcaddrstr = NULL; } if (cfg->password) { server->rcon_password = cfg->password; cfg->password = NULL; } list_add(&server->list, &cfg->servers); return true; error: /* FIXME: free addrs */ xfree(server->shortname); xfree(server); return false; } static void do_list(struct cfg *cfg) { struct server *server; list_for_each_entry(server, &cfg->servers, list) info("%s", server->shortname); } static void set_server(const char *name) { struct server *server; assert_die(cfg, "invalid arguments"); list_for_each_entry(server, &cfg->servers, list) { if (streq(name, server->shortname)) { /* This puts the chosen server first in the list */ list_del(&server->list); list_add(&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->addrstr) { if (!**argv) { error("Missing arguments"); usage(false); } set_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->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_list; 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"); } dump_config(); } 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; while (true) { int option_index = 0; static struct option long_options[] = { { "password", required_argument, 0, 'p' }, { "rcon-address", required_argument, 0, 'a' }, { "mc-address", required_argument, 0, 'A' }, { "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:a:m:c:fvh", 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 'm': cfg->mcaddrstr = xstrdup(optarg); break; case 'c': cfg->cfgdir = optarg; break; case 'f': cfg->force_stop = true; break; case 'v': debug_mask |= DBG_VERBOSE; break; case 'h': usage(true); default: error("Invalid options"); usage(false); } } if (!cfg->password) { e = getenv("RCON_PASSWORD"); if (e) cfg->password = xstrdup(e); } if (!cfg->addrstr) { e = getenv("RCON_ADDRESS"); if (e) cfg->addrstr = xstrdup(e); } if (!cfg->mcaddrstr) { e = getenv("MC_ADDRESS"); if (e) cfg->mcaddrstr = xstrdup(e); } } int main(int argc, char **argv) { int rv = EXIT_FAILURE; 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->addrstr || cfg->mcaddrstr) if (!create_server_from_cmdline_args()) goto out; /* FIXME: Should return bool */ cfg->cmd(cfg); rv = EXIT_SUCCESS; out: /* FIXME: Not enough cleanup */ if (cfg->password) explicit_bzero(cfg->password, strlen(cfg->password)); xfree(cfg->password); xfree(cfg->addrstr); xfree(cfg->mcaddrstr); xfree(cfg); exit(rv); }