/* SPDX-License-Identifier: GPL-2.0 */ #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; }