diff options
Diffstat (limited to 'minecctl')
-rw-r--r-- | minecctl/meson.build | 1 | ||||
-rw-r--r-- | minecctl/minecctl.c | 322 |
2 files changed, 304 insertions, 19 deletions
diff --git a/minecctl/meson.build b/minecctl/meson.build index 3490338..ac8210d 100644 --- a/minecctl/meson.build +++ b/minecctl/meson.build @@ -9,6 +9,7 @@ minecctl_deps = [ executable( 'minecctl', minecctl_sources, + link_args: [ '-lanl' ], dependencies: minecctl_deps, ) diff --git a/minecctl/minecctl.c b/minecctl/minecctl.c index e29dcef..d601980 100644 --- a/minecctl/minecctl.c +++ b/minecctl/minecctl.c @@ -1,39 +1,323 @@ +#define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <inttypes.h> +#include <stdarg.h> +#include <unistd.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <errno.h> #include "rcon-protocol.h" +#include "utils.h" +#include "config-parser.h" +#include "server-config-options.h" -int -main(int argc, char **argv) +void +__debug(enum debug_lvl lvl, const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); +} + +__attribute__((noreturn)) void +__die(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + + exit(EXIT_FAILURE); +}; + +void * +__zmalloc(const char *fn, int line, size_t size) +{ + void *ptr; + + assert_die(!empty_str(fn) && line > 0 && size > 0, "invalid arguments"); + + ptr = calloc(1, size); + if (!ptr) + die("malloc: %m"); + return ptr; +} + +char * +__xstrdup(const char *fn, int line, const char *s) +{ + char *ptr; + + assert_die(!empty_str(fn) && line > 0 && !empty_str(s), "invalid arguments"); + + ptr = strdup(s); + if (!ptr) + die("strdup: %m"); + return ptr; +} + +char * +__xstrndup(const char *fn, int line, const char *s, size_t n) +{ + char *ptr; + + assert_die(!empty_str(fn) && line > 0 && !empty_str(s) && n > 0, "invalid arguments"); + + ptr = strndup(s, n); + if (ptr) + die("strdup: %m"); + return ptr; +} + +void +__xfree(const char *fn, int line, void *ptr) +{ + assert_die(!empty_str(fn) && line > 0, "invalid arguments"); + + free(ptr); +} + +static void +send_packet(int sfd, const char *buf, size_t len) +{ + size_t off = 0; + ssize_t r; + + while (true) { + r = write(sfd, buf + off, len - off); + if (r < 0) { + if (errno == EINTR) + continue; + else + die("Failed to write packet: %m"); + } + + off += r; + if (off == len) + break; + } +} + +static void +read_packet(int sfd, char *buf, size_t len, int32_t *id, int32_t *type, const char **msg) +{ + size_t off = 0; + size_t r; + const char *error; + + while (true) { + r = read(sfd, buf + off, len - off); + if (r < 0) { + if (errno == EINTR) + continue; + else + die("Failed to read reply: %m"); + } + + if (r == 0) + die("Failed, connection closed"); + + off += r; + if (rcon_protocol_packet_complete(buf, off)) + break; + + if (off >= len) + die("Reply too large %zu and %zu", off, len); + } + + if (!rcon_protocol_read_packet(buf, off, id, type, msg, &error)) + die("Failed to parse response: %s", error); +} + +static void +send_msg(int sfd, char *buf, size_t len, enum rcon_packet_type type, + const char *msg, enum rcon_packet_type *rtype, const char **reply) +{ + static unsigned rcon_packet_id = 1; + size_t plen; + int32_t id; + + if (!rcon_protocol_create_packet(buf, len, &plen, + rcon_packet_id, type, msg)) + die("Failed to create rcon packet"); + + send_packet(sfd, buf, plen); + read_packet(sfd, buf, len, &id, rtype, reply); + + if (id != rcon_packet_id) + die("Invalid reply id"); + + rcon_packet_id++; +} + +static void +send_login(int sfd, const char *password) +{ + char buf[4096]; + int32_t rtype; + const char *reply; + + send_msg(sfd, buf, sizeof(buf), RCON_PACKET_LOGIN, password, &rtype, &reply); + + if (rtype == RCON_PACKET_LOGIN_OK) + info("Login ok"); + else if (rtype == RCON_PACKET_LOGIN_FAIL) + die("Login failure, invalid password?"); + else + die("Invalid return code: %" PRIi32, rtype); +} + +static void +send_cmd(int sfd, const char *cmd) { char buf[4096]; - size_t len; - int32_t id, type; - const char *msg, *error; + int32_t rtype; + const char *reply; + + send_msg(sfd, buf, sizeof(buf), RCON_PACKET_COMMAND, "stop", &rtype, &reply); + + if (rtype == RCON_PACKET_RESPONSE) + info("Command (%s) sent, reply: %s", cmd, reply); + else + die("Invalid return code: %" PRIi32, rtype); +} - fprintf(stderr, "Started\n"); +static int +connect_any(struct list_head *addrs) +{ + struct saddr *saddr; + bool connected = false; + int sfd; + + list_for_each_entry(saddr, addrs, list) { + sfd = socket(saddr->storage.ss_family, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (sfd < 0) + die("socket: %m"); + + socket_set_low_latency(sfd, true, true, true); - if (!rcon_protocol_create_packet(buf, sizeof(buf), &len, - 1, RCON_PACKET_LOGIN, - "test")) { - fprintf(stderr, "Failed to create packet\n"); - exit(EXIT_FAILURE); + if (connect(sfd, (struct sockaddr *)&saddr->storage, saddr->addrlen) < 0) { + close(sfd); + continue; + } + + connected = true; + break; } - if (!rcon_protocol_packet_complete(buf, len)) { - fprintf(stderr, "Packet not complete\n"); - exit(EXIT_FAILURE); + if (!connected) + die("Failed to connect to remote host"); + + return sfd; +} + +static void +parse_config(char *buf, const char *filename, + const char **password, struct list_head *addrs) +{ + *password = NULL; + list_init(addrs); + + /* FIXME: filename argument is superfluous */ + if (!config_parse_header(filename, SERVER_CFG_HEADER, &buf)) + die("Unable to parse %s: invalid header", filename); + + /* FIXME: this will cause superflous DNS lookups of other cfg entries */ + while (true) { + int key; + const char *keyname; + struct cfg_value value; + + if (!config_parse_line(filename, &buf, scfg_key_map, + &key, &keyname, &value)) + break; + + switch (key) { + case SCFG_KEY_RCON: + list_replace(&value.saddrs, addrs); + break; + case SCFG_KEY_RCON_PASSWORD: + *password = value.str; + break; + default: + continue; + } + + if (*password && !list_empty(addrs)) + break; } - if (!rcon_protocol_read_packet(buf, len, &id, &type, &msg, &error)) { - fprintf(stderr, "Packet parsing failed: %s\n", error); - exit(EXIT_FAILURE); + if (!*password) + die("rcon password not found in %s", filename); + + if (list_empty(addrs)) + die("rcon address not found in %s", filename); +} + +static void +read_file(const char *filename, char *buf, size_t len) +{ + int fd; + size_t off = 0; + ssize_t r; + + fd = open(filename, O_RDONLY | O_CLOEXEC); + if (fd < 0) + die("Failed to open %s: %m", filename); + + while (true) { + r = read(fd, buf + off, len - off - 1); + if (r < 0) + die("Failed to read %s: %m", filename); + else if (r == 0) + break; + + off += r; + if (off == len) + die("Failed to read %s: file too large", filename); } - fprintf(stderr, "Packet - id: %" PRIi32 ", type: %" PRIi32 ", msg: %s\n", - id, type, msg); + buf[off] = '\0'; + close(fd); +} + +int +main(int argc, char **argv) +{ + char buf[4096]; + const char *password; + const char *filename; + struct list_head addrs; + struct saddr *saddr; + int fd; + + debug_mask = DBG_ERROR | DBG_INFO; + + if (argc != 2) + die("Usage: minecctl CFGFILE"); + + filename = argv[1]; + + read_file(filename, buf, sizeof(buf)); + + parse_config(buf, filename, &password, &addrs); + + info("Password: %s", password); + list_for_each_entry(saddr, &addrs, list) + info("Address: %s", saddr->addrstr); + + fd = connect_any(&addrs); + + send_login(fd, password); + + send_cmd(fd, "stop"); exit(EXIT_SUCCESS); } |