forked from Telodendria/Telodendria
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.)
1619 lines
45 KiB
C
1619 lines
45 KiB
C
/*
|
||
* 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 user’s 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;
|
||
}
|