summaryrefslogtreecommitdiff
path: root/shared
diff options
context:
space:
mode:
authorDavid Härdeman <david@hardeman.nu>2020-06-27 15:18:45 +0200
committerDavid Härdeman <david@hardeman.nu>2020-06-27 15:18:45 +0200
commit99b2c70137fef05a5a18f439b9010ddba455f5cb (patch)
treeb0c08cfce14019cd634e6b4b84d0cf0f6e8eee6a /shared
parenta87e894ba3f3a8915389f651fb034f0d1835630c (diff)
Create a shared mc protocol implementation and use it in the proxy and cmd line tool
Diffstat (limited to 'shared')
-rw-r--r--shared/mc-protocol.c282
-rw-r--r--shared/mc-protocol.h13
-rw-r--r--shared/meson.build1
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',
]