diff options
Diffstat (limited to 'minecproxy/idle.c')
-rw-r--r-- | minecproxy/idle.c | 378 |
1 files changed, 378 insertions, 0 deletions
diff --git a/minecproxy/idle.c b/minecproxy/idle.c new file mode 100644 index 0000000..c49846d --- /dev/null +++ b/minecproxy/idle.c @@ -0,0 +1,378 @@ +#define _GNU_SOURCE +#include <inttypes.h> +#include <sys/socket.h> +#include <netinet/ip.h> +#include <arpa/inet.h> +#include <string.h> +#include <ctype.h> +#include <errno.h> + +#include "main.h" +#include "uring.h" +#include "server.h" +#include "idle.h" +#include "ptimer.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, 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; +} + +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; + int player_count = -1; + int r; + + assert_return(task); + assert_task_alive(DBG_IDLE, task); + + debug(DBG_IDLE, "res: %i", 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); + +out: + uring_task_close_fd(task); + server_set_active_players(server, player_count); + return; +} + +static void +idle_check_handshake_sent(struct uring_task *task, int res) +{ + assert_return(task); + assert_task_alive(DBG_IDLE, task); + + debug(DBG_IDLE, "sent %i bytes", res); + if (res < 0) { + uring_task_close_fd(task); + return; + } + + uring_tbuf_read_until(task, + idle_check_handshake_complete, + idle_check_handshake_reply); +} + +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); + + 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); +} + +static void +idle_cb(struct ptimer_task *ptask) +{ + struct idle *idle = container_of(ptask, struct idle, ptask); + struct server *server; + + assert_return(ptask); + assert_task_alive(DBG_IDLE, &idle->task); + + debug(DBG_IDLE, "timer fired"); + + list_for_each_entry(server, &cfg->servers, list) + server_idle_check(server); +} + +static void +idle_free(struct uring_task *task) +{ + struct idle *idle = container_of(task, struct idle, task); + + assert_return(task); + debug(DBG_IDLE, "task %p, idle %p", task, idle); + xfree(idle); +} + +void +idle_refdump() +{ + assert_return_silent(cfg->idle); + + uring_task_refdump(&cfg->idle->task); +} + +void +idle_delete() +{ + assert_return(cfg->idle); + + debug(DBG_IDLE, "closing fd %i", cfg->idle->task.fd); + ptimer_del_task(&cfg->idle->ptask); + uring_task_destroy(&cfg->idle->task); + cfg->idle = NULL; +} + +void +idle_init() +{ + struct idle *idle; + + assert_return(!cfg->idle); + + idle = zmalloc(sizeof(*idle)); + if (!idle) + die("malloc: %m"); + + ptask_init(&idle->ptask, 60, 0, idle_cb); + uring_task_init(&idle->task, "idle", uring_parent(), idle_free); + ptimer_add_task(&idle->ptask); + cfg->idle = idle; +} + |