mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(admin): derive admin tenant host via shared relay_url_authority
Issue 3 reopen (Max): buzz-admin's resolve_admin_tenant derived its lookup host with Url::host_str(), which drops an explicit non-default port and IPv6 brackets. For the default RELAY_URL ws://localhost:3000 the admin would look up community host `localhost` while startup seeding (and live request resolution) bind `localhost:3000` — so the admin CLI's membership writes would miss, or hit, the wrong deployment community. Lift relay_url_authority into buzz-core::tenant as the single canonical helper so the relay's host-resolution seam (startup seeding, bind_deployment_community) and the buzz-admin CLI derive a byte-identical authority: host plus explicit non-default port, IPv6 brackets preserved, default ports collapsed — exactly as normalize_host shapes an inbound Host header. The relay tenant module now `pub use`-re-exports it (no behavior change at the relay seam); buzz-admin calls it directly. Adds 4 buzz-core unit tests pinning the authority shape: non-default-port retention (localhost:3000, relay.example:8443), default-port collapse (:443/:80), IPv6 brackets ([::1]:3000), and unparseable/empty -> empty (callers fail closed on empty). Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
This commit is contained in:
co-authored by
Tyler Longwell
parent
2adae32bc7
commit
ce747ec759
@@ -24,7 +24,7 @@ use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use buzz_core::kind::KIND_NIP43_MEMBERSHIP_LIST;
|
||||
use buzz_core::tenant::{normalize_host, TenantContext};
|
||||
use buzz_core::tenant::{relay_url_authority, TenantContext};
|
||||
use buzz_db::{Db, DbConfig};
|
||||
use buzz_pubsub::{EventTopic, PubSubManager};
|
||||
use clap::{Parser, Subcommand};
|
||||
@@ -405,15 +405,14 @@ async fn connect_db() -> Result<Db> {
|
||||
async fn resolve_admin_tenant(db: &Db) -> Result<TenantContext> {
|
||||
let relay_url =
|
||||
std::env::var("RELAY_URL").unwrap_or_else(|_| "ws://localhost:3000".to_string());
|
||||
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();
|
||||
let host = normalize_host(&raw_host);
|
||||
// Derive the authority the *same* way startup seeding and live request
|
||||
// resolution do (`buzz_core::tenant::relay_url_authority`): host plus an
|
||||
// explicit non-default port, IPv6 brackets preserved. A plain
|
||||
// `Url::host_str()` drops the port/brackets, so for `ws://localhost:3000`
|
||||
// the admin would look up `localhost` while startup seeded `localhost:3000`
|
||||
// — and `wss://relay.example:8443` would resolve `relay.example`. Sharing
|
||||
// the helper keeps buzz-admin byte-identical to the community startup seeds.
|
||||
let host = relay_url_authority(&relay_url);
|
||||
let record = db.lookup_community_by_host(&host).await?.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"RELAY_URL host '{host}' is not mapped to a community.\n\
|
||||
|
||||
@@ -137,6 +137,41 @@ pub fn normalize_host(host: &str) -> String {
|
||||
host
|
||||
}
|
||||
|
||||
/// Extract the authority (host plus an explicit non-default port, if present)
|
||||
/// from a relay URL in the same normalized shape as request `Host` headers and
|
||||
/// `communities.host`.
|
||||
///
|
||||
/// Shared by the relay's host-resolution seam (startup community seeding and
|
||||
/// the deployment-community bind), the relay's `bind_deployment_community`, and
|
||||
/// the `buzz-admin` CLI's tenant resolution. All of these must derive the
|
||||
/// *byte-identical* authority that live request resolution
|
||||
/// ([`crate::tenant::normalize_host`]) produces from an inbound `Host`, or a
|
||||
/// bootstrapped/looked-up community lands under a host no request resolves to.
|
||||
///
|
||||
/// In particular this preserves an explicit non-default port (`relay:8443` →
|
||||
/// `relay:8443`) and IPv6 brackets (`[::1]:3000`) — both of which a naive
|
||||
/// `Url::host_str()` drops. Returns the empty string when `relay_url` has no
|
||||
/// parseable host (the caller fails closed on empty).
|
||||
#[must_use]
|
||||
pub 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)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -196,4 +231,39 @@ mod tests {
|
||||
assert_eq!(normalize_host(""), "");
|
||||
assert_eq!(normalize_host(" "), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relay_url_authority_keeps_explicit_nondefault_port() {
|
||||
// The default dev seed: startup, bind_deployment_community, and
|
||||
// buzz-admin must all derive `localhost:3000` (NOT bare `localhost`),
|
||||
// or the admin lookup misses the community startup seeded.
|
||||
assert_eq!(relay_url_authority("ws://localhost:3000"), "localhost:3000");
|
||||
assert_eq!(
|
||||
relay_url_authority("wss://relay.example:8443"),
|
||||
"relay.example:8443"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relay_url_authority_collapses_default_ports() {
|
||||
// Default ports collapse to the bare host, matching how an inbound
|
||||
// `Host` header for the same deployment normalizes.
|
||||
assert_eq!(relay_url_authority("wss://relay.example:443"), "relay.example");
|
||||
assert_eq!(relay_url_authority("ws://relay.example:80"), "relay.example");
|
||||
assert_eq!(relay_url_authority("wss://relay.example"), "relay.example");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relay_url_authority_preserves_ipv6_brackets() {
|
||||
// `host_str()` strips IPv6 brackets and the port; `relay_url_authority`
|
||||
// must keep both so the authority matches `communities.host`.
|
||||
assert_eq!(relay_url_authority("ws://[::1]:3000"), "[::1]:3000");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relay_url_authority_unparseable_is_empty() {
|
||||
// No parseable host → empty authority; callers fail closed.
|
||||
assert_eq!(relay_url_authority("not a url"), "");
|
||||
assert_eq!(relay_url_authority(""), "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ pub async fn bind_deployment_community<R: HostResolver>(
|
||||
resolver: &R,
|
||||
relay_url: &str,
|
||||
) -> Result<TenantContext, BindError<R::Error>> {
|
||||
bind_community(resolver, &relay_url_authority(relay_url)).await
|
||||
bind_community(resolver, &buzz_core::tenant::relay_url_authority(relay_url)).await
|
||||
}
|
||||
|
||||
/// Extract the relay URL authority in the same normalized shape as request
|
||||
@@ -115,26 +115,12 @@ pub async fn bind_deployment_community<R: HostResolver>(
|
||||
/// `pub` so startup ([`crate::main`], a separate binary crate) can seed the
|
||||
/// deployment's own community under the *same* normalized host that live request
|
||||
/// resolution ([`bind_community`]) will derive — the two must agree or the
|
||||
/// bootstrapped owner lands in a community no request ever resolves to. Returns
|
||||
/// the empty string when `relay_url` has no parseable host.
|
||||
pub 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)
|
||||
}
|
||||
/// bootstrapped owner lands in a community no request ever resolves to.
|
||||
///
|
||||
/// This is a thin re-export of [`buzz_core::tenant::relay_url_authority`]: the
|
||||
/// canonical implementation lives in `buzz-core` so the relay seam *and* the
|
||||
/// `buzz-admin` CLI derive a byte-identical authority (same port/IPv6 handling).
|
||||
pub use buzz_core::tenant::relay_url_authority;
|
||||
|
||||
/// Production [`HostResolver`]: the relay resolves hosts against the durable
|
||||
/// `communities` host map in Postgres.
|
||||
|
||||
Reference in New Issue
Block a user