[REF] Separate the room functions
Some checks failed
Compile Telodendria / Compile Telodendria (x86, alpine-v3.19) (push) Has been cancelled
Compile Telodendria / Compile Telodendria (x86, debian-v12.4) (push) Has been cancelled
Compile Telodendria / Compile Telodendria (x86, freebsd-v14.0) (push) Has been cancelled
Compile Telodendria / Compile Telodendria (x86, netbsd-v9.3) (push) Has been cancelled
Compile Telodendria / Compile Telodendria (x86_64, alpine-v3.19) (push) Has been cancelled
Compile Telodendria / Compile Telodendria (x86_64, debian-v12.4) (push) Has been cancelled
Compile Telodendria / Compile Telodendria (x86_64, freebsd-v14.0) (push) Has been cancelled
Compile Telodendria / Compile Telodendria (x86_64, netbsd-v9.3) (push) Has been cancelled
Compile Telodendria / Compile Telodendria (x86_64, openbsd-v7.4) (push) Has been cancelled

User.c, you're next.
This commit is contained in:
LDA 2024-08-30 09:42:58 +02:00
parent 5b51d9159e
commit 01de46b299
21 changed files with 2324 additions and 1926 deletions

@ -1 +0,0 @@
Subproject commit 346b912a0633cceac10780b8a103f6c89b5ba89f

4
configure vendored
View file

@ -15,7 +15,7 @@ TOOLS="tools/src"
SCHEMA="Schema"
CYTOPLASM="Cytoplasm"
CFLAGS="-O1 -D_DEFAULT_SOURCE -I${INCLUDE} -I${BUILD}"
CFLAGS="-O1 -D_DEFAULT_SOURCE -I${INCLUDE} -I${SRC} -I${BUILD}"
LIBS="-lm -pthread -lCytoplasm"
@ -155,7 +155,7 @@ print_obj() {
get_deps() {
src="$1"
${CC} -I${INCLUDE} -I${BUILD} $(if [ -n "${CYTOPLASM}" ]; then echo "-I${CYTOPLASM}/include"; fi) -E "$src" \
${CC} -I${SRC} -I${INCLUDE} -I${BUILD} $(if [ -n "${CYTOPLASM}" ]; then echo "-I${CYTOPLASM}/include"; fi) -E "$src" \
| grep '^#' \
| awk '{print $3}' \
| cut -d '"' -f 2 \

View file

@ -1,6 +0,0 @@
all:
sh tools/bin/td
install:
install build/telodendria $(PREFIX)/bin/telodendria
find man -name 'telodendria*\.[1-8]' -exec install {} $(PREFIX)/{} \;

View file

@ -79,6 +79,8 @@ SignalHandler(int signal)
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. */
HttpServerStop(server);
}

1922
src/Room.c

File diff suppressed because it is too large Load diff

73
src/Room/Info.c Normal file
View file

@ -0,0 +1,73 @@
#include "Room/internal.h"
char *
RoomIdGet(Room * room)
{
return room ? room->id : NULL;
}
int
RoomVersionGet(Room * room)
{
return room ? room->version : 0;
}
ServerPart
RoomGetCreator(Room *room)
{
ServerPart ret = { 0 };
if (!room)
{
return ret;
}
return room->creator;
}
Array *
RoomPrevEventsGet(Room *room)
{
HashMap *json;
if (!room)
{
return NULL;
}
json = DbJson(room->leaves_ref);
return JsonValueAsArray(JsonGet(json, 1, "leaves"));
}
/* TODO: This has it's fair share of problems, as a malicious
* homeserver could *easily* fake a PDU depth and put this
* function to the depth limit. */
uint64_t
RoomGetDepth(Room *room)
{
Array *leaves;
HashMap *pdu;
size_t i;
uint64_t max;
if (!room)
{
return 0;
}
leaves = RoomPrevEventsGet(room);
if (!leaves)
{
return 0;
}
max = 0;
for (i = 0; i < ArraySize(leaves); i++)
{
uint64_t depth;
pdu = JsonValueAsObject(ArrayGet(leaves, i));
depth = JsonValueAsInteger(HashMapGet(pdu, "depth"));
if (depth > max)
{
max = depth;
}
}
return max;
}

286
src/Room/Populate.c Normal file
View file

@ -0,0 +1,286 @@
/*
* 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 <Cytoplasm/HashMap.h>
#include <Cytoplasm/Array.h>
#include <Cytoplasm/Json.h>
#include <Parser.h>
#include <Config.h>
#include <User.h>
#include <Room.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Base64.h>
#include <Cytoplasm/Util.h>
#include <Cytoplasm/Str.h>
#include <Cytoplasm/Log.h>
#include <Cytoplasm/Db.h>
#include <Schema/RoomCreateRequest.h>
#include <Schema/PduV1.h>
#include <Schema/PduV3.h>
#include <CanonicalJson.h>
#include <Config.h>
#include <Parser.h>
#include <State.h>
#include <Event.h>
#include <stdlib.h>
#include <string.h>
#include "Room/internal.h"
void
RoomPopulate(Room *room, User *user, RoomCreateRequest *req, ServerPart s)
{
CommonID sender;
char *sender_str, *key, *version;
HashMap *content, *event, *override, *pl_content;
Array *initial_states;
JsonValue *val;
int64_t pl = 100;
size_t i;
char *join_rules_preset = NULL;
char *history_visibility_preset = NULL;
char *guest_access_preset = NULL;
bool trusted_room = false;
sender.sigil = '@';
sender.local = UserGetName(user);
sender.server = s;
sender_str = ParserRecomposeCommonID(sender);
/* m.room.create */
content = HashMapCreate();
if (room->version <= 10)
{
JsonSet(content, JsonValueString(sender_str), 1, "creator");
}
version = req->room_version ?
StrDuplicate(req->room_version) : StrInt(room->version);
JsonSet(content, JsonValueString(version), 1, "room_version");
Free(version);
while (HashMapIterate(req->creation_content, &key, (void **) &val))
{
JsonValue *content_v = HashMapGet(content, key);
if (content_v &&
!StrEquals(key, "creator") &&
!StrEquals(key, "room_version"))
{
continue;
}
JsonValueFree(HashMapSet(content, key, JsonValueDuplicate(val)));
}
event = RoomEventCreate(sender_str, "m.room.create", "", content);
JsonFree(RoomEventSend(room, event, NULL));
JsonFree(event);
UserAddJoin(user, room->id);
/* m.room.member */
content = HashMapCreate();
JsonSet(content, JsonValueString("join"), 1, "membership");
event = RoomEventCreate(sender_str, "m.room.member", sender_str, content);
JsonFree(RoomEventSend(room, event, NULL));
JsonFree(event);
/* m.room.power_levels */
content = HashMapCreate();
JsonSet(
content,
JsonValueInteger(pl),
2, "users", sender_str);
override = req->power_level_content_override;
while (override && HashMapIterate(override, &key, (void **) &val))
{
JsonValue *main = HashMapGet(content, key);
if (main)
{
HashMapDelete(content, key);
JsonValueFree(main);
}
HashMapSet(content, key, JsonValueDuplicate(val));
}
event = RoomEventCreate(sender_str, "m.room.power_levels", "", content);
JsonFree(RoomEventSend(room, event, NULL));
JsonFree(event);
/* Presets */
switch (req->preset)
{
case ROOM_CREATE_PUBLIC:
join_rules_preset = "public";
history_visibility_preset = "shared";
guest_access_preset = "forbidden";
break;
case ROOM_CREATE_TRUSTED:
trusted_room = true;
/* Fallthrough */
case ROOM_CREATE_PRIVATE:
join_rules_preset = "invite";
history_visibility_preset = "shared";
guest_access_preset = "can_join";
break;
default:
switch (req->visibility)
{
case ROOM_PUBLIC:
join_rules_preset = "public";
history_visibility_preset = "shared";
guest_access_preset = "forbidden";
break;
case ROOM_PRIVATE:
join_rules_preset = "invite";
history_visibility_preset = "shared";
guest_access_preset = "can_join";
break;
}
break;
}
/* Write out presets */
#define SetIfExistent(p,a) do { \
if (p##_preset) \
{ \
content = HashMapCreate(); \
JsonSet( \
content, \
JsonValueString(join_rules_preset) \
, 1, a); \
event = RoomEventCreate( \
sender_str, \
"m.room." #p, "", content); \
JsonFree(RoomEventSend(room, event, NULL)); \
JsonFree(event); \
} \
} \
while (0)
SetIfExistent(join_rules, "join_rule");
SetIfExistent(history_visibility, "history_visibility");
SetIfExistent(guest_access, "guest_access");
#undef SetIfExistent
/* User-provided initial states */
initial_states = req->initial_state;
for (i = 0; i < ArraySize(initial_states); i++)
{
RoomStateEvent *rse = ArrayGet(initial_states, i);
HashMap *rseObject = RoomStateEventToJson(rse);
if (!HashMapGet(rseObject, "state_key"))
{
HashMapSet(rseObject, "state_key", JsonValueString(""));
}
HashMapSet(rseObject, "sender", JsonValueString(sender_str));
JsonFree(RoomEventSend(room, rseObject, NULL));
JsonFree(rseObject);
}
/* Name and topic. */
if (req->name)
{
content = HashMapCreate();
JsonSet(content, JsonValueString(req->name), 1, "name");
event = RoomEventCreate(sender_str, "m.room.name", "", content);
JsonFree(RoomEventSend(room, event, NULL));
JsonFree(event);
}
if (req->topic)
{
content = HashMapCreate();
JsonSet(content, JsonValueString(req->topic), 1, "topic");
event = RoomEventCreate(sender_str, "m.room.topic", "", content);
JsonFree(RoomEventSend(room, event, NULL));
JsonFree(event);
}
/* Custom alias */
if (req->room_alias_name && !RoomResolveAlias(room->db, room->id))
{
CommonID full;
char *fullStr, *serverStr;
full.sigil = '#';
full.local = req->room_alias_name;
full.server = s;
fullStr = ParserRecomposeCommonID(full);
serverStr = ParserRecomposeServerPart(room->creator);
RoomAddAlias(room->db, fullStr, room->id, sender_str, serverStr);
content = HashMapCreate();
JsonSet(content, JsonValueString(fullStr), 1, "alias");
event = RoomEventCreate(
sender_str,
"m.room.canonical_alias", "", content);
JsonFree(RoomEventSend(room, event, NULL));
JsonFree(event);
Free(fullStr);
Free(serverStr);
}
/* Invites */
pl_content = HashMapCreate();
JsonSet(pl_content, JsonValueInteger(pl), 2, "users", sender_str);
for (i = 0; i < ArraySize(req->invite); i++)
{
char *user_id = ArrayGet(req->invite, i);
if (!user_id || !ValidCommonID(user_id, '@'))
{
/* TODO: Raise error. */
break;
}
RoomSendInvite(user, req->is_direct, user_id, room);
if (trusted_room)
{
JsonValue *own = JsonValueInteger(pl);
JsonSet(pl_content, own, 2, "users", user_id);
}
}
event = RoomEventCreate(sender_str, "m.room.power_levels", "", pl_content);
JsonFree(RoomEventSend(room, event, NULL));
JsonFree(event);
JsonValueFree(JsonSet(
DbJson(room->leaves_ref),
JsonValueBoolean(req->is_direct), 1, "is_direct"));
/* TODO: The rest of the events mandated by the specification on
* POST /createRoom, and error management. */
Free(sender_str);
/* TODO: Error management (and invite_3pid, later) */
}

282
src/Room/State.c Normal file
View file

@ -0,0 +1,282 @@
#include "Room/internal.h"
#include <Cytoplasm/Memory.h>
#include <Config.h>
#include <State.h>
State *
RoomStateGet(Room * room)
{
HashMap *database_state;
if (!room)
{
return NULL;
}
/* TODO: Consider caching the deserialised result, as doing that on a
* large state would probably eat up a lot of time! */
/* TODO: Update the cached state */
database_state = DbJson(room->state_ref);
return StateDeserialise(database_state);
}
State *
RoomStateGetID(Room * room, char *event_id)
{
HashMap *event;
State *state;
if (!room || !event_id)
{
return NULL;
}
event = RoomEventFetch(room, event_id);
if (!event)
{
return NULL;
}
state = StateResolve(room, event);
JsonFree(event);
return state;
}
static char *
GetCurrentMembership(Room *room, State *s, char *mxid)
{
State *state;
HashMap *event;
char *event_id;
char *ret;
if (!room || !mxid)
{
return NULL;
}
if (s)
{
state = s;
}
else
{
state = StateCurrent(room);
}
event_id = StateGet(state, "m.room.member", mxid);
event = RoomEventFetch(room, event_id);
if (!s)
{
StateFree(state);
}
ret = StrDuplicate(
JsonValueAsString(JsonGet(event, 2, "content", "membership"))
);
JsonFree(event);
return ret;
}
bool
RoomIsEventVisible(Room *room, User *user, char *eventId)
{
State *state;
HashMap *entryV;
char *visibility;
char *membership, *currentMembership;
CommonID cid;
char *server;
char *mxid;
bool ret = false;
if (!room || !user || !eventId)
{
return false;
}
server = ConfigGetServerName(room->db);
cid.sigil = '@';
cid.local = UserGetName(user);
ParseServerPart(server, &cid.server);
mxid = ParserRecomposeCommonID(cid);
Free(server);
state = RoomStateGetID(room, eventId);
entryV = RoomEventFetch(room,
StateGet(state, "m.room.history_visibility", "")
);
if (!(visibility = JsonValueAsString(
JsonGet(entryV, 2, "content", "history_visibility"))))
{
visibility = "shared";
}
visibility = StrDuplicate(visibility);
JsonFree(entryV);
membership = GetCurrentMembership(room, state, mxid);
currentMembership = GetCurrentMembership(room, NULL, mxid);
StateFree(state);
/* TODO: (for shared) "and the user joined the room at _any_
* point after the event was sent, allow."
* Aargh. *Why?* */
if (StrEquals(visibility, "world_readable"))
{
ret = true;
}
else if (StrEquals(membership, "join"))
{
ret = true;
}
else if (StrEquals(visibility, "shared") &&
StrEquals(currentMembership, "join")) /* Check the current
* membership, as a hack */
{
ret = true;
}
else if (StrEquals(visibility, "invited") &&
StrEquals(membership, "invite"))
{
ret = true;
}
Free(mxid);
Free(membership);
Free(visibility);
Free(currentMembership);
ServerPartFree(cid.server);
return ret;
}
#define PrepareState(room, S, type, key, n) \
do \
{ \
id_##n = StateGet(S, type, key); \
if (!id_##n) \
{ \
goto finish; \
} \
n = RoomEventFetch(room, id_##n); \
if (!n) \
{ \
goto finish; \
} \
} \
while (0)
#define FinishState(name) \
if (name) \
{ \
JsonFree(name); \
} \
return ret
bool
RoomIsJoinRule(Room * room, State *state, char *jr)
{
HashMap *joinrule = NULL;
char *id_joinrule = NULL;
bool ret = false;
if (!room || !state || !jr)
{
return false;
}
PrepareState(room, state, "m.room.join_rules", "", joinrule);
ret = StrEquals(
JsonValueAsString(JsonGet(joinrule, 2, "content", "join_rule")),
jr);
finish:
FinishState(joinrule);
}
/* Verifies if user has a specific membership before [e_id] in the room. */
bool
RoomUserHasMembership(Room * room, State *state, char *user, char *mbr)
{
HashMap *membership = NULL;
char *id_membership = NULL;
char *membership_value;
bool ret = false;
if (!room || !state || !user || !mbr)
{
return false;
}
PrepareState(room, state, "m.room.member", user, membership);
membership_value =
JsonValueAsString(JsonGet(membership, 2, "content", "membership"));
ret = StrEquals(membership_value, mbr);
finish:
FinishState(membership);
}
/* Computes the smallest PL needed to do something somewhere */
int64_t
RoomMinPL(Room * room, State *state, char *type, char *act)
{
HashMap *pl = NULL;
JsonValue *val;
char *id_pl;
int64_t ret = 0, def = 0;
if (!room || !state || !act)
{
return 0;
}
PrepareState(room, state, "m.room.power_levels", "", pl);
/* Every other act has a minimum PL of 0 */
def = 0;
if (StrEquals(act, "ban") ||
StrEquals(act, "kick") ||
StrEquals(act, "redact") ||
StrEquals(act, "state_default") ||
(StrEquals(type, "notifications") && StrEquals(act, "room")))
{
def = 50;
}
if (!type)
{
val = JsonGet(pl, 2, "content", act);
}
else
{
val = JsonGet(pl, 3, "content", type, act);
}
ret = ParsePL(val, def);
finish:
FinishState(pl);
}
/* Finds the power level of an user at a state. */
/* TODO: The creator should have PL100 by default. */
int64_t
RoomUserPL(Room * room, State *state, char *user)
{
HashMap *pl = NULL;
char *id_pl;
int64_t ret = 0, def = 0;
if (!room || !state || !user)
{
return 0;
}
PrepareState(room, state, "m.room.power_levels", "", pl);
def = RoomMinPL(room, state, NULL, "users_default");
ret = ParsePL(JsonGet(pl, 3, "content", "users", user), def);
finish:
FinishState(pl);
}

65
src/Room/V1/Auth/Alias.c Normal file
View file

@ -0,0 +1,65 @@
#include "Room/internal.h"
#include <Schema/PduV1.h>
#include <Parser.h>
static bool
VerifyServers(char *id1, char *id2)
{
CommonID cid1;
CommonID cid2;
char *str1;
char *str2;
bool ret = false;
if (!ParseCommonID(id1, &cid1))
{
return false;
}
if (!ParseCommonID(id2, &cid2))
{
return false;
}
str1 = ParserRecomposeServerPart(cid1.server);
str2 = ParserRecomposeServerPart(cid2.server);
if (StrEquals(str1, str2))
{
ret = true;
goto end;
}
end:
Free(str1);
Free(str2);
CommonIDFree(cid1);
CommonIDFree(cid2);
return ret;
}
bool
AuthoriseAliasV1(PduV1 pdu, char **errp)
{
/* Step 4.1: If event has no state_key, reject. */
if (!pdu.state_key || StrEquals(pdu.state_key, ""))
{
if (errp)
{
*errp = "Step 4.1 fail: no state key in the alias";
}
return false;
}
/* Step 4.2: If sender's domain doesn't matches state_key, reject. */
if (!VerifyServers(pdu.state_key, pdu.sender))
{
if (errp)
{
*errp = "Step 4.2 fail: alias domain doesnt match statekey";
}
return false;
}
/* Step 4.3: Otherwise, allow. */
return true;
}

174
src/Room/V1/Auth/Auth.c Normal file
View file

@ -0,0 +1,174 @@
#include "Room/internal.h"
#include "Room/V1/Auth/internal.h"
#include <Schema/PduV1.h>
#include <Parser.h>
static bool
VerifyServers(char *id1, char *id2)
{
CommonID cid1;
CommonID cid2;
char *str1;
char *str2;
bool ret = false;
if (!ParseCommonID(id1, &cid1))
{
return false;
}
if (!ParseCommonID(id2, &cid2))
{
return false;
}
str1 = ParserRecomposeServerPart(cid1.server);
str2 = ParserRecomposeServerPart(cid2.server);
if (StrEquals(str1, str2))
{
ret = true;
goto end;
}
end:
Free(str1);
Free(str2);
CommonIDFree(cid1);
CommonIDFree(cid2);
return ret;
}
bool
RoomAuthoriseEventV1(Room * room, PduV1 pdu, State *state, char **errp)
{
HashMap *create_event;
char *create_event_id;
JsonValue *federate;
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. */
if (!ConsiderAuthEventsV1(room, pdu, errp))
{
return false;
}
/* Step 3: If the content of the m.room.create event in the room state
* 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.
*/
create_event_id = StateGet(state, "m.room.create", "");
if (!state || !create_event_id)
{
/* At this point, [create_event_id] has to exist */
if (errp)
{
*errp = "No creation event in the state. Needless to say, "
"your room is done for.";
}
return false;
}
create_event = RoomEventFetch(room, create_event_id);
federate = JsonGet(create_event, 2, "content", "m.federate");
if (JsonValueType(federate) == JSON_BOOLEAN)
{
if (!JsonValueAsBoolean(federate))
{
char *c_sender =
JsonValueAsString(JsonGet(create_event, 1, "sender"));
char *p_sender = pdu.sender;
if (!VerifyServers(c_sender, p_sender))
{
if (errp)
{
*errp = "Trying to access a room with m.federate off.";
}
return false;
}
}
}
JsonFree(create_event);
/* Step 4: If type is m.room.aliases */
if (StrEquals(pdu.type, "m.room.aliases"))
{
return AuthoriseAliasV1(pdu, errp);
}
/* Step 5: If type is m.room.member */
if (StrEquals(pdu.type, "m.room.member"))
{
return AuthoriseMemberV1(room, pdu, state, errp);
}
/* Step 6: If the sender's current membership state is not join, reject. */
if (!RoomUserHasMembership(room, state, pdu.sender, "join"))
{
return false;
}
/* Step 7: If type is m.room.third_party_invite */
if (StrEquals(pdu.type, "m.room.third_party_invite"))
{
/* Allow if and only if sender's current power level is greater than
* or equal to the invite level */
int64_t min_pl = RoomMinPL(room, state, NULL, "invite");
return pdu_pl >= min_pl;
}
/* Step 8: If the event type's required power level is greater than the
* sender's power level, reject. */
if (event_pl > pdu_pl)
{
return false;
}
/* Step (9): If the event has a state_key that starts with an @ and does
* not match the sender, reject. */
if (pdu.state_key && *pdu.state_key == '@')
{
if (!StrEquals(pdu.state_key, pdu.sender))
{
return false;
}
}
/* Step 10: If type is m.room.power_levels */
if (StrEquals(pdu.type, "m.room.power_levels"))
{
return AuthorisePowerLevelsV1(room, pdu, state);
}
/* Step 11: If type is m.room.redaction */
if (StrEquals(pdu.type, "m.room.redaction"))
{
int64_t min_pl = RoomMinPL(room, state, NULL, "redact");
/* Step 11.1: If the sender's power level is greater than or equal
* to the redact level, allow. */
if (pdu_pl >= min_pl)
{
return true;
}
/* 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. */
if (pdu.redacts && VerifyServers(pdu.redacts, pdu.event_id))
{
return true;
}
/* Step 11.3: Otherwise, reject. */
return false;
}
/* Step 12: Otherwise, allow. */
return true;
}

View file

@ -0,0 +1,183 @@
#include "Room/internal.h"
#include "Room/V1/Auth/internal.h"
#include <Schema/PduV1.h>
#include <Cytoplasm/Log.h>
#include <Parser.h>
static bool
VerifyPDUV1(PduV1 *auth_pdu)
{
/* TODO: The PDU could come from an unknown source, which may lack
* the tools to verify softfailing(or we may not trust them)*/
/* TODO:
* https://spec.matrix.org/v1.7/server-server-api/
* #checks-performed-on-receipt-of-a-pdu */
if (RoomIsRejectedV1(*auth_pdu))
{
Log(LOG_ERR, "Auth PDU has been rejected.");
return false;
}
if (RoomIsSoftfailedV1(*auth_pdu))
{
Log(LOG_ERR, "Auth PDU has been softfailed.");
}
return true; /* This only shows whenever an event was rejected, not
* soft-failed */
}
static bool
ValidAuthEventV1(PduV1 *auth_pdu, PduV1 *pdu)
{
if (IsState(auth_pdu, "m.room.create", ""))
{
return true;
}
if (IsState(auth_pdu, "m.room.power_levels", ""))
{
/* TODO: Check if it's the latest in terms of [pdu] */
return true;
}
if (IsState(auth_pdu, "m.room.member", pdu->sender))
{
/* TODO: Check if it's the latest in terms of [pdu] */
return true;
}
if (StrEquals(pdu->type, "m.room.member"))
{
char *membership =
JsonValueAsString(JsonGet(pdu->content, 1, "membership"));
JsonValue *third_pid =
JsonGet(pdu->content, 1, "third_party_invite");
/* The PDU's state_key is the target. So we check if if the
* auth PDU would count as the target's membership. Was very fun to
* find that out when I wanted to kick my 'yukari' alt. */
if (IsState(auth_pdu, "m.room.member", pdu->state_key))
{
/* TODO: Check if it's the latest in terms of [pdu] */
return true;
}
if ((StrEquals(membership, "join") ||
StrEquals(membership, "invite")) &&
IsState(auth_pdu, "m.room.join_rules",""))
{
/* TODO: Check if it's the latest in terms of [pdu] */
return true;
}
if (StrEquals(membership, "invite") && third_pid)
{
HashMap *tpid = JsonValueAsObject(third_pid);
JsonValue *token =
JsonGet(tpid, 2, "signed", "token");
char *token_str = JsonValueAsString(token);
if (IsState(auth_pdu, "m.room.third_party_invite", token_str))
{
/* TODO: Check if it is the latest. */
return true;
}
}
/* V1 simply doesn't have the concept of restricted rooms,
* so we can safely skip this one for this function. */
}
return false;
}
bool
ConsiderAuthEventsV1(Room * room, PduV1 pdu, char **errp)
{
char *ignored;
size_t i;
bool room_create = false;
HashMap *state_keytype;
state_keytype = HashMapCreate();
for (i = 0; i < ArraySize(pdu.auth_events); i++)
{
char *event_id = JsonValueAsString(ArrayGet(pdu.auth_events, i));
HashMap *event = RoomEventFetch(room, event_id);
PduV1 auth_pdu = { 0 };
char *key_type_id;
if (!PduV1FromJson(event, &auth_pdu, &ignored))
{
JsonFree(event);
HashMapFree(state_keytype);
if (errp)
{
*errp = "Couldn't parse an auth event";
}
return false; /* Yeah... we aren't doing that. */
}
/* TODO: Find a better way to do this. Using HashMaps as sets
* *works*, but it's not the best of ideas here. Also, we're using
* strings to compare things, which yeah. */
key_type_id = StrConcat(3, auth_pdu.type, ",", auth_pdu.state_key);
if (HashMapGet(state_keytype, key_type_id))
{
/* Duplicate found! We actually ignore it's actual value. */
if (errp)
{
*errp = "Duplicate auth event was found";
}
JsonFree(event);
PduV1Free(&auth_pdu);
HashMapFree(state_keytype);
Free(key_type_id);
return false;
}
/* Whenever event is valid or not really doesn't matter, as we're
* not using it's value anywhere. */
HashMapSet(state_keytype, key_type_id, event);
Free(key_type_id);
/* Step 2.2: If there are entries whose type and state_key don't
* match those specified by the auth events selection algorithm
* described in the server specification, reject. */
if (!ValidAuthEventV1(&auth_pdu, &pdu))
{
if (errp)
{
*errp = "Invalid authevent given.";
}
JsonFree(event);
PduV1Free(&auth_pdu);
HashMapFree(state_keytype);
return false;
}
/* Step 2.3: If there are entries which were themselves rejected
* under the checks performed on receipt of a PDU, reject.
* TODO */
if (!VerifyPDUV1(&auth_pdu))
{
if (errp)
{
*errp = "Event depends on rejected PDUs";
}
PduV1Free(&auth_pdu);
JsonFree(event);
HashMapFree(state_keytype);
return false;
}
/* Step 2.4: If there is no m.room.create event among the entries,
* reject. */
if (!room_create && IsState(&auth_pdu, "m.room.create", ""))
{
room_create = true; /* Here, we check for the opposite. */
}
JsonFree(event);
PduV1Free(&auth_pdu);
}
HashMapFree(state_keytype);
if (!room_create && errp)
{
*errp = "Room creation event was not in the PDU's auth events";
}
return room_create; /* Step 2.4 is actually done here. */
}

68
src/Room/V1/Auth/Create.c Normal file
View file

@ -0,0 +1,68 @@
#include "Room/internal.h"
#include "Room/V1/Auth/internal.h"
#include <Schema/PduV1.h>
#include <Parser.h>
bool
AuthoriseCreateV1(PduV1 pdu, char **errp)
{
bool ret = true;
CommonID sender = { 0 }, room_id = { 0 };
char *sender_serv = NULL, *id_serv = NULL;
if (ArraySize(pdu.auth_events) > 0)
{
if (errp)
{
*errp = "Room creation event has auth events";
}
ret = false;
goto finish;
}
if (!ParseCommonID(pdu.room_id, &room_id) ||
!ParseCommonID(pdu.sender, &sender))
{
if (errp)
{
*errp = "Couldn't parse the sender/room ID";
}
ret = false;
goto finish;
}
sender_serv = ParserRecomposeServerPart(sender.server);
id_serv = ParserRecomposeServerPart(room_id.server);
if (!StrEquals(sender_serv, id_serv))
{
if (errp)
{
*errp = "Room is not properly namespaced";
}
ret = false;
goto finish;
}
/* TODO: Check room_version as in step 1.3 */
if (!HashMapGet(pdu.content, "creator"))
{
if (errp)
{
*errp = "Room creation event has auth events";
}
ret = false;
goto finish;
}
finish:
if (sender_serv)
{
Free(sender_serv);
}
if (id_serv)
{
Free(id_serv);
}
CommonIDFree(sender);
CommonIDFree(room_id);
return ret;
}

371
src/Room/V1/Auth/Member.c Normal file
View file

@ -0,0 +1,371 @@
#include "Room/internal.h"
#include <Schema/PduV1.h>
#include <Parser.h>
static bool
AuthorizeInviteMembershipV1(Room * room, PduV1 pdu, State *state, char **errp)
{
int64_t invite_level;
int64_t pdu_level;
JsonValue *third_pi;
/* Step 5.3.1: If content has a third_party_invite property */
if ((third_pi = JsonGet(pdu.content, 1, "third_party_invite")))
{
JsonValue *signed_val, *mxid, *token;
HashMap *third_pi_obj = JsonValueAsObject(third_pi), *signed_obj;
HashMap *third_pi_event;
char *third_pi_id, *thirdpi_event_sender;
/* Step 5.3.1.1: If target user is banned, reject. */
if (RoomUserHasMembership(room, state, pdu.state_key, "ban"))
{
if (errp)
{
*errp = "Step 5.3.1.1 fail: target is banned";
}
return false;
}
/* Step 5.3.1.2: If content.third_party_invite does not have a signed
* property, reject. */
if (!(signed_val = JsonGet(third_pi_obj, 1, "signed")))
{
if (errp)
{
*errp = "Step 5.3.1.2 fail: unsigned 3PII";
}
return false;
}
signed_obj = JsonValueAsObject(signed_val);
/* Step 5.3.1.3: If signed does not have mxid and token properties,
* reject. */
if (!(mxid = JsonGet(signed_obj, 1, "mxid")))
{
if (errp)
{
*errp = "Step 5.3.1.3 fail: no MXID in 3PII";
}
return false;
}
if (!(token = JsonGet(signed_obj, 1, "token")))
{
if (errp)
{
*errp = "Step 5.3.1.3 fail: no token in 3PII";
}
return false;
}
/* Step 5.3.1.4: If mxid does not match state_key, reject. */
if (!StrEquals(JsonValueAsString(mxid), pdu.state_key))
{
if (errp)
{
*errp = "Step 5.3.1.4 fail: 3PII's MXID != state_key";
}
return false;
}
/* Step 5.3.1.5: If there is no m.room.third_party_invite event
* in the current room state with state_key matching token, reject. */
if (!(third_pi_id = StateGet(
state,
"m.room.third_party_invite", JsonValueAsString(token))))
{
if (errp)
{
*errp = "Step 5.3.1.5 fail: no proper 3PII event";
}
return false;
}
third_pi_event = RoomEventFetch(room, third_pi_id);
/* Step 5.3.1.6: If sender does not match sender of the
* m.room.third_party_invite, reject. */
thirdpi_event_sender = JsonValueAsString(JsonGet(third_pi_event, 1, "sender"));
if (!StrEquals(thirdpi_event_sender, pdu.sender))
{
if (errp)
{
*errp = "Step 5.3.1.6 fail: sender does not match 3PII";
}
JsonFree(third_pi_event);
return false;
}
JsonFree(third_pi_event);
/* TODO:
* Step 5.3.1.7: If any signature in signed matches any public key in
* the m.room.third_party_invite event, allow.
*
* The public keys are in content of m.room.third_party_invite as:
* - A single public key in the public_key property.
* - A list of public keys in the public_keys property. */
/* Step 5.3.1.8: Otherwise, reject. */
if (errp)
{
*errp = "Step 5.3.1.8 fail: signature check do not match";
}
return false;
}
/* Step 5.3.2: If the sender's current membership state is not join,
* reject. */
if (!RoomUserHasMembership(room, state, pdu.sender, "join"))
{
if (errp)
{
*errp = "Step 5.3.2 fail: sender is not 'join'ed";
}
return false;
}
/* Step 5.3.3: If target user's current membership state is join or ban, reject. */
if (RoomUserHasMembership(room, state, pdu.state_key, "join") ||
RoomUserHasMembership(room, state, pdu.state_key, "ban"))
{
if (errp)
{
*errp = "Step 5.3.3 fail: target is 'join'|'ban'd";
}
return false;
}
/* Step 5.3.4: If the sender's power level is greater than or equal to the
* invite level, allow. */
invite_level = RoomMinPL(room, state, NULL, "invite");
pdu_level = RoomUserPL(room, state, pdu.sender);
if (pdu_level >= invite_level)
{
return true;
}
/* Step 5.3.5: Otherwise, reject. */
if (errp)
{
*errp = "Step 5.3.5 fail: sender has no permissions to do so";
}
return false;
}
static bool
AuthorizeLeaveMembershipV1(Room * room, PduV1 pdu, State *state, char **errp)
{
int64_t ban_level = RoomMinPL(room, state, NULL, "ban");
int64_t kick_level = RoomMinPL(room, state, NULL, "kick");
int64_t sender_level = RoomUserPL(room, state, pdu.sender);
int64_t target_level = RoomUserPL(room, state, pdu.state_key);
/* Step 5.4.1: If the sender matches state_key, allow if and only if
* that user's current membership state is invite or join. */
if (StrEquals(pdu.sender, pdu.state_key))
{
bool flag =
RoomUserHasMembership(room, state, pdu.sender, "invite") ||
RoomUserHasMembership(room, state, pdu.sender, "join");
if (!flag && errp)
{
*errp = "Step 5.4.1 fail: user tries to leave but is "
"~'invite' AND ~'join'.";
}
return flag;
}
/* Step 5.4.2: If the sender's current membership state is not join,
* reject. */
if (!RoomUserHasMembership(room, state, pdu.sender, "join"))
{
if (errp)
{
*errp = "Step 5.4.2 fail: sender tries to kick but is "
"~'invite'.";
}
return false;
}
/* Step 5.4.3: If the target user's current membership state is ban,
* and the sender's power level is less than the ban level, reject. */
if (RoomUserHasMembership(room, state, pdu.state_key, "ban") &&
sender_level < ban_level)
{
if (errp)
{
*errp = "Step 5.4.3 fail: sender tries to unban but has no "
"permissions to do so.";
}
return false;
}
/* Step 5.4.4: If the sender's power level is greater than or equal to
* the kick level, and the target user's power level is less than the
* sender's power level, allow. */
if ((sender_level >= kick_level) && target_level < sender_level)
{
return true;
}
/* Step 5.4.5: Otherwise, reject. */
if (errp)
{
*errp = "Step 5.4.5 fail: sender tried to kick but has no "
"permissions to do so.";
}
return false;
}
static bool
AuthorizeBanMembershipV1(Room * room, PduV1 pdu, State *state, char **errp)
{
int64_t ban_pl, pdu_pl, target_pl;
/* Step 5.5.1: If the sender's current membership state is not join, reject. */
if (!RoomUserHasMembership(room, state, pdu.sender, "join"))
{
if (errp)
{
*errp = "Step 5.5.1 fail: sender tries to ban but is not "
"'join'ed";
}
return false;
}
/* Step 5.5.2: If the sender's power level is greater than or equal
* to the ban level, and the target user's power level is less than
* the sender's power level, allow. */
ban_pl = RoomMinPL(room, state, NULL, "ban");
pdu_pl = RoomUserPL(room, state, pdu.sender);
target_pl = RoomUserPL(room, state, pdu.state_key);
if ((pdu_pl >= ban_pl) && (target_pl < pdu_pl))
{
return true;
}
/* Step 5.5.3: Otherwise, reject. */
if (errp)
{
*errp = "Step 5.5.3 fail: sender tries to ban has no permissions to "
"do so";
}
return false;
}
static bool
AuthorizeJoinMembershipV1(Room * room, PduV1 pdu, State *state, char **errp)
{
/* Step 5.2.1: If the only previous event is an m.room.create and the
* state_key is the creator, allow. */
Array *prev = pdu.prev_events;
if (ArraySize(prev) == 1)
{
/* Interperet prev properly, as a list of JsonObjects. */
char *prev_id = JsonValueAsString(ArrayGet(prev, 0));
HashMap *prev_event = RoomEventFetch(room, prev_id);
bool flag = false;
if (prev_event)
{
char *type = JsonValueAsString(HashMapGet(prev_event, "type"));
char *sender = JsonValueAsString(HashMapGet(prev_event, "sender"));
if (StrEquals(type, "m.room.create") &&
StrEquals(sender, pdu.state_key))
{
flag = true;
}
}
JsonFree(prev_event);
if (flag)
{
return true;
}
}
/* Step 5.2.2: If the sender does not match state_key, reject. */
if (!StrEquals(pdu.sender, pdu.state_key))
{
if (errp)
{
*errp = "Step 5.2.2 fail: sender does not match statekey "
"on 'join'";
}
return false;
}
/* Step 5.2.3: If the sender is banned, reject. */
if (RoomUserHasMembership(room, state, pdu.sender, "ban"))
{
if (errp)
{
*errp = "Step 5.2.2 fail: sender is banned on 'join'";
}
return false;
}
/* Step 5.2.4: If the join_rule is invite then allow if membership
* state is invite or join. */
if (RoomIsJoinRule(room, state, "invite") &&
(RoomUserHasMembership(room, state, pdu.sender, "invite") ||
RoomUserHasMembership(room, state, pdu.sender, "join")))
{
return true;
}
/* Step 5.2.5: If the join_rule is public, allow. */
if (RoomIsJoinRule(room, state, "public"))
{
return true;
}
/* Step 5.2.6: Otherwise, reject. */
if (errp)
{
*errp = "Step 5.2.6 fail: join_rule does not allow 'join'";
}
return false;
}
bool
AuthoriseMemberV1(Room * room, PduV1 pdu, State *state, char **errp)
{
JsonValue *membership;
char *membership_str;
/* Step 5.1: If there is no state_key property, or no membership
* property in content, reject. */
if (!pdu.state_key ||
StrEquals(pdu.state_key, "") ||
!(membership = JsonGet(pdu.content, 1, "membership")))
{
if (errp)
{
*errp = "Step 5.1 fail: broken membership's statekey/membership";
}
return false;
}
if (JsonValueType(membership) != JSON_STRING)
{
/* Also check for the type */
if (errp)
{
*errp = "Step 5.1 fail: broken membership's membership";
}
return false;
}
membership_str = JsonValueAsString(membership);
#define JumpIfMembership(mem, func) do { \
if (StrEquals(membership_str, mem)) \
{ \
return func(room, pdu, state, errp); \
} \
} while (0)
/* Step 5.2: If membership is join. */
JumpIfMembership("join", AuthorizeJoinMembershipV1);
/* Step 5.3: If membership is invite. */
JumpIfMembership("invite", AuthorizeInviteMembershipV1);
/* Step 5.4: If membership is leave. */
JumpIfMembership("leave", AuthorizeLeaveMembershipV1);
/* Step 5.5: If membership is ban. */
JumpIfMembership("ban", AuthorizeBanMembershipV1);
/* Step 5.6: Otherwise, the membership is unknown. Reject. */
return false;
#undef JumpIfMembership
}

182
src/Room/V1/Auth/PL.c Normal file
View file

@ -0,0 +1,182 @@
#include "Room/internal.h"
#include <Schema/PduV1.h>
#include <Parser.h>
bool
AuthorisePowerLevelsV1(Room * room, PduV1 pdu, State *state)
{
/* Step 10.1: If the users property in content is not an object with
* keys that are valid user IDs with values that are integers
* (or a string that is an integer), reject. */
JsonValue *users = JsonGet(pdu.content, 1, "users");
HashMap *users_o, *prev_plevent;
char *user_id, *prev_pl_id, *ev_type;
JsonValue *power_level, *ev_obj;
bool flag = true;
int64_t userpl = RoomUserPL(room, state, pdu.sender);
HashMap *event_obj;
if (JsonValueType(users) != JSON_OBJECT)
{
return false;
}
users_o = JsonValueAsObject(users);
while (HashMapIterate(users_o, &user_id, (void **) &power_level))
{
CommonID as_cid;
if (!flag)
{
continue;
}
if (!ParseCommonID(user_id, &as_cid))
{
flag = false;
}
if (as_cid.sigil != '@')
{
CommonIDFree(as_cid);
flag = false;
continue;
}
/* Verify powerlevels.
* We'll use INT64_MAX as a sentinel value, as this isn't
* a valid powervalue for the specification. */
if (ParsePL(power_level, INT64_MAX) == INT64_MAX)
{
flag = false;
CommonIDFree(as_cid);
continue;
}
CommonIDFree(as_cid);
}
/* HashMapIterate does not support breaking, so we just set a
* flag to be used. */
if (!flag)
{
return false;
}
/* Step 10.2: If there is no previous m.room.power_levels event
* in the room, allow. */
if (!(prev_pl_id = StateGet(state, "m.room.power_levels", "")))
{
return true;
}
/* 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);
#define CheckChange(prop) do \
{ \
JsonValue *old = \
JsonGet(prev_plevent, 2, "content", prop);\
JsonValue *new = \
JsonGet(pdu.content, 1, prop); \
int64_t oldv = 0, newv = 0; \
oldv = JsonValueAsInteger(old); \
newv = JsonValueAsInteger(new); \
if ((old && !new) || (!old && new) || \
(oldv != newv)) \
{ \
if (old && (oldv > userpl)) \
{ \
return false; \
} \
if (new && (newv > userpl)) \
{ \
return false; \
} \
} \
} \
while(0)
CheckChange("users_default");
CheckChange("events_default");
CheckChange("state_default");
CheckChange("ban");
CheckChange("redact");
CheckChange("kick");
CheckChange("invite");
#undef CheckChange
#define CheckPLOld(prop) \
event_obj = \
JsonValueAsObject(JsonGet(prev_plevent, 2, "content", prop)); \
flag = true; \
while (HashMapIterate(event_obj, &ev_type, (void **) &ev_obj)) \
{ \
JsonValue *new; \
int64_t new_pl, old_pl; \
\
if (!flag) \
{ \
continue; \
} \
\
new = JsonGet(pdu.content, 2, prop, ev_type); \
old_pl = ParsePL(new, INT64_MAX); \
if (((new_pl = ParsePL(new, INT64_MAX)) != INT64_MAX) && \
((new_pl != old_pl) && old_pl > userpl)) \
{ \
flag = false; \
} \
} \
if (!flag) \
{ \
JsonFree(prev_plevent); \
return false; \
} \
flag = true
#define CheckPLNew(prop) \
event_obj = \
JsonValueAsObject(JsonGet(pdu.content, 1, prop)); \
flag = true; \
while (HashMapIterate(event_obj, &ev_type, (void **) &ev_obj)) \
{ \
JsonValue *old; \
int64_t new_pl, old_pl; \
\
if (!flag) \
{ \
continue; \
} \
\
old = JsonGet(prev_plevent, 3, "content", prop, ev_type); \
new_pl = ParsePL(ev_obj, INT64_MAX); \
if (((old_pl = ParsePL(old, INT64_MAX)) != INT64_MAX && \
new_pl != old_pl) && new_pl > userpl) \
{ \
flag = false; \
} \
} \
if (!flag) \
{ \
JsonFree(prev_plevent); \
return false; \
} \
flag = true
/* Step 10.4: For each entry being changed in, or removed from, the
* events property:
* - If the current value is greater than the sender's current
* power level, reject. */
CheckPLOld("events");
/* Step 10.5: For each entry being added to, or changed in, the events
* property:
* - If the new value is greater than the sender's current power level,
* reject. */
CheckPLNew("events");
/* Steps 10.6 and 10.7 are effectively the same. */
CheckPLOld("users");
CheckPLNew("users");
#undef CheckPLOld
#undef CheckPLNew
/* Step 10.8: Otherwise, allow. */
JsonFree(prev_plevent);
return true;
}

View file

@ -0,0 +1,35 @@
#ifndef TELODENDRIA_IROOM_V1_AUTH_H
#define TELODENDRIA_IROOM_V1_AUTH_H
#include "Room/internal.h"
/**
* Verifies if a room creation PDU would be allowed
* by the auth rules in room version 1 (step 1)
*/
extern bool AuthoriseCreateV1(PduV1, char **);
/**
* Verifies if an auth event PDU would be allowed by the
* auth rules in room version 1 (step 2)
*/
extern bool ConsiderAuthEventsV1(Room *, PduV1, char **);
/**
* Verifies if an alias PDU would be authorised in room
* version 1 (step 4)
*/
extern bool AuthoriseAliasV1(PduV1, char **);
/**
* Verifies if a membership PDUv1 would be allowed by the
* auth rules in room version 1 (step 5)
*/
extern bool AuthoriseMemberV1(Room *, PduV1, State *, char **);
/**
* Verifies if a PDU (power levels) would be allowed by
* the auth rules in room version 1 (step 10)
*/
extern bool AuthorisePowerLevelsV1(Room *, PduV1, State *);
#endif

17
src/Room/V1/Info.c Normal file
View file

@ -0,0 +1,17 @@
#include "Room/internal.h"
#include <Schema/PduV1.h>
#include <Parser.h>
bool
RoomIsSoftfailedV1(PduV1 pdu)
{
return pdu._unsigned.pdu_status == PDUV1_STATUS_SOFTFAIL;
}
bool
RoomIsRejectedV1(PduV1 pdu)
{
return pdu._unsigned.pdu_status == PDUV1_STATUS_SOFTFAIL;
}

73
src/Room/V1/Populate.c Normal file
View file

@ -0,0 +1,73 @@
#include "Room/internal.h"
#include <Schema/PduV1.h>
#include <Parser.h>
bool
PopulateEventV1(Room * room, HashMap * event, PduV1 * pdu, ServerPart serv)
{
char *unused;
Array *prev_events;
size_t i;
CommonID cid;
if (PduV1FromJson(event, pdu, &unused))
{
/* TODO: Clean up some fields */
return true;
}
/* Consider the PDU dropped by default */
pdu->_unsigned.pdu_status = PDUV1_STATUS_DROPPED;
/* TODO: Create a PDU of our own, signed and everything.
* https://telodendria.io/blog/matrix-protocol-overview
* has some ideas on how this could be done(up until stage 5). */
pdu->sender =
StrDuplicate(JsonValueAsString(JsonGet(event, 1, "sender")));
pdu->type =
StrDuplicate(JsonValueAsString(JsonGet(event, 1, "type")));
pdu->redacts = NULL;
if (JsonGet(event, 1, "state_key"))
{
pdu->state_key =
StrDuplicate(JsonValueAsString(JsonGet(event, 1, "state_key")));
}
pdu->auth_events = ArrayCreate();
pdu->origin_server_ts = UtilTsMillis();
pdu->content =
JsonDuplicate(JsonValueAsObject(JsonGet(event, 1, "content")));
pdu->room_id = StrDuplicate(room->id);
pdu->signatures = HashMapCreate();
pdu->depth = RoomGetDepth(room) + 1;
pdu->depth = pdu->depth >= INT64_MAX ? INT64_MAX : pdu->depth;
/* Create a random event ID for this PDU.
* TODO: Optionally verify whenever it's already used, but that
* would be unlikely considering the lengths of event IDs. */
cid.sigil = '$';
cid.local = StrRandom(32);
cid.server.hostname = StrDuplicate(serv.hostname);
cid.server.port = serv.port ? StrDuplicate(serv.port) : NULL;
pdu->event_id = ParserRecomposeCommonID(cid);
CommonIDFree(cid);
/* Fill prev_events with actual event data.
* Note that we don't actually *clear* out these from our list, as
* that should be done later. */
pdu->prev_events = ArrayCreate();
prev_events = RoomPrevEventsGet(room);
for (i = 0; i < ArraySize(prev_events); i++)
{
HashMap *event = JsonValueAsObject(ArrayGet(prev_events, i));
JsonValue *event_id = JsonGet(event, 1, "event_id");
ArrayAdd(pdu->prev_events, JsonValueDuplicate(event_id));
}
/* TODO: Signatures.
* We currently *don't* have an Ed25519 implementation. */
return false;
}

396
src/Room/V1/Send.c Normal file
View file

@ -0,0 +1,396 @@
#include "Room/internal.h"
#include <Schema/PduV1.h>
#include <Parser.h>
static char *
RoomHashEventV1(PduV1 pdu)
{
HashMap *json = PduV1ToJson(&pdu);
char *b64;
b64 = EventContentHash(json);
JsonFree(json);
return b64;
}
static bool
EventFits(HashMap *pdu)
{
int size = CanonicalJsonEncode(pdu, NULL);
JsonValue *key;
/* Main PDU length is 65536 bytes */
if (size > 65536)
{
return false;
}
#define VerifyKey(k,s) do \
{ \
if ((key = JsonGet(pdu, 1, k)) && \
(strlen(JsonValueAsString(key)) > s)) \
{ \
return false; \
} \
} \
while (0)
VerifyKey("sender", 255);
VerifyKey("room_id", 255);
VerifyKey("state_key", 255);
VerifyKey("type", 255);
VerifyKey("event_id", 255);
return true;
#undef VerifyKey
}
static bool
EventFitsV1(PduV1 pdu)
{
HashMap *hm;
bool ret;
hm = PduV1ToJson(&pdu);
ret = EventFits(hm);
JsonFree(hm);
return ret;
}
static PduV1Status
RoomGetEventStatusV1(Room *room, PduV1 *pdu, State *prev, bool client, char **errp)
{
if (!room || !pdu || !prev)
{
if (errp)
{
*errp = "Illegal arguments given to RoomGetEventStatusV1";
}
return PDUV1_STATUS_DROPPED;
}
if (!EventFitsV1(*pdu))
{
/* Reject this event as it is too large. */
if (errp)
{
*errp = "PDU is too large to fit";
}
return PDUV1_STATUS_DROPPED;
}
if (!RoomAuthoriseEventV1(room, *pdu, prev, errp))
{
/* Reject this event as the current state does not allow it.
* TODO: Make the auth check function return a string showing the
* errorr status to the user. */
return PDUV1_STATUS_DROPPED;
}
if (!client)
{
State *current = StateCurrent(room);
if (!RoomAuthoriseEventV1(room, *pdu, current, NULL))
{
StateFree(current);
return PDUV1_STATUS_SOFTFAIL;
}
StateFree(current);
}
return PDUV1_STATUS_ACCEPTED;
}
bool
RoomAddEventV1(Room *room, PduV1 pdu)
{
DbRef *event_ref;
Array *prev_events = NULL, *leaves = NULL;
HashMap *leaves_json = NULL, *pdu_json = NULL;
JsonValue *leaves_val;
char *safe_id;
size_t i;
if (!room || room->version >= 3 ||
pdu._unsigned.pdu_status == PDUV1_STATUS_DROPPED)
{
return false;
}
/* Insert our PDU into the event table, regardless of status */
safe_id = CreateSafeID(pdu.event_id);
event_ref = DbCreate(room->db, 4, "rooms", room->id, "events", safe_id);
pdu_json = PduV1ToJson(&pdu);
DbJsonSet(event_ref, pdu_json);
DbUnlock(room->db, event_ref);
JsonFree(pdu_json);
Free(safe_id);
/* Only accepted PDUs get to do the news */
if (pdu._unsigned.pdu_status == PDUV1_STATUS_ACCEPTED)
{
/* Remove managed leaves here. */
leaves_json = DbJson(room->leaves_ref);
leaves_val = JsonValueDuplicate(JsonGet(leaves_json, 1, "leaves"));
leaves = JsonValueAsArray(leaves_val);
Free(leaves_val); /* We do not care about the array's JSON shell. */
prev_events = pdu.prev_events;
for (i = 0; i < ArraySize(prev_events); i++)
{
JsonValue *event_val = ArrayGet(prev_events, i);
char *event_id = JsonValueAsString(event_val);
size_t j;
ssize_t delete_index = -1;
for (j = 0; j < ArraySize(leaves); j++)
{
JsonValue *leaf_val = ArrayGet(leaves, j);
HashMap *leaf_object = JsonValueAsObject(leaf_val);
char *leaf_id =
JsonValueAsString(JsonGet(leaf_object, 1, "event_id"));
if (StrEquals(leaf_id, event_id))
{
delete_index = j;
break;
}
}
if (delete_index == -1)
{
continue;
}
JsonValueFree(ArrayDelete(leaves, delete_index));
}
/* Add our current PDU to the leaves. */
ArrayAdd(leaves, JsonValueObject(PduV1ToJson(&pdu)));
leaves_json = JsonDuplicate(leaves_json);
JsonValueFree(HashMapDelete(leaves_json, "leaves"));
JsonSet(leaves_json, JsonValueArray(leaves), 1, "leaves");
DbJsonSet(room->leaves_ref, leaves_json);
JsonFree(leaves_json);
}
for (i = 0; i < ArraySize(prev_events); i++)
{
JsonValue *event_val = ArrayGet(prev_events, i);
char *id = JsonValueAsString(event_val);
char *error = NULL;
PduV1 prev_pdu = { 0 };
HashMap *prev_object = NULL;
Array *next_events = NULL;
event_ref = DbLock(room->db, 4, "rooms", room->id, "events", id);
PduV1FromJson(DbJson(event_ref), &prev_pdu, &error);
/* Update the next events view. Note that this works even if
* the event is soft-failed/rejected. */
if (!prev_pdu._unsigned.next_events)
{
prev_pdu._unsigned.next_events = ArrayCreate();
}
next_events = prev_pdu._unsigned.next_events;
ArrayAdd(next_events, StrDuplicate(pdu.event_id));
prev_object = PduV1ToJson(&prev_pdu);
DbJsonSet(event_ref, prev_object);
JsonFree(prev_object);
PduV1Free(&prev_pdu);
DbUnlock(room->db, event_ref);
}
/* Accepted PDUs should be the only one that users should be
* notified about. */
if (pdu._unsigned.pdu_status == PDUV1_STATUS_ACCEPTED)
{
State *state;
char *type, *state_key, *event_id;
pdu_json = PduV1ToJson(&pdu);
/* If we have a membership change, then add it to the
* proper table. */
if (StrEquals(pdu.type, "m.room.member"))
{
CommonID *id = UserIdParse(pdu.state_key, NULL);
User *user = UserLock(room->db, id->local);
char *membership = JsonValueAsString(
HashMapGet(pdu.content, "membership")
);
if (StrEquals(membership, "join") && user)
{
UserAddJoin(user, room->id);
UserPushJoinSync(user, room->id);
}
else if (StrEquals(membership, "invite") && user)
{
UserAddInvite(user, room->id);
UserPushInviteSync(user, room->id);
}
else if ((StrEquals(membership, "leave") && user) ||
StrEquals(membership, "ban"))
{
UserRemoveInvite(user, room->id);
UserRemoveJoin(user, room->id);
}
UserIdFree(id);
UserUnlock(user);
}
/* Notify the user by pushing out the user */
state = StateCurrent(room);
while (StateIterate(state, &type, &state_key, (void **) &event_id))
{
if (StrEquals(type, "m.room.member"))
{
CommonID *id = UserIdParse(state_key, NULL);
User *user = UserLock(room->db, id->local);
UserPushEvent(user, pdu_json);
UserIdFree(id);
UserUnlock(user);
}
Free(type);
Free(state_key);
}
StateFree(state);
JsonFree(pdu_json);
}
return true;
}
HashMap *
RoomEventSendV1(Room * room, HashMap * event, char **errp)
{
PduV1 pdu = { 0 };
HashMap *pdu_object = NULL;
bool client_event, valid = false;
State *state = NULL;
PduV1Status status;
client_event = !PopulateEventV1(room, event, &pdu, RoomGetCreator(room));
pdu_object = PduV1ToJson(&pdu);
state = StateResolve(room, pdu_object);
if (client_event)
{
char *ev_id;
#define AddState(type, key) do \
{ \
ev_id = StateGet(state, type, key); \
if (ev_id) \
{ \
JsonValue *v = JsonValueString(ev_id); \
ArrayAdd(pdu.auth_events, v); \
} \
} \
while (0)
/*
* Implemented from
* https://spec.matrix.org/v1.7/server-server-api/
* #auth-events-selection */
AddState("m.room.create", "");
AddState("m.room.power_levels", "");
AddState("m.room.member", pdu.sender);
if (StrEquals(pdu.type, "m.room.member"))
{
char *target = pdu.state_key;
char *membership =
JsonValueAsString(JsonGet(pdu.content, 1, "membership"));
char *auth_via =
JsonValueAsString(
JsonGet(
pdu.content, 1, "join_authorised_via_users_server")
);
HashMap *inv =
JsonValueAsObject(
JsonGet(pdu.content, 1, "third_party_invite"));
if (target && !StrEquals(target, pdu.sender))
{
AddState("m.room.member", target);
}
if (StrEquals(membership, "join") ||
StrEquals(membership, "invite"))
{
AddState("m.room.join_rules", "");
}
if (StrEquals(membership, "invite") && inv)
{
char *token =
JsonValueAsString(JsonGet(inv, 2, "signed", "token"));
AddState("m.room.third_party_invite", token);
}
if (auth_via && room->version >= 8)
{
AddState("m.room.member", auth_via);
}
}
pdu.hashes.sha256 = RoomHashEventV1(pdu);
#undef AddState
}
/* It seems like we need to behave differently in terms of
* verifying PDUs from the client/federation.
* - In the client, we just do not care about any events that
* are incorrect. We simply drop them, as if they never existed.
* - In the server on the otherhand, the only place where we can
* possibly drop events as such is if it fails signatures. In
* other cases, we *have* to store it(ableit with flags, to
* restrict what we can do).
* - Rejection: We avoid relaying/linking those to anything.
* They must NOT be used for stateres.
* - Softfail: Essentially almost the same as rejects, except
* that they *are* used for stateres.
* I guess a way to do this may be to add a CheckAuthStatus
* function that also verifies if it is a client event, and returns
* an enum:
* - DROPPED: Do NOT process it _at all_
* - REJECT: Process that event as if it was rejected
* - SOFTFAIL: Process the event as if it was softfailed
* The main issue is storing it in the PDU. A naive approach would be to
* add the status to the unsigned field of the PDU, and add functions to
* return the status. I guess that is possible, but then again, can we
* really abuse the unsigned field for this?
*/
/* TODO: For PDU events, we should verify their hashes. */
status = RoomGetEventStatusV1(room, &pdu, state, client_event, errp);
if (status == PDUV1_STATUS_DROPPED)
{
goto finish;
}
pdu._unsigned.pdu_status = status;
StateFree(state);
RoomAddEventV1(room, pdu);
state = NULL;
valid = true;
/* If it is a client event, we should make sure that we shout at
* every other homeserver about our new event. */
finish:
if (state)
{
StateFree(state);
}
if (pdu_object)
{
JsonFree(pdu_object);
pdu_object = NULL;
}
if (valid)
{
pdu_object = PduV1ToJson(&pdu);
}
PduV1Free(&pdu);
return pdu_object;
}

90
src/Room/internal.h Normal file
View file

@ -0,0 +1,90 @@
#ifndef TELODENDRIA_IROOM_H
#define TELODENDRIA_IROOM_H
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Util.h>
#include <Cytoplasm/Str.h>
#include <Cytoplasm/Db.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <CanonicalJson.h>
#include <Parser.h>
#include <Config.h>
#include <State.h>
#include <User.h>
#define IsState(p, typ, key) (StrEquals((p)->type, typ) && \
StrEquals((p)->state_key, key))
struct Room
{
Db *db;
DbRef *state_ref;
DbRef *leaves_ref; /* Reference to the leaf list */
ServerPart creator;
char *id;
int version;
};
/**
* Populates a room with the events required for its creation.
*/
extern void RoomPopulate(Room *, User *, RoomCreateRequest *, ServerPart);
/**
* Verifies if the current room state has a joinrule set to a
* specific value.
*/
extern bool RoomIsJoinRule(Room *, State *, char *);
/**
* Tries to parse a PL value from a JsonValue, with a default
* powerlevel, if it cannot parse it.
*/
extern int64_t ParsePL(JsonValue *, int64_t);
/**
* Computes the lowest powerlevel required to execute an action
* in a room.
*/
extern int64_t RoomMinPL(Room *, State *, char *, char *);
/**
* Computes the user's powerlevel at a specific state.
*/
extern int64_t RoomUserPL(Room *, State *, char *);
/**
* Populates an event to a valid PDUv1(and returns true if
* properly created).
*/
extern bool PopulateEventV1(Room *, HashMap *, PduV1 *, ServerPart);
/**
* Sends an event to a room, be it a PDUv1/client event
*/
extern HashMap * RoomEventSendV1(Room *, HashMap *, char **);
/**
* Verifies if the user has a specific membership at a given state.
*/
extern bool RoomUserHasMembership(Room *, State *, char *, char *);
/**
* Computes a PDU's contenthash.
*/
extern char * EventContentHash(HashMap *);
/**
* Creates a new "DB-safe" ID for events.
*/
extern char * CreateSafeID(char *);
#endif

View file

@ -1626,12 +1626,17 @@ UserFetchMessages(User *user, int n, char *token, char **next)
HashMap *event = RoomEventFetch(room, curr);
Array *prevEvents;
size_t j;
bool toFree = true;
Free(curr);
/* Push event into our message list.
* TODO: Check if the user has the right to see the event/room. */
ArrayAdd(messages, event);
if (RoomIsEventVisible(room, user, curr))
{
ArrayAdd(messages, event);
toFree = false;
}
Free(curr);
prevEvents = JsonValueAsArray(HashMapGet(event, "prev_events"));
if (dir)
@ -1653,6 +1658,11 @@ UserFetchMessages(User *user, int n, char *token, char **next)
{
limited = true;
}
if (toFree)
{
JsonFree(event);
event = NULL;
}
}
for (i = 0; i < ArraySize(nexts); i++)
{

View file

@ -152,6 +152,12 @@ extern HashMap * RoomEventSend(Room *, HashMap *, char **);
*/
extern void RoomSendInvite(User *, bool, char *, Room *);
/**
* See if a user is allowed to see an event in a room,
* based on its visibility.
*/
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