1use std::fmt;
5use std::net::IpAddr;
6use std::str::FromStr;
7
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
12#[serde(rename_all = "lowercase")]
13pub enum NetworkPolicy {
14 Allow,
15 Deny,
16 Allowlist,
17}
18
19impl NetworkPolicy {
20 pub const ALLOWED: &'static str = "allow, deny, allowlist";
22
23 pub fn parse(value: &str) -> Option<Self> {
24 match value {
25 "allow" => Some(Self::Allow),
26 "deny" => Some(Self::Deny),
27 "allowlist" => Some(Self::Allowlist),
28 _ => None,
29 }
30 }
31
32 pub fn as_str(&self) -> &'static str {
33 match self {
34 Self::Allow => "allow",
35 Self::Deny => "deny",
36 Self::Allowlist => "allowlist",
37 }
38 }
39}
40
41impl FromStr for NetworkPolicy {
42 type Err = String;
43
44 fn from_str(value: &str) -> Result<Self, Self::Err> {
45 Self::parse(value).ok_or_else(|| {
46 format!(
47 "invalid network policy `{value}`; expected one of: {}",
48 Self::ALLOWED
49 )
50 })
51 }
52}
53
54impl<'de> Deserialize<'de> for NetworkPolicy {
55 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
56 where
57 D: serde::Deserializer<'de>,
58 {
59 let value = String::deserialize(deserializer)?;
60 Self::from_str(&value).map_err(serde::de::Error::custom)
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
66#[serde(transparent)]
67pub struct NetworkHost(String);
68
69impl NetworkHost {
70 pub const ALLOWED: &'static str = "exact host, IP address, *.example.com (subdomains), or **.example.com (apex and subdomains); internationalized names must use punycode; URLs, ports, paths, numeric or hex-looking DNS endings, single-label wildcards, empty strings, and bare * are not allowed";
71
72 pub fn as_str(&self) -> &str {
73 &self.0
74 }
75}
76
77impl fmt::Display for NetworkHost {
78 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
79 formatter.write_str(&self.0)
80 }
81}
82
83impl FromStr for NetworkHost {
84 type Err = String;
85
86 fn from_str(raw: &str) -> Result<Self, Self::Err> {
87 if raw.is_empty() {
88 return Err(invalid_host(raw));
89 }
90 if raw.trim() != raw {
91 return Err(invalid_host(raw));
92 }
93
94 let canonical = raw.to_ascii_lowercase();
95 let canonical = canonical.strip_suffix('.').unwrap_or(&canonical);
96 if canonical.is_empty()
97 || canonical == "*"
98 || canonical.contains("//")
99 || canonical.contains('/')
100 || canonical.contains('?')
101 || canonical.contains('#')
102 {
103 return Err(invalid_host(raw));
104 }
105
106 if let Ok(ip) = canonical.parse::<IpAddr>() {
107 let ip = match ip {
108 IpAddr::V6(ipv6) => ipv6.to_ipv4_mapped().map_or(IpAddr::V6(ipv6), IpAddr::V4),
109 ipv4 => ipv4,
110 };
111 return Ok(Self(ip.to_string()));
112 }
113 if canonical.contains(':') {
114 return Err(invalid_host(raw));
115 }
116
117 let (prefix, host) = if let Some(host) = canonical.strip_prefix("**.") {
118 ("**.", host)
119 } else if let Some(host) = canonical.strip_prefix("*.") {
120 ("*.", host)
121 } else {
122 ("", canonical)
123 };
124 if host.contains('*')
125 || (!prefix.is_empty() && host.parse::<IpAddr>().is_ok())
126 || (!prefix.is_empty() && !host.contains('.'))
127 || !valid_dns_host(host)
128 || host.rsplit('.').next().is_some_and(numeric_or_hex_label)
129 {
130 return Err(invalid_host(raw));
131 }
132 Ok(Self(format!("{prefix}{host}")))
133 }
134}
135
136fn numeric_or_hex_label(label: &str) -> bool {
137 label.bytes().all(|byte| byte.is_ascii_digit())
138 || label.strip_prefix("0x").is_some_and(|digits| {
139 !digits.is_empty() && digits.bytes().all(|b| b.is_ascii_hexdigit())
140 })
141}
142
143impl<'de> Deserialize<'de> for NetworkHost {
144 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
145 where
146 D: serde::Deserializer<'de>,
147 {
148 let value = String::deserialize(deserializer)?;
149 Self::from_str(&value).map_err(serde::de::Error::custom)
150 }
151}
152
153fn valid_dns_host(host: &str) -> bool {
154 !host.is_empty()
155 && host.len() <= 253
156 && host.split('.').all(|label| {
157 !label.is_empty()
158 && label.len() <= 63
159 && label.is_ascii()
160 && label
161 .bytes()
162 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
163 && label
164 .as_bytes()
165 .first()
166 .is_some_and(u8::is_ascii_alphanumeric)
167 && label
168 .as_bytes()
169 .last()
170 .is_some_and(u8::is_ascii_alphanumeric)
171 })
172}
173
174fn invalid_host(raw: &str) -> String {
175 format!(
176 "invalid network host `{raw}`; expected {}",
177 NetworkHost::ALLOWED
178 )
179}