#include #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 "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, ...) { va_list ap; if (use_colors) { if (lvl & DBG_ERROR) fprintf(stderr, ANSI_RED); else if (!(lvl & (DBG_INFO | DBG_VERBOSE))) fprintf(stderr, ANSI_GREY); } va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); if (use_colors && !(lvl & (DBG_INFO | DBG_VERBOSE))) fprintf(stderr, ANSI_NORMAL); } _noreturn_ void __die(const char *fmt, ...) { va_list ap; va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); exit(EXIT_FAILURE); } void * __zmalloc(const char *fn, int line, size_t size) { void *ptr; assert_die(!empty_str(fn) && line > 0 && size > 0, "invalid arguments"); ptr = calloc(1, size); if (!ptr) die("malloc: %m"); return ptr; } char * __xstrdup(const char *fn, int line, const char *s) { char *ptr; assert_die(!empty_str(fn) && line > 0 && !empty_str(s), "invalid arguments"); ptr = strdup(s); if (!ptr) die("strdup: %m"); return ptr; } char * __xstrndup(const char *fn, int line, const char *s, size_t n) { char *ptr; assert_die(!empty_str(fn) && line > 0 && !empty_str(s) && n > 0, "invalid arguments"); ptr = strndup(s, n); if (ptr) die("strdup: %m"); return ptr; } void __xfree(const char *fn, int line, void *ptr) { assert_die(!empty_str(fn) && line > 0, "invalid arguments"); free(ptr); } static void dump_config() { assert_die(cfg, "cfg not set"); info("Configuration:"); info("password : %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("force stop : %s", cfg->force_stop ? "yes" : "no"); info("addrs : %sempty", list_empty(&cfg->addrs) ? "" : "not "); info("mcaddrs : %sempty", list_empty(&cfg->mcaddrs) ? "" : "not "); info("known_servers : %sempty", list_empty(&cfg->addrs) ? "" : "not "); } static void parse_server_config(char *buf, const char *filename) { assert_die(buf && filename && cfg, "invalid arguments"); if (!config_parse_header(SERVER_CFG_HEADER, &buf)) die("Unable to parse %s: invalid/missing header", 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(filename, &buf, scfg_key_map, &key, &keyname, &value, false)) break; switch (key) { case SCFG_KEY_RCON: if (!list_empty(&cfg->addrs)) die("rcon address defined twice in %s", filename); list_replace(&value.saddrs, &cfg->addrs); break; case SCFG_KEY_RCON_PASSWORD: if (!cfg->password) cfg->password = xstrdup(value.str); break; case SCFG_KEY_REMOTE: if (!list_empty(&cfg->mcaddrs)) die("rcon address defined twice in %s", filename); list_replace(&value.saddrs, &cfg->mcaddrs); default: continue; } if (cfg->password && !list_empty(&cfg->addrs) && !list_empty(&cfg->mcaddrs)) break; } if (!cfg->password) verbose("rcon password not found in %s", filename); if (list_empty(&cfg->addrs)) verbose("rcon address not found in %s", filename); if (list_empty(&cfg->mcaddrs)) verbose("mc server address not found in %s", filename); } static void read_server_config() { char buf[4096]; size_t off = 0; ssize_t r; int dfd; int fd; 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", cfg->server->filename); close(dfd); while (true) { r = read(fd, buf + off, sizeof(buf) - off - 1); if (r < 0) die("Failed to read %s: %m", cfg->server->filename); else if (r == 0) break; off += r; 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); dump_config(); } _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 char * strv_join(char * const *strv) { size_t len = 0; char *r, *to; for (unsigned i = 0; strv[i]; i++) len += strlen(strv[i]) + 1; if (len == 0) return NULL; r = zmalloc(len); to = r; for (unsigned i = 0; strv[i]; i++) { if (i > 0) *(to++) = ' '; to = stpcpy(to, strv[i]); } return r; } static void get_known_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; list_for_each_entry(server, &cfg->known_servers, list) info("%s", server->shortname); } static void set_server(const char *name) { struct server *server; assert_die(cfg, "invalid arguments"); assert_die(!cfg->server, "can't set server twice"); 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(false); } char * ask_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; } int connect_any(struct list_head *addrs, bool may_fail) { struct saddr *saddr; bool connected = false; int sfd; if (list_empty(addrs)) die("No address to connect to"); list_for_each_entry(saddr, addrs, list) { sfd = socket(saddr->storage.ss_family, SOCK_STREAM | SOCK_CLOEXEC, 0); if (sfd < 0) die("socket: %m"); socket_set_low_latency(sfd, true, true, true); if (connect(sfd, (struct sockaddr *)&saddr->storage, saddr->addrlen) < 0) { close(sfd); continue; } connected = true; break; } if (!connected) { if (may_fail) return -1; else die("Failed to connect to remote host"); } return sfd; } 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); } 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' }, { "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) { debug_mask = DBG_ERROR | DBG_INFO; set_use_colors(); cfg = zmalloc(sizeof(*cfg)); list_init(&cfg->addrs); list_init(&cfg->mcaddrs); list_init(&cfg->known_servers); parse_cmdline(argc, argv); get_known_servers(); parse_command(&argv[optind]); if (!cfg->cmd) die("Command not parsed correctly"); if (cfg->server) { info("Would read file %s", cfg->server->filename); read_server_config(); } else if (cfg->addrstr) { struct cfg_value value; /* FIXME: create a struct server here, fill in details like name */ if (!strtosockaddrs(cfg->addrstr, &value, false)) die("Unable to parse address: %s", cfg->addrstr); 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); } cfg->cmd(cfg); if (cfg->password) explicit_bzero(cfg->password, strlen(cfg->password)); xfree(cfg->password); xfree(cfg->addrstr); xfree(cfg->mcaddrstr); xfree(cfg); exit(EXIT_SUCCESS); }