diff --git a/Cytoplasm b/Cytoplasm deleted file mode 160000 index 346b912..0000000 --- a/Cytoplasm +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 346b912a0633cceac10780b8a103f6c89b5ba89f diff --git a/configure b/configure index 17bacfa..87a4de3 100755 --- a/configure +++ b/configure @@ -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 \ diff --git a/contrib/Makefile b/contrib/Makefile deleted file mode 100644 index b76cbd7..0000000 --- a/contrib/Makefile +++ /dev/null @@ -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)/{} \; diff --git a/src/Main.c b/src/Main.c index ba5a476..a08d503 100644 --- a/src/Main.c +++ b/src/Main.c @@ -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); } diff --git a/src/Room.c b/src/Room.c index f07b389..952729b 100644 --- a/src/Room.c +++ b/src/Room.c @@ -27,7 +27,6 @@ #include #include #include -#include #include #include @@ -51,21 +50,7 @@ #include #include -#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; -}; +#include "Room/internal.h" static char * GenerateRoomId(ServerPart s) @@ -81,236 +66,6 @@ GenerateRoomId(ServerPart s) return string; } -static 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) */ -} Room * RoomCreate(Db * db, User *user, RoomCreateRequest * req, ServerPart s) { @@ -419,121 +174,7 @@ RoomUnlock(Room * room) DbUnlock(db, leaves_ref); } -char * -RoomIdGet(Room * room) -{ - return room ? room->id : NULL; -} - -int -RoomVersionGet(Room * room) -{ - return room ? room->version : 0; -} - -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; -} - -#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, 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. */ -static 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); -} -static int64_t +int64_t ParsePL(JsonValue *v, int64_t def) { if (!v) @@ -559,1101 +200,7 @@ ParsePL(JsonValue *v, int64_t def) return def; } -/* Computes the smallest PL needed to do something somewhere */ -static 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 before [e_id] was sent. */ -/* TODO: The creator should have PL100 by default. */ -static 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); -} - -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; - } - - /* 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; -} -static 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; -} - -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; -} -static bool -VerifyPDUV1(PduV1 *auth_pdu) -{ - /* TODO: The PDU could come from an unknown source, which may lack - * the tools to verify softfailing(or a source that we may not trust)*/ - /* 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 -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. */ -} -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, 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; -} -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; -} -static 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 -} -static 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; -} -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; -} char * EventContentHash(HashMap * pdu_json) { @@ -1674,231 +221,8 @@ EventContentHash(HashMap * pdu_json) return b64; } -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; -} -static 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; -} +/* TODO: Separate this when we eventually steamroll */ static HashMap * RoomEventSendV3(Room * room, HashMap * event) { @@ -1907,6 +231,7 @@ RoomEventSendV3(Room * room, HashMap * event) (void) event; return NULL; } + HashMap * RoomEventSend(Room * room, HashMap * event, char **errp) { @@ -1923,7 +248,7 @@ RoomEventSend(Room * room, HashMap * event, char **errp) return RoomEventSendV3(room, event); } -static char * +char * CreateSafeID(char *unsafe_id) { size_t length = strlen(unsafe_id); @@ -1971,192 +296,6 @@ finish: 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 = 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 * RoomEventCreate(char *sender, char *type, char *key, HashMap *c) { @@ -2178,42 +317,6 @@ RoomEventCreate(char *sender, char *type, char *key, HashMap *c) return event; } -/* TODO: This has it's fair share of problems, as a malicious - * homserver 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; -} void RoomAddAlias(Db *db, char *roomAlias, char *roomId, char *sender, char *serv) { @@ -2364,7 +467,8 @@ RoomSendInvite(User *sender, bool direct, char *user, Room *room) JsonFree(RoomEventSend(room, event, NULL)); JsonFree(event); - /* TODO: Send to "local" invite list if user is local. */ + /* TODO: Send to "local" invite list if user is local. + * Otherwise, ping the entire world about it. */ ConfigUnlock(&conf); Free(senderStr); @@ -2608,15 +712,3 @@ failure: return NULL; #undef CopyField } - -bool -RoomIsSoftfailedV1(PduV1 pdu) -{ - return pdu._unsigned.pdu_status == PDUV1_STATUS_SOFTFAIL; -} - -bool -RoomIsRejectedV1(PduV1 pdu) -{ - return pdu._unsigned.pdu_status == PDUV1_STATUS_SOFTFAIL; -} diff --git a/src/Room/Info.c b/src/Room/Info.c new file mode 100644 index 0000000..c58b88d --- /dev/null +++ b/src/Room/Info.c @@ -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; +} diff --git a/src/Room/Populate.c b/src/Room/Populate.c new file mode 100644 index 0000000..f2e8e68 --- /dev/null +++ b/src/Room/Populate.c @@ -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 +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#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) */ +} + diff --git a/src/Room/State.c b/src/Room/State.c new file mode 100644 index 0000000..d50efc2 --- /dev/null +++ b/src/Room/State.c @@ -0,0 +1,282 @@ +#include "Room/internal.h" + +#include + +#include +#include + +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); +} diff --git a/src/Room/V1/Auth/Alias.c b/src/Room/V1/Auth/Alias.c new file mode 100644 index 0000000..6b0f71c --- /dev/null +++ b/src/Room/V1/Auth/Alias.c @@ -0,0 +1,65 @@ +#include "Room/internal.h" + +#include + +#include + +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; +} diff --git a/src/Room/V1/Auth/Auth.c b/src/Room/V1/Auth/Auth.c new file mode 100644 index 0000000..5a23eae --- /dev/null +++ b/src/Room/V1/Auth/Auth.c @@ -0,0 +1,174 @@ +#include "Room/internal.h" +#include "Room/V1/Auth/internal.h" + +#include + +#include + +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; +} diff --git a/src/Room/V1/Auth/AuthEvents.c b/src/Room/V1/Auth/AuthEvents.c new file mode 100644 index 0000000..ddc8b3a --- /dev/null +++ b/src/Room/V1/Auth/AuthEvents.c @@ -0,0 +1,183 @@ +#include "Room/internal.h" +#include "Room/V1/Auth/internal.h" + +#include + +#include + +#include + +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. */ +} diff --git a/src/Room/V1/Auth/Create.c b/src/Room/V1/Auth/Create.c new file mode 100644 index 0000000..1338ac6 --- /dev/null +++ b/src/Room/V1/Auth/Create.c @@ -0,0 +1,68 @@ +#include "Room/internal.h" +#include "Room/V1/Auth/internal.h" + +#include + +#include + +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; +} + diff --git a/src/Room/V1/Auth/Member.c b/src/Room/V1/Auth/Member.c new file mode 100644 index 0000000..23499f8 --- /dev/null +++ b/src/Room/V1/Auth/Member.c @@ -0,0 +1,371 @@ +#include "Room/internal.h" + +#include + +#include + +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 +} diff --git a/src/Room/V1/Auth/PL.c b/src/Room/V1/Auth/PL.c new file mode 100644 index 0000000..e530f21 --- /dev/null +++ b/src/Room/V1/Auth/PL.c @@ -0,0 +1,182 @@ +#include "Room/internal.h" + +#include + +#include + +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; +} diff --git a/src/Room/V1/Auth/internal.h b/src/Room/V1/Auth/internal.h new file mode 100644 index 0000000..875592f --- /dev/null +++ b/src/Room/V1/Auth/internal.h @@ -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 diff --git a/src/Room/V1/Info.c b/src/Room/V1/Info.c new file mode 100644 index 0000000..552bd7f --- /dev/null +++ b/src/Room/V1/Info.c @@ -0,0 +1,17 @@ +#include "Room/internal.h" + +#include + +#include + +bool +RoomIsSoftfailedV1(PduV1 pdu) +{ + return pdu._unsigned.pdu_status == PDUV1_STATUS_SOFTFAIL; +} + +bool +RoomIsRejectedV1(PduV1 pdu) +{ + return pdu._unsigned.pdu_status == PDUV1_STATUS_SOFTFAIL; +} diff --git a/src/Room/V1/Populate.c b/src/Room/V1/Populate.c new file mode 100644 index 0000000..5285975 --- /dev/null +++ b/src/Room/V1/Populate.c @@ -0,0 +1,73 @@ +#include "Room/internal.h" + +#include + +#include + +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; +} diff --git a/src/Room/V1/Send.c b/src/Room/V1/Send.c new file mode 100644 index 0000000..7727875 --- /dev/null +++ b/src/Room/V1/Send.c @@ -0,0 +1,396 @@ +#include "Room/internal.h" + +#include + +#include + +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; +} diff --git a/src/Room/internal.h b/src/Room/internal.h new file mode 100644 index 0000000..cf601c1 --- /dev/null +++ b/src/Room/internal.h @@ -0,0 +1,90 @@ +#ifndef TELODENDRIA_IROOM_H +#define TELODENDRIA_IROOM_H + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#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 diff --git a/src/User.c b/src/User.c index ac539a2..efcfbb5 100644 --- a/src/User.c +++ b/src/User.c @@ -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++) { diff --git a/src/include/Room.h b/src/include/Room.h index 880a3dd..30fe483 100644 --- a/src/include/Room.h +++ b/src/include/Room.h @@ -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