1use crate::request::Request;
40use std::net::IpAddr;
41use std::str::FromStr;
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct Cidr {
46 addr: IpAddr,
47 prefix: u8,
48}
49
50impl FromStr for Cidr {
51 type Err = String;
52
53 fn from_str(s: &str) -> Result<Self, String> {
54 let (addr, prefix) = match s.split_once('/') {
55 Some((a, p)) => (a, Some(p)),
56 None => (s, None),
57 };
58 let addr: IpAddr = addr
59 .trim()
60 .parse()
61 .map_err(|_| format!("invalid address in {s:?}"))?;
62 let addr = addr.to_canonical();
63 let max = if addr.is_ipv4() { 32 } else { 128 };
64 let prefix = match prefix {
65 Some(p) => p
66 .trim()
67 .parse::<u8>()
68 .ok()
69 .filter(|p| *p <= max)
70 .ok_or_else(|| format!("invalid prefix length in {s:?}"))?,
71 None => max,
72 };
73 Ok(Self { addr, prefix })
74 }
75}
76
77impl Cidr {
78 pub fn contains(&self, ip: IpAddr) -> bool {
79 let ip = ip.to_canonical();
80 match (self.addr, ip) {
81 (IpAddr::V4(net), IpAddr::V4(ip)) => {
82 prefix_matches(&net.octets(), &ip.octets(), self.prefix)
83 }
84 (IpAddr::V6(net), IpAddr::V6(ip)) => {
85 prefix_matches(&net.octets(), &ip.octets(), self.prefix)
86 }
87 _ => false,
88 }
89 }
90}
91
92fn prefix_matches(net: &[u8], ip: &[u8], prefix: u8) -> bool {
93 let full = (prefix / 8) as usize;
94 if net[..full] != ip[..full] {
95 return false;
96 }
97 let rest = prefix % 8;
98 rest == 0 || {
99 let mask = 0xffu8 << (8 - rest);
100 net[full] & mask == ip[full] & mask
101 }
102}
103
104#[derive(Debug, Clone, Default)]
106pub struct TrustedProxies {
107 nets: Vec<Cidr>,
108}
109
110impl TrustedProxies {
111 pub fn new() -> Self {
113 Self::default()
114 }
115
116 pub fn trust(mut self, cidr: &str) -> Result<Self, String> {
118 self.nets.push(cidr.parse()?);
119 self.nets.dedup();
120 Ok(self)
121 }
122
123 pub fn private_networks() -> Self {
127 let mut t = Self::new();
128 for net in [
129 "127.0.0.0/8",
130 "10.0.0.0/8",
131 "172.16.0.0/12",
132 "192.168.0.0/16",
133 "169.254.0.0/16",
134 "::1/128",
135 "fc00::/7",
136 "fe80::/10",
137 ] {
138 t.nets.push(net.parse().expect("built-in network"));
139 }
140 t
141 }
142
143 pub fn contains(&self, ip: IpAddr) -> bool {
144 self.nets.iter().any(|n| n.contains(ip))
145 }
146
147 pub fn is_empty(&self) -> bool {
148 self.nets.is_empty()
149 }
150}
151
152fn parse_hop(raw: &str) -> Option<IpAddr> {
155 let s = raw.trim().trim_matches('"');
156 if let Some(rest) = s.strip_prefix('[') {
157 return rest.split(']').next()?.parse().ok();
158 }
159 if let Ok(ip) = s.parse() {
160 return Some(ip);
161 }
162 let (host, port) = s.rsplit_once(':')?;
164 port.parse::<u16>().ok()?;
165 host.parse().ok()
166}
167
168fn forwarded_hops(value: &str) -> Vec<(Option<String>, Option<String>)> {
170 value
171 .split(',')
172 .map(|element| {
173 let mut for_ = None;
174 let mut proto = None;
175 for pair in element.split(';') {
176 if let Some((k, v)) = pair.split_once('=') {
177 let v = v.trim().trim_matches('"').to_string();
178 match k.trim().to_ascii_lowercase().as_str() {
179 "for" => for_ = Some(v),
180 "proto" => proto = Some(v.to_ascii_lowercase()),
181 _ => {}
182 }
183 }
184 }
185 (for_, proto)
186 })
187 .collect()
188}
189
190impl Request {
191 pub fn client_ip(&self) -> Option<IpAddr> {
194 let peer = self.remote_addr?.ip();
195 let Some(trusted) = self.trusted_proxies.as_deref() else {
196 return Some(peer);
197 };
198 if !trusted.contains(peer) {
199 return Some(peer);
200 }
201 let hops: Vec<String> = match self.headers.get("forwarded").and_then(|v| v.to_str().ok()) {
202 Some(f) => forwarded_hops(f)
203 .into_iter()
204 .map(|(for_, _)| for_.unwrap_or_default())
205 .collect(),
206 None => self
207 .headers
208 .get_all("x-forwarded-for")
209 .iter()
210 .filter_map(|v| v.to_str().ok())
211 .flat_map(|v| v.split(','))
212 .map(|h| h.trim().to_string())
213 .collect(),
214 };
215 let mut known = peer;
216 for hop in hops.iter().rev() {
217 match parse_hop(hop) {
218 Some(ip) if trusted.contains(ip) => known = ip,
219 Some(ip) => return Some(ip),
220 None => return Some(known),
222 }
223 }
224 Some(known)
225 }
226
227 pub(crate) fn forwarded_https(&self) -> bool {
229 let (Some(trusted), Some(peer)) = (self.trusted_proxies.as_deref(), self.remote_addr)
230 else {
231 return false;
232 };
233 if !trusted.contains(peer.ip()) {
234 return false;
235 }
236 if let Some(f) = self.headers.get("forwarded").and_then(|v| v.to_str().ok()) {
238 if let Some((_, Some(proto))) = forwarded_hops(f).into_iter().next_back() {
239 return proto == "https";
240 }
241 }
242 self.headers
243 .get("x-forwarded-proto")
244 .and_then(|v| v.to_str().ok())
245 .and_then(|v| v.rsplit(',').next())
246 .is_some_and(|v| v.trim().eq_ignore_ascii_case("https"))
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 fn ip(s: &str) -> IpAddr {
255 s.parse().unwrap()
256 }
257
258 #[test]
259 fn cidr_matching() {
260 let net: Cidr = "10.1.0.0/16".parse().unwrap();
261 assert!(net.contains(ip("10.1.200.3")));
262 assert!(!net.contains(ip("10.2.0.1")));
263 let odd: Cidr = "172.16.0.0/12".parse().unwrap();
264 assert!(odd.contains(ip("172.31.255.255")));
265 assert!(!odd.contains(ip("172.32.0.1")));
266 let host: Cidr = "203.0.113.7".parse().unwrap();
267 assert!(host.contains(ip("203.0.113.7")) && !host.contains(ip("203.0.113.8")));
268 let v6: Cidr = "fd00::/8".parse().unwrap();
269 assert!(v6.contains(ip("fd12::1")) && !v6.contains(ip("fe80::1")));
270 assert!(!net.contains(ip("fd00::1")));
272 assert!(net.contains(ip("::ffff:10.1.0.9")));
273 let all: Cidr = "0.0.0.0/0".parse().unwrap();
274 assert!(all.contains(ip("8.8.8.8")));
275 }
276
277 #[test]
278 fn cidr_rejects_garbage() {
279 for bad in [
280 "",
281 "10.0.0.0/33",
282 "10.0.0.0/x",
283 "nope",
284 "::1/129",
285 "10.0.0/8",
286 ] {
287 assert!(bad.parse::<Cidr>().is_err(), "{bad}");
288 }
289 }
290
291 #[test]
292 fn hop_parsing() {
293 assert_eq!(parse_hop("203.0.113.9"), Some(ip("203.0.113.9")));
294 assert_eq!(parse_hop(" 203.0.113.9:4711 "), Some(ip("203.0.113.9")));
295 assert_eq!(parse_hop("\"[2001:db8::1]:4711\""), Some(ip("2001:db8::1")));
296 assert_eq!(parse_hop("2001:db8::1"), Some(ip("2001:db8::1")));
297 assert_eq!(parse_hop("unknown"), None);
298 assert_eq!(parse_hop("_hidden"), None);
299 assert_eq!(parse_hop(""), None);
300 }
301
302 #[test]
303 fn private_networks_preset() {
304 let t = TrustedProxies::private_networks();
305 for private in [
306 "127.0.0.1",
307 "10.9.9.9",
308 "192.168.1.1",
309 "172.20.0.1",
310 "::1",
311 "fd00::5",
312 ] {
313 assert!(t.contains(ip(private)), "{private}");
314 }
315 for public in ["8.8.8.8", "172.32.0.1", "2001:db8::1"] {
316 assert!(!t.contains(ip(public)), "{public}");
317 }
318 assert!(TrustedProxies::new().is_empty());
319 }
320}