From 4d0fcab10e91ad5962837f7dd428f5bca1c8c980 Mon Sep 17 00:00:00 2001 From: David Härdeman Date: Thu, 25 Jun 2020 17:01:24 +0200 Subject: Flesh out minecctl some more --- minecctl/minecctl-rcon.c | 363 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 363 insertions(+) create mode 100644 minecctl/minecctl-rcon.c (limited to 'minecctl/minecctl-rcon.c') 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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); +} + -- cgit v1.2.3