/* SPDX-License-Identifier: GPL-2.0 */ #include #include #include #include #include #include #include #include #include #include "shared/utils.h" #include "shared/rcon-protocol.h" #include "minecctl.h" #include "server.h" #include "rcon-commands.h" #include "misc.h" static void send_packet(int sfd, const char *buf, size_t len) { size_t off = 0; ssize_t r; for (off = 0; off < len; off += r) { r = write(sfd, buf + off, len - off); if (r < 0) { if (errno == EAGAIN || errno == EINTR) continue; die("Failed to write packet: %m"); } } } /* 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; ssize_t r; const char *error; for (off = 0; off < len; off += r) { if (rcon_protocol_packet_complete(buf, off)) break; r = read(sfd, buf + off, len - off); if (r < 0) { if (errno == EINTR) continue; die("Failed to read reply: %m"); } else if (r == 0) die("Failed, connection closed"); } if (off >= len) die("Reply too large %zu and %zu", off, len); else 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, int32_t *rtype, const char **reply) { static uint32_t rcon_packet_id = 1; const char *error; 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: Shouldn't die */ if (!rcon_protocol_verify_response(rcon_packet_id, id, type, *rtype, &error)) die("Invalid response: %s", error); rcon_packet_id++; } /* FIXME: return error message */ static int rcon_login(struct cfg *cfg, struct server *server) { const char *error; char buf[4096]; int32_t rtype; const char *reply; struct saddr *saddr; int fd = -1; assert_die(cfg && server, "invalid arguments"); if (list_empty(&server->scfg.rcons)) { error("%s: rcon address unknown", server->scfg.name); goto error; } fd = connect_any(&server->scfg.rcons, &saddr, &error); if (fd < 0) { verbose("%s: unable to connect - %s", server->scfg.name, error); goto error; } else verbose("%s: connected to %s", server->scfg.name, saddr->addrstr); if (!server->scfg.rcon_password) server->scfg.rcon_password = ask_password(); if (!server->scfg.rcon_password) { error("%s: can't login - password missing", server->scfg.name); goto error; } send_msg(fd, buf, sizeof(buf), RCON_PACKET_LOGIN, server->scfg.rcon_password, &rtype, &reply); explicit_bzero(buf, sizeof(buf)); free_password(&server->scfg.rcon_password); if (!rcon_protocol_verify_response(1, 1, RCON_PACKET_LOGIN, rtype, &error)) { error("%s: invalid response - %s", server->scfg.name, error); goto error; } else verbose("%s: login ok", server->scfg.name); 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; } info("%s%s%s", ansi_grey, reply, ansi_normal); 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; } static bool do_one_info(struct cfg *cfg, struct server *server) { char buf[4096]; char tbuf[4096]; const char *reply; unsigned cplayers, maxplayers, gtime; unsigned epacks, apacks; unsigned bannedplayers, bannedips; int fd; fd = rcon_login(cfg, server); if (fd < 0) return false; info("• %s", server->scfg.name); 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"); close(fd); return true; } bool do_info(struct cfg *cfg) { struct server *server; if (cfg->default_set) { server = server_get_default(cfg); if (!server) { error("failed to get default server"); return false; } do_one_info(cfg, server); } else { server_read_all_configs(cfg, false); list_for_each_entry(server, &cfg->servers, list) do_one_info(cfg, server); } return true; } 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) { info("• %s: %sfail%s - unable to login", server->scfg.name, ansi_red, ansi_normal); return false; } if (!cfg->force_stop) { unsigned current; if (!get_player_count(fd, ¤t, NULL)) { info("• %s: %sfail%s - unable to get player count", server->scfg.name, ansi_red, ansi_normal); return false; } else if (current > 0) { info("• %s: %sfail%s - has active players " "(use -f to force)", server->scfg.name, ansi_red, ansi_normal); return false; } } info("• %s: sending stop command", server->scfg.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; bool rv = true; server_read_all_configs(cfg, false); list_for_each_entry(server, &cfg->servers, list) { /* FIXME: print more info, error checks */ stop_one_server(cfg, server); } return rv; } bool do_rcon_pcount(struct cfg *cfg, struct server *server, unsigned *online, unsigned *max, const char **error) { bool rv; int fd; fd = rcon_login(cfg, server); if (fd < 0) { *error = "failed to login"; return false; } rv = get_player_count(fd, online, max); if (!rv) *error = "failed to get player count"; close(fd); return rv; } 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->scfg.name) + STRLEN("): ") + 1); sprintf(prompt, "%s (%s): ", program_invocation_short_name, server->scfg.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_commands(struct cfg *cfg) { struct server *server; bool rv = true; int fd; if (!cfg->commands) { error("No commands to send"); return false; } server = server_get_default(cfg); fd = rcon_login(cfg, server); if (fd < 0) return false; for (char *const *cmd = cfg->commands; *cmd; cmd++) { if (!send_cmd(fd, *cmd)) { rv = false; break; } } close(fd); return rv; }