Add Signed Fetch Statistics #312
|
@ -157,7 +157,8 @@ defmodule Pleroma.Application do
|
|||
build_cachex("failed_proxy_url", limit: 2500),
|
||||
build_cachex("banned_urls", default_ttl: :timer.hours(24 * 30), limit: 5_000),
|
||||
build_cachex("translations", default_ttl: :timer.hours(24 * 30), limit: 2500),
|
||||
build_cachex("instances", default_ttl: :timer.hours(24), ttl_interval: 1000, limit: 2500)
|
||||
build_cachex("instances", default_ttl: :timer.hours(24), ttl_interval: 1000, limit: 2500),
|
||||
build_cachex("request_signatures", default_ttl: :timer.hours(24 * 30), limit: 3000)
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
@ -43,4 +43,6 @@ defmodule Pleroma.Instances do
|
|||
url_or_host
|
||||
end
|
||||
end
|
||||
|
||||
defdelegate set_request_signatures(url_or_host), to: Instance
|
||||
luna marked this conversation as resolved
Outdated
|
||||
end
|
||||
|
|
|
@ -26,6 +26,7 @@ defmodule Pleroma.Instances.Instance do
|
|||
field(:favicon, :string)
|
||||
field(:metadata_updated_at, :naive_datetime)
|
||||
field(:nodeinfo, :map, default: %{})
|
||||
field(:has_request_signatures, :boolean)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
@ -34,7 +35,14 @@ defmodule Pleroma.Instances.Instance do
|
|||
|
||||
def changeset(struct, params \\ %{}) do
|
||||
struct
|
||||
|> cast(params, [:host, :unreachable_since, :favicon, :nodeinfo, :metadata_updated_at])
|
||||
|> cast(params, [
|
||||
:host,
|
||||
:unreachable_since,
|
||||
:favicon,
|
||||
:nodeinfo,
|
||||
:metadata_updated_at,
|
||||
:has_request_signatures
|
||||
])
|
||||
|> validate_required([:host])
|
||||
|> unique_constraint(:host)
|
||||
end
|
||||
|
@ -316,4 +324,24 @@ defmodule Pleroma.Instances.Instance do
|
|||
end)
|
||||
end
|
||||
end
|
||||
|
||||
def set_request_signatures(url_or_host) when is_binary(url_or_host) do
|
||||
host = host(url_or_host)
|
||||
existing_record = Repo.get_by(Instance, %{host: host})
|
||||
changes = %{has_request_signatures: true}
|
||||
|
||||
cond do
|
||||
is_nil(existing_record) ->
|
||||
%Instance{}
|
||||
|> changeset(Map.put(changes, :host, host))
|
||||
|> Repo.insert()
|
||||
|
||||
true ->
|
||||
existing_record
|
||||
|> changeset(changes)
|
||||
|> Repo.update()
|
||||
end
|
||||
end
|
||||
|
||||
def set_request_signatures(_), do: {:error, :invalid_input}
|
||||
end
|
||||
|
|
|
@ -7,8 +7,12 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
|
|||
import Phoenix.Controller, only: [get_format: 1, text: 2]
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Web.Router
|
||||
alias Pleroma.Signature
|
||||
alias Pleroma.Instances
|
||||
require Logger
|
||||
|
||||
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
||||
|
||||
def init(options) do
|
||||
options
|
||||
end
|
||||
|
@ -57,6 +61,7 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
|
|||
|
||||
conn
|
||||
|> assign(:valid_signature, HTTPSignatures.validate_conn(conn))
|
||||
|> assign(:signature_actor_id, signature_host(conn))
|
||||
|> assign_valid_signature_on_route_aliases(rest)
|
||||
end
|
||||
|
||||
|
@ -78,6 +83,36 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
|
|||
conn |> get_req_header("signature") |> Enum.at(0, false)
|
||||
end
|
||||
|
||||
defp maybe_require_signature(
|
||||
%{assigns: %{valid_signature: true, signature_actor_id: actor_id}} = conn
|
||||
) do
|
||||
# inboxes implicitly need http signatures for authentication
|
||||
# so we don't really know if the instance will have broken federation after
|
||||
# we turn on authorized_fetch_mode.
|
||||
#
|
||||
# to "check" this is a signed fetch, verify if method is GET
|
||||
if conn.method == "GET" do
|
||||
actor_host = URI.parse(actor_id).host
|
||||
|
||||
case @cachex.get(:request_signatures_cache, actor_host) do
|
||||
{:ok, nil} ->
|
||||
Logger.debug("Successful signature from #{actor_host}")
|
||||
Instances.set_request_signatures(actor_host)
|
||||
@cachex.put(:request_signatures_cache, actor_host, true)
|
||||
|
||||
{:ok, true} ->
|
||||
:noop
|
||||
|
||||
any ->
|
||||
Logger.warn(
|
||||
luna marked this conversation as resolved
Outdated
floatingghost
commented
this is a nitpick, but this error message is a bit hard to parse perhaps you want something like this is a nitpick, but this error message is a bit hard to parse
perhaps you want something like `expected request signature cache to return a boolean, instead got ...`?
|
||||
"expected request signature cache to return a boolean, instead got #{inspect(any)}"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
conn
|
||||
end
|
||||
|
||||
defp maybe_require_signature(%{assigns: %{valid_signature: true}} = conn), do: conn
|
||||
|
||||
defp maybe_require_signature(conn) do
|
||||
|
@ -90,4 +125,14 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
|
|||
conn
|
||||
end
|
||||
end
|
||||
|
||||
defp signature_host(conn) do
|
||||
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
||||
{:ok, actor_id} <- Signature.key_id_to_actor_id(kid) do
|
||||
actor_id
|
||||
else
|
||||
e ->
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
defmodule Pleroma.Repo.Migrations.AddHasRequestSignatures do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:instances) do
|
||||
add(:has_request_signatures, :boolean, default: false, null: false)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
defmodule Pleroma.InstancesTest do
|
||||
alias Pleroma.Instances
|
||||
alias Pleroma.Instances.Instance
|
||||
|
||||
use Pleroma.DataCase
|
||||
|
||||
|
@ -121,4 +122,21 @@ defmodule Pleroma.InstancesTest do
|
|||
refute Instances.reachable?(host)
|
||||
end
|
||||
end
|
||||
|
||||
describe "set_request_signatures/1" do
|
||||
test "sets instance has request signatures" do
|
||||
host = "domain.com"
|
||||
|
||||
{:ok, instance} = Instances.set_request_signatures(host)
|
||||
luna marked this conversation as resolved
floatingghost
commented
maybe you want to check that the expected value is cached here as well maybe you want to check that the expected value is cached here as well
|
||||
assert instance.has_request_signatures
|
||||
|
||||
{:ok, cached_instance} = Instance.get_cached_by_url(host)
|
||||
assert cached_instance.has_request_signatures
|
||||
end
|
||||
|
||||
test "returns error status on non-binary input" do
|
||||
assert {:error, _} = Instances.set_request_signatures(nil)
|
||||
assert {:error, _} = Instances.set_request_signatures(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,34 +1,90 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
|
||||
use Pleroma.Web.ConnCase
|
||||
use Pleroma.Web.ConnCase, async: false
|
||||
import Pleroma.Factory
|
||||
alias Pleroma.Web.Plugs.HTTPSignaturePlug
|
||||
alias Pleroma.Instances.Instance
|
||||
alias Pleroma.Repo
|
||||
|
||||
import Plug.Conn
|
||||
import Phoenix.Controller, only: [put_format: 2]
|
||||
import Mock
|
||||
|
||||
test "it call HTTPSignatures to check validity if the actor sighed it" do
|
||||
setup_with_mocks([
|
||||
{HTTPSignatures, [],
|
||||
[
|
||||
signature_for_conn: fn _ ->
|
||||
%{"keyId" => "http://mastodon.example.org/users/admin#main-key"}
|
||||
end,
|
||||
validate_conn: fn conn ->
|
||||
Map.get(conn.assigns, :valid_signature, true)
|
||||
end
|
||||
]}
|
||||
]) do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp submit_to_plug(host), do: submit_to_plug(host, :get, "/doesntmattter")
|
||||
|
||||
defp submit_to_plug(host, method, path) do
|
||||
params = %{"actor" => "http://#{host}/users/admin"}
|
||||
|
||||
build_conn(method, path, params)
|
||||
|> put_req_header(
|
||||
"signature",
|
||||
"keyId=\"http://#{host}/users/admin#main-key"
|
||||
)
|
||||
|> put_format("activity+json")
|
||||
|> HTTPSignaturePlug.call(%{})
|
||||
end
|
||||
|
||||
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
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header(
|
||||
"signature",
|
||||
"keyId=\"http://mastodon.example.org/users/admin#main-key"
|
||||
)
|
||||
|> put_format("activity+json")
|
||||
|> HTTPSignaturePlug.call(%{})
|
||||
conn =
|
||||
conn
|
||||
|> 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 == false
|
||||
assert called(HTTPSignatures.validate_conn(:_))
|
||||
end
|
||||
assert conn.assigns.valid_signature == true
|
||||
assert conn.assigns.signature_actor_id == params["actor"]
|
||||
assert conn.halted == false
|
||||
assert called(HTTPSignatures.validate_conn(:_))
|
||||
end
|
||||
|
||||
test "it sets request signatures property on the instance" do
|
||||
host = "mastodon.example.org"
|
||||
conn = submit_to_plug(host)
|
||||
assert conn.assigns.valid_signature == true
|
||||
instance = Repo.get_by(Instance, %{host: host})
|
||||
assert instance.has_request_signatures
|
||||
end
|
||||
|
||||
test "it does not set request signatures property on the instance when using inbox" do
|
||||
host = "mastodon.example.org"
|
||||
conn = submit_to_plug(host, :post, "/inbox")
|
||||
assert conn.assigns.valid_signature == true
|
||||
|
||||
# we don't even create the instance entry if its just POST /inbox
|
||||
refute Repo.get_by(Instance, %{host: host})
|
||||
end
|
||||
|
||||
test "it does not set request signatures property on the instance when its cached" do
|
||||
host = "mastodon.example.org"
|
||||
Cachex.put(:request_signatures_cache, host, true)
|
||||
conn = submit_to_plug(host)
|
||||
assert conn.assigns.valid_signature == true
|
||||
|
||||
# we don't even create the instance entry if it was already done
|
||||
refute Repo.get_by(Instance, %{host: host})
|
||||
end
|
||||
|
||||
describe "requires a signature when `authorized_fetch_mode` is enabled" do
|
||||
|
@ -41,40 +97,39 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
|
|||
[conn: conn]
|
||||
end
|
||||
|
||||
test "when signature header is present", %{conn: conn} do
|
||||
with_mock HTTPSignatures, validate_conn: fn _ -> false end do
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header(
|
||||
"signature",
|
||||
"keyId=\"http://mastodon.example.org/users/admin#main-key"
|
||||
)
|
||||
|> HTTPSignaturePlug.call(%{})
|
||||
test "and signature is present and incorrect", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> assign(:valid_signature, false)
|
||||
|> put_req_header(
|
||||
"signature",
|
||||
"keyId=\"http://mastodon.example.org/users/admin#main-key"
|
||||
)
|
||||
|> HTTPSignaturePlug.call(%{})
|
||||
|
||||
assert conn.assigns.valid_signature == false
|
||||
assert conn.halted == true
|
||||
assert conn.status == 401
|
||||
assert conn.state == :sent
|
||||
assert conn.resp_body == "Request not signed"
|
||||
assert called(HTTPSignatures.validate_conn(:_))
|
||||
end
|
||||
|
||||
with_mock HTTPSignatures, validate_conn: fn _ -> true end do
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header(
|
||||
"signature",
|
||||
"keyId=\"http://mastodon.example.org/users/admin#main-key"
|
||||
)
|
||||
|> HTTPSignaturePlug.call(%{})
|
||||
|
||||
assert conn.assigns.valid_signature == true
|
||||
assert conn.halted == false
|
||||
assert called(HTTPSignatures.validate_conn(:_))
|
||||
end
|
||||
assert conn.assigns.valid_signature == false
|
||||
assert conn.halted == true
|
||||
assert conn.status == 401
|
||||
assert conn.state == :sent
|
||||
assert conn.resp_body == "Request not signed"
|
||||
assert called(HTTPSignatures.validate_conn(:_))
|
||||
end
|
||||
|
||||
test "halts the connection when `signature` header is not present", %{conn: conn} do
|
||||
test "and signature is correct", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header(
|
||||
"signature",
|
||||
"keyId=\"http://mastodon.example.org/users/admin#main-key"
|
||||
)
|
||||
|> HTTPSignaturePlug.call(%{})
|
||||
|
||||
assert conn.assigns.valid_signature == true
|
||||
assert conn.halted == false
|
||||
assert called(HTTPSignatures.validate_conn(:_))
|
||||
end
|
||||
|
||||
test "and halts the connection when `signature` header is not present", %{conn: conn} do
|
||||
conn = HTTPSignaturePlug.call(conn, %{})
|
||||
assert conn.assigns[:valid_signature] == nil
|
||||
assert conn.halted == true
|
||||
|
@ -82,16 +137,16 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
|
|||
assert conn.state == :sent
|
||||
assert conn.resp_body == "Request not signed"
|
||||
end
|
||||
end
|
||||
|
||||
test "aliases redirected /object endpoints", _ do
|
||||
obj = insert(:note)
|
||||
act = insert(:note_activity, note: obj)
|
||||
params = %{"actor" => "someparam"}
|
||||
path = URI.parse(obj.data["id"]).path
|
||||
conn = build_conn(:get, path, params)
|
||||
test "aliases redirected /object endpoints", _ do
|
||||
obj = insert(:note)
|
||||
act = insert(:note_activity, note: obj)
|
||||
params = %{"actor" => "someparam"}
|
||||
path = URI.parse(obj.data["id"]).path
|
||||
conn = build_conn(:get, path, params)
|
||||
|
||||
assert ["/notice/#{act.id}", "/notice/#{act.id}?actor=someparam"] ==
|
||||
HTTPSignaturePlug.route_aliases(conn)
|
||||
end
|
||||
assert ["/notice/#{act.id}", "/notice/#{act.id}?actor=someparam"] ==
|
||||
HTTPSignaturePlug.route_aliases(conn)
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue
you could probably use a
defdelegate set_request_signatures(url_or_host), to: Instance
here