forked from Telodendria/Telodendria
Move configuration to database, add process control API, fix memory leaks.
This commit is contained in:
parent
ff4d265dcc
commit
0cca38115a
30 changed files with 830 additions and 213 deletions
13
TODO.txt
13
TODO.txt
|
@ -52,10 +52,11 @@ Milestone: v0.3.0
|
|||
|
||||
[~] Move configuration to database
|
||||
[x] Token permissions
|
||||
[ ] Initial configuration
|
||||
[ ] If no config, create one-time use registration token that
|
||||
[x] Initial configuration
|
||||
[x] If no config, create one-time use registration token that
|
||||
grants user admin privileges.
|
||||
[ ] /_telodendria/admin/config endpoint
|
||||
[~] /_telodendria/admin/config endpoint
|
||||
[x] JsonDuplicate()
|
||||
[x] Refactor TelodendriaConfig to just Config
|
||||
|
||||
[ ] Documentation
|
||||
|
@ -72,9 +73,11 @@ Milestone: v0.3.0
|
|||
[ ] send-patch
|
||||
[ ] Log
|
||||
[ ] TelodendriaConfig -> Config
|
||||
[ ] telodendria.conf
|
||||
[ ] HashMap
|
||||
[ ] HttpRouter
|
||||
[ ] Str
|
||||
[ ] Admin API
|
||||
|
||||
[~] Client-Server API
|
||||
[x] 4: Token-based user registration
|
||||
|
@ -91,7 +94,9 @@ Milestone: v0.3.0
|
|||
[~] Deactivate
|
||||
[x] Make sure UserLogin() fails if user is deactivated.
|
||||
[x] Change password
|
||||
[x] Whoami
|
||||
[~] Whoami
|
||||
[ ] Attach device id to user object
|
||||
[ ] Use UserAuthenticate()
|
||||
[~] 9: User Data
|
||||
[ ] 5: Capabilities negotiation
|
||||
[ ] 10: Security (Rate Limiting)
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
"timestampFormat": "none",
|
||||
"level": "debug"
|
||||
},
|
||||
"dataDir": "./data",
|
||||
"listen": [
|
||||
{
|
||||
"port": 8008,
|
||||
|
|
191
src/Config.c
191
src/Config.c
|
@ -25,31 +25,32 @@
|
|||
#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 <Log.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <ctype.h>
|
||||
#include <string.h>
|
||||
#include <limits.h>
|
||||
|
||||
#define CONFIG_REQUIRE(key, type) \
|
||||
value = HashMapGet(config, key); \
|
||||
if (!value) \
|
||||
{ \
|
||||
Log(LOG_ERR, "Missing required " key " directive."); \
|
||||
tConfig->err = "Missing required " key " directive."; \
|
||||
goto error; \
|
||||
} \
|
||||
if (JsonValueType(value) == JSON_NULL) \
|
||||
{ \
|
||||
Log(LOG_ERR, "Missing value for " key " directive."); \
|
||||
tConfig->err = "Missing value for " key " directive."; \
|
||||
goto error; \
|
||||
} \
|
||||
if (JsonValueType(value) != type) \
|
||||
{ \
|
||||
Log(LOG_ERR, "Expected " key " to be of type " #type); \
|
||||
tConfig->err = "Expected " key " to be of type " #type; \
|
||||
goto error; \
|
||||
}
|
||||
|
||||
|
@ -62,14 +63,13 @@
|
|||
{ \
|
||||
if (JsonValueType(value) != JSON_STRING) \
|
||||
{ \
|
||||
Log(LOG_ERR, "Expected " key " to be of type JSON_STRING"); \
|
||||
tConfig->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; \
|
||||
}
|
||||
|
||||
|
@ -79,14 +79,13 @@
|
|||
{ \
|
||||
if (JsonValueType(value) != JSON_INTEGER) \
|
||||
{ \
|
||||
Log(LOG_ERR, "Expected " key " to be of type JSON_INTEGER"); \
|
||||
tConfig->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; \
|
||||
}
|
||||
|
||||
|
@ -113,8 +112,7 @@ ConfigParseListen(Config * tConfig, Array * listen)
|
|||
|
||||
if (!ArraySize(listen))
|
||||
{
|
||||
Log(LOG_ERR, "Listen array cannot be empty; you must specify at least");
|
||||
Log(LOG_ERR, "one listener.");
|
||||
tConfig->err = "Listen array cannot be empty; you must specify at least one listener.";
|
||||
goto error;
|
||||
}
|
||||
|
||||
|
@ -123,7 +121,7 @@ ConfigParseListen(Config * tConfig, Array * listen)
|
|||
tConfig->servers = ArrayCreate();
|
||||
if (!tConfig->servers)
|
||||
{
|
||||
Log(LOG_ERR, "Unable to allocate memory for listener configurations.");
|
||||
tConfig->err = "Unable to allocate memory for listener configurations.";
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
@ -136,14 +134,13 @@ ConfigParseListen(Config * tConfig, Array * listen)
|
|||
|
||||
if (!serverCfg)
|
||||
{
|
||||
Log(LOG_ERR, "Unable to allocate memory for listener configuration.");
|
||||
tConfig->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.");
|
||||
tConfig->err = "Invalid value in listener array. All listeners must be objects.";
|
||||
goto error;
|
||||
}
|
||||
|
||||
|
@ -155,22 +152,17 @@ ConfigParseListen(Config * tConfig, Array * listen)
|
|||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -183,7 +175,7 @@ ConfigParseListen(Config * tConfig, Array * listen)
|
|||
}
|
||||
else if (JsonValueType(val) != JSON_OBJECT)
|
||||
{
|
||||
Log(LOG_ERR, "Invalid value for listener.tls. It must be an object.");
|
||||
tConfig->err = "Invalid value for listener.tls. It must be an object.";
|
||||
goto error;
|
||||
}
|
||||
else
|
||||
|
@ -196,7 +188,7 @@ ConfigParseListen(Config * tConfig, Array * listen)
|
|||
|
||||
if (!serverCfg->tlsCert || !serverCfg->tlsKey)
|
||||
{
|
||||
Log(LOG_ERR, "TLS cert and key must both be valid file names.");
|
||||
tConfig->err = "TLS cert and key must both be valid file names.";
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
@ -231,7 +223,7 @@ ConfigParseLog(Config * tConfig, HashMap * config)
|
|||
}
|
||||
else
|
||||
{
|
||||
Log(LOG_ERR, "Invalid value for log.output: '%s'.", str);
|
||||
tConfig->err = "Invalid value for log.output";
|
||||
goto error;
|
||||
}
|
||||
|
||||
|
@ -259,7 +251,7 @@ ConfigParseLog(Config * tConfig, HashMap * config)
|
|||
}
|
||||
else
|
||||
{
|
||||
Log(LOG_ERR, "Invalid value for log.level: '%s'.", tConfig->logLevel);
|
||||
tConfig->err = "Invalid value for log.level.";
|
||||
goto error;
|
||||
}
|
||||
|
||||
|
@ -278,7 +270,7 @@ ConfigParseLog(Config * tConfig, HashMap * config)
|
|||
{
|
||||
if (JsonValueType(value) != JSON_BOOLEAN)
|
||||
{
|
||||
Log(LOG_ERR, "Expected type JSON_BOOLEAN for log.color.");
|
||||
tConfig->err = "Expected type JSON_BOOLEAN for log.color.";
|
||||
goto error;
|
||||
}
|
||||
|
||||
|
@ -294,6 +286,42 @@ error:
|
|||
return 0;
|
||||
}
|
||||
|
||||
void
|
||||
ConfigFree(Config * tConfig)
|
||||
{
|
||||
if (!tConfig)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Free(tConfig->serverName);
|
||||
Free(tConfig->baseUrl);
|
||||
Free(tConfig->identityServer);
|
||||
|
||||
Free(tConfig->uid);
|
||||
Free(tConfig->gid);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Config *
|
||||
ConfigParse(HashMap *config)
|
||||
{
|
||||
|
@ -329,11 +357,10 @@ ConfigParse(HashMap * config)
|
|||
}
|
||||
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'.");
|
||||
tConfig->err = "Error allocating memory for default config value 'baseUrl'.";
|
||||
goto error;
|
||||
}
|
||||
|
||||
|
@ -354,15 +381,11 @@ ConfigParse(HashMap * config)
|
|||
}
|
||||
else
|
||||
{
|
||||
Log(LOG_ERR, "Config directive 'runAs' should be a JSON object");
|
||||
Log(LOG_ERR, "that contains a 'uid' and 'gid'.");
|
||||
tConfig->err = "Config directive 'runAs' should be a JSON object 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);
|
||||
|
@ -383,46 +406,100 @@ ConfigParse(HashMap * config)
|
|||
goto error;
|
||||
}
|
||||
|
||||
tConfig->ok = 1;
|
||||
tConfig->err = NULL;
|
||||
return tConfig;
|
||||
|
||||
error:
|
||||
ConfigFree(tConfig);
|
||||
tConfig->ok = 0;
|
||||
return tConfig;
|
||||
}
|
||||
|
||||
int
|
||||
ConfigExists(Db *db)
|
||||
{
|
||||
return DbExists(db, 1, "config");
|
||||
}
|
||||
|
||||
int
|
||||
ConfigCreateDefault(Db *db)
|
||||
{
|
||||
DbRef *ref;
|
||||
HashMap *json;
|
||||
Array *listeners;
|
||||
HashMap *listen;
|
||||
|
||||
char hostname[HOST_NAME_MAX + 1];
|
||||
|
||||
if (!db)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
ref = DbCreate(db, 1, "config");
|
||||
if (!ref)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
json = DbJson(ref);
|
||||
|
||||
JsonSet(json, JsonValueString("file"), 2, "log", "output");
|
||||
|
||||
listeners = ArrayCreate();
|
||||
listen = HashMapCreate();
|
||||
HashMapSet(listen, "port", JsonValueInteger(8008));
|
||||
HashMapSet(listen, "tls", JsonValueBoolean(0));
|
||||
ArrayAdd(listeners, JsonValueObject(listen));
|
||||
HashMapSet(json, "listen", JsonValueArray(listeners));
|
||||
|
||||
if (gethostname(hostname, HOST_NAME_MAX + 1) < 0)
|
||||
{
|
||||
strcpy(hostname, "localhost");
|
||||
}
|
||||
HashMapSet(json, "serverName", JsonValueString(hostname));
|
||||
|
||||
HashMapSet(json, "federation", JsonValueBoolean(1));
|
||||
HashMapSet(json, "registration", JsonValueBoolean(0));
|
||||
|
||||
return DbUnlock(db, ref);
|
||||
}
|
||||
|
||||
Config *
|
||||
ConfigLock(Db *db)
|
||||
{
|
||||
Config *config;
|
||||
DbRef *ref = DbLock(db, 1, "config");
|
||||
|
||||
if (!ref)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void
|
||||
ConfigFree(Config * tConfig)
|
||||
config = ConfigParse(DbJson(ref));
|
||||
if (config)
|
||||
{
|
||||
if (!tConfig)
|
||||
{
|
||||
return;
|
||||
config->db = db;
|
||||
config->ref = ref;
|
||||
}
|
||||
|
||||
Free(tConfig->serverName);
|
||||
Free(tConfig->baseUrl);
|
||||
Free(tConfig->identityServer);
|
||||
return config;
|
||||
}
|
||||
|
||||
Free(tConfig->uid);
|
||||
Free(tConfig->gid);
|
||||
Free(tConfig->dataDir);
|
||||
|
||||
Free(tConfig->logTimestamp);
|
||||
|
||||
if (tConfig->servers)
|
||||
int
|
||||
ConfigUnlock(Config *config)
|
||||
{
|
||||
size_t i;
|
||||
Db *db;
|
||||
DbRef *dbRef;
|
||||
|
||||
for (i = 0; i < ArraySize(tConfig->servers); i++)
|
||||
if (!config)
|
||||
{
|
||||
HttpServerConfig *serverCfg = ArrayGet(tConfig->servers, i);
|
||||
|
||||
Free(serverCfg->tlsCert);
|
||||
Free(serverCfg->tlsKey);
|
||||
Free(serverCfg);
|
||||
return 0;
|
||||
}
|
||||
|
||||
ArrayFree(tConfig->servers);
|
||||
}
|
||||
db = config->db;
|
||||
dbRef = config->ref;
|
||||
|
||||
Free(tConfig);
|
||||
ConfigFree(config);
|
||||
return DbUnlock(db, dbRef);
|
||||
}
|
||||
|
|
38
src/Db.c
38
src/Db.c
|
@ -346,6 +346,9 @@ DbOpen(char *dir, size_t cache)
|
|||
|
||||
pthread_mutex_init(&db->lock, NULL);
|
||||
|
||||
db->mostRecent = NULL;
|
||||
db->leastRecent = NULL;
|
||||
|
||||
if (db->maxCache)
|
||||
{
|
||||
db->cache = HashMapCreate();
|
||||
|
@ -353,9 +356,6 @@ DbOpen(char *dir, size_t cache)
|
|||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
db->mostRecent = NULL;
|
||||
db->leastRecent = NULL;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -365,6 +365,18 @@ DbOpen(char *dir, size_t cache)
|
|||
return db;
|
||||
}
|
||||
|
||||
void
|
||||
DbMaxCacheSet(Db * db, size_t cache)
|
||||
{
|
||||
if (!db)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
db->maxCache = cache;
|
||||
DbCacheEvict(db);
|
||||
}
|
||||
|
||||
void
|
||||
DbClose(Db * db)
|
||||
{
|
||||
|
@ -520,7 +532,7 @@ DbLockFromArr(Db * db, Array * args)
|
|||
}
|
||||
else
|
||||
{
|
||||
Array *name = ArrayCreate();
|
||||
Array *name;
|
||||
size_t i;
|
||||
|
||||
/* Not in cache; load from disk */
|
||||
|
@ -548,13 +560,14 @@ DbLockFromArr(Db * db, Array * args)
|
|||
pthread_mutex_init(&ref->lock, NULL);
|
||||
pthread_mutex_lock(&ref->lock);
|
||||
|
||||
name = ArrayCreate();
|
||||
for (i = 0; i < ArraySize(args); i++)
|
||||
{
|
||||
ArrayAdd(name, StrDuplicate(ArrayGet(args, i)));
|
||||
}
|
||||
ref->name = name;
|
||||
|
||||
if (db->cache)
|
||||
if (db->maxCache)
|
||||
{
|
||||
ref->ts = UtilServerTs();
|
||||
ref->size = DbComputeSize(ref->json);
|
||||
|
@ -758,7 +771,7 @@ DbUnlock(Db * db, DbRef * ref)
|
|||
|
||||
StreamClose(ref->stream);
|
||||
|
||||
if (db->cache)
|
||||
if (db->maxCache)
|
||||
{
|
||||
db->cacheSize -= ref->size;
|
||||
ref->size = DbComputeSize(ref->json);
|
||||
|
@ -887,3 +900,16 @@ DbJson(DbRef * ref)
|
|||
{
|
||||
return ref ? ref->json : NULL;
|
||||
}
|
||||
|
||||
int
|
||||
DbJsonSet(DbRef * ref, HashMap * json)
|
||||
{
|
||||
if (!ref || !json)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
JsonFree(ref->json);
|
||||
ref->json = JsonDuplicate(json);
|
||||
return 1;
|
||||
}
|
||||
|
|
|
@ -321,13 +321,17 @@ HashMapSet(HashMap * map, char *key, void *value)
|
|||
unsigned long hash;
|
||||
size_t index;
|
||||
|
||||
key = StrDuplicate(key);
|
||||
|
||||
if (!map || !key || !value)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
key = StrDuplicate(key);
|
||||
if (!key)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (map->count + 1 > map->capacity * map->maxLoad)
|
||||
{
|
||||
HashMapGrow(map);
|
||||
|
|
78
src/Json.c
78
src/Json.c
|
@ -701,6 +701,11 @@ JsonFree(HashMap * object)
|
|||
char *key;
|
||||
JsonValue *value;
|
||||
|
||||
if (!object)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
while (HashMapIterate(object, &key, (void **) &value))
|
||||
{
|
||||
JsonValueFree(value);
|
||||
|
@ -709,6 +714,79 @@ JsonFree(HashMap * object)
|
|||
HashMapFree(object);
|
||||
}
|
||||
|
||||
JsonValue *
|
||||
JsonValueDuplicate(JsonValue *val)
|
||||
{
|
||||
JsonValue *new;
|
||||
size_t i;
|
||||
|
||||
if (!val)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
new = JsonValueAllocate();
|
||||
if (!new)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
new->type = val->type;
|
||||
|
||||
switch(val->type)
|
||||
{
|
||||
case JSON_OBJECT:
|
||||
new->as.object = JsonDuplicate(val->as.object);
|
||||
break;
|
||||
case JSON_ARRAY:
|
||||
new->as.array = ArrayCreate();
|
||||
for (i = 0; i < ArraySize(val->as.array); i++)
|
||||
{
|
||||
ArrayAdd(new->as.array, JsonValueDuplicate(ArrayGet(val->as.array, i)));
|
||||
}
|
||||
break;
|
||||
case JSON_STRING:
|
||||
new->as.string = StrDuplicate(val->as.string);
|
||||
break;
|
||||
case JSON_INTEGER:
|
||||
case JSON_FLOAT:
|
||||
case JSON_BOOLEAN:
|
||||
/* These are by value, not by reference */
|
||||
new->as = val->as;
|
||||
case JSON_NULL:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return new;
|
||||
}
|
||||
|
||||
HashMap *
|
||||
JsonDuplicate(HashMap * object)
|
||||
{
|
||||
HashMap *new;
|
||||
char *key;
|
||||
JsonValue *val;
|
||||
|
||||
if (!object)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
new = HashMapCreate();
|
||||
if (!new)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
while (HashMapIterate(object, &key, (void **) &val))
|
||||
{
|
||||
HashMapSet(new, key, JsonValueDuplicate(val));
|
||||
}
|
||||
|
||||
return new;
|
||||
}
|
||||
|
||||
static int
|
||||
JsonConsumeWhitespace(JsonParserState * state)
|
||||
{
|
||||
|
|
|
@ -122,7 +122,6 @@ LogConfigFree(LogConfig * config)
|
|||
return;
|
||||
}
|
||||
|
||||
StreamClose(config->out);
|
||||
Free(config);
|
||||
|
||||
if (config == globalConfig)
|
||||
|
|
228
src/Main.c
228
src/Main.c
|
@ -28,6 +28,7 @@
|
|||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <limits.h>
|
||||
|
||||
#include <grp.h>
|
||||
#include <pwd.h>
|
||||
|
@ -39,17 +40,41 @@
|
|||
#include <HashMap.h>
|
||||
#include <Json.h>
|
||||
#include <HttpServer.h>
|
||||
#include <Routes.h>
|
||||
#include <Matrix.h>
|
||||
#include <Db.h>
|
||||
#include <Cron.h>
|
||||
#include <Uia.h>
|
||||
#include <Util.h>
|
||||
#include <Str.h>
|
||||
|
||||
static Array *httpServers = NULL;
|
||||
#include <Matrix.h>
|
||||
#include <User.h>
|
||||
#include <RegToken.h>
|
||||
#include <Routes.h>
|
||||
|
||||
static Array *httpServers;
|
||||
static volatile int restart;
|
||||
static unsigned long startTs;
|
||||
|
||||
void
|
||||
Restart(void)
|
||||
{
|
||||
raise(SIGUSR1);
|
||||
}
|
||||
|
||||
void
|
||||
Shutdown(void)
|
||||
{
|
||||
raise(SIGINT);
|
||||
}
|
||||
|
||||
unsigned long
|
||||
Uptime(void)
|
||||
{
|
||||
return UtilServerTs() - startTs;
|
||||
}
|
||||
|
||||
static void
|
||||
TelodendriaSignalHandler(int signal)
|
||||
SignalHandler(int signal)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
|
@ -57,6 +82,9 @@ TelodendriaSignalHandler(int signal)
|
|||
{
|
||||
case SIGPIPE:
|
||||
return;
|
||||
case SIGUSR1:
|
||||
restart = 1;
|
||||
/* Fall through */
|
||||
case SIGINT:
|
||||
if (!httpServers)
|
||||
{
|
||||
|
@ -76,30 +104,26 @@ TelodendriaSignalHandler(int signal)
|
|||
typedef enum ArgFlag
|
||||
{
|
||||
ARG_VERSION = (1 << 0),
|
||||
ARG_CONFIGTEST = (1 << 1),
|
||||
ARG_VERBOSE = (1 << 2)
|
||||
} ArgFlag;
|
||||
|
||||
int
|
||||
main(int argc, char **argv)
|
||||
{
|
||||
int exit = EXIT_SUCCESS;
|
||||
int exit;
|
||||
|
||||
/* Arg parsing */
|
||||
int opt;
|
||||
int flags = 0;
|
||||
char *configArg = NULL;
|
||||
|
||||
/* Config file */
|
||||
Stream *configFile = NULL;
|
||||
HashMap *config = NULL;
|
||||
int flags;
|
||||
char *dbPath;
|
||||
|
||||
/* Program configuration */
|
||||
Config *tConfig = NULL;
|
||||
Config *tConfig;
|
||||
Stream *logFile;
|
||||
|
||||
/* User validation */
|
||||
struct passwd *userInfo = NULL;
|
||||
struct group *groupInfo = NULL;
|
||||
struct passwd *userInfo;
|
||||
struct group *groupInfo;
|
||||
|
||||
/* HTTP server management */
|
||||
size_t i;
|
||||
|
@ -109,7 +133,30 @@ main(int argc, char **argv)
|
|||
struct sigaction sigAction;
|
||||
|
||||
MatrixHttpHandlerArgs matrixArgs;
|
||||
Cron *cron = NULL;
|
||||
Cron *cron;
|
||||
|
||||
char startDir[PATH_MAX];
|
||||
|
||||
start:
|
||||
/* Global variables */
|
||||
httpServers = NULL;
|
||||
restart = 0;
|
||||
|
||||
/* For getopt() */
|
||||
opterr = 1;
|
||||
optind = 1;
|
||||
|
||||
/* Local variables */
|
||||
exit = EXIT_SUCCESS;
|
||||
flags = 0;
|
||||
dbPath = NULL;
|
||||
tConfig = NULL;
|
||||
logFile = NULL;
|
||||
userInfo = NULL;
|
||||
groupInfo = NULL;
|
||||
cron = NULL;
|
||||
|
||||
startTs = UtilServerTs();
|
||||
|
||||
memset(&matrixArgs, 0, sizeof(matrixArgs));
|
||||
|
||||
|
@ -121,12 +168,12 @@ main(int argc, char **argv)
|
|||
|
||||
TelodendriaPrintHeader();
|
||||
|
||||
while ((opt = getopt(argc, argv, "f:Vvn")) != -1)
|
||||
while ((opt = getopt(argc, argv, "d:Vv")) != -1)
|
||||
{
|
||||
switch (opt)
|
||||
{
|
||||
case 'f':
|
||||
configArg = optarg;
|
||||
case 'd':
|
||||
dbPath = optarg;
|
||||
break;
|
||||
case 'V':
|
||||
flags |= ARG_VERSION;
|
||||
|
@ -134,9 +181,6 @@ main(int argc, char **argv)
|
|||
case 'v':
|
||||
flags |= ARG_VERBOSE;
|
||||
break;
|
||||
case 'n':
|
||||
flags |= ARG_CONFIGTEST;
|
||||
break;
|
||||
case '?':
|
||||
exit = EXIT_FAILURE;
|
||||
goto finish;
|
||||
|
@ -156,27 +200,41 @@ main(int argc, char **argv)
|
|||
goto finish;
|
||||
}
|
||||
|
||||
if (!configArg)
|
||||
if (!dbPath)
|
||||
{
|
||||
Log(LOG_ERR, "No configuration file specified.");
|
||||
Log(LOG_ERR, "No database directory specified.");
|
||||
exit = EXIT_FAILURE;
|
||||
goto finish;
|
||||
}
|
||||
else if (strcmp(configArg, "-") == 0)
|
||||
|
||||
if (!getcwd(startDir, PATH_MAX))
|
||||
{
|
||||
configFile = StreamStdin();
|
||||
Log(LOG_ERR, "Unable to determine current working directory.");
|
||||
exit = EXIT_FAILURE;
|
||||
goto finish;
|
||||
}
|
||||
|
||||
if (chdir(dbPath) != 0)
|
||||
{
|
||||
Log(LOG_ERR, "Unable to change into data directory: %s.", strerror(errno));
|
||||
exit = EXIT_FAILURE;
|
||||
goto finish;
|
||||
}
|
||||
else
|
||||
{
|
||||
StreamClose(StreamStdin());
|
||||
Log(LOG_DEBUG, "Changed working directory to: %s", dbPath);
|
||||
}
|
||||
|
||||
configFile = StreamOpen(configArg, "r");
|
||||
if (!configFile)
|
||||
matrixArgs.db = DbOpen(".", 0);
|
||||
if (!matrixArgs.db)
|
||||
{
|
||||
Log(LOG_ERR, "Unable to open configuration file '%s' for reading.", configArg);
|
||||
Log(LOG_ERR, "Unable to open data directory as a database.");
|
||||
exit = EXIT_FAILURE;
|
||||
goto finish;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log(LOG_DEBUG, "Opened database.");
|
||||
}
|
||||
|
||||
Log(LOG_NOTICE, "Building routing tree...");
|
||||
|
@ -188,30 +246,56 @@ main(int argc, char **argv)
|
|||
goto finish;
|
||||
}
|
||||
|
||||
Log(LOG_NOTICE, "Processing configuration file '%s'.", configArg);
|
||||
|
||||
config = JsonDecode(configFile);
|
||||
StreamClose(configFile);
|
||||
|
||||
if (!config)
|
||||
if (!ConfigExists(matrixArgs.db))
|
||||
{
|
||||
Log(LOG_ERR, "Syntax error in configuration file.");
|
||||
char *token;
|
||||
RegTokenInfo *info;
|
||||
|
||||
Log(LOG_NOTICE, "No configuration exists in the opened database.");
|
||||
Log(LOG_NOTICE, "A default configuration will be created, and a");
|
||||
Log(LOG_NOTICE, "new single-use registration token that grants all");
|
||||
Log(LOG_NOTICE, "privileges will be created so an admin user can");
|
||||
Log(LOG_NOTICE, "be created to configure this database using the");
|
||||
Log(LOG_NOTICE, "administrator API.");
|
||||
|
||||
if (!ConfigCreateDefault(matrixArgs.db))
|
||||
{
|
||||
Log(LOG_ERR, "Unable to create default configuration.");
|
||||
exit = EXIT_FAILURE;
|
||||
goto finish;
|
||||
}
|
||||
|
||||
tConfig = ConfigParse(config);
|
||||
JsonFree(config);
|
||||
token = StrRandom(32);
|
||||
info = RegTokenCreate(matrixArgs.db, token, NULL, 0, 1, USER_ALL);
|
||||
if (!info)
|
||||
{
|
||||
Free(token);
|
||||
Log(LOG_ERR, "Unable to create admin registration token.");
|
||||
exit = EXIT_FAILURE;
|
||||
goto finish;
|
||||
}
|
||||
|
||||
Log(LOG_NOTICE, "Admin Registration token: %s", token);
|
||||
|
||||
Free(token);
|
||||
RegTokenClose(info);
|
||||
RegTokenFree(info);
|
||||
}
|
||||
|
||||
Log(LOG_NOTICE, "Loading configuration...");
|
||||
|
||||
tConfig = ConfigLock(matrixArgs.db);
|
||||
if (!tConfig)
|
||||
{
|
||||
Log(LOG_ERR, "Error locking the configuration.");
|
||||
Log(LOG_ERR, "The configuration object is corrupted or otherwise invalid.");
|
||||
Log(LOG_ERR, "Please restore from a backup.");
|
||||
exit = EXIT_FAILURE;
|
||||
goto finish;
|
||||
}
|
||||
|
||||
if (flags & ARG_CONFIGTEST)
|
||||
} else if (!tConfig->ok)
|
||||
{
|
||||
Log(LOG_INFO, "Configuration is OK.");
|
||||
Log(LOG_ERR, tConfig->err);
|
||||
exit = EXIT_FAILURE;
|
||||
goto finish;
|
||||
}
|
||||
|
||||
|
@ -236,20 +320,9 @@ main(int argc, char **argv)
|
|||
|
||||
LogConfigLevelSet(LogConfigGlobal(), flags & ARG_VERBOSE ? LOG_DEBUG : tConfig->logLevel);
|
||||
|
||||
if (chdir(tConfig->dataDir) != 0)
|
||||
{
|
||||
Log(LOG_ERR, "Unable to change into data directory: %s.", strerror(errno));
|
||||
exit = EXIT_FAILURE;
|
||||
goto finish;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log(LOG_DEBUG, "Changed working directory to: %s", tConfig->dataDir);
|
||||
}
|
||||
|
||||
if (tConfig->flags & CONFIG_LOG_FILE)
|
||||
{
|
||||
Stream *logFile = StreamOpen("telodendria.log", "a");
|
||||
logFile = StreamOpen("telodendria.log", "a");
|
||||
|
||||
if (!logFile)
|
||||
{
|
||||
|
@ -261,7 +334,6 @@ main(int argc, char **argv)
|
|||
|
||||
Log(LOG_INFO, "Logging to the log file. Check there for all future messages.");
|
||||
LogConfigOutputSet(LogConfigGlobal(), logFile);
|
||||
StreamClose(StreamStdout());
|
||||
}
|
||||
else if (tConfig->flags & CONFIG_LOG_STDOUT)
|
||||
{
|
||||
|
@ -291,14 +363,10 @@ main(int argc, char **argv)
|
|||
Log(LOG_DEBUG, "Base URL: %s", tConfig->baseUrl);
|
||||
Log(LOG_DEBUG, "Identity Server: %s", tConfig->identityServer);
|
||||
Log(LOG_DEBUG, "Run As: %s:%s", tConfig->uid, tConfig->gid);
|
||||
Log(LOG_DEBUG, "Data Directory: %s", tConfig->dataDir);
|
||||
Log(LOG_DEBUG, "Max Cache: %ld", tConfig->maxCache);
|
||||
Log(LOG_DEBUG, "Flags: %x", tConfig->flags);
|
||||
LogConfigUnindent(LogConfigGlobal());
|
||||
|
||||
/* Arguments to pass into the HTTP handler */
|
||||
matrixArgs.config = tConfig;
|
||||
|
||||
httpServers = ArrayCreate();
|
||||
if (!httpServers)
|
||||
{
|
||||
|
@ -423,11 +491,9 @@ main(int argc, char **argv)
|
|||
|
||||
/* These config values are no longer needed; don't hold them in
|
||||
* memory anymore */
|
||||
Free(tConfig->dataDir);
|
||||
Free(tConfig->uid);
|
||||
Free(tConfig->gid);
|
||||
|
||||
tConfig->dataDir = NULL;
|
||||
tConfig->uid = NULL;
|
||||
tConfig->gid = NULL;
|
||||
|
||||
|
@ -438,18 +504,10 @@ main(int argc, char **argv)
|
|||
Log(LOG_WARNING, "and ensure that maxCache is a valid number of bytes.");
|
||||
}
|
||||
|
||||
matrixArgs.db = DbOpen(".", tConfig->maxCache);
|
||||
DbMaxCacheSet(matrixArgs.db, tConfig->maxCache);
|
||||
|
||||
if (!matrixArgs.db)
|
||||
{
|
||||
Log(LOG_ERR, "Unable to open data directory as a database.");
|
||||
exit = EXIT_FAILURE;
|
||||
goto finish;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log(LOG_DEBUG, "Opened database.");
|
||||
}
|
||||
ConfigUnlock(tConfig);
|
||||
tConfig = NULL;
|
||||
|
||||
cron = CronCreate(60 * 1000); /* 1-minute tick */
|
||||
if (!cron)
|
||||
|
@ -489,7 +547,7 @@ main(int argc, char **argv)
|
|||
}
|
||||
|
||||
|
||||
sigAction.sa_handler = TelodendriaSignalHandler;
|
||||
sigAction.sa_handler = SignalHandler;
|
||||
sigfillset(&sigAction.sa_mask);
|
||||
sigAction.sa_flags = SA_RESTART;
|
||||
|
||||
|
@ -507,6 +565,7 @@ main(int argc, char **argv)
|
|||
|
||||
SIGACTION(SIGINT, &sigAction, NULL);
|
||||
SIGACTION(SIGPIPE, &sigAction, NULL);
|
||||
SIGACTION(SIGUSR1, &sigAction, NULL);
|
||||
|
||||
#undef SIGACTION
|
||||
|
||||
|
@ -542,16 +601,15 @@ finish:
|
|||
Log(LOG_DEBUG, "Stopped and freed job scheduler.");
|
||||
}
|
||||
|
||||
ConfigUnlock(tConfig);
|
||||
Log(LOG_DEBUG, "Unlocked configuration.");
|
||||
|
||||
DbClose(matrixArgs.db);
|
||||
Log(LOG_DEBUG, "Closed database.");
|
||||
|
||||
HttpRouterFree(matrixArgs.router);
|
||||
Log(LOG_DEBUG, "Freed routing tree.");
|
||||
|
||||
ConfigFree(tConfig);
|
||||
|
||||
Log(LOG_DEBUG, "Exiting with code '%d'.", exit);
|
||||
|
||||
/*
|
||||
* Uninstall the memory hook because it uses the Log
|
||||
* API, whose configuration is being freed now, so it
|
||||
|
@ -560,6 +618,19 @@ finish:
|
|||
MemoryHook(NULL, NULL);
|
||||
|
||||
LogConfigFree(LogConfigGlobal());
|
||||
StreamClose(logFile);
|
||||
|
||||
if (restart)
|
||||
{
|
||||
/*
|
||||
* Change back into starting directory so initial chdir()
|
||||
* call works.
|
||||
*/
|
||||
chdir(startDir);
|
||||
goto start;
|
||||
}
|
||||
|
||||
StreamClose(StreamStdout());
|
||||
|
||||
/* Standard error should never have been opened, but just in case
|
||||
* it was, this doesn't hurt anything. */
|
||||
|
@ -572,5 +643,6 @@ finish:
|
|||
/* Free any leaked memory now, just in case the operating system
|
||||
* we're running on won't do it for us. */
|
||||
MemoryFreeAll();
|
||||
|
||||
return exit;
|
||||
}
|
||||
|
|
|
@ -203,7 +203,7 @@ RegTokenCreate(Db * db, char *name, char *owner, unsigned long expires, int uses
|
|||
|
||||
unsigned long timestamp = UtilServerTs();
|
||||
|
||||
if (!db || !name || !owner)
|
||||
if (!db || !name)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
|
|
@ -67,6 +67,9 @@ RouterBuild(void)
|
|||
R("/_matrix/client/v3/profile/(.*)", RouteUserProfile);
|
||||
R("/_matrix/client/v3/profile/(.*)/(avatar_url|displayname)", RouteUserProfile);
|
||||
|
||||
R("/_telodendria/admin/(restart|shutdown|stats)", RouteProcControl);
|
||||
R("/_telodendria/admin/config", RouteConfig);
|
||||
|
||||
#undef R
|
||||
|
||||
return router;
|
||||
|
|
|
@ -65,45 +65,53 @@ ROUTE_IMPL(RouteChangePwd, path, argp)
|
|||
char *token;
|
||||
char *newPassword;
|
||||
|
||||
Config *config = ConfigLock(db);
|
||||
if (!config)
|
||||
{
|
||||
Log(LOG_ERR, "Password endpoint failed to lock configuration.");
|
||||
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
|
||||
return MatrixErrorCreate(M_UNKNOWN);
|
||||
}
|
||||
|
||||
(void) path;
|
||||
|
||||
if (HttpRequestMethodGet(args->context) != HTTP_POST)
|
||||
{
|
||||
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
|
||||
return MatrixErrorCreate(M_UNRECOGNIZED);
|
||||
response = MatrixErrorCreate(M_UNRECOGNIZED);
|
||||
goto finish;
|
||||
}
|
||||
|
||||
response = MatrixGetAccessToken(args->context, &token);
|
||||
if (response)
|
||||
{
|
||||
JsonFree(request);
|
||||
return response;
|
||||
goto finish;
|
||||
}
|
||||
|
||||
request = JsonDecode(HttpServerStream(args->context));
|
||||
if (!request)
|
||||
{
|
||||
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
|
||||
return MatrixErrorCreate(M_NOT_JSON);
|
||||
response = MatrixErrorCreate(M_NOT_JSON);
|
||||
goto finish;
|
||||
}
|
||||
|
||||
uiaFlows = ArrayCreate();
|
||||
ArrayAdd(uiaFlows, PasswordFlow());
|
||||
uiaResult = UiaComplete(uiaFlows, args->context,
|
||||
args->matrixArgs->db, request, &response,
|
||||
args->matrixArgs->config);
|
||||
db, request, &response,
|
||||
config);
|
||||
UiaFlowsFree(uiaFlows);
|
||||
|
||||
if (uiaResult < 0)
|
||||
{
|
||||
JsonFree(request);
|
||||
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
|
||||
return MatrixErrorCreate(M_UNKNOWN);
|
||||
response = MatrixErrorCreate(M_UNKNOWN);
|
||||
goto finish;
|
||||
}
|
||||
else if (!uiaResult)
|
||||
{
|
||||
JsonFree(request);
|
||||
return response;
|
||||
goto finish;
|
||||
}
|
||||
|
||||
newPassword = JsonValueAsString(HashMapGet(request, "new_password"));
|
||||
|
@ -111,7 +119,8 @@ ROUTE_IMPL(RouteChangePwd, path, argp)
|
|||
{
|
||||
JsonFree(request);
|
||||
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
|
||||
return MatrixErrorCreate(M_BAD_JSON);
|
||||
response = MatrixErrorCreate(M_BAD_JSON);
|
||||
goto finish;
|
||||
}
|
||||
|
||||
val = HashMapGet(request, "logout_devices");
|
||||
|
@ -125,9 +134,9 @@ ROUTE_IMPL(RouteChangePwd, path, argp)
|
|||
|
||||
if (!user)
|
||||
{
|
||||
JsonFree(request);
|
||||
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
|
||||
return MatrixErrorCreate(M_UNKNOWN_TOKEN);
|
||||
response = MatrixErrorCreate(M_UNKNOWN_TOKEN);
|
||||
goto finish;
|
||||
}
|
||||
|
||||
UserSetPassword(user, newPassword);
|
||||
|
@ -139,8 +148,11 @@ ROUTE_IMPL(RouteChangePwd, path, argp)
|
|||
UserDeleteTokens(user, token);
|
||||
}
|
||||
|
||||
response = HashMapCreate();
|
||||
|
||||
finish:
|
||||
ConfigUnlock(config);
|
||||
UserUnlock(user);
|
||||
JsonFree(request);
|
||||
response = HashMapCreate();
|
||||
return response;
|
||||
}
|
||||
|
|
137
src/Routes/RouteConfig.c
Normal file
137
src/Routes/RouteConfig.c
Normal file
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* 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 <Routes.h>
|
||||
|
||||
#include <User.h>
|
||||
#include <Main.h>
|
||||
#include <Memory.h>
|
||||
|
||||
#include <string.h>
|
||||
|
||||
ROUTE_IMPL(RouteConfig, path, argp)
|
||||
{
|
||||
RouteArgs *args = argp;
|
||||
HashMap *response;
|
||||
char *token;
|
||||
|
||||
User *user = NULL;
|
||||
Config *config = NULL;
|
||||
|
||||
HashMap *request = NULL;
|
||||
Config *newConf;
|
||||
|
||||
(void) path;
|
||||
|
||||
response = MatrixGetAccessToken(args->context, &token);
|
||||
if (response)
|
||||
{
|
||||
goto finish;
|
||||
}
|
||||
|
||||
user = UserAuthenticate(args->matrixArgs->db, token);
|
||||
if (!user)
|
||||
{
|
||||
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
|
||||
response = MatrixErrorCreate(M_UNKNOWN_TOKEN);
|
||||
goto finish;
|
||||
}
|
||||
|
||||
if (!(UserGetPrivileges(user) & USER_CONFIG))
|
||||
{
|
||||
HttpResponseStatus(args->context, HTTP_FORBIDDEN);
|
||||
response = MatrixErrorCreate(M_FORBIDDEN);
|
||||
goto finish;
|
||||
}
|
||||
|
||||
config = ConfigLock(args->matrixArgs->db);
|
||||
if (!config)
|
||||
{
|
||||
Log(LOG_ERR, "Config endpoint failed to lock configuration.");
|
||||
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
|
||||
response = MatrixErrorCreate(M_UNKNOWN);
|
||||
goto finish;
|
||||
}
|
||||
|
||||
switch (HttpRequestMethodGet(args->context))
|
||||
{
|
||||
case HTTP_GET:
|
||||
response = JsonDuplicate(DbJson(config->ref));
|
||||
break;
|
||||
case HTTP_POST:
|
||||
request = JsonDecode(HttpServerStream(args->context));
|
||||
if (!request)
|
||||
{
|
||||
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
|
||||
response = MatrixErrorCreate(M_NOT_JSON);
|
||||
break;
|
||||
}
|
||||
|
||||
newConf = ConfigParse(request);
|
||||
if (!newConf)
|
||||
{
|
||||
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
|
||||
response = MatrixErrorCreate(M_UNKNOWN);
|
||||
break;
|
||||
}
|
||||
|
||||
if (newConf->ok)
|
||||
{
|
||||
if (DbJsonSet(config->ref, request))
|
||||
{
|
||||
response = HashMapCreate();
|
||||
/*
|
||||
* TODO: Apply configuration and set this only if a main
|
||||
* component was reconfigured, such as the listeners.
|
||||
*/
|
||||
HashMapSet(response, "restart_required", JsonValueBoolean(1));
|
||||
}
|
||||
else
|
||||
{
|
||||
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
|
||||
response = MatrixErrorCreate(M_UNKNOWN);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
|
||||
/* TODO: Attach newConf->err as message */
|
||||
response = MatrixErrorCreate(M_BAD_JSON);
|
||||
}
|
||||
|
||||
ConfigFree(newConf);
|
||||
JsonFree(request);
|
||||
break;
|
||||
case HTTP_PUT:
|
||||
/* TODO: Support incremental changes to the config */
|
||||
default:
|
||||
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
|
||||
response = MatrixErrorCreate(M_UNRECOGNIZED);
|
||||
break;
|
||||
}
|
||||
|
||||
finish:
|
||||
UserUnlock(user);
|
||||
ConfigUnlock(config);
|
||||
return response;
|
||||
}
|
|
@ -57,6 +57,14 @@ ROUTE_IMPL(RouteLogin, path, argp)
|
|||
UserLoginInfo *loginInfo;
|
||||
char *fullUsername;
|
||||
|
||||
Config *config = ConfigLock(db);
|
||||
if (!config)
|
||||
{
|
||||
Log(LOG_ERR, "Login endpoint failed to lock configuration.");
|
||||
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
|
||||
return MatrixErrorCreate(M_UNKNOWN);
|
||||
}
|
||||
|
||||
(void) path;
|
||||
|
||||
switch (HttpRequestMethodGet(args->context))
|
||||
|
@ -157,8 +165,7 @@ ROUTE_IMPL(RouteLogin, path, argp)
|
|||
break;
|
||||
}
|
||||
|
||||
userId = UserIdParse(JsonValueAsString(val),
|
||||
args->matrixArgs->config->serverName);
|
||||
userId = UserIdParse(JsonValueAsString(val), config->serverName);
|
||||
if (!userId)
|
||||
{
|
||||
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
|
||||
|
@ -166,7 +173,7 @@ ROUTE_IMPL(RouteLogin, path, argp)
|
|||
break;
|
||||
}
|
||||
|
||||
if (strcmp(userId->server, args->matrixArgs->config->serverName) != 0
|
||||
if (strcmp(userId->server, config->serverName) != 0
|
||||
|| !UserExists(db, userId->localpart))
|
||||
{
|
||||
HttpResponseStatus(args->context, HTTP_FORBIDDEN);
|
||||
|
@ -276,14 +283,13 @@ ROUTE_IMPL(RouteLogin, path, argp)
|
|||
}
|
||||
|
||||
fullUsername = StrConcat(4, "@", UserGetName(user), ":",
|
||||
args->matrixArgs->config->serverName);
|
||||
config->serverName);
|
||||
HashMapSet(response, "user_id", JsonValueString(fullUsername));
|
||||
Free(fullUsername);
|
||||
|
||||
HashMapSet(response, "well_known",
|
||||
JsonValueObject(
|
||||
MatrixClientWellKnown(args->matrixArgs->config->baseUrl,
|
||||
args->matrixArgs->config->identityServer)));
|
||||
MatrixClientWellKnown(config->baseUrl, config->identityServer)));
|
||||
|
||||
UserAccessTokenFree(loginInfo->accessToken);
|
||||
Free(loginInfo->refreshToken);
|
||||
|
@ -300,5 +306,6 @@ ROUTE_IMPL(RouteLogin, path, argp)
|
|||
|
||||
UserIdFree(userId);
|
||||
JsonFree(request);
|
||||
ConfigUnlock(config);
|
||||
return response;
|
||||
}
|
||||
|
|
92
src/Routes/RouteProcControl.c
Normal file
92
src/Routes/RouteProcControl.c
Normal file
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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 <Routes.h>
|
||||
|
||||
#include <User.h>
|
||||
#include <Main.h>
|
||||
#include <Memory.h>
|
||||
|
||||
#include <string.h>
|
||||
|
||||
ROUTE_IMPL(RouteProcControl, path, argp)
|
||||
{
|
||||
RouteArgs *args = argp;
|
||||
char *op = ArrayGet(path, 0);
|
||||
HashMap *response;
|
||||
char *token;
|
||||
User *user = NULL;
|
||||
|
||||
response = MatrixGetAccessToken(args->context, &token);
|
||||
if (response)
|
||||
{
|
||||
goto finish;
|
||||
}
|
||||
|
||||
user = UserAuthenticate(args->matrixArgs->db, token);
|
||||
if (!user)
|
||||
{
|
||||
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
|
||||
response = MatrixErrorCreate(M_UNKNOWN_TOKEN);
|
||||
goto finish;
|
||||
}
|
||||
|
||||
if (!(UserGetPrivileges(user) & USER_PROC_CONTROL))
|
||||
{
|
||||
HttpResponseStatus(args->context, HTTP_FORBIDDEN);
|
||||
response = MatrixErrorCreate(M_FORBIDDEN);
|
||||
goto finish;
|
||||
}
|
||||
|
||||
if (strcmp(op, "restart") == 0)
|
||||
{
|
||||
Restart();
|
||||
}
|
||||
else if (strcmp(op, "shutdown") == 0)
|
||||
{
|
||||
Shutdown();
|
||||
}
|
||||
else if (strcmp(op, "stats") == 0)
|
||||
{
|
||||
response = HashMapCreate();
|
||||
|
||||
HashMapSet(response, "version", JsonValueString(TELODENDRIA_VERSION));
|
||||
HashMapSet(response, "memory_allocated", JsonValueInteger(MemoryAllocated()));
|
||||
HashMapSet(response, "uptime", JsonValueInteger(Uptime()));
|
||||
|
||||
goto finish;
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Should be impossible */
|
||||
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
|
||||
response = MatrixErrorCreate(M_UNKNOWN);
|
||||
goto finish;
|
||||
}
|
||||
|
||||
response = HashMapCreate();
|
||||
|
||||
finish:
|
||||
UserUnlock(user);
|
||||
return response;
|
||||
}
|
|
@ -77,19 +77,29 @@ ROUTE_IMPL(RouteRegister, path, argp)
|
|||
char *session;
|
||||
DbRef *sessionRef;
|
||||
|
||||
Config *config = ConfigLock(db);
|
||||
if (!config)
|
||||
{
|
||||
Log(LOG_ERR, "Registration endpoint failed to lock configuration.");
|
||||
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
|
||||
return MatrixErrorCreate(M_UNKNOWN);
|
||||
}
|
||||
|
||||
if (ArraySize(path) == 0)
|
||||
{
|
||||
if (HttpRequestMethodGet(args->context) != HTTP_POST)
|
||||
{
|
||||
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
|
||||
return MatrixErrorCreate(M_UNRECOGNIZED);
|
||||
response = MatrixErrorCreate(M_UNRECOGNIZED);
|
||||
goto end;
|
||||
}
|
||||
|
||||
request = JsonDecode(HttpServerStream(args->context));
|
||||
if (!request)
|
||||
{
|
||||
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
|
||||
return MatrixErrorCreate(M_NOT_JSON);
|
||||
response = MatrixErrorCreate(M_NOT_JSON);
|
||||
goto end;
|
||||
}
|
||||
|
||||
val = HashMapGet(request, "username");
|
||||
|
@ -103,7 +113,7 @@ ROUTE_IMPL(RouteRegister, path, argp)
|
|||
}
|
||||
username = StrDuplicate(JsonValueAsString(val));
|
||||
|
||||
if (!UserValidate(username, args->matrixArgs->config->serverName))
|
||||
if (!UserValidate(username, config->serverName))
|
||||
{
|
||||
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
|
||||
response = MatrixErrorCreate(M_INVALID_USERNAME);
|
||||
|
@ -121,14 +131,14 @@ ROUTE_IMPL(RouteRegister, path, argp)
|
|||
uiaFlows = ArrayCreate();
|
||||
ArrayAdd(uiaFlows, RouteRegisterRegFlow());
|
||||
|
||||
if (args->matrixArgs->config->flags & CONFIG_REGISTRATION)
|
||||
if (config->flags & CONFIG_REGISTRATION)
|
||||
{
|
||||
ArrayAdd(uiaFlows, UiaDummyFlow());
|
||||
}
|
||||
|
||||
uiaResult = UiaComplete(uiaFlows, args->context,
|
||||
args->matrixArgs->db, request, &response,
|
||||
args->matrixArgs->config);
|
||||
db, request, &response,
|
||||
config);
|
||||
|
||||
if (uiaResult < 0)
|
||||
{
|
||||
|
@ -224,7 +234,7 @@ ROUTE_IMPL(RouteRegister, path, argp)
|
|||
user = UserCreate(db, username, password);
|
||||
response = HashMapCreate();
|
||||
|
||||
fullUsername = StrConcat(4, "@", UserGetName(user), ":", args->matrixArgs->config->serverName);
|
||||
fullUsername = StrConcat(4, "@", UserGetName(user), ":", config->serverName);
|
||||
HashMapSet(response, "user_id", JsonValueString(fullUsername));
|
||||
Free(fullUsername);
|
||||
|
||||
|
@ -302,7 +312,7 @@ finish:
|
|||
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
|
||||
response = MatrixErrorCreate(M_MISSING_PARAM);
|
||||
}
|
||||
else if (!UserValidate(username, args->matrixArgs->config->serverName))
|
||||
else if (!UserValidate(username, config->serverName))
|
||||
{
|
||||
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
|
||||
response = MatrixErrorCreate(M_INVALID_USERNAME);
|
||||
|
@ -325,5 +335,7 @@ finish:
|
|||
}
|
||||
}
|
||||
|
||||
end:
|
||||
ConfigUnlock(config);
|
||||
return response;
|
||||
}
|
||||
|
|
|
@ -42,12 +42,22 @@ ROUTE_IMPL(RouteUserProfile, path, argp)
|
|||
UserId *userId = NULL;
|
||||
User *user = NULL;
|
||||
|
||||
char *serverName = args->matrixArgs->config->serverName;
|
||||
char *serverName;
|
||||
char *username = NULL;
|
||||
char *entry = NULL;
|
||||
char *token = NULL;
|
||||
char *value = NULL;
|
||||
|
||||
Config *config = ConfigLock(db);
|
||||
if (!config)
|
||||
{
|
||||
Log(LOG_ERR, "User profile endpoint failed to lock configuration.");
|
||||
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
|
||||
return MatrixErrorCreate(M_UNKNOWN);
|
||||
}
|
||||
|
||||
serverName = config->serverName;
|
||||
|
||||
username = ArrayGet(path, 0);
|
||||
userId = UserIdParse(username, serverName);
|
||||
if (!userId)
|
||||
|
@ -160,6 +170,7 @@ ROUTE_IMPL(RouteUserProfile, path, argp)
|
|||
break;
|
||||
}
|
||||
finish:
|
||||
ConfigUnlock(config);
|
||||
Free(username);
|
||||
Free(entry);
|
||||
UserIdFree(userId);
|
||||
|
|
|
@ -33,15 +33,26 @@
|
|||
ROUTE_IMPL(RouteWellKnown, path, argp)
|
||||
{
|
||||
RouteArgs *args = argp;
|
||||
HashMap *response;
|
||||
|
||||
Config *config = ConfigLock(args->matrixArgs->db);
|
||||
if (!config)
|
||||
{
|
||||
Log(LOG_ERR, "Well-known endpoint failed to lock configuration.");
|
||||
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
|
||||
return MatrixErrorCreate(M_UNKNOWN);
|
||||
}
|
||||
|
||||
if (MATRIX_PATH_EQUALS(ArrayGet(path, 0), "client"))
|
||||
{
|
||||
return MatrixClientWellKnown(args->matrixArgs->config->baseUrl,
|
||||
args->matrixArgs->config->identityServer);
|
||||
response = MatrixClientWellKnown(config->baseUrl, config->identityServer);
|
||||
}
|
||||
else
|
||||
{
|
||||
HttpResponseStatus(args->context, HTTP_NOT_FOUND);
|
||||
return MatrixErrorCreate(M_NOT_FOUND);
|
||||
response = MatrixErrorCreate(M_NOT_FOUND);
|
||||
}
|
||||
|
||||
ConfigUnlock(config);
|
||||
return response;
|
||||
}
|
||||
|
|
|
@ -45,6 +45,14 @@ ROUTE_IMPL(RouteWhoami, path, argp)
|
|||
char *userID;
|
||||
char *deviceID;
|
||||
|
||||
Config *config = ConfigLock(db);
|
||||
if (!config)
|
||||
{
|
||||
Log(LOG_ERR, "Who am I endpoint failed to lock configuration.");
|
||||
HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR);
|
||||
return MatrixErrorCreate(M_UNKNOWN);
|
||||
}
|
||||
|
||||
(void) path;
|
||||
|
||||
/* Get the request */
|
||||
|
@ -52,14 +60,15 @@ ROUTE_IMPL(RouteWhoami, path, argp)
|
|||
if (response)
|
||||
{
|
||||
/* No token? */
|
||||
return response;
|
||||
goto finish;
|
||||
}
|
||||
|
||||
/* Authenticate with our token */
|
||||
if (!DbExists(db, 3, "tokens", "access", token))
|
||||
{
|
||||
HttpResponseStatus(args->context, HTTP_UNAUTHORIZED);
|
||||
return MatrixErrorCreate(M_UNKNOWN_TOKEN);
|
||||
response = MatrixErrorCreate(M_UNKNOWN_TOKEN);
|
||||
goto finish;
|
||||
}
|
||||
|
||||
ref = DbLock(db, 3, "tokens", "access", token);
|
||||
|
@ -69,7 +78,7 @@ ROUTE_IMPL(RouteWhoami, path, argp)
|
|||
|
||||
userID = StrConcat(4, "@",
|
||||
JsonValueAsString(HashMapGet(tokenJson, "user")),
|
||||
":", args->matrixArgs->config->serverName);
|
||||
":", config->serverName);
|
||||
|
||||
deviceID = StrDuplicate(JsonValueAsString(HashMapGet(tokenJson, "device")));
|
||||
|
||||
|
@ -80,5 +89,8 @@ ROUTE_IMPL(RouteWhoami, path, argp)
|
|||
|
||||
Free(userID);
|
||||
Free(deviceID);
|
||||
|
||||
finish:
|
||||
ConfigUnlock(config);
|
||||
return response;
|
||||
}
|
||||
|
|
|
@ -89,6 +89,11 @@ StrDuplicate(const char *inStr)
|
|||
size_t len;
|
||||
char *outStr;
|
||||
|
||||
if (!inStr)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
len = strlen(inStr);
|
||||
outStr = Malloc(len + 1); /* For the null terminator */
|
||||
if (!outStr)
|
||||
|
|
|
@ -197,3 +197,4 @@ TelodendriaPrintHeader(void)
|
|||
"Documentation/Support: https://telodendria.io");
|
||||
Log(LOG_INFO, "");
|
||||
}
|
||||
|
||||
|
|
|
@ -784,6 +784,10 @@ UserDecodePrivilege(const char *p)
|
|||
{
|
||||
return USER_GRANT_PRIVILEGES;
|
||||
}
|
||||
else if (strcmp(p, "PROC_CONTROL") == 0)
|
||||
{
|
||||
return USER_PROC_CONTROL;
|
||||
}
|
||||
else
|
||||
{
|
||||
return USER_NONE;
|
||||
|
@ -816,6 +820,7 @@ UserEncodePrivileges(int privileges)
|
|||
A(USER_ISSUE_TOKENS, "ISSUE_TOKENS");
|
||||
A(USER_CONFIG, "CONFIG");
|
||||
A(USER_GRANT_PRIVILEGES, "GRANT_PRIVILEGES");
|
||||
A(USER_PROC_CONTROL, "PROC_CONTROL");
|
||||
|
||||
#undef A
|
||||
|
||||
|
|
|
@ -25,9 +25,9 @@
|
|||
#ifndef TELODENDRIA_CONFIG_H
|
||||
#define TELODENDRIA_CONFIG_H
|
||||
|
||||
#include <Log.h>
|
||||
#include <HashMap.h>
|
||||
#include <Array.h>
|
||||
#include <Db.h>
|
||||
|
||||
typedef enum ConfigFlag
|
||||
{
|
||||
|
@ -41,13 +41,15 @@ typedef enum ConfigFlag
|
|||
|
||||
typedef struct Config
|
||||
{
|
||||
Db *db;
|
||||
DbRef *ref;
|
||||
|
||||
char *serverName;
|
||||
char *baseUrl;
|
||||
char *identityServer;
|
||||
|
||||
char *uid;
|
||||
char *gid;
|
||||
char *dataDir;
|
||||
|
||||
unsigned int flags;
|
||||
|
||||
|
@ -57,12 +59,27 @@ typedef struct Config
|
|||
int logLevel;
|
||||
|
||||
Array *servers;
|
||||
|
||||
int ok;
|
||||
char *err;
|
||||
} Config;
|
||||
|
||||
extern Config *
|
||||
Config *
|
||||
ConfigParse(HashMap *);
|
||||
|
||||
extern void
|
||||
void
|
||||
ConfigFree(Config *);
|
||||
|
||||
extern int
|
||||
ConfigExists(Db *);
|
||||
|
||||
extern int
|
||||
ConfigCreateDefault(Db *);
|
||||
|
||||
extern Config *
|
||||
ConfigLock(Db *);
|
||||
|
||||
extern int
|
||||
ConfigUnlock(Config *);
|
||||
|
||||
#endif /* TELODENDRIA_CONFIG_H */
|
||||
|
|
|
@ -35,6 +35,9 @@ typedef struct DbRef DbRef;
|
|||
extern Db *
|
||||
DbOpen(char *, size_t);
|
||||
|
||||
extern void
|
||||
DbMaxCacheSet(Db *, size_t);
|
||||
|
||||
extern void
|
||||
DbClose(Db *);
|
||||
|
||||
|
@ -62,4 +65,7 @@ extern void
|
|||
extern HashMap *
|
||||
DbJson(DbRef *);
|
||||
|
||||
extern int
|
||||
DbJsonSet(DbRef *, HashMap *);
|
||||
|
||||
#endif
|
||||
|
|
|
@ -93,9 +93,16 @@ extern JsonValue *
|
|||
extern void
|
||||
JsonValueFree(JsonValue *);
|
||||
|
||||
extern JsonValue *
|
||||
JsonValueDuplicate(JsonValue *);
|
||||
|
||||
extern HashMap *
|
||||
JsonDuplicate(HashMap *);
|
||||
|
||||
extern void
|
||||
JsonFree(HashMap *);
|
||||
|
||||
|
||||
extern void
|
||||
JsonEncodeString(const char *, Stream *);
|
||||
|
||||
|
|
16
src/include/Main.h
Normal file
16
src/include/Main.h
Normal file
|
@ -0,0 +1,16 @@
|
|||
#ifndef TELODENDRIA_MAIN_H
|
||||
#define TELODENDRIA_MAIN_H
|
||||
|
||||
extern void
|
||||
Restart(void);
|
||||
|
||||
extern void
|
||||
Shutdown(void);
|
||||
|
||||
extern unsigned long
|
||||
Uptime(void);
|
||||
|
||||
extern int
|
||||
main(int argc, char **argv);
|
||||
|
||||
#endif /* TELODENDRIA_MAIN_H */
|
|
@ -70,7 +70,6 @@ typedef enum MatrixError
|
|||
|
||||
typedef struct MatrixHttpHandlerArgs
|
||||
{
|
||||
Config *config;
|
||||
Db *db;
|
||||
HttpRouter *router;
|
||||
} MatrixHttpHandlerArgs;
|
||||
|
|
|
@ -69,6 +69,9 @@ ROUTE(RouteUiaFallback);
|
|||
ROUTE(RouteStaticDefault);
|
||||
ROUTE(RouteStaticLogin);
|
||||
|
||||
ROUTE(RouteProcControl);
|
||||
ROUTE(RouteConfig);
|
||||
|
||||
#undef ROUTE
|
||||
|
||||
#endif
|
||||
|
|
|
@ -32,9 +32,10 @@
|
|||
#define USER_ISSUE_TOKENS (1 << 1)
|
||||
#define USER_CONFIG (1 << 2)
|
||||
#define USER_GRANT_PRIVILEGES (1 << 3)
|
||||
#define USER_PROC_CONTROL (1 << 4)
|
||||
|
||||
#define USER_NONE 0
|
||||
#define USER_ALL ((1 << 4) - 1)
|
||||
#define USER_ALL ((1 << 5) - 1)
|
||||
|
||||
typedef struct User User;
|
||||
|
||||
|
|
|
@ -158,10 +158,9 @@ recipe_build() {
|
|||
|
||||
recipe_run() {
|
||||
if [ -f "build/$PROG" ]; then
|
||||
"build/$PROG" -f contrib/development.conf
|
||||
"build/$PROG" -d data
|
||||
|
||||
dataDir=$(< contrib/development.conf json -s "dataDir->@decode")
|
||||
if [ -f "$dataDir/Memory.txt" ]; then
|
||||
if [ -f "data/Memory.txt" ]; then
|
||||
echo "WARNING: Memory.txt exists in the data directory; this means"
|
||||
echo "Telodendria is leaking memory. Please fix memory leaks."
|
||||
fi
|
||||
|
|
|
@ -44,6 +44,7 @@ RESPONSE=$(login_payload | http -X POST -d @- "$BASE/_matrix/client/v3/login")
|
|||
ACCESS_TOKEN=$(echo "$RESPONSE" | json -s "access_token->@decode")
|
||||
|
||||
if [ -z "$ACCESS_TOKEN" ]; then
|
||||
echo "Failed to log in."
|
||||
echo "$RESPONSE" | json
|
||||
exit 1
|
||||
fi
|
||||
|
|
Loading…
Reference in a new issue