diff --git a/lib/matrix_server.ex b/lib/matrix_server.ex index d3f085c..676905e 100644 --- a/lib/matrix_server.ex +++ b/lib/matrix_server.ex @@ -1,5 +1,5 @@ defmodule MatrixServer do - alias MatrixServer.OrderedMap + alias MatrixServer.EncodableMap def get_mxid(localpart) when is_binary(localpart) do "@#{localpart}:#{server_name()}" @@ -76,8 +76,7 @@ defmodule MatrixServer do def encode_canonical_json(object) do object - |> Map.drop([:signatures, :unsigned]) - |> OrderedMap.from_map() + |> EncodableMap.from_map() |> Jason.encode() end @@ -134,4 +133,11 @@ defmodule MatrixServer do datetime1 end end + + def encode_url_safe_base64(data) do + data + |> encode_unpadded_base64() + |> String.replace("+", "-") + |> String.replace("/", "_") + end end diff --git a/lib/matrix_server/ordered_map.ex b/lib/matrix_server/encodable_map.ex similarity index 59% rename from lib/matrix_server/ordered_map.ex rename to lib/matrix_server/encodable_map.ex index 088f87d..f3200ae 100644 --- a/lib/matrix_server/ordered_map.ex +++ b/lib/matrix_server/encodable_map.ex @@ -1,10 +1,10 @@ # https://github.com/michalmuskala/jason/issues/69 -defmodule MatrixServer.OrderedMap do - alias MatrixServer.OrderedMap +defmodule MatrixServer.EncodableMap do + alias MatrixServer.EncodableMap defstruct pairs: [] - defimpl Jason.Encoder, for: OrderedMap do + defimpl Jason.Encoder, for: EncodableMap do def encode(%{pairs: pairs}, opts) do Jason.Encode.keyword(pairs, opts) end @@ -14,6 +14,9 @@ defmodule MatrixServer.OrderedMap do pairs = map |> Enum.map(fn + {k, v} when is_struct(v, DateTime) -> + {k, DateTime.to_unix(v, :millisecond)} + {k, v} when is_map(v) -> {k, from_map(v)} @@ -22,6 +25,6 @@ defmodule MatrixServer.OrderedMap do end) |> Enum.sort() - %OrderedMap{pairs: pairs} + %EncodableMap{pairs: pairs} end end diff --git a/lib/matrix_server/key_server.ex b/lib/matrix_server/key_server.ex index ae44e15..00076bd 100644 --- a/lib/matrix_server/key_server.ex +++ b/lib/matrix_server/key_server.ex @@ -42,6 +42,8 @@ defmodule MatrixServer.KeyServer do # https://blog.swwomm.com/2020/09/elixir-ed25519-signatures-with-enacl.html defp sign_object(object, private_key) do + object = Map.drop(object, [:signatures, :unsigned]) + with {:ok, json} <- MatrixServer.encode_canonical_json(object) do signature = json diff --git a/lib/matrix_server/room_server.ex b/lib/matrix_server/room_server.ex index 78d2ee0..a3a3f81 100644 --- a/lib/matrix_server/room_server.ex +++ b/lib/matrix_server/room_server.ex @@ -70,72 +70,85 @@ defmodule MatrixServer.RoomServer do end @impl true - def handle_call( - {:create_room, account, - %CreateRoom{room_version: room_version, name: name, topic: topic, preset: preset}}, - _from, - %{room_id: room_id} = state - ) do - result = - Repo.transaction(fn -> - room = Repo.one!(from r in Room, where: r.id == ^room_id) - create_room = Event.create_room(room, account, room_version) - join_creator = Event.join(room, account, [create_room.event_id]) - pls = Event.power_levels(room, account, [create_room.event_id, join_creator.event_id]) - auth_events = [create_room.event_id, join_creator.event_id, pls.event_id] - name_event = if name, do: Event.name(room, account, name, auth_events) - topic_event = if topic, do: Event.topic(room, account, topic, auth_events) + def handle_call({:create_room, account, input}, _from, %{room_id: room_id} = state) do + # TODO: power_level_content_override, initial_state, invite, invite_3pid + room = Repo.one!(from r in Room, where: r.id == ^room_id) - # TODO: power_level_content_override, initial_state, invite, invite_3pid - events = - [create_room, join_creator, pls] ++ - room_creation_preset(account, preset, room, auth_events) ++ - [name_event, topic_event] - - result = - events - |> Enum.reject(&Kernel.is_nil/1) - |> Enum.reduce_while({%{}, room}, fn event, {state_set, room} -> - case verify_and_insert_event(event, state_set, room) do - {:ok, state_set, room} -> {:cont, {state_set, room}} - {:error, reason} -> {:halt, {:error, reason}} - end - end) - - case result do - {:error, reason} -> - Repo.rollback(reason) - - {state_set, room} -> - serialized_state_set = - Enum.map(state_set, fn {{type, state_key}, event} -> - [type, state_key, event.event_id] - end) - - Repo.update!(change(room, state: serialized_state_set)) - state_set + case create_room_events(room, account, input) do + events when is_list(events) -> + case Repo.transaction(create_room_insert_events(room, events)) do + {:ok, state_set} -> {:reply, {:ok, room_id}, %{state | state_set: state_set}} + {:error, reason} -> {:reply, {:error, reason}, state} + _ -> {:reply, {:error, :unknown}, state} end - end) - case result do - {:ok, state_set} -> {:reply, {:ok, room_id}, %{state | state_set: state_set}} - {:error, reason} -> {:reply, {:error, reason}, state} - _ -> {:reply, {:error, :unknown}, state} + :error -> + {:reply, {:error, :event_creation}, state} end end def handle_call({:server_in_room, domain}, _from, %{state_set: state_set} = state) do - result = Enum.any?(state_set, fn - {{"m.room.member", user_id}, %Event{content: %{"membership" => "join"}}} -> - MatrixServer.get_domain(user_id) == domain + result = + Enum.any?(state_set, fn + {{"m.room.member", user_id}, %Event{content: %{"membership" => "join"}}} -> + MatrixServer.get_domain(user_id) == domain - _ -> - false - end) + _ -> + false + end) {:reply, result, state} end + defp create_room_events(room, account, %CreateRoom{ + room_version: room_version, + name: name, + topic: topic, + preset: preset + }) do + with {:ok, create_room} <- Event.create_room(room, account, room_version), + {:ok, join_creator} <- Event.join(room, account, [create_room]), + {:ok, pls} <- Event.power_levels(room, account, [create_room, join_creator]), + auth_events <- [create_room, join_creator, pls], + {:ok, preset_events} <- room_creation_preset(account, preset, room, auth_events), + {:ok, name_event} <- + if(name, do: Event.name(room, account, name, auth_events), else: {:ok, nil}), + {:ok, topic_event} <- + if(topic, do: Event.topic(room, account, topic, auth_events), else: {:ok, nil}) do + events = [create_room, join_creator, pls] ++ preset_events ++ [name_event, topic_event] + + Enum.reject(events, &Kernel.is_nil/1) + else + _ -> :error + end + end + + defp create_room_insert_events(room, events) do + fn -> + result = + Enum.reduce_while(events, {%{}, room}, fn event, {state_set, room} -> + case verify_and_insert_event(event, state_set, room) do + {:ok, state_set, room} -> {:cont, {state_set, room}} + {:error, reason} -> {:halt, {:error, reason}} + end + end) + + case result do + {:error, reason} -> + Repo.rollback(reason) + + {state_set, room} -> + serialized_state_set = + Enum.map(state_set, fn {{type, state_key}, event} -> + [type, state_key, event.event_id] + end) + + Repo.update!(change(room, state: serialized_state_set)) + state_set + end + end + end + # TODO: trusted_private_chat: # All invitees are given the same power level as the room creator. defp room_creation_preset(account, nil, %Room{visibility: visibility} = room, auth_events) do @@ -156,11 +169,11 @@ defmodule MatrixServer.RoomServer do "public_chat" -> {"public", "shared", "forbidden"} end - [ - Event.join_rules(room, account, join_rule, auth_events), - Event.history_visibility(room, account, his_vis, auth_events), - Event.guest_access(room, account, guest_access, auth_events) - ] + with {:ok, join_rules} <- Event.join_rules(room, account, join_rule, auth_events), + {:ok, his_vis} <- Event.history_visibility(room, account, his_vis, auth_events), + {:ok, guest_access} <- Event.guest_access(room, account, guest_access, auth_events) do + {:ok, [join_rules, his_vis, guest_access]} + end end defp verify_and_insert_event( diff --git a/lib/matrix_server/schema/event.ex b/lib/matrix_server/schema/event.ex index f95349a..20e76da 100644 --- a/lib/matrix_server/schema/event.ex +++ b/lib/matrix_server/schema/event.ex @@ -3,7 +3,7 @@ defmodule MatrixServer.Event do import Ecto.Query - alias MatrixServer.{Repo, Room, Event, Account, OrderedMap, KeyServer} + alias MatrixServer.{Repo, Room, Event, Account, EncodableMap, KeyServer} @primary_key {:event_id, :string, []} schema "events" do @@ -26,46 +26,58 @@ defmodule MatrixServer.Event do %Event{ room_id: room_id, sender: MatrixServer.get_mxid(localpart), - event_id: generate_event_id(), origin_server_ts: DateTime.utc_now(), prev_events: [], auth_events: [] } end - def create_room(room, %Account{localpart: localpart} = creator, room_version, auth_events \\ []) do + def create_room( + room, + %Account{localpart: localpart} = creator, + room_version, + generate_id \\ true + ) do mxid = MatrixServer.get_mxid(localpart) - %Event{ + event = %Event{ new(room, creator) | type: "m.room.create", state_key: "", content: %{ "creator" => mxid, "room_version" => room_version || MatrixServer.default_room_version() - }, - auth_events: auth_events + } } + + if generate_id, do: set_event_id(event), else: event end - def join(room, %Account{localpart: localpart} = sender, auth_events \\ []) do + def join(room, %Account{localpart: localpart} = sender, auth_events, generate_id \\ true) do mxid = MatrixServer.get_mxid(localpart) - %Event{ + event = %Event{ new(room, sender) | type: "m.room.member", state_key: mxid, content: %{ "membership" => "join" }, - auth_events: auth_events + auth_events: Enum.map(auth_events, & &1.event_id) } + + if generate_id, do: set_event_id(event), else: event end - def power_levels(room, %Account{localpart: localpart} = sender, auth_events \\ []) do + def power_levels( + room, + %Account{localpart: localpart} = sender, + auth_events, + generate_id \\ true + ) do mxid = MatrixServer.get_mxid(localpart) - %Event{ + event = %Event{ new(room, sender) | type: "m.room.power_levels", state_key: "", @@ -85,72 +97,80 @@ defmodule MatrixServer.Event do "room" => 50 } }, - auth_events: auth_events + auth_events: Enum.map(auth_events, & &1.event_id) } + + if generate_id, do: set_event_id(event), else: event end - def name(room, sender, name, auth_events \\ []) do - %Event{ + def name(room, sender, name, auth_events, generate_id \\ true) do + event = %Event{ new(room, sender) | type: "m.room.name", state_key: "", content: %{ "name" => name }, - auth_events: auth_events + auth_events: Enum.map(auth_events, & &1.event_id) } + + if generate_id, do: set_event_id(event), else: event end - def topic(room, sender, topic, auth_events \\ []) do - %Event{ + def topic(room, sender, topic, auth_events, generate_id \\ true) do + event = %Event{ new(room, sender) | type: "m.room.topic", state_key: "", content: %{ "topic" => topic }, - auth_events: auth_events + auth_events: Enum.map(auth_events, & &1.event_id) } + + if generate_id, do: set_event_id(event), else: event end - def join_rules(room, sender, join_rule, auth_events \\ []) do - %Event{ + def join_rules(room, sender, join_rule, auth_events, generate_id \\ true) do + event = %Event{ new(room, sender) | type: "m.room.join_rules", state_key: "", content: %{ "join_rule" => join_rule }, - auth_events: auth_events + auth_events: Enum.map(auth_events, & &1.event_id) } + + if generate_id, do: set_event_id(event), else: event end - def history_visibility(room, sender, history_visibility, auth_events \\ []) do - %Event{ + def history_visibility(room, sender, history_visibility, auth_events, generate_id \\ true) do + event = %Event{ new(room, sender) | type: "m.room.history_visibility", state_key: "", content: %{ "history_visibility" => history_visibility }, - auth_events: auth_events + auth_events: Enum.map(auth_events, & &1.event_id) } + + if generate_id, do: set_event_id(event), else: event end - def guest_access(room, sender, guest_access, auth_events \\ []) do - %Event{ + def guest_access(room, sender, guest_access, auth_events, generate_id \\ true) do + event = %Event{ new(room, sender) | type: "m.room.guest_access", state_key: "", content: %{ "guest_access" => guest_access }, - auth_events: auth_events + auth_events: Enum.map(auth_events, & &1.event_id) } - end - def generate_event_id do - "$" <> MatrixServer.random_string(17) <> ":" <> MatrixServer.server_name() + if generate_id, do: set_event_id(event), else: event end def is_control_event(%Event{type: "m.room.power_levels", state_key: ""}), do: true @@ -275,7 +295,10 @@ defmodule MatrixServer.Event do end def sign(event) do - content_hash = calculate_content_hash(event) + content_hash = + event + |> calculate_content_hash() + |> MatrixServer.encode_unpadded_base64() event |> Map.put(:hashes, %{"sha256" => content_hash}) @@ -284,20 +307,15 @@ defmodule MatrixServer.Event do end defp calculate_content_hash(event) do - result = + m = event |> MatrixServer.to_serializable_map() |> Map.drop([:unsigned, :signature, :hashes]) - |> OrderedMap.from_map() + |> EncodableMap.from_map() |> Jason.encode() - case result do - {:ok, json} -> - :crypto.hash(:sha256, json) - |> MatrixServer.encode_unpadded_base64() - - error -> - error + with {:ok, json} <- Jason.encode(m) do + :crypto.hash(:sha256, json) end end @@ -346,4 +364,29 @@ defmodule MatrixServer.Event do "users", "users_default" ]) + + defp redact_content(_, _), do: %{} + + def set_event_id(event) do + with {:ok, event_id} <- generate_event_id(event) do + {:ok, %Event{event | event_id: event_id}} + end + end + + defp generate_event_id(event) do + with {:ok, hash} <- calculate_reference_hash(event) do + {:ok, "$" <> MatrixServer.encode_url_safe_base64(hash)} + end + end + + defp calculate_reference_hash(event) do + redacted_event = + event + |> redact() + |> Map.drop([:unsigned, :signature, :age_ts]) + + with {:ok, json} <- MatrixServer.encode_canonical_json(redacted_event) do + {:ok, :crypto.hash(:sha256, json)} + end + end end diff --git a/lib/matrix_server_web/federation/controllers/event_controller.ex b/lib/matrix_server_web/federation/controllers/event_controller.ex index b3068c7..9721a0d 100644 --- a/lib/matrix_server_web/federation/controllers/event_controller.ex +++ b/lib/matrix_server_web/federation/controllers/event_controller.ex @@ -2,7 +2,9 @@ defmodule MatrixServerWeb.Federation.EventController do use MatrixServerWeb, :controller use MatrixServerWeb.Federation.AuthenticateServer - def event(conn, %{"event_id" => event_id}) do - + def event(conn, %{"event_id" => _event_id}) do + conn + |> put_status(200) + |> json(%{}) end end