Unify HTTP metadata handling
This commit is contained in:
@@ -135,7 +135,7 @@ In `prod`, these environment variables are used:
|
||||
- `DATABASE_URL` (**required**), e.g. `ecto://USER:PASS@HOST/parrhesia_prod`
|
||||
- `POOL_SIZE` (optional, default `32`)
|
||||
- `PORT` (optional, default `4413`)
|
||||
- `PARRHESIA_*` runtime overrides for relay config, identity, sync, ACL, limits, policies, listeners, retention, and features
|
||||
- `PARRHESIA_*` runtime overrides for relay config, metadata, identity, sync, ACL, limits, policies, listeners, retention, and features
|
||||
- `PARRHESIA_EXTRA_CONFIG` (optional path to an extra runtime config file)
|
||||
|
||||
`config/runtime.exs` reads these values at runtime in production releases.
|
||||
@@ -145,6 +145,7 @@ In `prod`, these environment variables are used:
|
||||
For runtime overrides, use the `PARRHESIA_...` prefix:
|
||||
|
||||
- `PARRHESIA_RELAY_URL`
|
||||
- `PARRHESIA_METADATA_HIDE_VERSION`
|
||||
- `PARRHESIA_IDENTITY_*`
|
||||
- `PARRHESIA_SYNC_*`
|
||||
- `PARRHESIA_ACL_*`
|
||||
@@ -181,6 +182,7 @@ CSV env vars use comma-separated values. Boolean env vars accept `1/0`, `true/fa
|
||||
| Atom key | ENV | Default | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `:relay_url` | `PARRHESIA_RELAY_URL` | `ws://localhost:4413/relay` | Advertised relay URL and auth relay tag target |
|
||||
| `:metadata.hide_version?` | `PARRHESIA_METADATA_HIDE_VERSION` | `true` | Hides the relay version from outbound `User-Agent` and NIP-11 when enabled |
|
||||
| `:acl.protected_filters` | `PARRHESIA_ACL_PROTECTED_FILTERS` | `[]` | JSON-encoded protected filter list for sync ACL checks |
|
||||
| `:identity.path` | `PARRHESIA_IDENTITY_PATH` | `nil` | Optional path for persisted relay identity material |
|
||||
| `:identity.private_key` | `PARRHESIA_IDENTITY_PRIVATE_KEY` | `nil` | Optional inline relay private key |
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
import Config
|
||||
|
||||
project_version =
|
||||
case Mix.Project.config()[:version] do
|
||||
version when is_binary(version) -> version
|
||||
version -> to_string(version)
|
||||
end
|
||||
|
||||
config :postgrex, :json_library, JSON
|
||||
|
||||
config :parrhesia,
|
||||
metadata: [
|
||||
name: "Parrhesia",
|
||||
version: project_version,
|
||||
hide_version?: true
|
||||
],
|
||||
database: [
|
||||
separate_read_pool?: config_env() != :test
|
||||
],
|
||||
|
||||
@@ -132,6 +132,7 @@ if config_env() == :prod do
|
||||
repo_defaults = Application.get_env(:parrhesia, Parrhesia.Repo, [])
|
||||
read_repo_defaults = Application.get_env(:parrhesia, Parrhesia.ReadRepo, [])
|
||||
relay_url_default = Application.get_env(:parrhesia, :relay_url)
|
||||
metadata_defaults = Application.get_env(:parrhesia, :metadata, [])
|
||||
|
||||
moderation_cache_enabled_default =
|
||||
Application.get_env(:parrhesia, :moderation_cache_enabled, true)
|
||||
@@ -646,6 +647,15 @@ if config_env() == :prod do
|
||||
|
||||
config :parrhesia,
|
||||
relay_url: string_env.("PARRHESIA_RELAY_URL", relay_url_default),
|
||||
metadata: [
|
||||
name: Keyword.get(metadata_defaults, :name, "Parrhesia"),
|
||||
version: Keyword.get(metadata_defaults, :version, "0.0.0"),
|
||||
hide_version?:
|
||||
bool_env.(
|
||||
"PARRHESIA_METADATA_HIDE_VERSION",
|
||||
Keyword.get(metadata_defaults, :hide_version?, true)
|
||||
)
|
||||
],
|
||||
acl: [
|
||||
protected_filters:
|
||||
json_env.(
|
||||
|
||||
48
lib/parrhesia/http.ex
Normal file
48
lib/parrhesia/http.ex
Normal file
@@ -0,0 +1,48 @@
|
||||
defmodule Parrhesia.HTTP do
|
||||
@moduledoc false
|
||||
|
||||
alias Parrhesia.Metadata
|
||||
|
||||
@default_headers [{"user-agent", Metadata.user_agent()}]
|
||||
|
||||
@spec default_headers() :: [{String.t(), String.t()}]
|
||||
def default_headers, do: @default_headers
|
||||
|
||||
@spec get(Keyword.t()) :: {:ok, Req.Response.t()} | {:error, Exception.t()}
|
||||
def get(options) when is_list(options) do
|
||||
Req.get(put_default_headers(options))
|
||||
end
|
||||
|
||||
@spec post(Keyword.t()) :: {:ok, Req.Response.t()} | {:error, Exception.t()}
|
||||
def post(options) when is_list(options) do
|
||||
Req.post(put_default_headers(options))
|
||||
end
|
||||
|
||||
@spec put_default_headers(Keyword.t()) :: Keyword.t()
|
||||
def put_default_headers(options) when is_list(options) do
|
||||
Keyword.update(options, :headers, @default_headers, &merge_headers(&1, @default_headers))
|
||||
end
|
||||
|
||||
defp merge_headers(headers, defaults) do
|
||||
existing_names =
|
||||
headers
|
||||
|> List.wrap()
|
||||
|> Enum.reduce(MapSet.new(), fn
|
||||
{name, _value}, acc -> MapSet.put(acc, normalize_header_name(name))
|
||||
_other, acc -> acc
|
||||
end)
|
||||
|
||||
headers ++
|
||||
Enum.reject(defaults, fn {name, _value} ->
|
||||
MapSet.member?(existing_names, normalize_header_name(name))
|
||||
end)
|
||||
end
|
||||
|
||||
defp normalize_header_name(name) when is_atom(name) do
|
||||
name
|
||||
|> Atom.to_string()
|
||||
|> String.downcase()
|
||||
end
|
||||
|
||||
defp normalize_header_name(name) when is_binary(name), do: String.downcase(name)
|
||||
end
|
||||
29
lib/parrhesia/metadata.ex
Normal file
29
lib/parrhesia/metadata.ex
Normal file
@@ -0,0 +1,29 @@
|
||||
defmodule Parrhesia.Metadata do
|
||||
@moduledoc false
|
||||
|
||||
@metadata Application.compile_env(:parrhesia, :metadata, [])
|
||||
@name Keyword.get(@metadata, :name, "Parrhesia")
|
||||
@version Keyword.get(@metadata, :version, "0.0.0")
|
||||
@hide_version? Keyword.get(@metadata, :hide_version?, true)
|
||||
|
||||
@spec name() :: String.t()
|
||||
def name, do: @name
|
||||
|
||||
@spec version() :: String.t()
|
||||
def version, do: @version
|
||||
|
||||
@spec hide_version?() :: boolean()
|
||||
def hide_version?, do: @hide_version?
|
||||
|
||||
@spec name_and_version() :: String.t()
|
||||
def name_and_version, do: "#{@name}/#{@version}"
|
||||
|
||||
@spec user_agent() :: String.t()
|
||||
def user_agent do
|
||||
if hide_version?() do
|
||||
name()
|
||||
else
|
||||
name_and_version()
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,7 @@
|
||||
defmodule Parrhesia.NIP66.Probe do
|
||||
@moduledoc false
|
||||
|
||||
alias Parrhesia.HTTP
|
||||
alias Parrhesia.Sync.Transport.WebSockexClient
|
||||
|
||||
@type result :: %{
|
||||
@@ -145,7 +146,7 @@ defmodule Parrhesia.NIP66.Probe do
|
||||
defp fetch_nip11(relay_url, timeout_ms) do
|
||||
started_at = System.monotonic_time()
|
||||
|
||||
case Req.get(
|
||||
case HTTP.get(
|
||||
url: relay_info_url(relay_url),
|
||||
headers: [{"accept", "application/nostr+json"}],
|
||||
decode_body: false,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
defmodule Parrhesia.Sync.RelayInfoClient do
|
||||
@moduledoc false
|
||||
|
||||
alias Parrhesia.HTTP
|
||||
alias Parrhesia.Sync.TLS
|
||||
|
||||
@spec verify_remote_identity(map(), keyword()) :: :ok | {:error, term()}
|
||||
@@ -18,7 +19,7 @@ defmodule Parrhesia.Sync.RelayInfoClient do
|
||||
end
|
||||
|
||||
defp default_request(url, opts) do
|
||||
case Req.get(
|
||||
case HTTP.get(
|
||||
url: url,
|
||||
headers: [{"accept", "application/nostr+json"}],
|
||||
decode_body: false,
|
||||
|
||||
@@ -4,21 +4,27 @@ defmodule Parrhesia.Web.RelayInfo do
|
||||
"""
|
||||
|
||||
alias Parrhesia.API.Identity
|
||||
alias Parrhesia.Metadata
|
||||
alias Parrhesia.NIP43
|
||||
alias Parrhesia.Web.Listener
|
||||
|
||||
@spec document(Listener.t()) :: map()
|
||||
def document(listener) do
|
||||
%{
|
||||
"name" => "Parrhesia",
|
||||
document = %{
|
||||
"name" => Metadata.name(),
|
||||
"description" => "Nostr/Marmot relay",
|
||||
"pubkey" => relay_pubkey(),
|
||||
"self" => relay_pubkey(),
|
||||
"supported_nips" => supported_nips(),
|
||||
"software" => "https://git.teralink.net/self/parrhesia",
|
||||
"version" => Application.spec(:parrhesia, :vsn) |> to_string(),
|
||||
"limitation" => limitations(listener)
|
||||
}
|
||||
|
||||
if Metadata.hide_version?() do
|
||||
document
|
||||
else
|
||||
Map.put(document, "version", Metadata.version())
|
||||
end
|
||||
end
|
||||
|
||||
defp supported_nips do
|
||||
|
||||
@@ -4,6 +4,9 @@ defmodule Parrhesia.ConfigTest do
|
||||
alias Parrhesia.Web.Listener
|
||||
|
||||
test "returns configured relay limits/policies/features" do
|
||||
assert Parrhesia.Config.get([:metadata, :name]) == "Parrhesia"
|
||||
assert Parrhesia.Config.get([:metadata, :version]) == "0.5.0"
|
||||
assert Parrhesia.Config.get([:metadata, :hide_version?]) == true
|
||||
assert Parrhesia.Config.get([:limits, :max_frame_bytes]) == 1_048_576
|
||||
assert Parrhesia.Config.get([:limits, :max_event_bytes]) == 262_144
|
||||
assert Parrhesia.Config.get([:limits, :max_event_future_skew_seconds]) == 900
|
||||
|
||||
32
test/parrhesia/http_test.exs
Normal file
32
test/parrhesia/http_test.exs
Normal file
@@ -0,0 +1,32 @@
|
||||
defmodule Parrhesia.HTTPTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Parrhesia.HTTP
|
||||
alias Parrhesia.Metadata
|
||||
|
||||
test "default headers advertise the configured user agent" do
|
||||
assert Metadata.hide_version?() == true
|
||||
assert HTTP.default_headers() == [{"user-agent", Metadata.user_agent()}]
|
||||
end
|
||||
|
||||
test "default headers are added without overriding request-specific headers" do
|
||||
options =
|
||||
HTTP.put_default_headers(
|
||||
headers: [{"accept", "application/nostr+json"}],
|
||||
decode_body: false
|
||||
)
|
||||
|
||||
assert Keyword.get(options, :headers) == [
|
||||
{"accept", "application/nostr+json"},
|
||||
{"user-agent", Metadata.user_agent()}
|
||||
]
|
||||
|
||||
assert Keyword.get(options, :decode_body) == false
|
||||
end
|
||||
|
||||
test "explicit user-agent overrides suppress the default case-insensitively" do
|
||||
options = HTTP.put_default_headers(headers: [{"User-Agent", "custom-agent"}])
|
||||
|
||||
assert Keyword.get(options, :headers) == [{"User-Agent", "custom-agent"}]
|
||||
end
|
||||
end
|
||||
17
test/parrhesia/web/relay_info_test.exs
Normal file
17
test/parrhesia/web/relay_info_test.exs
Normal file
@@ -0,0 +1,17 @@
|
||||
defmodule Parrhesia.Web.RelayInfoTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Parrhesia.Web.Listener
|
||||
alias Parrhesia.Web.RelayInfo
|
||||
|
||||
test "nip-11 omits version when metadata hides it" do
|
||||
document =
|
||||
:parrhesia
|
||||
|> Application.get_env(:listeners, %{})
|
||||
|> Keyword.fetch!(:public)
|
||||
|> then(&Listener.from_opts(listener: &1))
|
||||
|> RelayInfo.document()
|
||||
|
||||
refute Map.has_key?(document, "version")
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user