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.

225 lines
6.1KB

  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
  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. 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(body) do
  84. with {:ok, doc} <- XML.parse_document(body) do
  85. subject = XML.string_from_xpath("//Subject", doc)
  86. subscribe_address =
  87. ~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template}
  88. |> XML.string_from_xpath(doc)
  89. ap_id =
  90. ~s{//Link[@rel="self" and @type="application/activity+json"]/@href}
  91. |> XML.string_from_xpath(doc)
  92. data = %{
  93. "subject" => subject,
  94. "subscribe_address" => subscribe_address,
  95. "ap_id" => ap_id
  96. }
  97. {:ok, data}
  98. end
  99. end
  100. defp webfinger_from_json(body) do
  101. with {:ok, doc} <- Jason.decode(body) do
  102. data =
  103. Enum.reduce(doc["links"], %{"subject" => doc["subject"]}, fn link, data ->
  104. case {link["type"], link["rel"]} do
  105. {"application/activity+json", "self"} ->
  106. Map.put(data, "ap_id", link["href"])
  107. {"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "self"} ->
  108. Map.put(data, "ap_id", link["href"])
  109. {nil, "http://ostatus.org/schema/1.0/subscribe"} ->
  110. Map.put(data, "subscribe_address", link["template"])
  111. _ ->
  112. Logger.debug("Unhandled type: #{inspect(link["type"])}")
  113. data
  114. end
  115. end)
  116. {:ok, data}
  117. end
  118. end
  119. def get_template_from_xml(body) do
  120. xpath = "//Link[@rel='lrdd']/@template"
  121. with {:ok, doc} <- XML.parse_document(body),
  122. template when template != nil <- XML.string_from_xpath(xpath, doc) do
  123. {:ok, template}
  124. end
  125. end
  126. def find_lrdd_template(domain) do
  127. with {:ok, %{status: status, body: body}} when status in 200..299 <-
  128. HTTP.get("http://#{domain}/.well-known/host-meta") do
  129. get_template_from_xml(body)
  130. else
  131. _ ->
  132. with {:ok, %{body: body, status: status}} when status in 200..299 <-
  133. HTTP.get("https://#{domain}/.well-known/host-meta") do
  134. get_template_from_xml(body)
  135. else
  136. e -> {:error, "Can't find LRDD template: #{inspect(e)}"}
  137. end
  138. end
  139. end
  140. defp get_address_from_domain(domain, encoded_account) when is_binary(domain) do
  141. case find_lrdd_template(domain) do
  142. {:ok, template} ->
  143. String.replace(template, "{uri}", encoded_account)
  144. _ ->
  145. "https://#{domain}/.well-known/webfinger?resource=#{encoded_account}"
  146. end
  147. end
  148. defp get_address_from_domain(_, _), do: nil
  149. @spec finger(String.t()) :: {:ok, map()} | {:error, any()}
  150. def finger(account) do
  151. account = String.trim_leading(account, "@")
  152. domain =
  153. with [_name, domain] <- String.split(account, "@") do
  154. domain
  155. else
  156. _e ->
  157. URI.parse(account).host
  158. end
  159. encoded_account = URI.encode("acct:#{account}")
  160. with address when is_binary(address) <- get_address_from_domain(domain, encoded_account),
  161. response <-
  162. HTTP.get(
  163. address,
  164. [{"accept", "application/xrd+xml,application/jrd+json"}]
  165. ),
  166. {:ok, %{status: status, body: body, headers: headers}} when status in 200..299 <-
  167. response do
  168. case List.keyfind(headers, "content-type", 0) do
  169. {_, content_type} ->
  170. case Plug.Conn.Utils.media_type(content_type) do
  171. {:ok, "application", subtype, _} when subtype in ~w(xrd+xml xml) ->
  172. webfinger_from_xml(body)
  173. {:ok, "application", subtype, _} when subtype in ~w(jrd+json json) ->
  174. webfinger_from_json(body)
  175. _ ->
  176. {:error, {:content_type, content_type}}
  177. end
  178. _ ->
  179. {:error, {:content_type, nil}}
  180. end
  181. else
  182. e ->
  183. Logger.debug(fn -> "Couldn't finger #{account}" end)
  184. Logger.debug(fn -> inspect(e) end)
  185. {:error, e}
  186. end
  187. end
  188. end