From 99b2c70137fef05a5a18f439b9010ddba455f5cb Mon Sep 17 00:00:00 2001 From: David Härdeman Date: Sat, 27 Jun 2020 15:18:45 +0200 Subject: Create a shared mc protocol implementation and use it in the proxy and cmd line tool --- minecctl/mc-commands.c | 77 +++++++++++++ minecctl/mc-commands.h | 6 + minecctl/meson.build | 1 + minecctl/misc-commands.c | 15 +++ minecctl/misc-commands.h | 2 + minecctl/misc.c | 9 +- minecctl/rcon-commands.c | 19 ++-- minecctl/rcon-commands.h | 2 +- minecproxy/idle.c | 258 ++++--------------------------------------- shared/mc-protocol.c | 282 +++++++++++++++++++++++++++++++++++++++++++++++ shared/mc-protocol.h | 13 +++ shared/meson.build | 1 + 12 files changed, 433 insertions(+), 252 deletions(-) create mode 100644 minecctl/mc-commands.c create mode 100644 minecctl/mc-commands.h create mode 100644 shared/mc-protocol.c create mode 100644 shared/mc-protocol.h diff --git a/minecctl/mc-commands.c b/minecctl/mc-commands.c new file mode 100644 index 0000000..0ac20d0 --- /dev/null +++ b/minecctl/mc-commands.c @@ -0,0 +1,77 @@ +#include + +#include "utils.h" +#include "minecctl.h" +#include "server.h" +#include "mc-commands.h" +#include "misc.h" +#include "mc-protocol.h" + +bool +do_mc_pcount(struct cfg *cfg, unsigned *online, unsigned *max) +{ + struct server *server; + struct saddr *saddr; + char buf[4096]; + size_t plen, off; + ssize_t r; + bool rv = false; + int fd; + + server = server_get_default(cfg); + + fd = connect_any(&server->mc_addrs, true); + if (fd < 0) { + error("%s: unable to connect", server->name); + return false; + } + + /* FIXME: connect_any needs to indicate the address it used */ + saddr = list_first_entry(&server->mc_addrs, struct saddr, list); + if (!saddr) { + error("No saddr"); + goto out; + } + + if (!mc_protocol_create_status_request(buf, sizeof(buf), &plen, saddr)) { + error("Failed to create req"); + goto out; + } + + /* FIXME: do proper checks for EINTR etc */ + off = 0; + while (off < plen) { + r = write(fd, buf + off, plen - off); + if (r <= 0) { + error("write failed: %zi (%m)", r); + goto out; + } + off += r; + } + + off = 0; + while (off < sizeof(buf)) { + r = read(fd, buf + off, sizeof(buf) - off); + if (r <= 0) { + error("Read failed %zi: %m", r); + goto out; + } + + off += r; + + if (mc_is_handshake_complete(buf, off)) { + rv = true; + break; + } + } + + if (!mc_protocol_parse_status_reply(buf, off, online, max)) { + error("Failed to get player count"); + return false; + } + +out: + close(fd); + return rv; +} + diff --git a/minecctl/mc-commands.h b/minecctl/mc-commands.h new file mode 100644 index 0000000..3140a73 --- /dev/null +++ b/minecctl/mc-commands.h @@ -0,0 +1,6 @@ +#ifndef foomccommandshfoo +#define foomccomanndshfoo + +bool do_mc_pcount(struct cfg *cfg, unsigned *online, unsigned *max); + +#endif diff --git a/minecctl/meson.build b/minecctl/meson.build index e3bddcc..7a41203 100644 --- a/minecctl/meson.build +++ b/minecctl/meson.build @@ -2,6 +2,7 @@ minecctl_sources = [ 'minecctl.c', 'server.c', 'rcon-commands.c', + 'mc-commands.c', 'misc-commands.c', 'misc.c', ] diff --git a/minecctl/misc-commands.c b/minecctl/misc-commands.c index 6b70c55..c64a005 100644 --- a/minecctl/misc-commands.c +++ b/minecctl/misc-commands.c @@ -2,6 +2,8 @@ #include "minecctl.h" #include "server.h" #include "misc-commands.h" +#include "rcon-commands.h" +#include "mc-commands.h" bool do_list(struct cfg *cfg) @@ -16,3 +18,16 @@ do_list(struct cfg *cfg) return true; } +bool +do_pcount(struct cfg *cfg) +{ + unsigned x, y; + + if (do_rcon_pcount(cfg, &y, &x)) + error("Rcon says %u/%u", y, x); + + if (do_mc_pcount(cfg, &y, &x)) + error("MC says %u/%u", y, x); + + return true; +} diff --git a/minecctl/misc-commands.h b/minecctl/misc-commands.h index e0dc675..8270601 100644 --- a/minecctl/misc-commands.h +++ b/minecctl/misc-commands.h @@ -3,5 +3,7 @@ bool do_list(struct cfg *cfg); +bool do_pcount(struct cfg *cfg); + #endif diff --git a/minecctl/misc.c b/minecctl/misc.c index bb33161..72eb03c 100644 --- a/minecctl/misc.c +++ b/minecctl/misc.c @@ -68,8 +68,13 @@ connect_any(struct list_head *addrs, bool may_fail) bool connected = false; int sfd; - if (list_empty(addrs)) - die("No address to connect to"); + /* FIXME: check callers and coordinate debug msg */ + if (list_empty(addrs)) { + if (may_fail) + return -1; + else + die("No address to connect to"); + } list_for_each_entry(saddr, addrs, list) { verbose("Attempting connection to %s", saddr->addrstr); diff --git a/minecctl/rcon-commands.c b/minecctl/rcon-commands.c index 02b970f..cf43c5e 100644 --- a/minecctl/rcon-commands.c +++ b/minecctl/rcon-commands.c @@ -387,23 +387,22 @@ do_stop_all(struct cfg *cfg) { } bool -do_pcount(struct cfg *cfg) { - int fd; - unsigned current, max; +do_rcon_pcount(struct cfg *cfg, unsigned *online, unsigned *max) +{ struct server *server; + bool rv; + int fd; 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; - } + rv = get_player_count(fd, online, max); + + close(fd); + + return rv; } bool diff --git a/minecctl/rcon-commands.h b/minecctl/rcon-commands.h index 5366bf9..1714dd5 100644 --- a/minecctl/rcon-commands.h +++ b/minecctl/rcon-commands.h @@ -9,7 +9,7 @@ bool do_stop(struct cfg *cfg); bool do_stop_all(struct cfg *cfg); -bool do_pcount(struct cfg *cfg); +bool do_rcon_pcount(struct cfg *cfg, unsigned *online, unsigned *max); bool do_console(struct cfg *cfg); diff --git a/minecproxy/idle.c b/minecproxy/idle.c index 8fcb934..70d8099 100644 --- a/minecproxy/idle.c +++ b/minecproxy/idle.c @@ -11,195 +11,32 @@ #include "server.h" #include "idle.h" #include "ptimer.h" +#include "mc-protocol.h" struct idle { struct ptimer_task ptask; struct uring_task task; }; -static inline void -write_byte(char **pos, char byte) -{ - assert_return(pos && *pos); - - **pos = byte; - (*pos)++; -} - -#define MC_HELO 0x00 -#define MC_NEXT_STATE_STATUS 0x01 -#define MC_GET_STATUS 0x00 -#define MC_VARINT_MAX_BYTES 5 -#define MC_STATUS_REPLY 0x00 -#define MC_UNDEFINED_VERSION -1 - -static inline void -write_varint(char **pos, int32_t orig) -{ - assert_return(pos && *pos); - - uint32_t val = (uint32_t)orig; - - while (val) { - **pos = val & 0x7f; - val >>= 7; - if (val > 0) - **pos |= 0x80; - (*pos)++; - } -} - -/* - * return value: - * positive = varint parsed - * zero = need more bytes - * negative = error - */ -static inline int -read_varint(char **pos, size_t *remain, int32_t *res) -{ - unsigned consumed; - uint32_t val = 0; - - assert_return(pos && *pos && remain && res, -1); - - for (consumed = 1; consumed <= *remain; consumed++) { - uint32_t tmp; - - tmp = **pos & 0x7f; - val += (tmp << (7 * (consumed - 1))); - (*pos)++; - - if (!(tmp & 0x80)) - break; - } - - if (consumed > *remain) - return 0; - else if (consumed > MC_VARINT_MAX_BYTES) - return -1; - - *remain -= consumed; - *res = (int32_t)val; - return 1; -} - -static inline void -write_bytes(char **pos, const char *bytes, size_t n) -{ - assert_return(pos && *pos && bytes && n > 0); - - memcpy(*pos, bytes, n); - *pos += n; -} - -static inline void -write_str(char **pos, const char *str) -{ - size_t len; - - assert_return(pos && *pos && !empty_str(str)); - - len = strlen(str); - write_varint(pos, len); - write_bytes(pos, str, len); -} - -static inline void -write_cmd(char **pos, const char *begin, const char *end) -{ - assert_return(pos && *pos && begin && end && end > begin); - - write_varint(pos, end - begin); - write_bytes(pos, begin, end - begin); -} - static int idle_check_handshake_complete(struct uring_task *task, _unused_ int res) { - size_t remain; - char *pos; - int32_t mclen; int r; assert_return(task, -EINVAL); assert_task_alive_or(DBG_IDLE, task, return -EINTR); - remain = task->tbuf->len; - pos = task->tbuf->buf; - - r = read_varint(&pos, &remain, &mclen); - if (r < 0) { - error("failed to parse message length"); - return -EINVAL; - } else if (r == 0) { - return 0; - } else if (mclen < 2) { - error("short MC message"); - return -EINVAL; - } - - if (mclen < remain) { - debug(DBG_IDLE, "short MC message - len: %" PRIi32 ", remain: %zu", - mclen, remain); - return 0; - } - - debug(DBG_IDLE, "Complete message"); - return 1; -} - -#define ONLINE_NEEDLE "\"online\"" -static int -get_player_count(const char *pos, size_t remain) -{ - /* - * Example JSON (line breaks added): - * {"description":{ - * "text":"A Minecraft Server"}, - * "players":{"max":20,"online":0}, - * "version":{"name":"1.15.2","protocol":578} - * } - */ - char *online; - char *end; - unsigned count; - - assert_return(pos && remain > 0, -1); - - online = memmem(pos, remain, ONLINE_NEEDLE, STRLEN(ONLINE_NEEDLE)); - if (!online) { - error("could not find online count in JSON"); - return -1; - } - - remain -= (online - pos); - - end = memchr(online, '}', remain); - if (!end) { - error("could not parse JSON (no end)"); - return -1; - } - *end = '\0'; - - if (sscanf(online, ONLINE_NEEDLE " : %u", &count) != 1) { - error("could not parse JSON (online count)"); - return -1; - } - - return count; + r = mc_is_handshake_complete(task->tbuf->buf, task->tbuf->len); + debug(DBG_IDLE, "mc_is_handshake_complete returned %i", r); + return r; } static void idle_check_handshake_reply(struct uring_task *task, int res) { struct server *server = container_of(task, struct server, idle_task); - int32_t mclen; - int32_t jsonlen; - char *pos; - size_t remain; + unsigned online, max; int player_count = -1; - int r; assert_return(task); assert_task_alive(DBG_IDLE, task); @@ -208,55 +45,12 @@ idle_check_handshake_reply(struct uring_task *task, int res) if (res < 0) goto out; - /* - fprintf(stderr, "Received MC message (%i bytes):", res); - for (int i = 0; i < res; i++) - fprintf(stderr, "0x%02hhx ", idle->remotebuf[i]); - fprintf(stderr, "n"); - */ - - remain = server->idle_buf.len; - pos = server->idle_buf.buf; - - r = read_varint(&pos, &remain, &mclen); - if (r <= 0 || mclen < 2 || mclen < remain) { - /* Should not happen since the msg has been checked already */ - error("invalid message"); - goto out; - } - - debug(DBG_IDLE, "MC message - len: %" PRIi32 ", remain: %zu", - mclen, remain); - - if (*pos != MC_STATUS_REPLY) { - error("unknown server reply (0x%02hhx)", *pos); - goto out; - } - - pos++; - remain--; - - r = read_varint(&pos, &remain, &jsonlen); - if (r <= 0) { - error("could not read JSON length"); - goto out; - } - - debug(DBG_IDLE, "MC - json len: %" PRIi32 ", remain: %zu", - jsonlen, remain); - - if (jsonlen < remain) { - error("invalid JSON length"); - goto out; - } - - /* - fprintf(stderr, "JSON: "); - for (int i = 0; i < jsonlen; i++) - fprintf(stderr, "%c", pos[i]); - */ - - player_count = get_player_count(pos, remain); + if (mc_protocol_parse_status_reply(server->idle_buf.buf, + server->idle_buf.len, + &online, &max)) + player_count = online; + else + error("failed to parse reply"); out: uring_task_close_fd(task); @@ -284,31 +78,17 @@ idle_check_handshake_sent(struct uring_task *task, int res) void idle_check_get_player_count(struct server *server, struct connection *conn) { - char buf[1024]; - char *pos; - char *cmdbuf = server->idle_buf.buf; - uint16_t port; - char hostname[INET6_ADDRSTRLEN]; - assert_return(server && conn && server->idle_task.priv); - port = saddr_port(&conn->remote); - saddr_addr(&conn->remote, hostname, sizeof(hostname)); - - pos = buf; - write_byte(&pos, MC_HELO); - write_varint(&pos, MC_UNDEFINED_VERSION); - write_str(&pos, hostname); - write_byte(&pos, (port >> 8) & 0xff); - write_byte(&pos, (port >> 0) & 0xff); - write_byte(&pos, MC_NEXT_STATE_STATUS); - write_cmd(&cmdbuf, buf, pos); - - pos = buf; - write_byte(&pos, MC_GET_STATUS); - write_cmd(&cmdbuf, buf, pos); + if (!mc_protocol_create_status_request(server->idle_buf.buf, + sizeof(server->idle_buf.buf), + &server->idle_buf.len, + &conn->remote)) { + error("failed to create mc request"); + /* FIXME: is this enough? */ + return; + } - server->idle_buf.len = (cmdbuf - server->idle_buf.buf); debug(DBG_IDLE, "sending MC message (%zu bytes)", server->idle_buf.len); uring_tbuf_write(&server->idle_task, idle_check_handshake_sent); diff --git a/shared/mc-protocol.c b/shared/mc-protocol.c new file mode 100644 index 0000000..afc9b38 --- /dev/null +++ b/shared/mc-protocol.c @@ -0,0 +1,282 @@ +#include +#include +#include + +#include "utils.h" + +#define MC_HELO 0x00 +#define MC_NEXT_STATE_STATUS 0x01 +#define MC_GET_STATUS 0x00 +#define MC_VARINT_MAX_BYTES 5 +#define MC_STATUS_REPLY 0x00 +#define MC_UNDEFINED_VERSION -1 + +static inline unsigned +write_byte(char **pos, char byte) +{ + if (pos && *pos) { + **pos = byte; + (*pos)++; + } + return 1; +} +static inline unsigned +write_varint(char **pos, int32_t v) +{ + uint32_t u = (uint32_t)v; + unsigned rv = 0; + + do { + char x; + + x = u & 0x7f; + if (u >>= 7) + x |= 0x80; + if (pos && *pos) + *((*pos)++) = x; + rv++; + } while (u); + + return rv; +} + +static inline unsigned +write_bytes(char **pos, const char *bytes, size_t n) +{ + if (pos && *pos) { + memcpy(*pos, bytes, n); + *pos += n; + } + + return n; +} + +static inline unsigned +write_str(char **pos, const char *str, size_t len) +{ + unsigned rv; + + rv = write_varint(pos, len); + rv += write_bytes(pos, str, len); + + return rv; +} + +/* + * return value: + * > 0 = varint parsed + * 0 = need more bytes + * < 0 = error + */ +static inline int +read_varint(const char **from, size_t *remain, int32_t *res) +{ + unsigned consumed; + uint32_t val = 0; + + assert_return(from && *from && remain && res, -1); + + for (consumed = 1; consumed <= *remain; consumed++) { + uint32_t tmp; + + tmp = **from & 0x7f; + val += (tmp << (7 * (consumed - 1))); + (*from)++; + + if (!(tmp & 0x80)) + break; + } + + if (consumed > *remain) + return 0; + else if (consumed > MC_VARINT_MAX_BYTES) + return -1; + + *remain -= consumed; + *res = (int32_t)val; + return 1; +} + +/* + * return value: + * > 0 = varint parsed + * 0 = need more bytes + * < 0 = error + */ +int +mc_is_handshake_complete(const char *buf, size_t len) +{ + int32_t mclen; + int r; + + assert_return(buf && len > 0, -1); + + r = read_varint(&buf, &len, &mclen); + if (r < 0) { + error("failed to parse message length"); + return -1; + } else if (r == 0) { + return 0; + } else if (mclen < 2) { + error("short MC message"); + return -1; + } + + if (mclen < len) { + debug(DBG_IDLE, "short MC message - len: %" PRIi32 ", remain: %zu", + mclen, len); + return 0; + } + + debug(DBG_IDLE, "Complete message"); + return 1; +} + +#define PLAYERS_NEEDLE "\"players\"" +#define MAX_NEEDLE "\"max\"" +#define ONLINE_NEEDLE "\"online\"" +static bool +mc_get_player_count(const char *buf, size_t remain, + unsigned *ronline, unsigned *rmax) +{ + /* + * Example JSON (line breaks added): + * {"description":{ + * "text":"A Minecraft Server"}, + * "players":{"max":20,"online":0}, + * "version":{"name":"1.15.2","protocol":578} + * } + */ + char *players; + char *pmax; + char *ponline; + unsigned online, max; + + assert_return(buf && remain > 0, false); + + players = memmem(buf, remain, PLAYERS_NEEDLE, STRLEN(PLAYERS_NEEDLE)); + if (!players) + goto error; + + remain -= (players - buf); + + pmax = memmem(players, remain, MAX_NEEDLE, STRLEN(MAX_NEEDLE)); + ponline = memmem(players, remain, ONLINE_NEEDLE, STRLEN(ONLINE_NEEDLE)); + if (!pmax || !ponline) + goto error; + + if ((sscanf(pmax, MAX_NEEDLE " : %u", &max) != 1) || + (sscanf(ponline, ONLINE_NEEDLE " : %u", &online) != 1)) + goto error; + + if (ronline) + *ronline = online; + + if (rmax) + *rmax = max; + + return true; + +error: + error("could not parse server status reply"); + return false; +} + +bool +mc_protocol_parse_status_reply(const char *buf, size_t len, + unsigned *online, unsigned *max) +{ + const char *from = buf; + size_t remain = len; + int32_t mclen; + int32_t jsonlen; + int r; + + assert_return(buf && len > 0, -1); + + r = read_varint(&from, &remain, &mclen); + if (r <= 0 || mclen < 2 || mclen < remain) { + /* Should not happen since the msg has been checked already */ + error("invalid message"); + return false; + } + + debug(DBG_IDLE, "MC message - len: %" PRIi32 ", remain: %zu", + mclen, remain); + + if (*from != MC_STATUS_REPLY) { + error("unknown server reply (0x%02hhx)", *from); + return false; + } + + from++; + remain--; + + r = read_varint(&from, &remain, &jsonlen); + if (r <= 0) { + error("could not read JSON length"); + return false; + } + + debug(DBG_IDLE, "MC - json len: %" PRIi32 ", remain: %zu", + jsonlen, remain); + + if (jsonlen < remain) { + error("invalid JSON length"); + return false; + } + + if (mc_get_player_count(from, remain, online, max)) + return true; + + return false; +} + +bool +mc_protocol_create_status_request(char *buf, size_t len, size_t *rlen, + struct saddr *saddr) +{ + uint16_t port; + char hostname[INET6_ADDRSTRLEN]; + size_t bodylen, hostlen, packetlen; + char *to = buf; + + assert_return(buf && len > 0 && rlen && saddr, false); + + port = saddr_port(saddr); + saddr_addr(saddr, hostname, sizeof(hostname)); + hostlen = strlen(hostname); + + /* First, a handshake, calculate body length... */ + bodylen = write_byte(NULL, MC_HELO); + bodylen += write_varint(NULL, MC_UNDEFINED_VERSION); + bodylen += write_str(NULL, hostname, hostlen); + bodylen += write_byte(NULL, (port >> 8) & 0xff); + bodylen += write_byte(NULL, (port >> 0) & 0xff); + bodylen += write_byte(NULL, MC_NEXT_STATE_STATUS); + + /* ...check buffer size... */ + if (len < (MC_VARINT_MAX_BYTES + bodylen + 2 /* = status req */)) + return false; + + /* ...write header... */ + packetlen = write_varint(&to, bodylen); + + /* ...write body... */ + packetlen += write_byte(&to, MC_HELO); + packetlen += write_varint(&to, MC_UNDEFINED_VERSION); + packetlen += write_str(&to, hostname, hostlen); + packetlen += write_byte(&to, (port >> 8) & 0xff); + packetlen += write_byte(&to, (port >> 0) & 0xff); + packetlen += write_byte(&to, MC_NEXT_STATE_STATUS); + + /* ...then a status request, again with a header... */ + packetlen += write_varint(&to, 1); + + /* ...and body */ + packetlen += write_byte(&to, MC_GET_STATUS); + + *rlen = packetlen; + return true; +} + diff --git a/shared/mc-protocol.h b/shared/mc-protocol.h new file mode 100644 index 0000000..8ecc02a --- /dev/null +++ b/shared/mc-protocol.h @@ -0,0 +1,13 @@ +#ifndef foomcprotocolhfoo +#define foomcprotocolhfoo + +int mc_is_handshake_complete(const char *buf, size_t len); + +bool mc_protocol_parse_status_reply(const char *buf, size_t len, + unsigned *online, unsigned *max); + +bool mc_protocol_create_status_request(char *buf, size_t len, size_t *rlen, + struct saddr *saddr); + +#endif + diff --git a/shared/meson.build b/shared/meson.build index ccfad4a..5b15c05 100644 --- a/shared/meson.build +++ b/shared/meson.build @@ -1,5 +1,6 @@ srcs_libshared = [ 'rcon-protocol.c', + 'mc-protocol.c', 'config-parser.c', 'utils.c', ] -- cgit v1.2.3