Preserve local relay host for desktop HTTP calls

Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr
2026-06-27 21:39:57 -04:00
parent 8356aa4d21
commit 1ac082d075
+13 -27
View File
@@ -75,30 +75,16 @@ pub fn relay_http_base_url(relay_url: &str) -> String {
let trimmed = relay_url.trim().trim_end_matches('/');
if let Some(suffix) = trimmed.strip_prefix("wss://") {
return format!("https://{}", normalize_loopback_host(suffix));
return format!("https://{}", suffix);
}
if let Some(suffix) = trimmed.strip_prefix("ws://") {
return format!("http://{}", normalize_loopback_host(suffix));
return format!("http://{}", suffix);
}
trimmed.to_string()
}
/// Rewrite an exact `localhost` host to `127.0.0.1`, preserving port and path.
///
/// macOS resolves `localhost` to both `::1` and `127.0.0.1`; reqwest's
/// happy-eyeballs may try `::1` first, which fails when the relay binds IPv4
/// only (`0.0.0.0`). Forcing the IPv4 literal makes every HTTP write connect on
/// the first attempt. Exact-match only — `localhost.evil.com` is untouched.
fn normalize_loopback_host(authority: &str) -> String {
let host_len = authority.find([':', '/']).unwrap_or(authority.len());
if &authority[..host_len] == "localhost" {
return format!("127.0.0.1{}", &authority[host_len..]);
}
authority.to_string()
}
pub fn relay_api_base_url() -> String {
if let Some(base) = configured_env_var("BUZZ_RELAY_HTTP") {
return base.trim_end_matches('/').to_string();
@@ -669,21 +655,24 @@ mod tests {
);
}
// ── relay_http_base_url loopback normalization ───────────────────────────
// ── relay_http_base_url scheme conversion ────────────────────────────────
#[test]
fn loopback_ws_localhost_rewritten_to_ipv4() {
fn loopback_ws_localhost_preserves_authority() {
// Tenant host-binding keys off the HTTP Host/authority. The desktop must
// not rewrite localhost to 127.0.0.1, or local dev HTTP calls target a
// different unmapped community than the WebSocket URL.
assert_eq!(
relay_http_base_url("ws://localhost:3000"),
"http://127.0.0.1:3000"
"http://localhost:3000"
);
}
#[test]
fn loopback_trailing_slash_rewritten_to_ipv4() {
fn loopback_trailing_slash_removed_authority_preserved() {
assert_eq!(
relay_http_base_url("ws://localhost:3000/"),
"http://127.0.0.1:3000"
"http://localhost:3000"
);
}
@@ -704,9 +693,7 @@ mod tests {
}
#[test]
fn localhost_substring_host_not_rewritten() {
// Exact-match only: a host that merely starts with "localhost" must NOT
// be rewritten, or a malicious host could hijack the loopback path.
fn localhost_substring_host_unchanged() {
assert_eq!(
relay_http_base_url("ws://localhost.evil.com:3000"),
"http://localhost.evil.com:3000"
@@ -714,11 +701,10 @@ mod tests {
}
#[test]
fn loopback_wss_localhost_rewritten_to_ipv4() {
// The wss:// dev arm normalizes identically to ws://.
fn loopback_wss_localhost_preserves_authority() {
assert_eq!(
relay_http_base_url("wss://localhost:3000"),
"https://127.0.0.1:3000"
"https://localhost:3000"
);
}