webfinger/finger: only accept authority of query domain
And permit refetching once(!) unless initial query
was already designated as the canonical authority.
(Only once to not get stuck in loops)
Fixes an oversight in c80aec05de.
The argument for why subjects from both authorities can be accepted
hinged on the assumption that only paths under direct control of the
domain operator are involved since both webfinger and host-meta
are /.well-known/ paths.
However, HTTP redirects or the LRDD schema inside the initial domains
host-meta may point at _any_ path on another domain, including e.g.
paths containing user uploads, thus enabling third-parties to
illegitimately claim handles from urelated domains, _if_ the victim
domain can be made to serve attacker-prepared JSON (e.g. via user
uploads or (media) proxies).
With trust being limited to initial domain and refetches we do not need
to make guesses about whether and when being redirected to indicates
authorisation of the final domain. It requires more fetch requests in
no-FEP-2c59 setups, but makes it more robust.
As a side effect current FEP-less Mastodon setups should happen to work.
This commit is contained in:
parent
9baf4d16e5
commit
eb361dd456
5 changed files with 388 additions and 77 deletions
|
|
@ -47,9 +47,10 @@ Akkoma strongly encourages ActivityPub implementations to include
|
|||
a FEP-2c59-compliant WebFinger backlink in their actor documents.
|
||||
|
||||
Without FEP-2c59 and if different domains are used for ActivityPub and the Webfinger subject,
|
||||
Akkoma relies on the presence of an host-meta LRDD template on the ActivityPub domain
|
||||
or a HTTP redirect from the ActivityPub domain’s `/.well-known/webfinger` to an equivalent endpoint
|
||||
on the domain used in the `subject` to discover and validate the domain association.
|
||||
Akkoma relies on either the presence of an host-meta LRDD template on the ActivityPub domain
|
||||
or a working WebFinger endpoint on the ActivityPub domain. Additionally all WebFinger endpoints
|
||||
related to the ActivityPub and canonical WebFinger domain SHOULD also respond to queries about
|
||||
an alternative acct URI constructed with the WebFinger domain passed as the resource.
|
||||
Without FEP-2c59 Akkoma may not become aware of changes to the
|
||||
preferred WebFinger `subject` domain for already discovered users.
|
||||
|
||||
|
|
|
|||
|
|
@ -154,11 +154,9 @@ defmodule Pleroma.Web.WebFinger.Finger do
|
|||
query_uri = make_finger_uri(domain, resource)
|
||||
resp = HTTP.Backoff.get(query_uri, [{"accept", "application/xrd+xml,application/jrd+json"}])
|
||||
|
||||
with {:ok, %{url: resolved_uri, status: status} = resp_data} when status in 200..299 <- resp,
|
||||
with {:ok, %{status: status} = resp_data} when status in 200..299 <- resp,
|
||||
{_, {:ok, parsed_data}} <- {:parse, parse_finger_response(resp_data)} do
|
||||
resolved_domain = URI.parse(resolved_uri).host
|
||||
|
||||
{:ok, resolved_domain, parsed_data}
|
||||
{:ok, parsed_data}
|
||||
else
|
||||
{:ok, %Tesla.Env{} = env} -> {:error, map_fetch_error_reason(env)}
|
||||
{:parse, {:error, _} = error} -> error
|
||||
|
|
@ -180,6 +178,47 @@ defmodule Pleroma.Web.WebFinger.Finger do
|
|||
end
|
||||
end
|
||||
|
||||
# Parsed WebFinger response data with the subject acct URI (and thus parsed_subject and normalised_subject)
|
||||
# being verified to be authorised by the domain in which authority it lies
|
||||
# (i.e. make sure an "acct:user@domain.example" is acknowlledged by domain.example)
|
||||
# Does NOT verify the actor pointed at in "ap_id" agrees to the handle!
|
||||
defp finger_data_with_domainauth(domain, resource, allow_refetch \\ true) do
|
||||
with {:ok, %{"subject" => finger_subject} = preparsed_data} <-
|
||||
finger_unverified_data(domain, resource),
|
||||
handle <- normalise_webfinger_handle(finger_subject),
|
||||
{nick_user, nick_domain} <- parse_handle(handle),
|
||||
{_, false} <- {:no_domain, nick_domain == nil},
|
||||
# We cannot reliably accepted redirects as auth for the final domain.
|
||||
# Thus only accepted result if matching the _initial_ domain, else allow a single refetch
|
||||
# (traversing longer redirect chains may risk getting stuck in loops)
|
||||
{_, true, _} <-
|
||||
{:domainauth, nick_domain == domain, {nick_domain, resource_from_mention(handle)}} do
|
||||
parsed_data =
|
||||
preparsed_data
|
||||
|> Map.put("normalised_subject", handle)
|
||||
|> Map.put("parsed_subject", {nick_user, nick_domain})
|
||||
|
||||
{:ok, parsed_data}
|
||||
else
|
||||
{:domainauth, _, {nick_domain, new_resource}} ->
|
||||
if allow_refetch do
|
||||
finger_data_with_domainauth(nick_domain, new_resource)
|
||||
else
|
||||
Logger.error(
|
||||
"Spoofed WebFinger response: #{inspect(domain)} responded with subject from #{inspect(nick_domain)} when no alias was expected!"
|
||||
)
|
||||
|
||||
{:error, :finger_domain_spoof}
|
||||
end
|
||||
|
||||
{:no_domain, _} ->
|
||||
{:error, :no_domain}
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Discovers and verifies the WebFinger handle of an ActivityPub actor for use as a nickname.
|
||||
If the actor or instance does not use WebFinger or just temporarily unavailable no value
|
||||
|
|
@ -199,15 +238,10 @@ defmodule Pleroma.Web.WebFinger.Finger do
|
|||
|
||||
with {_, false} <- {:no_domain, domain == nil || ap_domain == nil},
|
||||
{_, false} <- {:matching_domain, domain == ap_domain},
|
||||
# We check for an exact match to the preferred handle which will ALWAYS
|
||||
# belong to the initial query domain, thus we do not need to consider the final domain here.
|
||||
# If the query domain delegates to another domain via host-meta or HTTP redirects on
|
||||
# ./well-known/ paths (which ought to be directly controlled by the operator),
|
||||
# this clearly indicates consent of the query domain to allow the final domain to manage this data
|
||||
{_, {:ok, _, %{"ap_id" => fingered_ap_id, "subject" => finger_subject}}} <-
|
||||
{:query, finger_unverified_data(domain, ap_id)},
|
||||
# Since we already query the preferred domain, no refetches ought to be necessary
|
||||
{_, {:ok, %{"ap_id" => fingered_ap_id, "normalised_subject" => finger_handle}}} <-
|
||||
{:query, finger_data_with_domainauth(domain, ap_id, false)},
|
||||
{_, false} <- {:fingered_data_mismatch, ap_id != fingered_ap_id},
|
||||
finger_handle <- normalise_webfinger_handle(finger_subject),
|
||||
{_, false} <- {:fingered_data_mismatch, preferred_handle != finger_handle} do
|
||||
{:ok, preferred_handle}
|
||||
else
|
||||
|
|
@ -226,18 +260,15 @@ defmodule Pleroma.Web.WebFinger.Finger do
|
|||
ap_domain = URI.parse(ap_id).host
|
||||
|
||||
with {_, false} <- {:no_domain, ap_domain == nil},
|
||||
{_, {:ok, finger_domain, %{"ap_id" => fingered_ap_id, "subject" => finger_subject}}} <-
|
||||
{:query, finger_unverified_data(ap_domain, ap_id)},
|
||||
{_,
|
||||
{:ok,
|
||||
%{
|
||||
"ap_id" => fingered_ap_id,
|
||||
"normalised_subject" => handle,
|
||||
"parsed_subject" => {nick_user, _}
|
||||
}}} <-
|
||||
{:query, finger_data_with_domainauth(ap_domain, ap_id)},
|
||||
{_, false} <- {:fingered_data_mismatch, fingered_ap_id != ap_id},
|
||||
handle <- normalise_webfinger_handle(finger_subject),
|
||||
{nick_user, nick_domain} <- parse_handle(handle),
|
||||
# Mastodon in its infinite wisdom encourages setups for custom WebFinger domains,
|
||||
# such that the actual WebFinger response is _never_ served directly from the domain used in handles.
|
||||
# Unlike in domain authority checks for AP IDs, here only fixed /.well-known URLs are queried,
|
||||
# thus a redirect on this endpoint can be considered an approval from the redirecting domain
|
||||
# (but not the redirected-to domain!) and it should be safe to accept both domain authorities here.
|
||||
{_, false} <-
|
||||
{:finger_domain_spoof, nick_domain != finger_domain && nick_domain != ap_domain},
|
||||
ap_name <- actor_data["preferredUsername"],
|
||||
{_, false} <- {:fingered_data_mismatch, ap_name != nil && ap_name != nick_user} do
|
||||
{:ok, handle}
|
||||
|
|
@ -305,16 +336,17 @@ defmodule Pleroma.Web.WebFinger.Finger do
|
|||
resource = resource_from_mention(mention_handle)
|
||||
|
||||
with {_, false} <- {:invalid_handle, qname == nil || qdomain == nil},
|
||||
{_, {:ok, finger_domain, %{"ap_id" => fingered_ap_id, "subject" => finger_subject}}} <-
|
||||
{:query, finger_unverified_data(qdomain, resource)},
|
||||
handle <- normalise_webfinger_handle(finger_subject),
|
||||
{nick_user, nick_domain} <- parse_handle(handle),
|
||||
# see comment in finger_actor for why both domains can and need to be accepted
|
||||
{_, false} <-
|
||||
{:finger_domain_spoof, nick_domain != finger_domain && nick_domain != qdomain},
|
||||
{_,
|
||||
{:ok,
|
||||
%{
|
||||
"ap_id" => fingered_ap_id,
|
||||
"normalised_subject" => handle,
|
||||
"parsed_subject" => {nick_user, nick_domain}
|
||||
}}} <-
|
||||
{:query, finger_data_with_domainauth(qdomain, resource)},
|
||||
{_, {:ok, data}} <-
|
||||
{:fetch, Fetcher.fetch_and_contain_remote_object_from_id(fingered_ap_id)} do
|
||||
verify_ap_data_from_finger(data, handle, finger_domain, nick_user)
|
||||
verify_ap_data_from_finger(data, handle, nick_domain, nick_user)
|
||||
else
|
||||
{:query, error} -> error
|
||||
{:fetch, error} -> error
|
||||
|
|
@ -339,7 +371,7 @@ defmodule Pleroma.Web.WebFinger.Finger do
|
|||
end
|
||||
|
||||
with {_, domain} when is_binary(domain) <- {:domain, domain},
|
||||
{:ok, _, data} <- finger_unverified_data(domain, resource) do
|
||||
{:ok, data} <- finger_unverified_data(domain, resource) do
|
||||
{:ok, data}
|
||||
else
|
||||
{:domain, _} -> {:error, :no_domain}
|
||||
|
|
|
|||
|
|
@ -56,11 +56,17 @@ defmodule Pleroma.Web.WebFinger.FingerTest do
|
|||
end
|
||||
|
||||
describe "finger_mention/1" do
|
||||
test "accepts content in authority of final domain" do
|
||||
test "follows subjects to canonical domain to verify" do
|
||||
# Not FEP-2c59, but otherwise one possible sane setup
|
||||
Tesla.Mock.mock(fn
|
||||
%{url: "https://fedi.example.com/.well-known/webfinger?resource=" <> rsrc}
|
||||
when rsrc in ["acct:user@fedi.example.com", "https://fedi.example.com/users/user"] ->
|
||||
%{url: url}
|
||||
when url in [
|
||||
"https://fedi.example.com/.well-known/webfinger?resource=" <>
|
||||
"acct:user@fedi.example.com",
|
||||
"https://fedi.example.com/.well-known/webfinger?resource=" <>
|
||||
"https://fedi.example.com/users/user",
|
||||
"https://example.com/.well-known/webfinger?resource=" <> "acct:user@example.com"
|
||||
] ->
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
|
|
@ -97,10 +103,110 @@ defmodule Pleroma.Web.WebFinger.FingerTest do
|
|||
{:ok, "user@example.com", _} = Finger.finger_mention("user@fedi.example.com")
|
||||
end
|
||||
|
||||
test "rejects spoof attempt via redirect to untrusted cross-domain path when handle does not exist on authorative domain" do
|
||||
Tesla.Mock.mock(fn
|
||||
%{
|
||||
url:
|
||||
"https://stinkyplace.example/.well-known/webfinger?resource=acct:doppelgaenger@stinkyplace.example"
|
||||
} ->
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body:
|
||||
File.read!("test/fixtures/webfinger/pleroma-webfinger.json")
|
||||
|> String.replace("{{domain}}", "shinyplace.example")
|
||||
|> String.replace("{{nickname}}", "doppelgaenger")
|
||||
|> String.replace("{{subdomain}}", "stinkyplace.example"),
|
||||
headers: [{"content-type", "application/jrd+json"}],
|
||||
url: "https://shinyplace.example/user-uploads/123/webfinger.json"
|
||||
}}
|
||||
|
||||
%{url: url}
|
||||
when url in [
|
||||
"https://stinkyplace.example/.well-known/host-meta",
|
||||
"https://shinyplace.example/.well-known/host-meta",
|
||||
"https://shinyplace.example/.well-known/webfinger?resource=acct:doppelgaenger@shinyplace.example"
|
||||
] ->
|
||||
{:ok, %Tesla.Env{status: 404, url: url}}
|
||||
end)
|
||||
|
||||
assert {:error, :not_found} =
|
||||
Finger.finger_mention("@doppelgaenger@stinkyplace.example")
|
||||
end
|
||||
|
||||
test "only uses data from authorative domain when refetching" do
|
||||
Tesla.Mock.mock(fn
|
||||
%{
|
||||
url:
|
||||
"https://stinkyplace.example/.well-known/webfinger?resource=acct:doppelgaenger@stinkyplace.example"
|
||||
} ->
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body:
|
||||
File.read!("test/fixtures/webfinger/pleroma-webfinger.json")
|
||||
|> String.replace("{{domain}}", "shinyplace.example")
|
||||
|> String.replace("{{nickname}}", "doppelgaenger")
|
||||
|> String.replace("{{subdomain}}", "stinkyplace.example"),
|
||||
headers: [{"content-type", "application/jrd+json"}],
|
||||
url: "https://shinyplace.example/user-uploads/123/webfinger.json"
|
||||
}}
|
||||
|
||||
%{
|
||||
url:
|
||||
"https://shinyplace.example/.well-known/webfinger?resource=acct:doppelgaenger@shinyplace.example" =
|
||||
url
|
||||
} ->
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body:
|
||||
File.read!("test/fixtures/webfinger/pleroma-webfinger.json")
|
||||
|> String.replace("{{domain}}", "shinyplace.example")
|
||||
|> String.replace("{{nickname}}", "doppelgaenger")
|
||||
|> String.replace("{{subdomain}}", "shinyplace.example"),
|
||||
headers: [{"content-type", "application/jrd+json"}],
|
||||
url: url
|
||||
}}
|
||||
|
||||
%{url: "https://shinyplace.example/users/doppelgaenger" = url} ->
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
url: url,
|
||||
headers: [{"content-type", "application/activity+json"}],
|
||||
body: """
|
||||
{
|
||||
"id": "#{url}",
|
||||
"type": "Service",
|
||||
"inbox": "#{url}/inbox",
|
||||
"outbox": "#{url}/inbox"
|
||||
}
|
||||
"""
|
||||
}}
|
||||
|
||||
%{url: url}
|
||||
when url in [
|
||||
"https://stinkyplace.example/.well-known/host-meta",
|
||||
"https://shinyplace.example/.well-known/host-meta"
|
||||
] ->
|
||||
{:ok, %Tesla.Env{status: 404, url: url}}
|
||||
end)
|
||||
|
||||
{:ok, "doppelgaenger@shinyplace.example",
|
||||
%{"id" => "https://shinyplace.example/users/doppelgaenger"}} =
|
||||
Finger.finger_mention("@doppelgaenger@stinkyplace.example")
|
||||
end
|
||||
|
||||
test "accepts content in authority of query domain" do
|
||||
# Early 2026 Mastodon style
|
||||
Tesla.Mock.mock(fn
|
||||
%{url: "https://example.com/.well-known/webfinger?resource=acct:user@example.com"} ->
|
||||
%{url: url}
|
||||
when url in [
|
||||
"https://example.com/.well-known/webfinger?resource=acct:user@example.com",
|
||||
"https://fedi.example.com/.well-known/webfinger?resource=acct:user@example.com",
|
||||
"https://fedi.example.com/.well-known/webfinger?resource=https://fedi.example.com/users/user"
|
||||
] ->
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
|
|
@ -114,11 +220,15 @@ defmodule Pleroma.Web.WebFinger.FingerTest do
|
|||
|> String.replace("{{subdomain}}", "fedi.example.com")
|
||||
}}
|
||||
|
||||
%{url: "https://fedi.example.com/.well-known/host-meta"} ->
|
||||
%{url: url}
|
||||
when url in [
|
||||
"https://example.com/.well-known/host-meta",
|
||||
"https://fedi.example.com/.well-known/host-meta"
|
||||
] ->
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
status: 404,
|
||||
url: "https://example.com/.well-known/host-meta"
|
||||
url: url
|
||||
}}
|
||||
|
||||
%{url: "https://fedi.example.com/users/user"} ->
|
||||
|
|
@ -137,7 +247,7 @@ defmodule Pleroma.Web.WebFinger.FingerTest do
|
|||
{:ok, "user@example.com", _} = Finger.finger_mention("user@example.com")
|
||||
end
|
||||
|
||||
test "errors when being served content from unrelated third-party domain" do
|
||||
test "errors when being served content from unrelated third-party domain which is not registered" do
|
||||
Tesla.Mock.mock(fn
|
||||
%{url: "https://example.com/.well-known/webfinger?resource=acct:user@example.com"} ->
|
||||
{:ok,
|
||||
|
|
@ -152,15 +262,19 @@ defmodule Pleroma.Web.WebFinger.FingerTest do
|
|||
|> String.replace("{{subdomain}}", "fedi.example.org")
|
||||
}}
|
||||
|
||||
%{url: "https://example.com/.well-known/host-meta"} ->
|
||||
%{url: url}
|
||||
when url in [
|
||||
"https://example.com/.well-known/host-meta",
|
||||
"https://shinyplace.example/.well-known/webfinger?resource=acct:user@shinyplace.example"
|
||||
] ->
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
status: 404,
|
||||
url: "https://example.com/.well-known/host-meta"
|
||||
url: url
|
||||
}}
|
||||
end)
|
||||
|
||||
{:error, :finger_domain_spoof} = Finger.finger_mention("user@example.com")
|
||||
{:error, :not_found} = Finger.finger_mention("user@example.com")
|
||||
end
|
||||
|
||||
test "should use the webfinger property to look up the webfinger data for an actor" do
|
||||
|
|
@ -207,11 +321,13 @@ defmodule Pleroma.Web.WebFinger.FingerTest do
|
|||
{:ok, "user@example.com", _data} = Finger.finger_mention("@user@example.com")
|
||||
end
|
||||
|
||||
test "allows HTTP redirects to serve as webfinger domain delegation" do
|
||||
test "works with HTTP redirects on .well-known webfinger path" do
|
||||
Tesla.Mock.mock(fn
|
||||
%{
|
||||
url: "https://example.com/.well-known/webfinger?resource=acct:user@example.com"
|
||||
} ->
|
||||
%{url: url}
|
||||
when url in [
|
||||
"https://example.com/.well-known/webfinger?resource=acct:user@example.com",
|
||||
"https://somewhere-else.com/.well-known/webfinger?resource=acct:another-user@somewhere-else.com"
|
||||
] ->
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
|
|
@ -252,11 +368,13 @@ defmodule Pleroma.Web.WebFinger.FingerTest do
|
|||
{:ok, "another-user@somewhere-else.com", _data} = Finger.finger_mention("@user@example.com")
|
||||
end
|
||||
|
||||
test "should reject a cross-domain webfinger if the final actor has an incorrect webfinger property" do
|
||||
test "should reject a cross-domain webfinger if the final actor has an incorrect webfinger property even if domain agrees to handle" do
|
||||
Tesla.Mock.mock(fn
|
||||
%{
|
||||
url: "https://example.com/.well-known/webfinger?resource=acct:user@example.com"
|
||||
} ->
|
||||
%{url: url}
|
||||
when url in [
|
||||
"https://example.com/.well-known/webfinger?resource=acct:user@example.com",
|
||||
"https://somewhere-else.com/.well-known/webfinger?resource=acct:another-user@somewhere-else.com"
|
||||
] ->
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
|
|
@ -300,9 +418,11 @@ defmodule Pleroma.Web.WebFinger.FingerTest do
|
|||
test "should refetch the initial actor if no backlink exists on the final actor" do
|
||||
Tesla.Mock.mock(fn
|
||||
# first, the initial webfinger we fetch points to somewhere-else.com
|
||||
%{
|
||||
url: "https://example.com/.well-known/webfinger?resource=acct:user@example.com"
|
||||
} ->
|
||||
%{url: url}
|
||||
when url in [
|
||||
"https://example.com/.well-known/webfinger?resource=acct:user@example.com",
|
||||
"https://somewhere-else.com/.well-known/webfinger?resource=acct:another-user@somewhere-else.com"
|
||||
] ->
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
|
|
@ -545,7 +665,8 @@ defmodule Pleroma.Web.WebFinger.FingerTest do
|
|||
|> String.replace("{{nickname}}", "user")
|
||||
|> String.replace("{{subdomain}}", "social.example.com"),
|
||||
headers: [{"content-type", "application/jrd+json"}],
|
||||
url: "https://oops-this-was-a-redirect/somewhere"
|
||||
url:
|
||||
"https://oops-this-was-a-redirect-to-a-trusted-path/.well-known/my-static-webfinger"
|
||||
}}
|
||||
|
||||
%{url: "https://example.com/.well-known/host-meta"} ->
|
||||
|
|
@ -566,6 +687,43 @@ defmodule Pleroma.Web.WebFinger.FingerTest do
|
|||
})
|
||||
end
|
||||
|
||||
test "refuses domain mismatches early with webfinger backlink" do
|
||||
Tesla.Mock.mock(fn
|
||||
# we should finger the webfinger property
|
||||
%{
|
||||
url:
|
||||
"https://example.com/.well-known/webfinger?resource=https://social.example.com/users/user"
|
||||
} ->
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body:
|
||||
File.read!("test/fixtures/webfinger/pleroma-webfinger.json")
|
||||
|> String.replace("{{domain}}", "broken.example.com")
|
||||
|> String.replace("{{nickname}}", "user")
|
||||
|> String.replace("{{subdomain}}", "social.example.com"),
|
||||
headers: [{"content-type", "application/jrd+json"}],
|
||||
url: "https://broken.example.com/.well-known/webfinger"
|
||||
}}
|
||||
|
||||
%{url: "https://example.com/.well-known/host-meta"} ->
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
url: "https://example.com/.well-known/host-meta",
|
||||
body:
|
||||
File.read!("test/fixtures/webfinger/masto-host-meta.xml")
|
||||
|> String.replace("{{domain}}", "example.com")
|
||||
}}
|
||||
end)
|
||||
|
||||
assert {:error, :finger_domain_spoof} =
|
||||
Finger.finger_actor(%{
|
||||
"id" => "https://social.example.com/users/user",
|
||||
"webfinger" => "user@example.com"
|
||||
})
|
||||
end
|
||||
|
||||
test "can discover nick from WebFinger query alone if actor contains no hints" do
|
||||
Tesla.Mock.mock(fn
|
||||
# we should finger the ID directly
|
||||
|
|
@ -644,6 +802,101 @@ defmodule Pleroma.Web.WebFinger.FingerTest do
|
|||
"id" => "https://example.com/users/user"
|
||||
})
|
||||
end
|
||||
|
||||
require Logger
|
||||
|
||||
test "enusures final WebFinger response actually links back to inital actor (with FEP-2c59)" do
|
||||
Tesla.Mock.mock(fn
|
||||
%{
|
||||
url:
|
||||
"https://shinyplace.example/.well-known/webfinger?resource=https://example.com/users/user" =
|
||||
url
|
||||
} ->
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body:
|
||||
File.read!("test/fixtures/webfinger/masto-webfinger.json")
|
||||
|> String.replace("{{domain}}", "shinyplace.example")
|
||||
|> String.replace("{{nickname}}", "user")
|
||||
|> String.replace("{{subdomain}}", "example.com")
|
||||
|> String.replace("{{apid}}", "https://example.com/users/not-this-user"),
|
||||
headers: [{"content-type", "application/jrd+json"}],
|
||||
url: url
|
||||
}}
|
||||
|
||||
%{url: "https://shinyplace.example/.well-known/host-meta" = url} ->
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
url: url,
|
||||
body:
|
||||
File.read!("test/fixtures/webfinger/masto-host-meta.xml")
|
||||
|> String.replace("{{domain}}", "shinyplace.example")
|
||||
}}
|
||||
end)
|
||||
|
||||
assert {:error, :fingered_data_mismatch} =
|
||||
Finger.finger_actor(%{
|
||||
"id" => "https://example.com/users/user",
|
||||
"webfinger" => "user@shinyplace.example"
|
||||
})
|
||||
end
|
||||
|
||||
test "enusures final WebFinger response actually links back to inital acotr (without FEP-2c59)" do
|
||||
Tesla.Mock.mock(fn
|
||||
%{
|
||||
url:
|
||||
"https://example.com/.well-known/webfinger?resource=https://example.com/users/user" =
|
||||
url
|
||||
} ->
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body:
|
||||
File.read!("test/fixtures/webfinger/masto-webfinger.json")
|
||||
# Indicates different WebFinger domain is preferred
|
||||
|> String.replace("{{domain}}", "shinyplace.example")
|
||||
|> String.replace("{{nickname}}", "user")
|
||||
|> String.replace("{{subdomain}}", "example.com")
|
||||
# This still matches the initially fingered actor
|
||||
|> String.replace("{{apid}}", "https://example.com/users/user"),
|
||||
headers: [{"content-type", "application/jrd+json"}],
|
||||
url: url
|
||||
}}
|
||||
|
||||
%{
|
||||
url:
|
||||
"https://shinyplace.example/.well-known/webfinger?resource=acct:user@shinyplace.example" =
|
||||
url
|
||||
} ->
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body:
|
||||
File.read!("test/fixtures/webfinger/masto-webfinger.json")
|
||||
|> String.replace("{{domain}}", "shinyplace.example")
|
||||
|> String.replace("{{nickname}}", "user")
|
||||
|> String.replace("{{subdomain}}", "shinyplace.example")
|
||||
# different AP id than initially fingered actor we’re trying to enrich here!
|
||||
|> String.replace("{{apid}}", "https://shinyplace.com/users/user"),
|
||||
headers: [{"content-type", "application/jrd+json"}],
|
||||
url: url
|
||||
}}
|
||||
|
||||
%{url: url}
|
||||
when url in [
|
||||
"https://example.com/.well-known/host-meta",
|
||||
"https://shinyplace.example/.well-known/host-meta"
|
||||
] ->
|
||||
{:ok, %Tesla.Env{status: 404, url: url}}
|
||||
end)
|
||||
|
||||
assert {:error, :fingered_data_mismatch} =
|
||||
Finger.finger_actor(%{
|
||||
"id" => "https://example.com/users/user"
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
describe "finger_raw_data/1" do
|
||||
|
|
@ -803,10 +1056,27 @@ defmodule Pleroma.Web.WebFinger.FingerTest do
|
|||
|
||||
Tesla.Mock.json(fake_webfinger, url: url)
|
||||
|
||||
%{url: "https://bad.com/.well-known/host-meta"} ->
|
||||
{:ok, %Tesla.Env{status: 404}}
|
||||
# the AP id from fake WebFInger response
|
||||
%{url: "https://bad.com/webfingertest" = url} ->
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
url: url,
|
||||
headers: [{"content-type", "application/activity+json"}],
|
||||
body: """
|
||||
{
|
||||
"id": "#{url}",
|
||||
"type": "Service",
|
||||
"inbox": "#{url}/inbox",
|
||||
"outbox": "#{url}/inbox"
|
||||
}
|
||||
"""
|
||||
}}
|
||||
|
||||
%{url: url} ->
|
||||
{:ok, %Tesla.Env{status: 404, url: url}}
|
||||
end)
|
||||
|
||||
assert {:error, :finger_domain_spoof} = Finger.finger_mention("meanie@bad.com")
|
||||
assert {:error, :not_found} = Finger.finger_mention("meanie@bad.com")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1715,7 +1715,11 @@ defmodule HttpRequestMock do
|
|||
end
|
||||
|
||||
Macros.mock_masto_webfinger(
|
||||
"https://sub.mastodon.example/.well-known/webfinger?resource=acct:a@mastodon.example",
|
||||
[
|
||||
"https://sub.mastodon.example/.well-known/webfinger?resource=https://sub.mastodon.example/users/a",
|
||||
"https://sub.mastodon.example/.well-known/webfinger?resource=acct:a@mastodon.example",
|
||||
"https://mastodon.example/.well-known/webfinger?resource=acct:a@mastodon.example"
|
||||
],
|
||||
"a",
|
||||
"mastodon.example",
|
||||
"sub.mastodon.example"
|
||||
|
|
@ -1774,12 +1778,12 @@ defmodule HttpRequestMock do
|
|||
}}
|
||||
end
|
||||
|
||||
def get(
|
||||
"https://sub.pleroma.example/.well-known/webfinger?resource=acct:a@pleroma.example" = url,
|
||||
_,
|
||||
_,
|
||||
_
|
||||
) do
|
||||
def get(url, _, _, _)
|
||||
when url in [
|
||||
"https://sub.pleroma.example/.well-known/webfinger?resource=https://sub.pleroma.example/users/a",
|
||||
"https://sub.pleroma.example/.well-known/webfinger?resource=acct:a@pleroma.example",
|
||||
"https://pleroma.example/.well-known/webfinger?resource=acct:a@pleroma.example"
|
||||
] do
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
|
|
|
|||
|
|
@ -3,16 +3,20 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule HttpRequestMockMacros do
|
||||
defmacro mock_masto_webfinger(url, nick, webfinger_domain, ap_domain \\ nil, ap_id \\ nil) do
|
||||
quote do
|
||||
def get(unquote(url) = url, _, _, _) do
|
||||
webfinger_response_masto(
|
||||
url,
|
||||
unquote(nick),
|
||||
unquote(webfinger_domain),
|
||||
unquote(ap_domain),
|
||||
unquote(ap_id)
|
||||
)
|
||||
defmacro mock_masto_webfinger(urls, nick, webfinger_domain, ap_domain \\ nil, ap_id \\ nil) do
|
||||
urls = if is_binary(urls), do: [urls], else: urls
|
||||
|
||||
for url <- urls do
|
||||
quote do
|
||||
def get(unquote(url) = url, _, _, _) do
|
||||
webfinger_response_masto(
|
||||
url,
|
||||
unquote(nick),
|
||||
unquote(webfinger_domain),
|
||||
unquote(ap_domain),
|
||||
unquote(ap_id)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue