diff options
| author | David Härdeman <david@hardeman.nu> | 2020-06-25 17:01:24 +0200 | 
|---|---|---|
| committer | David Härdeman <david@hardeman.nu> | 2020-06-25 17:01:24 +0200 | 
| commit | 4d0fcab10e91ad5962837f7dd428f5bca1c8c980 (patch) | |
| tree | 3529036819aec2a56d769f3f8626fb24c625d4b2 /minecctl/minecctl-rcon.c | |
| parent | 7e980225821aaa3073fc46d2dc248e9571d3c298 (diff) | |
Flesh out minecctl some more
Diffstat (limited to 'minecctl/minecctl-rcon.c')
| -rw-r--r-- | minecctl/minecctl-rcon.c | 363 | 
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, >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); +} +  | 
