Refactor Webfinger module, use XRD host-meta to find webfinger endpoint
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
@@ -654,7 +654,7 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
@spec make_actor_from_nickname(String.t()) :: {:ok, %Actor{}} | {:error, any()}
|
||||
def make_actor_from_nickname(nickname) do
|
||||
case WebFinger.finger(nickname) do
|
||||
{:ok, %{"url" => url}} when not is_nil(url) ->
|
||||
{:ok, url} when is_binary(url) ->
|
||||
make_actor_from_url(url)
|
||||
|
||||
_e ->
|
||||
|
||||
@@ -159,7 +159,7 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do
|
||||
@spec finger_actor(String.t()) :: {:ok, String.t()} | {:error, String.t()}
|
||||
defp finger_actor(nickname) do
|
||||
case WebFinger.finger(nickname) do
|
||||
{:ok, %{"url" => url}} when not is_nil(url) ->
|
||||
{:ok, url} when is_binary(url) ->
|
||||
{:ok, url}
|
||||
|
||||
_e ->
|
||||
|
||||
@@ -12,26 +12,37 @@ defmodule Mobilizon.Federation.WebFinger do
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.WebFinger.XmlBuilder
|
||||
alias Mobilizon.Service.HTTP.WebfingerClient
|
||||
alias Mobilizon.Service.HTTP.{HostMetaClient, WebfingerClient}
|
||||
alias Mobilizon.Web.Endpoint
|
||||
alias Mobilizon.Web.Router.Helpers, as: Routes
|
||||
require Jason
|
||||
require Logger
|
||||
import SweetXml
|
||||
|
||||
def host_meta do
|
||||
base_url = Endpoint.url()
|
||||
%URI{host: host} = URI.parse(base_url)
|
||||
|
||||
{
|
||||
:XRD,
|
||||
%{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},
|
||||
{
|
||||
:Link,
|
||||
%{
|
||||
rel: "lrdd",
|
||||
type: "application/xrd+xml",
|
||||
template: "#{base_url}/.well-known/webfinger?resource={uri}"
|
||||
%{
|
||||
xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0",
|
||||
"xmlns:hm": "http://host-meta.net/ns/1.0"
|
||||
},
|
||||
[
|
||||
{
|
||||
:"hm:Host",
|
||||
host
|
||||
},
|
||||
{
|
||||
:Link,
|
||||
%{
|
||||
rel: "lrdd",
|
||||
type: "application/jrd+json",
|
||||
template: "#{base_url}/.well-known/webfinger?resource={uri}"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|> XmlBuilder.to_doc()
|
||||
end
|
||||
@@ -56,29 +67,116 @@ defmodule Mobilizon.Federation.WebFinger do
|
||||
end
|
||||
|
||||
@spec represent_actor(Actor.t()) :: struct()
|
||||
def represent_actor(actor), do: represent_actor(actor, "JSON")
|
||||
def represent_actor(%Actor{} = actor), do: represent_actor(actor, "JSON")
|
||||
|
||||
@spec represent_actor(Actor.t(), String.t()) :: struct()
|
||||
def represent_actor(actor, "JSON") do
|
||||
%{
|
||||
"subject" => "acct:#{actor.preferred_username}@#{Endpoint.host()}",
|
||||
"aliases" => [actor.url],
|
||||
"links" => [
|
||||
def represent_actor(%Actor{} = actor, "JSON") do
|
||||
links =
|
||||
[
|
||||
%{"rel" => "self", "type" => "application/activity+json", "href" => actor.url},
|
||||
%{
|
||||
"rel" => "https://webfinger.net/rel/profile-page/",
|
||||
"type" => "text/html",
|
||||
"href" => actor.url
|
||||
},
|
||||
%{
|
||||
"rel" => "http://ostatus.org/schema/1.0/subscribe",
|
||||
"template" => "#{Routes.page_url(Endpoint, :interact, uri: nil)}{uri}"
|
||||
}
|
||||
]
|
||||
|> maybe_add_avatar(actor)
|
||||
|> maybe_add_profile_page(actor)
|
||||
|
||||
%{
|
||||
"subject" => "acct:#{actor.preferred_username}@#{Endpoint.host()}",
|
||||
"aliases" => [actor.url],
|
||||
"links" => links
|
||||
}
|
||||
end
|
||||
|
||||
defp webfinger_from_json(doc) do
|
||||
defp maybe_add_avatar(data, %Actor{avatar: avatar}) when not is_nil(avatar) do
|
||||
data ++
|
||||
[
|
||||
%{
|
||||
"rel" => "http://webfinger.net/rel/avatar",
|
||||
"type" => avatar.content_type,
|
||||
"href" => avatar.url
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
defp maybe_add_avatar(data, _actor), do: data
|
||||
|
||||
defp maybe_add_profile_page(data, %Actor{type: :Group, url: url}) do
|
||||
data ++
|
||||
[
|
||||
%{
|
||||
"rel" => "http://webfinger.net/rel/profile-page/",
|
||||
"type" => "text/html",
|
||||
"href" => url
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
defp maybe_add_profile_page(data, _actor), do: data
|
||||
|
||||
@doc """
|
||||
Finger an actor to retreive it's ActivityPub ID/URL
|
||||
|
||||
Fetches the Extensible Resource Descriptor endpoint `/.well-known/host-meta` to find the Webfinger endpoint (usually `/.well-known/webfinger?resource=`) with `find_webfinger_endpoint/1` and then performs a Webfinger query to get the ActivityPub ID associated to an actor.
|
||||
"""
|
||||
@spec finger(String.t()) :: {:ok, String.t()} | {:error, atom()}
|
||||
def finger(actor) do
|
||||
actor = String.trim_leading(actor, "@")
|
||||
|
||||
with address when is_binary(address) <- apply_webfinger_endpoint(actor),
|
||||
false <- address_invalid(address),
|
||||
{:ok, %{body: body, status: code}} when code in 200..299 <-
|
||||
WebfingerClient.get(address),
|
||||
{:ok, %{"url" => url}} <- webfinger_from_json(body) do
|
||||
{:ok, url}
|
||||
else
|
||||
e ->
|
||||
Logger.debug("Couldn't finger #{actor}")
|
||||
Logger.debug(inspect(e))
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Fetches the Extensible Resource Descriptor endpoint `/.well-known/host-meta` to find the Webfinger endpoint (usually `/.well-known/webfinger?resource=`)
|
||||
"""
|
||||
@spec find_webfinger_endpoint(String.t()) :: String.t()
|
||||
def find_webfinger_endpoint(domain) when is_binary(domain) do
|
||||
with {:ok, %{body: body}} <- fetch_document("http://#{domain}/.well-known/host-meta"),
|
||||
link_template <- find_link_from_template(body) do
|
||||
{:ok, link_template}
|
||||
end
|
||||
end
|
||||
|
||||
@spec apply_webfinger_endpoint(String.t()) :: String.t() | {:error, :host_not_found}
|
||||
defp apply_webfinger_endpoint(actor) do
|
||||
with {:ok, domain} <- domain_from_federated_actor(actor) do
|
||||
case find_webfinger_endpoint(domain) do
|
||||
{:ok, link_template} ->
|
||||
String.replace(link_template, "{uri}", "acct:#{actor}")
|
||||
|
||||
_ ->
|
||||
"http://#{domain}/.well-known/webfinger?resource=acct:#{actor}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@spec domain_from_federated_actor(String.t()) :: {:ok, String.t()} | {:error, :host_not_found}
|
||||
defp domain_from_federated_actor(actor) do
|
||||
case String.split(actor, "@") do
|
||||
[_name, domain] ->
|
||||
{:ok, domain}
|
||||
|
||||
_e ->
|
||||
host = URI.parse(actor).host
|
||||
if is_nil(host), do: {:error, :host_not_found}, else: {:ok, host}
|
||||
end
|
||||
end
|
||||
|
||||
@spec webfinger_from_json(map() | String.t()) ::
|
||||
{:ok, map()} | {:error, :webfinger_information_not_json}
|
||||
defp webfinger_from_json(doc) when is_map(doc) do
|
||||
data =
|
||||
Enum.reduce(doc["links"], %{"subject" => doc["subject"]}, fn link, data ->
|
||||
case {link["type"], link["rel"]} do
|
||||
@@ -97,31 +195,26 @@ defmodule Mobilizon.Federation.WebFinger do
|
||||
{:ok, data}
|
||||
end
|
||||
|
||||
def finger(actor) do
|
||||
actor = String.trim_leading(actor, "@")
|
||||
defp webfinger_from_json(_doc), do: {:error, :webfinger_information_not_json}
|
||||
|
||||
domain =
|
||||
case String.split(actor, "@") do
|
||||
[_name, domain] ->
|
||||
domain
|
||||
@spec find_link_from_template(String.t()) :: String.t() | {:error, :link_not_found}
|
||||
defp find_link_from_template(doc) do
|
||||
with res when res in [nil, ""] <-
|
||||
xpath(doc, ~x"//Link[@rel=\"lrdd\"][@type=\"application/json\"]/@template"s),
|
||||
res when res in [nil, ""] <- xpath(doc, ~x"//Link[@rel=\"lrdd\"]/@template"s),
|
||||
do: {:error, :link_not_found}
|
||||
end
|
||||
|
||||
_e ->
|
||||
URI.parse(actor).host
|
||||
end
|
||||
@spec fetch_document(String.t()) :: Tesla.Env.result()
|
||||
defp fetch_document(endpoint) do
|
||||
with {:error, err} <- HostMetaClient.get(endpoint), do: {:error, err}
|
||||
end
|
||||
|
||||
address = "http://#{domain}/.well-known/webfinger?resource=acct:#{actor}"
|
||||
|
||||
Logger.debug(inspect(address))
|
||||
|
||||
with false <- is_nil(domain),
|
||||
{:ok, %{body: body, status: code}} when code in 200..299 <-
|
||||
WebfingerClient.get(address) do
|
||||
webfinger_from_json(body)
|
||||
else
|
||||
e ->
|
||||
Logger.debug(fn -> "Couldn't finger #{actor}" end)
|
||||
Logger.debug(fn -> inspect(e) end)
|
||||
{:error, e}
|
||||
@spec address_invalid(String.t()) :: false | {:error, :invalid_address}
|
||||
defp address_invalid(address) do
|
||||
with %URI{host: host, scheme: scheme} <- URI.parse(address),
|
||||
true <- is_nil(host) or is_nil(scheme) do
|
||||
{:error, :invalid_address}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
24
lib/service/http/host_meta_client.ex
Normal file
24
lib/service/http/host_meta_client.ex
Normal file
@@ -0,0 +1,24 @@
|
||||
defmodule Mobilizon.Service.HTTP.HostMetaClient do
|
||||
@moduledoc """
|
||||
Tesla HTTP Basic Client
|
||||
with XML middleware
|
||||
"""
|
||||
|
||||
use Tesla
|
||||
alias Mobilizon.Config
|
||||
|
||||
@default_opts [
|
||||
recv_timeout: 20_000
|
||||
]
|
||||
|
||||
adapter(Tesla.Adapter.Hackney, @default_opts)
|
||||
|
||||
plug(Tesla.Middleware.FollowRedirects)
|
||||
|
||||
plug(Tesla.Middleware.Timeout, timeout: 10_000)
|
||||
|
||||
plug(Tesla.Middleware.Headers, [
|
||||
{"User-Agent", Config.instance_user_agent()},
|
||||
{"Accept", "application/xrd+xml, application/xml, text/xml"}
|
||||
])
|
||||
end
|
||||
Reference in New Issue
Block a user