Convert configuration file to JSON

This commit is contained in:
Jordan Bancino 2022-12-09 23:57:30 +00:00
parent c0534b0e05
commit fb5a8e4587
8 changed files with 260 additions and 1154 deletions

View file

@ -28,9 +28,6 @@ Due: January 1, 2023
[x] Array [x] Array
[x] Base64 [x] Base64
[x] CanonicalJson [x] CanonicalJson
[ ] Config
[ ] API (Config.3)
[ ] File format (Config.5)
[x] HashMap [x] HashMap
[ ] Http [ ] Http
[ ] HttpServer [ ] HttpServer

View file

@ -1,20 +1,14 @@
# {
# Telodendria development configuration file. "serverName": "localhost",
# "baseUrl": "http://localhost:8008",
"dataDir": "./data",
server-name "localhost"; "federation": true,
base-url "http://localhost:8008"; "registration": true,
"threads": 2,
# Make this directory if Telodendria complains that it's missing. "log": {
data-dir "./data"; "output": "stdout",
"level": "debug",
federation "true"; "timestampFormat": "none",
registration "true"; "color": true
log "stdout" { }
# level "debug"; }
timestampFormat "none";
color "true";
};
threads "4";
max-connections "32";
max-cache "1K";

View file

@ -1,33 +1,13 @@
# {
# Telodendria configuration file. "serverName": "example.com",
# "baseUrl": "https://matrix.example.com",
# The following man pages document the configuration: "identityServer": "https://identity.example.com",
# "dataDir": "/var/telodendria",
# - telodendria.conf(5) "federation": true,
# - Config(5) "registration": false,
# "threads": 4,
# Alternatively, find the man pages online at the "maxCache": 512000000,
# following URL: "log": {
# "output": "file"
# 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";

View file

@ -1,4 +1,4 @@
.Dd $Mdocdate: November 8 2022 $ .Dd $Mdocdate: December 9 2022 $
.Dt TELODENDRIA.CONF 5 .Dt TELODENDRIA.CONF 5
.Os Telodendria Project .Os Telodendria Project
.Sh NAME .Sh NAME
@ -16,21 +16,13 @@ option, and is typically located at
.Pa /etc/telodendria.conf .Pa /etc/telodendria.conf
.sp .sp
.Nm .Nm
uses OpenBSD-style syntax, though it is a little more rigid in its uses JSON for its configuration file syntax, which should be
parser. All values must be surrounded by quotes, and each directive familiar. Very early versions of
must be ended with a semicolon. .Nm
.Sh MACROS used a custom OpenBSD-style configuration file, but this was
Macros can be defined that will later be expanded in context. not as versatile or familiar as JSON.
Macro names must start with a letter, digit, or underscore, and may .Sh DIRECTIVES
contain only those characters. Macros are not expanded inside quotes. Here are the top-level directives:
.sp
For example:
.Bd -literal -offset indent
macro1 = "value1";
directive $macro1;
.Ed
.Sh GLOBAL OPTIONS
Here are the settings that can be set globally:
.Bl -tag -width Ds .Bl -tag -width Ds
.It Ic listen Ar port .It Ic listen Ar port
The port to listen on. Telodendria will bind to all interfaces, but it The port to listen on. Telodendria will bind to all interfaces, but it
@ -47,7 +39,7 @@ the internet.
.Ar port .Ar port
should be a decimal port number. This directive is entirely optional. If 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 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 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 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 change unless you want unexpected things to happen, or you want to start
@ -55,7 +47,7 @@ over.
.Ar name .Ar name
should be a DNS name that can be publically resolved. This directive should be a DNS name that can be publically resolved. This directive
is required. is required.
.It Ic base-url Ar url .It Ic baseUrl Ar url
Set the server's base URL. Set the server's base URL.
.Ar url .Ar url
should be a valid URL, complete with the protocol. It does not need to should be a valid URL, complete with the protocol. It does not need to
@ -69,16 +61,16 @@ manifest.
.Pp .Pp
This directive is optional. If it is not specified, it is automatically This directive is optional. If it is not specified, it is automatically
deduced from the server name. 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. The identity server that clients should use to perform identity lookups.
.Pp .Pp
.Ar url .Ar url
follows the same rules as follows the same rules as
.Ic base-url . .Ic baseUrl .
.Pp .Pp
This directive is optional. If it is not specified, it is automatically This directive is optional. If it is not specified, it is automatically
set to be the same as the base URL. 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 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 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. 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 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 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. 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 The data directory into which Telodendria will write all user and event
information. Telodendria doesn't use a database like other Matrix homeserver information. Telodendria doesn't use a database like other Matrix homeserver
implementations; it uses a flat-file directory structure, similar to how an 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 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 a large amount of users without difficulty or security issues. This directive is
required. required.
.It Ic log Ar stdout|file|syslog .It Ic log Ar logObj
The log configuration. Telodendria uses its own logging facility, which can output The log file configuration. Telodendria uses its own logging facility, which can
logs to standard output, a file, or the syslog. If set to 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 , .Ar file ,
Telodendria will log to Telodendria will log to
.Pa telodendria.log .Pa telodendria.log
inside the inside the
.Ic data-dir . .Ic dataDir .
.Pp .It Ic level Ar error|warning|notice|message|debug
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
The level of messages to log at. Each level shows all the levels above it. For The level of messages to log at. Each level shows all the levels above it. For
example, setting the level to example, setting the level to
.Ar error .Ar error
will show only errors, while setting the level to will show only errors, while setting the level to
.Ar warning .Ar warning
will show warnings and errors. will show warnings and errors.
.Ar task .Ar notice
shows tasks, warnings, and errors, and so on. The shows notices, warnings, and errors, and so on. The
.Ar debug .Ar debug
level shows all messages. level shows all messages.
.It Ic timestampFormat Ar format|none|default .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 efficient number of threads ultimately depends on the configuration of the
machine running Telodendria, so you may just have to play around with different machine running Telodendria, so you may just have to play around with different
values here to see which gives the best performance. 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 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 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. 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 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 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 is stored in memory. This option limits the size of the memory cache. If you have

View file

@ -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 <Config.h>
#include <Memory.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>
#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;
}

View file

@ -35,7 +35,7 @@
#include <TelodendriaConfig.h> #include <TelodendriaConfig.h>
#include <Log.h> #include <Log.h>
#include <HashMap.h> #include <HashMap.h>
#include <Config.h> #include <Json.h>
#include <HttpServer.h> #include <HttpServer.h>
#include <Matrix.h> #include <Matrix.h>
#include <Db.h> #include <Db.h>
@ -136,7 +136,6 @@ main(int argc, char **argv)
/* Config file */ /* Config file */
FILE *configFile = NULL; FILE *configFile = NULL;
ConfigParseResult *configParseResult = NULL;
HashMap *config = NULL; HashMap *config = NULL;
/* Program configuration */ /* Program configuration */
@ -237,29 +236,25 @@ main(int argc, char **argv)
Log(lc, LOG_NOTICE, "Processing configuration file '%s'.", configArg); Log(lc, LOG_NOTICE, "Processing configuration file '%s'.", configArg);
configParseResult = ConfigParse(configFile); config = JsonDecode(configFile);
if (!ConfigParseResultOk(configParseResult)) fclose(configFile);
if (!config)
{ {
Log(lc, LOG_ERR, "Syntax error on line %d.", Log(lc, LOG_ERR, "Syntax error in configuration file.");
ConfigParseResultLineNumber(configParseResult));
exit = EXIT_FAILURE; exit = EXIT_FAILURE;
goto finish; goto finish;
} }
config = ConfigParseResultGet(configParseResult);
ConfigParseResultFree(configParseResult);
fclose(configFile);
tConfig = TelodendriaConfigParse(config, lc); tConfig = TelodendriaConfigParse(config, lc);
JsonFree(config);
if (!tConfig) if (!tConfig)
{ {
exit = EXIT_FAILURE; exit = EXIT_FAILURE;
goto finish; goto finish;
} }
ConfigFree(config);
if (flags & ARG_CONFIGTEST) if (flags & ARG_CONFIGTEST)
{ {
Log(lc, LOG_INFO, "Configuration is OK."); 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 */ unveil(NULL, NULL); /* Done with unveil(), so disable it */
#endif #endif
LogConfigTimeStampFormatSet(lc, tConfig->logTimestamp); if (!tConfig->logTimestamp || strcmp(tConfig->logTimestamp, "default") != 0)
{
LogConfigTimeStampFormatSet(lc, tConfig->logTimestamp);
}
if (tConfig->flags & TELODENDRIA_LOG_COLOR) if (tConfig->flags & TELODENDRIA_LOG_COLOR)
{ {

View file

@ -23,61 +23,179 @@
*/ */
#include <TelodendriaConfig.h> #include <TelodendriaConfig.h>
#include <Memory.h> #include <Memory.h>
#include <Config.h> #include <Json.h>
#include <HashMap.h> #include <HashMap.h>
#include <Log.h> #include <Log.h>
#include <Array.h> #include <Array.h>
#include <Util.h> #include <Util.h>
#include <Db.h>
#include <stdlib.h> #include <stdlib.h>
#include <ctype.h> #include <ctype.h>
#include <string.h> #include <string.h>
static int #define CONFIG_REQUIRE(key, type) \
IsInteger(char *str) value = HashMapGet(config, key); \
{ if (!value) \
while (*str) { \
{ Log(lc, LOG_ERR, "Missing required " key " directive."); \
if (!isdigit((unsigned char) *str)) goto error; \
{ } \
return 0; if (JsonValueType(value) == JSON_NULL) \
} { \
str++; 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; return 1;
error:
return 0;
} }
#define GET_DIRECTIVE(name) \ int
directive = (ConfigDirective *) HashMapGet(config, name); \ ConfigParseLog(LogConfig * lc, TelodendriaConfig * tConfig, HashMap * config)
if (!directive) { \ {
Log(lc, LOG_ERR, "Missing required configuration directive: '%s'.", name); \ JsonValue *value;
goto error; \ char *str;
} \
children = ConfigChildrenGet(directive); \
value = ConfigValuesGet(directive); \
#define ASSERT_NO_CHILDREN(name) if (children) { \ CONFIG_REQUIRE("output", JSON_STRING);
Log(lc, LOG_ERR, "Unexpected child values in directive: '%s'.", name); \ str = JsonValueAsString(value);
goto error; \
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 * TelodendriaConfig *
TelodendriaConfigParse(HashMap * config, LogConfig * lc) TelodendriaConfigParse(HashMap * config, LogConfig * lc)
{ {
TelodendriaConfig *tConfig; TelodendriaConfig *tConfig;
JsonValue *value;
ConfigDirective *directive;
Array *value;
HashMap *children;
if (!config || !lc) if (!config || !lc)
{ {
@ -92,41 +210,15 @@ TelodendriaConfigParse(HashMap * config, LogConfig * lc)
memset(tConfig, 0, sizeof(TelodendriaConfig)); memset(tConfig, 0, sizeof(TelodendriaConfig));
directive = (ConfigDirective *) HashMapGet(config, "listen"); CONFIG_OPTIONAL_INTEGER(tConfig->listenPort, "listen", 8008);
children = ConfigChildrenGet(directive);
value = ConfigValuesGet(directive);
if (!directive) CONFIG_REQUIRE("serverName", JSON_STRING);
CONFIG_COPY_STRING(tConfig->serverName);
value = HashMapGet(config, "baseUrl");
if (value)
{ {
tConfig->listenPort = 8008; CONFIG_COPY_STRING(tConfig->baseUrl);
}
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);
} }
else else
{ {
@ -134,273 +226,65 @@ TelodendriaConfigParse(HashMap * config, LogConfig * lc)
tConfig->baseUrl = Malloc(strlen(tConfig->serverName) + 10); tConfig->baseUrl = Malloc(strlen(tConfig->serverName) + 10);
if (!tConfig->baseUrl) 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; goto error;
} }
sprintf(tConfig->baseUrl, "https://%s", tConfig->serverName); sprintf(tConfig->baseUrl, "https://%s", tConfig->serverName);
} }
directive = (ConfigDirective *) HashMapGet(config, "identity-server"); CONFIG_OPTIONAL_STRING(tConfig->identityServer, "identityServer", NULL);
children = ConfigChildrenGet(directive);
value = ConfigValuesGet(directive);
if (directive) value = HashMapGet(config, "runAs");
if (value && JsonValueType(value) != JSON_NULL)
{ {
ASSERT_NO_CHILDREN("identity-server"); if (JsonValueType(value) == JSON_OBJECT)
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))
{ {
case 1: if (!ConfigParseRunAs(lc, tConfig, JsonValueAsObject(value)))
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)
{ {
Log(lc, LOG_ERR, "max-connections must be greater than zero.");
goto error; goto error;
} }
} }
else 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; goto error;
} }
} }
GET_DIRECTIVE("max-cache"); CONFIG_REQUIRE("dataDir", JSON_STRING);
ASSERT_NO_CHILDREN("max-cache"); CONFIG_COPY_STRING(tConfig->dataDir);
ASSERT_VALUES("max-cache", 1);
tConfig->maxCache = UtilParseBytes(ArrayGet(value, 0));
GET_DIRECTIVE("federation"); CONFIG_OPTIONAL_INTEGER(tConfig->threads, "threads", 1);
ASSERT_NO_CHILDREN("federation"); CONFIG_OPTIONAL_INTEGER(tConfig->maxConnections, "maxConnections", 32);
ASSERT_VALUES("federation", 1); 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; 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"); CONFIG_REQUIRE("registration", JSON_BOOLEAN);
ASSERT_NO_CHILDREN("registration"); if (JsonValueAsBoolean(value))
ASSERT_VALUES("registration", 1);
if (strcmp(ArrayGet(value, 0), "true") == 0)
{ {
tConfig->flags |= TELODENDRIA_REGISTRATION; 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; goto error;
} }
return tConfig; return tConfig;
error: error:
TelodendriaConfigFree(tConfig); TelodendriaConfigFree(tConfig);
return NULL; return NULL;
} }
#undef GET_DIRECTIVE
#undef ASSERT_NO_CHILDREN
#undef ASSERT_VALUES
void void
TelodendriaConfigFree(TelodendriaConfig * tConfig) TelodendriaConfigFree(TelodendriaConfig * tConfig)
{ {

View file

@ -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 <stdio.h>
#include <HashMap.h>
#include <Array.h>
/*
* 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 */