Reject requests from specified instances if `authorized_fetch_mode` is enabled
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
d39f803bdd
commit
c899af1d6a
|
@ -216,6 +216,7 @@
|
|||
allow_relay: true,
|
||||
public: true,
|
||||
quarantined_instances: [],
|
||||
rejected_instances: [],
|
||||
static_dir: "instance/static/",
|
||||
allowed_post_formats: [
|
||||
"text/plain",
|
||||
|
|
|
@ -714,6 +714,18 @@
|
|||
{"*.quarantined.com", "Reason"}
|
||||
]
|
||||
},
|
||||
%{
|
||||
key: :rejected_instances,
|
||||
type: {:list, :tuple},
|
||||
key_placeholder: "instance",
|
||||
value_placeholder: "reason",
|
||||
description:
|
||||
"List of ActivityPub instances to reject requests from if authorized_fetch_mode is enabled",
|
||||
suggestions: [
|
||||
{"rejected.com", "Reason"},
|
||||
{"*.rejected.com", "Reason"}
|
||||
]
|
||||
},
|
||||
%{
|
||||
key: :static_dir,
|
||||
type: :string,
|
||||
|
|
|
@ -41,6 +41,7 @@ To add configuration to your config file, you can copy it from the base config.
|
|||
* `allow_relay`: Permits remote instances to subscribe to all public posts of your instance. This may increase the visibility of your instance.
|
||||
* `public`: Makes the client API in authenticated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network. Note that there is a dependent setting restricting or allowing unauthenticated access to specific resources, see `restrict_unauthenticated` for more details.
|
||||
* `quarantined_instances`: ActivityPub instances where private (DMs, followers-only) activities will not be send.
|
||||
* `rejected_instances`: ActivityPub instances to reject requests from if authorized_fetch_mode is enabled.
|
||||
* `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML).
|
||||
* `extended_nickname_format`: Set to `true` to use extended local nicknames format (allows underscores/dashes). This will break federation with
|
||||
older software for theses nicknames.
|
||||
|
|
|
@ -37,8 +37,7 @@ def key_id_to_actor_id(key_id) do
|
|||
end
|
||||
|
||||
def fetch_public_key(conn) do
|
||||
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
||||
{:ok, actor_id} <- key_id_to_actor_id(kid),
|
||||
with {:ok, actor_id} <- get_actor_id(conn),
|
||||
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
|
||||
{:ok, public_key}
|
||||
else
|
||||
|
@ -48,8 +47,7 @@ def fetch_public_key(conn) do
|
|||
end
|
||||
|
||||
def refetch_public_key(conn) do
|
||||
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
||||
{:ok, actor_id} <- key_id_to_actor_id(kid),
|
||||
with {:ok, actor_id} <- get_actor_id(conn),
|
||||
{:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),
|
||||
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
|
||||
{:ok, public_key}
|
||||
|
@ -59,6 +57,16 @@ def refetch_public_key(conn) do
|
|||
end
|
||||
end
|
||||
|
||||
def get_actor_id(conn) do
|
||||
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
||||
{:ok, actor_id} <- key_id_to_actor_id(kid) do
|
||||
{:ok, actor_id}
|
||||
else
|
||||
e ->
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
def sign(%User{} = user, headers) do
|
||||
with {:ok, %{keys: keys}} <- User.ensure_keys_present(user),
|
||||
{:ok, private_key, _} <- Keys.keys_from_pem(keys) do
|
||||
|
|
|
@ -105,6 +105,7 @@ def features do
|
|||
|
||||
def federation do
|
||||
quarantined = Config.get([:instance, :quarantined_instances], [])
|
||||
rejected = Config.get([:instance, :rejected_instances], [])
|
||||
|
||||
if Config.get([:mrf, :transparency]) do
|
||||
{:ok, data} = MRF.describe()
|
||||
|
@ -124,6 +125,12 @@ def federation do
|
|||
|> Enum.map(fn {instance, reason} -> {instance, %{"reason" => reason}} end)
|
||||
|> Map.new()
|
||||
})
|
||||
|> Map.put(
|
||||
:rejected_instances,
|
||||
rejected
|
||||
|> Enum.map(fn {instance, reason} -> {instance, %{"reason" => reason}} end)
|
||||
|> Map.new()
|
||||
)
|
||||
else
|
||||
%{}
|
||||
end
|
||||
|
|
|
@ -5,6 +5,10 @@
|
|||
defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
|
||||
import Plug.Conn
|
||||
import Phoenix.Controller, only: [get_format: 1, text: 2]
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Web.ActivityPub.MRF
|
||||
|
||||
require Logger
|
||||
|
||||
def init(options) do
|
||||
|
@ -19,7 +23,9 @@ def call(conn, _opts) do
|
|||
if get_format(conn) == "activity+json" do
|
||||
conn
|
||||
|> maybe_assign_valid_signature()
|
||||
|> maybe_assign_actor_id()
|
||||
|> maybe_require_signature()
|
||||
|> maybe_filter_requests()
|
||||
else
|
||||
conn
|
||||
end
|
||||
|
@ -46,6 +52,16 @@ defp maybe_assign_valid_signature(conn) do
|
|||
end
|
||||
end
|
||||
|
||||
defp maybe_assign_actor_id(%{assigns: %{valid_signature: true}} = conn) do
|
||||
adapter = Application.get_env(:http_signatures, :adapter)
|
||||
|
||||
{:ok, actor_id} = adapter.get_actor_id(conn)
|
||||
|
||||
assign(conn, :actor_id, actor_id)
|
||||
end
|
||||
|
||||
defp maybe_assign_actor_id(conn), do: conn
|
||||
|
||||
defp has_signature_header?(conn) do
|
||||
conn |> get_req_header("signature") |> Enum.at(0, false)
|
||||
end
|
||||
|
@ -62,4 +78,28 @@ defp maybe_require_signature(conn) do
|
|||
conn
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_filter_requests(%{halted: true} = conn), do: conn
|
||||
|
||||
defp maybe_filter_requests(conn) do
|
||||
if Pleroma.Config.get([:activitypub, :authorized_fetch_mode], false) do
|
||||
%{host: host} = URI.parse(conn.assigns.actor_id)
|
||||
|
||||
if MRF.subdomain_match?(rejected_domains(), host) do
|
||||
conn
|
||||
|> put_status(:unauthorized)
|
||||
|> halt()
|
||||
else
|
||||
conn
|
||||
end
|
||||
else
|
||||
conn
|
||||
end
|
||||
end
|
||||
|
||||
defp rejected_domains do
|
||||
Config.get([:instance, :rejected_instances])
|
||||
|> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
|
||||
|> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
|
||||
end
|
||||
end
|
||||
|
|
|
@ -70,6 +70,14 @@ test "it returns error when not found user" do
|
|||
end
|
||||
end
|
||||
|
||||
describe "get_actor_id/1" do
|
||||
test "it returns actor id" do
|
||||
ap_id = "https://mastodon.social/users/lambadalambda"
|
||||
|
||||
assert Signature.get_actor_id(make_fake_conn(ap_id)) == {:ok, ap_id}
|
||||
end
|
||||
end
|
||||
|
||||
describe "sign/2" do
|
||||
test "it returns signature headers" do
|
||||
user =
|
||||
|
|
|
@ -10,11 +10,15 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
|
|||
import Phoenix.Controller, only: [put_format: 2]
|
||||
import Mock
|
||||
|
||||
test "it call HTTPSignatures to check validity if the actor sighed it" do
|
||||
test "it call HTTPSignatures to check validity if the actor signed it" do
|
||||
params = %{"actor" => "http://mastodon.example.org/users/admin"}
|
||||
conn = build_conn(:get, "/doesntmattter", params)
|
||||
|
||||
with_mock HTTPSignatures, validate_conn: fn _ -> true end do
|
||||
with_mock HTTPSignatures,
|
||||
validate_conn: fn _ -> true end,
|
||||
signature_for_conn: fn _ ->
|
||||
%{"keyId" => "http://mastodon.example.org/users/admin#main-key"}
|
||||
end do
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header(
|
||||
|
@ -41,7 +45,11 @@ test "it call HTTPSignatures to check validity if the actor sighed it" do
|
|||
end
|
||||
|
||||
test "when signature header is present", %{conn: conn} do
|
||||
with_mock HTTPSignatures, validate_conn: fn _ -> false end do
|
||||
with_mock HTTPSignatures,
|
||||
validate_conn: fn _ -> false end,
|
||||
signature_for_conn: fn _ ->
|
||||
%{"keyId" => "http://mastodon.example.org/users/admin#main-key"}
|
||||
end do
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header(
|
||||
|
@ -58,7 +66,11 @@ test "when signature header is present", %{conn: conn} do
|
|||
assert called(HTTPSignatures.validate_conn(:_))
|
||||
end
|
||||
|
||||
with_mock HTTPSignatures, validate_conn: fn _ -> true end do
|
||||
with_mock HTTPSignatures,
|
||||
validate_conn: fn _ -> true end,
|
||||
signature_for_conn: fn _ ->
|
||||
%{"keyId" => "http://mastodon.example.org/users/admin#main-key"}
|
||||
end do
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header(
|
||||
|
@ -82,4 +94,47 @@ test "halts the connection when `signature` header is not present", %{conn: conn
|
|||
assert conn.resp_body == "Request not signed"
|
||||
end
|
||||
end
|
||||
|
||||
test "rejects requests from `rejected_instances` when `authorized_fetch_mode` is enabled" do
|
||||
clear_config([:activitypub, :authorized_fetch_mode], true)
|
||||
clear_config([:instance, :rejected_instances], [{"mastodon.example.org", "no reason"}])
|
||||
|
||||
with_mock HTTPSignatures,
|
||||
validate_conn: fn _ -> true end,
|
||||
signature_for_conn: fn _ ->
|
||||
%{"keyId" => "http://mastodon.example.org/users/admin#main-key"}
|
||||
end do
|
||||
conn =
|
||||
build_conn(:get, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
|
||||
|> put_req_header(
|
||||
"signature",
|
||||
"keyId=\"http://mastodon.example.org/users/admin#main-key"
|
||||
)
|
||||
|> put_format("activity+json")
|
||||
|> HTTPSignaturePlug.call(%{})
|
||||
|
||||
assert conn.assigns.valid_signature == true
|
||||
assert conn.halted == true
|
||||
assert called(HTTPSignatures.validate_conn(:_))
|
||||
end
|
||||
|
||||
with_mock HTTPSignatures,
|
||||
validate_conn: fn _ -> true end,
|
||||
signature_for_conn: fn _ ->
|
||||
%{"keyId" => "http://allowed.example.org/users/admin#main-key"}
|
||||
end do
|
||||
conn =
|
||||
build_conn(:get, "/doesntmattter", %{"actor" => "http://allowed.example.org/users/admin"})
|
||||
|> put_req_header(
|
||||
"signature",
|
||||
"keyId=\"http://allowed.example.org/users/admin#main-key"
|
||||
)
|
||||
|> put_format("activity+json")
|
||||
|> HTTPSignaturePlug.call(%{})
|
||||
|
||||
assert conn.assigns.valid_signature == true
|
||||
assert conn.halted == false
|
||||
assert called(HTTPSignatures.validate_conn(:_))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue