#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; va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); } _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("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 connect_any(struct list_head *addrs) { struct saddr *saddr; bool connected = false; int sfd; 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) die("Failed to connect to remote host"); return sfd; } static void parse_server_config(char *buf, const char *filename, struct list_head *addrs) { 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); /* 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: list_replace(&value.saddrs, addrs); break; case SCFG_KEY_RCON_PASSWORD: if (!cfg->password) cfg->password = xstrdup(value.str); break; default: continue; } if (cfg->password && !list_empty(addrs)) break; } 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_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, &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) { debug_mask = DBG_ERROR | DBG_INFO; 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); read_server_config(); } 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); } info("here %p", cfg->password); if (!cfg->password) cfg->password = prompt_password(); info("here %p", cfg->password); dump_config(); if (list_empty(&cfg->addrs)) die("Remote address not found"); cfg->fd = connect_any(&cfg->addrs); cfg->cmd(cfg); xfree(cfg); exit(EXIT_SUCCESS); }