telodendria/src/Room.c
lda e36f4357ab [ADD/WIP] Start testing code
I think I'll manage PDU depth later(with an actual good way to handle
it properly(that is not just setting it to the max and calling it a
day.)
2024-05-17 23:57:32 +02:00

1619 lines
45 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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/Array.h>
#include <Cytoplasm/Json.h>
/*#include "Cytoplasm/Stream.h"*/
#include "Cytoplasm/HashMap.h"
#include "Parser.h"
#include "User.h"
#include <Room.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Util.h>
#include <Cytoplasm/Str.h>
#include <Cytoplasm/Db.h>
#include <Schema/RoomCreateRequest.h>
#include <Schema/PduV1.h>
#include <Schema/PduV3.h>
#include <CanonicalJson.h>
#include <Parser.h>
#include <State.h>
#include <stdlib.h>
#include <string.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;
};
static char *
GenerateRoomId(ServerPart s)
{
CommonID cid;
char *string;
cid.sigil = '!';
cid.local = StrRandom(32);
cid.server = s;
string = ParserRecomposeCommonID(cid);
Free(cid.local);
return string;
}
Room *
RoomCreate(Db * db, User *user, RoomCreateRequest * req, ServerPart s)
{
Room *room;
char *version_string, *full_creator;
int version_num = 1;
HashMap *json;
if (!db || !req || !user)
{
return NULL;
}
version_string = req->room_version;
if (version_string)
{
/* TODO: Eventually use something else than room version 1 by
* default, and maybe add a config parameter. */
version_num = atoi(version_string);
version_num = version_num == 0 ? 1 : version_num;
}
room = Malloc(sizeof(Room));
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->version = version_num;
room->state_ref = DbCreate(db, 3, "rooms", room->id, "state");
room->leaves_ref = DbCreate(db, 3, "rooms", room->id, "leaves");
json = DbJson(room->leaves_ref);
JsonSet(json, JsonValueArray(ArrayCreate()), 1, "leaves");
full_creator = ParserRecomposeServerPart(room->creator);
JsonSet(json, JsonValueString(full_creator), 1, "creator");
Free(full_creator);
{
HashMap *event = HashMapCreate();
HashMap *content = HashMapCreate();
CommonID sender;
char *sender_str;
sender.sigil = '@';
sender.local = UserGetName(user);
sender.server = s;
sender_str = ParserRecomposeCommonID(sender);
JsonSet(event, JsonValueString(sender_str), 1, "sender");
if (room->version <= 10)
{
JsonSet(content, JsonValueString(sender_str), 1, "creator");
}
Free(sender_str);
JsonSet(event, JsonValueString("m.room.create"), 1, "type");
JsonSet(event, JsonValueString(""), 1, "state_key");
JsonSet(event, JsonValueObject(content), 1, "content");
JsonFree(RoomEventSend(room, event));
JsonFree(event);
}
{
HashMap *event = HashMapCreate();
HashMap *content = HashMapCreate();
CommonID sender;
char *sender_str;
sender.sigil = '@';
sender.local = UserGetName(user);
sender.server = s;
sender_str = ParserRecomposeCommonID(sender);
JsonSet(event, JsonValueString(sender_str), 1, "sender");
JsonSet(content, JsonValueString("join"), 1, "membership");
JsonSet(event, JsonValueString("m.room.member"), 1, "type");
JsonSet(event, JsonValueString(sender_str), 1, "state_key");
Free(sender_str);
JsonSet(event, JsonValueObject(content), 1, "content");
JsonFree(RoomEventSend(room, event));
JsonFree(event);
}
/* TODO: The rest of the events mandated by the specification on
* POST /createRoom. Also clean up that code, so that it is more
* straightforward(and short). */
return room;
}
Room *
RoomLock(Db * db, char *id)
{
DbRef *state_ref, *leaves_ref;
HashMap *json;
Room *room;
if (!db || !id)
{
return NULL;
}
state_ref = DbLock(db, 3, "rooms", id, "state");
leaves_ref = DbLock(db, 3, "rooms", id, "leaves");
if (!state_ref || !leaves_ref)
{
return NULL;
}
room = Malloc(sizeof(Room));
if (!room)
{
DbUnlock(db, state_ref);
DbUnlock(db, leaves_ref);
return NULL;
}
room->db = db;
room->state_ref = state_ref;
room->leaves_ref = leaves_ref;
room->id = StrDuplicate(id);
json = DbJson(room->leaves_ref);
ParseServerPart(
JsonValueAsString(JsonGet(json, 1, "creator")),
&room->creator);
return room;
}
int
RoomUnlock(Room * room)
{
Db *db;
DbRef *state_ref;
DbRef *leaves_ref;
if (!room)
{
return 0;
}
db = room->db;
state_ref = room->state_ref;
leaves_ref = room->leaves_ref;
Free(room->id);
Free(room);
ServerPartFree(room->creator);
return DbUnlock(db, state_ref) &&
DbUnlock(db, leaves_ref);
}
char *
RoomIdGet(Room * room)
{
return room ? room->id : NULL;
}
int
RoomVersionGet(Room * room)
{
return room ? room->version : 0;
}
HashMap *
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! */
database_state = DbJson(room->state_ref);
return StateDeserialise(database_state);
}
HashMap *
RoomStateGetID(Room * room, char *event_id)
{
HashMap *event, *state;
if (!room || !event_id)
{
return NULL;
}
event = RoomEventFetch(room, event_id);
if (!event)
{
return NULL;
}
state = StateResolve(room, event);
JsonFree(event);
return state;
}
#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
static bool
RoomIsJoinRule(Room * room, HashMap *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, 1, "join_rule")),
jr);
finish:
FinishState(joinrule);
}
/* Verifies if user has a specific membership before [e_id] in the room. */
static bool
RoomUserHasMembership(Room * room, HashMap *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);
}
static int64_t
ParsePL(JsonValue *v, int64_t def)
{
if (!v)
{
return def;
}
if (JsonValueType(v) == JSON_INTEGER)
{
return JsonValueAsInteger(v);
}
if (JsonValueType(v) == JSON_STRING)
{
char *string = JsonValueAsString(v), *end;
int64_t value= strtoll(string, &end, 10);
if (!((*string != '\0') && (*end == '\0')))
{
/* Invalid string: return the default. */
return def;
}
return value;
}
return def;
}
/* Computes the smallest PL needed to do something somewhere */
static int64_t
RoomMinPL(Room * room, HashMap *state, char *type, char *act)
{
HashMap *pl = NULL;
JsonValue *val;
char *id_pl;
int64_t ret, def;
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, 1, act);
}
else
{
val = JsonGet(pl, 2, type, act);
}
ret = ParsePL(val, def);
finish:
FinishState(pl);
}
/* Finds the power level of an user before [e_id] was sent. */
static int64_t
RoomUserPL(Room * room, HashMap *state, char *user)
{
HashMap *pl = NULL;
char *id_pl;
int64_t ret, def;
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, 2, "users", user), def);
finish:
FinishState(pl);
}
static 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))
{
return true;
}
/* 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();
/* 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: Signature and alldat. */
return false;
}
static bool
AuthoriseCreateV1(PduV1 pdu)
{
bool ret = true;
CommonID sender = { 0 }, room_id = { 0 };
char *sender_serv = NULL, *id_serv = NULL;
if (ArraySize(pdu.auth_events) > 0)
{
ret = false;
goto finish;
}
if (!ParseCommonID(pdu.room_id, &room_id) ||
!ParseCommonID(pdu.sender, &sender))
{
ret = false;
goto finish;
}
sender_serv = ParserRecomposeServerPart(sender.server);
id_serv = ParserRecomposeServerPart(room_id.server);
if (!StrEquals(sender_serv, id_serv))
{
ret = false;
goto finish;
}
/* TODO: Check room_version as in step 1.3 */
if (!HashMapGet(pdu.content, "creator"))
{
ret = false;
goto finish;
}
finish:
if (sender_serv)
{
Free(sender_serv);
}
if (id_serv)
{
Free(id_serv);
}
CommonIDFree(sender);
CommonIDFree(room_id);
return ret;
}
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");
if (IsState(auth_pdu, "m.room.member", pdu->sender))
{
/* 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;
}
static bool
VerifyPDUV1(PduV1 *auth_pdu)
{
/* TODO:
* https://spec.matrix.org/v1.7/server-server-api/
* #checks-performed-on-receipt-of-a-pdu */
(void) auth_pdu;
return true; /* This only shows whenever an event was rejected, not
* soft-failed */
}
static bool
ConsiderAuthEventsV1(Room * room, PduV1 pdu)
{
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);
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. */
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))
{
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))
{
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);
return room_create; /* Step 2.4 is actually done here. */
}
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;
}
static bool
AuthoriseAliasV1(PduV1 pdu)
{
/* Step 4.1: If event has no state_key, reject. */
if (!pdu.state_key || StrEquals(pdu.state_key, ""))
{
return false;
}
/* Step 4.2: If sender's domain doesn't matches state_key, reject. */
if (!VerifyServers(pdu.state_key, pdu.sender))
{
return false;
}
/* Step 4.3: Otherwise, allow. */
return true;
}
static bool
AuthorizeInviteMembershipV1(Room * room, PduV1 pdu, HashMap *state)
{
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"))
{
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")))
{
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")))
{
return false;
}
if (!(token = JsonGet(signed_obj, 1, "token")))
{
return false;
}
/* Step 5.3.1.4: If mxid does not match state_key, reject. */
if (!StrEquals(JsonValueAsString(mxid), pdu.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))))
{
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))
{
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. */
return false;
}
/* Step 5.3.2: If the sender's current membership state is not join,
* reject. */
if (!RoomUserHasMembership(room, state, pdu.sender, "join"))
{
return false;
}
/* Step 5.3.3: If target users current membership state is join or ban, reject. */
if (RoomUserHasMembership(room, state, pdu.state_key, "join") ||
RoomUserHasMembership(room, state, pdu.state_key, "join"))
{
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. */
return false;
}
static bool
AuthorizeLeaveMembershipV1(Room * room, PduV1 pdu, HashMap *state)
{
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.sender);
/* 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))
{
return
RoomUserHasMembership(room, state, pdu.sender, "invite") ||
RoomUserHasMembership(room, state, pdu.sender, "join");
}
/* Step 5.4.2: If the sender's current membership state is not join,
* reject. */
if (!RoomUserHasMembership(room, state, pdu.sender, "join"))
{
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)
{
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. */
return false;
}
static bool
AuthorizeBanMembershipV1(Room * room, PduV1 pdu, HashMap *state)
{
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"))
{
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.sender);
if ((pdu_pl >= ban_pl) && (target_pl < pdu_pl))
{
return true;
}
/* Step 5.5.3: Otherwise, reject. */
return false;
}
static bool
AuthorizeJoinMembershipV1(Room * room, PduV1 pdu, HashMap *state)
{
/* 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));
char *ignored;
HashMap *prev_event = RoomEventFetch(room, prev_id);
PduV1 prev_pdu;
if (prev && PduV1FromJson(prev_event, &prev_pdu, &ignored))
{
if (StrEquals(prev_pdu.type, "m.room.create") &&
StrEquals(prev_pdu.sender, pdu.state_key))
{
PduV1Free(&prev_pdu);
JsonFree(prev_event);
return true;
}
PduV1Free(&prev_pdu);
}
JsonFree(prev_event);
}
/* Step 5.2.2: If the sender does not match state_key, reject. */
if (!StrEquals(pdu.sender, pdu.state_key))
{
return false;
}
/* Step 5.2.3: If the sender is banned, reject. */
if (RoomUserHasMembership(room, state, pdu.sender, "ban"))
{
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. */
return false;
}
static bool
AuthoriseMemberV1(Room * room, PduV1 pdu, HashMap *state)
{
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")))
{
return false;
}
if (JsonValueType(membership) != JSON_STRING)
{
/* Also check for the type */
return false;
}
membership_str = JsonValueAsString(membership);
#define JumpIfMembership(mem, func) do { \
if (StrEquals(membership_str, mem)) \
{ \
return func(room, pdu, state); \
} \
} 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
}
static bool
AuthorisePowerLevelsV1(Room * room, PduV1 pdu, HashMap *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;
}
/* 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;
}
}
/* 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, newv; \
if ((old && !new) || (!old && new) || \
((oldv = JsonValueAsInteger(old)) != \
(newv = JsonValueAsInteger(new)))) \
{ \
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); \
StateFree(state); \
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); \
StateFree(state); \
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;
}
bool
RoomAuthoriseEventV1(Room * room, PduV1 pdu, HashMap *state)
{
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);
}
/* Step 2: Considering the event's auth_events. */
if (!ConsiderAuthEventsV1(room, pdu))
{
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 */
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))
{
return false;
}
}
}
JsonFree(create_event);
/* Step 4: If type is m.room.aliases */
if (StrEquals(pdu.type, "m.room.aliases"))
{
return AuthoriseAliasV1(pdu);
}
/* Step 5: If type is m.room.member */
if (StrEquals(pdu.type, "m.room.member"))
{
return AuthoriseMemberV1(room, pdu, state);
}
/* 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;
}
static char *
RoomHashEvent(HashMap * pdu_json)
{
HashMap * copy = JsonDuplicate(pdu_json);
char *hash;
JsonValueFree(HashMapDelete(copy, "unsigned"));
JsonValueFree(HashMapDelete(copy, "signatures"));
JsonValueFree(HashMapDelete(copy, "hashes"));
hash = CanonicalJsonHash(copy);
JsonFree(copy);
return hash;
}
static char *
RoomHashEventV1(PduV1 pdu)
{
HashMap *json = PduV1ToJson(&pdu);
char *sha = RoomHashEvent(json);
JsonFree(json);
return sha;
}
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 HashMap *
RoomEventSendV1(Room * room, HashMap * event)
{
PduV1 pdu = { 0 };
HashMap *pdu_object = NULL;
bool client_event, valid = false;
HashMap *state = NULL;
client_event = !PopulateEventV1(room, event, &pdu, RoomGetCreator(room));
pdu_object = PduV1ToJson(&pdu);
state = StateResolve(room, pdu_object); /* Compute the state
* at that point for later. */
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
}
/* TODO: For PDU events, we should verify their hashes. */
if (!EventFitsV1(pdu))
{
/* Reject this event as it is too large. */
goto finish;
}
if (!RoomAuthoriseEventV1(room, pdu, state))
{
/* Reject this event as the current state does not allow it.
* TODO: Make the auth check function return a string showing the
* error status to the user. */
goto finish;
}
RoomAddEventV1(room, pdu);
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)
{
JsonFree(state);
}
if (pdu_object)
{
JsonFree(pdu_object);
pdu_object = NULL;
}
if (valid)
{
pdu_object = PduV1ToJson(&pdu);
}
PduV1Free(&pdu);
return pdu_object;
}
static HashMap *
RoomEventSendV3(Room * room, HashMap * event)
{
/* TODO */
(void) room;
(void) event;
return NULL;
}
HashMap *
RoomEventSend(Room * room, HashMap * event)
{
if (!room || !event)
{
return NULL;
}
if (room->version < 3)
{
/* Manage with PDUv1 */
return RoomEventSendV1(room, event);
}
/* Manage with PDUv3 otherwise */
return RoomEventSendV3(room, event);
}
static char *
CreateSafeID(char *unsafe_id)
{
size_t length = strlen(unsafe_id);
char *safe_id = Malloc(length + 1);
size_t i;
/* Creates a room ID safe to be put into the database
* for room version 3 and above.
* (with '/'s replaced by '-') */
memcpy(safe_id, unsafe_id, length + 1);
for (i = 0; i < length; i++)
{
if (safe_id[i] == '/')
{
safe_id[i] = '-';
}
}
return safe_id;
}
HashMap *
RoomEventFetch(Room *room, char *id)
{
DbRef *event_ref;
HashMap *ret;
char *safe_id;
if (!room || !id)
{
return NULL;
}
/* 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);
if (!event_ref)
{
/* TODO: Fetch from another homeserver if possible. */
ret = NULL;
goto finish;
}
ret = JsonDuplicate(DbJson(event_ref));
DbUnlock(room->db, event_ref);
finish:
Free(safe_id);
return ret;
}
Array *
RoomPrevEventsGet(Room *room)
{
HashMap *json;
if (!room)
{
return NULL;
}
json = DbJson(room->leaves_ref);
return JsonValueAsArray(JsonGet(json, 1, "leaves"));
}
ServerPart
RoomGetCreator(Room *room)
{
ServerPart ret = { 0 };
if (!room)
{
return ret;
}
return room->creator;
}
bool
RoomAddEventV1(Room *room, PduV1 pdu)
{
DbRef *event_ref;
Array *prev_events, *leaves;
HashMap *leaves_json, *pdu_json;
JsonValue *leaves_val;
char *safe_id;
size_t i;
if (!room || room->version >= 3)
{
return false;
}
leaves_json = DbJson(room->leaves_ref);
leaves_val = JsonValueDuplicate(JsonGet(leaves_json, 1, "leaves"));
leaves = JsonValueAsArray(leaves_val);
Free(leaves_val);
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));
}
safe_id = CreateSafeID(pdu.event_id);
event_ref = DbCreate(room->db, 4, "rooms", room->id, "events", safe_id);
Free(safe_id);
pdu_json = PduV1ToJson(&pdu);
DbJsonSet(event_ref, pdu_json);
ArrayAdd(leaves, JsonValueObject(pdu_json));
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);
DbUnlock(room->db, event_ref);
/* TODO: Store DAG relationships, somehow.
* Also keep track of PDU depth. */
return true;
}