diff options
| author | David Härdeman <david@hardeman.nu> | 2020-06-27 15:18:45 +0200 | 
|---|---|---|
| committer | David Härdeman <david@hardeman.nu> | 2020-06-27 15:18:45 +0200 | 
| commit | 99b2c70137fef05a5a18f439b9010ddba455f5cb (patch) | |
| tree | b0c08cfce14019cd634e6b4b84d0cf0f6e8eee6a /shared | |
| parent | a87e894ba3f3a8915389f651fb034f0d1835630c (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.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',  ]  | 
