From 54420f003639f1e077a79e529069fe3989061477 Mon Sep 17 00:00:00 2001 From: Jordan Bancino Date: Thu, 11 Jan 2024 19:33:50 -0500 Subject: [PATCH] Improvements to #44: Implement #22 and #9 (#51) This pull request makes a very small commit on top of #44. Closes #44. Closes #9. Closes #22. Co-authored-by: LoaD Accumulator Co-authored-by: lda Co-authored-by: lda Reviewed-on: https://git.telodendria.io/Telodendria/Telodendria/pulls/51 --- docs/CHANGELOG.md | 6 + docs/user/admin/privileges.md | 8 +- src/Parser.c | 503 +++++++++++++++++++++++++++++++ src/Routes/RouteAliasDirectory.c | 106 ++++++- src/Routes/RouteFilter.c | 6 +- src/Routes/RouteLogin.c | 8 +- src/Routes/RouteRoomAliases.c | 65 +++- src/Routes/RouteUserProfile.c | 12 +- src/Routes/RouteVersions.c | 7 +- src/Routes/RouteWhoami.c | 1 - src/Uia.c | 7 +- src/User.c | 40 +-- src/include/Parser.h | 106 +++++++ src/include/User.h | 19 +- 14 files changed, 817 insertions(+), 77 deletions(-) create mode 100644 src/Parser.c create mode 100644 src/include/Parser.h diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 8ab1b23..dc84eed 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -41,6 +41,8 @@ will now be maintained separately and have its own releases as well. custom scripts. - Greatly simplified some endpoint code by using Cytoplasm's `j2s` for parsing request bodies. +- Create a parser API for grammars found in Matrix, and refactor the +User API to use it. ### New Features @@ -58,6 +60,10 @@ the ability to change only a subset of the configuration. - **GET** `/_telodendria/admin/tokens/[token]` - **POST** `/_telodendria/admin/tokens` - **DELETE** `/_telodendria/admin/tokens/[token]` + - **GET** `/_matrix/client/v3/directory/room/[alias]` + - **PUT** `/_matrix/client/v3/directory/room/[alias]` + - **DELETE** `/_matrix/client/v3/directory/room/[alias]` + - **GET** `/_matrix/client/v3/rooms/[id]/aliases` ## v0.3.0 diff --git a/docs/user/admin/privileges.md b/docs/user/admin/privileges.md index 9a8da06..b62e8d9 100644 --- a/docs/user/admin/privileges.md +++ b/docs/user/admin/privileges.md @@ -16,10 +16,10 @@ registration tokens. configuration. - **GRANT_PRIVILEGES:** Allows a user to modify his or her own privileges or the privileges of other local users. -- **ALIAS:** Allows a user to modify room aliases created by other -users. By default, users can only manage their own room aliases, but -an administrator may wish to take over an alias or remove an offensive -alias. +- **ALIAS:** Allows a user to modify and see room aliases created by +other users. By default, users can only manage their own room aliases, +but an administrator may wish to take over an alias or remove an +offensive alias. - **PROC_CONTROL:** Allows a user to get statistics on the running process, as well as shutdown and resetart the Telodendria daemon itself. Typically this will pair well with **CONFIG**, because there diff --git a/src/Parser.c b/src/Parser.c new file mode 100644 index 0000000..982fb5c --- /dev/null +++ b/src/Parser.c @@ -0,0 +1,503 @@ +/* + * Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with + * other valuable contributors. See CONTRIBUTORS.txt for the full list. + * + * 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 + + +/* Iterate through a char **. */ +#define Iterate(s) (*(*s)++) + +/* Parse an extended localpart */ +static int +ParseUserLocalpart(char **str, char **out) +{ + char c; + char *start; + size_t length; + + if (!str || !out) + { + return 0; + } + /* An extended localpart contains every ASCII printable character, + * except an ':'. */ + start = *str; + while (isascii((c = Iterate(str))) && c != ':' && c) + { + /* Do nothing */ + } + length = (size_t) (*str - start) - 1; + if (length < 1) + { + *str = start; + return 0; + } + if (c == ':') + { + --(*str); + } + + *out = Malloc(length + 1); + memcpy(*out, start, length); + (*out)[length] = '\0'; + + return 1; +} +/* Parses an IPv4 address. */ +static int +ParseIPv4(char **str, char **out) +{ + /* Be *very* careful with this buffer */ + char buffer[4]; + char *start; + size_t length; + char c; + + int digit = 0; + int digits = 0; + + memset(buffer, 0, sizeof(buffer)); + start = *str; + + /* An IPv4 address is made of 4 blocks between 1-3 digits, like so: + * (1-3)*DIGIT.(1-3)*DIGIT.(1-3)*DIGIT.(1-3)*DIGIT */ + while ((isdigit(c = Iterate(str)) || c == '.') && c && digits < 4) + { + if (isdigit(c)) + { + digit++; + continue; + } + if (digit < 1 || digit > 3) + { + /* Current digit is too long for the spec! */ + *str = start; + return 0; + } + memcpy(buffer, *str - digit - 1, digit); + if (atoi(buffer) > 255) + { + /* Current digit is too large for the spec! */ + *str = start; + return 0; + } + memset(buffer, 0, sizeof(buffer)); + digit = 0; + digits++; /* We have parsed a digit. */ + } + if (c == '.' || digits != 3) + { + *str = start; + return 0; + } + length = (size_t) (*str - start) - 1; + *out = Malloc(length + 1); + memcpy(*out, start, length); + (*str)--; + return 1; +} +static int +IsIPv6Char(char c) +{ + return isxdigit(c) || c == ':' || c == '.'; +} +static int +ParseIPv6(char **str, char **out) +{ + char *start; + size_t length; + char c; + + int filled = 0; + int digit = 0; + int digits = 0; + + start = *str; + length = 0; + + if (Iterate(str) != '[') + { + goto fail; + } + + while ((c = Iterate(str)) && IsIPv6Char(c)) + { + char *ipv4; + if (isxdigit(c)) + { + digit++; + length++; + continue; + } + if (c == ':') + { + if (**str == ':') + { + digit = 0; + if (!filled) + { + filled = 1; + length++; + c = Iterate(str); /* Skip over the character */ + continue; + } + /* RFC3513 says the following: + * > 'The "::" can only appear once in an address.' */ + *str = start; + return 0; + } + if (digit < 1 || digit > 4) + { + goto fail; + } + /* We do not have to check whenever the digit here is valid, + * because it has to be. */ + digit = 0; + digits++; + + length++; + continue; + } + /* The only remaining character being '.', we are probably dealing + * with an IPv4 literal. */ + *str -= digit + 1; + length -= digit + 1; + if (ParseIPv4(str, &ipv4)) + { + length += strlen(ipv4); + Free(ipv4); + c = Iterate(str); + filled = 1; + goto end; + } + } +end: + --(*str); + if (Iterate(str) != ']') + { + goto fail; + } + + length = (size_t) (*str - start); + if (length < 4 || length > 47) + { + goto fail; + } + *out = Malloc(length + 1); + memset(*out, '\0', length + 1); + memcpy(*out, start, length); + + return 1; +fail: + *str = start; + return 0; +} +static int +ParseHostname(char **str, char **out) +{ + char *start; + size_t length = 0; + char c; + + start = *str; + while ((c = Iterate(str)) && + (isalnum(c) || c == '.' || c == '-') && + ++length < 256) + { + /* Do nothing. */ + } + if (length < 1 || length > 255) + { + *str = start; + return 0; + } + length = (size_t) (*str - start) - 1; + *out = Malloc(length + 1); + memcpy(*out, start, length); + (*str)--; + return 1; +} + +static int +ParseServerName(char **str, ServerPart *out) +{ + char c; + char *start; + char *startPort; + size_t chars = 0; + + char *host = NULL; + char *port = NULL; + + if (!str || !out) + { + return 0; + } + + start = *str; + + if (!host) + { + /* If we can parse an IPv4 address, use that. */ + ParseIPv4(str, &host); + } + if (!host) + { + /* If we can parse an IPv6 address, use that. */ + ParseIPv6(str, &host); + } + if (!host) + { + /* If we can parse an hostname, use that. */ + ParseHostname(str, &host); + } + if (!host) + { + /* Can't parse a valid server name. */ + return 0; + } + /* Now, there's only 2 options: a ':', or the end(everything else.) */ + if (**str != ':') + { + /* We're done. */ + out->hostname = host; + out->port = NULL; + return 1; + } + /* TODO: Separate this out */ + startPort = ++(*str); + while(isdigit(c = Iterate(str)) && c && ++chars < 5) + { + /* Do nothing. */ + } + if (chars < 1 || chars > 5) + { + *str = start; + Free(host); + host = NULL; + return 0; + } + + port = Malloc(chars + 1); + memcpy(port, startPort, chars); + port[chars] = '\0'; + if (atol(port) > 65535) + { + Free(port); + Free(host); + *str = start; + return 0; + } + + out->hostname = host; + out->port = port; + + return 1; +} +int +ParseServerPart(char *str, ServerPart *part) +{ + /* This is a wrapper behind the internal ParseServerName. */ + if (!str || !part) + { + return 0; + } + return ParseServerName(&str, part); +} +void +ServerPartFree(ServerPart part) +{ + if (part.hostname) + { + Free(part.hostname); + } + if (part.port) + { + Free(part.port); + } +} + +int +ParseCommonID(char *str, CommonID *id) +{ + char sigil; + + if (!str || !id) + { + return 0; + } + + /* There must at least be 2 chararacters: the sigil and a string.*/ + if (strlen(str) < 2) + { + return 0; + } + + sigil = *str++; + /* Some sigils have the following restriction: + * > MUST NOT exceed 255 bytes (including the # sigil and the domain). + */ + if ((sigil == '#' || sigil == '@') && strlen(str) > 255) + { + return 0; + } + id->sigil = sigil; + id->local = NULL; + id->server.hostname = NULL; + id->server.port = NULL; + + switch (sigil) + { + case '$': + /* For event IDs, it depends on the version, so we're just + * accepting it all. */ + if (!ParseUserLocalpart(&str, &id->local)) + { + return 0; + } + if (*str == ':') + { + (*str)++; + if (!ParseServerName(&str, &id->server)) + { + Free(id->local); + id->local = NULL; + return 0; + } + return 1; + } + break; + case '!': + case '#': /* It seems like the localpart should be the same as the + user's: everything, except ':'. */ + case '@': + if (!ParseUserLocalpart(&str, &id->local)) + { + return 0; + } + if (*str++ != ':') + { + Free(id->local); + id->local = NULL; + return 0; + } + if (!ParseServerName(&str, &id->server)) + { + Free(id->local); + id->local = NULL; + return 0; + } + break; + } + return 1; +} + +void +CommonIDFree(CommonID id) +{ + if (id.local) + { + Free(id.local); + } + ServerPartFree(id.server); +} +int +ValidCommonID(char *str, char sigil) +{ + CommonID id; + int ret; + memset(&id, 0, sizeof(CommonID)); + if (!str) + { + return 0; + } + + ret = ParseCommonID(str, &id) && id.sigil == sigil; + + CommonIDFree(id); + return ret; +} +char * +ParserRecomposeServerPart(ServerPart serverPart) +{ + if (serverPart.hostname && serverPart.port) + { + return StrConcat(3, serverPart.hostname, ":", serverPart.port); + } + if (serverPart.hostname) + { + return StrDuplicate(serverPart.hostname); + } + return NULL; +} +char * +ParserRecomposeCommonID(CommonID id) +{ + char *ret = Malloc(2 * sizeof(char)); + ret[0] = id.sigil; + ret[1] = '\0'; + + if (id.local) + { + char *tmp = StrConcat(2, ret, id.local); + Free(ret); + + ret = tmp; + } + if (id.server.hostname) + { + char *server = ParserRecomposeServerPart(id.server); + char *tmp = StrConcat(4, "@", ret, ":", server); + Free(ret); + Free(server); + + ret = tmp; + } + return ret; +} +int +ParserServerNameEquals(ServerPart serverPart, char *str) +{ + char *idServer; + int ret; + if (!str) + { + return 0; + } + idServer = ParserRecomposeServerPart(serverPart); + + ret = StrEquals(idServer, str); + Free(idServer); + + return ret; +} diff --git a/src/Routes/RouteAliasDirectory.c b/src/Routes/RouteAliasDirectory.c index b1ae042..2a93b1c 100644 --- a/src/Routes/RouteAliasDirectory.c +++ b/src/Routes/RouteAliasDirectory.c @@ -25,8 +25,12 @@ #include +#include #include #include + +#include +#include #include ROUTE_IMPL(RouteAliasDirectory, path, argp) @@ -38,20 +42,40 @@ ROUTE_IMPL(RouteAliasDirectory, path, argp) HashMap *response; Db *db = args->matrixArgs->db; - DbRef *ref; + DbRef *ref = NULL; HashMap *aliases; + HashMap *idObject; JsonValue *val; + Array *arr; char *token; + char *msg; User *user = NULL; - /* TODO: Return HTTP 400 M_INVALID_PARAM if alias is invalid */ + CommonID aliasID; + Config config; + + aliasID.sigil = '\0'; + aliasID.local = NULL; + aliasID.server.hostname = NULL; + aliasID.server.port = NULL; + + ConfigLock(db, &config); + + if (!ParseCommonID(alias, &aliasID) || aliasID.sigil != '#') + { + msg = "Invalid room alias."; + HttpResponseStatus(args->context, HTTP_BAD_REQUEST); + response = MatrixErrorCreate(M_INVALID_PARAM, msg); + goto finish; + } ref = DbLock(db, 1, "aliases"); if (!ref && !(ref = DbCreate(db, 1, "aliases"))) { + msg = "Unable to access alias database.", HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR); - response = MatrixErrorCreate(M_UNKNOWN, "Unable to access alias database."); + response = MatrixErrorCreate(M_UNKNOWN, msg); goto finish; } @@ -69,8 +93,9 @@ ROUTE_IMPL(RouteAliasDirectory, path, argp) } else { + msg = "There is no mapped room ID for this room alias."; HttpResponseStatus(args->context, HTTP_NOT_FOUND); - response = MatrixErrorCreate(M_NOT_FOUND, "There is no mapped room ID for this room alias."); + response = MatrixErrorCreate(M_NOT_FOUND, msg); } break; case HTTP_PUT: @@ -92,9 +117,21 @@ ROUTE_IMPL(RouteAliasDirectory, path, argp) if (HttpRequestMethodGet(args->context) == HTTP_PUT) { HashMap *newAlias; + char *id; + char *serverPart; - /* TODO: Validate alias domain and make sure it matches - * server name and is well formed. */ + serverPart = ParserRecomposeServerPart(aliasID.server); + if (!StrEquals(serverPart, config.serverName)) + { + msg = "Invalid server name."; + HttpResponseStatus(args->context, HTTP_BAD_REQUEST); + response = MatrixErrorCreate(M_INVALID_PARAM, msg); + + Free(serverPart); + goto finish; + } + + Free(serverPart); if (JsonGet(aliases, 2, "alias", alias)) { @@ -111,40 +148,81 @@ ROUTE_IMPL(RouteAliasDirectory, path, argp) goto finish; } - if (!JsonValueAsString(HashMapGet(request, "room_id"))) + id = JsonValueAsString(HashMapGet(request, "room_id")); + if (!id) { HttpResponseStatus(args->context, HTTP_BAD_REQUEST); response = MatrixErrorCreate(M_BAD_JSON, "Missing or invalid room_id."); goto finish; } - - /* TODO: Validate room ID to make sure it is well - * formed. */ + + if (!ValidCommonID(id, '!')) + { + msg = "Invalid room ID."; + HttpResponseStatus(args->context, HTTP_BAD_REQUEST); + response = MatrixErrorCreate(M_INVALID_PARAM, msg); + goto finish; + } newAlias = HashMapCreate(); HashMapSet(newAlias, "createdBy", JsonValueString(UserGetName(user))); - HashMapSet(newAlias, "id", JsonValueDuplicate(HashMapGet(request, "room_id"))); + HashMapSet(newAlias, "id", JsonValueString(id)); HashMapSet(newAlias, "servers", JsonValueArray(ArrayCreate())); JsonSet(aliases, JsonValueObject(newAlias), 2, "alias", alias); + + if (!(idObject = JsonValueAsObject(JsonGet(aliases, 2, "id", id)))) + { + arr = ArrayCreate(); + idObject = HashMapCreate(); + HashMapSet(idObject, "aliases", JsonValueArray(arr)); + JsonSet(aliases, JsonValueObject(idObject), 2, "id", id); + } + val = HashMapGet(idObject, "aliases"); + arr = JsonValueAsArray(val); + ArrayAdd(arr, JsonValueString(alias)); + } else { - if (!JsonGet(aliases, 2, "alias", alias)) + HashMap *roomAlias; + char *id; + + if (!(val = JsonGet(aliases, 2, "alias", alias))) { HttpResponseStatus(args->context, HTTP_NOT_FOUND); response = MatrixErrorCreate(M_NOT_FOUND, "Room alias not found."); goto finish; } + roomAlias = JsonValueAsObject(val); + id = StrDuplicate(JsonValueAsString(HashMapGet(roomAlias, "id"))); - if (!(UserGetPrivileges(user) & USER_ALIAS) && !StrEquals(UserGetName(user), JsonValueAsString(JsonGet(aliases, 3, "alias", alias, "createdBy")))) + if (!(UserGetPrivileges(user) & USER_ALIAS) && !StrEquals(UserGetName(user), JsonValueAsString(JsonGet(roomAlias, 1, "createdBy")))) { HttpResponseStatus(args->context, HTTP_UNAUTHORIZED); response = MatrixErrorCreate(M_UNAUTHORIZED, NULL); + Free(id); goto finish; } JsonValueFree(HashMapDelete(JsonValueAsObject(HashMapGet(aliases, "alias")), alias)); + + idObject = JsonValueAsObject(JsonGet(aliases, 2, "id", id)); + if (idObject) + { + size_t i; + val = HashMapGet(idObject, "aliases"); + arr = JsonValueAsArray(val); + for (i = 0; i < ArraySize(arr); i++) + { + if (StrEquals(JsonValueAsString(ArrayGet(arr, i)), alias)) + { + JsonValueFree(ArrayDelete(arr, i)); + break; + } + } + } + Free(id); } response = HashMapCreate(); @@ -156,6 +234,8 @@ ROUTE_IMPL(RouteAliasDirectory, path, argp) } finish: + CommonIDFree(aliasID); + ConfigUnlock(&config); UserUnlock(user); DbUnlock(db, ref); JsonFree(request); diff --git a/src/Routes/RouteFilter.c b/src/Routes/RouteFilter.c index c2ead37..c8f4fa8 100644 --- a/src/Routes/RouteFilter.c +++ b/src/Routes/RouteFilter.c @@ -64,7 +64,7 @@ ROUTE_IMPL(RouteFilter, path, argp) HashMap *response = NULL; User *user = NULL; - UserId *id = NULL; + CommonID *id = NULL; char *token = NULL; char *serverName = NULL; @@ -97,7 +97,7 @@ ROUTE_IMPL(RouteFilter, path, argp) goto finish; } - if (!StrEquals(id->server, serverName)) + if (!ParserServerNameEquals(id->server, serverName)) { msg = "Cannot use /filter for non-local users."; HttpResponseStatus(args->context, HTTP_UNAUTHORIZED); @@ -119,7 +119,7 @@ ROUTE_IMPL(RouteFilter, path, argp) goto finish; } - if (!StrEquals(id->localpart, UserGetName(user))) + if (!StrEquals(id->local, UserGetName(user))) { msg = "Unauthorized to use /filter."; HttpResponseStatus(args->context, HTTP_UNAUTHORIZED); diff --git a/src/Routes/RouteLogin.c b/src/Routes/RouteLogin.c index 158629d..6234f15 100644 --- a/src/Routes/RouteLogin.c +++ b/src/Routes/RouteLogin.c @@ -49,7 +49,7 @@ ROUTE_IMPL(RouteLogin, path, argp) LoginRequest loginRequest; LoginRequestUserIdentifier userIdentifier; - UserId *userId = NULL; + CommonID *userId = NULL; Db *db = args->matrixArgs->db; @@ -160,8 +160,8 @@ ROUTE_IMPL(RouteLogin, path, argp) break; } - if (!StrEquals(userId->server, config.serverName) - || !UserExists(db, userId->localpart)) + if (!ParserServerNameEquals(userId->server, config.serverName) + || !UserExists(db, userId->local)) { msg = "Unknown user ID."; HttpResponseStatus(args->context, HTTP_FORBIDDEN); @@ -175,7 +175,7 @@ ROUTE_IMPL(RouteLogin, path, argp) password = loginRequest.password; refreshToken = loginRequest.refresh_token; - user = UserLock(db, userId->localpart); + user = UserLock(db, userId->local); if (!user) { diff --git a/src/Routes/RouteRoomAliases.c b/src/Routes/RouteRoomAliases.c index 25a080a..2fa97bf 100644 --- a/src/Routes/RouteRoomAliases.c +++ b/src/Routes/RouteRoomAliases.c @@ -25,25 +25,80 @@ #include +#include #include +#include + +#include +#include ROUTE_IMPL(RouteRoomAliases, path, argp) { RouteArgs *args = argp; char *roomId = ArrayGet(path, 0); + char *token; + char *msg; - HashMap *request = NULL; HashMap *response = NULL; + HashMap *aliases = NULL; + HashMap *reversealias = NULL; + + JsonValue *val; Db *db = args->matrixArgs->db; DbRef *ref = NULL; - (void) roomId; + User *user = NULL; - /* TODO: Placeholder; remove. */ - goto finish; + if (HttpRequestMethodGet(args->context) != HTTP_GET) + { + msg = "Route only accepts GET."; + HttpResponseStatus(args->context, HTTP_FORBIDDEN); + response = MatrixErrorCreate(M_FORBIDDEN, msg); + goto finish; + } + + response = MatrixGetAccessToken(args->context, &token); + if (response) + { + goto finish; + } + user = UserAuthenticate(db, token); + if (!user) + { + HttpResponseStatus(args->context, HTTP_UNAUTHORIZED); + response = MatrixErrorCreate(M_UNKNOWN_TOKEN, NULL); + goto finish; + } + + /* TODO: Check whenever the user is in the room or if its world readable + * once this is implemented instead of just checking for the ALIAS + * privilege. */ + if (!(UserGetPrivileges(user) & USER_ALIAS)) + { + msg = "User is not allowed to get this room's aliases."; + HttpResponseStatus(args->context, HTTP_FORBIDDEN); + response = MatrixErrorCreate(M_FORBIDDEN, msg); + goto finish; + } + + ref = DbLock(db, 1, "aliases"); + aliases = DbJson(ref); + reversealias = JsonValueAsObject(JsonGet(aliases, 2, "id", roomId)); + if (!reversealias) + { + /* We do not know about the room ID. */ + msg = "Unknown room ID."; + HttpResponseStatus(args->context, HTTP_BAD_REQUEST); + response = MatrixErrorCreate(M_INVALID_PARAM, msg); + goto finish; + } + + response = HashMapCreate(); + val = JsonGet(reversealias, 1, "aliases"); + HashMapSet(response, "aliases", JsonValueDuplicate(val)); finish: DbUnlock(db, ref); - JsonFree(request); + UserUnlock(user); return response; } diff --git a/src/Routes/RouteUserProfile.c b/src/Routes/RouteUserProfile.c index 5c1af7b..4c438b0 100644 --- a/src/Routes/RouteUserProfile.c +++ b/src/Routes/RouteUserProfile.c @@ -40,7 +40,7 @@ ROUTE_IMPL(RouteUserProfile, path, argp) HashMap *request = NULL; HashMap *response = NULL; - UserId *userId = NULL; + CommonID *userId = NULL; User *user = NULL; char *serverName; @@ -73,7 +73,7 @@ ROUTE_IMPL(RouteUserProfile, path, argp) response = MatrixErrorCreate(M_INVALID_PARAM, msg); goto finish; } - if (strcmp(userId->server, serverName)) + if (!ParserServerNameEquals(userId->server, serverName)) { /* TODO: Implement lookup over federation. */ msg = "User profile endpoint currently doesn't support lookup over " @@ -87,7 +87,7 @@ ROUTE_IMPL(RouteUserProfile, path, argp) switch (HttpRequestMethodGet(args->context)) { case HTTP_GET: - user = UserLock(db, userId->localpart); + user = UserLock(db, userId->local); if (!user) { msg = "Couldn't lock user."; @@ -147,11 +147,11 @@ ROUTE_IMPL(RouteUserProfile, path, argp) StrEquals(entry, "avatar_url")) { /* Check if user has privilege to do that action. */ - if (StrEquals(userId->localpart, UserGetName(user))) + if (StrEquals(userId->local, UserGetName(user))) { value = JsonValueAsString(HashMapGet(request, entry)); - /* TODO: Make UserSetProfile notify other - * parties of the change */ + /* TODO: Make UserSetProfile notify other parties of + * the change */ UserSetProfile(user, entry, value); response = HashMapCreate(); goto finish; diff --git a/src/Routes/RouteVersions.c b/src/Routes/RouteVersions.c index 12dcbb4..08560c3 100644 --- a/src/Routes/RouteVersions.c +++ b/src/Routes/RouteVersions.c @@ -37,9 +37,14 @@ ROUTE_IMPL(RouteVersions, path, argp) (void) argp; #define DECLARE_SPEC_VERSION(x) ArrayAdd(versions, JsonValueString(x)) + DECLARE_SPEC_VERSION("v1.2"); + DECLARE_SPEC_VERSION("v1.3"); + DECLARE_SPEC_VERSION("v1.4"); + DECLARE_SPEC_VERSION("v1.5"); + DECLARE_SPEC_VERSION("v1.6"); + /* The curently supported version. */ DECLARE_SPEC_VERSION("v1.7"); - /* Declare additional spec version support here. */ #undef DECLARE_SPEC_VERSION diff --git a/src/Routes/RouteWhoami.c b/src/Routes/RouteWhoami.c index b98df15..cfd8ee8 100644 --- a/src/Routes/RouteWhoami.c +++ b/src/Routes/RouteWhoami.c @@ -43,7 +43,6 @@ ROUTE_IMPL(RouteWhoami, path, argp) char *token; char *userID; char *deviceID; - char *msg; Config config; diff --git a/src/Uia.c b/src/Uia.c index 2c0abb8..8c83440 100644 --- a/src/Uia.c +++ b/src/Uia.c @@ -351,7 +351,7 @@ UiaComplete(Array * flows, HttpServerContext * context, Db * db, char *password = JsonValueAsString(HashMapGet(auth, "password")); HashMap *identifier = JsonValueAsObject(HashMapGet(auth, "identifier")); char *type; - UserId *userId; + CommonID *userId; User *user; if (!password || !identifier) @@ -366,7 +366,8 @@ UiaComplete(Array * flows, HttpServerContext * context, Db * db, config.serverName); if (!type || !StrEquals(type, "m.id.user") - || !userId || !StrEquals(userId->server, config.serverName)) + || !userId + || !ParserServerNameEquals(userId->server, config.serverName)) { HttpResponseStatus(context, HTTP_UNAUTHORIZED); ret = BuildResponse(flows, db, response, session, dbRef); @@ -374,7 +375,7 @@ UiaComplete(Array * flows, HttpServerContext * context, Db * db, goto finish; } - user = UserLock(db, userId->localpart); + user = UserLock(db, userId->local); if (!user) { HttpResponseStatus(context, HTTP_UNAUTHORIZED); diff --git a/src/User.c b/src/User.c index 9e36239..1fc872d 100644 --- a/src/User.c +++ b/src/User.c @@ -31,6 +31,8 @@ #include #include +#include + #include struct User @@ -882,10 +884,11 @@ finish: return arr; } -UserId * +CommonID * UserIdParse(char *id, char *defaultServer) { - UserId *userId; + CommonID *userId; + char *server; if (!id) { @@ -898,48 +901,38 @@ UserIdParse(char *id, char *defaultServer) return NULL; } - userId = Malloc(sizeof(UserId)); + userId = Malloc(sizeof(CommonID)); if (!userId) { goto finish; } + memset(userId, 0, sizeof(CommonID)); /* Fully-qualified user ID */ if (*id == '@') { - char *localStart = id + 1; - char *serverStart = localStart; - - while (*serverStart != ':' && *serverStart != '\0') + if (!ParseCommonID(id, userId) || !userId->server.hostname) { - serverStart++; - } + UserIdFree(userId); - if (*serverStart == '\0') - { - Free(userId); userId = NULL; goto finish; } - - *serverStart = '\0'; - serverStart++; - - userId->localpart = StrDuplicate(localStart); - userId->server = StrDuplicate(serverStart); } else { /* Treat it as just a localpart */ - userId->localpart = StrDuplicate(id); - userId->server = StrDuplicate(defaultServer); + userId->local = StrDuplicate(id); + ParseServerPart(defaultServer, &userId->server); } - if (!UserHistoricalValidate(userId->localpart, userId->server)) + server = ParserRecomposeServerPart(userId->server); + if (!UserHistoricalValidate(userId->local, server)) { UserIdFree(userId); userId = NULL; } + Free(server); finish: Free(id); @@ -947,12 +940,11 @@ finish: } void -UserIdFree(UserId * id) +UserIdFree(CommonID * id) { if (id) { - Free(id->localpart); - Free(id->server); + CommonIDFree(*id); Free(id); } } diff --git a/src/include/Parser.h b/src/include/Parser.h new file mode 100644 index 0000000..a4cadfa --- /dev/null +++ b/src/include/Parser.h @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with + * other valuable contributors. See CONTRIBUTORS.txt for the full list. + * + * 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_PARSER_H +#define TELODENDRIA_PARSER_H + +/*** + * @Nm Parser + * @Nd Functions for dealing with grammars found in Matrix + * @Dd November 25 2023 + * @Xr User + * + * The + * .Nm + * API provides an interface for parsing grammars within the + * Matrix specification + */ + +/** + * The host[:port] format in a servername. + */ +typedef struct ServerPart { + char *hostname; + char *port; +} ServerPart; +/** + * A common identifier in the form '&local[:server]', where & determines the + * *type* of the identifier. + */ +typedef struct CommonID { + char sigil; + char *local; + ServerPart server; +} CommonID; + +/** + * Parses a common identifier, as per the Common Identifier Format as defined + * by the [matrix] specification. + */ +extern int ParseCommonID(char *, CommonID *); + +/** + * Parses the server part in a common identifier. + */ +extern int ParseServerPart(char *, ServerPart *); + +/** + * Checks whenever the string is a valid common ID with the correct sigil. + */ +extern int ValidCommonID(char *, char); + +/** + * Frees a CommonID's values. Note that it doesn't free the CommonID itself. + */ +extern void CommonIDFree(CommonID); + +/** + * Frees a ServerPart's values. Note that it doesn't free the ServerPart + * itself, and that + * .Fn CommonIDFree + * automatically deals with its server part. + */ +extern void ServerPartFree(ServerPart); + +/** + * Recompose a Common ID into a string which lives in the heap, and must + * therefore be freed with + * .Fn Free . + */ +extern char * ParserRecomposeCommonID(CommonID); + +/** + * Recompose a server part into a string which lives in the heap, and must + * therefore be freed with + * .Fn Free . + */ +extern char * ParserRecomposeServerPart(ServerPart); + +/** + * Compares whenever a ServerName is equivalent to a server name string. + */ +extern int ParserServerNameEquals(ServerPart, char *); + + +#endif /* TELODENDRIA_PARSER_H */ diff --git a/src/include/User.h b/src/include/User.h index 6eb5530..f84ac94 100644 --- a/src/include/User.h +++ b/src/include/User.h @@ -43,6 +43,8 @@ #include #include +#include + /** * Many functions here operate on an opaque user structure. */ @@ -88,15 +90,6 @@ typedef struct UserLoginInfo char *refreshToken; } UserLoginInfo; -/** - * A description of a Matrix user ID. - */ -typedef struct UserId -{ - char *localpart; - char *server; -} UserId; - /** * Take a localpart and domain as separate parameters and validate them * against the rules of the Matrix specification. The reasion the @@ -303,15 +296,15 @@ extern Array *UserEncodePrivileges(int); extern int UserDecodePrivilege(const char *); /** - * Parse either a localpart or a fully qualified Matrix ID. If the + * Parse either a localpart or a fully qualified Matrix common ID. If the * first argument is a localpart, then the second argument is used as * the server name. */ -extern UserId * UserIdParse(char *, char *); +extern CommonID * UserIdParse(char *, char *); /** - * Free the memory associated with the parsed Matrix ID. + * Frees the user's common ID and the memory allocated for it. */ -extern void UserIdFree(UserId *); +extern void UserIdFree(CommonID *); #endif /* TELODENDRIA_USER_H */