WebFinger: Serve /.well-known/host-meta.json requests

This commit is contained in:
Haelwenn (lanodan) Monnier 2023-09-07 01:33:35 +02:00
parent 44f272b9fa
commit beddda8547
6 changed files with 107 additions and 10 deletions

View file

@ -0,0 +1 @@
- Add support for serving `/.well-known/host-meta.json` requests. If you use a different hostname between WebFinger and Pleroma, like say `user@domain.tld` pointing to `social.domain.tld`, you'll need to update your HTTP server configuration.

View file

@ -21,7 +21,7 @@ Both account identifiers are unique and required for Pleroma. An important risk
As said earlier, each Pleroma user has an `acct`: URI, which is used for discovery and authentication. When you add @user@example.org, a webfinger query is performed. This is done in two steps:
1. Querying `https://example.org/.well-known/host-meta` (where the domain of the URL matches the domain part of the `acct`: URI) to get information on how to perform the query.
1. Querying `https://example.org/.well-known/host-meta` or `https://example.org/.well-known/host-meta.json` (where the domain of the URL matches the domain part of the `acct`: URI) to get information on how to perform the query.
This file will indeed contain a URL template of the form `https://example.org/.well-known/webfinger?resource={uri}` that will be used in the second step.
2. Fill the returned template with the `acct`: URI to be queried and perform the query: `https://example.org/.well-known/webfinger?resource=acct:user@example.org`
@ -57,6 +57,9 @@ With nginx, it would be as simple as adding:
location = /.well-known/host-meta {
return 301 https://pleroma.example.org$request_uri;
}
location = /.well-known/host-meta.json {
return 301 https://pleroma.example.org$request_uri;
}
```
in example.org's server block.

View file

@ -920,6 +920,7 @@ defmodule Pleroma.Web.Router do
pipe_through(:well_known)
get("/host-meta", WebFinger.WebFingerController, :host_meta)
get("/host-meta.json", WebFinger.WebFingerController, :host_meta_json)
get("/webfinger", WebFinger.WebFingerController, :webfinger)
get("/nodeinfo", Nodeinfo.NodeinfoController, :schemas)
end

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.WebFinger do
@ -9,9 +9,13 @@ defmodule Pleroma.Web.WebFinger do
alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.XML
alias Pleroma.XmlBuilder
require Jason
require Logger
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
# Technically not WebFinger but RFC6415 "Web Host Metadata"
def host_meta do
base_url = Endpoint.url()
@ -30,6 +34,21 @@ defmodule Pleroma.Web.WebFinger do
|> XmlBuilder.to_doc()
end
def host_meta_json do
base_url = Endpoint.url()
%{
"links" => [
%{
"rel" => "lrdd",
"type" => "application/jrd+json",
"template" => "#{base_url}/.well-known/webfinger?resource={uri}"
}
]
}
end
# WebFinger RFC7033
def webfinger(resource, fmt) when fmt in ["XML", "JSON"] do
host = Pleroma.Web.Endpoint.host()
@ -68,7 +87,7 @@ defmodule Pleroma.Web.WebFinger do
[user.ap_id | user.also_known_as]
end
def represent_user(user, "JSON") do
defp represent_user(user, "JSON") do
%{
"subject" => "acct:#{user.nickname}@#{domain()}",
"aliases" => gather_aliases(user),
@ -76,7 +95,7 @@ defmodule Pleroma.Web.WebFinger do
}
end
def represent_user(user, "XML") do
defp represent_user(user, "XML") do
aliases =
user
|> gather_aliases()
@ -146,7 +165,7 @@ defmodule Pleroma.Web.WebFinger do
end
end
def get_template_from_xml(body) do
defp get_template_from_xml(body) do
xpath = "//Link[@rel='lrdd']/@template"
with {:ok, doc} <- XML.parse_document(body),
@ -155,16 +174,61 @@ defmodule Pleroma.Web.WebFinger do
end
end
def find_lrdd_template(domain) do
defp cached_find_lrdd_template(domain) do
@cachex.fetch!(:webfinger_cache, "lrdd:#{domain}", fn _ ->
with {:ok, _} = template <- find_lrdd_template(domain) do
{:commit, template}
else
e -> {:ignore, e}
end
end)
end
defp find_lrdd_template(domain) do
with {:ok, _} = template <- find_lrdd_template_json(domain) do
template
else
:error ->
with {:ok, _} = template <- find_lrdd_template_xml(domain) do
template
end
end
end
defp find_lrdd_template_json(domain) do
# WebFinger is restricted to HTTPS - https://tools.ietf.org/html/rfc7033#section-9.1
meta_url = "https://#{domain}/.well-known/host-meta.json"
with
{_, {:ok, %{status: status, body: body} = resp}} when status in 200..299 <- {:http, HTTP.get(meta_json_url, [{"accept", "application/json"}])},
{_, content_type} when is_binary(content_type) <- {:content_type, Tesla.get_header(resp, "content-type")},
true <- content_type =~ ~r[^application/(jrd\+)?json(; .*)?],
%{"template" => template} <- Enum.find(body, fn link -> link["rel"] == "lrdd" and Map.has_key?(link, "template" end) do
{:ok, template}
else
{:http, e} ->
Logger.warn("WebFinger: #{meta_url} HTTP error: #{inspect(e)}")
:error
{:content_type, _} ->
Logger.warn("WebFinger: #{meta_url} had no Content-Type header")
:error
false ->
Logger.warn("WebFinger: #{meta_url} gave #{content_type} instead of application/json")
:error
nil ->
Logger.warn("WebFinger: #{meta_url} had no LRDD template")
:error
end
end
defp find_lrdd_template_xml(domain) do
# WebFinger is restricted to HTTPS - https://tools.ietf.org/html/rfc7033#section-9.1
meta_url = "https://#{domain}/.well-known/host-meta"
with {:ok, %{status: status, body: body}} when status in 200..299 <- HTTP.get(meta_url) do
get_template_from_xml(body)
else
error ->
Logger.warn("Can't find LRDD template in #{inspect(meta_url)}: #{inspect(error)}")
{:error, :lrdd_not_found}
error -> {:error, error}
end
end

View file

@ -18,6 +18,14 @@ defmodule Pleroma.Web.WebFinger.WebFingerController do
|> send_resp(200, xml)
end
def host_meta_json(conn, _params) do
jrd = WebFinger.host_meta_json()
conn
|> put_resp_content_type("application/jrd+json")
|> json(jrd)
end
def webfinger(%{assigns: %{format: format}} = conn, %{"resource" => resource})
when format in ["xml", "xrd+xml"] do
with {:ok, response} <- WebFinger.webfinger(resource, "XML") do

View file

@ -22,9 +22,29 @@ defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do
|> get("/.well-known/host-meta")
assert response.status == 200
assert get_resp_header(response, "content-type") == ["application/xrd+xml; charset=utf-8"]
assert response.resp_body ==
~s(<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" template="#{Pleroma.Web.Endpoint.url()}/.well-known/webfinger?resource={uri}" type="application/xrd+xml" /></XRD>)
~s[<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" template="#{Pleroma.Web.Endpoint.url()}/.well-known/webfinger?resource={uri}" type="application/xrd+xml" /></XRD>]
end
test "GET host-meta.json" do
response =
build_conn()
|> get("/.well-known/host-meta.json")
# RFC6415 (Web Host Metadata) says it MUST be application/json
assert get_resp_header(resp, "content-type") == ["application/json; charset=utf-8"]
assert response == json_response(resp, 200)
assert response["links"] == [
%{
"rel" => "lrdd",
"type" => "application/jrd+json",
"template" => "#{Pleroma.Web.Endpoint.url()}/.well-known/webfinger?resource={uri}"
}
]
end
test "Webfinger JRD" do