summaryrefslogtreecommitdiff
path: root/minecproxy/idle.c
diff options
context:
space:
mode:
Diffstat (limited to 'minecproxy/idle.c')
-rw-r--r--minecproxy/idle.c378
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;
+}
+