From 5d75244bc0da0e90a590f6bd6986555ce3280476 Mon Sep 17 00:00:00 2001 From: Pim Kunis Date: Mon, 16 Aug 2021 14:29:01 +0200 Subject: [PATCH] Add common struct to represent matrix identifiers Use identifier struct in profile query validation --- lib/matrix_server/identifier.ex | 173 ++++++++++++++++++ .../controllers/query_controller.ex | 12 +- 2 files changed, 179 insertions(+), 6 deletions(-) create mode 100644 lib/matrix_server/identifier.ex diff --git a/lib/matrix_server/identifier.ex b/lib/matrix_server/identifier.ex new file mode 100644 index 0000000..c1fa23d --- /dev/null +++ b/lib/matrix_server/identifier.ex @@ -0,0 +1,173 @@ +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_web/federation/controllers/query_controller.ex b/lib/matrix_server_web/federation/controllers/query_controller.ex index 127b78c..829c3cd 100644 --- a/lib/matrix_server_web/federation/controllers/query_controller.ex +++ b/lib/matrix_server_web/federation/controllers/query_controller.ex @@ -5,7 +5,7 @@ defmodule MatrixServerWeb.Federation.QueryController do import MatrixServerWeb.Error import Ecto.Query - alias MatrixServer.{Repo, Account} + alias MatrixServer.{Repo, Account, Identifier} defmodule ProfileRequest do use Ecto.Schema @@ -14,7 +14,7 @@ defmodule MatrixServerWeb.Federation.QueryController do @primary_key false embedded_schema do - field :user_id, :string + field :user_id, Identifier field :field, :string end @@ -31,10 +31,10 @@ defmodule MatrixServerWeb.Federation.QueryController do end def profile(conn, params) do - with {:ok, %ProfileRequest{user_id: user_id}} <- ProfileRequest.validate(params) do - if MatrixServer.get_domain(user_id) == MatrixServer.server_name() do - localpart = MatrixServer.get_localpart(user_id) - + with {:ok, + %ProfileRequest{user_id: %Identifier{type: :user, 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 %Account{} -> # TODO: Return displayname and avatar_url when we implement them.