From f9ee858e9404c2c306e094b6b2055100530c8210 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Sat, 24 Oct 2026 17:58:52 -0400 Subject: [PATCH] fix(service): allow plain HTTP to every loopback address Connector, OAuth, vector, graph and model endpoints accepted plain HTTP only for localhost, 127.0.0.1 and [::1]. A local model or connector on 127.0.0.2 (any other 127/8 address) or [::ffff:127.0.0.1] was refused with "Use HTTPS, or HTTP on loopback", although it is loopback. The graph engine's provider check already accepts all of 127/8. Signed-off-by: Rudy Celekli --- crates/graphicle/src/model.rs | 43 +++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/crates/graphicle/src/model.rs b/crates/graphicle/src/model.rs index 542b534..b1cd2a0 100644 --- a/crates/graphicle/src/model.rs +++ b/crates/graphicle/src/model.rs @@ -290,11 +290,20 @@ pub fn checked_url(value: &str) -> Result { } Ok(url) } +/// `localhost` or any loopback address: 127.0.0.0/8, `::1`, and IPv4-mapped +/// 127.0.0.0/8. fn loopback(url: &url::Url) -> bool { - matches!( - url.host_str(), - Some("localhost" | "127.0.0.1" | "[::1]" | "::1") - ) + match url.host() { + Some(url::Host::Domain(host)) => host == "localhost", + Some(url::Host::Ipv4(address)) => address.is_loopback(), + Some(url::Host::Ipv6(address)) => { + address.is_loopback() + || address + .to_ipv4_mapped() + .is_some_and(|mapped| mapped.is_loopback()) + } + None => false, + } } /// No user name, password, query or fragment. fn bare(url: &url::Url) -> bool { @@ -1578,6 +1587,32 @@ mod tests { assert!(checked_url("http://192.168.210.33:8000/v1").is_err()); } + #[test] + fn plain_http_reaches_every_loopback_address_and_nothing_else() { + for url in [ + "http://localhost:11434/v1", + "http://127.0.0.1:11434/v1", + "http://127.0.0.2:11434/v1", + "http://127.255.255.254/v1", + "http://[::1]:8080/v1", + "http://[::ffff:127.0.0.1]:8080/v1", + ] { + assert!(checked_url(url).is_ok(), "{url}"); + assert!(provider_url_with(url, true, &[]).is_ok(), "{url}"); + } + for url in [ + "http://128.0.0.1/v1", + "http://126.255.255.255/v1", + "http://0.0.0.0/v1", + "http://[::2]/v1", + "http://[::ffff:10.0.0.1]/v1", + "http://127.0.0.1.example.com/v1", + "http://localhost.example.com/v1", + ] { + assert!(checked_url(url).is_err(), "{url}"); + } + } + #[test] fn the_plain_http_host_list_takes_exact_hosts_only() { assert!(http_hosts("").unwrap().is_empty());