diff --git a/lib/matrix_server.ex b/lib/matrix_server.ex index 97d7bcc..4cc0a2f 100644 --- a/lib/matrix_server.ex +++ b/lib/matrix_server.ex @@ -2,6 +2,9 @@ defmodule MatrixServer do alias MatrixServer.EncodableMap @random_string_alphabet Enum.into(?a..?z, []) ++ Enum.into(?A..?Z, []) + @ipv6_regex ~r/^\[(?[^\]]+)\](?:\d{1,5})?$/ + @ipv4_regex ~r/^(?[^:]+)(?:\d{1,5})?$/ + @dns_regex ~r/^[[:alnum:]-.]{1,255}$/ @spec get_mxid(String.t()) :: String.t() def get_mxid(localpart) when is_binary(localpart) do @@ -110,7 +113,8 @@ defmodule MatrixServer do %{object | signatures: new_sigs} end - @spec validate_change_simple(Ecto.Changeset.t(), atom(), (term() -> boolean())) :: Ecto.Changeset.t() + @spec validate_change_simple(Ecto.Changeset.t(), atom(), (term() -> boolean())) :: + Ecto.Changeset.t() def validate_change_simple(changeset, field, func) do augmented_func = fn _, val -> if func.(val), do: [], else: [{field, "invalid"}] @@ -146,4 +150,35 @@ defmodule MatrixServer do |> String.replace("+", "-") |> String.replace("/", "_") end + + @spec valid_domain?(String.t()) :: boolean() + def valid_domain?(domain) do + if String.starts_with?(domain, "[") do + # Parse as ipv6. + with %{"ip" => ip} <- + Regex.named_captures(@ipv6_regex, domain), + {:ok, _} <- :inet.parse_address(String.to_charlist(ip)) do + true + else + _ -> false + end + else + # Parse as ipv4 or dns name. + case Regex.named_captures(@ipv4_regex, domain) do + nil -> + false + + %{"hostname" => hostname} -> + # Try to parse as ipv4. + case String.to_charlist(hostname) |> :inet.parse_address() do + {:ok, _} -> + true + + {:error, _} -> + # Try to parse as dns name. + Regex.match?(@dns_regex, hostname) + end + end + end + end end diff --git a/lib/matrix_server/identifier.ex b/lib/matrix_server/identifier.ex deleted file mode 100644 index c1fa23d..0000000 --- a/lib/matrix_server/identifier.ex +++ /dev/null @@ -1,173 +0,0 @@ -defmodule MatrixServer.Identifier do - use Ecto.Type - - alias MatrixServer.Identifier - - defstruct [:type, :localpart, :domain] - - def type(), do: :string - - def cast(id) when is_binary(id) do - with %Identifier{} = identifier <- parse(id) do - {:ok, identifier} - end - end - - def cast(_), do: :error - - defp parse(id) do - {sigil, id} = String.split_at(id, 1) - - case get_type(sigil) do - {:ok, :event} -> - parse_event(id) - - {:ok, type} when type in [:user, :room, :group, :alias] -> - case String.split(id, ":", parts: 2) do - [localpart, domain] -> - case type do - :user -> parse_user(localpart, domain) - :room -> parse_room(localpart, domain) - :group -> parse_group(localpart, domain) - :alias -> parse_alias(localpart, domain) - end - - _ -> - :error - end - - :error -> - :error - end - end - - defp get_type("@"), do: {:ok, :user} - defp get_type("!"), do: {:ok, :room} - defp get_type("$"), do: {:ok, :event} - defp get_type("+"), do: {:ok, :group} - defp get_type("#"), do: {:ok, :alias} - defp get_type(_), do: :error - - @user_regex ~r/^[a-z0-9._=\-\/]+$/ - defp parse_user(localpart, domain) do - if String.length(localpart) + String.length(domain) + 2 <= 255 and - Regex.match?(@user_regex, localpart) and - valid_domain?(domain) do - %Identifier{type: :user, localpart: localpart, domain: domain} - else - :error - end - end - - defp parse_room(localpart, domain) do - if valid_domain?(domain) do - %Identifier{type: :room, localpart: localpart, domain: domain} - else - :error - end - end - - @event_regex ~r/^[[:alnum:]-_]+$/ - defp parse_event(localpart) do - if Regex.match?(@event_regex, localpart) do - %Identifier{type: :event, localpart: localpart} - else - :error - end - end - - @group_regex ~r/^[[:lower:][:digit:]._=\-\/]+$/ - defp parse_group(localpart, domain) do - if String.length(localpart) + String.length(domain) + 2 <= 255 and - Regex.match?(@group_regex, localpart) and - valid_domain?(domain) do - %Identifier{type: :group, localpart: localpart, domain: domain} - else - :error - end - end - - defp parse_alias(localpart, domain) do - if String.length(localpart) + String.length(domain) + 2 <= 255 and valid_domain?(domain) do - %Identifier{type: :alias, localpart: localpart, domain: domain} - else - :error - end - end - - def load(id) when is_binary(id) do - {:ok, parse_unvalidated(id)} - end - - def load(_), do: :error - - defp parse_unvalidated(id) do - {sigil, id} = String.split_at(id, 1) - - case get_type(sigil) do - {:ok, :event} -> - %Identifier{type: :event, localpart: id} - - {:ok, type} -> - [localpart, domain] = String.split(id, ":", parts: 2) - - identifier = %Identifier{localpart: localpart, domain: domain} - - case type do - :user -> %Identifier{identifier | type: :user} - :room -> %Identifier{identifier | type: :room} - :group -> %Identifier{identifier | type: :group} - :alias -> %Identifier{identifier | type: :alias} - end - end - end - - def dump(%Identifier{type: :event, localpart: localpart}) do - {:ok, "$" <> localpart} - end - - def dump(%Identifier{type: type, localpart: localpart, domain: domain}) do - {:ok, type_to_sigil(type) <> localpart <> ":" <> domain} - end - - def dump(_), do: :error - - defp type_to_sigil(:user), do: "@" - defp type_to_sigil(:room), do: "!" - defp type_to_sigil(:event), do: "$" - defp type_to_sigil(:group), do: "+" - defp type_to_sigil(:alias), do: "#" - - @ipv6_regex ~r/^\[(?[^\]]+)\](?:\d{1,5})?$/ - @ipv4_regex ~r/^(?[^:]+)(?:\d{1,5})?$/ - @dns_regex ~r/^[[:alnum:]-.]{1,255}$/ - defp valid_domain?(domain) do - if String.starts_with?(domain, "[") do - # Parse as ipv6. - with %{"ip" => ip} <- - Regex.named_captures(@ipv6_regex, domain), - {:ok, _} <- :inet.parse_address(String.to_charlist(ip)) do - true - else - _ -> false - end - else - # Parse as ipv4 or dns name. - case Regex.named_captures(@ipv4_regex, domain) do - nil -> - false - - %{"hostname" => hostname} -> - # Try to parse as ipv4. - case String.to_charlist(hostname) |> :inet.parse_address() do - {:ok, _} -> - true - - {:error, _} -> - # Try to parse as dns name. - Regex.match?(@dns_regex, hostname) - end - end - end - end -end diff --git a/lib/matrix_server/room_server.ex b/lib/matrix_server/room_server.ex index 25a7cd6..97fccaa 100644 --- a/lib/matrix_server/room_server.ex +++ b/lib/matrix_server/room_server.ex @@ -43,7 +43,11 @@ defmodule MatrixServer.RoomServer do end end - @spec create_room(pid(), MatrixServer.Account.t(), MatrixServerWeb.Client.Request.CreateRoom.t()) :: {:ok, String.t()} | {:error, atom()} + @spec create_room( + pid(), + MatrixServer.Account.t(), + MatrixServerWeb.Client.Request.CreateRoom.t() + ) :: {:ok, String.t()} | {:error, atom()} def create_room(pid, account, input) do GenServer.call(pid, {:create_room, account, input}) end diff --git a/lib/matrix_server/schema/account.ex b/lib/matrix_server/schema/account.ex index a62fa93..dc69b6e 100644 --- a/lib/matrix_server/schema/account.ex +++ b/lib/matrix_server/schema/account.ex @@ -8,8 +8,8 @@ defmodule MatrixServer.Account do alias Ecto.Multi @type t :: %__MODULE__{ - password_hash: String.t() - } + password_hash: String.t() + } @max_mxid_length 255 diff --git a/lib/matrix_server/schema/room.ex b/lib/matrix_server/schema/room.ex index fbb898b..67d6370 100644 --- a/lib/matrix_server/schema/room.ex +++ b/lib/matrix_server/schema/room.ex @@ -8,10 +8,10 @@ defmodule MatrixServer.Room do alias MatrixServerWeb.Client.Request.CreateRoom @type t :: %__MODULE__{ - visibility: :public | :private, - state: list(list(String.t())), - forward_extremities: list(String.t()) - } + visibility: :public | :private, + state: list(list(String.t())), + forward_extremities: list(String.t()) + } @primary_key {:id, :string, []} schema "rooms" do diff --git a/lib/matrix_server/state_resolution/authorization.ex b/lib/matrix_server/state_resolution/authorization.ex index d7f080d..61b7417 100644 --- a/lib/matrix_server/state_resolution/authorization.ex +++ b/lib/matrix_server/state_resolution/authorization.ex @@ -204,8 +204,8 @@ defmodule MatrixServer.StateResolution.Authorization do defp get_action_power_level(:invite, _), do: 50 defp get_action_power_level(:ban, %{"ban" => pl}), do: pl defp get_action_power_level(:ban, _), do: 50 - defp get_action_power_level(:redact, %{"redact" => pl}), do: pl - defp get_action_power_level(:redact, _), do: 50 + # defp get_action_power_level(:redact, %{"redact" => pl}), do: pl + # defp get_action_power_level(:redact, _), do: 50 defp get_action_power_level(:kick, %{"kick" => pl}), do: pl defp get_action_power_level(:kick, _), do: 50 diff --git a/lib/matrix_server/types/alias_id.ex b/lib/matrix_server/types/alias_id.ex new file mode 100644 index 0000000..f738486 --- /dev/null +++ b/lib/matrix_server/types/alias_id.ex @@ -0,0 +1,43 @@ +defmodule MatrixServer.Types.AliasId do + use Ecto.Type + + alias MatrixServer.Types.AliasId + + defstruct [:localpart, :domain] + + defimpl String.Chars, for: AliasId do + def to_string(%AliasId{localpart: localpart, domain: domain}) do + "#" <> localpart <> ":" <> domain + end + end + + def type(), do: :string + + def cast(s) when is_binary(s) do + with "#" <> rest <- s, + [localpart, domain] <- String.split(rest, ":", parts: 2) do + if String.length(localpart) + String.length(domain) + 2 <= 255 and + MatrixServer.valid_domain?(domain) do + %AliasId{localpart: localpart, domain: domain} + else + :error + end + else + _ -> :error + end + end + + def cast(_), do: :error + + def load(s) when is_binary(s) do + "@" <> rest = s + [localpart, domain] = String.split(rest, ":", parts: 2) + + %AliasId{localpart: localpart, domain: domain} + end + + def load(_), do: :error + + def dump(%AliasId{} = alias_id), do: to_string(alias_id) + def dump(_), do: :error +end diff --git a/lib/matrix_server/types/event_id.ex b/lib/matrix_server/types/event_id.ex new file mode 100644 index 0000000..47fe242 --- /dev/null +++ b/lib/matrix_server/types/event_id.ex @@ -0,0 +1,42 @@ +defmodule MatrixServer.Types.EventId do + use Ecto.Type + + alias MatrixServer.Types.EventId + + defstruct [:id] + + @id_regex ~r/^[[:alnum:]-_]+$/ + + defimpl String.Chars, for: EventId do + def to_string(%EventId{id: id}) do + "$" <> id + end + end + + def type(), do: :string + + def cast(s) when is_binary(s) do + with "$" <> id <- s do + if Regex.match?(@id_regex, id) do + %EventId{id: id} + else + :error + end + else + _ -> :error + end + end + + def cast(_), do: :error + + def load(s) when is_binary(s) do + "$" <> rest = s + + %EventId{id: rest} + end + + def load(_), do: :error + + def dump(%EventId{} = event_id), do: to_string(event_id) + def dump(_), do: :error +end diff --git a/lib/matrix_server/types/group_id.ex b/lib/matrix_server/types/group_id.ex new file mode 100644 index 0000000..d73d8dc --- /dev/null +++ b/lib/matrix_server/types/group_id.ex @@ -0,0 +1,46 @@ +defmodule MatrixServer.Types.GroupId do + use Ecto.Type + + alias MatrixServer.Types.GroupId + + defstruct [:localpart, :domain] + + @localpart_regex ~r/^[[:lower:][:digit:]._=\-\/]+$/ + + defimpl String.Chars, for: GroupId do + def to_string(%GroupId{localpart: localpart, domain: domain}) do + "+" <> localpart <> ":" <> domain + end + end + + def type(), do: :string + + def cast(s) when is_binary(s) do + with "+" <> rest <- s, + [localpart, domain] <- String.split(rest, ":", parts: 2) do + if String.length(localpart) + String.length(domain) + 2 <= 255 and + Regex.match?(@localpart_regex, localpart) and + MatrixServer.valid_domain?(domain) do + %GroupId{localpart: localpart, domain: domain} + else + :error + end + else + _ -> :error + end + end + + def cast(_), do: :error + + def load(s) when is_binary(s) do + "@" <> rest = s + [localpart, domain] = String.split(rest, ":", parts: 2) + + %GroupId{localpart: localpart, domain: domain} + end + + def load(_), do: :error + + def dump(%GroupId{} = group_id), do: to_string(group_id) + def dump(_), do: :error +end diff --git a/lib/matrix_server/types/room_id.ex b/lib/matrix_server/types/room_id.ex new file mode 100644 index 0000000..0e7cf00 --- /dev/null +++ b/lib/matrix_server/types/room_id.ex @@ -0,0 +1,42 @@ +defmodule MatrixServer.Types.RoomId do + use Ecto.Type + + alias MatrixServer.Types.RoomId + + defstruct [:localpart, :domain] + + defimpl String.Chars, for: RoomId do + def to_string(%RoomId{localpart: localpart, domain: domain}) do + "!" <> localpart <> ":" <> domain + end + end + + def type(), do: :string + + def cast(s) when is_binary(s) do + with "!" <> rest <- s, + [localpart, domain] <- String.split(rest, ":", parts: 2) do + if MatrixServer.valid_domain?(domain) do + %RoomId{localpart: localpart, domain: domain} + else + :error + end + else + _ -> :error + end + end + + def cast(_), do: :error + + def load(s) when is_binary(s) do + "!" <> rest = s + [localpart, domain] = String.split(rest, ":", parts: 2) + + %RoomId{localpart: localpart, domain: domain} + end + + def load(_), do: :error + + def dump(%RoomId{} = room_id), do: to_string(room_id) + def dump(_), do: :error +end diff --git a/lib/matrix_server/types/user_id.ex b/lib/matrix_server/types/user_id.ex new file mode 100644 index 0000000..585f05e --- /dev/null +++ b/lib/matrix_server/types/user_id.ex @@ -0,0 +1,45 @@ +defmodule MatrixServer.Types.UserId do + use Ecto.Type + + alias MatrixServer.Types.UserId + + defstruct [:localpart, :domain] + + @localpart_regex ~r/^[a-z0-9._=\-\/]+$/ + + defimpl String.Chars, for: UserId do + def to_string(%UserId{localpart: localpart, domain: domain}) do + "@" <> localpart <> ":" <> domain + end + end + + def type(), do: :string + + def cast(s) when is_binary(s) do + with "@" <> rest <- s, + [localpart, domain] <- String.split(rest, ":", parts: 2) do + if String.length(localpart) + String.length(domain) + 2 <= 255 and + Regex.match?(@localpart_regex, localpart) and MatrixServer.valid_domain?(domain) do + %UserId{localpart: localpart, domain: domain} + else + :error + end + else + _ -> :error + end + end + + def cast(_), do: :error + + def load(s) when is_binary(s) do + "@" <> rest = s + [localpart, domain] = String.split(rest, ":", parts: 2) + + %UserId{localpart: localpart, domain: domain} + end + + def load(_), do: :error + + def dump(%UserId{} = user_id), do: to_string(user_id) + def dump(_), do: :error +end diff --git a/lib/matrix_server_web/client/request/create_room.ex b/lib/matrix_server_web/client/request/create_room.ex index d37d129..4f260c3 100644 --- a/lib/matrix_server_web/client/request/create_room.ex +++ b/lib/matrix_server_web/client/request/create_room.ex @@ -6,14 +6,14 @@ defmodule MatrixServerWeb.Client.Request.CreateRoom do alias Ecto.Changeset @type t :: %__MODULE__{ - visibility: String.t(), - room_alias_name: String.t(), - name: String.t(), - topic: String.t(), - invite: list(String.t()), - room_version: String.t(), - preset: String.t() - } + visibility: String.t(), + room_alias_name: String.t(), + name: String.t(), + topic: String.t(), + invite: list(String.t()), + room_version: String.t(), + preset: String.t() + } @primary_key false embedded_schema do diff --git a/lib/matrix_server_web/federation/controllers/query_controller.ex b/lib/matrix_server_web/federation/controllers/query_controller.ex index 829c3cd..84b9754 100644 --- a/lib/matrix_server_web/federation/controllers/query_controller.ex +++ b/lib/matrix_server_web/federation/controllers/query_controller.ex @@ -5,7 +5,8 @@ defmodule MatrixServerWeb.Federation.QueryController do import MatrixServerWeb.Error import Ecto.Query - alias MatrixServer.{Repo, Account, Identifier} + alias MatrixServer.{Repo, Account} + alias MatrixServer.Types.UserId defmodule ProfileRequest do use Ecto.Schema @@ -14,7 +15,7 @@ defmodule MatrixServerWeb.Federation.QueryController do @primary_key false embedded_schema do - field :user_id, Identifier + field :user_id, UserId field :field, :string end @@ -31,8 +32,7 @@ defmodule MatrixServerWeb.Federation.QueryController do end def profile(conn, params) do - with {:ok, - %ProfileRequest{user_id: %Identifier{type: :user, localpart: localpart, domain: domain}}} <- + with {:ok, %ProfileRequest{user_id: %UserId{localpart: localpart, domain: domain}}} <- ProfileRequest.validate(params) do if domain == MatrixServer.server_name() do case Repo.one(from a in Account, where: a.localpart == ^localpart) do diff --git a/mix.exs b/mix.exs index 3493cca..7b7aad7 100644 --- a/mix.exs +++ b/mix.exs @@ -11,7 +11,10 @@ defmodule MatrixServer.MixProject do start_permanent: Mix.env() == :prod, aliases: aliases(), deps: deps(), - dialyzer: [plt_add_deps: :app_tree] + dialyzer: [ + plt_add_deps: :app_tree, + flags: [:error_handling, :race_conditions, :underspecs, :unknown, :unmatched_returns] + ] ] end