Implement retrieving a single event over federation
Fix url encoding during homeserver signature check
This commit is contained in:
parent
1881b7f3d6
commit
e510c3bb6a
14 changed files with 173 additions and 22 deletions
|
@ -38,7 +38,6 @@ defmodule MatrixServer do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO Eventually move to regex with named captures.
|
|
||||||
@spec get_localpart(String.t()) :: String.t() | nil
|
@spec get_localpart(String.t()) :: String.t() | nil
|
||||||
def get_localpart(id) do
|
def get_localpart(id) do
|
||||||
with [part, _] <- String.split(id, ":", parts: 2),
|
with [part, _] <- String.split(id, ":", parts: 2),
|
||||||
|
@ -85,17 +84,25 @@ defmodule MatrixServer do
|
||||||
# https://stackoverflow.com/questions/41523762/41671211
|
# https://stackoverflow.com/questions/41523762/41671211
|
||||||
@spec to_serializable_map(struct()) :: map()
|
@spec to_serializable_map(struct()) :: map()
|
||||||
def to_serializable_map(struct) do
|
def to_serializable_map(struct) do
|
||||||
association_fields = struct.__struct__.__schema__(:associations)
|
association_fields =
|
||||||
|
if Kernel.function_exported?(struct.__struct__, :__schema__, 1) do
|
||||||
|
struct.__struct__.__schema__(:associations)
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
waste_fields = association_fields ++ [:__meta__]
|
waste_fields = association_fields ++ [:__meta__]
|
||||||
|
|
||||||
struct
|
struct
|
||||||
|> Map.from_struct()
|
|> Map.from_struct()
|
||||||
|> Map.drop(waste_fields)
|
|> Enum.reject(fn {k, v} ->
|
||||||
|
is_nil(v) or k in waste_fields
|
||||||
|
end)
|
||||||
|
|> Enum.into(%{})
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec serialize_and_encode(struct()) :: {:ok, String.t()} | {:error, Jason.EncodeError.t()}
|
@spec serialize_and_encode(struct()) :: {:ok, String.t()} | {:error, Jason.EncodeError.t()}
|
||||||
def serialize_and_encode(struct) do
|
def serialize_and_encode(struct) do
|
||||||
# TODO: handle nil values in struct?
|
|
||||||
struct
|
struct
|
||||||
|> to_serializable_map()
|
|> to_serializable_map()
|
||||||
|> encode_canonical_json()
|
|> encode_canonical_json()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# https://github.com/michalmuskala/jason/issues/69
|
# https://github.com/michalmuskala/jason/issues/69
|
||||||
defmodule MatrixServer.EncodableMap do
|
defmodule MatrixServer.EncodableMap do
|
||||||
alias MatrixServer.EncodableMap
|
alias MatrixServer.{EncodableMap, Event}
|
||||||
alias MatrixServer.Types.{UserId, RoomId, EventId, GroupId, AliasId}
|
alias MatrixServer.Types.{UserId, RoomId, EventId, GroupId, AliasId}
|
||||||
|
|
||||||
defstruct pairs: []
|
defstruct pairs: []
|
||||||
|
@ -15,12 +15,10 @@ defmodule MatrixServer.EncodableMap 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}
|
{k, v}
|
||||||
when is_struct(v, UserId) or is_struct(v, RoomId) or is_struct(v, EventId) or
|
when is_struct(v, UserId) or is_struct(v, RoomId) or is_struct(v, EventId) or
|
||||||
is_struct(v, GroupId) or is_struct(v, AliasId) ->
|
is_struct(v, GroupId) or is_struct(v, AliasId) ->
|
||||||
|
# Simply convert IDs to a string.
|
||||||
{k, to_string(v)}
|
{k, to_string(v)}
|
||||||
|
|
||||||
{k, v} when is_map(v) ->
|
{k, v} when is_map(v) ->
|
||||||
|
|
|
@ -48,6 +48,8 @@ defmodule MatrixServer.KeyServer do
|
||||||
object = Map.drop(object, [:signatures, :unsigned])
|
object = Map.drop(object, [:signatures, :unsigned])
|
||||||
|
|
||||||
with {:ok, json} <- MatrixServer.encode_canonical_json(object) do
|
with {:ok, json} <- MatrixServer.encode_canonical_json(object) do
|
||||||
|
IO.puts(json)
|
||||||
|
|
||||||
signature =
|
signature =
|
||||||
json
|
json
|
||||||
|> :enacl.sign_detached(private_key)
|
|> :enacl.sign_detached(private_key)
|
||||||
|
|
|
@ -18,6 +18,9 @@ defmodule MatrixServer.RoomServer do
|
||||||
GenServer.start_link(__MODULE__, opts, name: name)
|
GenServer.start_link(__MODULE__, opts, name: name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec get_room_server(Room.t()) :: {:error, :not_found} | DynamicSupervisor.on_start_child()
|
||||||
|
def get_room_server(%Room{id: room_id}), do: get_room_server(room_id)
|
||||||
|
|
||||||
# Get room server pid, or spin one up for the room.
|
# Get room server pid, or spin one up for the room.
|
||||||
# If the room does not exist, return an error.
|
# If the room does not exist, return an error.
|
||||||
@spec get_room_server(String.t()) :: {:error, :not_found} | DynamicSupervisor.on_start_child()
|
@spec get_room_server(String.t()) :: {:error, :not_found} | DynamicSupervisor.on_start_child()
|
||||||
|
|
|
@ -9,7 +9,7 @@ defmodule MatrixServer.Event do
|
||||||
# TODO: Could refactor to also always set prev_events, but not necessary.
|
# TODO: Could refactor to also always set prev_events, but not necessary.
|
||||||
@type t :: %__MODULE__{
|
@type t :: %__MODULE__{
|
||||||
type: String.t(),
|
type: String.t(),
|
||||||
origin_server_ts: DateTime.t(),
|
origin_server_ts: integer(),
|
||||||
state_key: String.t(),
|
state_key: String.t(),
|
||||||
sender: UserId.t(),
|
sender: UserId.t(),
|
||||||
content: map(),
|
content: map(),
|
||||||
|
@ -23,7 +23,7 @@ defmodule MatrixServer.Event do
|
||||||
@primary_key {:event_id, :string, []}
|
@primary_key {:event_id, :string, []}
|
||||||
schema "events" do
|
schema "events" do
|
||||||
field :type, :string
|
field :type, :string
|
||||||
field :origin_server_ts, :utc_datetime_usec
|
field :origin_server_ts, :integer
|
||||||
field :state_key, :string
|
field :state_key, :string
|
||||||
field :sender, UserId
|
field :sender, UserId
|
||||||
field :content, :map
|
field :content, :map
|
||||||
|
@ -36,12 +36,38 @@ defmodule MatrixServer.Event do
|
||||||
belongs_to :room, Room, type: :string
|
belongs_to :room, Room, type: :string
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defimpl Jason.Encoder, for: Event do
|
||||||
|
@pdu_keys [
|
||||||
|
:auth_events,
|
||||||
|
:content,
|
||||||
|
:depth,
|
||||||
|
:hashes,
|
||||||
|
:origin,
|
||||||
|
:origin_server_ts,
|
||||||
|
:prev_events,
|
||||||
|
:redacts,
|
||||||
|
:room_id,
|
||||||
|
:sender,
|
||||||
|
:signatures,
|
||||||
|
:state_key,
|
||||||
|
:type,
|
||||||
|
:unsigned
|
||||||
|
]
|
||||||
|
|
||||||
|
def encode(event, opts) do
|
||||||
|
event
|
||||||
|
|> Map.take(@pdu_keys)
|
||||||
|
|> Map.update!(:sender, &Kernel.to_string/1)
|
||||||
|
|> Jason.Encode.map(opts)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@spec new(Room.t(), Account.t()) :: %Event{}
|
@spec new(Room.t(), Account.t()) :: %Event{}
|
||||||
def new(%Room{id: room_id}, %Account{localpart: localpart}) do
|
def new(%Room{id: room_id}, %Account{localpart: localpart}) do
|
||||||
%Event{
|
%Event{
|
||||||
room_id: room_id,
|
room_id: room_id,
|
||||||
sender: %UserId{localpart: localpart, domain: MatrixServer.server_name()},
|
sender: %UserId{localpart: localpart, domain: MatrixServer.server_name()},
|
||||||
origin_server_ts: DateTime.utc_now(),
|
origin_server_ts: DateTime.utc_now() |> DateTime.to_unix(:millisecond),
|
||||||
prev_events: [],
|
prev_events: [],
|
||||||
auth_events: []
|
auth_events: []
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ defmodule MatrixServer.ServerKeyInfo do
|
||||||
|
|
||||||
@primary_key {:server_name, :string, []}
|
@primary_key {:server_name, :string, []}
|
||||||
schema "server_key_info" do
|
schema "server_key_info" do
|
||||||
field :valid_until, :utc_datetime
|
field :valid_until, :integer
|
||||||
|
|
||||||
has_many :signing_keys, SigningKey, foreign_key: :server_name
|
has_many :signing_keys, SigningKey, foreign_key: :server_name
|
||||||
end
|
end
|
||||||
|
@ -43,7 +43,7 @@ defmodule MatrixServer.ServerKeyInfo do
|
||||||
verify_keys: verify_keys,
|
verify_keys: verify_keys,
|
||||||
valid_until_ts: valid_until_ts
|
valid_until_ts: valid_until_ts
|
||||||
}} <- HTTPClient.get_signing_keys(client),
|
}} <- HTTPClient.get_signing_keys(client),
|
||||||
{:ok, valid_until} <- DateTime.from_unix(valid_until_ts) do
|
{:ok, valid_until} <- DateTime.from_unix(valid_until_ts, :millisecond) do
|
||||||
signing_keys =
|
signing_keys =
|
||||||
Enum.map(verify_keys, fn {key_id, %{"key" => key}} ->
|
Enum.map(verify_keys, fn {key_id, %{"key" => key}} ->
|
||||||
[server_name: server_name, signing_key_id: key_id, signing_key: key]
|
[server_name: server_name, signing_key_id: key_id, signing_key: key]
|
||||||
|
@ -52,7 +52,8 @@ defmodule MatrixServer.ServerKeyInfo do
|
||||||
# Always check every week to prevent misuse.
|
# Always check every week to prevent misuse.
|
||||||
ski = %ServerKeyInfo{
|
ski = %ServerKeyInfo{
|
||||||
server_name: server_name,
|
server_name: server_name,
|
||||||
valid_until: MatrixServer.min_datetime(in_a_week, valid_until)
|
valid_until:
|
||||||
|
MatrixServer.min_datetime(in_a_week, valid_until) |> DateTime.to_unix(:millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
case upsert_multi(server_name, ski, signing_keys) |> Repo.transaction() do
|
case upsert_multi(server_name, ski, signing_keys) |> Repo.transaction() do
|
||||||
|
|
|
@ -18,6 +18,12 @@ defmodule MatrixServer.Types.UserId do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defimpl Jason.Encoder, for: UserId do
|
||||||
|
def encode(user_id, opts) do
|
||||||
|
Jason.Encode.string(to_string(user_id), opts)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def type(), do: :string
|
def type(), do: :string
|
||||||
|
|
||||||
def cast(s) when is_binary(s) do
|
def cast(s) when is_binary(s) do
|
||||||
|
|
|
@ -12,6 +12,7 @@ defmodule MatrixServerWeb.Error do
|
||||||
invalid_room_state:
|
invalid_room_state:
|
||||||
{400, "M_INVALID_ROOM_STATE", "The request would leave the room in an invalid state."},
|
{400, "M_INVALID_ROOM_STATE", "The request would leave the room in an invalid state."},
|
||||||
unauthorized: {400, "M_UNAUTHORIZED", "The request was unauthorized."},
|
unauthorized: {400, "M_UNAUTHORIZED", "The request was unauthorized."},
|
||||||
|
invalid_param: {400, "M_INVALID_PARAM", "A request parameter was invalid."},
|
||||||
unknown_token: {401, "M_UNKNOWN_TOKEN", "Invalid access token."},
|
unknown_token: {401, "M_UNKNOWN_TOKEN", "Invalid access token."},
|
||||||
missing_token: {401, "M_MISSING_TOKEN", "Access token required."},
|
missing_token: {401, "M_MISSING_TOKEN", "Access token required."},
|
||||||
not_found: {404, "M_NOT_FOUND", "The requested resource was not found."},
|
not_found: {404, "M_NOT_FOUND", "The requested resource was not found."},
|
||||||
|
|
|
@ -12,8 +12,18 @@ defmodule MatrixServerWeb.Federation.AuthenticateServer do
|
||||||
method: method,
|
method: method,
|
||||||
query_string: query_string
|
query_string: query_string
|
||||||
}) do
|
}) do
|
||||||
|
# TODO: This will break if request ends with '?'.
|
||||||
|
uri = URI.decode_www_form(path)
|
||||||
|
|
||||||
|
uri =
|
||||||
|
if String.length(query_string) > 0 do
|
||||||
|
uri <> "?" <> URI.decode_www_form(query_string)
|
||||||
|
else
|
||||||
|
uri
|
||||||
|
end
|
||||||
|
|
||||||
object_to_sign = %{
|
object_to_sign = %{
|
||||||
uri: path <> "?" <> URI.decode_www_form(query_string),
|
uri: uri,
|
||||||
method: method,
|
method: method,
|
||||||
destination: MatrixServer.server_name()
|
destination: MatrixServer.server_name()
|
||||||
}
|
}
|
||||||
|
@ -39,6 +49,7 @@ defmodule MatrixServerWeb.Federation.AuthenticateServer do
|
||||||
ServerKeyInfo.with_fresh_signing_keys(origin) do
|
ServerKeyInfo.with_fresh_signing_keys(origin) do
|
||||||
Enum.find_value(keys, false, fn %SigningKey{signing_key: signing_key} ->
|
Enum.find_value(keys, false, fn %SigningKey{signing_key: signing_key} ->
|
||||||
with {:ok, decoded_key} <- MatrixServer.decode_base64(signing_key) do
|
with {:ok, decoded_key} <- MatrixServer.decode_base64(signing_key) do
|
||||||
|
IO.puts(encoded_object)
|
||||||
MatrixServer.sign_verify(raw_sig, encoded_object, decoded_key)
|
MatrixServer.sign_verify(raw_sig, encoded_object, decoded_key)
|
||||||
else
|
else
|
||||||
_ -> false
|
_ -> false
|
||||||
|
|
|
@ -2,9 +2,44 @@ 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
|
import MatrixServerWeb.Error
|
||||||
conn
|
import Ecto.Query
|
||||||
|> put_status(200)
|
|
||||||
|> json(%{})
|
alias MatrixServer.{Repo, Event, RoomServer}
|
||||||
|
alias MatrixServerWeb.Federation.Transaction
|
||||||
|
|
||||||
|
def event(%Plug.Conn{assigns: %{origin: origin}} = conn, %{"event_id" => event_id}) do
|
||||||
|
query =
|
||||||
|
Event
|
||||||
|
|> where([e], e.event_id == ^event_id)
|
||||||
|
|> preload(:room)
|
||||||
|
|
||||||
|
case Repo.one(query) do
|
||||||
|
%Event{room: room} = event ->
|
||||||
|
case RoomServer.get_room_server(room) do
|
||||||
|
{:ok, pid} ->
|
||||||
|
if RoomServer.server_in_room(pid, origin) do
|
||||||
|
data = Transaction.new([event])
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_status(200)
|
||||||
|
|> json(data)
|
||||||
|
else
|
||||||
|
put_error(
|
||||||
|
conn,
|
||||||
|
:unauthorized,
|
||||||
|
"Origin server is not allowed to see requested event."
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
put_error(conn, :unknown)
|
||||||
|
end
|
||||||
|
|
||||||
|
nil ->
|
||||||
|
put_error(conn, :not_found, "Event or room not found.")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def event(conn, _), do: put_error(conn, :bad_json)
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,7 +20,7 @@ defmodule MatrixServerWeb.Federation.KeyController do
|
||||||
server_name: MatrixServer.server_name(),
|
server_name: MatrixServer.server_name(),
|
||||||
verify_keys: keys,
|
verify_keys: keys,
|
||||||
old_verify_keys: %{},
|
old_verify_keys: %{},
|
||||||
valid_until_ts: valid_until
|
valid_until_ts: DateTime.to_unix(valid_until, :millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
case KeyServer.sign_object(data) do
|
case KeyServer.sign_object(data) do
|
||||||
|
|
|
@ -29,7 +29,9 @@ defmodule MatrixServerWeb.Federation.HTTPClient do
|
||||||
with {:ok,
|
with {:ok,
|
||||||
%GetSigningKeys{server_name: server_name, verify_keys: verify_keys, signatures: sigs} =
|
%GetSigningKeys{server_name: server_name, verify_keys: verify_keys, signatures: sigs} =
|
||||||
response} <- tesla_request(:get, client, path, GetSigningKeys),
|
response} <- tesla_request(:get, client, path, GetSigningKeys),
|
||||||
{:ok, encoded_body} <- MatrixServer.serialize_and_encode(response),
|
serializable_response <- MatrixServer.to_serializable_map(response),
|
||||||
|
serializable_response <- Map.drop(serializable_response, [:signatures]),
|
||||||
|
{:ok, encoded_body} <- MatrixServer.encode_canonical_json(serializable_response),
|
||||||
server_sigs when not is_nil(server_sigs) <- sigs[server_name] do
|
server_sigs when not is_nil(server_sigs) <- sigs[server_name] do
|
||||||
# For each verify key, check if there is a matching signature.
|
# For each verify key, check if there is a matching signature.
|
||||||
# If not, invalidate the whole response.
|
# If not, invalidate the whole response.
|
||||||
|
@ -58,12 +60,20 @@ defmodule MatrixServerWeb.Federation.HTTPClient do
|
||||||
Tesla.get(client, path)
|
Tesla.get(client, path)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_event(client, event_id) do
|
||||||
|
path = RouteHelpers.event_path(Endpoint, :event, event_id)
|
||||||
|
|
||||||
|
Tesla.get(client, path)
|
||||||
|
end
|
||||||
|
|
||||||
defp tesla_request(method, client, path, request_schema) do
|
defp tesla_request(method, client, path, request_schema) do
|
||||||
with {:ok, %Tesla.Env{body: body}} <- Tesla.request(client, url: path, method: method),
|
with {:ok, %Tesla.Env{body: body}} <- Tesla.request(client, url: path, method: method),
|
||||||
%Ecto.Changeset{valid?: true} = cs <- apply(request_schema, :changeset, [body]) do
|
%Ecto.Changeset{valid?: true} = cs <- apply(request_schema, :changeset, [body]) do
|
||||||
{:ok, Ecto.Changeset.apply_changes(cs)}
|
{:ok, Ecto.Changeset.apply_changes(cs)}
|
||||||
else
|
else
|
||||||
_ -> :error
|
x ->
|
||||||
|
IO.inspect(x)
|
||||||
|
:error
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
36
lib/matrix_server_web/federation/transaction.ex
Normal file
36
lib/matrix_server_web/federation/transaction.ex
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
defmodule MatrixServerWeb.Federation.Transaction do
|
||||||
|
alias MatrixServer.Event
|
||||||
|
alias MatrixServerWeb.Federation.Transaction
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
@type edu :: any()
|
||||||
|
|
||||||
|
@type t :: %__MODULE__{
|
||||||
|
origin: String.t(),
|
||||||
|
origin_server_ts: integer(),
|
||||||
|
pdus: [Event.t()],
|
||||||
|
edus: [edu()] | nil
|
||||||
|
}
|
||||||
|
|
||||||
|
defstruct [:origin, :origin_server_ts, :pdus, :edus]
|
||||||
|
|
||||||
|
defimpl Jason.Encoder, for: Transaction do
|
||||||
|
@fields [:origin, :origin_server_ts, :pdus, :edus]
|
||||||
|
|
||||||
|
def encode(transaction, opts) do
|
||||||
|
transaction
|
||||||
|
|> Map.take(@fields)
|
||||||
|
|> Jason.Encode.map(opts)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec new([Event.t()], [edu()] | nil) :: t()
|
||||||
|
def new(pdu_events, edus \\ nil) do
|
||||||
|
%Transaction{
|
||||||
|
origin: MatrixServer.server_name(),
|
||||||
|
origin_server_ts: System.os_time(:millisecond),
|
||||||
|
pdus: Enum.map(pdu_events, &MatrixServer.to_serializable_map/1),
|
||||||
|
edus: edus
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,15 @@
|
||||||
|
defmodule MatrixServer.Repo.Migrations.MoveTimestampsToIntsAgain do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table(:events) do
|
||||||
|
remove :origin_server_ts, :utc_datetime_usec, null: false
|
||||||
|
add :origin_server_ts, :bigint, null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
alter table(:server_key_info) do
|
||||||
|
remove :valid_until, :utc_datetime_usec, null: false
|
||||||
|
add :valid_until, :bigint, default: 0, null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue