Lock signature verification and add per-IP ingest limits
This commit is contained in:
@@ -6,6 +6,7 @@ defmodule Parrhesia.ApplicationTest do
|
||||
assert is_pid(Process.whereis(Parrhesia.Telemetry))
|
||||
assert is_pid(Process.whereis(Parrhesia.Config))
|
||||
assert is_pid(Process.whereis(Parrhesia.Web.EventIngestLimiter))
|
||||
assert is_pid(Process.whereis(Parrhesia.Web.IPEventIngestLimiter))
|
||||
assert is_pid(Process.whereis(Parrhesia.Storage.Supervisor))
|
||||
assert is_pid(Process.whereis(Parrhesia.Subscriptions.Supervisor))
|
||||
assert is_pid(Process.whereis(Parrhesia.Fanout.Dispatcher))
|
||||
|
||||
@@ -10,6 +10,8 @@ defmodule Parrhesia.ConfigTest do
|
||||
assert Parrhesia.Config.get([:limits, :max_event_ingest_per_window]) == 120
|
||||
assert Parrhesia.Config.get([:limits, :max_tags_per_event]) == 256
|
||||
assert Parrhesia.Config.get([:limits, :max_tag_values_per_filter]) == 128
|
||||
assert Parrhesia.Config.get([:limits, :ip_max_event_ingest_per_window]) == 1_000
|
||||
assert Parrhesia.Config.get([:limits, :ip_event_ingest_window_seconds]) == 1
|
||||
assert Parrhesia.Config.get([:limits, :relay_max_event_ingest_per_window]) == 10_000
|
||||
assert Parrhesia.Config.get([:limits, :relay_event_ingest_window_seconds]) == 1
|
||||
assert Parrhesia.Config.get([:limits, :event_ingest_window_seconds]) == 1
|
||||
@@ -21,6 +23,7 @@ defmodule Parrhesia.ConfigTest do
|
||||
assert Parrhesia.Config.get([:policies, :marmot_media_max_imeta_tags_per_event]) == 8
|
||||
assert Parrhesia.Config.get([:policies, :marmot_media_reject_mip04_v1]) == true
|
||||
assert Parrhesia.Config.get([:policies, :marmot_push_max_trigger_age_seconds]) == 120
|
||||
assert Parrhesia.Config.get([:features, :verify_event_signatures_locked?]) == false
|
||||
assert Parrhesia.Config.get([:features, :verify_event_signatures]) == false
|
||||
assert Parrhesia.Config.get([:features, :nip_50_search]) == true
|
||||
assert Parrhesia.Config.get([:features, :marmot_push_notifications]) == false
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
defmodule Parrhesia.Tasks.Nip66PublisherTest do
|
||||
use Parrhesia.IntegrationCase, async: false
|
||||
use Parrhesia.IntegrationCase, async: false, sandbox: :shared
|
||||
|
||||
alias Parrhesia.API.Events
|
||||
alias Parrhesia.API.Identity
|
||||
|
||||
@@ -225,6 +225,50 @@ defmodule Parrhesia.Web.ConnectionTest do
|
||||
]
|
||||
end
|
||||
|
||||
test "EVENT applies per-IP ingress throttling across connections" do
|
||||
limiter =
|
||||
start_supervised!(
|
||||
{Parrhesia.Web.IPEventIngestLimiter,
|
||||
name: nil, max_events_per_window: 1, window_seconds: 60}
|
||||
)
|
||||
|
||||
first_state =
|
||||
connection_state(
|
||||
remote_ip: "203.0.113.10",
|
||||
remote_ip_event_ingest_limiter: limiter
|
||||
)
|
||||
|
||||
second_state =
|
||||
connection_state(
|
||||
remote_ip: "203.0.113.10",
|
||||
remote_ip_event_ingest_limiter: limiter
|
||||
)
|
||||
|
||||
first_event = valid_event(%{"content" => "first from ip"}) |> recalculate_event_id()
|
||||
second_event = valid_event(%{"content" => "second from ip"}) |> recalculate_event_id()
|
||||
|
||||
assert {:push, {:text, first_response}, _next_state} =
|
||||
Connection.handle_in(
|
||||
{JSON.encode!(["EVENT", first_event]), [opcode: :text]},
|
||||
first_state
|
||||
)
|
||||
|
||||
assert JSON.decode!(first_response) == ["OK", first_event["id"], true, "ok: event stored"]
|
||||
|
||||
assert {:push, {:text, second_response}, ^second_state} =
|
||||
Connection.handle_in(
|
||||
{JSON.encode!(["EVENT", second_event]), [opcode: :text]},
|
||||
second_state
|
||||
)
|
||||
|
||||
assert JSON.decode!(second_response) == [
|
||||
"OK",
|
||||
second_event["id"],
|
||||
false,
|
||||
"rate-limited: too many EVENT messages from this IP"
|
||||
]
|
||||
end
|
||||
|
||||
test "protected sync REQ requires matching ACL grant" do
|
||||
previous_acl = Application.get_env(:parrhesia, :acl, [])
|
||||
|
||||
|
||||
26
test/parrhesia/web/ip_event_ingest_limiter_test.exs
Normal file
26
test/parrhesia/web/ip_event_ingest_limiter_test.exs
Normal file
@@ -0,0 +1,26 @@
|
||||
defmodule Parrhesia.Web.IPEventIngestLimiterTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Parrhesia.Web.IPEventIngestLimiter
|
||||
|
||||
test "allows events up to the configured per-IP window cap" do
|
||||
limiter =
|
||||
start_supervised!(
|
||||
{IPEventIngestLimiter, name: nil, max_events_per_window: 2, window_seconds: 60}
|
||||
)
|
||||
|
||||
assert :ok = IPEventIngestLimiter.allow("203.0.113.10", limiter)
|
||||
assert :ok = IPEventIngestLimiter.allow("203.0.113.10", limiter)
|
||||
assert {:error, :ip_event_rate_limited} = IPEventIngestLimiter.allow("203.0.113.10", limiter)
|
||||
assert :ok = IPEventIngestLimiter.allow("203.0.113.11", limiter)
|
||||
end
|
||||
|
||||
test "allows events without a remote IP" do
|
||||
limiter =
|
||||
start_supervised!(
|
||||
{IPEventIngestLimiter, name: nil, max_events_per_window: 1, window_seconds: 60}
|
||||
)
|
||||
|
||||
assert :ok = IPEventIngestLimiter.allow(nil, limiter)
|
||||
end
|
||||
end
|
||||
@@ -49,6 +49,11 @@ defmodule Parrhesia.IntegrationCase do
|
||||
receive do
|
||||
:stop ->
|
||||
Sandbox.checkin(Repo)
|
||||
|
||||
# Allow the pool to process the checkin before this process
|
||||
# exits, so Postgrex does not see a dead client and log a
|
||||
# spurious disconnect error.
|
||||
Process.sleep(50)
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
@@ -5,4 +5,23 @@ exclude_tags =
|
||||
[:nak_e2e]
|
||||
end
|
||||
|
||||
# Suppress Postgrex disconnect logs that fire during sandbox teardown.
|
||||
# When a sandbox-owning process exits, the connection pool detects the
|
||||
# dead client and logs an error asynchronously. This is expected cleanup
|
||||
# noise, not a real failure — silence it so test output stays pristine.
|
||||
:logger.add_primary_filter(
|
||||
:suppress_sandbox_disconnect,
|
||||
{fn
|
||||
%{msg: {:string, chars}}, _extra ->
|
||||
if :string.find(IO.chardata_to_string(chars), "(DBConnection.ConnectionError)") != :nomatch do
|
||||
:stop
|
||||
else
|
||||
:ignore
|
||||
end
|
||||
|
||||
_event, _extra ->
|
||||
:ignore
|
||||
end, []}
|
||||
)
|
||||
|
||||
ExUnit.start(exclude: exclude_tags)
|
||||
|
||||
Reference in New Issue
Block a user