From a89a0f918925a662503c1bcb28bdb06ab9b7ef25 Mon Sep 17 00:00:00 2001 From: David Härdeman Date: Tue, 30 Jun 2020 08:10:04 +0200 Subject: Share config parsing fully between server and cmdline tool --- shared/config-parser.c | 449 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 449 insertions(+) (limited to 'shared/config-parser.c') diff --git a/shared/config-parser.c b/shared/config-parser.c index 13b26e1..ee30156 100644 --- a/shared/config-parser.c +++ b/shared/config-parser.c @@ -11,8 +11,456 @@ #include "utils.h" #include "config-parser.h" +#include "server-config-options.h" #include "config.h" +static void handle_dns_reply(struct server_config *scfg, enum scfg_keys type, + struct saddr *saddr) +{ + switch (type) { + case SCFG_KEY_LOCAL: + list_add(&saddr->list, &scfg->locals); + break; + case SCFG_KEY_REMOTE: + list_add(&saddr->list, &scfg->remotes); + break; + case SCFG_KEY_RCON: + list_add(&saddr->list, &scfg->rcons); + break; + default: + error("invalid DNS reply type"); + break; + } +} + +static void scfg_dns_cb(struct dns_async *dns, enum scfg_keys type) +{ + struct server_config *scfg; + struct sockaddr_in *in4; + struct sockaddr_in6 *in6; + struct saddr *saddr; + struct addrinfo *results = NULL, *ai; + int r; + + assert_return(dns); + + scfg = dns->priv; + debug(DBG_DNS, "called, dns: %p, name: %s", dns, dns->name); + + r = gai_error(&dns->gcb); + if (r == EAI_INPROGRESS) { + /* This shouldn't happen, assume we'll get called again */ + error("called with request in progress"); + return; + } else if (r == EAI_CANCELED) { + /* The server must be in the process of going away */ + goto out; + } else if (r < 0) { + error("DNS lookup of %s:%s failed: %s", dns->name, dns->port, + gai_strerror(r)); + goto out; + } + + results = dns->gcb.ar_result; + + for (ai = results; ai; ai = ai->ai_next) { + saddr = zmalloc(sizeof(*saddr)); + if (!saddr) { + error("DNS lookup of %s:%s failed: %m", dns->name, + dns->port); + goto out; + } + + switch (ai->ai_family) { + case AF_INET: + in4 = (struct sockaddr_in *)ai->ai_addr; + saddr_set_ipv4(saddr, in4->sin_addr.s_addr, + in4->sin_port); + break; + + case AF_INET6: + in6 = (struct sockaddr_in6 *)ai->ai_addr; + saddr_set_ipv6(saddr, &in6->sin6_addr, in6->sin6_port); + break; + + default: + error("getaddrinfo(%s:%s): unknown address family (%i)", + dns->name, dns->port, ai->ai_family); + xfree(saddr); + continue; + } + + handle_dns_reply(scfg, type, saddr); + } + +out: + freeaddrinfo(results); + list_del(&dns->list); + if (dns->notification_cb) + dns->notification_cb(scfg, true); + xfree(dns); +} + +static void scfg_dns_local_cb(struct dns_async *dns) +{ + scfg_dns_cb(dns, SCFG_KEY_LOCAL); +} + +static void scfg_dns_remote_cb(struct dns_async *dns) +{ + scfg_dns_cb(dns, SCFG_KEY_REMOTE); +} + +static void scfg_dns_rcon_cb(struct dns_async *dns) +{ + scfg_dns_cb(dns, SCFG_KEY_RCON); +} + +static bool handle_dns(struct server_config *scfg, enum scfg_keys type, + struct cfg_value *value, + notification_cb_t notification_cb) +{ + struct saddr *saddr, *tmp; + struct dns_async *dns; + + assert_return(scfg && type && value, false); + + switch (value->type) { + case CFG_VAL_TYPE_ADDRS: + debug(DBG_DNS, "%i: got immediate addrs", type); + + list_for_each_entry_safe(saddr, tmp, &value->saddrs, list) { + list_del(&saddr->list); + handle_dns_reply(scfg, type, saddr); + } + return true; + + case CFG_VAL_TYPE_ASYNC_ADDRS: + debug(DBG_DNS, "doing async lookup of DNS record: %p", + value->dns_async); + + dns = value->dns_async; + switch (type) { + case SCFG_KEY_LOCAL: + dns->cb = scfg_dns_local_cb; + break; + case SCFG_KEY_REMOTE: + dns->cb = scfg_dns_remote_cb; + break; + case SCFG_KEY_RCON: + dns->cb = scfg_dns_rcon_cb; + break; + default: + error("invalid DNS lookup type"); + return false; + } + + if (!!notification_cb) { + error("async DNS lookup received but not requested"); + xfree(dns); + return false; + } + + dns->priv = scfg; + dns->notification_cb = notification_cb; + list_add(&dns->list, &scfg->dnslookups); + dns->notification_cb(scfg, false); + return true; + + default: + return false; + } +} + +static inline char tohex(uint8_t val) +{ + static const char hex[] = "0123456789abcdef"; + + return hex[val & 0x0f]; +} + +/* + * Creates an escaped D-Bus object path for a given systemd service + * + * Escaping rules are documented here: + * https://dbus.freedesktop.org/doc/dbus-specification.html + * + * Essentially, everyting but a-z, A-Z, 0-9 is replaced by _xx where xx is + * the hexadecimal value of the character. + * + * Example: minecraft@world1.service -> minecraft_40world1_2eservice + */ +static char *systemd_object_path(const char *service) +{ + char *r; + char *d; + const char *s; + + assert_return(service && !empty_str(service), NULL); + + r = zmalloc(STRLEN(SYSTEMD_DBUS_PATH_PREFIX) + strlen(service) * 3 + 1); + if (!r) + return NULL; + + memcpy(r, SYSTEMD_DBUS_PATH_PREFIX, STRLEN(SYSTEMD_DBUS_PATH_PREFIX)); + d = r + STRLEN(SYSTEMD_DBUS_PATH_PREFIX); + + for (s = service; *s; s++) { + if ((*s >= 'a' && *s <= 'z') || (*s >= 'A' && *s <= 'Z') || + (*s >= '0' && *s <= '9')) { + *(d++) = *s; + continue; + } + + *(d++) = '_'; + *(d++) = tohex(*s >> 4); + *(d++) = tohex(*s); + } + + *d = '\0'; + return r; +} + +#define INVALID(fmt, ...) \ +do { \ + verbose("%s: " fmt, scfg->filename __VA_OPT__(,) __VA_ARGS__); \ + valid = false; \ +} while(0) + +bool scfg_parse(struct server_config *scfg, char *buf, + notification_cb_t notification_cb) +{ + char *pos; + bool valid = true; + + assert_return(scfg && buf, false); + + pos = buf; + + if (!config_parse_header(SERVER_CFG_HEADER, &pos)) { + INVALID("missing/invalid header"); + goto out; + } + + while (true) { + int key; + const char *keyname; + struct cfg_value value; + + if (!config_parse_line(scfg->filename, &pos, scfg_key_map, &key, + &keyname, &value, !!notification_cb)) + break; + + /* FIXME: Improve error reporting */ + if (key == SCFG_KEY_INVALID) { + INVALID("invalid line in config file"); + continue; + } + + debug(DBG_CFG, "%s: key %s", scfg->filename, keyname); + + switch (key) { + case SCFG_KEY_TYPE: + if (scfg->type != SERVER_TYPE_UNDEFINED) + INVALID("type defined multiple times"); + else if (streq(value.str, "proxy")) + scfg->type = SERVER_TYPE_PROXY; + else if (streq(value.str, "announce")) + scfg->type = SERVER_TYPE_ANNOUNCE; + else + INVALID("unknown type: %s", value.str); + break; + + case SCFG_KEY_NAME: + if (scfg->pretty_name) + INVALID("name defined multiple times"); + else if (empty_str(value.str)) + INVALID("name is an empty string"); + else if (!(scfg->pretty_name = xstrdup(value.str))) + INVALID("strdup: %m"); + break; + + case SCFG_KEY_PORT: + if (scfg->announce_port != 0) + INVALID("port defined multiple times"); + else + scfg->announce_port = value.uint16; + break; + + case SCFG_KEY_LOCAL: + if (!handle_dns(scfg, SCFG_KEY_LOCAL, &value, + notification_cb)) + INVALID("handle_dns failed"); + break; + + case SCFG_KEY_REMOTE: + if (!handle_dns(scfg, SCFG_KEY_REMOTE, &value, + notification_cb)) + INVALID("handle_dns failed"); + break; + + case SCFG_KEY_IDLE_TIMEOUT: + if (scfg->idle_timeout != 0) + INVALID("idle_timeout already set"); + else + scfg->idle_timeout = value.uint16; + break; + + case SCFG_KEY_STOP_METHOD: + if (scfg->stop_method != SERVER_STOP_METHOD_UNDEFINED) + INVALID("stop method defined multiple times"); + else if (streq(value.str, "exec")) + scfg->stop_method = SERVER_STOP_METHOD_EXEC; + else if (streq(value.str, "rcon")) + scfg->stop_method = SERVER_STOP_METHOD_RCON; + else if (streq(value.str, "systemd")) + scfg->stop_method = SERVER_STOP_METHOD_SYSTEMD; + else + INVALID("unknown stop method: %s", value.str); + break; + + case SCFG_KEY_START_METHOD: + if (scfg->start_method != SERVER_START_METHOD_UNDEFINED) + INVALID("start method defined multiple times"); + else if (streq(value.str, "exec")) + scfg->start_method = SERVER_START_METHOD_EXEC; + else if (streq(value.str, "systemd")) + scfg->start_method = SERVER_START_METHOD_SYSTEMD; + else + INVALID("unknown start method: %s", value.str); + break; + + case SCFG_KEY_STOP_EXEC: + if (scfg->stop_exec) + INVALID("stop exec defined multiple times"); + else if (!(scfg->stop_exec = xstrdup(value.str))) + INVALID("strdup: %m"); + break; + + case SCFG_KEY_START_EXEC: + if (scfg->start_exec) + INVALID("start exec defined multiple times"); + else if (!(scfg->start_exec = xstrdup(value.str))) + INVALID("strdup: %m"); + break; + + case SCFG_KEY_RCON: + if (!handle_dns(scfg, SCFG_KEY_RCON, &value, + notification_cb)) + INVALID("handle_dns failed"); + break; + + case SCFG_KEY_RCON_PASSWORD: + if (scfg->rcon_password) + INVALID("rcon_password defined multiple times"); + else if (!(scfg->rcon_password = xstrdup(value.str))) + INVALID("strdup: %m"); + break; + + case SCFG_KEY_SYSTEMD_SERVICE: + if (scfg->systemd_service) + INVALID("systemd_service defined multiple times"); + else { + const char *suffix; + char *tmp; + + suffix = strrchr(value.str, '.'); + if (!suffix || !streq(suffix, ".service")) { + tmp = zmalloc(strlen(value.str) + + STRLEN(".service") + 1); + if (tmp) + sprintf(tmp, "%s.service", + value.str); + } else + tmp = xstrdup(value.str); + + if (!tmp) { + INVALID("malloc/strdup: %m"); + break; + } + + scfg->systemd_service = tmp; + scfg->systemd_obj = + systemd_object_path(scfg->systemd_service); + if (!scfg->systemd_obj) + INVALID("object_path: %m"); + } + + break; + + case SCFG_KEY_INVALID: + default: + INVALID("invalid line"); + break; + } + } + +out: + return valid; +} + +void scfg_delete(struct server_config *scfg) +{ + struct saddr *saddr, *tmp; + struct dns_async *dns; + + xfree(scfg->filename); + xfree(scfg->pretty_name); + xfree(scfg->stop_exec); + xfree(scfg->start_exec); + /* FIXME: use free_password */ + xfree(scfg->rcon_password); + xfree(scfg->systemd_service); + xfree(scfg->systemd_obj); + + list_for_each_entry_safe(saddr, tmp, &scfg->remotes, list) { + list_del(&saddr->list); + xfree(saddr); + } + + list_for_each_entry_safe(saddr, tmp, &scfg->rcons, list) { + list_del(&saddr->list); + xfree(saddr); + } + + list_for_each_entry_safe(saddr, tmp, &scfg->locals, list) { + list_del(&saddr->list); + xfree(saddr); + } + + list_for_each_entry(dns, &scfg->dnslookups, list) + gai_cancel(&dns->gcb); +} + +bool scfg_init(struct server_config *scfg, const char *filename) +{ + assert_return(scfg, false); + + if (filename) { + scfg->filename = xstrdup(filename); + if (!scfg->filename) { + error("strdup: %m"); + return false; + } + } + scfg->type = SERVER_TYPE_UNDEFINED; + scfg->pretty_name = NULL; + scfg->announce_port = 0; + scfg->idle_timeout = 0; + scfg->stop_method = SERVER_STOP_METHOD_UNDEFINED; + scfg->start_method = SERVER_START_METHOD_UNDEFINED; + scfg->stop_exec = NULL; + scfg->start_exec = NULL; + scfg->rcon_password = NULL; + scfg->systemd_service = NULL; + scfg->systemd_obj = NULL; + INIT_LIST_HEAD(&scfg->remotes); + INIT_LIST_HEAD(&scfg->locals); + INIT_LIST_HEAD(&scfg->rcons); + INIT_LIST_HEAD(&scfg->dnslookups); + return true; +} + static void eat_whitespace_and_comments(char **pos) { assert_return(pos && *pos); @@ -126,6 +574,7 @@ static bool dnslookup(const char *name, uint16_t port, struct cfg_value *rvalue, results = dns->gcb.ar_result; + /* FIXME: parts can be shared with the async cb? */ for (ai = results; ai; ai = ai->ai_next) { saddr = zmalloc(sizeof(*saddr)); if (!saddr) { -- cgit v1.2.3