web_finger.ex 8.06 KB
Newer Older
Roger Braun's avatar
Roger Braun committed
1
defmodule Pleroma.Web.WebFinger do
Roger Braun's avatar
Roger Braun committed
2
  @httpoison Application.get_env(:pleroma, :httpoison)
3

4
  alias Pleroma.{User, XmlBuilder}
5
  alias Pleroma.Web
Roger Braun's avatar
Roger Braun committed
6
  alias Pleroma.Web.{XML, Salmon, OStatus}
lain's avatar
lain committed
7
  require Jason
8
  require Logger
Roger Braun's avatar
Roger Braun committed
9

10
  def host_meta do
lain's avatar
lain committed
11 12
    base_url = Web.base_url()

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

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

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

  def represent_user(user, "JSON") do
    {:ok, user} = ensure_keys_present(user)
    {:ok, _private, public} = Salmon.keys_from_pem(user.info["keys"])
    magic_key = Salmon.encode_key(public)
lain's avatar
lain committed
50

51
    %{
lain's avatar
lain committed
52
      "subject" => "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}",
53 54
      "aliases" => [user.ap_id],
      "links" => [
lain's avatar
lain committed
55 56 57 58 59 60 61 62 63 64
        %{
          "rel" => "http://schemas.google.com/g/2010#updates-from",
          "type" => "application/atom+xml",
          "href" => OStatus.feed_path(user)
        },
        %{
          "rel" => "http://webfinger.net/rel/profile-page",
          "type" => "text/html",
          "href" => user.ap_id
        },
65
        %{"rel" => "salmon", "href" => OStatus.salmon_path(user)},
lain's avatar
lain committed
66 67 68 69
        %{
          "rel" => "magic-public-key",
          "href" => "data:application/magic-public-key,#{magic_key}"
        },
70
        %{"rel" => "self", "type" => "application/activity+json", "href" => user.ap_id},
Ariadne Conill's avatar
Ariadne Conill committed
71 72 73 74 75
        %{
          "rel" => "self",
          "type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
          "href" => user.ap_id
        },
lain's avatar
lain committed
76 77 78 79
        %{
          "rel" => "http://ostatus.org/schema/1.0/subscribe",
          "template" => OStatus.remote_follow_path()
        }
80 81 82 83 84
      ]
    }
  end

  def represent_user(user, "XML") do
Roger Braun's avatar
Roger Braun committed
85 86 87
    {:ok, user} = ensure_keys_present(user)
    {:ok, _private, public} = Salmon.keys_from_pem(user.info["keys"])
    magic_key = Salmon.encode_key(public)
lain's avatar
lain committed
88

Roger Braun's avatar
Roger Braun committed
89
    {
lain's avatar
lain committed
90 91
      :XRD,
      %{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},
Roger Braun's avatar
Roger Braun committed
92
      [
lain's avatar
lain committed
93
        {:Subject, "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}"},
Roger Braun's avatar
Roger Braun committed
94
        {:Alias, user.ap_id},
lain's avatar
lain committed
95 96 97 98 99 100 101 102
        {:Link,
         %{
           rel: "http://schemas.google.com/g/2010#updates-from",
           type: "application/atom+xml",
           href: OStatus.feed_path(user)
         }},
        {:Link,
         %{rel: "http://webfinger.net/rel/profile-page", type: "text/html", href: user.ap_id}},
Roger Braun's avatar
Roger Braun committed
103
        {:Link, %{rel: "salmon", href: OStatus.salmon_path(user)}},
lain's avatar
lain committed
104 105
        {:Link,
         %{rel: "magic-public-key", href: "data:application/magic-public-key,#{magic_key}"}},
106
        {:Link, %{rel: "self", type: "application/activity+json", href: user.ap_id}},
lain's avatar
lain committed
107 108
        {:Link,
         %{rel: "http://ostatus.org/schema/1.0/subscribe", template: OStatus.remote_follow_path()}}
Roger Braun's avatar
Roger Braun committed
109 110
      ]
    }
lain's avatar
lain committed
111
    |> XmlBuilder.to_doc()
Roger Braun's avatar
Roger Braun committed
112
  end
113

Roger Braun's avatar
Roger Braun committed
114
  # This seems a better fit in Salmon
Roger Braun's avatar
Roger Braun committed
115 116
  def ensure_keys_present(user) do
    info = user.info || %{}
lain's avatar
lain committed
117

Roger Braun's avatar
Roger Braun committed
118 119 120
    if info["keys"] do
      {:ok, user}
    else
lain's avatar
lain committed
121
      {:ok, pem} = Salmon.generate_rsa_pem()
Roger Braun's avatar
Roger Braun committed
122
      info = Map.put(info, "keys", pem)
lain's avatar
lain committed
123

lain's avatar
lain committed
124 125
      Ecto.Changeset.change(user, info: info)
      |> User.update_and_set_cache()
Roger Braun's avatar
Roger Braun committed
126 127 128
    end
  end

Rachel Hutchison's avatar
Rachel Hutchison committed
129
  defp get_magic_key(magic_key) do
130
    "data:application/magic-public-key," <> magic_key = magic_key
Rachel Hutchison's avatar
Rachel Hutchison committed
131 132 133 134
    {:ok, magic_key}
  rescue
    MatchError -> {:error, "Missing magic key data."}
  end
lain's avatar
lain committed
135

Rachel Hutchison's avatar
Rachel Hutchison committed
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
  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
164

Rachel Hutchison's avatar
Rachel Hutchison committed
165 166 167 168 169 170 171 172
      {:ok, data}
    else
      {:error, e} ->
        {:error, e}

      e ->
        {:error, e}
    end
173 174
  end

175
  defp webfinger_from_json(doc) do
lain's avatar
lain committed
176 177 178 179 180 181
    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"])

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

lain's avatar
lain committed
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
          {_, "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)

204 205 206
    {:ok, data}
  end

207
  def get_template_from_xml(body) do
208
    xpath = "//Link[@rel='lrdd']/@template"
lain's avatar
lain committed
209

210
    with doc when doc != :error <- XML.parse_document(body),
lain's avatar
lain committed
211
         template when template != nil <- XML.string_from_xpath(xpath, doc) do
212 213 214 215 216
      {:ok, template}
    end
  end

  def find_lrdd_template(domain) do
lain's avatar
lain committed
217 218
    with {:ok, %{status_code: status_code, body: body}} when status_code in 200..299 <-
           @httpoison.get("http://#{domain}/.well-known/host-meta", [], follow_redirect: true) do
219 220
      get_template_from_xml(body)
    else
221
      _ ->
222 223 224
        with {:ok, %{body: body}} <- @httpoison.get("https://#{domain}/.well-known/host-meta", []) do
          get_template_from_xml(body)
        else
225
          e -> {:error, "Can't find LRDD template: #{inspect(e)}"}
226
        end
227 228 229 230
    end
  end

  def finger(account) do
lain's avatar
lain committed
231
    account = String.trim_leading(account, "@")
lain's avatar
lain committed
232 233 234 235 236 237 238 239

    domain =
      with [_name, domain] <- String.split(account, "@") do
        domain
      else
        _e ->
          URI.parse(account).host
      end
240

eal's avatar
eal committed
241 242 243 244
    address =
      case find_lrdd_template(domain) do
        {:ok, template} ->
          String.replace(template, "{uri}", URI.encode(account))
lain's avatar
lain committed
245

eal's avatar
eal committed
246
        _ ->
247
          "https://#{domain}/.well-known/webfinger?resource=acct:#{account}"
eal's avatar
eal committed
248
      end
249

lain's avatar
lain committed
250 251 252 253 254 255
    with response <-
           @httpoison.get(
             address,
             [Accept: "application/xrd+xml,application/jrd+json"],
             follow_redirect: true
           ),
256
         {:ok, %{status_code: status_code, body: body}} when status_code in 200..299 <- response do
lain's avatar
lain committed
257 258 259 260 261
      doc = XML.parse_document(body)

      if doc != :error do
        webfinger_from_xml(doc)
      else
Rachel Hutchison's avatar
Rachel Hutchison committed
262 263 264 265 266
        with {:ok, doc} <- Jason.decode(body) do
          webfinger_from_json(doc)
        else
          {:error, e} -> e
        end
lain's avatar
lain committed
267
      end
268 269
    else
      e ->
Mark Felder's avatar
Mark Felder committed
270
        Logger.debug(fn -> "Couldn't finger #{account}" end)
Roger Braun's avatar
Roger Braun committed
271
        Logger.debug(fn -> inspect(e) end)
272 273 274
        {:error, e}
    end
  end
Roger Braun's avatar
Roger Braun committed
275
end