logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma git clone https://hacktivis.me/git/pleroma.git

web_finger.ex (7429B)


  1. # Pleroma: A lightweight social networking server
  2. # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
  3. # SPDX-License-Identifier: AGPL-3.0-only
  4. defmodule Pleroma.Web.WebFinger do
  5. alias Pleroma.HTTP
  6. alias Pleroma.User
  7. alias Pleroma.Web.ActivityPub.Publisher
  8. alias Pleroma.Web.Endpoint
  9. alias Pleroma.Web.XML
  10. alias Pleroma.XmlBuilder
  11. require Jason
  12. require Logger
  13. def host_meta do
  14. base_url = Endpoint.url()
  15. {
  16. :XRD,
  17. %{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},
  18. {
  19. :Link,
  20. %{
  21. rel: "lrdd",
  22. type: "application/xrd+xml",
  23. template: "#{base_url}/.well-known/webfinger?resource={uri}"
  24. }
  25. }
  26. }
  27. |> XmlBuilder.to_doc()
  28. end
  29. def webfinger(resource, fmt) when fmt in ["XML", "JSON"] do
  30. host = Pleroma.Web.Endpoint.host()
  31. regex =
  32. if webfinger_domain = Pleroma.Config.get([__MODULE__, :domain]) do
  33. ~r/(acct:)?(?<username>[a-z0-9A-Z_\.-]+)@(#{host}|#{webfinger_domain})/
  34. else
  35. ~r/(acct:)?(?<username>[a-z0-9A-Z_\.-]+)@#{host}/
  36. end
  37. with %{"username" => username} <- Regex.named_captures(regex, resource),
  38. %User{} = user <- User.get_cached_by_nickname(username) do
  39. {:ok, represent_user(user, fmt)}
  40. else
  41. _e ->
  42. with %User{} = user <- User.get_cached_by_ap_id(resource) do
  43. {:ok, represent_user(user, fmt)}
  44. else
  45. _e ->
  46. {:error, "Couldn't find user"}
  47. end
  48. end
  49. end
  50. defp gather_links(%User{} = user) do
  51. [
  52. %{
  53. "rel" => "http://webfinger.net/rel/profile-page",
  54. "type" => "text/html",
  55. "href" => user.ap_id
  56. }
  57. ] ++ Publisher.gather_webfinger_links(user)
  58. end
  59. defp gather_aliases(%User{} = user) do
  60. [user.ap_id | user.also_known_as]
  61. end
  62. def represent_user(user, "JSON") do
  63. %{
  64. "subject" => "acct:#{user.nickname}@#{host()}",
  65. "aliases" => gather_aliases(user),
  66. "links" => gather_links(user)
  67. }
  68. end
  69. def represent_user(user, "XML") do
  70. aliases =
  71. user
  72. |> gather_aliases()
  73. |> Enum.map(&{:Alias, &1})
  74. links =
  75. gather_links(user)
  76. |> Enum.map(fn link -> {:Link, link} end)
  77. {
  78. :XRD,
  79. %{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},
  80. [
  81. {:Subject, "acct:#{user.nickname}@#{host()}"}
  82. ] ++ aliases ++ links
  83. }
  84. |> XmlBuilder.to_doc()
  85. end
  86. def host do
  87. Pleroma.Config.get([__MODULE__, :domain]) || Pleroma.Web.Endpoint.host()
  88. end
  89. defp webfinger_from_xml(body) do
  90. with {:ok, doc} <- XML.parse_document(body) do
  91. subject = XML.string_from_xpath("//Subject", doc)
  92. subscribe_address =
  93. ~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template}
  94. |> XML.string_from_xpath(doc)
  95. ap_id =
  96. ~s{//Link[@rel="self" and @type="application/activity+json"]/@href}
  97. |> XML.string_from_xpath(doc)
  98. data = %{
  99. "subject" => subject,
  100. "subscribe_address" => subscribe_address,
  101. "ap_id" => ap_id
  102. }
  103. {:ok, data}
  104. end
  105. end
  106. defp webfinger_from_json(body) do
  107. with {:ok, doc} <- Jason.decode(body) do
  108. data =
  109. Enum.reduce(doc["links"], %{"subject" => doc["subject"]}, fn link, data ->
  110. case {link["type"], link["rel"]} do
  111. {"application/activity+json", "self"} ->
  112. Map.put(data, "ap_id", link["href"])
  113. {"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "self"} ->
  114. Map.put(data, "ap_id", link["href"])
  115. {nil, "http://ostatus.org/schema/1.0/subscribe"} ->
  116. Map.put(data, "subscribe_address", link["template"])
  117. _ ->
  118. Logger.debug("Unhandled type: #{inspect(link["type"])}")
  119. data
  120. end
  121. end)
  122. {:ok, data}
  123. end
  124. end
  125. def get_template_from_xml(body) do
  126. xpath = "//Link[@rel='lrdd']/@template"
  127. with {:ok, doc} <- XML.parse_document(body),
  128. template when template != nil <- XML.string_from_xpath(xpath, doc) do
  129. {:ok, template}
  130. end
  131. end
  132. @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
  133. def find_lrdd_template(domain) do
  134. @cachex.fetch!(:host_meta_cache, domain, fn _ ->
  135. {:commit, fetch_lrdd_template(domain)}
  136. end)
  137. rescue
  138. e -> {:error, "Cachex error: #{inspect(e)}"}
  139. end
  140. defp fetch_lrdd_template(domain) do
  141. # WebFinger is restricted to HTTPS - https://tools.ietf.org/html/rfc7033#section-9.1
  142. meta_url = "https://#{domain}/.well-known/host-meta"
  143. with {:ok, %{status: status, body: body}} when status in 200..299 <- HTTP.get(meta_url) do
  144. get_template_from_xml(body)
  145. else
  146. error ->
  147. Logger.warning("Can't find LRDD template in #{inspect(meta_url)}: #{inspect(error)}")
  148. {:error, :lrdd_not_found}
  149. end
  150. end
  151. defp get_address_from_domain(domain, "acct:" <> _ = encoded_account) when is_binary(domain) do
  152. case find_lrdd_template(domain) do
  153. {:ok, template} ->
  154. String.replace(template, "{uri}", encoded_account)
  155. _ ->
  156. "https://#{domain}/.well-known/webfinger?resource=#{encoded_account}"
  157. end
  158. end
  159. defp get_address_from_domain(domain, account) when is_binary(domain) do
  160. encoded_account = URI.encode("acct:#{account}")
  161. get_address_from_domain(domain, encoded_account)
  162. end
  163. defp get_address_from_domain(_, _), do: {:error, :webfinger_no_domain}
  164. @spec finger(String.t()) :: {:ok, map()} | {:error, any()}
  165. def finger(account) do
  166. account = String.trim_leading(account, "@")
  167. domain =
  168. with [_name, domain] <- String.split(account, "@") do
  169. domain
  170. else
  171. _e ->
  172. URI.parse(account).host
  173. end
  174. with address when is_binary(address) <- get_address_from_domain(domain, account),
  175. {:ok, %{status: status, body: body, headers: headers}} when status in 200..299 <-
  176. HTTP.get(
  177. address,
  178. [{"accept", "application/xrd+xml,application/jrd+json"}]
  179. ) do
  180. case List.keyfind(headers, "content-type", 0) do
  181. {_, content_type} ->
  182. case Plug.Conn.Utils.media_type(content_type) do
  183. {:ok, "application", subtype, _} when subtype in ~w(xrd+xml xml) ->
  184. webfinger_from_xml(body)
  185. {:ok, "application", subtype, _} when subtype in ~w(jrd+json json) ->
  186. webfinger_from_json(body)
  187. _ ->
  188. {:error, {:content_type, content_type}}
  189. end
  190. _ ->
  191. {:error, {:content_type, nil}}
  192. end
  193. |> case do
  194. {:ok, data} -> validate_webfinger(address, data)
  195. error -> error
  196. end
  197. else
  198. error ->
  199. Logger.debug("Couldn't finger #{account}: #{inspect(error)}")
  200. error
  201. end
  202. end
  203. defp validate_webfinger(request_url, %{"subject" => "acct:" <> acct = subject} = data) do
  204. with [_name, acct_host] <- String.split(acct, "@"),
  205. {_, url} <- {:address, get_address_from_domain(acct_host, subject)},
  206. %URI{host: request_host} <- URI.parse(request_url),
  207. %URI{host: acct_host} <- URI.parse(url),
  208. {_, true} <- {:hosts_match, acct_host == request_host} do
  209. {:ok, data}
  210. else
  211. _ -> {:error, {:webfinger_invalid, request_url, data}}
  212. end
  213. end
  214. defp validate_webfinger(url, data), do: {:error, {:webfinger_invalid, url, data}}
  215. end