logo

pleroma

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

web_finger.ex (6321B)


  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. def find_lrdd_template(domain) do
  133. # WebFinger is restricted to HTTPS - https://tools.ietf.org/html/rfc7033#section-9.1
  134. meta_url = "https://#{domain}/.well-known/host-meta"
  135. with {:ok, %{status: status, body: body}} when status in 200..299 <- HTTP.get(meta_url) do
  136. get_template_from_xml(body)
  137. else
  138. error ->
  139. Logger.warning("Can't find LRDD template in #{inspect(meta_url)}: #{inspect(error)}")
  140. {:error, :lrdd_not_found}
  141. end
  142. end
  143. defp get_address_from_domain(domain, encoded_account) when is_binary(domain) do
  144. case find_lrdd_template(domain) do
  145. {:ok, template} ->
  146. String.replace(template, "{uri}", encoded_account)
  147. _ ->
  148. "https://#{domain}/.well-known/webfinger?resource=#{encoded_account}"
  149. end
  150. end
  151. defp get_address_from_domain(_, _), do: {:error, :webfinger_no_domain}
  152. @spec finger(String.t()) :: {:ok, map()} | {:error, any()}
  153. def finger(account) do
  154. account = String.trim_leading(account, "@")
  155. domain =
  156. with [_name, domain] <- String.split(account, "@") do
  157. domain
  158. else
  159. _e ->
  160. URI.parse(account).host
  161. end
  162. encoded_account = URI.encode("acct:#{account}")
  163. with address when is_binary(address) <- get_address_from_domain(domain, encoded_account),
  164. {:ok, %{status: status, body: body, headers: headers}} when status in 200..299 <-
  165. HTTP.get(
  166. address,
  167. [{"accept", "application/xrd+xml,application/jrd+json"}]
  168. ) do
  169. case List.keyfind(headers, "content-type", 0) do
  170. {_, content_type} ->
  171. case Plug.Conn.Utils.media_type(content_type) do
  172. {:ok, "application", subtype, _} when subtype in ~w(xrd+xml xml) ->
  173. webfinger_from_xml(body)
  174. {:ok, "application", subtype, _} when subtype in ~w(jrd+json json) ->
  175. webfinger_from_json(body)
  176. _ ->
  177. {:error, {:content_type, content_type}}
  178. end
  179. _ ->
  180. {:error, {:content_type, nil}}
  181. end
  182. else
  183. error ->
  184. Logger.debug("Couldn't finger #{account}: #{inspect(error)}")
  185. error
  186. end
  187. end
  188. end