summaryrefslogtreecommitdiff
path: root/minecctl/minecctl-rcon.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-rcon.c
parent7e980225821aaa3073fc46d2dc248e9571d3c298 (diff)
Flesh out minecctl some more
Diffstat (limited to 'minecctl/minecctl-rcon.c')
-rw-r--r--minecctl/minecctl-rcon.c363
1 files changed, 363 insertions, 0 deletions
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);
+}
+