diff options
-rw-r--r-- | config.h.in | 3 | ||||
-rw-r--r-- | meson.build | 1 | ||||
-rw-r--r-- | minecctl/meson.build | 4 | ||||
-rw-r--r-- | minecctl/minecctl-rcon.c | 363 | ||||
-rw-r--r-- | minecctl/minecctl-rcon.h | 18 | ||||
-rw-r--r-- | minecctl/minecctl.c | 668 | ||||
-rw-r--r-- | minecctl/minecctl.h | 25 | ||||
-rw-r--r-- | minecproxy/main.c | 10 | ||||
-rw-r--r-- | minecproxy/server-config.c | 23 | ||||
-rw-r--r-- | shared/ansi-colors.h | 12 | ||||
-rw-r--r-- | shared/config-parser.c | 38 | ||||
-rw-r--r-- | shared/config-parser.h | 5 | ||||
-rw-r--r-- | shared/meson.build | 4 | ||||
-rw-r--r-- | shared/rcon-protocol.h | 1 | ||||
-rw-r--r-- | shared/utils.h | 1 |
15 files changed, 1009 insertions, 167 deletions
diff --git a/config.h.in b/config.h.in index 98fc655..bf3bd99 100644 --- a/config.h.in +++ b/config.h.in @@ -23,3 +23,6 @@ #define DEFAULT_SOCKET_IPTOS true #define DEFAULT_SOCKET_NODELAY true + +#define SERVER_CONFIG_FILE_SUFFIX "mcserver" + diff --git a/meson.build b/meson.build index 8160a0a..902b5a9 100644 --- a/meson.build +++ b/meson.build @@ -17,6 +17,7 @@ cc_flags = [ ] cc_warning_flags = [ '-Wno-sign-compare', # Lots of pointless warnings + '-Werror=implicit-function-declaration', # A warning is too weak ] cc_flags += cc.get_supported_arguments(cc_warning_flags) cc_extra_flags = [ diff --git a/minecctl/meson.build b/minecctl/meson.build index ac8210d..9f320a6 100644 --- a/minecctl/meson.build +++ b/minecctl/meson.build @@ -1,9 +1,13 @@ minecctl_sources = [ 'minecctl.c', + 'minecctl-rcon.c', ] +dep_readline = dependency('readline') + minecctl_deps = [ dep_libshared, + dep_readline, ] executable( diff --git a/minecctl/minecctl-rcon.c b/minecctl/minecctl-rcon.c new file mode 100644 index 0000000..f5a9bb5 --- /dev/null +++ b/minecctl/minecctl-rcon.c @@ -0,0 +1,363 @@ +#include <stdio.h> +#include <unistd.h> +#include <errno.h> +#include <ctype.h> +#include <readline/readline.h> +#include <readline/history.h> +#include <alloca.h> +#include <inttypes.h> +#include <string.h> +#include <stdarg.h> + +#include "utils.h" +#include "minecctl.h" +#include "minecctl-rcon.h" +#include "rcon-protocol.h" + +static void +send_packet(int sfd, const char *buf, size_t len) +{ + size_t off = 0; + ssize_t r; + + while (true) { + r = write(sfd, buf + off, len - off); + if (r < 0) { + if (errno == EINTR) + continue; + else + die("Failed to write packet: %m"); + } + + off += r; + if (off == len) + break; + } +} + +/* Note: msg is null-terminated due to the mc protocol trailer */ +static void +read_packet(int sfd, char *buf, size_t len, int32_t *id, int32_t *type, const char **msg) +{ + size_t off = 0; + ssize_t r; + const char *error; + + while (true) { + r = read(sfd, buf + off, len - off); + if (r < 0) { + if (errno == EINTR) + continue; + else + die("Failed to read reply: %m"); + } + + if (r == 0) + die("Failed, connection closed"); + + off += r; + if (rcon_protocol_packet_complete(buf, off)) + break; + + if (off >= len) + die("Reply too large %zu and %zu", off, len); + } + + if (!rcon_protocol_read_packet(buf, off, id, type, msg, &error)) + die("Failed to parse response: %s", error); +} + +static void +send_msg(int sfd, char *buf, size_t len, enum rcon_packet_type type, + const char *msg, enum rcon_packet_type *rtype, const char **reply) +{ + static uint32_t rcon_packet_id = 1; + size_t plen; + int32_t id; + + if (!rcon_protocol_create_packet(buf, len, &plen, + rcon_packet_id, type, msg)) + die("Failed to create rcon packet"); + + send_packet(sfd, buf, plen); + + read_packet(sfd, buf, len, &id, rtype, reply); + + /* FIXME: this should be shared */ + if (type == RCON_PACKET_LOGIN) { + if (*rtype != RCON_PACKET_LOGIN_OK) + die("Invalid reply id"); + + if (id == RCON_PACKET_LOGIN_FAIL) + *rtype = RCON_PACKET_LOGIN_FAIL; + else if (id != rcon_packet_id) + die("Invalid reply id"); + } else { + if (id != rcon_packet_id) + die("Invalid reply"); + } + + rcon_packet_id++; +} + +static void +send_login(struct cfg *cfg) +{ + char buf[4096]; + int32_t rtype; + const char *reply; + + assert_die(cfg && cfg->fd >= 0 && cfg->password, "invalid arguments"); + + send_msg(cfg->fd, buf, sizeof(buf), RCON_PACKET_LOGIN, cfg->password, + &rtype, &reply); + + /* An rcon password isn't exactly super-secret, but can't hurt */ + explicit_bzero(buf, sizeof(buf)); + explicit_bzero(cfg->password, strlen(cfg->password)); + xfree(cfg->password); + cfg->password = NULL; + + if (rtype == RCON_PACKET_LOGIN_OK) + info("Login ok"); + else if (rtype == RCON_PACKET_LOGIN_FAIL) + die("Login failure, invalid password?"); + else + die("Invalid return code: %" PRIi32, rtype); +} + +static void +send_cmd(int sfd, const char *cmd) +{ + char buf[4096]; + int32_t rtype; + const char *reply; + + send_msg(sfd, buf, sizeof(buf), RCON_PACKET_COMMAND, cmd, &rtype, &reply); + + if (rtype != RCON_PACKET_RESPONSE) + die("Invalid return code: %" PRIi32, rtype); + else if (use_colors) + info("%s%s%s", ANSI_GREY, reply, ANSI_NORMAL); + else + info("%s", reply); +} + +static void +eat_whitespace(char **pos) +{ + char *end; + size_t len; + + while(isspace(**pos)) + (*pos)++; + + len = strlen(*pos); + if (len == 0) + return; + + end = *pos + len - 1; + while (isspace(*end)) + end--; + end++; + *end = '\0'; +} + +static void +get_info(int fd, char *buf, size_t buflen, const char *query, const char **reply) +{ + int32_t rtype; + + send_msg(fd, buf, buflen, RCON_PACKET_COMMAND, query, &rtype, reply); + if (rtype != RCON_PACKET_RESPONSE) + die("Invalid return code: %" PRIi32, rtype); +} + +/* midnight = 18000 */ +#define MCTIME_OFFSET 6000 +#define MCTIME_PER_DAY 24000 +#define MCTIME_PER_HOUR 1000 +#define MIN_PER_HOUR 60 + +static inline unsigned +mctime_days(unsigned mctime) { + return (mctime / MCTIME_PER_DAY); +} + +static inline unsigned +mctime_hh(unsigned mctime) { + return (mctime % MCTIME_PER_DAY) / MCTIME_PER_HOUR; +} + +static inline unsigned +mctime_mm(unsigned mctime) { + return ((mctime % MCTIME_PER_HOUR) * MIN_PER_HOUR) / MCTIME_PER_HOUR; +} + +static bool +get_one_status(struct cfg *cfg, char *buf, size_t len, const char *cmd, + size_t argc, const char *replyscan, const char **reply, ...) +{ + va_list ap; + int r; + + get_info(cfg->fd, buf, len, cmd, reply); + + va_start(ap, reply); + r = vsscanf(*reply, replyscan, ap); + va_end(ap); + + if (r == argc) + return true; + else + return false; +} + +void +do_status(struct cfg *cfg) { + char buf[4096]; + char tbuf[4096]; + const char *reply; + unsigned cplayers, maxplayers, gtime; + unsigned epacks, apacks; + unsigned bannedplayers, bannedips; + + send_login(cfg); + + if (get_one_status(cfg, buf, sizeof(buf), "seed", 1, + "Seed : [ %[^]]]", &reply, tbuf)) + info("Seed: %s", tbuf); + + if (get_one_status(cfg, buf, sizeof(buf), "difficulty", 1, + "The difficulty is %s", &reply, tbuf)) + info("Difficulty: %s", tbuf); + + if (get_one_status(cfg, buf, sizeof(buf), "list", 2, + "There are %u of a max %u players online", + &reply, &cplayers, &maxplayers)) + info("Players: %u/%u", cplayers, maxplayers); + + if (get_one_status(cfg, buf, sizeof(buf), "time query day", 1, + "The time is %u", &reply, >ime)) + info("In-game days: %u", gtime); + + if (get_one_status(cfg, buf, sizeof(buf), "time query gametime", 1, + "The time is %u", &reply, >ime)) + info("World age: %ud:%02uh:%02um", + mctime_days(gtime), mctime_hh(gtime), mctime_mm(gtime)); + + if (get_one_status(cfg, buf, sizeof(buf), "time query daytime", 1, + "The time is %u", &reply, >ime)) + info("Current in-game time: %02uh:%02um", + mctime_hh(gtime + MCTIME_OFFSET), mctime_mm(gtime + MCTIME_OFFSET)); + + if (get_one_status(cfg, buf, sizeof(buf), "datapack list enabled", 2, + "There are %u data packs enabled: %[^\n]", &reply, &epacks, tbuf)) + info("Enabled data packs (%u): %s", epacks, tbuf); + + if (get_one_status(cfg, buf, sizeof(buf), "datapack list available", 2, + "There are %u data packs available : %[^\n]", &reply, &apacks, tbuf)) + info("Available data packs (%u): %s", apacks, tbuf); + else if (streq(reply, "There are no more data packs available")) + info("Available data packs: none"); + + if (get_one_status(cfg, buf, sizeof(buf), "banlist players", 1, + "There are %u bans", &reply, &bannedplayers)) + info("Banned players: %u", bannedplayers); + else if (streq(reply, "There are no bans")) + info("Banned players: 0"); + + if (get_one_status(cfg, buf, sizeof(buf), "banlist ips", 1, + "There are %u bans", &reply, &bannedips)) + info("Banned IPs: %u", bannedips); + else if (streq(reply, "There are no bans")) + info("Banned IPs: 0"); +} + +void +do_ping(_unused_ struct cfg *cfg) { + die("Not implemented"); +} + +void +do_stop(_unused_ struct cfg *cfg) { + assert_die(cfg && cfg->fd >= 0, "invalid arguments"); + + send_login(cfg); + + send_cmd(cfg->fd, "stop"); +} + +void +do_stop_all(_unused_ struct cfg *cfg) { + die("Not implemented"); +} + +void +do_pcount(_unused_ struct cfg *cfg) { + send_login(cfg); + + send_cmd(cfg->fd, "list"); +} + +void +do_console(struct cfg *cfg) +{ + char *prompt; + char *cmd; + const char *sname; + + assert_die(cfg && cfg->fd >= 0, "invalid arguments"); + + if (cfg->server) + sname = cfg->server->shortname; + else if (cfg->addrstr) + sname = cfg->addrstr; + else + die("can't find server name"); + + prompt = alloca(strlen(program_invocation_short_name) + + STRLEN(" (") + strlen(sname) + STRLEN("): ") + 1); + sprintf(prompt, "%s (%s): ", program_invocation_short_name, sname); + + send_login(cfg); + + while (true) { + char *tmp; + + cmd = readline(prompt); + if (!cmd) + break; + + tmp = cmd; + eat_whitespace(&tmp); + if (*tmp == '\0') { + xfree(cmd); + continue; + } + + if (streq(tmp, "q") || streq(tmp, "quit") || + streq(tmp, "/q") || streq(tmp, "/quit")) + break; + + send_cmd(cfg->fd, tmp); + + if (streq(tmp, "stop") || streq(tmp, "/stop")) + /* The server waits for us to close the connection */ + break; + + xfree(cmd); + } + + xfree(cmd); +} + +void +do_command(_unused_ struct cfg *cfg) { + assert_die(cfg && cfg->fd >= 0, "invalid arguments"); + + send_login(cfg); + + send_cmd(cfg->fd, cfg->cmdstr); +} + diff --git a/minecctl/minecctl-rcon.h b/minecctl/minecctl-rcon.h new file mode 100644 index 0000000..181dc43 --- /dev/null +++ b/minecctl/minecctl-rcon.h @@ -0,0 +1,18 @@ +#ifndef foominecctlrconhfoo +#define foominecctlrconhfoo + +void do_status(struct cfg *cfg); + +void do_ping(struct cfg *cfg); + +void do_stop(struct cfg *cfg); + +void do_stop_all(struct cfg *cfg); + +void do_pcount(struct cfg *cfg); + +void do_console(struct cfg *cfg); + +void do_command(struct cfg *cfg); + +#endif diff --git a/minecctl/minecctl.c b/minecctl/minecctl.c index e233528..e71a1cb 100644 --- a/minecctl/minecctl.c +++ b/minecctl/minecctl.c @@ -8,12 +8,50 @@ #include <sys/stat.h> #include <fcntl.h> #include <errno.h> +#include <stdlib.h> +#include <getopt.h> +#include <dirent.h> +#include <termios.h> -#include "rcon-protocol.h" #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, ...) { @@ -84,108 +122,19 @@ __xfree(const char *fn, int line, void *ptr) } static void -send_packet(int sfd, const char *buf, size_t len) -{ - size_t off = 0; - ssize_t r; - - while (true) { - r = write(sfd, buf + off, len - off); - if (r < 0) { - if (errno == EINTR) - continue; - else - die("Failed to write packet: %m"); - } - - off += r; - if (off == len) - break; - } -} - -static void -read_packet(int sfd, char *buf, size_t len, int32_t *id, int32_t *type, const char **msg) -{ - size_t off = 0; - ssize_t r; - const char *error; - - while (true) { - r = read(sfd, buf + off, len - off); - if (r < 0) { - if (errno == EINTR) - continue; - else - die("Failed to read reply: %m"); - } - - if (r == 0) - die("Failed, connection closed"); - - off += r; - if (rcon_protocol_packet_complete(buf, off)) - break; - - if (off >= len) - die("Reply too large %zu and %zu", off, len); - } - - if (!rcon_protocol_read_packet(buf, off, id, type, msg, &error)) - die("Failed to parse response: %s", error); -} - -static void -send_msg(int sfd, char *buf, size_t len, enum rcon_packet_type type, - const char *msg, enum rcon_packet_type *rtype, const char **reply) -{ - static uint32_t rcon_packet_id = 1; - size_t plen; - int32_t id; - - if (!rcon_protocol_create_packet(buf, len, &plen, - rcon_packet_id, type, msg)) - die("Failed to create rcon packet"); - - send_packet(sfd, buf, plen); - read_packet(sfd, buf, len, &id, rtype, reply); - - if (id != rcon_packet_id) - die("Invalid reply id"); - - rcon_packet_id++; -} - -static void -send_login(int sfd, const char *password) -{ - char buf[4096]; - int32_t rtype; - const char *reply; - - send_msg(sfd, buf, sizeof(buf), RCON_PACKET_LOGIN, password, &rtype, &reply); - - if (rtype == RCON_PACKET_LOGIN_OK) - info("Login ok"); - else if (rtype == RCON_PACKET_LOGIN_FAIL) - die("Login failure, invalid password?"); - else - die("Invalid return code: %" PRIi32, rtype); -} - -static void -send_cmd(int sfd, const char *cmd) +dump_config() { - char buf[4096]; - int32_t rtype; - const char *reply; - - send_msg(sfd, buf, sizeof(buf), RCON_PACKET_COMMAND, "stop", &rtype, &reply); - - if (rtype == RCON_PACKET_RESPONSE) - info("Command (%s) sent, reply: %s", cmd, reply); - else - die("Invalid return code: %" PRIi32, rtype); + 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 @@ -218,12 +167,13 @@ connect_any(struct list_head *addrs) } static void -parse_config(char *buf, const char *filename, - const char **password, struct list_head *addrs) +parse_server_config(char *buf, const char *filename, + struct list_head *addrs) { - *password = NULL; 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); @@ -242,81 +192,529 @@ parse_config(char *buf, const char *filename, list_replace(&value.saddrs, addrs); break; case SCFG_KEY_RCON_PASSWORD: - *password = value.str; + if (!cfg->password) + cfg->password = xstrdup(value.str); break; default: continue; } - if (*password && !list_empty(addrs)) + if (cfg->password && !list_empty(addrs)) break; } - if (!*password) - die("rcon password not found in %s", filename); + 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_file(const char *filename, char *buf, size_t len) +read_server_config() { - int fd; + char buf[4096]; size_t off = 0; ssize_t r; + int dfd; + int fd; - fd = open(filename, O_RDONLY | O_CLOEXEC); + 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", filename); + die("Failed to open %s: %m", cfg->server->filename); + + close(dfd); while (true) { - r = read(fd, buf + off, len - off - 1); + r = read(fd, buf + off, sizeof(buf) - off - 1); if (r < 0) - die("Failed to read %s: %m", filename); + die("Failed to read %s: %m", cfg->server->filename); else if (r == 0) break; off += r; - if (off == len) - die("Failed to read %s: file too large", filename); + 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) { - char buf[4096]; - const char *password; - const char *filename; - struct list_head addrs; - struct saddr *saddr; - int fd; - debug_mask = DBG_ERROR | DBG_INFO; - if (argc != 2) - die("Usage: minecctl CFGFILE"); + 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); - filename = argv[1]; + read_server_config(); - read_file(filename, buf, sizeof(buf)); + } 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); + } - parse_config(buf, filename, &password, &addrs); + info("here %p", cfg->password); + if (!cfg->password) + cfg->password = prompt_password(); + info("here %p", cfg->password); - info("Password: %s", password); - list_for_each_entry(saddr, &addrs, list) - info("Address: %s", saddr->addrstr); + dump_config(); - fd = connect_any(&addrs); + if (list_empty(&cfg->addrs)) + die("Remote address not found"); - send_login(fd, password); + cfg->fd = connect_any(&cfg->addrs); - send_cmd(fd, "stop"); + cfg->cmd(cfg); + xfree(cfg); exit(EXIT_SUCCESS); } diff --git a/minecctl/minecctl.h b/minecctl/minecctl.h new file mode 100644 index 0000000..4918ca0 --- /dev/null +++ b/minecctl/minecctl.h @@ -0,0 +1,25 @@ +#ifndef foominecctlhfoo +#define foominecctlhfoo + +struct server { + char *filename; + char *shortname; + struct list_head list; +}; + +struct cfg { + char *password; + const char *cfgdir; + const char *addrstr; + char *cmdstr; + struct server *server; + void (*cmd)(struct cfg *cfg); + struct list_head addrs; + struct list_head known_servers; + int fd; +}; + +extern bool use_colors; + +#endif + diff --git a/minecproxy/main.c b/minecproxy/main.c index 790bbfe..99513ee 100644 --- a/minecproxy/main.c +++ b/minecproxy/main.c @@ -27,7 +27,7 @@ #include "igmp.h" #include "idle.h" #include "ptimer.h" -#include <config.h> +#include "config.h" /* Global */ struct cfg *cfg = NULL; @@ -38,14 +38,6 @@ static bool daemonize = false; static FILE *log_file = NULL; static const char *log_file_path = NULL; -#define ANSI_RED "\x1B[0;31m" -#define ANSI_GREEN "\x1B[0;32m" -#define ANSI_YELLOW "\x1B[0;33m" -#define ANSI_BLUE "\x1B[0;34m" -#define ANSI_MAGENTA "\x1B[0;35m" -#define ANSI_GREY "\x1B[0;38;5;245m" -#define ANSI_NORMAL "\x1B[0m" - static void msg(enum debug_lvl lvl, const char *fmt, va_list ap) { diff --git a/minecproxy/server-config.c b/minecproxy/server-config.c index 8786f88..92990d3 100644 --- a/minecproxy/server-config.c +++ b/minecproxy/server-config.c @@ -306,23 +306,6 @@ scfg_open_cb(struct uring_task *task, int res) uring_tbuf_read_until_eof(&server->task, scfg_read_cb); } -static bool -scfg_valid_filename(const char *name) -{ - const char *suffix; - - if (empty_str(name)) - return false; - if (name[0] == '.') - return false; - if ((suffix = strrchr(name, '.')) == NULL) - return false; - if (!streq(suffix, ".server")) - return false; - - return true; -} - struct server_cfg_monitor { struct uring_task task; char buf[4096] _alignas_(struct inotify_event); @@ -417,7 +400,7 @@ inotify_cb(struct uring_task *task, int res) continue; } - if (!scfg_valid_filename(event->name)) + if (!is_valid_server_config_filename(NULL, event->name)) continue; if (event->mask & (IN_MOVED_FROM | IN_DELETE)) @@ -490,9 +473,7 @@ server_cfg_monitor_init() die("opendir(%s): %m", cfg->cfg_dir); while ((dent = readdir(dir)) != NULL) { - if (dent->d_type != DT_REG && dent->d_type != DT_UNKNOWN) - continue; - if (!scfg_valid_filename(dent->d_name)) + if (!is_valid_server_config_filename(dent, NULL)) continue; server = server_new(dent->d_name); diff --git a/shared/ansi-colors.h b/shared/ansi-colors.h new file mode 100644 index 0000000..ba89c46 --- /dev/null +++ b/shared/ansi-colors.h @@ -0,0 +1,12 @@ +#ifndef fooansicolorshfoo +#define fooansicolorshfoo + +#define ANSI_RED "\x1B[0;31m" +#define ANSI_GREEN "\x1B[0;32m" +#define ANSI_YELLOW "\x1B[0;33m" +#define ANSI_BLUE "\x1B[0;34m" +#define ANSI_MAGENTA "\x1B[0;35m" +#define ANSI_GREY "\x1B[0;38;5;245m" +#define ANSI_NORMAL "\x1B[0m" + +#endif diff --git a/shared/config-parser.c b/shared/config-parser.c index 9f294f4..1f44db4 100644 --- a/shared/config-parser.c +++ b/shared/config-parser.c @@ -6,9 +6,12 @@ #include <unistd.h> #include <arpa/inet.h> #include <inttypes.h> +#include <sys/types.h> +#include <dirent.h> #include "utils.h" #include "config-parser.h" +#include "config.h" static void eat_whitespace_and_comments(char **pos) @@ -162,7 +165,7 @@ out: return rv; } -static bool +bool strtosockaddrs(const char *str, struct cfg_value *rvalue, bool async) { struct saddr *saddr; @@ -478,3 +481,36 @@ config_parse_header(const char *title, char **buf) return false; } + +bool +is_valid_server_config_filename(struct dirent *dent, const char *filename) +{ + const char *suffix; + + assert_return(!(dent && filename) && !(!dent && !filename), false); + + /* Maybe accept DT_LNK? */ + if (dent) { + switch (dent->d_type) { + case DT_UNKNOWN: + _fallthrough_; + case DT_REG: + break; + default: + return false; + } + filename = dent->d_name; + } + + if (empty_str(filename)) + return false; + if (filename[0] == '.') + return false; + if ((suffix = strrchr(filename, '.')) == NULL) + return false; + if (!streq(suffix, "." SERVER_CONFIG_FILE_SUFFIX)) + return false; + + return true; +} + diff --git a/shared/config-parser.h b/shared/config-parser.h index 7d99e31..7c5703e 100644 --- a/shared/config-parser.h +++ b/shared/config-parser.h @@ -5,6 +5,7 @@ #include <sys/socket.h> #include <netdb.h> #include <signal.h> +#include <dirent.h> enum cfg_value_type { CFG_VAL_TYPE_INVALID, @@ -47,6 +48,8 @@ struct cfg_value { }; }; +bool strtosockaddrs(const char *str, struct cfg_value *rvalue, bool async); + bool config_parse_line(const char *filename, char **buf, struct cfg_key_value_map *kvmap, int *rkey, const char **rkeyname, @@ -54,4 +57,6 @@ bool config_parse_line(const char *filename, char **buf, bool config_parse_header(const char *title, char **buf); +bool is_valid_server_config_filename(struct dirent *dent, const char *filename); + #endif diff --git a/shared/meson.build b/shared/meson.build index e4e4f29..ccfad4a 100644 --- a/shared/meson.build +++ b/shared/meson.build @@ -6,7 +6,9 @@ srcs_libshared = [ inc_libshared = include_directories('.') -deps_libshared = [] +deps_libshared = [ + dep_config_h, +] lib_libshared = static_library( 'shared', diff --git a/shared/rcon-protocol.h b/shared/rcon-protocol.h index 35997c4..097da1d 100644 --- a/shared/rcon-protocol.h +++ b/shared/rcon-protocol.h @@ -4,6 +4,7 @@ #include <stdbool.h> #include <stdint.h> +/* FIXME: FAIL is an id, not type, LOGIN_OK should be LOGIN_RESPONSE */ enum rcon_packet_type { RCON_PACKET_LOGIN = 3, RCON_PACKET_LOGIN_OK = 2, diff --git a/shared/utils.h b/shared/utils.h index 769d3e3..3ed1c87 100644 --- a/shared/utils.h +++ b/shared/utils.h @@ -40,6 +40,7 @@ extern unsigned debug_mask; #include "list.h" #include "debug.h" #include "external.h" +#include "ansi-colors.h" /* Length of longest DNS name = 253 + trailing dot */ #define FQDN_STR_LEN 254 |