web_finger.ex 6.4 KB
Newer Older
1
# Pleroma: A lightweight social networking server
kaniini's avatar
kaniini committed
2
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 4
# SPDX-License-Identifier: AGPL-3.0-only

lain's avatar
lain committed
5
defmodule Pleroma.Web.WebFinger do
lain's avatar
lain committed
6
  @httpoison Application.get_env(:pleroma, :httpoison)
7

Haelwenn's avatar
Haelwenn committed
8
  alias Pleroma.User
9
  alias Pleroma.Web
10
  alias Pleroma.Web.Federator.Publisher
11 12
  alias Pleroma.Web.XML
  alias Pleroma.XmlBuilder
lain's avatar
lain committed
13
  require Jason
lain's avatar
lain committed
14
  require Logger
lain's avatar
lain committed
15

16
  def host_meta do
lain's avatar
lain committed
17 18
    base_url = Web.base_url()

lain's avatar
lain committed
19
    {
lain's avatar
lain committed
20 21
      :XRD,
      %{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},
lain's avatar
lain committed
22
      {
lain's avatar
lain committed
23 24 25 26 27 28
        :Link,
        %{
          rel: "lrdd",
          type: "application/xrd+xml",
          template: "#{base_url}/.well-known/webfinger?resource={uri}"
        }
lain's avatar
lain committed
29 30
      }
    }
lain's avatar
lain committed
31
    |> XmlBuilder.to_doc()
lain's avatar
lain committed
32 33
  end

34
  def webfinger(resource, fmt) when fmt in ["XML", "JSON"] do
lain's avatar
lain committed
35
    host = Pleroma.Web.Endpoint.host()
lain's avatar
lain committed
36
    regex = ~r/(acct:)?(?<username>\w+)@#{host}/
lain's avatar
lain committed
37

38
    with %{"username" => username} <- Regex.named_captures(regex, resource),
minibikini's avatar
minibikini committed
39
         %User{} = user <- User.get_cached_by_nickname(username) do
40
      {:ok, represent_user(user, fmt)}
lain's avatar
lain committed
41 42
    else
      _e ->
43 44
        with %User{} = user <- User.get_cached_by_ap_id(resource) do
          {:ok, represent_user(user, fmt)}
lain's avatar
lain committed
45 46 47 48
        else
          _e ->
            {:error, "Couldn't find user"}
        end
kaniini's avatar
kaniini committed
49 50 51
    end
  end

52 53 54 55 56 57 58 59 60 61
  defp gather_links(%User{} = user) do
    [
      %{
        "rel" => "http://webfinger.net/rel/profile-page",
        "type" => "text/html",
        "href" => user.ap_id
      }
    ] ++ Publisher.gather_webfinger_links(user)
  end

kaniini's avatar
kaniini committed
62
  def represent_user(user, "JSON") do
63
    {:ok, user} = User.ensure_keys_present(user)
lain's avatar
lain committed
64

kaniini's avatar
kaniini committed
65
    %{
lain's avatar
lain committed
66
      "subject" => "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}",
kaniini's avatar
kaniini committed
67
      "aliases" => [user.ap_id],
68
      "links" => gather_links(user)
kaniini's avatar
kaniini committed
69 70 71 72
    }
  end

  def represent_user(user, "XML") do
73
    {:ok, user} = User.ensure_keys_present(user)
74 75 76 77

    links =
      gather_links(user)
      |> Enum.map(fn link -> {:Link, link} end)
lain's avatar
lain committed
78

lain's avatar
lain committed
79
    {
lain's avatar
lain committed
80 81
      :XRD,
      %{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},
lain's avatar
lain committed
82
      [
lain's avatar
lain committed
83
        {:Subject, "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}"},
84 85
        {:Alias, user.ap_id}
      ] ++ links
lain's avatar
lain committed
86
    }
lain's avatar
lain committed
87
    |> XmlBuilder.to_doc()
lain's avatar
lain committed
88
  end
lain's avatar
lain committed
89

Rachel H's avatar
Rachel H committed
90
  defp get_magic_key(magic_key) do
lain's avatar
lain committed
91
    "data:application/magic-public-key," <> magic_key = magic_key
Rachel H's avatar
Rachel H committed
92 93 94 95
    {:ok, magic_key}
  rescue
    MatchError -> {:error, "Missing magic key data."}
  end
lain's avatar
lain committed
96

Rachel H's avatar
Rachel H committed
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
  defp webfinger_from_xml(doc) do
    with magic_key <- XML.string_from_xpath(~s{//Link[@rel="magic-public-key"]/@href}, doc),
         {:ok, magic_key} <- get_magic_key(magic_key),
         topic <-
           XML.string_from_xpath(
             ~s{//Link[@rel="http://schemas.google.com/g/2010#updates-from"]/@href},
             doc
           ),
         subject <- XML.string_from_xpath("//Subject", doc),
         salmon <- XML.string_from_xpath(~s{//Link[@rel="salmon"]/@href}, doc),
         subscribe_address <-
           XML.string_from_xpath(
             ~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template},
             doc
           ),
         ap_id <-
           XML.string_from_xpath(
             ~s{//Link[@rel="self" and @type="application/activity+json"]/@href},
             doc
           ) do
      data = %{
        "magic_key" => magic_key,
        "topic" => topic,
        "subject" => subject,
        "salmon" => salmon,
        "subscribe_address" => subscribe_address,
        "ap_id" => ap_id
      }
lain's avatar
lain committed
125

Rachel H's avatar
Rachel H committed
126 127 128 129 130 131 132 133
      {:ok, data}
    else
      {:error, e} ->
        {:error, e}

      e ->
        {:error, e}
    end
lain's avatar
lain committed
134 135
  end

136
  defp webfinger_from_json(doc) do
lain's avatar
lain committed
137 138 139 140 141 142
    data =
      Enum.reduce(doc["links"], %{"subject" => doc["subject"]}, fn link, data ->
        case {link["type"], link["rel"]} do
          {"application/activity+json", "self"} ->
            Map.put(data, "ap_id", link["href"])

143 144 145
          {"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "self"} ->
            Map.put(data, "ap_id", link["href"])

lain's avatar
lain committed
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
          {_, "magic-public-key"} ->
            "data:application/magic-public-key," <> magic_key = link["href"]
            Map.put(data, "magic_key", magic_key)

          {"application/atom+xml", "http://schemas.google.com/g/2010#updates-from"} ->
            Map.put(data, "topic", link["href"])

          {_, "salmon"} ->
            Map.put(data, "salmon", link["href"])

          {_, "http://ostatus.org/schema/1.0/subscribe"} ->
            Map.put(data, "subscribe_address", link["template"])

          _ ->
            Logger.debug("Unhandled type: #{inspect(link["type"])}")
            data
        end
      end)

165 166 167
    {:ok, data}
  end

lain's avatar
lain committed
168
  def get_template_from_xml(body) do
169
    xpath = "//Link[@rel='lrdd']/@template"
lain's avatar
lain committed
170

lain's avatar
lain committed
171
    with doc when doc != :error <- XML.parse_document(body),
lain's avatar
lain committed
172
         template when template != nil <- XML.string_from_xpath(xpath, doc) do
lain's avatar
lain committed
173 174 175 176 177
      {:ok, template}
    end
  end

  def find_lrdd_template(domain) do
Maksim's avatar
Maksim committed
178
    with {:ok, %{status: status, body: body}} when status in 200..299 <-
Hakaba Hitoyo's avatar
Hakaba Hitoyo committed
179
           @httpoison.get("http://#{domain}/.well-known/host-meta", []) do
lain's avatar
lain committed
180 181
      get_template_from_xml(body)
    else
Thog's avatar
Thog committed
182
      _ ->
Roger Braun's avatar
Roger Braun committed
183 184 185
        with {:ok, %{body: body}} <- @httpoison.get("https://#{domain}/.well-known/host-meta", []) do
          get_template_from_xml(body)
        else
186
          e -> {:error, "Can't find LRDD template: #{inspect(e)}"}
Roger Braun's avatar
Roger Braun committed
187
        end
lain's avatar
lain committed
188 189 190 191
    end
  end

  def finger(account) do
lain's avatar
lain committed
192
    account = String.trim_leading(account, "@")
lain's avatar
lain committed
193 194 195 196 197 198 199 200

    domain =
      with [_name, domain] <- String.split(account, "@") do
        domain
      else
        _e ->
          URI.parse(account).host
      end
lain's avatar
lain committed
201

eal's avatar
eal committed
202 203 204 205
    address =
      case find_lrdd_template(domain) do
        {:ok, template} ->
          String.replace(template, "{uri}", URI.encode(account))
lain's avatar
lain committed
206

eal's avatar
eal committed
207
        _ ->
208
          "https://#{domain}/.well-known/webfinger?resource=acct:#{account}"
eal's avatar
eal committed
209
      end
210

lain's avatar
lain committed
211 212 213
    with response <-
           @httpoison.get(
             address,
Maksim's avatar
Maksim committed
214
             Accept: "application/xrd+xml,application/jrd+json"
lain's avatar
lain committed
215
           ),
Maksim's avatar
Maksim committed
216
         {:ok, %{status: status, body: body}} when status in 200..299 <- response do
lain's avatar
lain committed
217 218 219 220 221
      doc = XML.parse_document(body)

      if doc != :error do
        webfinger_from_xml(doc)
      else
Rachel H's avatar
Rachel H committed
222 223 224 225 226
        with {:ok, doc} <- Jason.decode(body) do
          webfinger_from_json(doc)
        else
          {:error, e} -> e
        end
lain's avatar
lain committed
227
      end
lain's avatar
lain committed
228 229
    else
      e ->
feld's avatar
feld committed
230
        Logger.debug(fn -> "Couldn't finger #{account}" end)
lain's avatar
lain committed
231
        Logger.debug(fn -> inspect(e) end)
lain's avatar
lain committed
232 233 234
        {:error, e}
    end
  end
lain's avatar
lain committed
235
end