Unify HTTP metadata handling

This commit is contained in:
2026-03-18 18:00:07 +01:00
parent c30449b318
commit 9014912e9d
11 changed files with 166 additions and 6 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View 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