summaryrefslogtreecommitdiff
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
parent7e980225821aaa3073fc46d2dc248e9571d3c298 (diff)
Flesh out minecctl some more
-rw-r--r--config.h.in3
-rw-r--r--meson.build1
-rw-r--r--minecctl/meson.build4
-rw-r--r--minecctl/minecctl-rcon.c363
-rw-r--r--minecctl/minecctl-rcon.h18
-rw-r--r--minecctl/minecctl.c668
-rw-r--r--minecctl/minecctl.h25
-rw-r--r--minecproxy/main.c10
-rw-r--r--minecproxy/server-config.c23
-rw-r--r--shared/ansi-colors.h12
-rw-r--r--shared/config-parser.c38
-rw-r--r--shared/config-parser.h5
-rw-r--r--shared/meson.build4
-rw-r--r--shared/rcon-protocol.h1
-rw-r--r--shared/utils.h1
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, &gtime))
+ info("In-game days: %u", gtime);
+
+ if (get_one_status(cfg, buf, sizeof(buf), "time query gametime", 1,
+ "The time is %u", &reply, &gtime))
+ 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, &gtime))
+ 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