/* SPDX-License-Identifier: GPL-2.0 */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "shared/utils.h" #include "minecctl.h" #include "minecctl-commands.h" #include "server.h" #include "rcon-commands.h" #include "misc-commands.h" #include "misc.h" static void dump_server(struct server *server) { struct saddr *saddr; info("│"); info("│ ▷ server"); info("│ ┌─────"); info("│ │ name : %s", server->scfg.name); info("│ │ file_read : %s", server->file_read ? "yes" : "no"); info("│ │ filename : %s", server->scfg.filename); switch (server->scfg.type) { case SERVER_TYPE_ANNOUNCE: info("│ │ type : announce"); break; case SERVER_TYPE_PROXY: info("│ │ type : proxy"); break; case SERVER_TYPE_UNDEFINED: _fallthrough_; default: info("│ │ type : "); break; } info("│ │ pretty_name : %s", server->scfg.pretty_name); switch (server->scfg.announce) { case SERVER_ANNOUNCE_ALWAYS: info("│ │ announce : always"); break; case SERVER_ANNOUNCE_NEVER: info("│ │ announce : never"); break; case SERVER_ANNOUNCE_WHEN_RUNNING: info("│ │ announce : when-running"); break; case SERVER_ANNOUNCE_UNDEFINED: _fallthrough_; default: info("│ │ announce : "); } info("│ │ announce_port : %" PRIu16, server->scfg.announce_port); info("│ │ idle_timeout : %u", server->scfg.idle_timeout); switch (server->scfg.stop_method) { case SERVER_STOP_METHOD_RCON: info("│ │ stop_method : rcon"); break; case SERVER_STOP_METHOD_SYSTEMD: info("│ │ stop_method : systemd"); break; case SERVER_STOP_METHOD_EXEC: info("│ │ stop_method : exec"); break; case SERVER_STOP_METHOD_UNDEFINED: _fallthrough_; default: info("│ │ stop_method : "); break; } switch (server->scfg.start_method) { case SERVER_START_METHOD_SYSTEMD: info("│ │ start_method : systemd"); break; case SERVER_START_METHOD_EXEC: info("│ │ start_method : exec"); break; case SERVER_START_METHOD_UNDEFINED: _fallthrough_; default: info("│ │ start_method : "); break; } info("│ │ stop_exec : %s", server->scfg.stop_exec); info("│ │ start_exec : %s", server->scfg.start_exec); info("│ │ rcon_password : %s", server->scfg.rcon_password); info("│ │ systemd_service : %s", server->scfg.systemd_service); info("│ │ systemd_obj : %s", server->scfg.systemd_obj); info("│ │ locals%s", list_empty(&server->scfg.locals) ? " : none": ""); list_for_each_entry(saddr, &server->scfg.locals, list) info("│ │ ⁃ %s", saddr->addrstr); info("│ │ remotes%s", list_empty(&server->scfg.remotes) ? " : none": ""); list_for_each_entry(saddr, &server->scfg.remotes, list) info("│ │ ⁃ %s", saddr->addrstr); info("│ │ rcons%s", list_empty(&server->scfg.rcons) ? " : none": ""); list_for_each_entry(saddr, &server->scfg.rcons, list) info("│ │ ⁃ %s", saddr->addrstr); info("│ └─────"); } void dump_config(struct cfg *cfg) { struct server *server; if (!(debug_mask & ~(DBG_ERROR | DBG_INFO | DBG_VERBOSE))) return; info("Configuration"); info("┌────────────"); info("│ cfg_dir : %p", cfg->cfg_dir); info("│ data_dir : %p", cfg->data_dir); info("│ rcon_password : %s", cfg->rcon_password); info("│ rcon_addrstr : %s", cfg->rcon_addrstr); info("│ mc_addrstr : %s", cfg->mc_addrstr); info("│ cmd : %p", cfg->cmd); info("│ force : %s", cfg->force ? "yes" : "no"); info("│ default set : %s", cfg->default_set ? "yes" : "no"); info("│ servers loaded: %s", cfg->server_list_loaded ? "yes" : "no"); info("│ commands%s", cfg->commands ? "" : " : none"); if (cfg->commands) { for (char *const *cmd = cfg->commands; *cmd; cmd++) info("│ ⁃ %s", *cmd); } list_for_each_entry(server, &cfg->servers, list) dump_server(server); info("└────────────"); } _noreturn_ static void usage(bool no_error) { info("Usage: %s [OPTIONS...] COMMAND\n" "\n" "Valid commands:\n" " init perform initial setup\n" " new SERVER create a new server\n" " delete SERVER delete a server (-f to delete world as well)\n" " list list known servers\n" " lint check validity of server configuration files\n" " info [SERVER] show information about a running SERVER (or all known servers)\n" " status [SERVER] check if SERVER (or all known servers) is running and accessible\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" " cmds [SERVER] CMDS... send multiple CMDS to SERVER\n" " [SERVER] CMD shorthand for \"cmd [SERVER] CMD\"\n" " [SERVER] shorthand for \"console [SERVER]\"\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" " -f, --force force stop/delete actions\n" " -v, --verbose enable extra logging\n" " -d, --debug enable debugging information\n" " -h, --help print this information\n" "\n" "Note: if ADDR is given as an option, SERVER must be omitted from\n" " the command and vice versa.\n" "\n" "See the minecctl(1) man page for details.\n", program_invocation_short_name); exit(no_error ? EXIT_FAILURE : EXIT_SUCCESS); } 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 cfg *cfg) { struct server *server; if (!cfg->rcon_addrstr && !cfg->mc_addrstr) return false; server = server_new(NULL); if (cfg->rcon_addrstr) { if (!str_to_addrs(cfg->rcon_addrstr, &server->scfg.rcons)) goto error; server->scfg.name = cfg->rcon_addrstr; cfg->rcon_addrstr = NULL; } if (cfg->mc_addrstr) { if (!str_to_addrs(cfg->mc_addrstr, &server->scfg.remotes)) goto error; if (!server->scfg.name) server->scfg.name = cfg->mc_addrstr; else xfree(cfg->mc_addrstr); cfg->mc_addrstr = NULL; } if (cfg->rcon_password) { server->scfg.rcon_password = cfg->rcon_password; cfg->rcon_password = NULL; } list_add(&server->list, &cfg->servers); return true; error: server_free(server); return false; } static inline void get_optional_server_arg(struct cfg *cfg, char *const **argv, bool server_mandatory, bool more) { if (!cfg->rcon_addrstr && !cfg->mc_addrstr) { if (server_mandatory && !**argv) { error("Missing arguments"); usage(false); } if (**argv && !server_set_default(cfg, **argv)) { error("\"%s\" is not a known server or command", **argv); usage(false); } if (**argv) (*argv)++; } if (!more && **argv) { error("Too many arguments"); usage(false); } } static void parse_command(struct cfg *cfg, 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_INIT: if (*argv) { error("Too many arguments"); usage(false); } cfg->cmd = do_init; break; case CMD_NEW: if (!*argv) { error("Missing arguments"); usage(false); } else if (*(argv + 1)) { error("Too many arguments"); usage(false); } cfg->commands = strv_copy(argv); cfg->cmd = do_new; break; case CMD_DELETE: if (!*argv) { error("Missing arguments"); usage(false); } else if (*(argv + 1)) { error("Too many arguments"); usage(false); } cfg->commands = strv_copy(argv); cfg->cmd = do_delete; break; case CMD_LIST: if (*argv) { error("Too many arguments"); usage(false); } cfg->cmd = do_list; break; case CMD_LINT: if (*argv) { error("Too many arguments"); usage(false); } cfg->cmd = do_lint; break; case CMD_STOPALL: if (*argv) { error("Too many arguments"); usage(false); } cfg->cmd = do_stop_all; break; case CMD_INFO: get_optional_server_arg(cfg, &argv, false, false); cfg->cmd = do_info; break; case CMD_STATUS: get_optional_server_arg(cfg, &argv, false, false); cfg->cmd = do_status; break; case CMD_STOP: get_optional_server_arg(cfg, &argv, true, false); cfg->cmd = do_stop; break; case CMD_PCOUNT: get_optional_server_arg(cfg, &argv, true, false); cfg->cmd = do_pcount; break; case CMD_CONSOLE: get_optional_server_arg(cfg, &argv, true, false); cfg->cmd = do_console; break; case CMD_COMMAND: _fallthrough_; case CMD_COMMANDS: get_optional_server_arg(cfg, &argv, true, true); if (!*argv) { error("Missing arguments"); usage(false); } if (cmd == CMD_COMMANDS) cfg->commands = strv_copy(argv); else cfg->commands = strv_from_strs(strv_join(argv), NULL); cfg->cmd = do_commands; break; case CMD_INVALID: /* shorthand notation */ get_optional_server_arg(cfg, &argv, true, true); if (!*argv) { /* !CMD = console */ cfg->cmd = do_console; } else { /* CMD... */ cfg->commands = strv_from_strs(strv_join(argv), NULL); cfg->cmd = do_commands; } break; default: die("Unreachable"); } } static void parse_cmdline(struct cfg *cfg, int argc, char *const *argv) { int c; char *e; assert_die(argc && argv && cfg, "invalid arguments"); if (argc < 2) { error("Missing arguments"); usage(false); } while (true) { int option_index = 0; /* clang-format off */ static struct option long_options[] = { { "rcon-password", required_argument, 0, 'p' }, { "rcon-address", required_argument, 0, 'r' }, { "mc-address", required_argument, 0, 'm' }, { "verbose", no_argument, 0, 'v' }, { "debug", no_argument, 0, 'd' }, { "force", no_argument, 0, 'f' }, { "help", no_argument, 0, 'h' }, { 0, 0, 0, 0 } }; /* clang-format on */ c = getopt_long(argc, argv, ":p:r:m:vdfh", 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 'v': debug_mask |= DBG_VERBOSE; break; case 'd': debug_mask = ~0; break; case 'f': cfg->force = 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 *const *argv) { struct cfg cfg = { .listen_port_min = 20000, .listen_port_max = 23999, .mc_port_min = 24000, .mc_port_max = 27999, .rcon_port_min = 28000, .rcon_port_max = 31999, .servers = LIST_HEAD_INIT(cfg.servers), }; bool success = false; debug_mask = DBG_ERROR | DBG_INFO; set_use_colors(); parse_cmdline(&cfg, argc, argv); parse_command(&cfg, &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(&cfg)) goto out; success = cfg.cmd(&cfg); out: server_free_all(&cfg); free_password(&cfg.rcon_password); strv_free(cfg.commands); xfree(cfg.rcon_addrstr); xfree(cfg.mc_addrstr); if (cfg.cfg_dir) closedir(cfg.cfg_dir); if (cfg.data_dir) closedir(cfg.data_dir); exit(success ? EXIT_SUCCESS : EXIT_FAILURE); }