diff --git a/TODO.txt b/TODO.txt index a754ab4..d71c644 100644 --- a/TODO.txt +++ b/TODO.txt @@ -44,11 +44,11 @@ Milestone: v0.3.0 [ ] /_telodendria/admin/config endpoint [ ] Refactor TelodendriaConfig to just Config (ConfigLock() and ConfigUnlock()) -[ ] Client-Server API - [ ] 4: Token-based user registration - [ ] Implement user-interactive auth flow - [ ] Token validity endpoint - [ ] Add m.login.registration_token to registration endpoint +[~] Client-Server API + [x] 4: Token-based user registration + [x] Implement user-interactive auth flow + [x] Token validity endpoint + [x] Add m.login.registration_token to registration endpoint flow - Ensure that registration tokens can be used even if registration is disabled. diff --git a/src/RegToken.c b/src/RegToken.c new file mode 100644 index 0000000..0e95d21 --- /dev/null +++ b/src/RegToken.c @@ -0,0 +1,251 @@ +/* + * 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 + +#include +#include + +#include +#include +#include +#include + +int +RegTokenValid(RegTokenInfo * token) +{ + HashMap *tokenJson; + int uses, used; + + unsigned long expiration; + + if (!token || !RegTokenExists(token->db, token->name)) + { + return 0; + } + + tokenJson = DbJson(token->ref); + uses = JsonValueAsInteger(HashMapGet(tokenJson, "uses")); + used = JsonValueAsInteger(HashMapGet(tokenJson, "used")); + expiration = JsonValueAsInteger(HashMapGet(tokenJson, "expires_on")); + + return (!expiration || (UtilServerTs() <= expiration)) && + (uses == -1 || used < uses); +} +void +RegTokenUse(RegTokenInfo * token) +{ + HashMap *tokenJson; + + if (!token || !RegTokenExists(token->db, token->name)) + { + return; + } + + if (token->uses >= 0 && token->used >= token->uses) + { + return; + } + + token->used++; + + /* Write the information to the hashmap */ + tokenJson = DbJson(token->ref); + JsonValueFree(HashMapSet(tokenJson, "used", JsonValueInteger(token->used))); +} + +int +RegTokenExists(Db * db, char *token) +{ + if (!token || !db) + { + return 0; + } + return DbExists(db, 3, "tokens", "registration", token); +} + +int +RegTokenDelete(RegTokenInfo * token) +{ + if (!token || !RegTokenClose(token)) + { + return 0; + } + if (!DbDelete(token->db, 3, "tokens", "registration", token->name)) + { + return 0; + } + Free(token->name); + Free(token->owner); + Free(token); + return 1; +} + +RegTokenInfo * +RegTokenGetInfo(Db * db, char *token) +{ + RegTokenInfo *ret; + + DbRef *tokenRef; + HashMap *tokenJson; + + if (!RegTokenExists(db, token)) + { + return NULL; + } + + tokenRef = DbLock(db, 3, "tokens", "registration", token); + if (!tokenRef) + { + return NULL; + } + tokenJson = DbJson(tokenRef); + ret = Malloc(sizeof(RegTokenInfo)); + + ret->db = db; + ret->ref = tokenRef; + + ret->owner = + StrDuplicate(JsonValueAsString(HashMapGet(tokenJson, "created_by"))); + ret->name = StrDuplicate(token); + + ret->expires = + JsonValueAsInteger(HashMapGet(tokenJson, "expires_on")); + ret->created = + JsonValueAsInteger(HashMapGet(tokenJson, "created_on")); + + ret->uses = + JsonValueAsInteger(HashMapGet(tokenJson, "uses")); + ret->used = + JsonValueAsInteger(HashMapGet(tokenJson, "used")); + + return ret; +} + +void +RegTokenFree(RegTokenInfo * tokeninfo) +{ + if (tokeninfo) + { + Free(tokeninfo->name); + Free(tokeninfo->owner); + Free(tokeninfo); + } +} +int +RegTokenClose(RegTokenInfo * tokeninfo) +{ + if (!tokeninfo) + { + return 0; + } + + return DbUnlock(tokeninfo->db, tokeninfo->ref); +} +static int +RegTokenVerify(char *token) +{ + size_t i, size; + char c; + + if (!token) + { + return 0; + } + /* The spec says the following: "The token required for this + * authentication [...] is an opaque string with maximum length of + * 64 characters in the range [A-Za-z0-9._~-]." */ + if ((size = strlen(token)) > 64) + { + return 0; + } + for (i = 0; i < size; i++) + { + c = token[i]; + if (!(isalnum(c) || c == '0' || c == '_' || c == '~' || c == '-')) + { + return 0; + } + } + + return 1; +} + +RegTokenInfo * +RegTokenCreate(Db * db, char *name, char *owner, unsigned long expires, int uses) +{ + RegTokenInfo *ret; + HashMap *tokenJson; + + unsigned long timestamp = UtilServerTs(); + + if (!db || !name || !owner) + { + return NULL; + } + + /* -1 indicates infinite uses; zero and all positive values are a valid + * number of uses; althought zero would be rather useless. Anything less + * than -1 doesn't make sense. */ + if (uses < -1) + { + return NULL; + } + + /* Verify the token */ + if (!RegTokenVerify(name) || (expires > 0 && expires < timestamp)) + { + return NULL; + } + ret = Malloc(sizeof(RegTokenInfo)); + /* Set the token's properties */ + ret->db = db; + ret->ref = DbCreate(db, 3, "tokens", "registration", name); + if (!ret->ref) + { + /* RegToken already exists or some weird fs error */ + Free(ret); + return NULL; + } + ret->name = StrDuplicate(name); + ret->owner = StrDuplicate(owner); + ret->used = 0; + ret->uses = uses; + ret->created = timestamp; + ret->expires = expires; + + /* Write user info to database. */ + tokenJson = DbJson(ret->ref); + HashMapSet(tokenJson, "created_by", + JsonValueString(ret->owner)); + HashMapSet(tokenJson, "created_on", + JsonValueInteger(ret->created)); + HashMapSet(tokenJson, "expires_on", + JsonValueInteger(ret->expires)); + HashMapSet(tokenJson, "used", + JsonValueInteger(ret->used)); + HashMapSet(tokenJson, "uses", + JsonValueInteger(ret->uses)); + + return ret; +} diff --git a/src/Routes/RouteMatrix.c b/src/Routes/RouteMatrix.c index 2e6e92c..7320812 100644 --- a/src/Routes/RouteMatrix.c +++ b/src/Routes/RouteMatrix.c @@ -109,10 +109,50 @@ ROUTE_IMPL(RouteMatrix, args) Free(pathPart); return response; } + else if (MATRIX_PATH_EQUALS(pathPart, "v1")) + { + /* TODO: This *really* does not look good. */ + Free(pathPart); + pathPart = MATRIX_PATH_POP(args->path); + if (MATRIX_PATH_EQUALS(pathPart, "register")) + { + Free(pathPart); + pathPart = MATRIX_PATH_POP(args->path); + if (MATRIX_PATH_EQUALS(pathPart, "m.login.registration_token")) + { + Free(pathPart); + pathPart = MATRIX_PATH_POP(args->path); + if (MATRIX_PATH_EQUALS(pathPart, "validity")) + { + Free(pathPart); + response = RouteTokenValid(args); + } + else + { + Free(pathPart); + HttpResponseStatus(args->context, HTTP_NOT_FOUND); + return MatrixErrorCreate(M_NOT_FOUND); + } + } + else + { + Free(pathPart); + HttpResponseStatus(args->context, HTTP_NOT_FOUND); + return MatrixErrorCreate(M_NOT_FOUND); + } + } + else + { + Free(pathPart); + HttpResponseStatus(args->context, HTTP_NOT_FOUND); + return MatrixErrorCreate(M_NOT_FOUND); + } + } else { Free(pathPart); HttpResponseStatus(args->context, HTTP_NOT_FOUND); return MatrixErrorCreate(M_NOT_FOUND); } + return response; } diff --git a/src/Routes/RouteRegister.c b/src/Routes/RouteRegister.c index 4c7723c..82a4c53 100644 --- a/src/Routes/RouteRegister.c +++ b/src/Routes/RouteRegister.c @@ -33,6 +33,21 @@ #include #include +static Array * +RouteRegisterRegFlow(void) +{ + Array *response = ArrayCreate(); + + if (!response) + { + return NULL; + } + + ArrayAdd(response, UiaStageBuild("m.login.registration_token", NULL)); + + return response; +} + ROUTE_IMPL(RouteRegister, args) { HashMap *request = NULL; @@ -73,13 +88,6 @@ ROUTE_IMPL(RouteRegister, args) return MatrixErrorCreate(M_NOT_JSON); } - if (!(args->matrixArgs->config->flags & TELODENDRIA_REGISTRATION)) - { - HttpResponseStatus(args->context, HTTP_FORBIDDEN); - response = MatrixErrorCreate(M_FORBIDDEN); - goto finish; - } - val = HashMapGet(request, "username"); if (val) { @@ -107,9 +115,12 @@ ROUTE_IMPL(RouteRegister, args) } uiaFlows = ArrayCreate(); - ArrayAdd(uiaFlows, UiaDummyFlow()); + ArrayAdd(uiaFlows, RouteRegisterRegFlow()); - /* TODO: Add registration token flow */ + if (args->matrixArgs->config->flags & TELODENDRIA_REGISTRATION) + { + ArrayAdd(uiaFlows, UiaDummyFlow()); + } uiaResult = UiaComplete(uiaFlows, args->context, args->matrixArgs->db, request, &response, diff --git a/src/Routes/RouteTokenValid.c b/src/Routes/RouteTokenValid.c new file mode 100644 index 0000000..fe0f553 --- /dev/null +++ b/src/Routes/RouteTokenValid.c @@ -0,0 +1,82 @@ +/* + * 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 + +#include + +#include +#include +#include +#include + +ROUTE_IMPL(RouteTokenValid, args) +{ + Db *db = args->matrixArgs->db; + + HashMap *response = NULL; + HashMap *request = NULL; + + RegTokenInfo *info = NULL; + + char *tokenstr; + + if (HttpRequestMethodGet(args->context) != HTTP_GET) + { + HttpResponseStatus(args->context, HTTP_BAD_REQUEST); + return MatrixErrorCreate(M_UNRECOGNIZED); + } + request = JsonDecode(HttpStream(args->context)); + if (!request) + { + HttpResponseStatus(args->context, HTTP_BAD_REQUEST); + return MatrixErrorCreate(M_NOT_JSON); + } + + tokenstr = JsonValueAsString(HashMapGet(request, "token")); + if (!tokenstr || !RegTokenExists(db, tokenstr)) + { + response = HashMapCreate(); + JsonFree(request); + HashMapSet(response, "valid", JsonValueBoolean(0)); + + return response; + } + info = RegTokenGetInfo(db, tokenstr); + response = HashMapCreate(); + if (!RegTokenValid(info)) + { + JsonFree(request); + RegTokenClose(info); + RegTokenFree(info); + HashMapSet(response, "valid", JsonValueBoolean(0)); + + return response; + } + + RegTokenClose(info); + RegTokenFree(info); + HashMapSet(response, "valid", JsonValueBoolean(1)); + JsonFree(request); + return response; +} diff --git a/src/Uia.c b/src/Uia.c index 7989489..e0e460a 100644 --- a/src/Uia.c +++ b/src/Uia.c @@ -25,6 +25,7 @@ #include +#include #include #include #include @@ -390,7 +391,30 @@ UiaComplete(Array * flows, HttpServerContext * context, Db * db, } else if (strcmp(authType, "m.login.registration_token") == 0) { - /* TODO */ + RegTokenInfo *tokenInfo; + + char *token = JsonValueAsString(HashMapGet(auth, "token")); + + if (!RegTokenExists(db, token)) + { + HttpResponseStatus(context, HTTP_UNAUTHORIZED); + ret = BuildResponse(flows, db, response, session, dbRef); + goto finish; + } + tokenInfo = RegTokenGetInfo(db, token); + if (!RegTokenValid(tokenInfo)) + { + RegTokenClose(tokenInfo); + RegTokenFree(tokenInfo); + + HttpResponseStatus(context, HTTP_UNAUTHORIZED); + ret = BuildResponse(flows, db, response, session, dbRef); + goto finish; + } + /* Use the token, and then close it. */ + RegTokenUse(tokenInfo); + RegTokenClose(tokenInfo); + RegTokenFree(tokenInfo); } else if (strcmp(authType, "m.login.recaptcha") == 0) { diff --git a/src/include/RegToken.h b/src/include/RegToken.h new file mode 100644 index 0000000..504e961 --- /dev/null +++ b/src/include/RegToken.h @@ -0,0 +1,70 @@ +/* + * 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. + */ +#ifndef TELODENDRIA_REGTOKEN_H +#define TELODENDRIA_REGTOKEN_H + +#include + +typedef struct RegTokenInfo +{ + Db *db; + DbRef *ref; + + char *name; + char *owner; + + int used; + int uses; + + unsigned long created; + unsigned long expires; + +} RegTokenInfo; + + +extern void + RegTokenUse(RegTokenInfo *); + +extern int + RegTokenExists(Db *, char *); + +extern int + RegTokenDelete(RegTokenInfo *); + +extern RegTokenInfo * + RegTokenGetInfo(Db *, char *); + +extern RegTokenInfo * + RegTokenCreate(Db *, char *, char *, unsigned long, int); + +extern void + RegTokenFree(RegTokenInfo *); + +extern int + RegTokenValid(RegTokenInfo *); + +extern int + RegTokenClose(RegTokenInfo *); + +#endif diff --git a/src/include/Routes.h b/src/include/Routes.h index fb22c33..eb4791a 100644 --- a/src/include/Routes.h +++ b/src/include/Routes.h @@ -67,6 +67,8 @@ ROUTE(RouteRegister); /* /_matrix/client/(r0|v3)/register */ ROUTE(RouteRefresh); /* /_matrix/client/(r0|v3)/refresh */ ROUTE(RouteWhoami); /* /_matrix/client/(r0|v3)/whoami */ +ROUTE(RouteTokenValid); /* /_matrix/client/v1/register/m.login.registration_token/validity */ + #undef ROUTE #endif