summaryrefslogtreecommitdiff
path: root/minecctl/minecctl.c
diff options
context:
space:
mode:
authorDavid Härdeman <david@hardeman.nu>2020-06-25 17:01:24 +0200
committerDavid Härdeman <david@hardeman.nu>2020-06-25 17:01:24 +0200
commit4d0fcab10e91ad5962837f7dd428f5bca1c8c980 (patch)
tree3529036819aec2a56d769f3f8626fb24c625d4b2 /minecctl/minecctl.c
parent7e980225821aaa3073fc46d2dc248e9571d3c298 (diff)
Flesh out minecctl some more
Diffstat (limited to 'minecctl/minecctl.c')
-rw-r--r--minecctl/minecctl.c668
1 files changed, 533 insertions, 135 deletions
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);
}