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:
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![