diff --git a/lib/matrix_server.ex b/lib/matrix_server.ex index 53213d3..7acc43d 100644 --- a/lib/matrix_server.ex +++ b/lib/matrix_server.ex @@ -31,4 +31,20 @@ defmodule MatrixServer do end def default_room_version, do: "7" + + def get_domain(id) do + case String.split(id, ":") do + [_, server_name] -> server_name + _ -> nil + end + end + + # https://elixirforum.com/t/22709/9 + def has_duplicates?(list) do + list + |> Enum.reduce_while(%MapSet{}, fn x, acc -> + if MapSet.member?(acc, x), do: {:halt, false}, else: {:cont, MapSet.put(acc, x)} + end) + |> is_boolean() + end end diff --git a/lib/matrix_server/event.ex b/lib/matrix_server/event.ex index d59e8a9..df0473b 100644 --- a/lib/matrix_server/event.ex +++ b/lib/matrix_server/event.ex @@ -1,9 +1,9 @@ defmodule MatrixServer.Event do use Ecto.Schema - import Ecto.Changeset + import Ecto.Query - alias MatrixServer.{Room, Event} + alias MatrixServer.{Repo, Room, Event} @primary_key {:event_id, :string, []} schema "events" do @@ -17,12 +17,6 @@ defmodule MatrixServer.Event do belongs_to :room, Room, type: :string end - def changeset(event, params \\ %{}) do - event - |> cast(params, [:type, :timestamp, :state_key, :sender, :content]) - |> validate_required([:type, :timestamp, :sender]) - end - def new(room_id, sender) do %Event{ room_id: room_id, @@ -122,4 +116,110 @@ defmodule MatrixServer.Event do def is_control_event(_), do: false def is_state_event(%Event{state_key: state_key}), do: state_key != nil + + # Perform validations that can be done before state resolution. + # For example checking the domain of the sender. + # We assume that required keys, as well as in the content, is already validated. + + # Rule 1.4 is left to changeset validation. + def prevalidate(%Event{ + type: "m.room.create", + prev_events: prev_events, + auth_events: auth_events, + room_id: room_id, + sender: sender + }) do + # TODO: error check on domains? + # TODO: rule 1.3 + + # Check rules: 1.1, 1.2 + prev_events == [] and + auth_events == [] and + MatrixServer.get_domain(sender) == MatrixServer.get_domain(room_id) + end + + def prevalidate(%Event{auth_events: auth_event_ids, prev_events: prev_event_ids} = event) do + prev_events = + Event + |> where([e], e.event_id in ^prev_event_ids) + |> Repo.all() + + auth_events = + Event + |> where([e], e.event_id in ^auth_event_ids) + |> Repo.all() + + state_pairs = Enum.map(auth_events, &{&1.type, &1.state_key}) + + # Check rules: 2.1, 2.2, 3 + length(auth_events) == length(auth_event_ids) and + length(prev_events) == length(prev_event_ids) and + not MatrixServer.has_duplicates?(state_pairs) and + valid_auth_events?(event, auth_events) and + Enum.find_value(state_pairs, &(&1 == {"m.room.create", ""})) and + do_prevalidate(event, auth_events, prev_events) + end + + # Rule 4.1 is left to changeset validation. + defp do_prevalidate(%Event{type: "m.room.aliases", sender: sender, state_key: state_key}, _, _) do + # Check rule: 4.2 + MatrixServer.get_domain(sender) == MatrixServer.get_domain(state_key) + end + + # Rule 5.1 is left to changeset validation. + # Rules 5.2.3, 5.2.4, 5.2.5 is left to state resolution. + # Check rule: 5.2.1 + defp do_prevalidate( + %Event{type: "m.room.member", content: %{"membership" => "join"}, sender: sender}, + _, + [%Event{type: "m.room.create", state_key: sender}] + ), + do: true + + # Check rule: 5.2.2 + defp do_prevalidate( + %Event{ + type: "m.room.member", + content: %{"membership" => "join"}, + sender: sender, + state_key: state_key + }, + _, + _ + ) + when sender != state_key, + do: false + + # TODO: Rule 5.3.1 + # All other rules will be checked during state resolution. + defp do_prevalidate(_, _, _), do: true + + defp valid_auth_events?( + %Event{type: type, sender: sender, state_key: state_key, content: content}, + auth_events + ) do + Enum.all?(auth_events, fn + %Event{type: "m.room.create", state_key: ""} -> + true + + %Event{type: "m.room.power_levels", state_key: ""} -> + true + + %Event{type: "m.room.member", state_key: ^sender} -> + true + + %Event{type: auth_type, state_key: auth_state_key} -> + if type == "m.room.member" do + %{"membership" => membership} = content + + (auth_type == "m.room.member" and auth_state_key == state_key) or + (membership in ["join", "invite"] and auth_type == "m.room.join_rules" and + auth_state_key == "") or + (membership == "invite" and auth_type == "m.room.third_party_invite" and + auth_state_key == "") + else + false + end + end) + end end diff --git a/lib/matrix_server/room.ex b/lib/matrix_server/room.ex index 767abba..9d23993 100644 --- a/lib/matrix_server/room.ex +++ b/lib/matrix_server/room.ex @@ -27,7 +27,7 @@ defmodule MatrixServer.Room do end def generate_room_id do - "!" <> MatrixServer.random_string(18) <> "@" <> MatrixServer.server_name() + "!" <> MatrixServer.random_string(18) <> ":" <> MatrixServer.server_name() end def update_forward_extremities(%Event{ diff --git a/lib/matrix_server/room_server.ex b/lib/matrix_server/room_server.ex index f88708a..cba4cd7 100644 --- a/lib/matrix_server/room_server.ex +++ b/lib/matrix_server/room_server.ex @@ -38,8 +38,16 @@ defmodule MatrixServer.RoomServer do Repo.transaction(fn -> with {:ok, create_room_event, state_set} <- room_creation_create_room(account, input, room, state_set), - {:ok, _join_creator_event, state_set} <- - room_creation_join_creator(account, room, state_set, create_room_event) do + {:ok, join_creator_event, state_set} <- + room_creation_join_creator(account, room, state_set, create_room_event), + {:ok, _power_levels_event, state_set} <- + room_creation_power_levels( + account, + room, + state_set, + create_room_event, + join_creator_event + ) do {:ok, %{room_id: room_id, state_set: state_set}} else _ -> {:error, :something} @@ -61,11 +69,24 @@ defmodule MatrixServer.RoomServer do %Account{localpart: localpart}, %Room{id: room_id}, state_set, - %Event{event_id: create_room_event_id} + %Event{event_id: create_room_id} ) do Event.join(room_id, MatrixServer.get_mxid(localpart)) - |> Map.put(:auth_events, [create_room_event_id]) - |> Map.put(:prev_events, [create_room_event_id]) + |> Map.put(:auth_events, [create_room_id]) + |> Map.put(:prev_events, [create_room_id]) + |> verify_and_insert_event(state_set) + end + + defp room_creation_power_levels( + %Account{localpart: localpart}, + %Room{id: room_id}, + state_set, + %Event{event_id: create_room_id}, + %Event{event_id: join_creator_id} + ) do + Event.power_levels(room_id, MatrixServer.get_mxid(localpart)) + |> Map.put(:auth_events, [create_room_id, join_creator_id]) + |> Map.put(:prev_events, [join_creator_id]) |> verify_and_insert_event(state_set) end @@ -77,24 +98,28 @@ defmodule MatrixServer.RoomServer do # 4. Passes authorization rules based on the event's auth events, otherwise it is rejected. # 5. Passes authorization rules based on the state at the event, otherwise it is rejected. # 6. Passes authorization rules based on the current state of the room, otherwise it is "soft failed". - if StateResolution.is_authorized_by_auth_events(event) do - state_set = StateResolution.resolve(event, false) + if Event.prevalidate(event) do + if StateResolution.is_authorized_by_auth_events(event) do + state_set = StateResolution.resolve(event, false) - if StateResolution.is_authorized(event, state_set) do - if StateResolution.is_authorized(event, current_state_set) do - # TODO: Assume the event is a forward extremity, should check this actually. - Room.update_forward_extremities(event) - {:ok, event} = Repo.insert(event) - state_set = StateResolution.resolve_forward_extremities(event) - {:ok, event, state_set} + if StateResolution.is_authorized(event, state_set) do + if StateResolution.is_authorized(event, current_state_set) do + # We assume here that the event is always a forward extremity. + Room.update_forward_extremities(event) + {:ok, event} = Repo.insert(event) + state_set = StateResolution.resolve_forward_extremities(event) + {:ok, event, state_set} + else + {:error, :soft_failed} + end else - {:error, :soft_failed} + {:error, :rejected} end else {:error, :rejected} end else - {:error, :rejected} + {:error, :invalid} end end