helm_schema/
fetch_policy.rs1use std::net::{IpAddr, Ipv6Addr};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct FetchPolicy {
11 allow_file: bool,
12 allow_network: bool,
13}
14
15impl FetchPolicy {
16 #[must_use]
18 pub const fn new(allow_file: bool, allow_network: bool) -> Self {
19 Self {
20 allow_file,
21 allow_network,
22 }
23 }
24
25 #[must_use]
28 pub const fn input_assembly(allow_network: bool) -> Self {
29 Self::new(true, allow_network)
30 }
31
32 pub(crate) fn validate_file_host(self, host: &str) -> Result<(), String> {
33 if !self.allow_file {
34 return Err("local file access is disabled by fetch policy".to_string());
35 }
36 if host.is_empty() {
37 return Ok(());
38 }
39 Err(format!(
40 "file:// authority host is not allowed by fetch policy: {host}"
41 ))
42 }
43
44 pub(crate) fn validate_network_host(self, host: Option<&str>) -> Result<(), String> {
45 if !self.allow_network {
46 return Err("network access is disabled by fetch policy".to_string());
47 }
48
49 let Some(host) = host else {
50 return Err("network URI is missing an authority host".to_string());
51 };
52 let normalized = host.trim_end_matches('.').to_ascii_lowercase();
53 if normalized == "localhost" || normalized.ends_with(".localhost") {
54 return Err(format!(
55 "network host is denied by fetch policy because it is loopback-local: {host}"
56 ));
57 }
58
59 let ip = parse_ip_literal(host);
60 if let Some(ip) = ip
61 && is_denied_ip(ip)
62 {
63 return Err(format!(
64 "network host is denied by fetch policy because it is loopback/link-local: {host}"
65 ));
66 }
67
68 Ok(())
69 }
70}
71
72fn parse_ip_literal(host: &str) -> Option<IpAddr> {
73 if let Some(inner) = host
74 .strip_prefix('[')
75 .and_then(|rest| rest.strip_suffix(']'))
76 {
77 return inner.parse::<Ipv6Addr>().ok().map(IpAddr::V6);
78 }
79 host.parse::<IpAddr>().ok()
80}
81
82fn is_denied_ip(ip: IpAddr) -> bool {
83 match ip {
84 IpAddr::V4(addr) => addr.is_loopback() || addr.is_link_local() || addr.is_unspecified(),
85 IpAddr::V6(addr) => {
86 addr.is_loopback() || addr.is_unicast_link_local() || addr == Ipv6Addr::UNSPECIFIED
87 }
88 }
89}