Merge branch 'develop' into gitlab-mr-iid-4426

This commit is contained in:
lain 2026-05-14 06:56:57 +00:00
commit c7c453ca21
34 changed files with 1574 additions and 115 deletions

View file

@ -0,0 +1,16 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.DependencyVersionTest do
use ExUnit.Case, async: true
test "uses majic 1.2" do
majic_version =
:majic
|> Application.spec(:vsn)
|> to_string()
assert Version.match?(majic_version, "~> 1.2")
end
end

View file

@ -11,6 +11,7 @@ defmodule Pleroma.SignatureTest do
import Mock
alias Pleroma.Signature
alias Pleroma.StubbedHTTPSignaturesMock, as: HTTPSignaturesMock
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
@ -103,6 +104,18 @@ defmodule Pleroma.SignatureTest do
end
end
describe "validate_signature/1" do
test "treats HTTP signature errors as failed validation" do
conn = %Plug.Conn{method: "GET", request_path: "/inbox", req_headers: []}
Mox.expect(HTTPSignaturesMock, :validate_conn, fn _conn ->
{:error, :request_target_header}
end)
assert Signature.validate_signature(conn) == false
end
end
describe "key_id_to_actor_id/1" do
test "it properly deduces the actor id for misskey" do
assert Signature.key_id_to_actor_id("https://example.com/users/1234/publickey") ==

View file

@ -0,0 +1,195 @@
# Pleroma: A lightweight social networking server
# Copyright © Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User.SearchTest do
use Pleroma.DataCase, async: false
import Pleroma.Factory
alias Pleroma.Instances
alias Pleroma.Repo
alias Pleroma.User
describe "search/2 mention suggestions" do
test "prioritizes followed/follower users before others" do
user = insert(:user)
related =
insert(:user,
local: false,
nickname: "hj@real.example",
ap_id: "https://real.example/users/hj",
last_status_at: ~N[2020-01-01 00:00:00]
)
other = insert(:user, nickname: "hj", last_status_at: ~N[2020-01-02 00:00:00])
{:ok, _related, _user} = User.follow(related, user)
results = User.search("hj", for_user: user) |> Enum.map(& &1.id)
assert results == [related.id, other.id]
end
test "orders followed/follower users by most recent activity" do
user = insert(:user)
older =
insert(:user,
local: false,
nickname: "ali@remote.example",
ap_id: "https://remote.example/users/ali",
last_status_at: ~N[2020-01-01 00:00:00]
)
newer =
insert(:user,
local: false,
nickname: "alia@remote.example",
ap_id: "https://remote.example/users/alia",
last_status_at: ~N[2020-01-02 00:00:00]
)
{:ok, _user, _older} = User.follow(user, older)
{:ok, _user, _newer} = User.follow(user, newer)
assert [newer.id, older.id] ==
User.search("ali", for_user: user)
|> Enum.map(& &1.id)
end
test "groups followed/follower users first and sorts them by recency" do
user = insert(:user)
following_newest =
insert(:user,
local: false,
nickname: "mentiontesta@related.example",
ap_id: "https://related.example/users/mentiontesta",
last_status_at: ~N[2020-01-03 00:00:00]
)
follower_middle =
insert(:user,
local: false,
nickname: "mentiontestb@related.example",
ap_id: "https://related.example/users/mentiontestb",
last_status_at: ~N[2020-01-02 00:00:00]
)
mutual_oldest =
insert(:user,
local: false,
nickname: "mentiontestc@related.example",
ap_id: "https://related.example/users/mentiontestc",
last_status_at: ~N[2020-01-01 00:00:00]
)
unrelated_newer =
insert(:user,
local: false,
nickname: "mentiontestd@unrelated.example",
ap_id: "https://unrelated.example/users/mentiontestd",
last_status_at: ~N[2020-01-04 00:00:00]
)
{:ok, _user, _following_newest} = User.follow(user, following_newest)
{:ok, _follower_middle, _user} = User.follow(follower_middle, user)
{:ok, _user, _mutual_oldest} = User.follow(user, mutual_oldest)
{:ok, _mutual_oldest, _user} = User.follow(mutual_oldest, user)
results = User.search("mentiontest", for_user: user)
assert Enum.map(results, & &1.id) ==
[following_newest.id, follower_middle.id, mutual_oldest.id, unrelated_newer.id]
end
test "uses last_active_at when last_status_at is missing" do
user = insert(:user)
older =
insert(:user,
local: false,
nickname: "activefallbacka@remote.example",
ap_id: "https://remote.example/users/activefallbacka",
last_status_at: nil,
last_active_at: ~N[2020-01-01 00:00:00]
)
newer =
insert(:user,
local: false,
nickname: "activefallbackb@remote.example",
ap_id: "https://remote.example/users/activefallbackb",
last_status_at: nil,
last_active_at: ~N[2020-01-02 00:00:00]
)
{:ok, _user, _older} = User.follow(user, older)
{:ok, _user, _newer} = User.follow(user, newer)
assert [newer.id, older.id] ==
User.search("activefallback", for_user: user)
|> Enum.map(& &1.id)
end
test "does not return deactivated users even if related" do
user = insert(:user)
active =
insert(:user,
local: false,
nickname: "deactivatedtesta@remote.example",
ap_id: "https://remote.example/users/deactivatedtesta",
last_status_at: ~N[2020-01-02 00:00:00]
)
deactivated =
insert(:user,
local: false,
nickname: "deactivatedtestb@remote.example",
ap_id: "https://remote.example/users/deactivatedtestb",
last_status_at: ~N[2020-01-03 00:00:00]
)
{:ok, _user, _active} = User.follow(user, active)
{:ok, _user, _deactivated} = User.follow(user, deactivated)
Repo.update!(Ecto.Changeset.change(deactivated, is_active: false))
results = User.search("deactivatedtest", for_user: user) |> Enum.map(& &1.id)
assert results == [active.id]
end
test "does not return users from unreachable instances" do
user = insert(:user)
{:ok, _instance} = Instances.set_unreachable("dead.example")
dead =
insert(:user,
local: false,
nickname: "ali@dead.example",
ap_id: "https://dead.example/users/ali",
last_status_at: ~N[2020-01-02 00:00:00]
)
alive =
insert(:user,
local: false,
nickname: "ali@alive.example",
ap_id: "https://alive.example/users/ali",
last_status_at: ~N[2020-01-02 00:00:00]
)
{:ok, _user, _alive} = User.follow(user, alive)
{:ok, _user, _dead} = User.follow(user, dead)
results = User.search("ali", for_user: user) |> Enum.map(& &1.id)
assert results == [alive.id]
end
end
end

View file

@ -950,6 +950,50 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
refute Activity.get_by_ap_id(data["id"])
end
test "does not process post with Host header not for us", %{conn: conn} do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
object_id = "https://one.com/objects/inbox-forged-note"
data = %{
"type" => "Create",
"actor" => alice.ap_id,
"id" => "https://one.com/activities/inbox-forged-create",
"context" => "https://one.com/contexts/inbox-forged-create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => object_id,
"actor" => alice.ap_id,
"attributedTo" => alice.ap_id,
"context" => "https://one.com/contexts/inbox-forged-create",
"content" => "forged post",
"published" => "2024-07-25T13:33:31Z",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => []
}
}
# Plug will complain when replacing raw host header with put_req_header.
# The Plug way is updating conn.host, but that isn't the raw header
# and that isn't used in the EnsureHostMatchesPlug, because it doesn't include the port.
conn =
conn
|> assign_valid_signature_for_actor(alice)
|> delete_req_header("host")
|> put_req_header("content-type", "application/activity+json")
conn = %{conn | req_headers: conn.req_headers ++ [{"host", "invalid.example.com"}]}
conn = post(conn, "/inbox", data)
assert "Host header does not match this instance" == conn.resp_body
assert 400 == conn.status
assert true == conn.halted
refute Activity.get_by_ap_id(data["id"])
refute Object.get_by_ap_id(object_id)
end
test "accept follow activity", %{conn: conn} do
clear_config([:instance, :federating], true)
relay = Relay.get_actor()

View file

@ -86,6 +86,43 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert activity.data["cc"] == [user.ap_id]
end
test "it rejects Flag activities when both reporter and reported account are remote" do
reporter = insert(:user, local: false, domain: "mastodon.cat")
reported = insert(:user, local: false, domain: "nicecrew.digital")
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"actor" => reporter.ap_id,
"content" => "blocked AND reported!!!",
"object" => [reported.ap_id, "https://nicecrew.digital/objects/report-status"],
"type" => "Flag"
}
assert {:reject, reason} = Transmogrifier.handle_incoming(message)
assert reason =~ "third-party report"
refute "Flag" |> Pleroma.Activity.Queries.by_type() |> Pleroma.Repo.one()
end
test "it accepts Flag activities with just actor id as object" do
user = insert(:user)
other_user = insert(:user)
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"cc" => [user.ap_id],
"object" => user.ap_id,
"type" => "Flag",
"content" => "blocked AND reported!!!",
"actor" => other_user.ap_id
}
assert {:ok, activity} = Transmogrifier.handle_incoming(message)
assert activity.data["content"] == "blocked AND reported!!!"
assert activity.data["actor"] == other_user.ap_id
assert activity.data["cc"] == [user.ap_id]
end
test "it accepts Move activities" do
old_user = insert(:user)
new_user = insert(:user)

View file

@ -0,0 +1,121 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.EnsureHostMatchesPlugTest do
use Pleroma.Web.ConnCase
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Plugs.EnsureHostMatchesPlug
import Plug.Conn
import Tesla.Mock
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
defp set_host(conn, host), do: %{conn | req_headers: conn.req_headers ++ [{"host", host}]}
describe "EnsureHostMatchesPlug" do
setup do
conn = build_conn(:post, "/cofe") |> assign(:valid_signature, true)
[conn: conn]
end
test "gracefully handles no Host header", %{conn: conn} do
conn = EnsureHostMatchesPlug.call(conn, %{})
assert conn.status == 400
assert conn.halted == true
assert conn.resp_body == "Host header not provided"
end
test "gracefully handles empty Host header", %{conn: conn} do
conn =
conn
|> set_host("")
|> EnsureHostMatchesPlug.call(%{})
assert conn.status == 400
assert conn.halted == true
assert conn.resp_body == "Host header not provided"
end
test "it rejects Host header not matching Endpoint URL", %{conn: conn} do
conn =
conn
|> set_host("invalid.example.com")
|> EnsureHostMatchesPlug.call(%{})
assert conn.status == 400
assert conn.halted == true
assert conn.resp_body == "Host header does not match this instance"
end
test "it rejects Host header not matching Endpoint with port", %{conn: conn} do
endpoint = URI.parse(Endpoint.url())
conn =
conn
|> set_host("invalid.example.com:#{endpoint.port}")
|> EnsureHostMatchesPlug.call(%{})
assert conn.status == 400
assert conn.halted == true
assert conn.resp_body == "Host header does not match this instance"
end
test "it rejects Host header not matching Endpoint port", %{conn: conn} do
endpoint = URI.parse(Endpoint.url())
conn =
conn
|> set_host("#{endpoint.host}:25")
|> EnsureHostMatchesPlug.call(%{})
assert conn.status == 400
assert conn.halted == true
assert conn.resp_body == "Host header does not match this instance"
end
test "it rejects multiple Host headers", %{conn: conn} do
conn =
conn
|> set_host("host1.example.com")
|> set_host("host2.example.com")
|> EnsureHostMatchesPlug.call(%{})
assert conn.status == 400
assert conn.halted == true
assert conn.resp_body == "More than one Host header provided"
end
test "it works for Host header without port", %{conn: conn} do
endpoint = URI.parse(Endpoint.url())
conn =
conn
|> set_host("#{endpoint.host}")
|> EnsureHostMatchesPlug.call(%{})
assert conn.halted == false
assert Map.get(conn.assigns, :valid_host_header, nil)
end
test "it works for Host header with port same as Endpoint", %{
conn: conn
} do
endpoint = URI.parse(Endpoint.url())
conn =
conn
|> set_host("#{endpoint.host}:#{endpoint.port}")
|> EnsureHostMatchesPlug.call(%{})
assert conn.halted == false
assert Map.get(conn.assigns, :valid_host_header, nil)
end
end
end

View file

@ -106,4 +106,38 @@ defmodule Pleroma.Web.Plugs.RemoteIpTest do
assert conn.remote_ip == {1, 1, 1, 1}
end
test "reserved ranges are configurable" do
clear_config([RemoteIp, :reserved], [])
conn =
conn(:get, "/")
|> put_req_header("x-forwarded-for", "1.1.1.1, 10.0.0.3")
|> RemoteIp.call(nil)
assert conn.remote_ip == {10, 0, 0, 3}
end
test "clients override reserved ranges" do
clear_config([RemoteIp, :clients], ["10.0.0.0/8"])
conn =
conn(:get, "/")
|> put_req_header("x-forwarded-for", "1.1.1.1, 10.0.0.3")
|> RemoteIp.call(nil)
assert conn.remote_ip == {10, 0, 0, 3}
end
test "clients override proxies" do
clear_config([RemoteIp, :clients], ["10.0.0.3"])
clear_config([RemoteIp, :proxies], ["10.0.0.0/8"])
conn =
conn(:get, "/")
|> put_req_header("x-forwarded-for", "1.1.1.1, 10.0.0.3")
|> RemoteIp.call(nil)
assert conn.remote_ip == {10, 0, 0, 3}
end
end

View file

@ -16,12 +16,13 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do
alias Pleroma.Signature
alias Pleroma.User
alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Federator
alias Pleroma.Workers.SignatureRetryWorker
defp signature_headers_for(%User{} = signer) do
[
{"host", "local.test"},
{"host", "#{URI.parse(Endpoint.url()).host}"},
{"date", "Thu, 25 Jul 2024 13:33:31 GMT"},
{"digest", "SHA-256=fake-digest"},
{"content-type", "application/activity+json"},
@ -245,6 +246,66 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do
refute Activity.get_by_ap_id(create["id"])
end
test "cancels when the Host header does not match Endpoint" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
create = %{
"type" => "Create",
"actor" => alice.ap_id,
"id" => "https://one.com/activities/invalid-signature-create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => "https://one.com/objects/invalid-signature-note",
"actor" => alice.ap_id,
"attributedTo" => alice.ap_id,
"content" => "forged post",
"published" => "2024-07-25T13:33:31Z",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => []
}
}
expect_signature_from(alice)
headers =
[
{"host", "invalid.example.com"},
{"date", "Thu, 25 Jul 2024 13:33:31 GMT"},
{"digest", "SHA-256=fake-digest"},
{"content-type", "application/activity+json"},
{
"signature",
"keyId=\"#{alice.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
}
]
assert {:ok, oban_job} =
Federator.incoming_failed_signature_ap_doc(%{
method: "POST",
req_headers: headers,
request_path: "/inbox",
params: create,
query_string: ""
})
log =
capture_log([level: :warning], fn ->
assert {:cancel, :host_header_mismatch} = SignatureRetryWorker.perform(oban_job)
end)
assert log =~ "Failed-signature inbox retry rejected"
assert log =~ "reason=:host_header_mismatch"
assert log =~ "payload_actor=\"https://one.com/users/alice\""
assert log =~ "signature_actor=\"https://one.com/users/alice\""
assert log =~ "activity_id=\"https://one.com/activities/invalid-signature-create\""
assert log =~ "type=\"Create\""
assert log =~ "request_path=\"/inbox\""
refute Activity.get_by_ap_id(create["id"])
end
test "processes the activity after refetching a valid matching signature" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
@ -309,11 +370,11 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do
"content-type" => "application/activity+json",
date: date,
digest: digest,
host: "local.test"
host: "#{URI.parse(Endpoint.url()).host}"
})
req_headers = [
["host", "local.test"],
["host", "#{URI.parse(Endpoint.url()).host}"],
["date", date],
["digest", digest],
["content-type", "application/activity+json"],

View file

@ -119,7 +119,10 @@ defmodule Pleroma.Web.ConnCase do
DataCase.stub_pipeline()
Mox.verify_on_exit!()
endpoint = URI.parse(Pleroma.Web.Endpoint.url())
conn = Phoenix.ConnTest.build_conn()
conn = %{conn | req_headers: [{"host", "#{endpoint.host}:#{endpoint.port}"}]}
{:ok, conn: Phoenix.ConnTest.build_conn()}
{:ok, conn: conn}
end
end