From fb5a8e4587f1aae9cfba2ec9a62526780b55cfd7 Mon Sep 17 00:00:00 2001 From: Jordan Bancino Date: Fri, 9 Dec 2022 23:57:30 +0000 Subject: [PATCH] Convert configuration file to JSON --- TODO.txt | 3 - contrib/development.conf | 34 +-- contrib/production.conf | 46 +-- man/man5/telodendria.conf.5 | 75 ++--- src/Config.c | 544 ------------------------------------ src/Telodendria.c | 26 +- src/TelodendriaConfig.c | 478 ++++++++++++------------------- src/include/Config.h | 208 -------------- 8 files changed, 260 insertions(+), 1154 deletions(-) delete mode 100644 src/Config.c delete mode 100644 src/include/Config.h diff --git a/TODO.txt b/TODO.txt index 3cddadf..ccae22e 100644 --- a/TODO.txt +++ b/TODO.txt @@ -28,9 +28,6 @@ Due: January 1, 2023 [x] Array [x] Base64 [x] CanonicalJson - [ ] Config - [ ] API (Config.3) - [ ] File format (Config.5) [x] HashMap [ ] Http [ ] HttpServer diff --git a/contrib/development.conf b/contrib/development.conf index 8f12b85..da6aa0b 100644 --- a/contrib/development.conf +++ b/contrib/development.conf @@ -1,20 +1,14 @@ -# -# Telodendria development configuration file. -# - -server-name "localhost"; -base-url "http://localhost:8008"; - -# Make this directory if Telodendria complains that it's missing. -data-dir "./data"; - -federation "true"; -registration "true"; -log "stdout" { -# level "debug"; - timestampFormat "none"; - color "true"; -}; -threads "4"; -max-connections "32"; -max-cache "1K"; +{ + "serverName": "localhost", + "baseUrl": "http://localhost:8008", + "dataDir": "./data", + "federation": true, + "registration": true, + "threads": 2, + "log": { + "output": "stdout", + "level": "debug", + "timestampFormat": "none", + "color": true + } +} diff --git a/contrib/production.conf b/contrib/production.conf index cff7bba..ce11832 100644 --- a/contrib/production.conf +++ b/contrib/production.conf @@ -1,33 +1,13 @@ -# -# Telodendria configuration file. -# -# The following man pages document the configuration: -# -# - telodendria.conf(5) -# - Config(5) -# -# Alternatively, find the man pages online at the -# following URL: -# -# https://telodendria.io/#documentation -# - -listen "8008"; - -server-name "example.com"; -base-url "https://matrix.example.com"; -identity-server "https://identity.example.com"; - -data-dir "/var/telodendria"; - -federation "true"; -registration "false"; - -log "file" { - level "message"; - timestampFormat "default"; -}; - -threads "4"; -max-connections "32"; -max-cache "512M"; +{ + "serverName": "example.com", + "baseUrl": "https://matrix.example.com", + "identityServer": "https://identity.example.com", + "dataDir": "/var/telodendria", + "federation": true, + "registration": false, + "threads": 4, + "maxCache": 512000000, + "log": { + "output": "file" + } +} diff --git a/man/man5/telodendria.conf.5 b/man/man5/telodendria.conf.5 index b08e5a1..f11e615 100644 --- a/man/man5/telodendria.conf.5 +++ b/man/man5/telodendria.conf.5 @@ -1,4 +1,4 @@ -.Dd $Mdocdate: November 8 2022 $ +.Dd $Mdocdate: December 9 2022 $ .Dt TELODENDRIA.CONF 5 .Os Telodendria Project .Sh NAME @@ -16,21 +16,13 @@ option, and is typically located at .Pa /etc/telodendria.conf .sp .Nm -uses OpenBSD-style syntax, though it is a little more rigid in its -parser. All values must be surrounded by quotes, and each directive -must be ended with a semicolon. -.Sh MACROS -Macros can be defined that will later be expanded in context. -Macro names must start with a letter, digit, or underscore, and may -contain only those characters. Macros are not expanded inside quotes. -.sp -For example: -.Bd -literal -offset indent -macro1 = "value1"; -directive $macro1; -.Ed -.Sh GLOBAL OPTIONS -Here are the settings that can be set globally: +uses JSON for its configuration file syntax, which should be +familiar. Very early versions of +.Nm +used a custom OpenBSD-style configuration file, but this was +not as versatile or familiar as JSON. +.Sh DIRECTIVES +Here are the top-level directives: .Bl -tag -width Ds .It Ic listen Ar port The port to listen on. Telodendria will bind to all interfaces, but it @@ -47,7 +39,7 @@ the internet. .Ar port should be a decimal port number. This directive is entirely optional. If it is omitted, then Telodendria will listen on port 8008 by default. -.It Ic server-name Ar name +.It Ic serverName Ar name Configure the domain name of your homeserver. Note that Matrix servers cannot be migrated to other domains, so once this is set, it should never change unless you want unexpected things to happen, or you want to start @@ -55,7 +47,7 @@ over. .Ar name should be a DNS name that can be publically resolved. This directive is required. -.It Ic base-url Ar url +.It Ic baseUrl Ar url Set the server's base URL. .Ar url should be a valid URL, complete with the protocol. It does not need to @@ -69,16 +61,16 @@ manifest. .Pp This directive is optional. If it is not specified, it is automatically deduced from the server name. -.It Ic identity-server Ar url +.It Ic identityServer Ar url The identity server that clients should use to perform identity lookups. .Pp .Ar url follows the same rules as -.Ic base-url . +.Ic baseUrl . .Pp This directive is optional. If it is not specified, it is automatically set to be the same as the base URL. -.It Ic id Ar uid Ar gid +.It Ic runAs Ar uidObj The effective UNIX user and group to drop to after binding to the socket and changing the filesystem root for the process. This only works if Telodendria is running as the root user, and is used as a security mechanism. @@ -86,7 +78,20 @@ If this option is set and Telodendria is started as a non-priviledged user, then a warning is printed to the log if that user does not match what's specified here. This directive is optional, but should be used as a sanity check, if nothing more, to make sure the permissions are working properly. -.It Ic data-dir Ar directory +.Pp +This directive takes an object with the following directives: +.Bl -tag -width Ds +.It Ic uid Ar user +The UNIX username to drop to. If +.Ic runAs +is specified, this directive is required. +.It Ic gid Ar group +The UNIX group to drop to. This directive is optional; if it is not +specified, then the value of +.Ic uid +is copied. +.El +.It Ic dataDir Ar directory The data directory into which Telodendria will write all user and event information. Telodendria doesn't use a database like other Matrix homeserver implementations; it uses a flat-file directory structure, similar to how an @@ -122,27 +127,27 @@ to run their own homeserver, you can aset this to which will allow anyone to create an account. Telodendria should be capable of handling a large amount of users without difficulty or security issues. This directive is required. -.It Ic log Ar stdout|file|syslog -The log configuration. Telodendria uses its own logging facility, which can output -logs to standard output, a file, or the syslog. If set to +.It Ic log Ar logObj +The log file configuration. Telodendria uses its own logging facility, which can +output logs to standard output, a file, or the syslog. This directive is required, +and it takes an object with the following directives: +.Bl -tag -width Ds +.It Ic output Ar stdout|file|syslog +The lot output destination. If set to .Ar file , Telodendria will log to .Pa telodendria.log inside the -.Ic data-dir . -.Pp -A number of child directives can -be added to this directive to customize the log output: -.Bl -tag -width Ds -.It Ic level Ar error|warning|task|message|debug +.Ic dataDir . +.It Ic level Ar error|warning|notice|message|debug The level of messages to log at. Each level shows all the levels above it. For example, setting the level to .Ar error will show only errors, while setting the level to .Ar warning will show warnings and errors. -.Ar task -shows tasks, warnings, and errors, and so on. The +.Ar notice +shows notices, warnings, and errors, and so on. The .Ar debug level shows all messages. .It Ic timestampFormat Ar format|none|default @@ -171,11 +176,11 @@ less than the total CPU core count, to prevent overloading the system. The most efficient number of threads ultimately depends on the configuration of the machine running Telodendria, so you may just have to play around with different values here to see which gives the best performance. -.It Ic max-connections Ar count +.It Ic maxConnections Ar count The maximum number of simultanious connections to allow to the daemon. This option prevents the daemon from allocating large amounts of memory in the even that it undergoes a denial of service attack. It typically does not need to be adjusted. -.It Ic max-cache Ar bytes +.It Ic maxCache Ar bytes The maximum size of the cache. Telodendria relies heavily on caching to speed things up. The cache grows as data is loaded from the data directory. All cache is stored in memory. This option limits the size of the memory cache. If you have diff --git a/src/Config.c b/src/Config.c deleted file mode 100644 index 8dabc94..0000000 --- a/src/Config.c +++ /dev/null @@ -1,544 +0,0 @@ -/* - * 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 - -#ifndef CONFIG_BUFFER_BLOCK -#define CONFIG_BUFFER_BLOCK 32 -#endif - -struct ConfigDirective -{ - Array *values; - HashMap *children; -}; - -struct ConfigParseResult -{ - unsigned int ok:1; - union - { - size_t lineNumber; - HashMap *confMap; - } data; -}; - -typedef enum ConfigToken -{ - TOKEN_UNKNOWN, - TOKEN_NAME, - TOKEN_MACRO_ASSIGNMENT, - TOKEN_VALUE, - TOKEN_SEMICOLON, - TOKEN_BLOCK_OPEN, - TOKEN_BLOCK_CLOSE, - TOKEN_MACRO, - TOKEN_EOF -} ConfigToken; - -typedef struct ConfigParserState -{ - FILE *stream; - unsigned int line; - - char *token; - size_t tokenSize; - size_t tokenLen; - ConfigToken tokenType; - - HashMap *macroMap; - -} ConfigParserState; - -unsigned int -ConfigParseResultOk(ConfigParseResult * result) -{ - return result ? result->ok : 0; -} - -size_t -ConfigParseResultLineNumber(ConfigParseResult * result) -{ - return result && !result->ok ? result->data.lineNumber : 0; -} - -HashMap * -ConfigParseResultGet(ConfigParseResult * result) -{ - return result && result->ok ? result->data.confMap : NULL; -} - -void -ConfigParseResultFree(ConfigParseResult * result) -{ - /* - * Note that if the parse was valid, the hash map - * needs to be freed separately. - */ - Free(result); -} - -Array * -ConfigValuesGet(ConfigDirective * directive) -{ - return directive ? directive->values : NULL; -} - -HashMap * -ConfigChildrenGet(ConfigDirective * directive) -{ - return directive ? directive->children : NULL; -} - -static void -ConfigDirectiveFree(ConfigDirective * directive) -{ - size_t i; - - if (!directive) - { - return; - } - - for (i = 0; i < ArraySize(directive->values); i++) - { - Free(ArrayGet(directive->values, i)); - } - - ArrayFree(directive->values); - - ConfigFree(directive->children); - - Free(directive); -} - -void -ConfigFree(HashMap * conf) -{ - char *key; - void *value; - - while (HashMapIterate(conf, &key, &value)) - { - ConfigDirectiveFree((ConfigDirective *) value); - Free(key); - } - - HashMapFree(conf); -} - -static ConfigParserState * -ConfigParserStateCreate(FILE * stream) -{ - ConfigParserState *state = Malloc(sizeof(ConfigParserState)); - - if (!state) - { - return NULL; - } - - state->macroMap = HashMapCreate(); - - if (!state->macroMap) - { - Free(state); - return NULL; - } - - state->stream = stream; - state->line = 1; - state->token = NULL; - state->tokenSize = 0; - state->tokenLen = 0; - state->tokenType = TOKEN_UNKNOWN; - - return state; -} - -static void -ConfigParserStateFree(ConfigParserState * state) -{ - char *key; - void *value; - - if (!state) - { - return; - } - - - Free(state->token); - - while (HashMapIterate(state->macroMap, &key, &value)) - { - Free(key); - Free(value); - } - - HashMapFree(state->macroMap); - - Free(state); -} - -static int -ConfigIsNameChar(int c) -{ - return isdigit(c) || isalpha(c) || (c == '-' || c == '_'); -} - -static char -ConfigConsumeWhitespace(ConfigParserState * state) -{ - int c; - - while (isspace(c = fgetc(state->stream))) - { - if (c == '\n') - { - state->line++; - } - } - return c; -} - -static void -ConfigConsumeLine(ConfigParserState * state) -{ - while (fgetc(state->stream) != '\n'); - state->line++; -} - -static void -ConfigTokenSeek(ConfigParserState * state) -{ - int c; - - /* If we already hit EOF, don't do anything */ - if (state->tokenType == TOKEN_EOF) - { - return; - } - while ((c = ConfigConsumeWhitespace(state)) == '#') - { - ConfigConsumeLine(state); - } - - /* - * After all whitespace and comments are consumed, identify the - * token by looking at the next character - */ - - if (feof(state->stream)) - { - state->tokenType = TOKEN_EOF; - return; - } - if (ConfigIsNameChar(c)) - { - state->tokenLen = 0; - - /* Read the key/macro into state->token */ - if (!state->token) - { - state->tokenSize = CONFIG_BUFFER_BLOCK; - state->token = Malloc(CONFIG_BUFFER_BLOCK); - } - state->token[state->tokenLen] = c; - state->tokenLen++; - - while (ConfigIsNameChar((c = fgetc(state->stream)))) - { - state->token[state->tokenLen] = c; - state->tokenLen++; - - if (state->tokenLen >= state->tokenSize) - { - state->tokenSize += CONFIG_BUFFER_BLOCK; - state->token = Realloc(state->token, - state->tokenSize); - } - } - - state->token[state->tokenLen] = '\0'; - state->tokenLen++; - - if (!isspace(c)) - { - state->tokenType = TOKEN_UNKNOWN; - } - else - { - state->tokenType = TOKEN_NAME; - - if (c == '\n') - { - state->line++; - } - } - - } - else - { - switch (c) - { - case '=': - state->tokenType = TOKEN_MACRO_ASSIGNMENT; - break; - case '"': - state->tokenLen = 0; - state->tokenType = TOKEN_VALUE; - - /* read the value into state->curtok */ - while ((c = fgetc(state->stream)) != '"') - { - if (c == '\n') - { - state->line++; - } - /* - * End of the stream reached without finding - * a closing quote - */ - if (feof(state->stream)) - { - state->tokenType = TOKEN_EOF; - break; - } - state->token[state->tokenLen] = c; - state->tokenLen++; - - if (state->tokenLen >= state->tokenSize) - { - state->tokenSize += CONFIG_BUFFER_BLOCK; - state->token = Realloc(state->token, - state->tokenSize); - } - } - state->token[state->tokenLen] = '\0'; - state->tokenLen++; - break; - case ';': - state->tokenType = TOKEN_SEMICOLON; - break; - case '{': - state->tokenType = TOKEN_BLOCK_OPEN; - break; - case '}': - state->tokenType = TOKEN_BLOCK_CLOSE; - break; - case '$': - state->tokenLen = 0; - /* read the macro name into state->curtok */ - while (ConfigIsNameChar(c = fgetc(state->stream))) - { - state->token[state->tokenLen] = c; - state->tokenLen++; - - if (state->tokenLen >= state->tokenSize) - { - state->tokenSize += CONFIG_BUFFER_BLOCK; - state->token = Realloc(state->token, - state->tokenSize); - } - } - state->token[state->tokenLen] = '\0'; - state->tokenLen++; - state->tokenType = TOKEN_MACRO; - - ungetc(c, state->stream); - break; - default: - state->tokenType = TOKEN_UNKNOWN; - break; - } - } - - /* Resize curtok to only use the bytes it needs */ - if (state->tokenLen) - { - state->tokenSize = state->tokenLen; - state->token = Realloc(state->token, state->tokenSize); - } -} - -static int -ConfigExpect(ConfigParserState * state, ConfigToken tokenType) -{ - return state->tokenType == tokenType; -} - - -static HashMap * -ConfigParseBlock(ConfigParserState * state, int level) -{ - HashMap *block = HashMapCreate(); - - ConfigTokenSeek(state); - - while (ConfigExpect(state, TOKEN_NAME)) - { - char *name = Malloc(state->tokenLen + 1); - - strcpy(name, state->token); - - ConfigTokenSeek(state); - if (ConfigExpect(state, TOKEN_VALUE) || ConfigExpect(state, TOKEN_MACRO)) - { - ConfigDirective *directive; - - directive = Malloc(sizeof(ConfigDirective)); - directive->children = NULL; - directive->values = ArrayCreate(); - - while (ConfigExpect(state, TOKEN_VALUE) || - ConfigExpect(state, TOKEN_MACRO)) - { - - char *dval; - char *dvalCpy; - - if (ConfigExpect(state, TOKEN_VALUE)) - { - dval = state->token; - } - else if (ConfigExpect(state, TOKEN_MACRO)) - { - dval = HashMapGet(state->macroMap, state->token); - if (!dval) - { - goto error; - } - } - else - { - dval = NULL; /* Should never happen */ - } - - /* dval is a pointer which is overwritten with the next - * token. */ - dvalCpy = Malloc(strlen(dval) + 1); - strcpy(dvalCpy, dval); - - ArrayAdd(directive->values, dvalCpy); - ConfigTokenSeek(state); - } - - if (ConfigExpect(state, TOKEN_BLOCK_OPEN)) - { - /* token_seek(state); */ - directive->children = ConfigParseBlock(state, level + 1); - if (!directive->children) - { - goto error; - } - } - - /* - * Append this directive to the current block, - * overwriting a directive at this level with the same name. - * - * Note that if a value already exists with this name, it is - * returned by HashMapSet() and then immediately passed to - * ConfigDirectiveFree(). If the value does not exist, then - * NULL is sent to ConfigDirectiveFree(), making it a no-op. - */ - ConfigDirectiveFree(HashMapSet(block, name, directive)); - } - else if (ConfigExpect(state, TOKEN_MACRO_ASSIGNMENT)) - { - ConfigTokenSeek(state); - if (ConfigExpect(state, TOKEN_VALUE)) - { - char *valueCopy = Malloc(strlen(state->token) + 1); - - strcpy(valueCopy, state->token); - Free(HashMapSet(state->macroMap, name, valueCopy)); - ConfigTokenSeek(state); - } - else - { - goto error; - } - } - else - { - goto error; - } - - if (!ConfigExpect(state, TOKEN_SEMICOLON)) - { - goto error; - } - ConfigTokenSeek(state); - } - - if (ConfigExpect(state, level ? TOKEN_BLOCK_CLOSE : TOKEN_EOF)) - { - ConfigTokenSeek(state); - return block; - } - else - { - goto error; - } - -error: - /* Only free the very top level, because this will recurse */ - if (!level) - { - ConfigFree(block); - } - return NULL; -} - -ConfigParseResult * -ConfigParse(FILE * stream) -{ - ConfigParseResult *result; - HashMap *conf; - ConfigParserState *state; - - result = Malloc(sizeof(ConfigParseResult)); - state = ConfigParserStateCreate(stream); - conf = ConfigParseBlock(state, 0); - - if (!conf) - { - result->ok = 0; - result->data.lineNumber = state->line; - } - else - { - result->ok = 1; - result->data.confMap = conf; - } - - ConfigParserStateFree(state); - return result; -} diff --git a/src/Telodendria.c b/src/Telodendria.c index 8859573..135e3ae 100644 --- a/src/Telodendria.c +++ b/src/Telodendria.c @@ -35,7 +35,7 @@ #include #include #include -#include +#include #include #include #include @@ -136,7 +136,6 @@ main(int argc, char **argv) /* Config file */ FILE *configFile = NULL; - ConfigParseResult *configParseResult = NULL; HashMap *config = NULL; /* Program configuration */ @@ -237,29 +236,25 @@ main(int argc, char **argv) Log(lc, LOG_NOTICE, "Processing configuration file '%s'.", configArg); - configParseResult = ConfigParse(configFile); - if (!ConfigParseResultOk(configParseResult)) + config = JsonDecode(configFile); + fclose(configFile); + + if (!config) { - Log(lc, LOG_ERR, "Syntax error on line %d.", - ConfigParseResultLineNumber(configParseResult)); + Log(lc, LOG_ERR, "Syntax error in configuration file."); exit = EXIT_FAILURE; goto finish; } - config = ConfigParseResultGet(configParseResult); - ConfigParseResultFree(configParseResult); - - fclose(configFile); - tConfig = TelodendriaConfigParse(config, lc); + JsonFree(config); + if (!tConfig) { exit = EXIT_FAILURE; goto finish; } - ConfigFree(config); - if (flags & ARG_CONFIGTEST) { Log(lc, LOG_INFO, "Configuration is OK."); @@ -277,7 +272,10 @@ main(int argc, char **argv) unveil(NULL, NULL); /* Done with unveil(), so disable it */ #endif - LogConfigTimeStampFormatSet(lc, tConfig->logTimestamp); + if (!tConfig->logTimestamp || strcmp(tConfig->logTimestamp, "default") != 0) + { + LogConfigTimeStampFormatSet(lc, tConfig->logTimestamp); + } if (tConfig->flags & TELODENDRIA_LOG_COLOR) { diff --git a/src/TelodendriaConfig.c b/src/TelodendriaConfig.c index 96746af..b0cbd62 100644 --- a/src/TelodendriaConfig.c +++ b/src/TelodendriaConfig.c @@ -23,61 +23,179 @@ */ #include #include -#include +#include #include #include #include #include +#include #include #include #include -static int -IsInteger(char *str) -{ - while (*str) - { - if (!isdigit((unsigned char) *str)) - { - return 0; - } - str++; +#define CONFIG_REQUIRE(key, type) \ + value = HashMapGet(config, key); \ + if (!value) \ + { \ + Log(lc, LOG_ERR, "Missing required " key " directive."); \ + goto error; \ + } \ + if (JsonValueType(value) == JSON_NULL) \ + { \ + Log(lc, LOG_ERR, "Missing value for " key " directive."); \ + goto error; \ + } \ + if (JsonValueType(value) != type) \ + { \ + Log(lc, LOG_ERR, "Expected " key " to be of type " #type); \ + goto error; \ } + +#define CONFIG_COPY_STRING(into) \ + into = UtilStringDuplicate(JsonValueAsString(value)); + +#define CONFIG_OPTIONAL_STRING(into, key, default) \ + value = HashMapGet(config, key); \ + if (value && JsonValueType(value) != JSON_NULL) \ + { \ + if (JsonValueType(value) != JSON_STRING) \ + { \ + Log(lc, LOG_ERR, "Expected " key " to be of type JSON_STRING"); \ + goto error; \ + } \ + into = UtilStringDuplicate(JsonValueAsString(value)); \ + } \ + else \ + { \ + Log(lc, LOG_INFO, "Using default value " #default " for " key "."); \ + into = default ? UtilStringDuplicate(default) : NULL; \ + } + +#define CONFIG_OPTIONAL_INTEGER(into, key, default) \ + value = HashMapGet(config, key); \ + if (value && JsonValueType(value) != JSON_NULL) \ + { \ + if (JsonValueType(value) != JSON_INTEGER) \ + { \ + Log(lc, LOG_ERR, "Expected " key " to be of type JSON_INTEGER"); \ + goto error; \ + } \ + into = JsonValueAsInteger(value); \ + } \ + else \ + { \ + Log(lc, LOG_INFO, "Using default value " #default " for " key "."); \ + into = default; \ + } + +int +ConfigParseRunAs(LogConfig * lc, TelodendriaConfig * tConfig, HashMap * config) +{ + JsonValue *value; + + CONFIG_REQUIRE("uid", JSON_STRING); + CONFIG_COPY_STRING(tConfig->uid); + + CONFIG_OPTIONAL_STRING(tConfig->gid, "gid", tConfig->uid); + return 1; + +error: + return 0; } -#define GET_DIRECTIVE(name) \ - directive = (ConfigDirective *) HashMapGet(config, name); \ - if (!directive) { \ - Log(lc, LOG_ERR, "Missing required configuration directive: '%s'.", name); \ - goto error; \ - } \ - children = ConfigChildrenGet(directive); \ - value = ConfigValuesGet(directive); \ +int +ConfigParseLog(LogConfig * lc, TelodendriaConfig * tConfig, HashMap * config) +{ + JsonValue *value; + char *str; -#define ASSERT_NO_CHILDREN(name) if (children) { \ - Log(lc, LOG_ERR, "Unexpected child values in directive: '%s'.", name); \ - goto error; \ + CONFIG_REQUIRE("output", JSON_STRING); + str = JsonValueAsString(value); + + if (strcmp(str, "stdout") == 0) + { + tConfig->flags |= TELODENDRIA_LOG_STDOUT; + } + else if (strcmp(str, "file") == 0) + { + tConfig->flags |= TELODENDRIA_LOG_FILE; + } + else if (strcmp(str, "syslog") == 0) + { + tConfig->flags |= TELODENDRIA_LOG_SYSLOG; + } + else + { + Log(lc, LOG_ERR, "Invalid value for log.output: '%s'.", str); + goto error; + } + + CONFIG_OPTIONAL_STRING(str, "level", "message"); + + if (strcmp(str, "message") == 0) + { + tConfig->logLevel = LOG_INFO; + } + else if (strcmp(str, "debug") == 0) + { + tConfig->logLevel = LOG_DEBUG; + } + else if (strcmp(str, "notice") == 0) + { + tConfig->logLevel = LOG_NOTICE; + } + else if (strcmp(str, "warning") == 0) + { + tConfig->logLevel = LOG_WARNING; + } + else if (strcmp(str, "error") == 0) + { + tConfig->logLevel = LOG_ERR; + } + else + { + Log(lc, LOG_ERR, "Invalid value for log.level: '%s'.", tConfig->logLevel); + goto error; + } + + Free(str); + + CONFIG_OPTIONAL_STRING(tConfig->logTimestamp, "timestampFormat", "default"); + + if (strcmp(tConfig->logTimestamp, "none") == 0) + { + Free(tConfig->logTimestamp); + tConfig->logTimestamp = NULL; + } + + value = HashMapGet(config, "color"); + if (value && JsonValueType(value) != JSON_NULL) + { + if (JsonValueType(value) != JSON_BOOLEAN) + { + Log(lc, LOG_ERR, "Expected type JSON_BOOLEAN for log.color."); + goto error; + } + + if (JsonValueAsBoolean(value)) + { + tConfig->flags |= TELODENDRIA_LOG_COLOR; + } + } + + return 1; + +error: + return 0; } -#define ASSERT_VALUES(name, expected) if (ArraySize(value) != expected) { \ - Log(lc, LOG_ERR, \ - "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; + JsonValue *value; if (!config || !lc) { @@ -92,41 +210,15 @@ TelodendriaConfigParse(HashMap * config, LogConfig * lc) memset(tConfig, 0, sizeof(TelodendriaConfig)); - directive = (ConfigDirective *) HashMapGet(config, "listen"); - children = ConfigChildrenGet(directive); - value = ConfigValuesGet(directive); + CONFIG_OPTIONAL_INTEGER(tConfig->listenPort, "listen", 8008); - if (!directive) + CONFIG_REQUIRE("serverName", JSON_STRING); + CONFIG_COPY_STRING(tConfig->serverName); + + value = HashMapGet(config, "baseUrl"); + if (value) { - tConfig->listenPort = 8008; - } - else - { - ASSERT_NO_CHILDREN("listen"); - ASSERT_VALUES("listen", 1); - - tConfig->listenPort = (unsigned short) atoi(ArrayGet(value, 0)); - if (!tConfig->listenPort) - { - Log(lc, LOG_ERR, "Expected numeric value for listen port, got '%s'.", ArrayGet(value, 1)); - goto error; - } - } - - GET_DIRECTIVE("server-name"); - ASSERT_NO_CHILDREN("server-name"); - ASSERT_VALUES("server-name", 1); - COPY_VALUE(tConfig->serverName, 0); - - directive = (ConfigDirective *) HashMapGet(config, "base-url"); - children = ConfigChildrenGet(directive); - value = ConfigValuesGet(directive); - - if (directive) - { - ASSERT_NO_CHILDREN("base-url"); - ASSERT_VALUES("base-url", 1); - COPY_VALUE(tConfig->baseUrl, 0); + CONFIG_COPY_STRING(tConfig->baseUrl); } else { @@ -134,273 +226,65 @@ TelodendriaConfigParse(HashMap * config, LogConfig * lc) tConfig->baseUrl = Malloc(strlen(tConfig->serverName) + 10); if (!tConfig->baseUrl) { - Log(lc, LOG_ERR, "Error allocating memory for default config value 'base-url'."); + Log(lc, LOG_ERR, "Error allocating memory for default config value 'baseUrl'."); goto error; } sprintf(tConfig->baseUrl, "https://%s", tConfig->serverName); } - directive = (ConfigDirective *) HashMapGet(config, "identity-server"); - children = ConfigChildrenGet(directive); - value = ConfigValuesGet(directive); + CONFIG_OPTIONAL_STRING(tConfig->identityServer, "identityServer", NULL); - if (directive) + value = HashMapGet(config, "runAs"); + if (value && JsonValueType(value) != JSON_NULL) { - ASSERT_NO_CHILDREN("identity-server"); - ASSERT_VALUES("identity-server", 1); - COPY_VALUE(tConfig->identityServer, 0); - } - else - { - Log(lc, LOG_WARNING, "Identity server not specified. No identity server will be advertised."); - tConfig->identityServer = NULL; - } - - directive = (ConfigDirective *) HashMapGet(config, "id"); - children = ConfigChildrenGet(directive); - value = ConfigValuesGet(directive); - - ASSERT_NO_CHILDREN("id"); - - if (directive) - { - - switch (ArraySize(value)) + if (JsonValueType(value) == JSON_OBJECT) { - case 1: - Log(lc, LOG_WARNING, "No run group specified; assuming it's the same as the user."); - COPY_VALUE(tConfig->uid, 0); - tConfig->gid = UtilStringDuplicate(tConfig->uid); - break; - case 2: - COPY_VALUE(tConfig->uid, 0); - COPY_VALUE(tConfig->gid, 1); - break; - default: - Log(lc, LOG_ERR, - "Wrong value count in directive 'id': got '%d', but expected 1 or 2.", - ArraySize(value)); - goto error; - } - } - else - { - tConfig->uid = NULL; - tConfig->gid = NULL; - } - - 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)); - if (!tConfig->threads) - { - Log(lc, LOG_ERR, "threads must be greater than zero"); - goto error; - } - } - else - { - Log(lc, LOG_ERR, - "Expected integer for directive 'threads', " - "but got '%s'.", ArrayGet(value, 0)); - goto error; - } - - directive = (ConfigDirective *) HashMapGet(config, "max-connections"); - if (!directive) - { - Log(lc, LOG_WARNING, "max-connections not specified; using defaults, which may change"); - tConfig->maxConnections = 32; - } - else - { - ASSERT_NO_CHILDREN("max-connections"); - ASSERT_VALUES("max-connections", 1); - if (IsInteger(ArrayGet(value, 0))) - { - tConfig->maxConnections = atoi(ArrayGet(value, 0)); - if (!tConfig->maxConnections) + if (!ConfigParseRunAs(lc, tConfig, JsonValueAsObject(value))) { - Log(lc, LOG_ERR, "max-connections must be greater than zero."); goto error; } } else { - Log(lc, LOG_ERR, "Expected integer for max-connections, got '%s'", ArrayGet(value, 0)); + Log(lc, LOG_ERR, "Config directive 'runAs' should be a JSON object"); + Log(lc, LOG_ERR, "that contains a 'uid' and 'gid'."); goto error; } } - GET_DIRECTIVE("max-cache"); - ASSERT_NO_CHILDREN("max-cache"); - ASSERT_VALUES("max-cache", 1); - tConfig->maxCache = UtilParseBytes(ArrayGet(value, 0)); + CONFIG_REQUIRE("dataDir", JSON_STRING); + CONFIG_COPY_STRING(tConfig->dataDir); - GET_DIRECTIVE("federation"); - ASSERT_NO_CHILDREN("federation"); - ASSERT_VALUES("federation", 1); + CONFIG_OPTIONAL_INTEGER(tConfig->threads, "threads", 1); + CONFIG_OPTIONAL_INTEGER(tConfig->maxConnections, "maxConnections", 32); + CONFIG_OPTIONAL_INTEGER(tConfig->maxCache, "maxCache", DB_MIN_CACHE); - if (strcmp(ArrayGet(value, 0), "true") == 0) + CONFIG_REQUIRE("federation", JSON_BOOLEAN); + if (JsonValueAsBoolean(value)) { tConfig->flags |= TELODENDRIA_FEDERATION; } - else if (strcmp(ArrayGet(value, 0), "false") != 0) - { - Log(lc, LOG_ERR, - "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) + CONFIG_REQUIRE("registration", JSON_BOOLEAN); + if (JsonValueAsBoolean(value)) { tConfig->flags |= TELODENDRIA_REGISTRATION; } - else if (strcmp(ArrayGet(value, 0), "false") != 0) + + CONFIG_REQUIRE("log", JSON_OBJECT); + if (!ConfigParseLog(lc, tConfig, JsonValueAsObject(value))) { - Log(lc, LOG_ERR, - "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_ERR, "Expected 1 value for log.level, got %d.", size); - goto error; - } - - cVal = ArrayGet(ConfigValuesGet(cDirective), 0); - if (strcmp(cVal, "message") == 0) - { - tConfig->logLevel = LOG_INFO; - } - else if (strcmp(cVal, "debug") == 0) - { - tConfig->logLevel = LOG_DEBUG; - } - else if (strcmp(cVal, "task") == 0) - { - tConfig->logLevel = LOG_NOTICE; - } - else if (strcmp(cVal, "warning") == 0) - { - tConfig->logLevel = LOG_WARNING; - } - else if (strcmp(cVal, "error") == 0) - { - tConfig->logLevel = LOG_ERR; - } - else - { - Log(lc, LOG_ERR, "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_ERR, "Expected 1 value for log.level, got %d.", size); - goto error; - } - - cVal = ArrayGet(ConfigValuesGet(cDirective), 0); - - if (strcmp(cVal, "none") == 0) - { - tConfig->logTimestamp = NULL; - } - else if (strcmp(cVal, "default") != 0) - { - tConfig->logTimestamp = UtilStringDuplicate(cVal); - } - } - - cDirective = HashMapGet(children, "color"); - if (cDirective) - { - size = ArraySize(ConfigValuesGet(cDirective)); - if (size > 1) - { - Log(lc, LOG_ERR, "Expected 1 value for log.level, got %d.", size); - goto error; - } - - cVal = ArrayGet(ConfigValuesGet(cDirective), 0); - - if (strcmp(cVal, "true") == 0) - { - tConfig->flags |= TELODENDRIA_LOG_COLOR; - } - else if (strcmp(cVal, "false") != 0) - { - Log(lc, LOG_ERR, "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) - { - tConfig->flags |= TELODENDRIA_LOG_STDOUT; - } - else if (strcmp(ArrayGet(value, 0), "file") == 0) - { - tConfig->flags |= TELODENDRIA_LOG_FILE; - } - else if (strcmp(ArrayGet(value, 0), "syslog") == 0) - { - tConfig->flags |= TELODENDRIA_LOG_SYSLOG; - } - else - { - Log(lc, LOG_ERR, "Unknown log value '%s', expected 'stdout', 'file', or 'syslog'.", - ArrayGet(value, 0)); goto error; } return tConfig; + error: TelodendriaConfigFree(tConfig); return NULL; } -#undef GET_DIRECTIVE -#undef ASSERT_NO_CHILDREN -#undef ASSERT_VALUES - void TelodendriaConfigFree(TelodendriaConfig * tConfig) { diff --git a/src/include/Config.h b/src/include/Config.h deleted file mode 100644 index 314f439..0000000 --- a/src/include/Config.h +++ /dev/null @@ -1,208 +0,0 @@ -/* - * 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. - */ - -/* - * Config.h: A heavily-modified version of Conifer2, a configuration - * file format specification and C parsing library written by Jordan - * Bancino. This library differs from Conifer2 in that the function - * naming convention has been updated to be consistent with Telodendria, - * and the underlying data structures have been overhauled to use the - * data structure libraries provided by Telodendria. - * - * Conifer2 was originally a learning project. It was very thoroughly - * debugged, however, and the configuration syntax was elegant, - * certainly more elegant than using JSON for a configuration file, - * so it was chosen to be the format for Telodendria's configuration - * file. The original Conifer2 project is now dead; Conifer2 lives on - * only as Telodendria's Config parsing library. - */ -#ifndef TELODENDRIA_CONFIG_H -#define TELODENDRIA_CONFIG_H - -#include - -#include -#include - -/* - * A configuration directive is a single key that may have at least one - * value, and any number of children. - */ -typedef struct ConfigDirective ConfigDirective; - -/* - * The parser returns a parse result object. This stores whether or - * not the parse was successful, and then also additional information - * about the parse, such as the line number on which parsing failed, - * or the collection of directives if the parsing succeeded. - * - * There are a number of ConfigParseResult methods that can be used - * to query the result of parsing. - */ -typedef struct ConfigParseResult ConfigParseResult; - -/* - * Parse a configuration file, and generate the structures needed to - * make it easy to read. - * - * Params: - * - * (FILE *) The input stream to read from. - * - * Return: A ConfigParseResult, which can be used to check whether or - * not the parsing was successful. If the parsing was sucessful, then - * this object contains the root directive, which can be used to - * retrieve configuration values out of. If the parsing failed, then - * this object contains the line number at which the parsing was - * aborted. - */ -extern ConfigParseResult * - ConfigParse(FILE *); - -/* - * Get whether or not a parse result indicates that parsing was - * successful or not. This function should be used to determine what - * to do next. If the parsing failed, your program should terminate - * with an error, otherwise, you can proceed to parse the configuration - * file. - * - * Params: - * - * (ConfigParseResult *) The output of ConfigParse() to check. - * - * Return: 0 if the configuration file is malformed, or otherwise - * could not be parsed. Any non-zero return value indicates that the - * configuration file was successfully parsed. - */ -extern unsigned int - ConfigParseResultOk(ConfigParseResult *); - -/* - * If, and only if, the configuration file parsing failed, then this - * function can be used to get the line number it failed at. Typically, - * this will be reported to the user and then the program will be - * terminated. - * - * Params: - * - * (ConfigParseResult *) The output of ConfigParse() to get the - * line number from. - * - * Return: The line number on which the configuration file parser - * choked, or 0 if the parsing was actually successful. - */ -extern size_t - ConfigParseResultLineNumber(ConfigParseResult *); - -/* - * Convert a ConfigParseResult into a HashMap containing the entire - * configuration file, if, and only if, the parsing was successful. - * - * Params: - * - * (ConfigParseResult *) The output of ConfigParse() to get the - * actual configuration data from. - * - * Return: A HashMap containing all the configuration data, or NULL - * if the parsing was not successful. This HashMap is a map of string - * keys to ConfigDirective objects. Use the standard HashMap methods - * to get ConfigDirectives, and then use the ConfigDirective functions - * to get information out of them. - */ -extern HashMap * - ConfigParseResultGet(ConfigParseResult *); - -/* - * Free the memory being used by the given ConfigParseResult. Note that - * it is safe to free the ConfigParseResult immediately after you have - * retrieved either the line number or the configuration data from it. - * Freeing the parse result does not free the configuration data. - * - * Params: - * - * (ConfigParseResult *) The output of ConfigParse() to free. This - * object will be invalidated, but pointers to - * the actual configuration data will still be - * valid. - */ -extern void - ConfigParseResultFree(ConfigParseResult *); - -/* - * Get an array of values associated with the given configuration - * directive. Directives can have any number of values, which are - * made accessible via the Array API. - * - * Params: - * - * (ConfigDirective *) The configuration directive to get the values - * for. - * - * Return: An array that contains at least 1 value. Configuration files - * cannot have value-less directives. If the passed directive is NULL, - * or there is an error allocating memory for an array, then NULL is - * returned. - */ -extern Array * - ConfigValuesGet(ConfigDirective *); - -/* - * Get a map of children associated with the given configuration - * directive. Configuration files can recurse with no practical limit, - * so directives can have any number of child directives. - * - * Params: - * - * (ConfigDirective *) The configuratio ndirective to get the - * children of. - * - * Return: A HashMap containing child directives, or NULL if the passed - * directive is NULL or has no children. - */ -extern HashMap * - ConfigChildrenGet(ConfigDirective *); - -/* - * Free all the memory associated with the given configuration hash - * map. Note: this will free *everything*. All Arrays, HashMaps, - * ConfigDirectives, and even strings will be invalidated. As such, - * this should be done after you either copy the values you want, or - * are done using them. It is highly recommended to use this function - * near the end of your program's execution during cleanup, otherwise - * copy any values you need into your own buffers. - * - * Note that this should only be run on the root configuration object, - * not any children. Running on children will produce undefined - * behavior. This function is recursive; it will get all the children - * under it. - * - * Params: - * - * (HashMap *) The configuration data to free. - * - */ -extern void - ConfigFree(HashMap *); - -#endif /* TELODENDRIA_CONFIG_H */