Add Signed Fetch Statistics #312

Merged
floatingghost merged 4 commits from luna/akkoma:http-signature-statistics into develop 2022-11-26 19:22:56 +00:00
7 changed files with 216 additions and 58 deletions

View File

@ -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

View File

@ -43,4 +43,6 @@ defmodule Pleroma.Instances do
url_or_host
end
end
defdelegate set_request_signatures(url_or_host), to: Instance
end

View File

@ -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

View File

@ -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(
"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

View File

@ -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

View File

@ -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
Review

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

View File

@ -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