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());