lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit bca548659e8752709a2b9388cbca490fb0eb7584
parent 2b34514651a66fb7837aac2ad37d49c8eb59757b
Author: triesap <tyson@radroots.org>
Date:   Tue, 16 Jun 2026 14:16:00 -0700

relay_transport: replace local dev relay policy

Diffstat:
Mcrates/relay_transport/src/error.rs | 4++--
Mcrates/relay_transport/src/publish.rs | 2+-
Mcrates/relay_transport/src/relay.rs | 41+++++++++++++++++++++--------------------
Mcrates/relay_transport/tests/transport.rs | 38++++++++++++++++++++++++++++++++++++--
4 files changed, 60 insertions(+), 25 deletions(-)

diff --git a/crates/relay_transport/src/error.rs b/crates/relay_transport/src/error.rs @@ -7,8 +7,8 @@ pub enum RadrootsRelayTransportError { #[error("Relay URL parse failed for `{url}`: {reason}")] RelayUrlParse { url: String, reason: String }, - #[error("Relay URL `{url}` uses ws outside local-dev policy")] - WsRequiresLocalPolicy { url: String }, + #[error("Relay URL `{url}` uses ws outside localhost relay policy")] + WsRequiresLocalhostPolicy { url: String }, #[error("Relay URL `{url}` has unsupported scheme `{scheme}`")] UnsupportedRelayScheme { url: String, scheme: String }, diff --git a/crates/relay_transport/src/publish.rs b/crates/relay_transport/src/publish.rs @@ -240,7 +240,7 @@ impl RadrootsRelayPublishAdapter for RadrootsNostrClientPublishAdapter { let mut receipts = Vec::new(); for relay_url in &target_strings { let relay = - crate::RadrootsRelayUrl::parse(relay_url, RadrootsRelayUrlPolicy::LocalDev)?; + crate::RadrootsRelayUrl::parse(relay_url, RadrootsRelayUrlPolicy::Localhost)?; let success = output.success.iter().any(|success_url| { success_url.to_string().trim_end_matches('/') == relay.as_str() }); diff --git a/crates/relay_transport/src/relay.rs b/crates/relay_transport/src/relay.rs @@ -9,12 +9,13 @@ use url::Url; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum RadrootsRelayUrlPolicy { Public, - LocalDev, + Localhost, } impl RadrootsRelayUrlPolicy { - fn accepts_ws(self) -> bool { - matches!(self, Self::LocalDev) + fn accepts_ws_host(self, host: &str) -> bool { + matches!(self, Self::Localhost) + && matches!(host, "localhost" | "127.0.0.1" | "::1" | "[::1]") } } @@ -32,12 +33,27 @@ impl RadrootsRelayUrl { url: original.to_owned(), reason: error.to_string(), })?; + if !parsed.username().is_empty() || parsed.password().is_some() { + return Err(RadrootsRelayTransportError::RelayUrlUserinfo { + url: original.to_owned(), + }); + } + let Some(host) = parsed.host_str().filter(|host| !host.is_empty()) else { + return Err(RadrootsRelayTransportError::EmptyRelayHost { + url: original.to_owned(), + }); + }; + if parsed.query().is_some() || parsed.fragment().is_some() { + return Err(RadrootsRelayTransportError::RelayUrlQueryOrFragment { + url: original.to_owned(), + }); + } let scheme = parsed.scheme(); match scheme { "wss" => {} - "ws" if policy.accepts_ws() => {} + "ws" if policy.accepts_ws_host(host) => {} "ws" => { - return Err(RadrootsRelayTransportError::WsRequiresLocalPolicy { + return Err(RadrootsRelayTransportError::WsRequiresLocalhostPolicy { url: original.to_owned(), }); } @@ -48,21 +64,6 @@ impl RadrootsRelayUrl { }); } } - if !parsed.username().is_empty() || parsed.password().is_some() { - return Err(RadrootsRelayTransportError::RelayUrlUserinfo { - url: original.to_owned(), - }); - } - if parsed.host_str().is_none_or(str::is_empty) { - return Err(RadrootsRelayTransportError::EmptyRelayHost { - url: original.to_owned(), - }); - } - if parsed.query().is_some() || parsed.fragment().is_some() { - return Err(RadrootsRelayTransportError::RelayUrlQueryOrFragment { - url: original.to_owned(), - }); - } let mut normalized = parsed.to_string(); if parsed.path() == "/" { normalized.pop(); diff --git a/crates/relay_transport/tests/transport.rs b/crates/relay_transport/tests/transport.rs @@ -106,13 +106,33 @@ fn relay_url_validation_and_target_normalization() { let relay = RadrootsRelayUrl::parse("wss://Relay.Example.com", RadrootsRelayUrlPolicy::Public) .expect("relay"); assert_eq!(relay.as_str(), RELAY_PRIMARY_WSS); + let relay_path = RadrootsRelayUrl::parse( + "wss://Relay.Example.com/nostr", + RadrootsRelayUrlPolicy::Public, + ) + .expect("relay path"); + assert_eq!(relay_path.as_str(), "wss://relay.example.com/nostr"); assert!( RadrootsRelayUrl::parse("ws://127.0.0.1:7777", RadrootsRelayUrlPolicy::Public).is_err() ); - let local = RadrootsRelayUrl::parse("ws://127.0.0.1:7777", RadrootsRelayUrlPolicy::LocalDev) + let local = RadrootsRelayUrl::parse("ws://localhost:7777", RadrootsRelayUrlPolicy::Localhost) .expect("local relay"); - assert_eq!(local.as_str(), "ws://127.0.0.1:7777"); + assert_eq!(local.as_str(), "ws://localhost:7777"); + let local_ipv4 = + RadrootsRelayUrl::parse("ws://127.0.0.1:7777", RadrootsRelayUrlPolicy::Localhost) + .expect("local ipv4 relay"); + assert_eq!(local_ipv4.as_str(), "ws://127.0.0.1:7777"); + let local_ipv6 = RadrootsRelayUrl::parse("ws://[::1]:7777", RadrootsRelayUrlPolicy::Localhost) + .expect("local ipv6 relay"); + assert_eq!(local_ipv6.as_str(), "ws://[::1]:7777"); + assert!( + RadrootsRelayUrl::parse("ws://example.com", RadrootsRelayUrlPolicy::Localhost).is_err() + ); + assert!( + RadrootsRelayUrl::parse("ws://192.168.1.10:7777", RadrootsRelayUrlPolicy::Localhost) + .is_err() + ); assert!( RadrootsRelayUrl::parse("https://relay.example.com", RadrootsRelayUrlPolicy::Public) @@ -133,6 +153,20 @@ fn relay_url_validation_and_target_normalization() { .is_err() ); assert!(RadrootsRelayUrl::parse("wss://", RadrootsRelayUrlPolicy::Public).is_err()); + assert!( + RadrootsRelayUrl::parse( + "wss://relay.example.com?subscription=1", + RadrootsRelayUrlPolicy::Public + ) + .is_err() + ); + assert!( + RadrootsRelayUrl::parse( + "wss://relay.example.com#fragment", + RadrootsRelayUrlPolicy::Public + ) + .is_err() + ); let targets = RadrootsRelayTargetSet::new( vec![