2018-12-24 13:34:45 +01:00
# Portions of this file are derived from Pleroma:
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social>
# SPDX-License-Identifier: AGPL-3.0-only
2018-12-27 11:24:04 +01:00
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/web/web_finger/web_finger.ex
2018-12-24 13:34:45 +01:00
2020-01-22 22:40:40 +01:00
defmodule Mobilizon.Federation.WebFinger do
2018-06-14 18:15:27 +02:00
@moduledoc """
2020-01-22 02:14:42 +01:00
Performs the WebFinger requests and responses ( JSON only ) .
2018-06-14 18:15:27 +02:00
"""
2018-05-17 11:32:23 +02:00
2018-10-11 17:37:39 +02:00
alias Mobilizon.Actors
2018-11-28 10:49:16 +01:00
alias Mobilizon.Actors.Actor
2021-04-22 12:17:56 +02:00
alias Mobilizon.Federation.ActivityPub.Actor , as : ActivityPubActor
2020-01-22 22:40:40 +01:00
alias Mobilizon.Federation.WebFinger.XmlBuilder
2021-04-09 10:19:25 +02:00
alias Mobilizon.Service.HTTP . { HostMetaClient , WebfingerClient }
2020-01-28 19:18:33 +01:00
alias Mobilizon.Web.Endpoint
2019-12-20 13:04:34 +01:00
alias Mobilizon.Web.Router.Helpers , as : Routes
2018-05-17 11:32:23 +02:00
require Jason
require Logger
2021-04-09 10:19:25 +02:00
import SweetXml
2018-05-17 11:32:23 +02:00
2021-09-10 11:36:05 +02:00
@doc """
Returns the Web Host Metadata ( for ` / . well - known / host - meta ` ) representation for the instance , following RFC6414 .
"""
@spec host_meta :: String . t ( )
2018-05-17 11:32:23 +02:00
def host_meta do
2020-01-28 19:18:33 +01:00
base_url = Endpoint . url ( )
2021-04-09 10:19:25 +02:00
% URI { host : host } = URI . parse ( base_url )
2018-05-17 11:32:23 +02:00
{
:XRD ,
2021-04-09 10:19:25 +02:00
%{
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} "
}
2018-05-17 11:32:23 +02:00
}
2021-04-09 10:19:25 +02:00
]
2018-05-17 11:32:23 +02:00
}
|> XmlBuilder . to_doc ( )
end
2021-09-10 11:36:05 +02:00
@doc """
Returns the Webfinger representation for the instance , following RFC7033 .
"""
@spec webfinger ( String . t ( ) , String . t ( ) ) :: { :ok , map } | { :error , :actor_not_found }
2018-05-17 11:32:23 +02:00
def webfinger ( resource , " JSON " ) do
2020-01-28 19:18:33 +01:00
host = Endpoint . host ( )
2018-05-18 09:56:21 +02:00
regex = ~r/ (acct:)?(?<name> \w +)@ #{ host } /
2018-05-17 11:32:23 +02:00
2018-11-28 10:49:16 +01:00
with %{ " name " = > name } <- Regex . named_captures ( regex , resource ) ,
% Actor { } = actor <- Actors . get_local_actor_by_name ( name ) do
2018-11-27 18:42:56 +01:00
{ :ok , represent_actor ( actor , " JSON " ) }
2018-05-17 11:32:23 +02:00
else
_e ->
2021-04-22 12:17:56 +02:00
case ActivityPubActor . get_or_fetch_actor_by_url ( resource ) do
2019-07-23 18:06:22 +02:00
{ :ok , % Actor { } = actor } when not is_nil ( actor ) ->
{ :ok , represent_actor ( actor , " JSON " ) }
2018-05-17 11:32:23 +02:00
_e ->
2021-09-10 11:36:05 +02:00
{ :error , :actor_not_found }
2018-05-17 11:32:23 +02:00
end
end
end
2021-09-10 11:36:05 +02:00
@doc """
Return an ` Mobilizon.Actors.Actor ` Webfinger representation ( as JSON )
"""
2021-09-10 11:27:59 +02:00
@spec represent_actor ( Actor . t ( ) ) :: map ( )
@spec represent_actor ( Actor . t ( ) , String . t ( ) ) :: map ( )
2021-04-09 10:19:25 +02:00
def represent_actor ( % Actor { } = actor ) , do : represent_actor ( actor , " JSON " )
2018-11-27 18:42:56 +01:00
2021-04-09 10:19:25 +02:00
def represent_actor ( % Actor { } = actor , " JSON " ) do
links =
[
2018-11-27 18:42:56 +01:00
%{ " rel " = > " self " , " type " = > " application/activity+json " , " href " = > actor . url } ,
2019-12-20 13:04:34 +01:00
%{
" rel " = > " http://ostatus.org/schema/1.0/subscribe " ,
" template " = > " #{ Routes . page_url ( Endpoint , :interact , uri : nil ) } {uri} "
2018-11-07 16:45:11 +01:00
}
2018-05-17 11:32:23 +02:00
]
2021-04-09 10:19:25 +02:00
|> maybe_add_avatar ( actor )
|> maybe_add_profile_page ( actor )
%{
" subject " = > " acct: #{ actor . preferred_username } @ #{ Endpoint . host ( ) } " ,
" aliases " = > [ actor . url ] ,
" links " = > links
2018-05-17 11:32:23 +02:00
}
end
2021-09-10 11:36:05 +02:00
@spec maybe_add_avatar ( list ( map ( ) ) , Actor . t ( ) ) :: list ( map ( ) )
2021-04-09 10:19:25 +02:00
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
2021-09-10 11:36:05 +02:00
@spec maybe_add_profile_page ( list ( map ( ) ) , Actor . t ( ) ) :: list ( map ( ) )
2021-04-09 10:19:25 +02:00
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
2021-09-10 11:36:05 +02:00
@type finger_errors ::
:host_not_found | :address_invalid | :http_error | :webfinger_information_not_json
2021-04-09 10:19:25 +02:00
@doc """
Finger an actor to retreive it ' s ActivityPub ID/URL
2021-09-10 11:36:05 +02:00
Fetches the Extensible Resource Descriptor endpoint ` / . well - known / host - meta ` to find the Webfinger endpoint ( usually ` / . well - known / webfinger? resource = ` ) and then performs a Webfinger query to get the ActivityPub ID associated to an actor .
2021-04-09 10:19:25 +02:00
"""
2021-09-10 11:36:05 +02:00
@spec finger ( String . t ( ) ) ::
{ :ok , String . t ( ) }
| { :error , finger_errors }
2021-04-09 10:19:25 +02:00
def finger ( actor ) do
actor = String . trim_leading ( actor , " @ " )
2021-09-10 11:36:05 +02:00
case validate_endpoint ( actor ) do
{ :ok , address } ->
case fetch_webfinger_data ( address ) do
{ :ok , %{ " url " = > url } } ->
{ :ok , url }
{ :error , err } ->
Logger . debug ( " Couldn't process webfinger data for #{ actor } " )
err
end
{ :error , err } ->
Logger . debug ( " Couldn't find webfinger endpoint for #{ actor } " )
{ :error , err }
2021-04-09 10:19:25 +02:00
end
end
2021-09-10 11:36:05 +02:00
@spec fetch_webfinger_data ( String . t ( ) ) ::
{ :ok , map ( ) } | { :error , :webfinger_information_not_json | :http_error }
defp fetch_webfinger_data ( address ) do
case WebfingerClient . get ( address ) do
{ :ok , %{ body : body , status : code } } when code in 200 . . 299 ->
webfinger_from_json ( body )
_ ->
{ :error , :http_error }
end
end
@spec validate_endpoint ( String . t ( ) ) ::
{ :ok , String . t ( ) } | { :error , :address_invalid | :host_not_found }
defp validate_endpoint ( actor ) do
case apply_webfinger_endpoint ( actor ) do
address when is_binary ( address ) ->
if address_invalid ( address ) do
{ :error , :address_invalid }
else
{ :ok , address }
end
_ ->
{ :error , :host_not_found }
end
end
# Fetches the Extensible Resource Descriptor endpoint `/.well-known/host-meta` to find the Webfinger endpoint (usually `/.well-known/webfinger?resource=`)
2021-09-10 11:27:59 +02:00
@spec find_webfinger_endpoint ( String . t ( ) ) ::
{ :ok , String . t ( ) } | { :error , :link_not_found } | { :error , any ( ) }
2021-09-10 11:36:05 +02:00
defp find_webfinger_endpoint ( domain ) when is_binary ( domain ) do
2021-04-09 10:19:25 +02:00
with { :ok , %{ body : body } } <- fetch_document ( " http:// #{ domain } /.well-known/host-meta " ) ,
2021-06-10 11:40:54 +02:00
link_template when is_binary ( link_template ) <- find_link_from_template ( body ) do
2021-04-09 10:19:25 +02:00
{ :ok , link_template }
2021-09-10 11:27:59 +02:00
else
{ :error , :link_not_found } -> { :error , :link_not_found }
{ :error , error } -> { :error , error }
2021-04-09 10:19:25 +02:00
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
2018-05-17 11:32:23 +02:00
data =
Enum . reduce ( doc [ " links " ] , %{ " subject " = > doc [ " subject " ] } , fn link , data ->
case { link [ " type " ] , link [ " rel " ] } do
{ " application/activity+json " , " self " } ->
Map . put ( data , " url " , link [ " href " ] )
2018-07-27 10:45:35 +02:00
2018-05-17 11:32:23 +02:00
_ ->
2018-07-27 10:45:35 +02:00
Logger . debug ( fn ->
2020-10-15 17:19:15 +02:00
" Unhandled type to finger: #{ inspect ( link [ " type " ] ) } "
2018-07-27 10:45:35 +02:00
end )
2018-05-17 11:32:23 +02:00
data
end
end )
{ :ok , data }
end
2021-04-09 10:19:25 +02:00
defp webfinger_from_json ( _doc ) , do : { :error , :webfinger_information_not_json }
2018-05-17 11:32:23 +02:00
2021-04-09 10:19:25 +02:00
@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 }
2021-06-10 11:40:54 +02:00
catch
:exit , _e ->
{ :error , :link_not_found }
2021-04-09 10:19:25 +02:00
end
2018-07-27 10:45:35 +02:00
2021-04-09 10:19:25 +02:00
@spec fetch_document ( String . t ( ) ) :: Tesla.Env . result ( )
defp fetch_document ( endpoint ) do
with { :error , err } <- HostMetaClient . get ( endpoint ) , do : { :error , err }
end
2018-05-17 11:32:23 +02:00
2021-04-09 10:19:25 +02:00
@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 }
2018-05-17 11:32:23 +02:00
end
end
end