741 lines
22 KiB
Elixir
741 lines
22 KiB
Elixir
defmodule Parrhesia.Policy.EventPolicy do
|
|
@moduledoc """
|
|
Write/read policy checks for relay operations.
|
|
"""
|
|
|
|
alias Parrhesia.API.ACL
|
|
alias Parrhesia.API.RequestContext
|
|
alias Parrhesia.Policy.ConnectionPolicy
|
|
alias Parrhesia.Storage
|
|
|
|
@type policy_error ::
|
|
:auth_required
|
|
| :pubkey_not_allowed
|
|
| :restricted_giftwrap
|
|
| :sync_read_not_allowed
|
|
| :sync_write_not_allowed
|
|
| :marmot_group_h_tag_required
|
|
| :marmot_group_h_values_exceeded
|
|
| :marmot_group_filter_window_too_wide
|
|
| :media_metadata_tags_exceeded
|
|
| :media_metadata_tag_value_too_large
|
|
| :media_metadata_url_too_long
|
|
| :media_metadata_invalid_url
|
|
| :media_metadata_invalid_hash
|
|
| :media_metadata_invalid_mime
|
|
| :media_metadata_mime_not_allowed
|
|
| :media_metadata_unsupported_version
|
|
| :push_notification_relay_tags_exceeded
|
|
| :push_notification_payload_too_large
|
|
| :push_notification_replay_window_exceeded
|
|
| :push_notification_missing_expiration
|
|
| :push_notification_expiration_too_far
|
|
| :push_notification_server_recipients_exceeded
|
|
| :protected_event_requires_auth
|
|
| :protected_event_pubkey_mismatch
|
|
| :pow_below_minimum
|
|
| :pubkey_banned
|
|
| :event_banned
|
|
|
|
@spec authorize_read([map()], MapSet.t(String.t())) :: :ok | {:error, policy_error()}
|
|
def authorize_read(filters, authenticated_pubkeys) when is_list(filters) do
|
|
authorize_read(filters, authenticated_pubkeys, request_context(authenticated_pubkeys))
|
|
end
|
|
|
|
@spec authorize_read([map()], MapSet.t(String.t()), RequestContext.t()) ::
|
|
:ok | {:error, policy_error()}
|
|
def authorize_read(filters, authenticated_pubkeys, %RequestContext{} = context)
|
|
when is_list(filters) do
|
|
auth_required? = config_bool([:policies, :auth_required_for_reads], false)
|
|
|
|
cond do
|
|
match?(
|
|
{:error, _reason},
|
|
ConnectionPolicy.authorize_authenticated_pubkeys(authenticated_pubkeys)
|
|
) ->
|
|
ConnectionPolicy.authorize_authenticated_pubkeys(authenticated_pubkeys)
|
|
|
|
auth_required? and MapSet.size(authenticated_pubkeys) == 0 ->
|
|
{:error, :auth_required}
|
|
|
|
giftwrap_restricted?(filters, authenticated_pubkeys) ->
|
|
{:error, :restricted_giftwrap}
|
|
|
|
match?({:error, _reason}, authorize_sync_reads(filters, context)) ->
|
|
authorize_sync_reads(filters, context)
|
|
|
|
true ->
|
|
enforce_marmot_group_read_guardrails(filters)
|
|
end
|
|
end
|
|
|
|
@spec authorize_write(map(), MapSet.t(String.t())) :: :ok | {:error, policy_error()}
|
|
def authorize_write(event, authenticated_pubkeys) when is_map(event) do
|
|
authorize_write(event, authenticated_pubkeys, request_context(authenticated_pubkeys))
|
|
end
|
|
|
|
@spec authorize_write(map(), MapSet.t(String.t()), RequestContext.t()) ::
|
|
:ok | {:error, policy_error()}
|
|
def authorize_write(event, authenticated_pubkeys, %RequestContext{} = context)
|
|
when is_map(event) do
|
|
checks = [
|
|
fn -> ConnectionPolicy.authorize_authenticated_pubkeys(authenticated_pubkeys) end,
|
|
fn -> maybe_require_auth_for_write(authenticated_pubkeys) end,
|
|
fn -> authorize_sync_write(event, context) end,
|
|
fn -> reject_if_pubkey_banned(event) end,
|
|
fn -> reject_if_event_banned(event) end,
|
|
fn -> enforce_pow(event) end,
|
|
fn -> enforce_protected_event(event, authenticated_pubkeys) end,
|
|
fn -> enforce_media_metadata_policy(event) end,
|
|
fn -> enforce_push_notification_policy(event) end
|
|
]
|
|
|
|
Enum.reduce_while(checks, :ok, fn check, :ok ->
|
|
case check.() do
|
|
:ok -> {:cont, :ok}
|
|
{:error, _reason} = error -> {:halt, error}
|
|
end
|
|
end)
|
|
end
|
|
|
|
@spec error_message(policy_error()) :: String.t()
|
|
def error_message(:auth_required), do: "auth-required: authentication required"
|
|
def error_message(:pubkey_not_allowed), do: "restricted: authenticated pubkey is not allowed"
|
|
|
|
def error_message(:restricted_giftwrap),
|
|
do: "restricted: giftwrap access requires recipient authentication"
|
|
|
|
def error_message(:sync_read_not_allowed),
|
|
do: "restricted: sync read not allowed for authenticated pubkey"
|
|
|
|
def error_message(:sync_write_not_allowed),
|
|
do: "restricted: sync write not allowed for authenticated pubkey"
|
|
|
|
def error_message(:marmot_group_h_tag_required),
|
|
do: "restricted: kind 445 queries must include a #h tag"
|
|
|
|
def error_message(:marmot_group_h_values_exceeded),
|
|
do: "rate-limited: kind 445 queries exceed maximum #h values"
|
|
|
|
def error_message(:marmot_group_filter_window_too_wide),
|
|
do: "rate-limited: kind 445 query window exceeds configured maximum"
|
|
|
|
def error_message(:media_metadata_tags_exceeded),
|
|
do: "rate-limited: too many media metadata tags in event"
|
|
|
|
def error_message(:media_metadata_tag_value_too_large),
|
|
do: "invalid: media metadata field value exceeds configured limit"
|
|
|
|
def error_message(:media_metadata_url_too_long),
|
|
do: "invalid: media metadata url exceeds configured limit"
|
|
|
|
def error_message(:media_metadata_invalid_url),
|
|
do: "invalid: media metadata url must be a valid http/https URL"
|
|
|
|
def error_message(:media_metadata_invalid_hash),
|
|
do: "invalid: media metadata x field must be 32-byte lowercase hex"
|
|
|
|
def error_message(:media_metadata_invalid_mime),
|
|
do: "invalid: media metadata mime type is invalid"
|
|
|
|
def error_message(:media_metadata_mime_not_allowed),
|
|
do: "blocked: media metadata mime type is not allowed"
|
|
|
|
def error_message(:media_metadata_unsupported_version),
|
|
do: "blocked: media metadata version is not supported"
|
|
|
|
def error_message(:push_notification_relay_tags_exceeded),
|
|
do: "rate-limited: push relay list contains too many relay tags"
|
|
|
|
def error_message(:push_notification_payload_too_large),
|
|
do: "rate-limited: push notification payload exceeds configured size limit"
|
|
|
|
def error_message(:push_notification_replay_window_exceeded),
|
|
do: "restricted: push notification trigger is outside replay window"
|
|
|
|
def error_message(:push_notification_missing_expiration),
|
|
do: "invalid: push notification trigger requires an expiration tag"
|
|
|
|
def error_message(:push_notification_expiration_too_far),
|
|
do: "invalid: push notification expiration exceeds configured window"
|
|
|
|
def error_message(:push_notification_server_recipients_exceeded),
|
|
do: "rate-limited: push notification trigger targets too many notification servers"
|
|
|
|
def error_message(:protected_event_requires_auth),
|
|
do: "auth-required: protected events require authenticated pubkey"
|
|
|
|
def error_message(:protected_event_pubkey_mismatch),
|
|
do: "restricted: protected event pubkey does not match authenticated pubkey"
|
|
|
|
def error_message(:pow_below_minimum), do: "pow: minimum proof-of-work difficulty not met"
|
|
def error_message(:pubkey_banned), do: "blocked: pubkey is banned"
|
|
def error_message(:event_banned), do: "blocked: event is banned"
|
|
|
|
defp maybe_require_auth_for_write(authenticated_pubkeys) do
|
|
if config_bool([:policies, :auth_required_for_writes], false) and
|
|
MapSet.size(authenticated_pubkeys) == 0 do
|
|
{:error, :auth_required}
|
|
else
|
|
:ok
|
|
end
|
|
end
|
|
|
|
defp authorize_sync_reads(filters, %RequestContext{} = context) do
|
|
Enum.reduce_while(filters, :ok, fn filter, :ok ->
|
|
case ACL.check(:sync_read, filter, context: context) do
|
|
:ok -> {:cont, :ok}
|
|
{:error, reason} -> {:halt, {:error, reason}}
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp authorize_sync_write(event, %RequestContext{} = context) do
|
|
ACL.check(:sync_write, event, context: context)
|
|
end
|
|
|
|
defp giftwrap_restricted?(filters, authenticated_pubkeys) do
|
|
if MapSet.size(authenticated_pubkeys) == 0 do
|
|
any_filter_targets_giftwrap?(filters)
|
|
else
|
|
not giftwrap_filters_include_authenticated_recipient?(filters, authenticated_pubkeys)
|
|
end
|
|
end
|
|
|
|
defp any_filter_targets_giftwrap?(filters) do
|
|
Enum.any?(filters, fn filter ->
|
|
case Map.get(filter, "kinds") do
|
|
kinds when is_list(kinds) -> 1059 in kinds
|
|
_other -> false
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp giftwrap_filters_include_authenticated_recipient?(filters, authenticated_pubkeys) do
|
|
Enum.all?(filters, fn filter ->
|
|
if targets_giftwrap?(filter) do
|
|
recipients = Map.get(filter, "#p") || []
|
|
recipients != [] and Enum.any?(recipients, &MapSet.member?(authenticated_pubkeys, &1))
|
|
else
|
|
true
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp targets_giftwrap?(filter) do
|
|
case Map.get(filter, "kinds") do
|
|
kinds when is_list(kinds) -> 1059 in kinds
|
|
_other -> false
|
|
end
|
|
end
|
|
|
|
defp enforce_marmot_group_read_guardrails(filters) do
|
|
cond do
|
|
marmot_group_h_tag_required_violation?(filters) ->
|
|
{:error, :marmot_group_h_tag_required}
|
|
|
|
marmot_group_h_values_exceeded?(filters) ->
|
|
{:error, :marmot_group_h_values_exceeded}
|
|
|
|
marmot_group_query_window_too_wide?(filters) ->
|
|
{:error, :marmot_group_filter_window_too_wide}
|
|
|
|
true ->
|
|
:ok
|
|
end
|
|
end
|
|
|
|
defp marmot_group_h_tag_required_violation?(filters) do
|
|
config_bool([:policies, :marmot_require_h_for_group_queries], true) and
|
|
Enum.any?(filters, fn filter ->
|
|
targets_marmot_group_events?(filter) and not valid_h_tag_values?(Map.get(filter, "#h"))
|
|
end)
|
|
end
|
|
|
|
defp marmot_group_h_values_exceeded?(filters) do
|
|
max_h_values = config_int([:policies, :marmot_group_max_h_values_per_filter], 32)
|
|
|
|
max_h_values > 0 and
|
|
Enum.any?(filters, fn filter ->
|
|
targets_marmot_group_events?(filter) and h_tag_values_count(filter) > max_h_values
|
|
end)
|
|
end
|
|
|
|
defp marmot_group_query_window_too_wide?(filters) do
|
|
max_window = config_int([:policies, :marmot_group_max_query_window_seconds], 2_592_000)
|
|
|
|
max_window > 0 and
|
|
Enum.any?(filters, fn filter ->
|
|
if targets_marmot_group_events?(filter) do
|
|
query_window_exceeds?(filter, max_window)
|
|
else
|
|
false
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp targets_marmot_group_events?(filter) do
|
|
case Map.get(filter, "kinds") do
|
|
kinds when is_list(kinds) -> 445 in kinds
|
|
_other -> false
|
|
end
|
|
end
|
|
|
|
defp h_tag_values_count(filter) do
|
|
case Map.get(filter, "#h") do
|
|
values when is_list(values) -> length(values)
|
|
_other -> 0
|
|
end
|
|
end
|
|
|
|
defp valid_h_tag_values?(values) when is_list(values) do
|
|
values != [] and Enum.all?(values, &lowercase_hex?(&1, 32))
|
|
end
|
|
|
|
defp valid_h_tag_values?(_values), do: false
|
|
|
|
defp query_window_exceeds?(filter, max_window) do
|
|
case {Map.get(filter, "since"), Map.get(filter, "until")} do
|
|
{since, until}
|
|
when is_integer(since) and since >= 0 and is_integer(until) and until >= 0 and
|
|
until >= since ->
|
|
until - since > max_window
|
|
|
|
_other ->
|
|
false
|
|
end
|
|
end
|
|
|
|
defp lowercase_hex?(value, bytes) when is_binary(value) do
|
|
byte_size(value) == bytes * 2 and
|
|
match?({:ok, _decoded}, Base.decode16(value, case: :lower))
|
|
end
|
|
|
|
defp lowercase_hex?(_value, _bytes), do: false
|
|
|
|
defp enforce_media_metadata_policy(event) do
|
|
imeta_tags =
|
|
event
|
|
|> Map.get("tags", [])
|
|
|> Enum.filter(&imeta_tag?/1)
|
|
|
|
max_imeta_tags = config_int([:policies, :marmot_media_max_imeta_tags_per_event], 8)
|
|
|
|
if max_imeta_tags > 0 and length(imeta_tags) > max_imeta_tags do
|
|
{:error, :media_metadata_tags_exceeded}
|
|
else
|
|
validate_imeta_tags(imeta_tags)
|
|
end
|
|
end
|
|
|
|
defp imeta_tag?(["imeta" | _rest]), do: true
|
|
defp imeta_tag?(_tag), do: false
|
|
|
|
defp validate_imeta_tags(imeta_tags) do
|
|
Enum.reduce_while(imeta_tags, :ok, fn tag, :ok ->
|
|
with {:ok, fields} <- parse_imeta_tag(tag),
|
|
:ok <- validate_imeta_fields(fields) do
|
|
{:cont, :ok}
|
|
else
|
|
{:error, _reason} = error -> {:halt, error}
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp parse_imeta_tag(["imeta" | fields]) when is_list(fields) do
|
|
if fields != [] and rem(length(fields), 2) == 0 do
|
|
parsed_fields =
|
|
fields
|
|
|> Enum.chunk_every(2)
|
|
|> Enum.reduce(%{}, fn [key, value], acc -> Map.put(acc, key, value) end)
|
|
|
|
{:ok, parsed_fields}
|
|
else
|
|
{:error, :media_metadata_tag_value_too_large}
|
|
end
|
|
end
|
|
|
|
defp parse_imeta_tag(_tag), do: {:error, :media_metadata_tag_value_too_large}
|
|
|
|
defp validate_imeta_fields(fields) do
|
|
with :ok <- validate_imeta_value_sizes(fields),
|
|
:ok <- validate_imeta_url(fields),
|
|
:ok <- validate_imeta_hash(fields),
|
|
:ok <- validate_imeta_mime(fields) do
|
|
validate_imeta_version(fields)
|
|
end
|
|
end
|
|
|
|
defp validate_imeta_value_sizes(fields) do
|
|
max_value_bytes = config_int([:policies, :marmot_media_max_field_value_bytes], 1024)
|
|
|
|
if max_value_bytes <= 0 or Enum.all?(Map.values(fields), &(byte_size(&1) <= max_value_bytes)) do
|
|
:ok
|
|
else
|
|
{:error, :media_metadata_tag_value_too_large}
|
|
end
|
|
end
|
|
|
|
defp validate_imeta_url(fields) do
|
|
case Map.get(fields, "url") do
|
|
nil ->
|
|
:ok
|
|
|
|
url ->
|
|
max_url_bytes = config_int([:policies, :marmot_media_max_url_bytes], 2048)
|
|
|
|
cond do
|
|
max_url_bytes > 0 and byte_size(url) > max_url_bytes ->
|
|
{:error, :media_metadata_url_too_long}
|
|
|
|
valid_http_url?(url) ->
|
|
:ok
|
|
|
|
true ->
|
|
{:error, :media_metadata_invalid_url}
|
|
end
|
|
end
|
|
end
|
|
|
|
defp validate_imeta_hash(fields) do
|
|
case Map.get(fields, "x") do
|
|
nil ->
|
|
:ok
|
|
|
|
hash ->
|
|
if lowercase_hex?(hash, 32) do
|
|
:ok
|
|
else
|
|
{:error, :media_metadata_invalid_hash}
|
|
end
|
|
end
|
|
end
|
|
|
|
defp validate_imeta_mime(fields) do
|
|
case Map.get(fields, "m") do
|
|
nil ->
|
|
:ok
|
|
|
|
mime_type ->
|
|
allowed_prefixes = config_list([:policies, :marmot_media_allowed_mime_prefixes], [])
|
|
|
|
cond do
|
|
not valid_mime_type?(mime_type) ->
|
|
{:error, :media_metadata_invalid_mime}
|
|
|
|
allowed_prefixes == [] ->
|
|
:ok
|
|
|
|
Enum.any?(allowed_prefixes, &String.starts_with?(mime_type, &1)) ->
|
|
:ok
|
|
|
|
true ->
|
|
{:error, :media_metadata_mime_not_allowed}
|
|
end
|
|
end
|
|
end
|
|
|
|
defp validate_imeta_version(fields) do
|
|
case Map.get(fields, "v") do
|
|
nil ->
|
|
:ok
|
|
|
|
"mip04-v2" ->
|
|
:ok
|
|
|
|
"mip04-v1" ->
|
|
if config_bool([:policies, :marmot_media_reject_mip04_v1], true) do
|
|
{:error, :media_metadata_unsupported_version}
|
|
else
|
|
:ok
|
|
end
|
|
|
|
_other ->
|
|
{:error, :media_metadata_unsupported_version}
|
|
end
|
|
end
|
|
|
|
defp valid_http_url?(url) when is_binary(url) do
|
|
case URI.parse(url) do
|
|
%URI{scheme: scheme, host: host}
|
|
when scheme in ["http", "https"] and is_binary(host) and host != "" ->
|
|
true
|
|
|
|
_other ->
|
|
false
|
|
end
|
|
end
|
|
|
|
defp valid_http_url?(_url), do: false
|
|
|
|
defp valid_mime_type?(mime_type) when is_binary(mime_type) do
|
|
String.match?(mime_type, ~r/^[a-z0-9!#$&^_.+\-]+\/[a-z0-9!#$&^_.+\-]+$/)
|
|
end
|
|
|
|
defp valid_mime_type?(_mime_type), do: false
|
|
|
|
defp enforce_push_notification_policy(event) do
|
|
if config_bool([:features, :marmot_push_notifications], false) do
|
|
case Map.get(event, "kind") do
|
|
10_050 -> validate_push_relay_list_event(event)
|
|
1059 -> maybe_validate_push_trigger_event(event)
|
|
_other -> :ok
|
|
end
|
|
else
|
|
:ok
|
|
end
|
|
end
|
|
|
|
defp validate_push_relay_list_event(event) do
|
|
relay_tags =
|
|
event
|
|
|> Map.get("tags", [])
|
|
|> Enum.filter(fn
|
|
["relay", _url | _rest] -> true
|
|
_tag -> false
|
|
end)
|
|
|
|
max_relay_tags = config_int([:policies, :marmot_push_max_relay_tags], 16)
|
|
|
|
if max_relay_tags > 0 and length(relay_tags) > max_relay_tags do
|
|
{:error, :push_notification_relay_tags_exceeded}
|
|
else
|
|
:ok
|
|
end
|
|
end
|
|
|
|
defp maybe_validate_push_trigger_event(event) do
|
|
push_server_pubkeys = config_list([:policies, :marmot_push_server_pubkeys], [])
|
|
|
|
if targets_push_server?(event, push_server_pubkeys) do
|
|
with :ok <- validate_push_payload_size(event),
|
|
:ok <- validate_push_replay_window(event),
|
|
:ok <- validate_push_expiration(event) do
|
|
validate_push_server_recipient_count(event, push_server_pubkeys)
|
|
end
|
|
else
|
|
:ok
|
|
end
|
|
end
|
|
|
|
defp targets_push_server?(event, push_server_pubkeys) do
|
|
event
|
|
|> recipient_pubkeys()
|
|
|> Enum.any?(&(&1 in push_server_pubkeys))
|
|
end
|
|
|
|
defp validate_push_payload_size(event) do
|
|
max_payload_bytes = config_int([:policies, :marmot_push_max_payload_bytes], 65_536)
|
|
content = Map.get(event, "content", "")
|
|
|
|
if is_binary(content) and (max_payload_bytes <= 0 or byte_size(content) <= max_payload_bytes) do
|
|
:ok
|
|
else
|
|
{:error, :push_notification_payload_too_large}
|
|
end
|
|
end
|
|
|
|
defp validate_push_replay_window(event) do
|
|
max_age_seconds = config_int([:policies, :marmot_push_max_trigger_age_seconds], 120)
|
|
created_at = Map.get(event, "created_at")
|
|
now = System.system_time(:second)
|
|
|
|
if max_age_seconds <= 0 or
|
|
(is_integer(created_at) and created_at >= 0 and now - created_at <= max_age_seconds) do
|
|
:ok
|
|
else
|
|
{:error, :push_notification_replay_window_exceeded}
|
|
end
|
|
end
|
|
|
|
defp validate_push_expiration(event) do
|
|
require_expiration? = config_bool([:policies, :marmot_push_require_expiration], true)
|
|
|
|
case expiration_tag_value(event) do
|
|
nil ->
|
|
if require_expiration?, do: {:error, :push_notification_missing_expiration}, else: :ok
|
|
|
|
expiration when is_integer(expiration) ->
|
|
max_expiration_window =
|
|
config_int([:policies, :marmot_push_max_expiration_window_seconds], 120)
|
|
|
|
created_at = Map.get(event, "created_at")
|
|
|
|
if max_expiration_window <= 0 or
|
|
(is_integer(created_at) and expiration >= created_at and
|
|
expiration - created_at <= max_expiration_window) do
|
|
:ok
|
|
else
|
|
{:error, :push_notification_expiration_too_far}
|
|
end
|
|
end
|
|
end
|
|
|
|
defp validate_push_server_recipient_count(event, push_server_pubkeys) do
|
|
max_server_recipients = config_int([:policies, :marmot_push_max_server_recipients], 1)
|
|
|
|
target_count =
|
|
event
|
|
|> recipient_pubkeys()
|
|
|> Enum.count(&(&1 in push_server_pubkeys))
|
|
|
|
if max_server_recipients > 0 and target_count > max_server_recipients do
|
|
{:error, :push_notification_server_recipients_exceeded}
|
|
else
|
|
:ok
|
|
end
|
|
end
|
|
|
|
defp recipient_pubkeys(event) do
|
|
event
|
|
|> Map.get("tags", [])
|
|
|> Enum.reduce([], fn
|
|
["p", recipient | _rest], acc -> [recipient | acc]
|
|
_tag, acc -> acc
|
|
end)
|
|
end
|
|
|
|
defp expiration_tag_value(event) do
|
|
event
|
|
|> Map.get("tags", [])
|
|
|> Enum.find_value(fn
|
|
["expiration", unix_seconds | _rest] -> parse_unix_seconds(unix_seconds)
|
|
_tag -> nil
|
|
end)
|
|
end
|
|
|
|
defp parse_unix_seconds(unix_seconds) when is_binary(unix_seconds) do
|
|
case Integer.parse(unix_seconds) do
|
|
{parsed, ""} when parsed >= 0 -> parsed
|
|
_other -> nil
|
|
end
|
|
end
|
|
|
|
defp parse_unix_seconds(_unix_seconds), do: nil
|
|
|
|
defp reject_if_pubkey_banned(event) do
|
|
with pubkey when is_binary(pubkey) <- Map.get(event, "pubkey"),
|
|
{:ok, true} <- Storage.moderation().pubkey_banned?(%{}, pubkey) do
|
|
{:error, :pubkey_banned}
|
|
else
|
|
{:ok, false} -> :ok
|
|
_other -> :ok
|
|
end
|
|
end
|
|
|
|
defp reject_if_event_banned(event) do
|
|
with event_id when is_binary(event_id) <- Map.get(event, "id"),
|
|
{:ok, true} <- Storage.moderation().event_banned?(%{}, event_id) do
|
|
{:error, :event_banned}
|
|
else
|
|
{:ok, false} -> :ok
|
|
_other -> :ok
|
|
end
|
|
end
|
|
|
|
defp enforce_pow(event) do
|
|
min_difficulty = config_int([:policies, :min_pow_difficulty], 0)
|
|
|
|
if min_difficulty <= 0 do
|
|
:ok
|
|
else
|
|
difficulty = event_pow_difficulty(event)
|
|
|
|
if difficulty >= min_difficulty do
|
|
:ok
|
|
else
|
|
{:error, :pow_below_minimum}
|
|
end
|
|
end
|
|
end
|
|
|
|
defp event_pow_difficulty(event) do
|
|
event
|
|
|> Map.get("id", "")
|
|
|> String.downcase()
|
|
|> String.graphemes()
|
|
|> Enum.reduce_while(0, fn
|
|
"0", acc -> {:cont, acc + 4}
|
|
hex, acc -> {:halt, acc + leading_zero_bits(hex)}
|
|
end)
|
|
end
|
|
|
|
defp leading_zero_bits("1"), do: 3
|
|
defp leading_zero_bits("2"), do: 2
|
|
defp leading_zero_bits("3"), do: 2
|
|
defp leading_zero_bits("4"), do: 1
|
|
defp leading_zero_bits("5"), do: 1
|
|
defp leading_zero_bits("6"), do: 1
|
|
defp leading_zero_bits("7"), do: 1
|
|
defp leading_zero_bits("8"), do: 0
|
|
defp leading_zero_bits("9"), do: 0
|
|
defp leading_zero_bits("a"), do: 0
|
|
defp leading_zero_bits("b"), do: 0
|
|
defp leading_zero_bits("c"), do: 0
|
|
defp leading_zero_bits("d"), do: 0
|
|
defp leading_zero_bits("e"), do: 0
|
|
defp leading_zero_bits("f"), do: 0
|
|
defp leading_zero_bits(_other), do: 0
|
|
|
|
defp enforce_protected_event(event, authenticated_pubkeys) do
|
|
protected? =
|
|
event
|
|
|> Map.get("tags", [])
|
|
|> Enum.any?(fn
|
|
["-" | _rest] -> true
|
|
_tag -> false
|
|
end)
|
|
|
|
cond do
|
|
not protected? ->
|
|
:ok
|
|
|
|
nip43_relay_access_kind?(Map.get(event, "kind")) ->
|
|
:ok
|
|
|
|
true ->
|
|
pubkey = Map.get(event, "pubkey")
|
|
|
|
cond do
|
|
MapSet.size(authenticated_pubkeys) == 0 -> {:error, :protected_event_requires_auth}
|
|
MapSet.member?(authenticated_pubkeys, pubkey) -> :ok
|
|
true -> {:error, :protected_event_pubkey_mismatch}
|
|
end
|
|
end
|
|
end
|
|
|
|
defp nip43_relay_access_kind?(kind) when kind in [8_000, 8_001, 13_534, 28_934, 28_935, 28_936],
|
|
do: true
|
|
|
|
defp nip43_relay_access_kind?(_kind), do: false
|
|
|
|
defp config_bool([scope, key], default) do
|
|
case Application.get_env(:parrhesia, scope, []) |> Keyword.get(key, default) do
|
|
true -> true
|
|
false -> false
|
|
_other -> default
|
|
end
|
|
end
|
|
|
|
defp config_int([scope, key], default) do
|
|
case Application.get_env(:parrhesia, scope, []) |> Keyword.get(key, default) do
|
|
value when is_integer(value) -> value
|
|
_other -> default
|
|
end
|
|
end
|
|
|
|
defp config_list([scope, key], default) do
|
|
case Application.get_env(:parrhesia, scope, []) |> Keyword.get(key, default) do
|
|
value when is_list(value) ->
|
|
if Enum.all?(value, &is_binary/1), do: value, else: default
|
|
|
|
_other ->
|
|
default
|
|
end
|
|
end
|
|
|
|
defp request_context(authenticated_pubkeys) do
|
|
%RequestContext{authenticated_pubkeys: authenticated_pubkeys}
|
|
end
|
|
end
|