forked from AkkomaGang/akkoma
Normalise public adressing to fix federation
Due to JSON-LD compaction the full address of public scope may also occur in shorter forms and the spec requires us to treat them all equivalently. To save us the pain of repeatedly checking for all variants internally, normalise inbound data to just one form. See note at: https://www.w3.org/TR/activitypub/#public-addressing This needs to happen very early, even before the other addressing fixes else an earlier validator will reject the object. This in turn required to move the list-tpye normalisation earlier as well, but since I was unsure about putting empty lists into the data when no such field existed before, I excluded this case and thus the later fixing had to be kept as well. Fixes: AkkomaGang/akkoma#670
This commit is contained in:
parent
b1c6621e66
commit
b0a46c1e2e
3 changed files with 162 additions and 80 deletions
|
@ -18,6 +18,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
## Fixed
|
||||
- Issue preventing fetching anything from IPv6-only instances
|
||||
- Issue allowing post content to leak via opengraph tags despite :estrict\_unauthenticated being set
|
||||
- Scope misdetection of remote posts resulting from not recognising
|
||||
JSON-LD-compacted forms of public scope; affected e.g. federation with bovine
|
||||
|
||||
## 2024.03
|
||||
|
||||
|
|
|
@ -58,21 +58,48 @@ def fix_summary(%{"summary" => _} = object) do
|
|||
|
||||
def fix_summary(object), do: Map.put(object, "summary", "")
|
||||
|
||||
def fix_addressing_list(map, field) do
|
||||
addrs = map[field]
|
||||
|
||||
defp fix_addressing_list(addrs) do
|
||||
cond do
|
||||
is_list(addrs) ->
|
||||
Map.put(map, field, Enum.filter(addrs, &is_binary/1))
|
||||
|
||||
is_binary(addrs) ->
|
||||
Map.put(map, field, [addrs])
|
||||
|
||||
true ->
|
||||
Map.put(map, field, [])
|
||||
is_list(addrs) -> Enum.filter(addrs, &is_binary/1)
|
||||
is_binary(addrs) -> [addrs]
|
||||
true -> []
|
||||
end
|
||||
end
|
||||
|
||||
# Due to JSON-LD simply "Public" and "as:Public" are equivalent to the full URI
|
||||
# but to simplify later checks we only want to deal with one reperesentation internally
|
||||
defp normalise_addressing_public_list(map, all_fields)
|
||||
|
||||
defp normalise_addressing_public_list(%{} = map, [field | fields]) do
|
||||
full_uri = Pleroma.Constants.as_public()
|
||||
|
||||
map =
|
||||
if map[field] != nil do
|
||||
new_fval =
|
||||
map[field]
|
||||
|> fix_addressing_list()
|
||||
|> Enum.map(fn
|
||||
"Public" -> full_uri
|
||||
"as:Public" -> full_uri
|
||||
x -> x
|
||||
end)
|
||||
|
||||
Map.put(map, field, new_fval)
|
||||
else
|
||||
map
|
||||
end
|
||||
|
||||
normalise_addressing_public_list(map, fields)
|
||||
end
|
||||
|
||||
defp normalise_addressing_public_list(map, _) do
|
||||
map
|
||||
end
|
||||
|
||||
defp normalise_addressing_public(map) do
|
||||
normalise_addressing_public_list(map, ["to", "cc", "bto", "bcc"])
|
||||
end
|
||||
|
||||
# if directMessage flag is set to true, leave the addressing alone
|
||||
def fix_explicit_addressing(%{"directMessage" => true} = object, _follower_collection),
|
||||
do: object
|
||||
|
@ -96,6 +123,10 @@ def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, follower_collect
|
|||
|> Map.put("cc", final_cc)
|
||||
end
|
||||
|
||||
def fix_addressing_list_key(map, field) do
|
||||
Map.put(map, field, fix_addressing_list(map[field]))
|
||||
end
|
||||
|
||||
def fix_addressing(object) do
|
||||
{:ok, %User{follower_address: follower_collection}} =
|
||||
object
|
||||
|
@ -103,10 +134,10 @@ def fix_addressing(object) do
|
|||
|> User.get_or_fetch_by_ap_id()
|
||||
|
||||
object
|
||||
|> fix_addressing_list("to")
|
||||
|> fix_addressing_list("cc")
|
||||
|> fix_addressing_list("bto")
|
||||
|> fix_addressing_list("bcc")
|
||||
|> fix_addressing_list_key("to")
|
||||
|> fix_addressing_list_key("cc")
|
||||
|> fix_addressing_list_key("bto")
|
||||
|> fix_addressing_list_key("bcc")
|
||||
|> fix_explicit_addressing(follower_collection)
|
||||
|> CommonFixes.fix_implicit_addressing(follower_collection)
|
||||
end
|
||||
|
@ -383,11 +414,28 @@ defp get_reported(objects) do
|
|||
end)
|
||||
end
|
||||
|
||||
def handle_incoming(data, options \\ [])
|
||||
def handle_incoming(data, options \\ []) do
|
||||
data = normalise_addressing_public(data)
|
||||
|
||||
data =
|
||||
if data["object"] != nil do
|
||||
object = normalise_addressing_public(data["object"])
|
||||
Map.put(data, "object", object)
|
||||
else
|
||||
data
|
||||
end
|
||||
|
||||
handle_incoming_normalised(data, options)
|
||||
end
|
||||
|
||||
defp handle_incoming_normalised(data, options)
|
||||
|
||||
# Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
|
||||
# with nil ID.
|
||||
def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
|
||||
defp handle_incoming_normalised(
|
||||
%{"type" => "Flag", "object" => objects, "actor" => actor} = data,
|
||||
_options
|
||||
) do
|
||||
with context <- data["context"] || Utils.generate_context_id(),
|
||||
content <- data["content"] || "",
|
||||
%User{} = actor <- User.get_cached_by_ap_id(actor),
|
||||
|
@ -408,20 +456,21 @@ def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} =
|
|||
end
|
||||
|
||||
# disallow objects with bogus IDs
|
||||
def handle_incoming(%{"id" => nil}, _options), do: :error
|
||||
def handle_incoming(%{"id" => ""}, _options), do: :error
|
||||
defp handle_incoming_normalised(%{"id" => nil}, _options), do: :error
|
||||
defp handle_incoming_normalised(%{"id" => ""}, _options), do: :error
|
||||
# length of https:// = 8, should validate better, but good enough for now.
|
||||
def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id) < 8,
|
||||
do: :error
|
||||
defp handle_incoming_normalised(%{"id" => id}, _options)
|
||||
when is_binary(id) and byte_size(id) < 8,
|
||||
do: :error
|
||||
|
||||
@doc "Rewrite misskey likes into EmojiReacts"
|
||||
def handle_incoming(
|
||||
%{
|
||||
"type" => "Like",
|
||||
"content" => reaction
|
||||
} = data,
|
||||
options
|
||||
) do
|
||||
# Rewrite misskey likes into EmojiReacts
|
||||
defp handle_incoming_normalised(
|
||||
%{
|
||||
"type" => "Like",
|
||||
"content" => reaction
|
||||
} = data,
|
||||
options
|
||||
) do
|
||||
if Pleroma.Emoji.is_unicode_emoji?(reaction) || Pleroma.Emoji.matches_shortcode?(reaction) do
|
||||
data
|
||||
|> Map.put("type", "EmojiReact")
|
||||
|
@ -433,11 +482,11 @@ def handle_incoming(
|
|||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data,
|
||||
options
|
||||
)
|
||||
when objtype in ~w{Question Answer Audio Video Event Article Note Page} do
|
||||
defp handle_incoming_normalised(
|
||||
%{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data,
|
||||
options
|
||||
)
|
||||
when objtype in ~w{Question Answer Audio Video Event Article Note Page} do
|
||||
fetch_options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
|
||||
|
||||
object =
|
||||
|
@ -469,8 +518,8 @@ def handle_incoming(
|
|||
end
|
||||
end
|
||||
|
||||
def handle_incoming(%{"type" => type} = data, _options)
|
||||
when type in ~w{Like EmojiReact Announce Add Remove} do
|
||||
defp handle_incoming_normalised(%{"type" => type} = data, _options)
|
||||
when type in ~w{Like EmojiReact Announce Add Remove} do
|
||||
with :ok <- ObjectValidator.fetch_actor_and_object(data),
|
||||
{:ok, activity, _meta} <- Pipeline.common_pipeline(data, local: false) do
|
||||
{:ok, activity}
|
||||
|
@ -480,11 +529,11 @@ def handle_incoming(%{"type" => type} = data, _options)
|
|||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => type} = data,
|
||||
_options
|
||||
)
|
||||
when type in ~w{Update Block Follow Accept Reject} do
|
||||
defp handle_incoming_normalised(
|
||||
%{"type" => type} = data,
|
||||
_options
|
||||
)
|
||||
when type in ~w{Update Block Follow Accept Reject} do
|
||||
with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
|
||||
{:ok, activity, _} <-
|
||||
Pipeline.common_pipeline(data, local: false) do
|
||||
|
@ -492,10 +541,10 @@ def handle_incoming(
|
|||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Delete"} = data,
|
||||
_options
|
||||
) do
|
||||
defp handle_incoming_normalised(
|
||||
%{"type" => "Delete"} = data,
|
||||
_options
|
||||
) do
|
||||
with {:ok, activity, _} <-
|
||||
Pipeline.common_pipeline(data, local: false) do
|
||||
{:ok, activity}
|
||||
|
@ -515,15 +564,15 @@ def handle_incoming(
|
|||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{
|
||||
"type" => "Undo",
|
||||
"object" => %{"type" => "Follow", "object" => followed},
|
||||
"actor" => follower,
|
||||
"id" => id
|
||||
} = _data,
|
||||
_options
|
||||
) do
|
||||
defp handle_incoming_normalised(
|
||||
%{
|
||||
"type" => "Undo",
|
||||
"object" => %{"type" => "Follow", "object" => followed},
|
||||
"actor" => follower,
|
||||
"id" => id
|
||||
} = _data,
|
||||
_options
|
||||
) do
|
||||
with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
|
||||
{:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
|
||||
{:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
|
||||
|
@ -534,28 +583,28 @@ def handle_incoming(
|
|||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{
|
||||
"type" => "Undo",
|
||||
"object" => %{"type" => type}
|
||||
} = data,
|
||||
_options
|
||||
)
|
||||
when type in ["Like", "EmojiReact", "Announce", "Block"] do
|
||||
defp handle_incoming_normalised(
|
||||
%{
|
||||
"type" => "Undo",
|
||||
"object" => %{"type" => type}
|
||||
} = data,
|
||||
_options
|
||||
)
|
||||
when type in ["Like", "EmojiReact", "Announce", "Block"] do
|
||||
with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
# For Undos that don't have the complete object attached, try to find it in our database.
|
||||
def handle_incoming(
|
||||
%{
|
||||
"type" => "Undo",
|
||||
"object" => object
|
||||
} = activity,
|
||||
options
|
||||
)
|
||||
when is_binary(object) do
|
||||
defp handle_incoming_normalised(
|
||||
%{
|
||||
"type" => "Undo",
|
||||
"object" => object
|
||||
} = activity,
|
||||
options
|
||||
)
|
||||
when is_binary(object) do
|
||||
with %Activity{data: data} <- Activity.get_by_ap_id(object) do
|
||||
activity
|
||||
|> Map.put("object", data)
|
||||
|
@ -565,15 +614,15 @@ def handle_incoming(
|
|||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{
|
||||
"type" => "Move",
|
||||
"actor" => origin_actor,
|
||||
"object" => origin_actor,
|
||||
"target" => target_actor
|
||||
},
|
||||
_options
|
||||
) do
|
||||
defp handle_incoming_normalised(
|
||||
%{
|
||||
"type" => "Move",
|
||||
"actor" => origin_actor,
|
||||
"object" => origin_actor,
|
||||
"target" => target_actor
|
||||
},
|
||||
_options
|
||||
) do
|
||||
with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
|
||||
# Use a dramatically shortened maximum age before refresh here because it is reasonable
|
||||
# for a user to
|
||||
|
@ -588,7 +637,7 @@ def handle_incoming(
|
|||
end
|
||||
end
|
||||
|
||||
def handle_incoming(_, _), do: :error
|
||||
defp handle_incoming_normalised(_, _), do: :error
|
||||
|
||||
@spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
|
||||
def get_obj_helper(id, options \\ []) do
|
||||
|
|
|
@ -137,6 +137,37 @@ test "successfully processes incoming AP docs with correct origin" do
|
|||
assert {:error, :already_present} = ObanHelpers.perform(job)
|
||||
end
|
||||
|
||||
test "successfully normalises public scope descriptors" do
|
||||
params = %{
|
||||
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||
"actor" => "http://mastodon.example.org/users/admin",
|
||||
"type" => "Create",
|
||||
"id" => "http://mastodon.example.org/users/admin/activities/1",
|
||||
"object" => %{
|
||||
"type" => "Note",
|
||||
"content" => "hi world!",
|
||||
"id" => "http://mastodon.example.org/users/admin/objects/1",
|
||||
"attributedTo" => "http://mastodon.example.org/users/admin",
|
||||
"to" => ["Public"]
|
||||
},
|
||||
"to" => ["as:Public"]
|
||||
}
|
||||
|
||||
assert {:ok, job} = Federator.incoming_ap_doc(params)
|
||||
assert {:ok, activity} = ObanHelpers.perform(job)
|
||||
assert activity.data["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
|
||||
|
||||
object =
|
||||
from(
|
||||
object in Pleroma.Object,
|
||||
where: fragment("(?)->>'id' = ?", object.data, ^activity.data["object"]),
|
||||
limit: 1
|
||||
)
|
||||
|> Repo.one()
|
||||
|
||||
assert object.data["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
|
||||
end
|
||||
|
||||
test "rejects incoming AP docs with incorrect origin" do
|
||||
params = %{
|
||||
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||
|
|
Loading…
Reference in a new issue