Calculate and set event id using reference hash
This commit is contained in:
parent
8696b0fb96
commit
1781575e75
6 changed files with 177 additions and 108 deletions
|
@ -1,5 +1,5 @@
|
||||||
defmodule MatrixServer do
|
defmodule MatrixServer do
|
||||||
alias MatrixServer.OrderedMap
|
alias MatrixServer.EncodableMap
|
||||||
|
|
||||||
def get_mxid(localpart) when is_binary(localpart) do
|
def get_mxid(localpart) when is_binary(localpart) do
|
||||||
"@#{localpart}:#{server_name()}"
|
"@#{localpart}:#{server_name()}"
|
||||||
|
@ -76,8 +76,7 @@ defmodule MatrixServer do
|
||||||
|
|
||||||
def encode_canonical_json(object) do
|
def encode_canonical_json(object) do
|
||||||
object
|
object
|
||||||
|> Map.drop([:signatures, :unsigned])
|
|> EncodableMap.from_map()
|
||||||
|> OrderedMap.from_map()
|
|
||||||
|> Jason.encode()
|
|> Jason.encode()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -134,4 +133,11 @@ defmodule MatrixServer do
|
||||||
datetime1
|
datetime1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def encode_url_safe_base64(data) do
|
||||||
|
data
|
||||||
|
|> encode_unpadded_base64()
|
||||||
|
|> String.replace("+", "-")
|
||||||
|
|> String.replace("/", "_")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
# https://github.com/michalmuskala/jason/issues/69
|
# https://github.com/michalmuskala/jason/issues/69
|
||||||
defmodule MatrixServer.OrderedMap do
|
defmodule MatrixServer.EncodableMap do
|
||||||
alias MatrixServer.OrderedMap
|
alias MatrixServer.EncodableMap
|
||||||
|
|
||||||
defstruct pairs: []
|
defstruct pairs: []
|
||||||
|
|
||||||
defimpl Jason.Encoder, for: OrderedMap do
|
defimpl Jason.Encoder, for: EncodableMap do
|
||||||
def encode(%{pairs: pairs}, opts) do
|
def encode(%{pairs: pairs}, opts) do
|
||||||
Jason.Encode.keyword(pairs, opts)
|
Jason.Encode.keyword(pairs, opts)
|
||||||
end
|
end
|
||||||
|
@ -14,6 +14,9 @@ defmodule MatrixServer.OrderedMap do
|
||||||
pairs =
|
pairs =
|
||||||
map
|
map
|
||||||
|> Enum.map(fn
|
|> Enum.map(fn
|
||||||
|
{k, v} when is_struct(v, DateTime) ->
|
||||||
|
{k, DateTime.to_unix(v, :millisecond)}
|
||||||
|
|
||||||
{k, v} when is_map(v) ->
|
{k, v} when is_map(v) ->
|
||||||
{k, from_map(v)}
|
{k, from_map(v)}
|
||||||
|
|
||||||
|
@ -22,6 +25,6 @@ defmodule MatrixServer.OrderedMap do
|
||||||
end)
|
end)
|
||||||
|> Enum.sort()
|
|> Enum.sort()
|
||||||
|
|
||||||
%OrderedMap{pairs: pairs}
|
%EncodableMap{pairs: pairs}
|
||||||
end
|
end
|
||||||
end
|
end
|
|
@ -42,6 +42,8 @@ defmodule MatrixServer.KeyServer do
|
||||||
|
|
||||||
# https://blog.swwomm.com/2020/09/elixir-ed25519-signatures-with-enacl.html
|
# https://blog.swwomm.com/2020/09/elixir-ed25519-signatures-with-enacl.html
|
||||||
defp sign_object(object, private_key) do
|
defp sign_object(object, private_key) do
|
||||||
|
object = Map.drop(object, [:signatures, :unsigned])
|
||||||
|
|
||||||
with {:ok, json} <- MatrixServer.encode_canonical_json(object) do
|
with {:ok, json} <- MatrixServer.encode_canonical_json(object) do
|
||||||
signature =
|
signature =
|
||||||
json
|
json
|
||||||
|
|
|
@ -70,32 +70,63 @@ defmodule MatrixServer.RoomServer do
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_call(
|
def handle_call({:create_room, account, input}, _from, %{room_id: room_id} = state) do
|
||||||
{: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)
|
|
||||||
|
|
||||||
# TODO: power_level_content_override, initial_state, invite, invite_3pid
|
# TODO: power_level_content_override, initial_state, invite, invite_3pid
|
||||||
events =
|
room = Repo.one!(from r in Room, where: r.id == ^room_id)
|
||||||
[create_room, join_creator, pls] ++
|
|
||||||
room_creation_preset(account, preset, room, auth_events) ++
|
|
||||||
[name_event, topic_event]
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
:error ->
|
||||||
|
{:reply, {:error, :event_creation}, state}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_call({:server_in_room, domain}, _from, %{state_set: state_set} = state) do
|
||||||
result =
|
result =
|
||||||
events
|
Enum.any?(state_set, fn
|
||||||
|> Enum.reject(&Kernel.is_nil/1)
|
{{"m.room.member", user_id}, %Event{content: %{"membership" => "join"}}} ->
|
||||||
|> Enum.reduce_while({%{}, room}, fn event, {state_set, room} ->
|
MatrixServer.get_domain(user_id) == domain
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
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
|
case verify_and_insert_event(event, state_set, room) do
|
||||||
{:ok, state_set, room} -> {:cont, {state_set, room}}
|
{:ok, state_set, room} -> {:cont, {state_set, room}}
|
||||||
{:error, reason} -> {:halt, {:error, reason}}
|
{:error, reason} -> {:halt, {:error, reason}}
|
||||||
|
@ -115,27 +146,9 @@ defmodule MatrixServer.RoomServer do
|
||||||
Repo.update!(change(room, state: serialized_state_set))
|
Repo.update!(change(room, state: serialized_state_set))
|
||||||
state_set
|
state_set
|
||||||
end
|
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}
|
|
||||||
end
|
end
|
||||||
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
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
false
|
|
||||||
end)
|
|
||||||
|
|
||||||
{:reply, result, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
# TODO: trusted_private_chat:
|
# TODO: trusted_private_chat:
|
||||||
# All invitees are given the same power level as the room creator.
|
# 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
|
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"}
|
"public_chat" -> {"public", "shared", "forbidden"}
|
||||||
end
|
end
|
||||||
|
|
||||||
[
|
with {:ok, join_rules} <- Event.join_rules(room, account, join_rule, auth_events),
|
||||||
Event.join_rules(room, account, join_rule, auth_events),
|
{:ok, his_vis} <- Event.history_visibility(room, account, his_vis, auth_events),
|
||||||
Event.history_visibility(room, account, his_vis, auth_events),
|
{:ok, guest_access} <- Event.guest_access(room, account, guest_access, auth_events) do
|
||||||
Event.guest_access(room, account, guest_access, auth_events)
|
{:ok, [join_rules, his_vis, guest_access]}
|
||||||
]
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp verify_and_insert_event(
|
defp verify_and_insert_event(
|
||||||
|
|
|
@ -3,7 +3,7 @@ defmodule MatrixServer.Event do
|
||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
alias MatrixServer.{Repo, Room, Event, Account, OrderedMap, KeyServer}
|
alias MatrixServer.{Repo, Room, Event, Account, EncodableMap, KeyServer}
|
||||||
|
|
||||||
@primary_key {:event_id, :string, []}
|
@primary_key {:event_id, :string, []}
|
||||||
schema "events" do
|
schema "events" do
|
||||||
|
@ -26,46 +26,58 @@ defmodule MatrixServer.Event do
|
||||||
%Event{
|
%Event{
|
||||||
room_id: room_id,
|
room_id: room_id,
|
||||||
sender: MatrixServer.get_mxid(localpart),
|
sender: MatrixServer.get_mxid(localpart),
|
||||||
event_id: generate_event_id(),
|
|
||||||
origin_server_ts: DateTime.utc_now(),
|
origin_server_ts: DateTime.utc_now(),
|
||||||
prev_events: [],
|
prev_events: [],
|
||||||
auth_events: []
|
auth_events: []
|
||||||
}
|
}
|
||||||
end
|
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)
|
mxid = MatrixServer.get_mxid(localpart)
|
||||||
|
|
||||||
%Event{
|
event = %Event{
|
||||||
new(room, creator)
|
new(room, creator)
|
||||||
| type: "m.room.create",
|
| type: "m.room.create",
|
||||||
state_key: "",
|
state_key: "",
|
||||||
content: %{
|
content: %{
|
||||||
"creator" => mxid,
|
"creator" => mxid,
|
||||||
"room_version" => room_version || MatrixServer.default_room_version()
|
"room_version" => room_version || MatrixServer.default_room_version()
|
||||||
},
|
|
||||||
auth_events: auth_events
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if generate_id, do: set_event_id(event), else: event
|
||||||
end
|
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)
|
mxid = MatrixServer.get_mxid(localpart)
|
||||||
|
|
||||||
%Event{
|
event = %Event{
|
||||||
new(room, sender)
|
new(room, sender)
|
||||||
| type: "m.room.member",
|
| type: "m.room.member",
|
||||||
state_key: mxid,
|
state_key: mxid,
|
||||||
content: %{
|
content: %{
|
||||||
"membership" => "join"
|
"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
|
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)
|
mxid = MatrixServer.get_mxid(localpart)
|
||||||
|
|
||||||
%Event{
|
event = %Event{
|
||||||
new(room, sender)
|
new(room, sender)
|
||||||
| type: "m.room.power_levels",
|
| type: "m.room.power_levels",
|
||||||
state_key: "",
|
state_key: "",
|
||||||
|
@ -85,72 +97,80 @@ defmodule MatrixServer.Event do
|
||||||
"room" => 50
|
"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
|
end
|
||||||
|
|
||||||
def name(room, sender, name, auth_events \\ []) do
|
def name(room, sender, name, auth_events, generate_id \\ true) do
|
||||||
%Event{
|
event = %Event{
|
||||||
new(room, sender)
|
new(room, sender)
|
||||||
| type: "m.room.name",
|
| type: "m.room.name",
|
||||||
state_key: "",
|
state_key: "",
|
||||||
content: %{
|
content: %{
|
||||||
"name" => name
|
"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
|
end
|
||||||
|
|
||||||
def topic(room, sender, topic, auth_events \\ []) do
|
def topic(room, sender, topic, auth_events, generate_id \\ true) do
|
||||||
%Event{
|
event = %Event{
|
||||||
new(room, sender)
|
new(room, sender)
|
||||||
| type: "m.room.topic",
|
| type: "m.room.topic",
|
||||||
state_key: "",
|
state_key: "",
|
||||||
content: %{
|
content: %{
|
||||||
"topic" => topic
|
"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
|
end
|
||||||
|
|
||||||
def join_rules(room, sender, join_rule, auth_events \\ []) do
|
def join_rules(room, sender, join_rule, auth_events, generate_id \\ true) do
|
||||||
%Event{
|
event = %Event{
|
||||||
new(room, sender)
|
new(room, sender)
|
||||||
| type: "m.room.join_rules",
|
| type: "m.room.join_rules",
|
||||||
state_key: "",
|
state_key: "",
|
||||||
content: %{
|
content: %{
|
||||||
"join_rule" => join_rule
|
"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
|
end
|
||||||
|
|
||||||
def history_visibility(room, sender, history_visibility, auth_events \\ []) do
|
def history_visibility(room, sender, history_visibility, auth_events, generate_id \\ true) do
|
||||||
%Event{
|
event = %Event{
|
||||||
new(room, sender)
|
new(room, sender)
|
||||||
| type: "m.room.history_visibility",
|
| type: "m.room.history_visibility",
|
||||||
state_key: "",
|
state_key: "",
|
||||||
content: %{
|
content: %{
|
||||||
"history_visibility" => history_visibility
|
"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
|
end
|
||||||
|
|
||||||
def guest_access(room, sender, guest_access, auth_events \\ []) do
|
def guest_access(room, sender, guest_access, auth_events, generate_id \\ true) do
|
||||||
%Event{
|
event = %Event{
|
||||||
new(room, sender)
|
new(room, sender)
|
||||||
| type: "m.room.guest_access",
|
| type: "m.room.guest_access",
|
||||||
state_key: "",
|
state_key: "",
|
||||||
content: %{
|
content: %{
|
||||||
"guest_access" => guest_access
|
"guest_access" => guest_access
|
||||||
},
|
},
|
||||||
auth_events: auth_events
|
auth_events: Enum.map(auth_events, & &1.event_id)
|
||||||
}
|
}
|
||||||
end
|
|
||||||
|
|
||||||
def generate_event_id do
|
if generate_id, do: set_event_id(event), else: event
|
||||||
"$" <> MatrixServer.random_string(17) <> ":" <> MatrixServer.server_name()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def is_control_event(%Event{type: "m.room.power_levels", state_key: ""}), do: true
|
def is_control_event(%Event{type: "m.room.power_levels", state_key: ""}), do: true
|
||||||
|
@ -275,7 +295,10 @@ defmodule MatrixServer.Event do
|
||||||
end
|
end
|
||||||
|
|
||||||
def sign(event) do
|
def sign(event) do
|
||||||
content_hash = calculate_content_hash(event)
|
content_hash =
|
||||||
|
event
|
||||||
|
|> calculate_content_hash()
|
||||||
|
|> MatrixServer.encode_unpadded_base64()
|
||||||
|
|
||||||
event
|
event
|
||||||
|> Map.put(:hashes, %{"sha256" => content_hash})
|
|> Map.put(:hashes, %{"sha256" => content_hash})
|
||||||
|
@ -284,20 +307,15 @@ defmodule MatrixServer.Event do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp calculate_content_hash(event) do
|
defp calculate_content_hash(event) do
|
||||||
result =
|
m =
|
||||||
event
|
event
|
||||||
|> MatrixServer.to_serializable_map()
|
|> MatrixServer.to_serializable_map()
|
||||||
|> Map.drop([:unsigned, :signature, :hashes])
|
|> Map.drop([:unsigned, :signature, :hashes])
|
||||||
|> OrderedMap.from_map()
|
|> EncodableMap.from_map()
|
||||||
|> Jason.encode()
|
|> Jason.encode()
|
||||||
|
|
||||||
case result do
|
with {:ok, json} <- Jason.encode(m) do
|
||||||
{:ok, json} ->
|
|
||||||
:crypto.hash(:sha256, json)
|
:crypto.hash(:sha256, json)
|
||||||
|> MatrixServer.encode_unpadded_base64()
|
|
||||||
|
|
||||||
error ->
|
|
||||||
error
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -346,4 +364,29 @@ defmodule MatrixServer.Event do
|
||||||
"users",
|
"users",
|
||||||
"users_default"
|
"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
|
end
|
||||||
|
|
|
@ -2,7 +2,9 @@ defmodule MatrixServerWeb.Federation.EventController do
|
||||||
use MatrixServerWeb, :controller
|
use MatrixServerWeb, :controller
|
||||||
use MatrixServerWeb.Federation.AuthenticateServer
|
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
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue