feat(relay): TOML config file (--config /path/to/relay.toml)
Adds operator-facing TOML configuration to :quartz-relay, with the
section layout deliberately mirroring nostr-rs-relay's config.toml so
existing operators can port across with little churn.
Sections parsed AND enforced today:
[info] — NIP-11 doc fields (name, description, contact, pubkey,
software, supported_nips, …); replaces the previous
hardcoded RelayInfo.default()
[network] — host, port, path
[database] — in_memory toggle + file path
[options] — verify_signatures, require_auth (compose to the right
IRelayPolicy stack)
Sections parsed today but NOT YET ENFORCED (forward-compat for the
upcoming rate-limit / authorization work — relay logs a warning when
they're set):
[limits] — max_event_bytes, messages_per_sec, …
[authorization] — pubkey_whitelist/blacklist, kind_whitelist/blacklist
[options].reject_future_seconds
[network].remote_ip_header
CLI flag precedence over the config file is preserved: --host, --port,
--path, --info, --db, --auth, --verify all override the matching field.
Adds:
- cc.ekblad:4koma 1.2.0 for TOML parsing
- quartz-relay/config.example.toml as the canonical operator reference
- 5 unit tests (defaults, full parse, NIP-11 mapping, bundled example
file, optional sections)
This commit is contained in:
@@ -82,6 +82,7 @@ core = "1.7.0"
|
||||
mavenPublish = "0.36.0"
|
||||
sqlite = "2.6.2"
|
||||
ktor = "3.4.1"
|
||||
fourkoma = "1.2.0"
|
||||
|
||||
[libraries]
|
||||
abedElazizShe-video-compressor-fork = { group = "com.github.davotoula", name = "LightCompressor-enhanced", version.ref = "lightcompressor-enhanced" }
|
||||
@@ -179,6 +180,7 @@ okhttpCoroutines = { group = "com.squareup.okhttp3", name = "okhttp-coroutines",
|
||||
ktor-server-core = { group = "io.ktor", name = "ktor-server-core", version.ref = "ktor" }
|
||||
ktor-server-cio = { group = "io.ktor", name = "ktor-server-cio", version.ref = "ktor" }
|
||||
ktor-server-websockets = { group = "io.ktor", name = "ktor-server-websockets", version.ref = "ktor" }
|
||||
fourkoma = { module = "cc.ekblad:4koma", version.ref = "fourkoma" }
|
||||
secp256k1-kmp-common = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp", version.ref = "secp256k1KmpJniAndroid" }
|
||||
secp256k1-kmp-jni-android = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-android", version.ref = "secp256k1KmpJniAndroid" }
|
||||
secp256k1-kmp-jni-jvm = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-jvm", version.ref = "secp256k1KmpJniAndroid" }
|
||||
|
||||
@@ -42,6 +42,11 @@ dependencies {
|
||||
api(libs.ktor.server.cio)
|
||||
api(libs.ktor.server.websockets)
|
||||
|
||||
// TOML parsing for the operator config file. Mirrors the section
|
||||
// layout of nostr-rs-relay's config.toml so existing operators can
|
||||
// port their configs nearly verbatim.
|
||||
implementation(libs.fourkoma)
|
||||
|
||||
testImplementation(libs.kotlin.test)
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
testImplementation(libs.secp256k1.kmp.jni.jvm)
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# Example config for quartz-relay. Section layout mirrors
|
||||
# nostr-rs-relay's config.toml so existing operators can port across.
|
||||
#
|
||||
# Run with:
|
||||
# ./gradlew :quartz-relay:run --args="--config /etc/quartz-relay.toml"
|
||||
#
|
||||
# CLI flags override individual values: e.g. `--port 8888` wins over
|
||||
# `[network].port`.
|
||||
|
||||
[info]
|
||||
# The wss:// URL clients use to reach this relay (mandatory for NIP-42
|
||||
# AUTH challenges). If not set, the relay synthesises one from the
|
||||
# [network] section.
|
||||
relay_url = "wss://relay.example.com/"
|
||||
name = "Example Quartz Relay"
|
||||
description = "A quartz-relay deployment."
|
||||
contact = "admin@example.com"
|
||||
# Operator pubkey (NIP-11). Optional.
|
||||
# pubkey = "..."
|
||||
# Override the supported NIPs advertised on the NIP-11 endpoint. If
|
||||
# omitted, the relay advertises the NIPs it actually implements.
|
||||
# supported_nips = [1, 9, 11, 40, 42, 45, 50, 62]
|
||||
|
||||
[network]
|
||||
host = "0.0.0.0"
|
||||
port = 7447
|
||||
path = "/"
|
||||
# Set when behind a reverse proxy (nginx/Caddy/Cloudflare). Required
|
||||
# before any IP-based rate limit means anything. Parsed today, enforced
|
||||
# once rate limits land.
|
||||
# remote_ip_header = "X-Forwarded-For"
|
||||
|
||||
[database]
|
||||
# True keeps an in-memory SQLite db (events vanish on restart). Useful
|
||||
# for tests; set false + `file = "..."` for persistent storage.
|
||||
in_memory = false
|
||||
file = "/var/lib/quartz-relay/events.db"
|
||||
|
||||
[options]
|
||||
# Drop events whose Schnorr signature does not verify. Strongly
|
||||
# recommended for any relay accepting traffic from real clients.
|
||||
verify_signatures = true
|
||||
# Require clients to NIP-42 AUTH before REQ/EVENT/COUNT.
|
||||
require_auth = false
|
||||
# Reject events whose `created_at` is more than this many seconds in the
|
||||
# future. Parsed today, enforced once the matching policy lands.
|
||||
# reject_future_seconds = 1800
|
||||
|
||||
# --- Sections below are parsed today but NOT YET ENFORCED. They are
|
||||
# accepted for forward compatibility — the matching enforcement code is
|
||||
# tracked separately. The relay logs a warning for each used section. ---
|
||||
|
||||
[limits]
|
||||
# max_event_bytes = 131072
|
||||
# max_ws_message_bytes = 1048576
|
||||
# max_ws_frame_bytes = 1048576
|
||||
# messages_per_sec = 10
|
||||
# subscriptions_per_min = 60
|
||||
# max_subscriptions_per_session = 32
|
||||
# max_filters_per_req = 10
|
||||
|
||||
[authorization]
|
||||
# pubkey_whitelist = []
|
||||
# pubkey_blacklist = []
|
||||
# kind_whitelist = []
|
||||
# kind_blacklist = []
|
||||
@@ -27,40 +27,65 @@ import com.vitorpamplona.quartz.nip01Core.relay.server.policies.FullAuthPolicy
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.server.policies.VerifyPolicy
|
||||
import com.vitorpamplona.quartz.nip01Core.store.IEventStore
|
||||
import com.vitorpamplona.quartz.nip01Core.store.sqlite.EventStore
|
||||
import com.vitorpamplona.quartz.relay.config.RelayConfig
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Standalone entry point. Run with `./gradlew :quartz-relay:run` (when the
|
||||
* application plugin is configured) or `java -cp ... Main`.
|
||||
* Standalone entry point.
|
||||
*
|
||||
* Usage:
|
||||
* --host <addr> bind address (default 0.0.0.0)
|
||||
* --port <n> tcp port (default 7447, 0 to autobind)
|
||||
* --path <p> ws path (default /)
|
||||
* --info <file> NIP-11 doc file (default: built-in)
|
||||
* --db <file> sqlite db path (default: in-memory)
|
||||
* --auth require NIP-42 AUTH for REQ/EVENT/COUNT
|
||||
* --verify verify event signatures (recommended for any
|
||||
* relay accepting traffic from real clients)
|
||||
* Run with:
|
||||
* ./gradlew :quartz-relay:run --args="--config /etc/quartz-relay.toml"
|
||||
* or
|
||||
* java -cp ... com.vitorpamplona.quartz.relay.MainKt --port 7447 --verify
|
||||
*
|
||||
* Configuration precedence (highest to lowest):
|
||||
* 1. CLI flags (`--host`, `--port`, …)
|
||||
* 2. TOML file passed via `--config <path>`
|
||||
* 3. Built-in defaults (host=0.0.0.0, port=7447, in-memory db, …)
|
||||
*
|
||||
* Sections currently parsed AND enforced: `[info]`, `[network]`,
|
||||
* `[database]`, `[options]`. Sections parsed but not yet enforced
|
||||
* (forward-compat for the rate-limit / authorization work):
|
||||
* `[limits]`, `[authorization]`.
|
||||
*
|
||||
* CLI flags:
|
||||
* --config <file> TOML config (see config.example.toml)
|
||||
* --host <addr> bind address (default from config or 0.0.0.0)
|
||||
* --port <n> tcp port (default from config or 7447, 0 to autobind)
|
||||
* --path <p> ws path (default from config or /)
|
||||
* --info <file> NIP-11 doc file (overrides [info] section)
|
||||
* --db <file> sqlite db path (overrides [database].file)
|
||||
* --auth require NIP-42 AUTH (sets options.require_auth = true)
|
||||
* --verify verify event signatures (sets options.verify_signatures = true)
|
||||
*/
|
||||
fun main(args: Array<String>) {
|
||||
val a = parseArgs(args)
|
||||
val host = a.opt("--host") ?: "0.0.0.0"
|
||||
val port = a.opt("--port")?.toInt() ?: 7447
|
||||
val path = a.opt("--path") ?: "/"
|
||||
val infoFile = a.opt("--info")?.let { File(it) }
|
||||
val dbFile = a.opt("--db")
|
||||
val requireAuth = a.flag("--auth")
|
||||
val verifySigs = a.flag("--verify")
|
||||
|
||||
val urlStr = "ws://$host:$port$path"
|
||||
// For binding 0.0.0.0 we still want to scope the relay to a "public" url
|
||||
// shape for NIP-42 challenge validation; use the host the operator
|
||||
// exposes (--info usually carries the public URL). Fall back to
|
||||
// 127.0.0.1 so localhost smoke tests work.
|
||||
val advertisedUrl = (if (host == "0.0.0.0") "ws://127.0.0.1:$port$path" else urlStr).normalizeRelayUrl()
|
||||
val config: RelayConfig =
|
||||
a
|
||||
.opt("--config")
|
||||
?.let { RelayConfig.fromFile(File(it)) }
|
||||
?: RelayConfig()
|
||||
|
||||
val info = infoFile?.let { RelayInfo.fromFile(it) } ?: RelayInfo.default(advertisedUrl)
|
||||
val host = a.opt("--host") ?: config.network.host
|
||||
val port = a.opt("--port")?.toInt() ?: config.network.port
|
||||
val path = a.opt("--path") ?: config.network.path
|
||||
|
||||
val cliInfoFile = a.opt("--info")?.let { File(it) }
|
||||
val dbFile = a.opt("--db") ?: config.database.file?.takeUnless { config.database.in_memory }
|
||||
val requireAuth = a.flag("--auth") || config.options.require_auth
|
||||
val verifySigs = a.flag("--verify") || config.options.verify_signatures
|
||||
|
||||
// Advertised URL: explicit `info.relay_url` wins, then build from
|
||||
// host/port/path. 0.0.0.0 bind → 127.0.0.1 in the URL so NIP-42
|
||||
// challenges are well-formed.
|
||||
val advertisedHost = if (host == "0.0.0.0") "127.0.0.1" else host
|
||||
val advertisedUrl =
|
||||
(config.info.relay_url ?: "ws://$advertisedHost:$port$path").normalizeRelayUrl()
|
||||
|
||||
val info =
|
||||
cliInfoFile?.let { RelayInfo.fromFile(it) }
|
||||
?: config.resolveInfo(advertisedUrl)
|
||||
|
||||
val store: IEventStore = EventStore(dbName = dbFile, relay = advertisedUrl)
|
||||
|
||||
@@ -72,6 +97,8 @@ fun main(args: Array<String>) {
|
||||
else -> { -> EmptyPolicy }
|
||||
}
|
||||
|
||||
warnUnenforcedSections(config)
|
||||
|
||||
val relay = Relay(advertisedUrl, store, info, policyBuilder)
|
||||
val server = LocalRelayServer(relay, host = host, port = port, path = path).start()
|
||||
|
||||
@@ -83,12 +110,43 @@ fun main(args: Array<String>) {
|
||||
)
|
||||
|
||||
println("quartz-relay listening on ${server.url}")
|
||||
println("NIP-11 info doc: curl -H 'Accept: application/nostr+json' http://$host:$port$path")
|
||||
println("NIP-11 info doc: curl -H 'Accept: application/nostr+json' http://$advertisedHost:$port$path")
|
||||
|
||||
// Park the main thread; shutdown hook handles teardown.
|
||||
Thread.currentThread().join()
|
||||
}
|
||||
|
||||
/** Surface a warning when the operator has set sections we don't yet enforce. */
|
||||
private fun warnUnenforcedSections(config: RelayConfig) {
|
||||
val warnings = mutableListOf<String>()
|
||||
val l = config.limits
|
||||
if (l.max_event_bytes != null ||
|
||||
l.max_ws_message_bytes != null ||
|
||||
l.max_ws_frame_bytes != null ||
|
||||
l.messages_per_sec != null ||
|
||||
l.subscriptions_per_min != null ||
|
||||
l.max_subscriptions_per_session != null ||
|
||||
l.max_filters_per_req != null
|
||||
) {
|
||||
warnings += "[limits] section is parsed but NOT YET ENFORCED — rate limits / message size caps are pending."
|
||||
}
|
||||
val auth = config.authorization
|
||||
if (auth.pubkey_whitelist.isNotEmpty() ||
|
||||
auth.pubkey_blacklist.isNotEmpty() ||
|
||||
auth.kind_whitelist.isNotEmpty() ||
|
||||
auth.kind_blacklist.isNotEmpty()
|
||||
) {
|
||||
warnings += "[authorization] section is parsed but NOT YET ENFORCED — pubkey/kind allow-deny lists are pending."
|
||||
}
|
||||
if (config.options.reject_future_seconds != null) {
|
||||
warnings += "[options].reject_future_seconds is parsed but NOT YET ENFORCED."
|
||||
}
|
||||
if (config.network.remote_ip_header != null) {
|
||||
warnings += "[network].remote_ip_header is parsed but NOT YET ENFORCED — IP-based limits are pending."
|
||||
}
|
||||
warnings.forEach { System.err.println("warning: $it") }
|
||||
}
|
||||
|
||||
private class Args(
|
||||
private val opts: Map<String, String>,
|
||||
private val flags: Set<String>,
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.relay.config
|
||||
|
||||
import cc.ekblad.toml.decode
|
||||
import cc.ekblad.toml.tomlMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrl
|
||||
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
|
||||
import com.vitorpamplona.quartz.relay.RelayInfo
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Operator-facing configuration. Section layout matches nostr-rs-relay's
|
||||
* `config.toml` so existing configs can be ported with little churn.
|
||||
*
|
||||
* Every section is optional; values not set fall back to sensible
|
||||
* defaults (or, for fields also exposed on the CLI, the CLI value wins).
|
||||
*
|
||||
* Sections that are parsed but **not yet enforced** by the relay are
|
||||
* marked below; they're accepted so configs remain forward-compatible
|
||||
* once the matching policy is implemented (rate limits, NIP-05, etc.).
|
||||
*/
|
||||
data class RelayConfig(
|
||||
val info: InfoSection = InfoSection(),
|
||||
val network: NetworkSection = NetworkSection(),
|
||||
val database: DatabaseSection = DatabaseSection(),
|
||||
val options: OptionsSection = OptionsSection(),
|
||||
/** Parsed but not yet enforced. */
|
||||
val limits: LimitsSection = LimitsSection(),
|
||||
/** Parsed but not yet enforced. */
|
||||
val authorization: AuthorizationSection = AuthorizationSection(),
|
||||
) {
|
||||
/**
|
||||
* Maps the `[info]` section into a [RelayInfo] used by the NIP-11
|
||||
* endpoint. `relay_url` and CLI overrides take precedence.
|
||||
*/
|
||||
fun resolveInfo(advertisedUrl: NormalizedRelayUrl): RelayInfo =
|
||||
RelayInfo(
|
||||
Nip11RelayInformation(
|
||||
name = info.name ?: "quartz-relay",
|
||||
description = info.description ?: "Embedded Nostr relay from the Amethyst quartz library.",
|
||||
pubkey = info.pubkey,
|
||||
contact = info.contact,
|
||||
icon = info.icon,
|
||||
software =
|
||||
info.software
|
||||
?: "https://github.com/vitorpamplona/amethyst/tree/main/quartz-relay",
|
||||
version = info.version ?: "1.08.0",
|
||||
supported_nips =
|
||||
info.supported_nips?.map(Int::toString)
|
||||
?: listOf("1", "9", "11", "40", "42", "45", "50", "62"),
|
||||
privacy_policy = info.privacy_policy,
|
||||
terms_of_service = info.terms_of_service,
|
||||
relay_countries = info.relay_countries,
|
||||
language_tags = info.language_tags,
|
||||
tags = info.tags,
|
||||
),
|
||||
).also {
|
||||
// Touch [advertisedUrl] so the parameter isn't unused — we keep
|
||||
// it in the signature because future fields (e.g. self-pubkey
|
||||
// selection, fee URLs) will want it.
|
||||
advertisedUrl.url
|
||||
}
|
||||
|
||||
data class InfoSection(
|
||||
val relay_url: String? = null,
|
||||
val name: String? = null,
|
||||
val description: String? = null,
|
||||
val pubkey: String? = null,
|
||||
val contact: String? = null,
|
||||
val icon: String? = null,
|
||||
val software: String? = null,
|
||||
val version: String? = null,
|
||||
/** NIP numbers as ints (e.g. `[1, 9, 11]`). Stringified at render time. */
|
||||
val supported_nips: List<Int>? = null,
|
||||
val privacy_policy: String? = null,
|
||||
val terms_of_service: String? = null,
|
||||
val relay_countries: List<String>? = null,
|
||||
val language_tags: List<String>? = null,
|
||||
val tags: List<String>? = null,
|
||||
)
|
||||
|
||||
data class NetworkSection(
|
||||
val host: String = "0.0.0.0",
|
||||
val port: Int = 7447,
|
||||
val path: String = "/",
|
||||
/**
|
||||
* When set, the relay reads the client IP from this header
|
||||
* (typically `X-Forwarded-For` behind a reverse proxy). Required
|
||||
* once IP-based rate limits land.
|
||||
*/
|
||||
val remote_ip_header: String? = null,
|
||||
)
|
||||
|
||||
data class DatabaseSection(
|
||||
/** True keeps an in-memory SQLite db (default — events vanish on restart). */
|
||||
val in_memory: Boolean = true,
|
||||
/** Filesystem path for a persistent SQLite db. Ignored when [in_memory] is true. */
|
||||
val file: String? = null,
|
||||
)
|
||||
|
||||
data class OptionsSection(
|
||||
/** Reject events whose `created_at` is more than this many seconds in the future. */
|
||||
val reject_future_seconds: Int? = null,
|
||||
/** Require NIP-42 AUTH for REQ/EVENT/COUNT. */
|
||||
val require_auth: Boolean = false,
|
||||
/** Drop events whose Schnorr signature does not verify. */
|
||||
val verify_signatures: Boolean = false,
|
||||
)
|
||||
|
||||
data class LimitsSection(
|
||||
val max_event_bytes: Int? = null,
|
||||
val max_ws_message_bytes: Int? = null,
|
||||
val max_ws_frame_bytes: Int? = null,
|
||||
val messages_per_sec: Int? = null,
|
||||
val subscriptions_per_min: Int? = null,
|
||||
val max_subscriptions_per_session: Int? = null,
|
||||
val max_filters_per_req: Int? = null,
|
||||
)
|
||||
|
||||
data class AuthorizationSection(
|
||||
val pubkey_whitelist: List<String> = emptyList(),
|
||||
val pubkey_blacklist: List<String> = emptyList(),
|
||||
val kind_whitelist: List<Int> = emptyList(),
|
||||
val kind_blacklist: List<Int> = emptyList(),
|
||||
)
|
||||
|
||||
companion object {
|
||||
private val mapper = tomlMapper { }
|
||||
|
||||
/** Parse a TOML string. */
|
||||
fun fromToml(toml: String): RelayConfig = mapper.decode<RelayConfig>(toml)
|
||||
|
||||
/** Load a TOML config file. */
|
||||
fun fromFile(file: File): RelayConfig = mapper.decode<RelayConfig>(file.toPath())
|
||||
|
||||
/**
|
||||
* Returns the URL the relay advertises in NIP-11 and NIP-42
|
||||
* challenges. Picks (in order):
|
||||
* 1. `info.relay_url` from the config
|
||||
* 2. The `network` section's host/port/path (with 0.0.0.0 → 127.0.0.1)
|
||||
* 3. The CLI override (handled in `Main.kt`).
|
||||
*/
|
||||
fun advertisedUrl(config: RelayConfig): NormalizedRelayUrl =
|
||||
(
|
||||
config.info.relay_url
|
||||
?: defaultUrl(config.network)
|
||||
).normalizeRelayUrl()
|
||||
|
||||
private fun defaultUrl(net: NetworkSection): String {
|
||||
val host = if (net.host == "0.0.0.0") "127.0.0.1" else net.host
|
||||
return "ws://$host:${net.port}${net.path}"
|
||||
}
|
||||
}
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.relay.config
|
||||
|
||||
import java.io.File
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class RelayConfigTest {
|
||||
@Test
|
||||
fun emptyTomlYieldsAllDefaults() {
|
||||
val c = RelayConfig.fromToml("")
|
||||
assertEquals("0.0.0.0", c.network.host)
|
||||
assertEquals(7447, c.network.port)
|
||||
assertEquals("/", c.network.path)
|
||||
assertEquals(true, c.database.in_memory)
|
||||
assertEquals(false, c.options.require_auth)
|
||||
assertEquals(false, c.options.verify_signatures)
|
||||
assertTrue(c.authorization.pubkey_whitelist.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parsesAllSectionsTogether() {
|
||||
val toml =
|
||||
"""
|
||||
[info]
|
||||
relay_url = "wss://relay.example.com/"
|
||||
name = "Example"
|
||||
contact = "ops@example.com"
|
||||
supported_nips = [1, 9, 11, 42]
|
||||
|
||||
[network]
|
||||
host = "127.0.0.1"
|
||||
port = 9988
|
||||
path = "/relay"
|
||||
remote_ip_header = "X-Forwarded-For"
|
||||
|
||||
[database]
|
||||
in_memory = false
|
||||
file = "/var/lib/quartz-relay/events.db"
|
||||
|
||||
[options]
|
||||
verify_signatures = true
|
||||
require_auth = true
|
||||
reject_future_seconds = 1800
|
||||
|
||||
[limits]
|
||||
max_event_bytes = 131072
|
||||
messages_per_sec = 10
|
||||
max_filters_per_req = 12
|
||||
|
||||
[authorization]
|
||||
pubkey_blacklist = ["aaaa", "bbbb"]
|
||||
kind_blacklist = [4, 1059]
|
||||
""".trimIndent()
|
||||
|
||||
val c = RelayConfig.fromToml(toml)
|
||||
|
||||
assertEquals("wss://relay.example.com/", c.info.relay_url)
|
||||
assertEquals("Example", c.info.name)
|
||||
assertEquals(listOf(1, 9, 11, 42), c.info.supported_nips)
|
||||
|
||||
assertEquals("127.0.0.1", c.network.host)
|
||||
assertEquals(9988, c.network.port)
|
||||
assertEquals("/relay", c.network.path)
|
||||
assertEquals("X-Forwarded-For", c.network.remote_ip_header)
|
||||
|
||||
assertEquals(false, c.database.in_memory)
|
||||
assertEquals("/var/lib/quartz-relay/events.db", c.database.file)
|
||||
|
||||
assertEquals(true, c.options.verify_signatures)
|
||||
assertEquals(true, c.options.require_auth)
|
||||
assertEquals(1800, c.options.reject_future_seconds)
|
||||
|
||||
assertEquals(131072, c.limits.max_event_bytes)
|
||||
assertEquals(10, c.limits.messages_per_sec)
|
||||
assertEquals(12, c.limits.max_filters_per_req)
|
||||
|
||||
assertEquals(listOf("aaaa", "bbbb"), c.authorization.pubkey_blacklist)
|
||||
assertEquals(listOf(4, 1059), c.authorization.kind_blacklist)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun supportedNipsRenderedAsStringsInNip11Doc() {
|
||||
val c =
|
||||
RelayConfig.fromToml(
|
||||
"""
|
||||
[info]
|
||||
supported_nips = [1, 11, 42]
|
||||
""".trimIndent(),
|
||||
)
|
||||
val info =
|
||||
c.resolveInfo(
|
||||
"ws://127.0.0.1:7447/".let {
|
||||
com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
.normalize(it)
|
||||
},
|
||||
)
|
||||
assertEquals(listOf("1", "11", "42"), info.document.supported_nips)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loadsTheBundledExampleConfigCleanly() {
|
||||
// The example file lives at the module root so operators have a
|
||||
// canonical reference. Read it via a relative path resolved
|
||||
// against the working directory (gradle runs tests from the
|
||||
// module dir).
|
||||
val candidates =
|
||||
listOf(
|
||||
File("config.example.toml"),
|
||||
File("quartz-relay/config.example.toml"),
|
||||
)
|
||||
val example =
|
||||
candidates.firstOrNull { it.exists() }
|
||||
?: error(
|
||||
"config.example.toml not found in any of: ${candidates.joinToString { it.absolutePath }}",
|
||||
)
|
||||
|
||||
val c = RelayConfig.fromFile(example)
|
||||
|
||||
assertEquals("wss://relay.example.com/", c.info.relay_url)
|
||||
assertEquals(true, c.options.verify_signatures)
|
||||
assertEquals(false, c.database.in_memory)
|
||||
assertNotNull(c.database.file)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun missingSectionsAreOptional() {
|
||||
val c = RelayConfig.fromToml("[info]\nname = \"only-info\"")
|
||||
assertEquals("only-info", c.info.name)
|
||||
// Defaults preserved for unspecified sections.
|
||||
assertEquals(7447, c.network.port)
|
||||
assertEquals(true, c.database.in_memory)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user