#define _GNU_SOURCE #include #include #include #include #include #include #include #include #include "main.h" #include "uring.h" #include "config.h" #include "server.h" #include "idle.h" struct idle { struct server *server; struct uring_task task; uint64_t value; struct uring_task idlecheck; struct connection conn; struct uring_task_buf tbuf; }; static void idle_check_free(struct uring_task *task) { struct idle *idle = container_of(task, struct idle, idlecheck); debug(DBG_IDLE, "task %p, idle %p\n", 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 = 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; 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) { 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); } static int idle_check_handshake_complete(struct cfg *cfg, struct uring_task *task, int res) { size_t remain; char *pos; int32_t mclen; int r; remain = task->tbuf->len; pos = task->tbuf->buf; r = read_varint(&pos, &remain, &mclen); if (r < 0) { error("failed to parse message length\n"); return -EINVAL; } else if (r == 0) { return 0; } else if (mclen < 2) { error("short MC message\n"); return -EINVAL; } if (mclen < remain) { debug(DBG_IDLE, "short MC message - len: %" PRIi32 ", remain: %zu\n", mclen, remain); return 0; } debug(DBG_IDLE, "Complete message\n"); return 1; } #define ONLINE_NEEDLE "\"online\"" static int get_player_count(struct cfg *cfg, 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; online = memmem(pos, remain, ONLINE_NEEDLE, strlen(ONLINE_NEEDLE)); if (!online) { error("could not find online count in JSON\n"); return -1; } remain -= (online - pos); end = memchr(online, '}', remain); if (!end) { error("could not parse JSON (no end)\n"); return -1; } *end = '\0'; if (sscanf(online, ONLINE_NEEDLE " : %u", &count) != 1) { error("could not parse JSON (online count)\n"); return -1; } return count; } 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 player_count; int r; debug(DBG_IDLE, "res: %i\n", res); if (res < 0) goto out; /* 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->tbuf.len; pos = idle->tbuf.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\n"); goto out; } debug(DBG_IDLE, "MC message - len: %" PRIi32 ", remain: %zu\n", mclen, remain); if (*pos != MC_STATUS_REPLY) { error("unknown server reply (0x%02hhx)\n", *pos); goto out; } pos++; remain--; r = read_varint(&pos, &remain, &jsonlen); if (r <= 0) { error("could not read JSON length\n"); goto out; } debug(DBG_IDLE, "MC - json len: %" PRIi32 ", remain: %zu\n", jsonlen, remain); if (jsonlen < remain) { error("invalid JSON length\n"); goto out; } /* fprintf(stderr, "JSON: "); for (int i = 0; i < jsonlen; i++) fprintf(stderr, "%c", pos[i]); fprintf(stderr, "\n"); */ player_count = get_player_count(cfg, pos, remain); if (player_count < 0) goto out; idle->server->state = SERVER_STATE_RUNNING; debug(DBG_IDLE, "%s: currently %i active players\n", idle->server->name, player_count); if (player_count > 0) idle->server->idle_count = 0; else if (player_count == 0) idle->server->idle_count++; if (idle->server->idle_count > idle->server->idle_timeout) { verbose("stopping idle server %s\n", idle->server->name); server_stop(cfg, idle->server); } out: uring_task_close_fd(cfg, task); 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); debug(DBG_IDLE, "sent %i bytes\n", res); if (res < 0) { uring_task_close_fd(cfg, task); return; } uring_tbuf_read_until(cfg, &idle->idlecheck, idle_check_handshake_complete, idle_check_handshake_reply); } static void idle_check_connected_cb(struct cfg *cfg, struct connection *conn, bool connected) { struct idle *idle = container_of(conn, struct idle, conn); char buf[1024]; char *pos; char *cmdbuf = idle->tbuf.buf; uint16_t port; char hostname[INET6_ADDRSTRLEN]; if (!connected) { debug(DBG_IDLE, "idle check connection to remote server (%s) failed\n", idle->server->name); idle->server->idle_count = 0; idle->server->state = SERVER_STATE_STOPPED; return; } debug(DBG_IDLE, "connected to remote %s\n", idle->conn.remote.addrstr); port = saddr_port(&conn->remote); saddr_addr(&conn->remote, hostname, sizeof(hostname)); 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->tbuf.len = (cmdbuf - idle->tbuf.buf); debug(DBG_IDLE, "sending MC message (%zu bytes)\n", idle->tbuf.len); uring_tbuf_write(cfg, &idle->idlecheck, idle_check_handshake_sent); } static void idle_cb(struct cfg *cfg, struct uring_task *task, int res) { struct idle *idle = container_of(task, struct idle, task); if (task->dead) { debug(DBG_IDLE, "task is dead\n"); return; } if (res != sizeof(idle->value)) { error("timerfd_read returned %i\n", res); perrordie("timerfd_read"); } debug(DBG_IDLE, "timer fired (value: %" PRIu64 ")\n", idle->value); if (!list_empty(&idle->server->proxys)) idle->server->idle_count = 0; else connect_any(cfg, &idle->idlecheck, &idle->server->remotes, &idle->conn, idle_check_connected_cb); uring_read(cfg, &idle->task, &idle->value, sizeof(idle->value), idle_cb); } static void idle_free(struct uring_task *task) { struct idle *idle = container_of(task, struct idle, task); debug(DBG_IDLE, "task %p, idle %p\n", task, idle); xfree(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; debug(DBG_IDLE, "closing fd %i\n", idle->task.fd); uring_task_destroy(cfg, &idle->idlecheck); uring_task_destroy(cfg, &idle->task); 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_set_fd(&idle->task, ifd); uring_task_init(&idle->idlecheck, "idlecheck", &idle->task, idle_check_free); uring_task_set_buf(&idle->idlecheck, &idle->tbuf); idle->server = server; server->idle = idle; uring_read(cfg, &idle->task, &idle->value, sizeof(idle->value), idle_cb); }