From 1e2f3e437492ecc841bc9852ab46ce0e218e4723 Mon Sep 17 00:00:00 2001 From: David Härdeman Date: Sun, 7 Jun 2020 23:41:54 +0200 Subject: Add basic support for checking idle status --- idle.c | 453 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 453 insertions(+) create mode 100644 idle.c (limited to 'idle.c') diff --git a/idle.c b/idle.c new file mode 100644 index 0000000..89a8f9a --- /dev/null +++ b/idle.c @@ -0,0 +1,453 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include + +#include "main.h" +#include "uring.h" +#include "config.h" +#include "server.h" +#include "idle.h" + +struct idle { + uint64_t value; + struct server *server; + struct uring_task task; + + struct uring_task idlecheck; + unsigned next_remote; + struct sockaddr_in46 remote; + char remotestr[ADDRSTRLEN]; + char remotebuf[4096]; + size_t remotebuflen; + size_t remotebufdone; +}; + +static void +idle_check_free(struct uring_task *task) +{ + struct idle *idle = container_of(task, struct idle, idlecheck); + + fprintf(stderr, "%s: called with task 0x%p and idle 0x%p\n", __func__, task, idle); +} + +static inline void +write_byte(char **pos, char byte) +{ + **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 + +static inline void +write_varint(char **pos, int32_t orig) +{ + uint32_t val = (uint32_t)orig; + + while (val) { + **pos = val & 0x7f; + val >>= 7; + if (val > 0) + **pos |= 0x80; + (*pos)++; + } +} + +/* + * return value: + * positive = need more bytes + * zero = value decoded + * negative = error + */ +static inline int +read_varint(char **pos, size_t *remain, int32_t *res) +{ + unsigned consumed; + uint32_t val = 0; + + 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 1; + else if (consumed > MC_VARINT_MAX_BYTES) + return -1; + + *remain -= consumed; + *res = (int32_t)val; + return 0; +} + +static inline void +write_bytes(char **pos, const char *bytes, size_t n) +{ + memcpy(*pos, bytes, n); + *pos += n; +} + +static inline void +write_str(char **pos, const char *str) +{ + write_bytes(pos, str, strlen(str)); +} + +static inline void +write_cmd(char **pos, const char *begin, const char *end) +{ + write_varint(pos, end - begin); + write_bytes(pos, begin, end - begin); +} + +#define JSON_NEEDLE "\"online\"" +static void +idle_check_handshake_reply(struct cfg *cfg, struct uring_task *task, int res) +{ + struct idle *idle = container_of(task, struct idle, idlecheck); + int32_t mclen; + int32_t jsonlen; + char *pos; + size_t remain; + int r; + + fprintf(stderr, "%s: received %i bytes\n", __func__, res); + if (res < 0) + goto error; + + idle->remotebuflen += res; + + /* + fprintf(stderr, "Received MC message (%i bytes):\n", res); + for (int i = 0; i < res; i++) + fprintf(stderr, "0x%02hhx ", idle->remotebuf[i]); + fprintf(stderr, "\n"); + */ + + remain = idle->remotebuflen; + pos = idle->remotebuf; + + r = read_varint(&pos, &remain, &mclen); + if (r < 0) { + fprintf(stderr, "Failed to parse message length\n"); + goto error; + } else if (r > 0) { + goto read_more; + } else if (mclen < 2) { + fprintf(stderr, "Short MC message\n"); + goto error; + } + + fprintf(stderr, "MC message len: %" PRIi32 "\n", mclen); + fprintf(stderr, "Remain: %zu\n", remain); + + if (mclen < remain) + goto read_more; + + if (*pos != MC_STATUS_REPLY) { + fprintf(stderr, "Unknown server reply\n"); + goto error; + } + pos++; + remain--; + + r = read_varint(&pos, &remain, &jsonlen); + if (r != 0) { + fprintf(stderr, "Could not read JSON length\n"); + goto error; + } + + fprintf(stderr, "MC json len: %" PRIi32 "\n", jsonlen); + fprintf(stderr, "Remain: %zu\n", remain); + + if (jsonlen < remain) { + fprintf(stderr, "Invalid JSON length\n"); + goto error; + } + + fprintf(stderr, "JSON: "); + for (int i = 0; i < jsonlen; i++) + fprintf(stderr, "%c", pos[i]); + fprintf(stderr, "\n"); + + /* + * 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; + + online = memmem(pos, remain, JSON_NEEDLE, strlen(JSON_NEEDLE)); + if (!online) { + fprintf(stderr, "Could not find online count in JSON\n"); + goto error; + } + + remain -= (online - pos); + + end = memchr(online, '}', remain); + if (!end) { + fprintf(stderr, "Could not parse JSON (no end)\n"); + goto error; + } + *end = '\0'; + + if (sscanf(online, JSON_NEEDLE " : %u", &count) != 1) { + fprintf(stderr, "Could not parse JSON (online count)\n"); + goto error; + } + + fprintf(stderr, "We have %u players\n", count); + if (count > 0) + idle->server->idle_count = 0; + else { + idle->server->idle_count++; + if (idle->server->idle_count > idle->server->idle_timeout) + fprintf(stderr, "Would shutdown idle server %s\n", idle->server->name); + } + + return; + +read_more: + if (idle->remotebuflen >= sizeof(idle->remotebuflen)) + goto error; + + uring_read(cfg, &idle->idlecheck, idle->remotebuf + idle->remotebuflen, + sizeof(idle->remotebuf) - idle->remotebuflen, + 0, idle_check_handshake_reply); + return; + +error: + /* FIXME */ + return; +} + +static void +idle_check_handshake_sent(struct cfg *cfg, struct uring_task *task, int res) +{ + struct idle *idle = container_of(task, struct idle, idlecheck); + + fprintf(stderr, "%s: sent %i bytes\n", __func__, res); + if (res < 0) { + return; + } + + idle->remotebufdone += res; + if (idle->remotebufdone < idle->remotebuflen) + uring_write(cfg, &idle->idlecheck, + idle->remotebuf + idle->remotebufdone, + idle->remotebuflen - idle->remotebufdone, + idle_check_handshake_sent); + else { + idle->remotebuflen = 0; + uring_read(cfg, &idle->idlecheck, idle->remotebuf, sizeof(idle->remotebuf), 0, idle_check_handshake_reply); + } +} + +static void idle_check_connect_next_remote(struct cfg *cfg, struct idle *idle); + +static void +idle_check_remote_connected(struct cfg *cfg, struct uring_task *task, int res) +{ + struct idle *idle = container_of(task, struct idle, idlecheck); + char buf[1024]; + char *pos; + char *cmdbuf = idle->remotebuf; + uint16_t port = 25565; + + fprintf(stderr, "%s: connected %i\n", __func__, res); + if (res < 0) { + idle_check_connect_next_remote(cfg, idle); + return; + } + + pos = buf; + write_byte(&pos, MC_HELO); + write_varint(&pos, -1); /* Protocol version, -1 = undefined */ + write_varint(&pos, strlen("hostname")); + 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); + + idle->remotebufdone = 0; + idle->remotebuflen = (cmdbuf - idle->remotebuf); + fprintf(stderr, "Sending MC message (%zu bytes):\n", idle->remotebuflen); + for (pos = idle->remotebuf; pos < cmdbuf; pos++) + fprintf(stderr, "0x%02hhx ", *pos); + fprintf(stderr, "\n"); + + uring_write(cfg, &idle->idlecheck, idle->remotebuf, idle->remotebuflen, idle_check_handshake_sent); +} + +/* FIXME: Parts of this could be shared with proxy.c, probably in server.c */ +static void +idle_check_connect_next_remote(struct cfg *cfg, struct idle *idle) +{ + struct sockaddr_in46 *remote, *tmp; + struct server *scfg = idle->server; + int sfd; + unsigned i = 0; + +again: + remote = NULL; + list_for_each_entry(tmp, &scfg->remotes, list) { + if (i == idle->next_remote) { + remote = tmp; + break; + } + i++; + } + + if (!remote) { + fprintf(stderr, "No more remote addresses to attempt\n"); + /* FIXME: put tasks */ + return; + } + + idle->next_remote++; + idle->remote = *remote; + sockaddr_to_str(&idle->remote, idle->remotestr, sizeof(idle->remotestr)); + fprintf(stderr, "%s: attempting idle check on %s (len %u)\n", + scfg->name, idle->remotestr, idle->remote.addrlen); + + sfd = socket(idle->remote.storage.ss_family, SOCK_STREAM, 0); + if (sfd < 0) { + perror("socket"); + goto again; + } + + uring_task_set_fd(&idle->idlecheck, sfd); + uring_connect(cfg, &idle->idlecheck, &idle->remote, idle_check_remote_connected); +} + +static void +idle_cb(struct cfg *cfg, struct uring_task *task, int res) +{ + struct idle *idle = container_of(task, struct idle, task); + + fprintf(stderr, "%s: ret is %i (ref %u)\n", __func__, res, task->refcount); + + if (task->dead) { + fprintf(stderr, "%s: task is dead\n", __func__); + uring_task_put(cfg, task); + return; + } + + if (res != sizeof(idle->value)) + perrordie("timerfd_read"); + + fprintf(stderr, "%s: called with value %" PRIu64 "\n", __func__, idle->value); + + idle->next_remote = 0; + if (!list_empty(&idle->server->proxys)) + idle->server->idle_count = 0; + else + idle_check_connect_next_remote(cfg, idle); + uring_read(cfg, &idle->task, &idle->value, sizeof(idle->value), 0, idle_cb); +} + +static void +idle_free(struct uring_task *task) +{ + struct idle *idle = container_of(task, struct idle, task); + + fprintf(stderr, "%s: called with task 0x%p and idle 0x%p\n", __func__, task, idle); + idle->server->idle = NULL; + free(idle); +} + +void +idle_refdump(struct idle *idle) +{ + if (!idle) + return; + + uring_task_refdump(&idle->task); + uring_task_refdump(&idle->idlecheck); +} + +void +idle_delete(struct cfg *cfg, struct server *server) +{ + struct idle *idle = server->idle; + + if (!idle) + return; + + fprintf(stderr, "%s called, closing fd %i\n", __func__, idle->task.fd); + uring_cancel(cfg, &idle->task); + uring_task_put(cfg, &idle->task); + uring_task_put(cfg, &idle->idlecheck); + server->idle = NULL; +} + +void +idle_init(struct cfg *cfg, struct server *server) +{ + struct idle *idle; + int ifd; + struct itimerspec tspec = { + .it_interval = { + .tv_sec = 60, + .tv_nsec = 0 + }, + .it_value = { + /* FIXME: change to 60 */ + .tv_sec = 4, + .tv_nsec = 0 + } + }; + + if (!server) + return; + + if (server->idle_timeout < 1) + return; + + idle = zmalloc(sizeof(*idle)); + if (!idle) + perrordie("malloc"); + + ifd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC); + if (ifd < 0) + perrordie("timerfd_create"); + + if (timerfd_settime(ifd, 0, &tspec, NULL) != 0) + perrordie("timerfd_settime"); + + uring_task_init(&idle->task, "idle", &server->task, idle_free); + uring_task_init(&idle->idlecheck, "idlecheck", &idle->task, idle_check_free); + uring_task_set_fd(&idle->task, ifd); + idle->server = server; + server->idle = idle; + + uring_read(cfg, &idle->task, &idle->value, sizeof(idle->value), 0, idle_cb); +} + -- cgit v1.2.3