logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma

web_finger.ex (5465B)


  1. # Pleroma: A lightweight social networking server
  2. # Copyright © 2017-2020 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
  8. alias Pleroma.Web.Federator.Publisher
  9. alias Pleroma.Web.XML
  10. alias Pleroma.XmlBuilder
  11. require Jason
  12. require Logger
  13. def host_meta do
  14. base_url = Web.base_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 = ~r/(acct:)?(?<username>[a-z0-9A-Z_\.-]+)@#{host}/
  32. with %{"username" => username} <- Regex.named_captures(regex, resource),
  33. %User{} = user <- User.get_cached_by_nickname(username) do
  34. {:ok, represent_user(user, fmt)}
  35. else
  36. _e ->
  37. with %User{} = user <- User.get_cached_by_ap_id(resource) do
  38. {:ok, represent_user(user, fmt)}
  39. else
  40. _e ->
  41. {:error, "Couldn't find user"}
  42. end
  43. end
  44. end
  45. defp gather_links(%User{} = user) do
  46. [
  47. %{
  48. "rel" => "http://webfinger.net/rel/profile-page",
  49. "type" => "text/html",
  50. "href" => user.ap_id
  51. }
  52. ] ++ Publisher.gather_webfinger_links(user)
  53. end
  54. def represent_user(user, "JSON") do
  55. {:ok, user} = User.ensure_keys_present(user)
  56. %{
  57. "subject" => "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}",
  58. "aliases" => [user.ap_id],
  59. "links" => gather_links(user)
  60. }
  61. end
  62. def represent_user(user, "XML") do
  63. {:ok, user} = User.ensure_keys_present(user)
  64. links =
  65. gather_links(user)
  66. |> Enum.map(fn link -> {:Link, link} end)
  67. {
  68. :XRD,
  69. %{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},
  70. [
  71. {:Subject, "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}"},
  72. {:Alias, user.ap_id}
  73. ] ++ links
  74. }
  75. |> XmlBuilder.to_doc()
  76. end
  77. defp webfinger_from_xml(doc) do
  78. subject = XML.string_from_xpath("//Subject", doc)
  79. subscribe_address =
  80. ~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template}
  81. |> XML.string_from_xpath(doc)
  82. ap_id =
  83. ~s{//Link[@rel="self" and @type="application/activity+json"]/@href}
  84. |> XML.string_from_xpath(doc)
  85. data = %{
  86. "subject" => subject,
  87. "subscribe_address" => subscribe_address,
  88. "ap_id" => ap_id
  89. }
  90. {:ok, data}
  91. end
  92. defp webfinger_from_json(doc) do
  93. data =
  94. Enum.reduce(doc["links"], %{"subject" => doc["subject"]}, fn link, data ->
  95. case {link["type"], link["rel"]} do
  96. {"application/activity+json", "self"} ->
  97. Map.put(data, "ap_id", link["href"])
  98. {"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "self"} ->
  99. Map.put(data, "ap_id", link["href"])
  100. _ ->
  101. Logger.debug("Unhandled type: #{inspect(link["type"])}")
  102. data
  103. end
  104. end)
  105. {:ok, data}
  106. end
  107. def get_template_from_xml(body) do
  108. xpath = "//Link[@rel='lrdd']/@template"
  109. with doc when doc != :error <- XML.parse_document(body),
  110. template when template != nil <- XML.string_from_xpath(xpath, doc) do
  111. {:ok, template}
  112. end
  113. end
  114. def find_lrdd_template(domain) do
  115. with {:ok, %{status: status, body: body}} when status in 200..299 <-
  116. HTTP.get("http://#{domain}/.well-known/host-meta") do
  117. get_template_from_xml(body)
  118. else
  119. _ ->
  120. with {:ok, %{body: body, status: status}} when status in 200..299 <-
  121. HTTP.get("https://#{domain}/.well-known/host-meta") do
  122. get_template_from_xml(body)
  123. else
  124. e -> {:error, "Can't find LRDD template: #{inspect(e)}"}
  125. end
  126. end
  127. end
  128. defp get_address_from_domain(domain, encoded_account) when is_binary(domain) do
  129. case find_lrdd_template(domain) do
  130. {:ok, template} ->
  131. String.replace(template, "{uri}", encoded_account)
  132. _ ->
  133. "https://#{domain}/.well-known/webfinger?resource=#{encoded_account}"
  134. end
  135. end
  136. defp get_address_from_domain(_, _), do: nil
  137. @spec finger(String.t()) :: {:ok, map()} | {:error, any()}
  138. def finger(account) do
  139. account = String.trim_leading(account, "@")
  140. domain =
  141. with [_name, domain] <- String.split(account, "@") do
  142. domain
  143. else
  144. _e ->
  145. URI.parse(account).host
  146. end
  147. encoded_account = URI.encode("acct:#{account}")
  148. with address when is_binary(address) <- get_address_from_domain(domain, encoded_account),
  149. response <-
  150. HTTP.get(
  151. address,
  152. [{"accept", "application/xrd+xml,application/jrd+json"}]
  153. ),
  154. {:ok, %{status: status, body: body}} when status in 200..299 <- response do
  155. doc = XML.parse_document(body)
  156. if doc != :error do
  157. webfinger_from_xml(doc)
  158. else
  159. with {:ok, doc} <- Jason.decode(body) do
  160. webfinger_from_json(doc)
  161. end
  162. end
  163. else
  164. e ->
  165. Logger.debug(fn -> "Couldn't finger #{account}" end)
  166. Logger.debug(fn -> inspect(e) end)
  167. {:error, e}
  168. end
  169. end
  170. end