From cdd78086423649237b69485befa4b13b76136d81 Mon Sep 17 00:00:00 2001 From: Jordan Bancino Date: Tue, 9 Aug 2022 20:05:41 -0400 Subject: [PATCH] Prototype the configuration file parser. Right now there's a nasty memory bug I need to fix. Will have to run this through valgrind. --- .cvsignore | 2 + contrib/development.conf | 13 + contrib/{telodendria.conf => production.conf} | 20 +- src/Log.c | 14 +- src/Telodendria.c | 46 +-- src/TelodendriaConfig.c | 317 ++++++++++++++++++ src/Util.c | 19 ++ src/include/TelodendriaConfig.h | 100 ++++++ src/include/Util.h | 16 + 9 files changed, 515 insertions(+), 32 deletions(-) create mode 100644 contrib/development.conf rename contrib/{telodendria.conf => production.conf} (90%) create mode 100644 src/TelodendriaConfig.c create mode 100644 src/include/TelodendriaConfig.h diff --git a/.cvsignore b/.cvsignore index 762ed61..96d744f 100644 --- a/.cvsignore +++ b/.cvsignore @@ -1,2 +1,4 @@ build .env +*.log +vgcore.* diff --git a/contrib/development.conf b/contrib/development.conf new file mode 100644 index 0000000..ff57223 --- /dev/null +++ b/contrib/development.conf @@ -0,0 +1,13 @@ +server-name "example.com"; +chroot "/var/telodendria"; +id "_telodendria" "_telodendria"; +data-dir "./data"; +federation "true"; +registration "false"; +log "stdout" { + level "debug"; + timestampFormat "none"; + color "true"; +}; +threads "4"; + diff --git a/contrib/telodendria.conf b/contrib/production.conf similarity index 90% rename from contrib/telodendria.conf rename to contrib/production.conf index cc3bec4..973aec9 100644 --- a/contrib/telodendria.conf +++ b/contrib/production.conf @@ -13,18 +13,20 @@ # should go through and check. # -# The address to listen on. You can specify multiple addresses by -# simply adding more values to this directive. It is recommended -# to only listen on localhost, and then configure a reverse proxy -# such as relayd(8) in front of it, because the server does not -# implement TLS. +# The address to listen on. It is recommended to listen on localhost, +# and then configure a reverse proxy # such as relayd(8) in front of +# it, because the server does not implement TLS. # # Also note that Telodendria doesn't provide multiple ports for # different things. All APIs are made available over the same port. # This works because Matrix allows the port configuration to be # shared via .well-known/matrix/, which this server does properly # serve. -listen "localhost:8008"; +# +# The first parameter is the host name or IP address to listen on, +# and the second parameter is the port name or number. See the +# getaddrinfo() manual page for more information. +listen "localhost" "8008"; # Configure the domain name of your homeserver. Note that Matrix # servers cannot be migrated to other domains, so once this is set, @@ -55,7 +57,7 @@ id "_telodendria" "_telodendria"; # event information. Telodendria doesn't use a database; it uses a # flat-file directory structure, sort of like how most SMTP servers # use Maildirs or mbox files. -data-dir "/data"; +data-dir "./data"; # Whether to enable federation or not. Matrix is by default # a federated protocol, but if you just want your own internal chat @@ -90,8 +92,8 @@ registration "false"; # configures the log file. If you're going to be running Telodendria # in a chroot, the log file will have to live inside the chroot. # -# Acceptable values here are "stdout", "stderr", or a log file. -log "/telodendria.log" { +# Acceptable values here are "stdout" or a log file. +log "./telodendria.log" { # The level to log. This can be one of "error", "warning", # "task", "message", or "debug", with each level showing all # the levels above it as well. For example, "error" shows diff --git a/src/Log.c b/src/Log.c index e694c39..a963027 100644 --- a/src/Log.c +++ b/src/Log.c @@ -100,6 +100,12 @@ LogConfigFlagSet(LogConfig * config, int flags) void LogConfigFree(LogConfig * config) { + if (!config) + { + return; + } + + fclose(config->out); free(config); } @@ -218,10 +224,6 @@ Log(LogConfig * config, LogLevel level, const char *msg,...) pthread_mutex_lock(&config->lock); - for (i = 0; i < config->indent; i++) - { - fputc(' ', config->out); - } doColor = LogConfigFlagGet(config, LOG_FLAG_COLOR) && isatty(fileno(config->out)); @@ -312,6 +314,10 @@ Log(LogConfig * config, LogLevel level, const char *msg,...) } fputc(' ', config->out); + for (i = 0; i < config->indent; i++) + { + fputc(' ', config->out); + } va_start(argp, msg); vfprintf(config->out, msg, argp); diff --git a/src/Telodendria.c b/src/Telodendria.c index 2ef69b3..111ef26 100644 --- a/src/Telodendria.c +++ b/src/Telodendria.c @@ -26,6 +26,8 @@ #include #include +#include + #include #include #include @@ -83,10 +85,10 @@ main(int argc, char **argv) ConfigParseResult *configParseResult = NULL; HashMap *config = NULL; - lc = LogConfigCreate(); + /* Program configuration */ + TelodendriaConfig *tConfig = NULL; - /* TODO: Remove */ - LogConfigLevelSet(lc, LOG_DEBUG); + lc = LogConfigCreate(); if (!lc) { @@ -147,15 +149,9 @@ main(int argc, char **argv) } } - Log(lc, LOG_MESSAGE, "Using configuration file '%s'.", configArg); + Log(lc, LOG_TASK, "Processing configuration file '%s'.", configArg); - Log(lc, LOG_DEBUG, "Executing ConfigParse()"); - - /* Read config here */ configParseResult = ConfigParse(configFile); - - Log(lc, LOG_DEBUG, "Exitting ConfigParse()"); - if (!ConfigParseResultOk(configParseResult)) { Log(lc, LOG_ERROR, "Syntax error on line %d.", @@ -167,18 +163,30 @@ main(int argc, char **argv) config = ConfigParseResultGet(configParseResult); ConfigParseResultFree(configParseResult); - Log(lc, LOG_DEBUG, "Closing configuration file."); fclose(configFile); - /* Configure log file */ + tConfig = TelodendriaConfigParse(config, lc); + if (!tConfig) + { + exit = EXIT_FAILURE; + goto finish; + } + + ConfigFree(config); + + Log(lc, LOG_DEBUG, "Configuration:"); + LogConfigIndent(lc); + Log(lc, LOG_DEBUG, "Listen On: %s:%s", tConfig->listenHost, tConfig->listenPort); + Log(lc, LOG_DEBUG, "Server Name: %s", tConfig->serverName); + Log(lc, LOG_DEBUG, "Chroot: %s", tConfig->chroot); + Log(lc, LOG_DEBUG, "Run As: %s:%s", tConfig->uid, tConfig->gid); + Log(lc, LOG_DEBUG, "Data Directory: %s", tConfig->dataDir); + Log(lc, LOG_DEBUG, "Threads: %d", tConfig->threads); + Log(lc, LOG_DEBUG, "Flags: %x", tConfig->flags); + LogConfigUnindent(lc); finish: - if (config) - { - Log(lc, LOG_DEBUG, "Freeing configuration structure."); - ConfigFree(config); - } - Log(lc, LOG_DEBUG, "Freeing log configuration and exiting with code '%d'.", exit); - LogConfigFree(lc); + Log(lc, LOG_DEBUG, "Exiting with code '%d'.", exit); + TelodendriaConfigFree(tConfig); return exit; } diff --git a/src/TelodendriaConfig.c b/src/TelodendriaConfig.c new file mode 100644 index 0000000..e7945a0 --- /dev/null +++ b/src/TelodendriaConfig.c @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2022 Jordan Bancino <@jordan:bancino.net> + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +static int +IsInteger(char *str) +{ + while (*str) + { + if (!isdigit(*str)) + { + return 0; + } + str++; + } + return 1; +} + +#define GET_DIRECTIVE(name) \ + directive = (ConfigDirective *) HashMapGet(config, name); \ + if (!directive) { \ + Log(lc, LOG_ERROR, "Missing required configuration directive: '%s'.", name); \ + goto error; \ + } \ + children = ConfigChildrenGet(directive); \ + value = ConfigValuesGet(directive); \ + +#define ASSERT_NO_CHILDREN(name) if (children) { \ + Log(lc, LOG_ERROR, "Unexpected child values in directive: '%s'.", name); \ + goto error; \ +} + +#define ASSERT_VALUES(name, expected) if (ArraySize(value) != expected) { \ + Log(lc, LOG_ERROR, \ + "Wrong value count in directive '%s': got '%d', but expected '%d'.", \ + name, ArraySize(value), expected); \ + goto error; \ +} + +#define COPY_VALUE(into, index) into = UtilStringDuplicate(ArrayGet(value, index)) + +TelodendriaConfig * +TelodendriaConfigParse(HashMap * config, LogConfig * lc) +{ + TelodendriaConfig *tConfig; + + ConfigDirective *directive; + Array *value; + HashMap *children; + + if (!config || !lc) + { + return NULL; + } + + tConfig = calloc(1, sizeof(tConfig)); + if (!tConfig) + { + return NULL; + } + + directive = (ConfigDirective *) HashMapGet(config, "listen"); + children = ConfigChildrenGet(directive); + value = ConfigValuesGet(directive); + + if (!directive) + { + Log(lc, LOG_WARNING, "No 'listen' directive specified; using defaults, which may change."); + tConfig->listenHost = UtilStringDuplicate("localhost"); + tConfig->listenPort = UtilStringDuplicate("8008"); + } + else + { + ASSERT_NO_CHILDREN("listen"); + ASSERT_VALUES("listen", 2); + COPY_VALUE(tConfig->listenHost, 0); + COPY_VALUE(tConfig->listenPort, 1); + } + + GET_DIRECTIVE("server-name"); + ASSERT_NO_CHILDREN("server-name"); + ASSERT_VALUES("server-name", 1); + COPY_VALUE(tConfig->serverName, 0); + + GET_DIRECTIVE("chroot"); + ASSERT_NO_CHILDREN("chroot"); + ASSERT_VALUES("chroot", 1); + COPY_VALUE(tConfig->chroot, 0); + + GET_DIRECTIVE("id"); + ASSERT_NO_CHILDREN("id"); + ASSERT_VALUES("id", 2); + COPY_VALUE(tConfig->uid, 0); + COPY_VALUE(tConfig->gid, 1); + + GET_DIRECTIVE("data-dir"); + ASSERT_NO_CHILDREN("data-dir"); + ASSERT_VALUES("data-dir", 1); + COPY_VALUE(tConfig->dataDir, 0); + + GET_DIRECTIVE("threads"); + ASSERT_NO_CHILDREN("threads"); + ASSERT_VALUES("threads", 1); + + if (IsInteger(ArrayGet(value, 0))) + { + tConfig->threads = atoi(ArrayGet(value, 0)); + } + else + { + Log(lc, LOG_ERROR, + "Expected integer for directive 'threads', " + "but got '%s'.", ArrayGet(value, 0)); + goto error; + } + + GET_DIRECTIVE("federation"); + ASSERT_NO_CHILDREN("federation"); + ASSERT_VALUES("federation", 1); + + if (strcmp(ArrayGet(value, 0), "true") == 0) + { + tConfig->flags |= TELODENDRIA_FEDERATION; + } + else if (strcmp(ArrayGet(value, 0), "false") != 0) + { + Log(lc, LOG_ERROR, + "Expected boolean value for directive 'federation', " + "but got '%s'.", ArrayGet(value, 0)); + goto error; + } + + GET_DIRECTIVE("registration"); + ASSERT_NO_CHILDREN("registration"); + ASSERT_VALUES("registration", 1); + if (strcmp(ArrayGet(value, 0), "true") == 0) + { + tConfig->flags |= TELODENDRIA_REGISTRATION; + } + else if (strcmp(ArrayGet(value, 0), "false") != 0) + { + Log(lc, LOG_ERROR, + "Expected boolean value for directive 'registration', " + "but got '%s'.", ArrayGet(value, 0)); + goto error; + } + + GET_DIRECTIVE("log"); + ASSERT_VALUES("log", 1); + + if (children) + { + ConfigDirective *cDirective; + char *cVal; + size_t size; + + cDirective = HashMapGet(children, "level"); + if (cDirective) + { + size = ArraySize(ConfigValuesGet(cDirective)); + if (size > 1) + { + Log(lc, LOG_ERROR, "Expected 1 value for log.level, got %d.", size); + goto error; + } + + cVal = ArrayGet(ConfigValuesGet(cDirective), 0); + if (strcmp(cVal, "message") == 0) + { + LogConfigLevelSet(lc, LOG_MESSAGE); + } + else if (strcmp(cVal, "debug") == 0) + { + LogConfigLevelSet(lc, LOG_DEBUG); + } + else if (strcmp(cVal, "task") == 0) + { + LogConfigLevelSet(lc, LOG_TASK); + } + else if (strcmp(cVal, "warning") == 0) + { + LogConfigLevelSet(lc, LOG_WARNING); + } + else if (strcmp(cVal, "error") == 0) + { + LogConfigLevelSet(lc, LOG_ERROR); + } + else + { + Log(lc, LOG_ERROR, "Invalid value for log.level: '%s'.", cVal); + goto error; + } + } + + cDirective = HashMapGet(children, "timestampFormat"); + if (cDirective) + { + size = ArraySize(ConfigValuesGet(cDirective)); + if (size > 1) + { + Log(lc, LOG_ERROR, "Expected 1 value for log.level, got %d.", size); + goto error; + } + + cVal = ArrayGet(ConfigValuesGet(cDirective), 0); + + if (strcmp(cVal, "none") == 0) + { + LogConfigTimeStampFormatSet(lc, NULL); + } + else if (strcmp(cVal, "default") != 0) + { + LogConfigTimeStampFormatSet(lc, UtilStringDuplicate(cVal)); + } + } + + cDirective = HashMapGet(children, "color"); + if (cDirective) + { + size = ArraySize(ConfigValuesGet(cDirective)); + if (size > 1) + { + Log(lc, LOG_ERROR, "Expected 1 value for log.level, got %d.", size); + goto error; + } + + cVal = ArrayGet(ConfigValuesGet(cDirective), 0); + + if (strcmp(cVal, "false") == 0) + { + LogConfigFlagClear(lc, LOG_FLAG_COLOR); + } + else if (strcmp(cVal, "true") != 0) + { + Log(lc, LOG_ERROR, "Expected boolean value for log.color, got '%s'.", cVal); + goto error; + } + } + } + + /* Set the actual log output file last */ + if (strcmp(ArrayGet(value, 0), "stdout") != 0) + { + FILE *out = fopen(ArrayGet(value, 0), "w"); + + if (!out) + { + Log(lc, LOG_ERROR, "Unable to open log file '%s' for writing.", + ArrayGet(value, 0)); + goto error; + } + + Log(lc, LOG_DEBUG, "Redirecting output to '%s'.", ArrayGet(value, 0)); + LogConfigOutputSet(lc, out); + } + + tConfig->logConfig = lc; + return tConfig; +error: + TelodendriaConfigFree(tConfig); + return NULL; +} + +#undef GET_DIRECTIVE +#undef ASSERT_NO_CHILDREN +#undef ASSERT_VALUES + +void +TelodendriaConfigFree(TelodendriaConfig * tConfig) +{ + if (!tConfig) + { + return; + } + + free(tConfig->listenHost); + free(tConfig->listenPort); + free(tConfig->serverName); + free(tConfig->chroot); + free(tConfig->uid); + free(tConfig->gid); + free(tConfig->dataDir); + + LogConfigFree(tConfig->logConfig); + + free(tConfig); +} diff --git a/src/Util.c b/src/Util.c index 179bb9a..ca45e93 100644 --- a/src/Util.c +++ b/src/Util.c @@ -24,6 +24,7 @@ #include #include +#include #include long @@ -86,3 +87,21 @@ UtilUtf8Encode(unsigned long utf8) return str; } + +char * +UtilStringDuplicate(char *inStr) +{ + size_t len; + char *outStr; + + len = strlen(inStr); + outStr = malloc(len + 1); /* For the null terminator */ + if (!outStr) + { + return NULL; + } + + strcpy(outStr, inStr); + + return outStr; +} diff --git a/src/include/TelodendriaConfig.h b/src/include/TelodendriaConfig.h new file mode 100644 index 0000000..398c525 --- /dev/null +++ b/src/include/TelodendriaConfig.h @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2022 Jordan Bancino <@jordan:bancino.net> + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/* + * TelodendriaConfig.h: Validate and maintain the Telodendria server's + * configuration data. This API builds on the Config API to add + * Telodendria-specific parsing. It takes a fully parsed Config, and + * converts it into a TelodendriaConfig, which is more structured. + */ +#ifndef TELODENDRIA_TELODENDRIACONFIG_H +#define TELODENDRIA_TELODENDRIACONFIG_H + +#include +#include + +typedef enum TelodendriaConfigFlag +{ + TELODENDRIA_FEDERATION = (1 << 0), + TELODENDRIA_REGISTRATION = (1 << 1) +} TelodendriaConfigFlag; + +/* + * Since this configuration will live in memory for a long time, it is + * important that unused values are freed as soon as possible. Therefore, + * the TelodendriaConfig structure is not opaque; values are accessed + * directly, and they can be freed as the program wishes. + * + * NOTE: If you're going to free a value, make sure you set the pointer + * to NULL. TelodendriaConfigFree() will call free() on all values. + */ +typedef struct TelodendriaConfig +{ + char *listenHost; + char *listenPort; + char *serverName; + char *chroot; + char *uid; + char *gid; + char *dataDir; + + unsigned int flags; + unsigned int threads; + + LogConfig *logConfig; +} TelodendriaConfig; + +/* + * Parse a Config map, extracting the necessary values, validating them, + * and then adding them to a new TelodendriaConfig for future use by the + * program. All values are copied, so the Config hash map can be safely + * freed if this function succeeds. + * + * Params: + * (HashMap *) A hash map from ConfigParse(). This should be a map of + * ConfigDirectives. + * (LogConfig *) A working log configuration. Messages are written to + * this log as the parsing progresses, and this log is + * copied into the resulting TelodendriaConfig. It is + * also potentially modified by the configuration file's + * "log" block. + * + * Return: A TelodendriaConfig that is completely independent of the passed + * configuration hash map, or NULL if one or more required values is missing. + */ +extern TelodendriaConfig * + TelodendriaConfigParse(HashMap *, LogConfig *); + +/* + * Free all of the memory allocated to the given configuration. This + * function unconditionally calls free() on all items in the structure, + * so make sure items that were already freed are NULL. + * + * Params: + * (TelodendriaConfig *) The configuration to free all the values for. + */ +extern void + TelodendriaConfigFree(TelodendriaConfig *); + +#endif diff --git a/src/include/Util.h b/src/include/Util.h index d1ada59..d00eda5 100644 --- a/src/include/Util.h +++ b/src/include/Util.h @@ -68,4 +68,20 @@ extern long extern char * UtilUtf8Encode(unsigned long); + +/* + * Duplicate a null-terminated string, and return a new string on the + * heap. + * + * Params: + * (char *) The string to duplicate. It can be located anywhere on + * the heap or the stack. + * + * Return: A pointer to a null-terminated string on the heap. You must + * free() it when you're done with it. This may also return NULL if the + * call to malloc() fails. + */ +extern char * + UtilStringDuplicate(char *); + #endif /* TELODENDRIA_UTIL_H */