From b02e89b8b82f0534fd1490a8780ac89d707181b6 Mon Sep 17 00:00:00 2001 From: David Härdeman Date: Tue, 23 Jun 2020 21:00:15 +0200 Subject: Move config-parser to shared lib --- shared/config-parser.c | 490 +++++++++++++++++++++++++++++++++++++++++++++++++ shared/config-parser.h | 59 ++++++ shared/meson.build | 1 + 3 files changed, 550 insertions(+) create mode 100644 shared/config-parser.c create mode 100644 shared/config-parser.h (limited to 'shared') diff --git a/shared/config-parser.c b/shared/config-parser.c new file mode 100644 index 0000000..9c89cf2 --- /dev/null +++ b/shared/config-parser.c @@ -0,0 +1,490 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +#include "utils.h" +#include "config-parser.h" + +static void +eat_whitespace_and_comments(char **pos) +{ + assert_return(pos && *pos); + + while (true) { + while (isspace(**pos)) + (*pos)++; + + if (**pos != '#') + return; + + while (**pos != '\r' && **pos != '\n' && **pos != '\0') + (*pos)++; + } +} + +static char * +get_line(char **pos) +{ + char *begin, *end; + + assert_return(pos && *pos, NULL); + + begin = *pos; + while (isspace(*begin)) + begin++; + + if (*begin == '\0') + return NULL; + + end = begin; + while (*end != '\n' && *end != '\0') + end++; + + if (*end == '\0') + *pos = end; + else + *pos = end + 1; + + while (isspace(*end)) { + *end = '\0'; + end--; + } + + return begin; +} + +static bool +dnslookup(const char *name, uint16_t port, struct cfg_value *rvalue, bool async) +{ + struct sockaddr_in *in4; + struct sockaddr_in6 *in6; + struct dns_async tmp; + struct dns_async *dns; + int mode = async ? GAI_NOWAIT : GAI_WAIT; + struct addrinfo *results = NULL, *ai; + struct saddr *saddr = NULL; + bool rv = false; + int r; + + assert_return(!empty_str(name) && strlen(name) < sizeof(dns->name) && port > 0 && rvalue, false); + + if (async) { + rvalue->type = CFG_VAL_TYPE_ASYNC_ADDRS; + rvalue->dns_async = NULL; + dns = zmalloc(sizeof(*dns)); + if (!dns) { + error("async DNS lookup of %s failed: %m", name); + goto out; + } + debug(DBG_DNS, "doing async DNS lookup of %s: %p", name, dns); + } else { + memset(&tmp, 0, sizeof(tmp)); + dns = &tmp; + debug(DBG_DNS, "doing sync DNS lookup of %s", name); + } + + sprintf(dns->name, "%s", name); + sprintf(dns->port, "%" PRIu16, port); + + dns->req.ai_family = AF_UNSPEC; + dns->req.ai_socktype = SOCK_STREAM; + dns->req.ai_protocol = 0; + dns->req.ai_flags = AI_NUMERICSERV; + + dns->sev.sigev_notify = SIGEV_SIGNAL; + dns->sev.sigev_signo = SIGUSR1; + dns->sev.sigev_value.sival_ptr = dns; + + dns->gcb.ar_name = dns->name; + dns->gcb.ar_service = dns->port; + dns->gcb.ar_request = &dns->req; + + struct gaicb *gcbs[] = { &dns->gcb }; + + r = getaddrinfo_a(mode, gcbs, ARRAY_SIZE(gcbs), &dns->sev); + if (r != 0) { + error("getaddrinfo(%s:%" PRIu16 "): %s", name, port, gai_strerror(r)); + goto out; + } + + if (async) { + rvalue->dns_async = dns; + rv = true; + goto out; + } + + results = dns->gcb.ar_result; + + for (ai = results; ai; ai = ai->ai_next) { + saddr = zmalloc(sizeof(*saddr)); + if (!saddr) { + error("sync DNS lookup of %s failed: %m", name); + 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); + error("addrstr: %s", saddr->addrstr); + list_add(&saddr->list, &rvalue->saddrs); + break; + + case AF_INET6: + in6 = (struct sockaddr_in6 *)ai->ai_addr; + saddr_set_ipv6(saddr, &in6->sin6_addr, in6->sin6_port); + error("addrstr: %s", saddr->addrstr); + list_add(&saddr->list, &rvalue->saddrs); + break; + + default: + error("getaddrinfo(%s:%s): unknown address family (%i)", + dns->name, dns->port, ai->ai_family); + xfree(saddr); + break; + } + } + + rv = true; + +out: + freeaddrinfo(results); + return rv; +} + +static bool +strtosockaddrs(const char *str, struct cfg_value *rvalue, bool async) +{ + struct saddr *saddr; + uint16_t port; + char *tmp; + struct list_head *list; + unsigned naddrs = 0; + + assert_return(!empty_str(str) && rvalue, false); + + rvalue->type = CFG_VAL_TYPE_ADDRS; + list = &rvalue->saddrs; + list_init(list); + + if (*str == '[') { + /* IPv6, [a:b:c...h]:p or [*]:p */ + debug(DBG_CFG, "attempting to parse IPv6 addr (%s)", str); + + str++; + tmp = strchr(str, ']'); + if (!tmp) + goto error; + *tmp = '\0'; + + saddr = zmalloc(sizeof(*saddr)); + if (!saddr) + goto error; + + /* early list_add to make sure saddr is free():d on error */ + list_add(&saddr->list, list); + + if (streq(str, "*")) + saddr->in6.sin6_addr = in6addr_any; + else if (inet_pton(AF_INET6, str, &saddr->in6.sin6_addr) <= 0) + goto error; + + tmp++; + if (*tmp != ':') + goto error; + + tmp++; + if (strtou16_strict(tmp, &port) < 0) + goto error; + + saddr_set_ipv6(saddr, NULL, htons(port)); + naddrs++; + + } else if (*str == '*') { + /* IPv4, *:p */ + debug(DBG_CFG, "attempting to parse IPv4 addr (%s)", str); + + str++; + if (*str != ':') + goto error; + + str++; + if (strtou16_strict(str, &port) < 0) + goto error; + + saddr = zmalloc(sizeof(*saddr)); + if (!saddr) + goto error; + + saddr_set_ipv4(saddr, INADDR_ANY, htons(port)); + list_add(&saddr->list, list); + naddrs++; + + } else if ((tmp = strchr(str, ':'))) { + /* IPv4, a.b.c.d:p or IPv4/6 hostname:p */ + debug(DBG_CFG, "attempting to parse IPv4 addr or hostname (%s)", str); + + *tmp = '\0'; + tmp++; + if (strtou16_strict(tmp, &port) < 0) + goto error; + + saddr = zmalloc(sizeof(*saddr)); + if (!saddr) + goto error; + + if (inet_pton(AF_INET, str, &saddr->in4.sin_addr) > 0) { + debug(DBG_CFG, "got an IPv4:port (%s:%" PRIu16 ")", str, port); + saddr_set_ipv4(saddr, saddr->in4.sin_addr.s_addr, htons(port)); + list_add(&saddr->list, list); + naddrs++; + goto success; + } + + xfree(saddr); + debug(DBG_CFG, "maybe got a hostname:port (%s:%" PRIu16 ")", str, port); + if (!dnslookup(str, port, rvalue, async)) + goto error; + + } else if (strtou16_strict(tmp, &port) == 0) { + /* Port */ + debug(DBG_CFG, "attempting to parse a port number (%s)", str); + + saddr = zmalloc(sizeof(*saddr)); + if (!saddr) + goto error; + + saddr_set_ipv6(saddr, &in6addr_any, htons(port)); + list_add(&saddr->list, list); + naddrs++; + + saddr = zmalloc(sizeof(*saddr)); + if (!saddr) + goto error; + + saddr_set_ipv4(saddr, INADDR_ANY, htons(port)); + list_add(&saddr->list, list); + naddrs++; + + } else { + /* Unknown */ + error("unable to parse address: %s", str); + goto error; + } + +success: + switch (rvalue->type) { + case CFG_VAL_TYPE_ADDRS: + if (list_empty(list) || naddrs == 0) { + error("empty address list"); + return false; + } + + debug(DBG_CFG, "parsed to %u addresses", naddrs); + return true; + + case CFG_VAL_TYPE_ASYNC_ADDRS: + debug(DBG_CFG, "looking up address asynchronously"); + return true; + + default: + error("invalid rvalue type"); + rvalue->type = CFG_VAL_TYPE_INVALID; + break; + } + +error: + if (rvalue->type == CFG_VAL_TYPE_ADDRS && !list_empty(list)) { + struct saddr *tmp; + + list_for_each_entry_safe(saddr, tmp, list, list) { + list_del(&saddr->list); + xfree(saddr); + } + } + return false; +} + +/* Returns true if there's data left to parse in buf */ +bool +config_parse_line(const char *filename, char **buf, + struct cfg_key_value_map *kvmap, int *rkey, + const char **rkeyname, struct cfg_value *rvalue) +{ + char *line, *tmp, *key; + int i; + + assert_return(buf && *buf && kvmap && rkey && rkeyname && rvalue, false); + + eat_whitespace_and_comments(buf); + line = get_line(buf); + if (!line) + return false; + + debug(DBG_CFG, "%s: parsing config line: %s", filename, line); + + tmp = line; + while (isspace(*tmp)) + tmp++; + + if (*tmp == '\0') + goto error; + + key = tmp; + while (*tmp != '\0' && !isspace(*tmp)) + tmp++; + + if (*tmp == '\0') + goto error; + + *tmp = '\0'; + tmp++; + + while (isspace(*tmp)) + tmp++; + + if (*tmp != '=') + goto error; + + tmp++; + while (isspace(*tmp)) + tmp++; + + if (*tmp == '\0') + goto error; + + for (i = 0; kvmap[i].key_name; i++) { + if (!streq(kvmap[i].key_name, key)) + continue; + + switch (kvmap[i].value_type) { + + case CFG_VAL_TYPE_STRING: + rvalue->type = CFG_VAL_TYPE_STRING; + rvalue->str = tmp; + break; + + case CFG_VAL_TYPE_UINT16: { + uint16_t v; + + if (strtou16_strict(tmp, &v) < 0) + goto error; + + rvalue->type = CFG_VAL_TYPE_UINT16; + rvalue->uint16 = v; + break; + } + + case CFG_VAL_TYPE_ADDRS: + if (!strtosockaddrs(tmp, rvalue, false)) + goto error; + + if (rvalue->type != CFG_VAL_TYPE_ADDRS) { + error("invalid type returned from strtosockaddrs"); + goto error; + } + + if (list_empty(&rvalue->saddrs)) { + error("empty address list"); + goto error; + } + break; + + case CFG_VAL_TYPE_ASYNC_ADDRS: + if (!strtosockaddrs(tmp, rvalue, true)) + goto error; + + switch (rvalue->type) { + case CFG_VAL_TYPE_ADDRS: + if (list_empty(&rvalue->saddrs)) { + error("empty address list"); + goto error; + } + break; + + case CFG_VAL_TYPE_ASYNC_ADDRS: + if (!rvalue->dns_async) { + error("dns_async not set"); + goto error; + } + break; + + default: + error("invalid type returned from strtosockaddrs"); + goto error; + } + + break; + + case CFG_VAL_TYPE_BOOL: + if (strcaseeq(tmp, "yes") || strcaseeq(tmp, "true")) { + rvalue->type = CFG_VAL_TYPE_BOOL; + rvalue->boolean = true; + } else if (strcaseeq(tmp, "no") || strcaseeq(tmp, "false")) { + rvalue->type = CFG_VAL_TYPE_BOOL; + rvalue->boolean = false; + } else { + error("invalid boolean value (%s)", tmp); + goto error; + } + break; + + case CFG_VAL_TYPE_INVALID: + /* fall through */ + default: + goto error; + } + + /* sanity check */ + if ((rvalue->type != kvmap[i].value_type) && + ((kvmap[i].value_type != CFG_VAL_TYPE_ASYNC_ADDRS) && + (rvalue->type != CFG_VAL_TYPE_ADDRS))) { + error("rvalue->type != kvmap->type"); + goto error; + } + + *rkey = kvmap[i].key_value; + *rkeyname = kvmap[i].key_name; + return true; + } + +error: + /* FIXME: the line is already mangled here, a line number would be nice */ + error("%s: invalid config line: %s", filename, line); + rvalue->type = CFG_VAL_TYPE_INVALID; + *rkey = 0; + *rkeyname = NULL; + return true; +} + +bool +config_parse_header(const char *filename, const char *title, char **buf) +{ + char *line; + + assert_return(!empty_str(filename) && !empty_str(title) && buf && *buf, false); + + eat_whitespace_and_comments(buf); + + line = get_line(buf); + if (!line) { + error("%s: missing header in configuration file", filename); + return false; + } else { + char titlehdr[strlen(title) + 3]; + + sprintf(titlehdr, "[%s]", title); + if (!streq(line, titlehdr)) { + error("%s: incorrect header in configuration file", filename); + return false; + } + } + + return true; +} diff --git a/shared/config-parser.h b/shared/config-parser.h new file mode 100644 index 0000000..3a117a3 --- /dev/null +++ b/shared/config-parser.h @@ -0,0 +1,59 @@ +#ifndef fooconfigparserhfoo +#define fooconfigparserhfoo + +#define _GNU_SOURCE +#include +#include +#include +#include + +enum cfg_value_type { + CFG_VAL_TYPE_INVALID, + CFG_VAL_TYPE_STRING, + CFG_VAL_TYPE_UINT16, + CFG_VAL_TYPE_ADDRS, + CFG_VAL_TYPE_ASYNC_ADDRS, + CFG_VAL_TYPE_BOOL, +}; + +struct dns_async; + +typedef void (dns_cb_t)(struct dns_async *); + +struct dns_async { + char name[FQDN_STR_LEN + 1]; + char port[PORT_STR_LEN + 1]; + struct addrinfo req; + struct gaicb gcb; + struct sigevent sev; + dns_cb_t *cb; + void *priv; + struct list_head list; +}; + +struct cfg_key_value_map { + const char *key_name; + int key_value; + enum cfg_value_type value_type; +}; + +struct cfg_value { + enum cfg_value_type type; + union { + const char *str; + uint16_t uint16; + struct list_head saddrs; + struct dns_async *dns_async; + bool boolean; + }; +}; + +bool config_parse_line(const char *filename, char **buf, + struct cfg_key_value_map *kvmap, + int *rkey, const char **rkeyname, + struct cfg_value *rvalue); + +bool config_parse_header(const char *filename, + const char *title, char **buf); + +#endif diff --git a/shared/meson.build b/shared/meson.build index ccf502e..e4e4f29 100644 --- a/shared/meson.build +++ b/shared/meson.build @@ -1,5 +1,6 @@ srcs_libshared = [ 'rcon-protocol.c', + 'config-parser.c', 'utils.c', ] -- cgit v1.2.3