diff --git a/Schema/RoomCreateRequest.json b/Schema/RoomCreateRequest.json index 2f853ff..2c31135 100644 --- a/Schema/RoomCreateRequest.json +++ b/Schema/RoomCreateRequest.json @@ -48,6 +48,9 @@ "room_alias_name": { "type": "string" }, + "room_id": { + "type": "string" + }, "preset": { "type": "RoomCreatePreset" } diff --git a/src/Main.c b/src/Main.c index 6778fc2..c5256cb 100644 --- a/src/Main.c +++ b/src/Main.c @@ -77,13 +77,13 @@ SignalHandler(int signal) return; } - UserNotifyAll(matrixArgs.db); for (i = 0; i < ArraySize(httpServers); i++) { HttpServer *server = ArrayGet(httpServers, i); /* TODO: Notify all users, so that the server doesn't have to * await for a sync reply. */ + UserNotifyAll(matrixArgs.db); HttpServerStop(server); } break; diff --git a/src/Room.c b/src/Room.c index 8a1dc2a..e8670ca 100644 --- a/src/Room.c +++ b/src/Room.c @@ -34,7 +34,6 @@ #include #include #include -#include #include #include @@ -54,13 +53,15 @@ #include "Room/internal.h" static char * -GenerateRoomId(ServerPart s) +GenerateRoomId(RoomCreateRequest *req, ServerPart s) { CommonID cid; char *string; cid.sigil = '!'; - cid.local = StrRandom(32); + cid.local = (req && req->room_id) ? + StrDuplicate(req->room_id) : + StrRandom(32); cid.server = s; string = ParserRecomposeCommonID(cid); Free(cid.local); @@ -91,7 +92,7 @@ RoomCreate(Db * db, User *user, RoomCreateRequest * req, ServerPart s) room->db = db; room->creator.hostname = s.hostname ? StrDuplicate(s.hostname) : NULL; room->creator.port = s.port ? StrDuplicate(s.port) : NULL; - room->id = GenerateRoomId(s); + room->id = GenerateRoomId(req, s); room->version = version_num; room->state_ref = DbCreate(db, 3, "rooms", room->id, "state"); @@ -277,8 +278,39 @@ CreateSafeID(char *unsafe_id) return safe_id; } +HashMap * +RoomEventFetchRaw(Room *room, char *id) +{ + char *safe_id; + HashMap *ret; + DbRef *event_ref; + + if (!room || !id) + { + return NULL; + } + + /* Let's try to locally find that event in our junk. */ + safe_id = CreateSafeID(id); + event_ref = DbLockIntent( + room->db, DB_HINT_READONLY, + 4, "rooms", room->id, "events", safe_id + ); + if (!event_ref) + { + /* TODO: Fetch from another homeserver if possible. */ + ret = NULL; + goto finish; + } + ret = JsonDuplicate(JsonValueAsObject(HashMapGet(DbJson(event_ref), "pdu"))); + + DbUnlock(room->db, event_ref); +finish: + Free(safe_id); + return ret; +} HashMap * -RoomEventFetch(Room *room, char *id) +RoomEventFetch(Room *room, char *id, bool prev) { char *safe_id, *redactor; HashMap *ret, *unsign; @@ -292,7 +324,10 @@ RoomEventFetch(Room *room, char *id) /* Let's try to locally find that event in our junk. */ safe_id = CreateSafeID(id); - event_ref = DbLock(room->db, 4, "rooms", room->id, "events", safe_id); + event_ref = DbLockIntent( + room->db, DB_HINT_READONLY, + 4, "rooms", room->id, "events", safe_id + ); if (!event_ref) { /* TODO: Fetch from another homeserver if possible. */ @@ -302,7 +337,6 @@ RoomEventFetch(Room *room, char *id) ret = JsonDuplicate(JsonValueAsObject(HashMapGet(DbJson(event_ref), "pdu"))); unsign = JsonValueAsObject(HashMapGet(ret, "unsigned")); - /* Overwrite a few unsigned properties on the fly. */ JsonValueFree(HashMapSet( unsign, @@ -321,12 +355,34 @@ RoomEventFetch(Room *room, char *id) JsonValueInteger(UtilTsMillis() - ts) )); - redactor = JsonValueAsString(HashMapGet(DbJson(event_ref), "redacted_by")); - JsonValueFree(HashMapSet( - unsign, - "redacted_because", - JsonValueObject(RoomEventFetch(room, redactor)) - )); + if (prev) + { + redactor = + JsonValueAsString(HashMapGet(DbJson(event_ref), "redacted_by")); + JsonValueFree(HashMapSet( + unsign, + "redacted_because", + JsonValueObject(RoomEventFetch(room, redactor, false)) + )); + } + + if (prev && HashMapGet(ret, "state_key")) + { + char *prev_ev; + HashMap *prev_obj; + JsonValue *prev_content; + prev_ev = + JsonValueAsString(HashMapGet(DbJson(event_ref), "prev_event")); + prev_obj = RoomEventFetch(room, prev_ev, false); + prev_content = HashMapGet(prev_obj, "content"); + JsonValueFree(HashMapSet( + unsign, + "prev_content", + JsonValueDuplicate(prev_content) + )); + + JsonFree(prev_obj); + } DbUnlock(room->db, event_ref); finish: @@ -562,7 +618,7 @@ RoomCanJoin(Room *room, char *user) /* Check join_rules */ joinRule = RoomEventFetch( room, - StateGet(state, "m.room.join_rules", "") + StateGet(state, "m.room.join_rules", ""), false ); joinRuleV = JsonValueAsString(JsonGet(joinRule, 2, "content", "join_rule")); @@ -807,6 +863,7 @@ RoomEventClientify(HashMap *pdu) CopyField("state_key",false); CopyField("redacts", false); + CopyField("unsigned", false); return event; diff --git a/src/Room/State.c b/src/Room/State.c index c34f58f..84f275b 100644 --- a/src/Room/State.c +++ b/src/Room/State.c @@ -15,7 +15,7 @@ RoomStateGetID(Room * room, char *event_id) return NULL; } - event = RoomEventFetch(room, event_id); + event = RoomEventFetch(room, event_id, false); if (!event) { return NULL; @@ -53,7 +53,7 @@ GetCurrentMembership(Room *room, State *s, char *mxid) } event_id = StateGet(state, "m.room.member", mxid); - event = RoomEventFetch(room, event_id); + event = RoomEventFetchRaw(room, event_id); if (!s) { @@ -94,7 +94,7 @@ RoomIsEventVisible(Room *room, User *user, char *eventId) state = RoomStateGetID(room, eventId); - entryV = RoomEventFetch(room, + entryV = RoomEventFetchRaw(room, StateGet(state, "m.room.history_visibility", "") ); if (!(visibility = JsonValueAsString( @@ -148,7 +148,7 @@ RoomIsEventVisible(Room *room, User *user, char *eventId) { \ goto finish; \ } \ - n = RoomEventFetch(room, id_##n); \ + n = RoomEventFetchRaw(room, id_##n); \ if (!n) \ { \ goto finish; \ diff --git a/src/Room/V1/Auth/Auth.c b/src/Room/V1/Auth/Auth.c index 5a23eae..dbbe658 100644 --- a/src/Room/V1/Auth/Auth.c +++ b/src/Room/V1/Auth/Auth.c @@ -48,12 +48,14 @@ RoomAuthoriseEventV1(Room * room, PduV1 pdu, State *state, char **errp) int64_t pdu_pl = RoomUserPL(room, state, pdu.sender); int64_t event_pl = RoomMinPL(room,state, "events", pdu.type); /* Step 1: If m.room.create */ + if (StrEquals(pdu.type, "m.room.create")) { return AuthoriseCreateV1(pdu, errp); } - /* Step 2: Considering the event's auth_events. */ + /* Step 2: Considering the event's auth_events. + * TODO: This is slow. */ if (!ConsiderAuthEventsV1(room, pdu, errp)) { return false; @@ -63,6 +65,7 @@ RoomAuthoriseEventV1(Room * room, PduV1 pdu, State *state, char **errp) * has the property m.federate set to false, and the sender domain of * the event does not match the sender domain of the create event, * reject. + * TODO: This is slow. */ create_event_id = StateGet(state, "m.room.create", ""); if (!state || !create_event_id) @@ -75,7 +78,7 @@ RoomAuthoriseEventV1(Room * room, PduV1 pdu, State *state, char **errp) } return false; } - create_event = RoomEventFetch(room, create_event_id); + create_event = RoomEventFetchRaw(room, create_event_id); federate = JsonGet(create_event, 2, "content", "m.federate"); if (JsonValueType(federate) == JSON_BOOLEAN) { @@ -157,6 +160,20 @@ RoomAuthoriseEventV1(Room * room, PduV1 pdu, State *state, char **errp) return true; } + if (StrEquals(create_event_id, pdu.redacts)) + { + /* This is _not_ spec behaviour. As such, I _need_ to document + * this oddity. Apparently, up until v11 came along, a user could + * redact a creation event. Freely. Without complaining. Of course, + * this breaks the room. + * + * As such, I have made the room to NOT redact creation events. + * Diverges that could be caused by this is, in my opinion low, + * and all other servers that completely listen to the spec will + * be broken. */ + return false; + } + /* Step 11.2: If the domain of the event_id of the event being * redacted is the same as the domain of the event_id of the * m.room.redaction, allow. */ diff --git a/src/Room/V1/Auth/AuthEvents.c b/src/Room/V1/Auth/AuthEvents.c index ddc8b3a..4df1ee2 100644 --- a/src/Room/V1/Auth/AuthEvents.c +++ b/src/Room/V1/Auth/AuthEvents.c @@ -94,7 +94,7 @@ ConsiderAuthEventsV1(Room * room, PduV1 pdu, char **errp) for (i = 0; i < ArraySize(pdu.auth_events); i++) { char *event_id = JsonValueAsString(ArrayGet(pdu.auth_events, i)); - HashMap *event = RoomEventFetch(room, event_id); + HashMap *event = RoomEventFetchRaw(room, event_id); PduV1 auth_pdu = { 0 }; char *key_type_id; diff --git a/src/Room/V1/Auth/Member.c b/src/Room/V1/Auth/Member.c index 23499f8..ec2ca0c 100644 --- a/src/Room/V1/Auth/Member.c +++ b/src/Room/V1/Auth/Member.c @@ -79,7 +79,7 @@ AuthorizeInviteMembershipV1(Room * room, PduV1 pdu, State *state, char **errp) } return false; } - third_pi_event = RoomEventFetch(room, third_pi_id); + third_pi_event = RoomEventFetchRaw(room, third_pi_id); /* Step 5.3.1.6: If sender does not match sender of the * m.room.third_party_invite, reject. */ @@ -256,7 +256,7 @@ AuthorizeJoinMembershipV1(Room * room, PduV1 pdu, State *state, char **errp) { /* Interperet prev properly, as a list of JsonObjects. */ char *prev_id = JsonValueAsString(ArrayGet(prev, 0)); - HashMap *prev_event = RoomEventFetch(room, prev_id); + HashMap *prev_event = RoomEventFetchRaw(room, prev_id); bool flag = false; if (prev_event) diff --git a/src/Room/V1/Auth/PL.c b/src/Room/V1/Auth/PL.c index e530f21..3ab310b 100644 --- a/src/Room/V1/Auth/PL.c +++ b/src/Room/V1/Auth/PL.c @@ -71,7 +71,7 @@ AuthorisePowerLevelsV1(Room * room, PduV1 pdu, State *state) /* Step 10.3: For the properties users_default, events_default, * state_default, ban, redact, kick, invite, check if they were * added, changed or removed. For each found alteration: */ - prev_plevent = RoomEventFetch(room, prev_pl_id); + prev_plevent = RoomEventFetchRaw(room, prev_pl_id); #define CheckChange(prop) do \ { \ JsonValue *old = \ diff --git a/src/Room/V1/Send.c b/src/Room/V1/Send.c index dce44a7..2bf6f4b 100644 --- a/src/Room/V1/Send.c +++ b/src/Room/V1/Send.c @@ -122,7 +122,7 @@ RedactPDU1(HashMap *obj) if (!StrEquals(key, "event_id") && !StrEquals(key, "type") && !StrEquals(key, "room_id") && !StrEquals(key, "sender") && !StrEquals(key, "state_key")&& !StrEquals(key, "content") && - !StrEquals(key, "hashes") && !StrEquals(key, "signature") && + !StrEquals(key, "hashes") && !StrEquals(key, "signatures") && !StrEquals(key, "depth") && !StrEquals(key, "prev_events") && !StrEquals(key, "auth_events")&& !StrEquals(key, "origin") && !StrEquals(key, "unsigned") && @@ -204,8 +204,9 @@ RoomAddEventV1(Room *room, PduV1 pdu, PduV1Status status) DbRef *event_ref; Array *prev_events = NULL, *leaves = NULL; HashMap *leaves_json = NULL, *pdu_json = NULL; + State *prev_state = NULL; JsonValue *leaves_val; - char *safe_id; + char *safe_id, *prev_id; size_t i; if (!room || room->version >= 3 || @@ -218,6 +219,9 @@ RoomAddEventV1(Room *room, PduV1 pdu, PduV1Status status) safe_id = CreateSafeID(pdu.event_id); event_ref = DbCreate(room->db, 4, "rooms", room->id, "events", safe_id); pdu_json = PduV1ToJson(&pdu); + prev_state = StateResolve(room, pdu_json); + prev_id = StrDuplicate(StateGet(prev_state, pdu.type, pdu.state_key)); + StateFree(prev_state); /* TODO: Use those values concretely. */ pdu._unsigned.pdu_status = status; @@ -228,6 +232,11 @@ RoomAddEventV1(Room *room, PduV1 pdu, PduV1Status status) JsonValueString(PduV1StatusToStr(status)), 1, "status" ); + JsonSet( + DbJson(event_ref), + JsonValueString(prev_id), + 1, "prev_event" + ); JsonSet( DbJson(event_ref), JsonValueArray(ArrayCreate()), @@ -236,6 +245,7 @@ RoomAddEventV1(Room *room, PduV1 pdu, PduV1Status status) DbUnlock(room->db, event_ref); Free(safe_id); + Free(prev_id); /* Only accepted PDUs get to do the news */ if (status == PDUV1_STATUS_ACCEPTED) @@ -317,13 +327,20 @@ RoomAddEventV1(Room *room, PduV1 pdu, PduV1Status status) JsonValueAsObject(HashMapGet(pdu.content, "m.relates_to")); Relation rel = { 0 }; State *state; - char *type, *state_key, *event_id; + char *type, *state_key, *event_id, *errp = NULL; pdu_json = PduV1ToJson(&pdu); /* If we have a membership change, then add it to the * proper table. */ - if (relates_to && RelationFromJson(relates_to, &rel, NULL)) + { + CommonID *id = UserIdParse(pdu.sender, NULL); + User *user = UserLockID(room->db, id); + UserPushEvent(user, pdu_json); + UserUnlock(user); + UserIdFree(id); + } + if (relates_to && RelationFromJson(relates_to, &rel, &errp)) { DbRef *relate = DbLock( room->db, @@ -392,7 +409,7 @@ RoomAddEventV1(Room *room, PduV1 pdu, PduV1Status status) ); JsonValueFree(HashMapSet( DbJson(eventRef), - "redacted_by", JsonValueString(pdu.redacts) + "redacted_by", JsonValueString(pdu.event_id) )); DbUnlock(room->db, eventRef); } diff --git a/src/Routes.c b/src/Routes.c index 1e98522..1e05c27 100644 --- a/src/Routes.c +++ b/src/Routes.c @@ -76,6 +76,7 @@ RouterBuild(void) R("/_matrix/client/v3/user/(.*)/filter", RouteFilter); R("/_matrix/client/v3/user/(.*)/filter/(.*)", RouteFilter); + R("/_matrix/client/v3/user/(.*)/account_data/(.*)", RouteLocalData); R("/_matrix/client/v3/rooms/(.*)/send/(.*)/(.*)", RouteSendEvent); R("/_matrix/client/v3/rooms/(.*)/redact/(.*)/(.*)", RouteRedact); @@ -101,6 +102,7 @@ RouterBuild(void) /* Spoofed endpoints, to be TODO'd */ R("/_matrix/client/v3/keys/(query|upload)", RouteKeyQuery); R("/_matrix/client/v3/pushrules", RoutePushrules); + R("/_matrix/federation/v1/hierarchy/(.*)", RouteHierarchy); /* Telodendria Admin API Routes */ diff --git a/src/Routes/RouteAliasDirectory.c b/src/Routes/RouteAliasDirectory.c index 2a93b1c..8a72492 100644 --- a/src/Routes/RouteAliasDirectory.c +++ b/src/Routes/RouteAliasDirectory.c @@ -84,12 +84,12 @@ ROUTE_IMPL(RouteAliasDirectory, path, argp) switch (HttpRequestMethodGet(args->context)) { case HTTP_GET: - val = JsonGet(aliases, 2, "alias", alias); + val = JsonGet(aliases, 2, "aliases", alias); if (val) { response = HashMapCreate(); HashMapSet(response, "room_id", JsonValueDuplicate(HashMapGet(JsonValueAsObject(val), "id"))); - HashMapSet(response, "servers", JsonValueDuplicate(JsonGet(aliases, 3, "alias", alias, "servers"))); + HashMapSet(response, "servers", JsonValueDuplicate(JsonGet(aliases, 3, "aliases", alias, "servers"))); } else { diff --git a/src/Routes/RouteCreateRoom.c b/src/Routes/RouteCreateRoom.c index beacfba..05de071 100644 --- a/src/Routes/RouteCreateRoom.c +++ b/src/Routes/RouteCreateRoom.c @@ -99,6 +99,13 @@ ROUTE_IMPL(RouteCreateRoom, path, argp) response = MatrixErrorCreate(M_BAD_JSON, err); goto finish; } + if (parsed.room_id && !(UserGetPrivileges(user) & USER_ALIAS)) + { + err = "Custom room ID used without ALIAS privileges."; + HttpResponseStatus(args->context, HTTP_UNAUTHORIZED); + response = MatrixErrorCreate(M_UNAUTHORIZED, err); + goto finish; + } /* No longer need this now that it is parsed */ JsonFree(request); diff --git a/src/Routes/RouteFetchEvent.c b/src/Routes/RouteFetchEvent.c index 1f49ed8..6b3d51b 100644 --- a/src/Routes/RouteFetchEvent.c +++ b/src/Routes/RouteFetchEvent.c @@ -106,7 +106,7 @@ ROUTE_IMPL(RouteFetchEvent, path, argp) response = MatrixErrorCreate(M_FORBIDDEN, err); goto finish; } - event = RoomEventFetch(room, eventId); + event = RoomEventFetch(room, eventId, true); response = RoomEventClientify(event); JsonFree(event); diff --git a/src/Routes/RouteHierarchy.c b/src/Routes/RouteHierarchy.c new file mode 100644 index 0000000..a5d5f5b --- /dev/null +++ b/src/Routes/RouteHierarchy.c @@ -0,0 +1,84 @@ +/* + * 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 +#include +#include + +#include + + +ROUTE_IMPL(RouteHierarchy, path, argp) +{ + RouteArgs *args = argp; + Db *db = args->matrixArgs->db; + + HashMap *request = NULL; + HashMap *response = NULL; + + User *user = NULL; + char *token = NULL; + + char *err; + + if (HttpRequestMethodGet(args->context) != HTTP_GET) + { + err = "Unknown request method."; + HttpResponseStatus(args->context, HTTP_BAD_REQUEST); + response = MatrixErrorCreate(M_UNRECOGNIZED, err); + 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; + } + + response = HashMapCreate(); + HashMapSet(response, "children", JsonValueArray(ArrayCreate())); + HashMapSet(response, "inaccessible_children", JsonValueArray(ArrayCreate())); + HashMapSet(response, "room", JsonValueObject(HashMapCreate())); + (void) path; +finish: + JsonFree(request); + UserUnlock(user); + return response; +} diff --git a/src/Routes/RouteKeyManagement.c b/src/Routes/RouteKeyManagement.c new file mode 100644 index 0000000..c095204 --- /dev/null +++ b/src/Routes/RouteKeyManagement.c @@ -0,0 +1,88 @@ +/* + * 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 +#include +#include + +#include + + +ROUTE_IMPL(RouteKeyQuery, path, argp) +{ + RouteArgs *args = argp; + Db *db = args->matrixArgs->db; + + HashMap *request = NULL; + HashMap *response = NULL; + + User *user = NULL; + char *token = NULL; + + char *err; + + if (HttpRequestMethodGet(args->context) != HTTP_POST) + { + err = "Unknown request method."; + HttpResponseStatus(args->context, HTTP_BAD_REQUEST); + response = MatrixErrorCreate(M_UNRECOGNIZED, err); + 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; + } + request = JsonDecode(HttpServerStream(args->context)); + if (!request) + { + HttpResponseStatus(args->context, HTTP_BAD_REQUEST); + response = MatrixErrorCreate(M_NOT_JSON, NULL); + goto finish; + } + + response = HashMapCreate(); + (void) path; +finish: + JsonFree(request); + UserUnlock(user); + return response; +} diff --git a/src/Routes/RouteLocalData.c b/src/Routes/RouteLocalData.c new file mode 100644 index 0000000..9bd3bbb --- /dev/null +++ b/src/Routes/RouteLocalData.c @@ -0,0 +1,158 @@ +/* + * 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 +#include + +#include + +ROUTE_IMPL(RouteLocalData, path, argp) +{ + RouteArgs *args = argp; + Db *db = args->matrixArgs->db; + + HashMap *request = NULL; + HashMap *response = NULL; + + User *user = NULL; + CommonID *id = NULL; + char *token = NULL; + + char *serverName = NULL; + + char *userParam = ArrayGet(path, 0); + char *dataKey = ArrayGet(path, 1); + + char *msg; + + if (!userParam || !dataKey) + { + /* Should be impossible */ + HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR); + return MatrixErrorCreate(M_UNKNOWN, NULL); + } + + serverName = ConfigGetServerName(db); + if (!serverName) + { + HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR); + response = MatrixErrorCreate(M_UNKNOWN, NULL); + goto finish; + } + + id = UserIdParse(userParam, serverName); + if (!id) + { + msg = "Invalid user ID."; + HttpResponseStatus(args->context, HTTP_BAD_REQUEST); + response = MatrixErrorCreate(M_INVALID_PARAM, msg); + goto finish; + } + + if (!ParserServerNameEquals(id->server, serverName)) + { + msg = "Cannot use /filter for non-local users."; + HttpResponseStatus(args->context, HTTP_UNAUTHORIZED); + response = MatrixErrorCreate(M_UNAUTHORIZED, 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; + } + + if (!StrEquals(id->local, UserGetName(user))) + { + msg = "Unauthorized to use /account_data."; + HttpResponseStatus(args->context, HTTP_UNAUTHORIZED); + response = MatrixErrorCreate(M_INVALID_PARAM, msg); + goto finish; + } + + switch (HttpRequestMethodGet(args->context)) + { + case HTTP_GET: + { + HashMap *accountData = UserGetAccountData(user, dataKey); + if (!accountData) + { + msg = "Couldn't find account data."; + HttpResponseStatus(args->context, HTTP_NOT_FOUND); + response = MatrixErrorCreate(M_NOT_FOUND, msg); + goto finish; + + } + response = accountData; + }; break; + case HTTP_PUT: + { + if (StrEquals(dataKey, "m.fully_read")) + { + msg = "Cannot use protected data key."; + HttpResponseStatus(args->context, HTTP_BAD_REQUEST); + response = MatrixErrorCreate(M_BAD_JSON, msg); + goto finish; + } + request = JsonDecode(HttpServerStream(args->context)); + if (!request) + { + HttpResponseStatus(args->context, HTTP_BAD_REQUEST); + response = MatrixErrorCreate(M_NOT_JSON, NULL); + goto finish; + } + UserSetAccountData(user, dataKey, request); + response = HashMapCreate(); + }; break; + default: + HttpResponseStatus(args->context, HTTP_BAD_REQUEST); + response = MatrixErrorCreate(M_UNRECOGNIZED, NULL); + break; + } + +finish: + Free(serverName); + UserIdFree(id); + UserUnlock(user); + JsonFree(request); + return response; +} diff --git a/src/Routes/RouteMembers.c b/src/Routes/RouteMembers.c index 6cfb471..4e3e470 100644 --- a/src/Routes/RouteMembers.c +++ b/src/Routes/RouteMembers.c @@ -146,7 +146,7 @@ ROUTE_IMPL(RouteMembers, path, argp) continue; } - if (IsAllowed((event = RoomEventFetch(room, entry)), params)) + if (IsAllowed((event = RoomEventFetch(room, entry, true)), params)) { ArrayAdd(batch, JsonValueObject(event)); event = NULL; diff --git a/src/Routes/RoutePushRules.c b/src/Routes/RoutePushRules.c index d6a474d..61217f9 100644 --- a/src/Routes/RoutePushRules.c +++ b/src/Routes/RoutePushRules.c @@ -77,53 +77,3 @@ finish: UserUnlock(user); return response; } - -ROUTE_IMPL(RouteKeyQuery, path, argp) -{ - RouteArgs *args = argp; - Db *db = args->matrixArgs->db; - - HashMap *request = NULL; - HashMap *response = NULL; - - User *user = NULL; - char *token = NULL; - - char *err; - - if (HttpRequestMethodGet(args->context) != HTTP_POST) - { - err = "Unknown request method."; - HttpResponseStatus(args->context, HTTP_BAD_REQUEST); - response = MatrixErrorCreate(M_UNRECOGNIZED, err); - 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; - } - request = JsonDecode(HttpServerStream(args->context)); - if (!request) - { - HttpResponseStatus(args->context, HTTP_BAD_REQUEST); - response = MatrixErrorCreate(M_NOT_JSON, NULL); - goto finish; - } - - response = HashMapCreate(); - (void) path; -finish: - JsonFree(request); - UserUnlock(user); - return response; -} diff --git a/src/Routes/RouteSendEvent.c b/src/Routes/RouteSendEvent.c index 28a6c30..e515d21 100644 --- a/src/Routes/RouteSendEvent.c +++ b/src/Routes/RouteSendEvent.c @@ -225,7 +225,9 @@ ROUTE_IMPL(RouteSendState, path, argp) } state = StateCurrent(room); - event = RoomEventFetch(room, StateGet(state, eventType, stateKey)); + event = RoomEventFetch( + room, StateGet(state, eventType, stateKey), true + ); if (!event) { err = "Event could not be found."; @@ -271,6 +273,7 @@ ROUTE_IMPL(RouteSendState, path, argp) if (!filled) { + Log(LOG_ERR, "%s", err); HttpResponseStatus(args->context, HTTP_UNAUTHORIZED); response = MatrixErrorCreate(M_FORBIDDEN, err); goto finish; diff --git a/src/Routes/RouteStaticResources.c b/src/Routes/RouteStaticResources.c index b89dafe..8432bc4 100644 --- a/src/Routes/RouteStaticResources.c +++ b/src/Routes/RouteStaticResources.c @@ -69,7 +69,7 @@ ROUTE_IMPL(RouteStaticResources, path, argp) "function jsonRequest(meth, url, json, cb) {" " var xhr = new XMLHttpRequest();" " xhr.open(meth, url);" - " xhr.setRequestHeader('Content-Type', 'application/json');" + " xhr.setRequestHeader('Content-Type', 'application/json');" " xhr.onreadystatechange = () => {" " if (xhr.readyState == 4) {" " cb(xhr);" diff --git a/src/Routes/RouteSync.c b/src/Routes/RouteSync.c index c38a937..0434991 100644 --- a/src/Routes/RouteSync.c +++ b/src/Routes/RouteSync.c @@ -70,6 +70,7 @@ ROUTE_IMPL(RouteSync, path, argp) SyncResponse sync = { 0 }; Filter *filterData = NULL; + Array *accountData; Array *invites; Array *joins; size_t i; @@ -149,6 +150,20 @@ ROUTE_IMPL(RouteSync, path, argp) * a hashmap of unknown keys pointing to a known type. */ sync.rooms.invite = HashMapCreate(); sync.rooms.join = HashMapCreate(); + sync.account_data.events = ArrayCreate(); + + /* account data */ + accountData = UserGetAccountDataSync(user, currBatch); + for (i = 0; i < ArraySize(accountData); i++) + { + char *key = ArrayGet(accountData, i); + Event *event = Malloc(sizeof(*event)); + event->type = StrDuplicate(key); + event->content = UserGetAccountData(user, key); + + ArrayAdd(sync.account_data.events, event); + } + UserFreeList(accountData); /* invites */ invites = UserGetInvites(user, currBatch); @@ -201,7 +216,7 @@ ROUTE_IMPL(RouteSync, path, argp) for (j = 0; j < ArraySize(el); j++) { char *event = ArrayGet(el, j); - HashMap *eventObj = RoomEventFetch(r, event); + HashMap *eventObj = RoomEventFetch(r, event, true); HashMap *filteredObj = FilterApply(filterData, eventObj); if (filteredObj) @@ -229,7 +244,7 @@ ROUTE_IMPL(RouteSync, path, argp) joined->state.events = ArrayCreate(); while (StateIterate(state, &type, &key, (void **) &id)) { - HashMap *e = RoomEventFetch(r, id); + HashMap *e = RoomEventFetch(r, id, true); StrippedStateEvent rs = StripStateEventSync(e); StrippedStateEvent *s = Malloc(sizeof(*s)); memcpy(s, &rs, sizeof(*s)); @@ -251,9 +266,9 @@ ROUTE_IMPL(RouteSync, path, argp) if (prevBatch) { + /* TODO: Should we be dropping syncs? */ UserDropSync(user, prevBatch); nextBatch = UserInitSyncDiff(user); - UserFillSyncDiff(user, nextBatch); } sync.next_batch = nextBatch; response = SyncResponseToJson(&sync); diff --git a/src/State.c b/src/State.c index b7a49cd..6c1bf6d 100644 --- a/src/State.c +++ b/src/State.c @@ -153,7 +153,7 @@ BuildBaseAndConflictV1(Room *room, Array *states, State *R, HashMap *conflicts) { arr = ArrayCreate(); } - hm = RoomEventFetch(room, event_id); + hm = RoomEventFetch(room, event_id, false); ArrayAdd(arr, hm); HashMapSet(conflicts, tuple, arr); } @@ -367,6 +367,10 @@ StateResolve(Room * room, HashMap * event) db = RoomGetDB(room); room_id = JsonValueAsString(HashMapGet(event, "room_id")); event_id = JsonValueAsString(HashMapGet(event, "event_id")); + if (!room_id || !event_id) + { + return NULL; + } if (DbExists(db, 4, "rooms", room_id, "state", event_id)) { DbRef *ref = DbLock(db, 4, @@ -393,8 +397,10 @@ StateResolve(Room * room, HashMap * event) for (i = 0; i < ArraySize(prevEvents); i++) { - HashMap *prevEvent = - RoomEventFetch(room, JsonValueAsString(ArrayGet(prevEvents, i))); + HashMap *prevEvent = RoomEventFetch( + room, JsonValueAsString(ArrayGet(prevEvents, i)), + false + ); State *state = StateResolve(room, prevEvent); if (HashMapGet(prevEvent, "state_key") && !IsRejected(prevEvent)) diff --git a/src/User.c b/src/User.c index f1eab86..75a20fd 100644 --- a/src/User.c +++ b/src/User.c @@ -1249,6 +1249,7 @@ UserInitSyncDiff(User *user) HashMapSet(data, "nextBatch", JsonValueString(nextBatch)); HashMapSet(data, "invites", JsonValueArray(ArrayCreate())); + HashMapSet(data, "account_data", JsonValueObject(HashMapCreate())); HashMapSet(data, "leaves", JsonValueArray(ArrayCreate())); HashMapSet(data, "joins", JsonValueObject(HashMapCreate())); @@ -1288,6 +1289,45 @@ UserPushInviteSync(User *user, char *roomId) UserNotifyUser(user->name); } void +UserPushAccountData(User *user, char *key) +{ + DbRef *syncRef; + HashMap *data; + Array *entries; + HashMap *join; + size_t i; + if (!user || !key) + { + return; + } + + entries = DbList(user->db, 3, "users", user->name, "sync"); + for (i = 0; i < ArraySize(entries); i++) + { + char *entry = ArrayGet(entries, i); + HashMap *accountEntry; + syncRef = DbLock(user->db, 4, "users", user->name, "sync", entry); + data = DbJson(syncRef); + join = JsonValueAsObject(HashMapGet(data, "account_data")); + + /* TODO */ + + if (!HashMapGet(join, key)) + { + accountEntry = HashMapCreate(); + JsonValueFree(HashMapSet(join, + key, JsonValueObject(accountEntry) + )); + Log(LOG_INFO, "user=%s's batch=%s", user->name, entry); + } + + DbUnlock(user->db, syncRef); + } + DbListFree(entries); + Log(LOG_INFO, "You have notified the '%s' about key=%s", user->name, key); + UserNotifyUser(user->name); +} +void UserPushJoinSync(User *user, char *roomId) { DbRef *syncRef; @@ -1413,7 +1453,7 @@ UserGetInvites(User *user, char *batch) void UserFillSyncDiff(User *user, char *batch) { - Array *joins; + Array *joins, *data; size_t i; DbRef *syncRef; if (!user || !batch) @@ -1421,6 +1461,8 @@ UserFillSyncDiff(User *user, char *batch) return; } + Log(LOG_WARNING, "This is an initial sync."); + joins = UserListJoins(user); syncRef = DbLock( user->db, 4, "users", user->name, "sync", batch @@ -1459,9 +1501,58 @@ UserFillSyncDiff(User *user, char *batch) } } UserFreeList(joins); + + data = DbList(user->db, 3, "users", user->name, "data"); + for (i = 0; i < ArraySize(data); i++) + { + char *id = ArrayGet(data, i); + DbRef *ref = DbLock(user->db, 4, "users", user->name, "data", id); + char *key = JsonValueAsString(HashMapGet(DbJson(ref), "key")); + HashMap *obj = DbJson(syncRef); + HashMap *aData = JsonValueAsObject(HashMapGet(obj, "account_data")); + + JsonValueFree(HashMapSet(aData, + key, JsonValueObject(HashMapCreate()) + )); + + DbUnlock(user->db, ref); + } + DbListFree(data); DbUnlock(user->db, syncRef); } Array * +UserGetAccountDataSync(User *user, char *batch) +{ + Db *db; + DbRef *syncRef; + HashMap *data; + Array *keys; + size_t i; + if (!user || !batch) + { + return NULL; + } + + db = user->db; + syncRef = DbLock(db, 4, "users", user->name, "sync", batch); + if (!syncRef) + { + return NULL; + } + + data = DbJson(syncRef); + + keys = HashMapKeys(JsonValueAsObject(HashMapGet(data, "account_data"))); + for (i = 0; i < ArraySize(keys); i++) + { + char *str = ArrayGet(keys, i); + ArraySet(keys, i, StrDuplicate(str)); + } + + DbUnlock(db, syncRef); + return keys; +} +Array * UserGetJoins(User *user, char *batch) { Db *db; @@ -1647,7 +1738,7 @@ UserFetchMessages(User *user, int n, char *token, char **next) for (i = 0; i < (size_t) n && ArraySize(nexts); i++) { char *curr = ArrayDelete(nexts, ArraySize(nexts) - 1); - HashMap *event = RoomEventFetch(room, curr); + HashMap *event = RoomEventFetch(room, curr, true); Array *prevEvents; size_t j; bool toFree = true; @@ -1772,7 +1863,7 @@ UserIsSyncOld(User *user, char *token) dt = UtilTsMillis() - JsonValueAsInteger(HashMapGet(map, "creation")); DbUnlock(user->db, ref); - return dt > (5 * 60 * 1000); /* 5-minutes of timeout. */ + return dt > (3 * 24 * 60 * 60 * 1000); /* Three days of timeout. */ } bool UserSyncExists(User *user, char *sync) @@ -2014,3 +2105,56 @@ UserNotifyAll(Db *db) } DbListFree(list); } +HashMap * +UserGetAccountData(User *user, char *key) +{ + DbRef *ref; + HashMap *ret; + if (!user || !key) + { + return NULL; + } + + ref = DbLockIntent(user->db, DB_HINT_READONLY, + 4, "users", user->name, "data", key + ); + if (!ref) + { + return NULL; + } + + ret = JsonDuplicate(JsonValueAsObject(HashMapGet(DbJson(ref), "content"))); + DbUnlock(user->db, ref); + + return ret; +} + +void +UserSetAccountData(User *user, char *key, HashMap *obj) +{ + DbRef *ref; + if (!user || !key || !obj) + { + return; + } + + ref = DbLock(user->db, + 4, "users", user->name, "data", key + ); + if (!ref) + { + ref = DbCreate(user->db, + 4, "users", user->name, "data", key + ); + } + if (!ref) + { + return; + } + + JsonValueFree(HashMapSet(DbJson(ref), "content", JsonValueObject(JsonDuplicate(obj)))); + JsonValueFree(HashMapSet(DbJson(ref), "key", JsonValueString(key))); + DbUnlock(user->db, ref); + + UserPushAccountData(user, key); +} diff --git a/src/include/Room.h b/src/include/Room.h index 96321b7..7d7e53b 100644 --- a/src/include/Room.h +++ b/src/include/Room.h @@ -154,9 +154,18 @@ extern bool RoomIsEventVisible(Room *, User *, char *); * Fetch a single event's PDU in a room into an * hashmap, given an event ID, from the database * if possible, or otherwise fetched from a remote - * homeserver participating in the room. + * homeserver participating in the room. If the boolean + * value is set, then the prev_content is set. */ -extern HashMap * RoomEventFetch(Room *, char *); +extern HashMap * RoomEventFetch(Room *, char *, bool); + +/** + * Behaves like + * .Fn RoomEventFetch , + * but avoids doing any postprocessing. + */ +extern HashMap * RoomEventFetchRaw(Room *, char *); + /** * Strips all the fields not required in a diff --git a/src/include/Routes.h b/src/include/Routes.h index 5546917..24fe785 100644 --- a/src/include/Routes.h +++ b/src/include/Routes.h @@ -95,6 +95,7 @@ ROUTE(RouteStaticLogin); ROUTE(RouteStaticResources); ROUTE(RouteFilter); +ROUTE(RouteLocalData); ROUTE(RouteProcControl); ROUTE(RouteConfig); @@ -122,6 +123,7 @@ ROUTE(RouteAdminTokens); ROUTE(RouteKeyQuery); ROUTE(RoutePushrules); +ROUTE(RouteHierarchy); #undef ROUTE #endif diff --git a/src/include/User.h b/src/include/User.h index a7650b3..27007f7 100644 --- a/src/include/User.h +++ b/src/include/User.h @@ -67,7 +67,7 @@ typedef enum UserPrivileges USER_PROC_CONTROL = (1 << 4), USER_ALIAS = (1 << 5), USER_APPSERVICE = (1 << 6), - USER_ALL = ((1 << 7) - 1) + USER_ALL = ((1 << 7) - 1), } UserPrivileges; /** @@ -397,6 +397,12 @@ extern void UserPushJoinSync(User *, char *); */ extern void UserPushEvent(User *, HashMap *); +/** + * Pushes a global account data key into every diff table + * of a user. + */ +extern void UserPushAccountData(User *, char *); + /** * Shows the invite list(as a room ID table) for an user * and a specified diff table, to be freed by @@ -404,6 +410,13 @@ extern void UserPushEvent(User *, HashMap *); */ extern Array * UserGetInvites(User *, char *); +/** + * Shows the global account data list for an user + * and a specified diff table, to be freed by + * .Fn UserFreeList . + */ +extern Array * UserGetAccountDataSync(User *, char *); + /** * Shows a list of rooms for an user and a specified diff @@ -412,6 +425,7 @@ extern Array * UserGetInvites(User *, char *); */ extern Array * UserGetJoins(User *, char *); + /** * Get a list of event IDs for a diff table(and room ID), * to be freed by @@ -497,4 +511,15 @@ extern bool UserAwaitNotification(char *, int); * .Fn UserInitialisePushTable . */ extern void UserDestroyPushTable(void); + +/** + * Returns a user's account data by key into a JSON object living + * on the heap. + */ +extern HashMap * UserGetAccountData(User *, char *); + +/** + * Replaces an account data entry. + */ +extern void UserSetAccountData(User *, char *, HashMap *); #endif /* TELODENDRIA_USER_H */