Normalise public adressing to fix federation
Some checks failed
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/test Pipeline failed
ci/woodpecker/pr/build-arm64 unknown status
ci/woodpecker/pr/build-amd64 unknown status
ci/woodpecker/pr/docs unknown status

Due to JSON LD compacting 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 internally, normalise all 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 filed
existed before, I excluded this case and thus the later fixing had to be
kept as well.

Fixes: #670
This commit is contained in:
Oneric 2024-01-23 20:10:01 +01:00
parent 086d6100e1
commit 505e498b1a
3 changed files with 162 additions and 80 deletions

View file

@ -24,6 +24,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Issue where a bad inbox URL could break federation - Issue where a bad inbox URL could break federation
- Issue where hashtag rel values would be scrubbed - Issue where hashtag rel values would be scrubbed
- Issue where short domains listed in `transparency_obfuscate_domains` were not actually obfuscated - Issue where short domains listed in `transparency_obfuscate_domains` were not actually obfuscated
- Scope misdetection of remote posts resulting from not recognising
JSON-LD-compacted forms of public scope; affected e.g. federation with bovine
## 2023.08 ## 2023.08

View file

@ -58,21 +58,48 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def fix_summary(object), do: Map.put(object, "summary", "") def fix_summary(object), do: Map.put(object, "summary", "")
def fix_addressing_list(map, field) do defp fix_addressing_list(addrs) do
addrs = map[field]
cond do cond do
is_list(addrs) -> is_list(addrs) -> Enum.filter(addrs, &is_binary/1)
Map.put(map, field, Enum.filter(addrs, &is_binary/1)) is_binary(addrs) -> [addrs]
true -> []
is_binary(addrs) ->
Map.put(map, field, [addrs])
true ->
Map.put(map, field, [])
end end
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 = "https://www.w3.org/ns/activitystreams#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 # if directMessage flag is set to true, leave the addressing alone
def fix_explicit_addressing(%{"directMessage" => true} = object, _follower_collection), def fix_explicit_addressing(%{"directMessage" => true} = object, _follower_collection),
do: object do: object
@ -96,6 +123,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Map.put("cc", final_cc) |> Map.put("cc", final_cc)
end end
def fix_addressing_list_key(map, field) do
Map.put(map, field, fix_addressing_list(map[field]))
end
def fix_addressing(object) do def fix_addressing(object) do
{:ok, %User{follower_address: follower_collection}} = {:ok, %User{follower_address: follower_collection}} =
object object
@ -103,10 +134,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> User.get_or_fetch_by_ap_id() |> User.get_or_fetch_by_ap_id()
object object
|> fix_addressing_list("to") |> fix_addressing_list_key("to")
|> fix_addressing_list("cc") |> fix_addressing_list_key("cc")
|> fix_addressing_list("bto") |> fix_addressing_list_key("bto")
|> fix_addressing_list("bcc") |> fix_addressing_list_key("bcc")
|> fix_explicit_addressing(follower_collection) |> fix_explicit_addressing(follower_collection)
|> CommonFixes.fix_implicit_addressing(follower_collection) |> CommonFixes.fix_implicit_addressing(follower_collection)
end end
@ -384,11 +415,28 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end) end)
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 # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
# with nil ID. # 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(), with context <- data["context"] || Utils.generate_context_id(),
content <- data["content"] || "", content <- data["content"] || "",
%User{} = actor <- User.get_cached_by_ap_id(actor), %User{} = actor <- User.get_cached_by_ap_id(actor),
@ -409,14 +457,15 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end end
# disallow objects with bogus IDs # disallow objects with bogus IDs
def handle_incoming(%{"id" => nil}, _options), do: :error defp handle_incoming_normalised(%{"id" => nil}, _options), do: :error
def handle_incoming(%{"id" => ""}, _options), do: :error defp handle_incoming_normalised(%{"id" => ""}, _options), do: :error
# length of https:// = 8, should validate better, but good enough for now. # 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, defp handle_incoming_normalised(%{"id" => id}, _options)
when is_binary(id) and byte_size(id) < 8,
do: :error do: :error
@doc "Rewrite misskey likes into EmojiReacts" # Rewrite misskey likes into EmojiReacts
def handle_incoming( defp handle_incoming_normalised(
%{ %{
"type" => "Like", "type" => "Like",
"content" => reaction "content" => reaction
@ -434,7 +483,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end end
end end
def handle_incoming( defp handle_incoming_normalised(
%{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data, %{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data,
options options
) )
@ -470,7 +519,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end end
end end
def handle_incoming(%{"type" => type} = data, _options) defp handle_incoming_normalised(%{"type" => type} = data, _options)
when type in ~w{Like EmojiReact Announce Add Remove} do when type in ~w{Like EmojiReact Announce Add Remove} do
with :ok <- ObjectValidator.fetch_actor_and_object(data), with :ok <- ObjectValidator.fetch_actor_and_object(data),
{:ok, activity, _meta} <- Pipeline.common_pipeline(data, local: false) do {:ok, activity, _meta} <- Pipeline.common_pipeline(data, local: false) do
@ -481,7 +530,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end end
end end
def handle_incoming( defp handle_incoming_normalised(
%{"type" => type} = data, %{"type" => type} = data,
_options _options
) )
@ -493,7 +542,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end end
end end
def handle_incoming( defp handle_incoming_normalised(
%{"type" => "Delete"} = data, %{"type" => "Delete"} = data,
_options _options
) do ) do
@ -516,7 +565,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end end
end end
def handle_incoming( defp handle_incoming_normalised(
%{ %{
"type" => "Undo", "type" => "Undo",
"object" => %{"type" => "Follow", "object" => followed}, "object" => %{"type" => "Follow", "object" => followed},
@ -535,7 +584,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end end
end end
def handle_incoming( defp handle_incoming_normalised(
%{ %{
"type" => "Undo", "type" => "Undo",
"object" => %{"type" => type} "object" => %{"type" => type}
@ -549,7 +598,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end end
# For Undos that don't have the complete object attached, try to find it in our database. # For Undos that don't have the complete object attached, try to find it in our database.
def handle_incoming( defp handle_incoming_normalised(
%{ %{
"type" => "Undo", "type" => "Undo",
"object" => object "object" => object
@ -566,7 +615,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end end
end end
def handle_incoming( defp handle_incoming_normalised(
%{ %{
"type" => "Move", "type" => "Move",
"actor" => origin_actor, "actor" => origin_actor,
@ -584,7 +633,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end end
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 @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
def get_obj_helper(id, options \\ []) do def get_obj_helper(id, options \\ []) do

View file

@ -137,6 +137,37 @@ defmodule Pleroma.Web.FederatorTest do
assert {:error, :already_present} = ObanHelpers.perform(job) assert {:error, :already_present} = ObanHelpers.perform(job)
end 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 test "rejects incoming AP docs with incorrect origin" do
params = %{ params = %{
"@context" => "https://www.w3.org/ns/activitystreams", "@context" => "https://www.w3.org/ns/activitystreams",