summaryrefslogtreecommitdiff
path: root/idle.c
diff options
context:
space:
mode:
Diffstat (limited to 'idle.c')
-rw-r--r--idle.c453
1 files changed, 453 insertions, 0 deletions
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 <inttypes.h>
+#include <sys/timerfd.h>
+#include <sys/socket.h>
+#include <netinet/ip.h>
+#include <arpa/inet.h>
+#include <string.h>
+#include <ctype.h>
+
+#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);
+}
+