rustauth_core/utils/
host.rs1use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum HostKind {
6 Public,
7 Localhost,
8 Loopback,
9 Private,
10 LinkLocal,
11 CloudMetadata,
12 Documentation,
13 SharedAddressSpace,
14 Benchmarking,
15 Multicast,
16 Broadcast,
17 Unspecified,
18 Reserved,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum HostLiteral {
24 Ipv4,
25 Ipv6,
26 Fqdn,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct HostClassification {
32 pub original: String,
33 pub canonical: String,
34 pub literal: HostLiteral,
35 pub kind: HostKind,
36}
37
38pub fn classify_host(host: &str) -> HostClassification {
40 let original = host.to_owned();
41 let canonical = normalize_host(host);
42
43 if canonical.is_empty() {
44 return HostClassification {
45 original,
46 canonical,
47 literal: HostLiteral::Fqdn,
48 kind: HostKind::Reserved,
49 };
50 }
51
52 if let Ok(ip) = canonical.parse::<IpAddr>() {
53 return classify_ip(original, ip);
54 }
55
56 let kind = classify_domain(&canonical);
57 HostClassification {
58 original,
59 canonical,
60 literal: HostLiteral::Fqdn,
61 kind,
62 }
63}
64
65pub fn is_loopback_ip(host: &str) -> bool {
67 let classification = classify_host(host);
68 matches!(
69 classification.literal,
70 HostLiteral::Ipv4 | HostLiteral::Ipv6
71 ) && classification.kind == HostKind::Loopback
72}
73
74pub fn is_loopback_host(host: &str) -> bool {
76 matches!(
77 classify_host(host).kind,
78 HostKind::Localhost | HostKind::Loopback
79 )
80}
81
82pub fn is_public_routable_host(host: &str) -> bool {
84 classify_host(host).kind == HostKind::Public
85}
86
87fn normalize_host(host: &str) -> String {
88 let mut value = host.trim().to_ascii_lowercase();
89 if value.is_empty() {
90 return value;
91 }
92
93 if let Some(stripped) = value
94 .strip_prefix('[')
95 .and_then(|rest| rest.split_once(']'))
96 {
97 value = stripped.0.to_owned();
98 } else if value.matches(':').count() == 1 {
99 if let Some((without_port, port)) = value.rsplit_once(':') {
100 if !without_port.is_empty() && port.chars().all(|character| character.is_ascii_digit())
101 {
102 value = without_port.to_owned();
103 }
104 }
105 }
106
107 if let Some((without_zone, _)) = value.split_once('%') {
108 value = without_zone.to_owned();
109 }
110
111 value.trim_end_matches('.').to_owned()
112}
113
114fn classify_ip(original: String, ip: IpAddr) -> HostClassification {
115 match ip {
116 IpAddr::V4(ip) => HostClassification {
117 original,
118 canonical: ip.to_string(),
119 literal: HostLiteral::Ipv4,
120 kind: classify_ipv4(ip),
121 },
122 IpAddr::V6(ip) => HostClassification {
123 original,
124 canonical: ip.to_string(),
125 literal: HostLiteral::Ipv6,
126 kind: classify_ipv6(ip),
127 },
128 }
129}
130
131fn classify_ipv4(ip: Ipv4Addr) -> HostKind {
132 let octets = ip.octets();
133
134 if ip.is_loopback() {
135 HostKind::Loopback
136 } else if ip.is_unspecified() {
137 HostKind::Unspecified
138 } else if ip == Ipv4Addr::BROADCAST {
139 HostKind::Broadcast
140 } else if ip.is_private() {
141 HostKind::Private
142 } else if ip.is_link_local() {
143 HostKind::LinkLocal
144 } else if ip.is_documentation() {
145 HostKind::Documentation
146 } else if ip.is_multicast() {
147 HostKind::Multicast
148 } else if octets[0] == 100 && (64..=127).contains(&octets[1]) {
149 HostKind::SharedAddressSpace
150 } else if octets[0] == 198 && matches!(octets[1], 18 | 19) {
151 HostKind::Benchmarking
152 } else if octets[0] == 0 || octets[0] >= 240 {
153 HostKind::Reserved
154 } else {
155 HostKind::Public
156 }
157}
158
159fn classify_ipv6(ip: Ipv6Addr) -> HostKind {
160 let segments = ip.segments();
161 let first_segment = segments[0];
162
163 if ip.is_loopback() {
164 HostKind::Loopback
165 } else if ip.is_unspecified() {
166 HostKind::Unspecified
167 } else if ip.is_multicast() {
168 HostKind::Multicast
169 } else if (first_segment & 0xfe00) == 0xfc00 {
170 HostKind::Private
171 } else if (first_segment & 0xffc0) == 0xfe80 {
172 HostKind::LinkLocal
173 } else if first_segment == 0x2001 && segments[1] == 0x0db8 {
174 HostKind::Documentation
175 } else {
176 HostKind::Public
177 }
178}
179
180fn classify_domain(host: &str) -> HostKind {
181 if host == "localhost" || host.ends_with(".localhost") {
182 HostKind::Localhost
183 } else if matches!(
184 host,
185 "metadata.google.internal"
186 | "metadata.goog"
187 | "instance-data.ec2.internal"
188 | "169.254.169.254"
189 ) {
190 HostKind::CloudMetadata
191 } else {
192 HostKind::Public
193 }
194}