diff options
Diffstat (limited to 'minecctl/rcon-commands.c')
-rw-r--r-- | minecctl/rcon-commands.c | 470 |
1 files changed, 470 insertions, 0 deletions
diff --git a/minecctl/rcon-commands.c b/minecctl/rcon-commands.c new file mode 100644 index 0000000..02b970f --- /dev/null +++ b/minecctl/rcon-commands.c @@ -0,0 +1,470 @@ +#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 "rcon-commands.h" +#include "server.h" +#include "rcon-protocol.h" +#include "misc.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 int +rcon_login(struct cfg *cfg, struct server *server) +{ + char buf[4096]; + int32_t rtype; + const char *reply; + int fd = -1; + + assert_die(cfg && server, "invalid arguments"); + + if (list_empty(&server->rcon_addrs)) { + error("%s: rcon address unknown", server->name); + goto error; + } + + fd = connect_any(&server->rcon_addrs, true); + if (fd < 0) { + error("%s: unable to connect", server->name); + goto error; + } + + if (!server->rcon_password) + server->rcon_password = ask_password(); + + if (!server->rcon_password) { + error("%s: can't login - password missing", server->name); + goto error; + } + + send_msg(fd, buf, sizeof(buf), RCON_PACKET_LOGIN, server->rcon_password, + &rtype, &reply); + + explicit_bzero(buf, sizeof(buf)); + free_password(&server->rcon_password); + + if (rtype == RCON_PACKET_LOGIN_OK) + verbose("%s: login ok", server->name); + else if (rtype == RCON_PACKET_LOGIN_FAIL) { + info("%s: login failure, invalid password?", server->name); + goto error; + } else { + error("%s: invalid return code: %" PRIi32, server->name, rtype); + goto error; + } + + return fd; + +error: + close(fd); + return -1; +} + +static bool +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); + return false; + } + + + if (use_colors) + info("%s%s%s", ANSI_GREY, reply, ANSI_NORMAL); + else + info("%s", reply); + + return true; +} + +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'; +} + +/* 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(int fd, char *buf, size_t len, const char *cmd, + size_t argc, const char *replyscan, const char **reply, ...) +{ + int32_t rtype; + va_list ap; + int r; + + send_msg(fd, buf, len, RCON_PACKET_COMMAND, cmd, &rtype, reply); + if (rtype != RCON_PACKET_RESPONSE) + die("Invalid return code: %" PRIi32, rtype); + + va_start(ap, reply); + r = vsscanf(*reply, replyscan, ap); + va_end(ap); + + if (r == argc) + return true; + else + return false; +} + +bool +do_status(struct cfg *cfg) { + char buf[4096]; + char tbuf[4096]; + const char *reply; + unsigned cplayers, maxplayers, gtime; + unsigned epacks, apacks; + unsigned bannedplayers, bannedips; + int fd; + struct server *server; + + server = server_get_default(cfg); + fd = rcon_login(cfg, server); + if (fd < 0) + return false; + + if (get_one_status(fd, buf, sizeof(buf), "seed", 1, + "Seed : [ %[^]]]", &reply, tbuf)) + info("Seed: %s", tbuf); + + if (get_one_status(fd, buf, sizeof(buf), "difficulty", 1, + "The difficulty is %s", &reply, tbuf)) + info("Difficulty: %s", tbuf); + + if (get_one_status(fd, 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(fd, buf, sizeof(buf), "time query day", 1, + "The time is %u", &reply, >ime)) + info("In-game days: %u", gtime); + + if (get_one_status(fd, 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(fd, 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(fd, 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(fd, 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(fd, 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(fd, 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"); + + return true; +} + +bool +do_ping(_unused_ struct cfg *cfg) { + die("Not implemented"); + return false; +} + +static bool +get_player_count(int fd, unsigned *current, unsigned *max) +{ + char buf[4096]; + const char *reply; + unsigned c, m; + + if (!get_one_status(fd, buf, sizeof(buf), "list", 2, + "There are %u of a max %u players online", + &reply, &c, &m)) + return false; + + if (current) + *current = c; + + if (max) + *max = m; + + return true; +} + +static bool +stop_one_server(struct cfg *cfg, struct server *server) +{ + int fd; + bool rv; + + fd = rcon_login(cfg, server); + if (fd < 0) + return false; + + if (cfg->force_stop) { + unsigned current; + + if (!get_player_count(fd, ¤t, NULL)) { + error("%s: unable to get player count, not stopping", + server->name); + return false; + } else if (current > 0) { + error("%s: has active players (use -f to force)", + server->name); + return false; + } + } + + info("%s: sending stop command", server->name); + rv = send_cmd(fd, "stop"); + close(fd); + + return rv; +} + +bool +do_stop(struct cfg *cfg) { + struct server *server; + + server = server_get_default(cfg); + return stop_one_server(cfg, server); +} + +bool +do_stop_all(struct cfg *cfg) { + struct server *server; + + list_for_each_entry(server, &cfg->servers, list) { + server_read_config(cfg, server); + stop_one_server(cfg, server); + } + + return true; +} + +bool +do_pcount(struct cfg *cfg) { + int fd; + unsigned current, max; + struct server *server; + + server = server_get_default(cfg); + fd = rcon_login(cfg, server); + if (fd < 0) + return false; + + if (get_player_count(fd, ¤t, &max)) { + info("Players: %u/%u", current, max); + return true; + } else { + die("Failed to get player count"); + return false; + } +} + +bool +do_console(struct cfg *cfg) +{ + char *prompt; + char *cmd; + int fd; + struct server *server; + + server = server_get_default(cfg); + fd = rcon_login(cfg, server); + if (fd < 0) + return false; + + prompt = alloca(strlen(program_invocation_short_name) + + STRLEN(" (") + strlen(server->name) + STRLEN("): ") + 1); + sprintf(prompt, "%s (%s): ", program_invocation_short_name, + server->name); + + 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(fd, tmp); + + if (streq(tmp, "stop") || streq(tmp, "/stop")) + /* The server waits for us to close the connection */ + break; + + free(cmd); + } + + xfree(cmd); + return true; +} + +bool +do_command(struct cfg *cfg) { + int fd; + struct server *server; + + server = server_get_default(cfg); + fd = rcon_login(cfg, server); + if (fd < 0) + return false; + + return send_cmd(fd, cfg->cmdstr); +} + |