diff --git a/lib/matrix_server.ex b/lib/matrix_server.ex index 4cc0a2f..e77af16 100644 --- a/lib/matrix_server.ex +++ b/lib/matrix_server.ex @@ -38,7 +38,6 @@ defmodule MatrixServer do end end - # TODO Eventually move to regex with named captures. @spec get_localpart(String.t()) :: String.t() | nil def get_localpart(id) do with [part, _] <- String.split(id, ":", parts: 2), @@ -85,17 +84,25 @@ defmodule MatrixServer do # https://stackoverflow.com/questions/41523762/41671211 @spec to_serializable_map(struct()) :: map() 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__] 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 @spec serialize_and_encode(struct()) :: {:ok, String.t()} | {:error, Jason.EncodeError.t()} def serialize_and_encode(struct) do - # TODO: handle nil values in struct? struct |> to_serializable_map() |> encode_canonical_json() diff --git a/lib/matrix_server/encodable_map.ex b/lib/matrix_server/encodable_map.ex index 3d98b29..e48a8e8 100644 --- a/lib/matrix_server/encodable_map.ex +++ b/lib/matrix_server/encodable_map.ex @@ -1,6 +1,6 @@ # https://github.com/michalmuskala/jason/issues/69 defmodule MatrixServer.EncodableMap do - alias MatrixServer.EncodableMap + alias MatrixServer.{EncodableMap, Event} alias MatrixServer.Types.{UserId, RoomId, EventId, GroupId, AliasId} defstruct pairs: [] @@ -15,12 +15,10 @@ defmodule MatrixServer.EncodableMap do pairs = map |> Enum.map(fn - {k, v} when is_struct(v, DateTime) -> - {k, DateTime.to_unix(v, :millisecond)} - {k, v} 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) -> + # Simply convert IDs to a string. {k, to_string(v)} {k, v} when is_map(v) -> diff --git a/lib/matrix_server/key_server.ex b/lib/matrix_server/key_server.ex index 39b4e07..fe80da5 100644 --- a/lib/matrix_server/key_server.ex +++ b/lib/matrix_server/key_server.ex @@ -48,6 +48,8 @@ defmodule MatrixServer.KeyServer do object = Map.drop(object, [:signatures, :unsigned]) with {:ok, json} <- MatrixServer.encode_canonical_json(object) do + IO.puts(json) + signature = json |> :enacl.sign_detached(private_key) diff --git a/lib/matrix_server/room_server.ex b/lib/matrix_server/room_server.ex index 97fccaa..58cfa63 100644 --- a/lib/matrix_server/room_server.ex +++ b/lib/matrix_server/room_server.ex @@ -18,6 +18,9 @@ defmodule MatrixServer.RoomServer do GenServer.start_link(__MODULE__, opts, name: name) 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. # If the room does not exist, return an error. @spec get_room_server(String.t()) :: {:error, :not_found} | DynamicSupervisor.on_start_child() diff --git a/lib/matrix_server/schema/event.ex b/lib/matrix_server/schema/event.ex index edd35e7..7f5eeda 100644 --- a/lib/matrix_server/schema/event.ex +++ b/lib/matrix_server/schema/event.ex @@ -9,7 +9,7 @@ defmodule MatrixServer.Event do # TODO: Could refactor to also always set prev_events, but not necessary. @type t :: %__MODULE__{ type: String.t(), - origin_server_ts: DateTime.t(), + origin_server_ts: integer(), state_key: String.t(), sender: UserId.t(), content: map(), @@ -23,7 +23,7 @@ defmodule MatrixServer.Event do @primary_key {:event_id, :string, []} schema "events" do field :type, :string - field :origin_server_ts, :utc_datetime_usec + field :origin_server_ts, :integer field :state_key, :string field :sender, UserId field :content, :map @@ -36,12 +36,38 @@ defmodule MatrixServer.Event do belongs_to :room, Room, type: :string 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{} def new(%Room{id: room_id}, %Account{localpart: localpart}) do %Event{ room_id: room_id, 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: [], auth_events: [] } diff --git a/lib/matrix_server/schema/server_key_info.ex b/lib/matrix_server/schema/server_key_info.ex index 9248697..539ac67 100644 --- a/lib/matrix_server/schema/server_key_info.ex +++ b/lib/matrix_server/schema/server_key_info.ex @@ -10,7 +10,7 @@ defmodule MatrixServer.ServerKeyInfo do @primary_key {:server_name, :string, []} schema "server_key_info" do - field :valid_until, :utc_datetime + field :valid_until, :integer has_many :signing_keys, SigningKey, foreign_key: :server_name end @@ -43,7 +43,7 @@ defmodule MatrixServer.ServerKeyInfo do verify_keys: verify_keys, valid_until_ts: valid_until_ts }} <- 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 = Enum.map(verify_keys, fn {key_id, %{"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. ski = %ServerKeyInfo{ 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 diff --git a/lib/matrix_server/types/user_id.ex b/lib/matrix_server/types/user_id.ex index 2754fbc..1797a68 100644 --- a/lib/matrix_server/types/user_id.ex +++ b/lib/matrix_server/types/user_id.ex @@ -18,6 +18,12 @@ defmodule MatrixServer.Types.UserId do 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 cast(s) when is_binary(s) do diff --git a/lib/matrix_server_web/error.ex b/lib/matrix_server_web/error.ex index 7f93163..257edba 100644 --- a/lib/matrix_server_web/error.ex +++ b/lib/matrix_server_web/error.ex @@ -12,6 +12,7 @@ defmodule MatrixServerWeb.Error do invalid_room_state: {400, "M_INVALID_ROOM_STATE", "The request would leave the room in an invalid state."}, 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."}, missing_token: {401, "M_MISSING_TOKEN", "Access token required."}, not_found: {404, "M_NOT_FOUND", "The requested resource was not found."}, diff --git a/lib/matrix_server_web/federation/authenticate_server.ex b/lib/matrix_server_web/federation/authenticate_server.ex index 85ef3ff..247af30 100644 --- a/lib/matrix_server_web/federation/authenticate_server.ex +++ b/lib/matrix_server_web/federation/authenticate_server.ex @@ -12,8 +12,18 @@ defmodule MatrixServerWeb.Federation.AuthenticateServer do method: method, query_string: query_string }) 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 = %{ - uri: path <> "?" <> URI.decode_www_form(query_string), + uri: uri, method: method, destination: MatrixServer.server_name() } @@ -39,6 +49,7 @@ defmodule MatrixServerWeb.Federation.AuthenticateServer do ServerKeyInfo.with_fresh_signing_keys(origin) do Enum.find_value(keys, false, fn %SigningKey{signing_key: signing_key} -> with {:ok, decoded_key} <- MatrixServer.decode_base64(signing_key) do + IO.puts(encoded_object) MatrixServer.sign_verify(raw_sig, encoded_object, decoded_key) else _ -> false diff --git a/lib/matrix_server_web/federation/controllers/event_controller.ex b/lib/matrix_server_web/federation/controllers/event_controller.ex index 9721a0d..a447cfa 100644 --- a/lib/matrix_server_web/federation/controllers/event_controller.ex +++ b/lib/matrix_server_web/federation/controllers/event_controller.ex @@ -2,9 +2,44 @@ defmodule MatrixServerWeb.Federation.EventController do use MatrixServerWeb, :controller use MatrixServerWeb.Federation.AuthenticateServer - def event(conn, %{"event_id" => _event_id}) do - conn - |> put_status(200) - |> json(%{}) + import MatrixServerWeb.Error + import Ecto.Query + + 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 + + def event(conn, _), do: put_error(conn, :bad_json) end diff --git a/lib/matrix_server_web/federation/controllers/key_controller.ex b/lib/matrix_server_web/federation/controllers/key_controller.ex index 4d14175..243e44b 100644 --- a/lib/matrix_server_web/federation/controllers/key_controller.ex +++ b/lib/matrix_server_web/federation/controllers/key_controller.ex @@ -20,7 +20,7 @@ defmodule MatrixServerWeb.Federation.KeyController do server_name: MatrixServer.server_name(), verify_keys: keys, old_verify_keys: %{}, - valid_until_ts: valid_until + valid_until_ts: DateTime.to_unix(valid_until, :millisecond) } case KeyServer.sign_object(data) do diff --git a/lib/matrix_server_web/federation/http_client.ex b/lib/matrix_server_web/federation/http_client.ex index e2eab15..ec4f424 100644 --- a/lib/matrix_server_web/federation/http_client.ex +++ b/lib/matrix_server_web/federation/http_client.ex @@ -29,7 +29,9 @@ defmodule MatrixServerWeb.Federation.HTTPClient do with {:ok, %GetSigningKeys{server_name: server_name, verify_keys: verify_keys, signatures: sigs} = 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 # For each verify key, check if there is a matching signature. # If not, invalidate the whole response. @@ -58,12 +60,20 @@ defmodule MatrixServerWeb.Federation.HTTPClient do Tesla.get(client, path) 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 with {:ok, %Tesla.Env{body: body}} <- Tesla.request(client, url: path, method: method), %Ecto.Changeset{valid?: true} = cs <- apply(request_schema, :changeset, [body]) do {:ok, Ecto.Changeset.apply_changes(cs)} else - _ -> :error + x -> + IO.inspect(x) + :error end end end diff --git a/lib/matrix_server_web/federation/transaction.ex b/lib/matrix_server_web/federation/transaction.ex new file mode 100644 index 0000000..6e2cd6b --- /dev/null +++ b/lib/matrix_server_web/federation/transaction.ex @@ -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 diff --git a/priv/repo/migrations/20210820184601_move_timestamps_to_ints_again.exs b/priv/repo/migrations/20210820184601_move_timestamps_to_ints_again.exs new file mode 100644 index 0000000..b834440 --- /dev/null +++ b/priv/repo/migrations/20210820184601_move_timestamps_to_ints_again.exs @@ -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