diff options
Diffstat (limited to 'shared')
-rw-r--r-- | shared/mc-protocol.c | 282 | ||||
-rw-r--r-- | shared/mc-protocol.h | 13 | ||||
-rw-r--r-- | shared/meson.build | 1 |
3 files changed, 296 insertions, 0 deletions
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 <stdint.h> +#include <inttypes.h> +#include <strings.h> + +#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', ] |