use crate::config::is_local_endpoint;
use crate::safety::is_private_or_internal;
use anyhow::Result;
use std::net::IpAddr;
pub fn is_private_or_internal_ip(ip: &IpAddr) -> bool {
is_private_or_internal(*ip)
}
pub fn is_private_network_host(host: &str) -> bool {
if host == "localhost" || host.ends_with(".localhost") {
return true;
}
let bare_host = if host.starts_with('[') && host.ends_with(']') {
let inner = &host[1..host.len() - 1];
if !inner.contains(':')
|| !inner
.chars()
.all(|c| c.is_ascii_hexdigit() || c == ':' || c == '.')
{
return false;
}
inner
} else {
host
};
if let Ok(ip) = bare_host.parse::<IpAddr>() {
return is_private_or_internal_ip(&ip);
}
false
}
pub(crate) fn is_trusted_local_network_host(host: &str) -> bool {
let bare_host = if host.starts_with('[') && host.ends_with(']') {
let inner = &host[1..host.len() - 1];
if !inner.contains(':')
|| !inner
.chars()
.all(|c| c.is_ascii_hexdigit() || c == ':' || c == '.')
{
return false;
}
inner
} else {
host
};
matches!(bare_host, "localhost" | "127.0.0.1" | "::1" | "0.0.0.0")
|| bare_host.ends_with(".localhost")
}
#[derive(Debug, Clone, Copy)]
pub struct NetworkPolicy {
pub allow_localhost: bool,
pub allow_private: bool,
}
pub fn validate_url_target(url: &url::Url, allow_private: bool) -> Result<NetworkPolicy> {
if url.scheme() != "http" && url.scheme() != "https" {
anyhow::bail!("Only HTTP and HTTPS URLs are allowed");
}
let allow_localhost = is_local_endpoint(url.as_str())
|| url.host_str().is_some_and(is_trusted_local_network_host);
if let Some(host) = url.host_str() {
if let Ok(ip) = host
.trim_start_matches('[')
.trim_end_matches(']')
.parse::<IpAddr>()
{
if is_private_or_internal_ip(&ip) && !(allow_private || allow_localhost) {
anyhow::bail!(
"Blocked request to private/internal network address: {}. \
Set SELFWARE_ALLOW_PRIVATE_NETWORK=1 to allow.",
host
);
}
}
}
Ok(NetworkPolicy {
allow_localhost,
allow_private,
})
}
#[cfg(test)]
#[path = "../../tests/unit/tools/net_policy/net_policy_test.rs"]
mod tests;