telodendria/src/Config.c
Jordan Bancino 2441f07848 Add support for spinning up multiple HTTP servers.
This is useful for having a TLS and a non-TLS version port, like Synapse.
I verified that the multiple-servers does in fact work as intended,
although the TLS server part is broken; I must be doing something
incorrectly with LibreSSL in setting up the server.
2023-03-23 02:12:45 +00:00

428 lines
11 KiB
C

/*
* Copyright (C) 2022-2023 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 <Json.h>
#include <HashMap.h>
#include <Log.h>
#include <Array.h>
#include <Str.h>
#include <Db.h>
#include <HttpServer.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>
#define CONFIG_REQUIRE(key, type) \
value = HashMapGet(config, key); \
if (!value) \
{ \
Log(LOG_ERR, "Missing required " key " directive."); \
goto error; \
} \
if (JsonValueType(value) == JSON_NULL) \
{ \
Log(LOG_ERR, "Missing value for " key " directive."); \
goto error; \
} \
if (JsonValueType(value) != type) \
{ \
Log(LOG_ERR, "Expected " key " to be of type " #type); \
goto error; \
}
#define CONFIG_COPY_STRING(into) \
into = StrDuplicate(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(LOG_ERR, "Expected " key " to be of type JSON_STRING"); \
goto error; \
} \
into = StrDuplicate(JsonValueAsString(value)); \
} \
else \
{ \
Log(LOG_INFO, "Using default value " #default " for " key "."); \
into = default ? StrDuplicate(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(LOG_ERR, "Expected " key " to be of type JSON_INTEGER"); \
goto error; \
} \
into = JsonValueAsInteger(value); \
} \
else \
{ \
Log(LOG_INFO, "Using default value " #default " for " key "."); \
into = default; \
}
static int
ConfigParseRunAs(Config * 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;
}
static int
ConfigParseListen(Config * tConfig, Array * listen)
{
size_t i;
if (!ArraySize(listen))
{
Log(LOG_ERR, "Listen array cannot be empty; you must specify at least");
Log(LOG_ERR, "one listener.");
goto error;
}
if (!tConfig->servers)
{
tConfig->servers = ArrayCreate();
if (!tConfig->servers)
{
Log(LOG_ERR, "Unable to allocate memory for listener configurations.");
goto error;
}
}
for (i = 0; i < ArraySize(listen); i++)
{
JsonValue *val = ArrayGet(listen, i);
HashMap *obj;
HttpServerConfig *serverCfg = Malloc(sizeof(HttpServerConfig));
if (!serverCfg)
{
Log(LOG_ERR, "Unable to allocate memory for listener configuration.");
goto error;
}
if (JsonValueType(val) != JSON_OBJECT)
{
Log(LOG_ERR, "Invalid value in listener array.");
Log(LOG_ERR, "All listeners must be objects.");
goto error;
}
obj = JsonValueAsObject(val);
serverCfg->port = JsonValueAsInteger(HashMapGet(obj, "port"));
serverCfg->threads = JsonValueAsInteger(HashMapGet(obj, "threads"));
serverCfg->maxConnections = JsonValueAsInteger(HashMapGet(obj, "maxConnections"));
if (!serverCfg->port)
{
Log(LOG_WARNING, "No or invalid port specified, listener will be ignored.");
Free(serverCfg);
continue;
}
if (!serverCfg->threads)
{
Log(LOG_DEBUG, "No or invalid number of threads specified for listener.");
Log(LOG_DEBUG, "Using default, which may be subject to change.");
serverCfg->threads = 4;
}
if (!serverCfg->maxConnections)
{
Log(LOG_DEBUG, "No or invalid number of maximum connections specified.");
Log(LOG_DEBUG, "Using default, which may be subject to change.");
serverCfg->maxConnections = 32;
}
val = HashMapGet(obj, "tls");
if ((JsonValueType(val) == JSON_BOOLEAN && !JsonValueAsBoolean(val)) || JsonValueType(val) == JSON_NULL)
{
serverCfg->flags = HTTP_FLAG_NONE;
serverCfg->tlsCert = NULL;
serverCfg->tlsKey = NULL;
}
else if (JsonValueType(val) != JSON_OBJECT)
{
Log(LOG_ERR, "Invalid value for listener.tls. It must be an object.");
goto error;
}
else
{
serverCfg->flags = HTTP_FLAG_TLS;
obj = JsonValueAsObject(val);
serverCfg->tlsCert = StrDuplicate(JsonValueAsString(HashMapGet(obj, "cert")));
serverCfg->tlsKey = StrDuplicate(JsonValueAsString(HashMapGet(obj, "key")));
if (!serverCfg->tlsCert || !serverCfg->tlsKey)
{
Log(LOG_ERR, "TLS cert and key must both be valid file names.");
goto error;
}
}
ArrayAdd(tConfig->servers, serverCfg);
}
return 1;
error:
return 0;
}
static int
ConfigParseLog(Config * tConfig, HashMap * config)
{
JsonValue *value;
char *str;
CONFIG_REQUIRE("output", JSON_STRING);
str = JsonValueAsString(value);
if (strcmp(str, "stdout") == 0)
{
tConfig->flags |= CONFIG_LOG_STDOUT;
}
else if (strcmp(str, "file") == 0)
{
tConfig->flags |= CONFIG_LOG_FILE;
}
else if (strcmp(str, "syslog") == 0)
{
tConfig->flags |= CONFIG_LOG_SYSLOG;
}
else
{
Log(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(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(LOG_ERR, "Expected type JSON_BOOLEAN for log.color.");
goto error;
}
if (JsonValueAsBoolean(value))
{
tConfig->flags |= CONFIG_LOG_COLOR;
}
}
return 1;
error:
return 0;
}
Config *
ConfigParse(HashMap * config)
{
Config *tConfig;
JsonValue *value;
if (!config)
{
return NULL;
}
tConfig = Malloc(sizeof(Config));
if (!tConfig)
{
return NULL;
}
memset(tConfig, 0, sizeof(Config));
CONFIG_REQUIRE("listen", JSON_ARRAY);
if (!ConfigParseListen(tConfig, JsonValueAsArray(value)))
{
goto error;
}
CONFIG_REQUIRE("serverName", JSON_STRING);
CONFIG_COPY_STRING(tConfig->serverName);
value = HashMapGet(config, "baseUrl");
if (value)
{
CONFIG_COPY_STRING(tConfig->baseUrl);
}
else
{
Log(LOG_WARNING, "Base URL not specified. Assuming it's 'https://%s'.", tConfig->serverName);
tConfig->baseUrl = Malloc(strlen(tConfig->serverName) + 10);
if (!tConfig->baseUrl)
{
Log(LOG_ERR, "Error allocating memory for default config value 'baseUrl'.");
goto error;
}
sprintf(tConfig->baseUrl, "https://%s", tConfig->serverName);
}
CONFIG_OPTIONAL_STRING(tConfig->identityServer, "identityServer", NULL);
value = HashMapGet(config, "runAs");
if (value && JsonValueType(value) != JSON_NULL)
{
if (JsonValueType(value) == JSON_OBJECT)
{
if (!ConfigParseRunAs(tConfig, JsonValueAsObject(value)))
{
goto error;
}
}
else
{
Log(LOG_ERR, "Config directive 'runAs' should be a JSON object");
Log(LOG_ERR, "that contains a 'uid' and 'gid'.");
goto error;
}
}
CONFIG_REQUIRE("dataDir", JSON_STRING);
CONFIG_COPY_STRING(tConfig->dataDir);
CONFIG_OPTIONAL_INTEGER(tConfig->maxCache, "maxCache", 0);
CONFIG_REQUIRE("federation", JSON_BOOLEAN);
if (JsonValueAsBoolean(value))
{
tConfig->flags |= CONFIG_FEDERATION;
}
CONFIG_REQUIRE("registration", JSON_BOOLEAN);
if (JsonValueAsBoolean(value))
{
tConfig->flags |= CONFIG_REGISTRATION;
}
CONFIG_REQUIRE("log", JSON_OBJECT);
if (!ConfigParseLog(tConfig, JsonValueAsObject(value)))
{
goto error;
}
return tConfig;
error:
ConfigFree(tConfig);
return NULL;
}
void
ConfigFree(Config * tConfig)
{
if (!tConfig)
{
return;
}
Free(tConfig->serverName);
Free(tConfig->baseUrl);
Free(tConfig->identityServer);
Free(tConfig->uid);
Free(tConfig->gid);
Free(tConfig->dataDir);
Free(tConfig->logTimestamp);
if (tConfig->servers)
{
size_t i;
for (i = 0; i < ArraySize(tConfig->servers); i++)
{
HttpServerConfig *serverCfg = ArrayGet(tConfig->servers, i);
Free(serverCfg->tlsCert);
Free(serverCfg->tlsKey);
Free(serverCfg);
}
ArrayFree(tConfig->servers);
}
Free(tConfig);
}