From 139d6dbda542c8322c12ebaaeecbc942e8efc9e4 Mon Sep 17 00:00:00 2001 From: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co> Date: Fri, 26 Jun 2026 22:25:07 -0400 Subject: [PATCH] fix(relay): preserve relay URL port for deployment binding Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co> --- crates/buzz-relay/src/main.rs | 15 ++------ crates/buzz-relay/src/tenant.rs | 66 ++++++++++++++++++++++++++++----- 2 files changed, 61 insertions(+), 20 deletions(-) diff --git a/crates/buzz-relay/src/main.rs b/crates/buzz-relay/src/main.rs index 6b520640c..e66666a35 100644 --- a/crates/buzz-relay/src/main.rs +++ b/crates/buzz-relay/src/main.rs @@ -346,18 +346,11 @@ async fn main() -> anyhow::Result<()> { // host (dev/CI runs single-community), failing closed if the host // isn't mapped — the reconciler is community-scoped now, so there is // no global "all channels" sweep. - let raw_host = url::Url::parse( - &reconcile_state - .config - .relay_url - .replace("ws://", "http://") - .replace("wss://", "https://"), + let tenant = match buzz_relay::tenant::bind_deployment_community( + &reconcile_state.db, + &reconcile_state.config.relay_url, ) - .ok() - .and_then(|u| u.host_str().map(|h| h.to_string())) - .unwrap_or_default(); - let tenant = match buzz_relay::tenant::bind_community(&reconcile_state.db, &raw_host) - .await + .await { Ok(ctx) => ctx, Err(e) => { diff --git a/crates/buzz-relay/src/tenant.rs b/crates/buzz-relay/src/tenant.rs index c9475751f..39e1f9242 100644 --- a/crates/buzz-relay/src/tenant.rs +++ b/crates/buzz-relay/src/tenant.rs @@ -94,15 +94,29 @@ pub async fn bind_deployment_community( resolver: &R, relay_url: &str, ) -> Result> { - let raw_host = url::Url::parse( - &relay_url - .replace("ws://", "http://") - .replace("wss://", "https://"), - ) - .ok() - .and_then(|u| u.host_str().map(|h| h.to_string())) - .unwrap_or_default(); - bind_community(resolver, &raw_host).await + bind_community(resolver, &relay_url_authority(relay_url)).await +} + +/// Extract the relay URL authority in the same normalized shape as request +/// `Host` headers and `communities.host`: host plus an explicit non-default +/// port, if present. +fn relay_url_authority(relay_url: &str) -> String { + let Ok(url) = url::Url::parse(relay_url) else { + return String::new(); + }; + let Some(host) = url.host() else { + return String::new(); + }; + let host = match host { + url::Host::Domain(domain) => domain.to_string(), + url::Host::Ipv4(addr) => addr.to_string(), + url::Host::Ipv6(addr) => format!("[{addr}]"), + }; + let authority = match url.port() { + Some(port) => format!("{host}:{port}"), + None => host, + }; + normalize_host(&authority) } /// Production [`HostResolver`]: the relay resolves hosts against the durable @@ -189,6 +203,40 @@ mod tests { } } + #[tokio::test] + async fn deployment_url_keeps_nondefault_port_for_lookup() { + let r = resolver_with("localhost:3000", 42); + let ctx = bind_deployment_community(&r, "ws://localhost:3000") + .await + .expect("deployment host should bind with non-default port"); + assert_eq!(ctx.community().as_uuid(), &Uuid::from_u128(42)); + assert_eq!(ctx.host(), "localhost:3000"); + + let wrong = resolver_with("localhost", 42); + let err = bind_deployment_community(&wrong, "ws://localhost:3000") + .await + .unwrap_err(); + assert!(matches!(err, BindError::UnmappedHost)); + } + + #[tokio::test] + async fn deployment_url_normalizes_default_ports() { + let r = resolver_with("relay.example", 9); + for url in ["ws://relay.example:80", "wss://relay.example:443"] { + let ctx = bind_deployment_community(&r, url) + .await + .unwrap_or_else(|_| panic!("url {url:?} should bind")); + assert_eq!(ctx.community().as_uuid(), &Uuid::from_u128(9)); + assert_eq!(ctx.host(), "relay.example", "url {url:?}"); + } + } + + #[test] + fn relay_url_authority_preserves_ipv6_brackets() { + assert_eq!(relay_url_authority("ws://[::1]:3000"), "[::1]:3000"); + assert_eq!(relay_url_authority("wss://[::1]:443"), "[::1]"); + } + #[tokio::test] async fn unmapped_host_fails_closed() { let r = resolver_with("relay.example", 1);