Fork of Pleroma with site-specific changes and feature branches https://git.pleroma.social/pleroma/pleroma
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

213 lines
5.6KB

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