seamless_auth/
client_ip.rs1use std::fmt;
2use std::net::IpAddr;
3
4use axum::http::HeaderMap;
5use axum::http::header::USER_AGENT;
6
7const MAX_USER_AGENT_LENGTH: usize = 512;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11struct Prefix {
12 addr: IpAddr,
13 bits: u8,
14}
15
16impl Prefix {
17 fn contains(&self, ip: IpAddr) -> bool {
18 match (self.addr, ip) {
19 (IpAddr::V4(net), IpAddr::V4(ip)) => {
20 masked(u32::from(net).into(), self.bits, 32)
21 == masked(u32::from(ip).into(), self.bits, 32)
22 }
23 (IpAddr::V6(net), IpAddr::V6(ip)) => {
24 masked(u128::from(net), self.bits, 128) == masked(u128::from(ip), self.bits, 128)
25 }
26 _ => false,
27 }
28 }
29}
30
31fn masked(value: u128, bits: u8, width: u8) -> u128 {
32 if bits == 0 {
33 return 0;
34 }
35 value >> (width - bits)
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct InvalidProxy(pub String);
41
42impl fmt::Display for InvalidProxy {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 write!(
45 f,
46 "seamless-auth: {:?} is not an address or CIDR subnet",
47 self.0
48 )
49 }
50}
51
52impl std::error::Error for InvalidProxy {}
53
54#[derive(Clone, Debug)]
61pub struct TrustedProxies {
62 prefixes: Vec<Prefix>,
63}
64
65impl TrustedProxies {
66 pub fn new<I, S>(proxies: I) -> Result<Self, InvalidProxy>
67 where
68 I: IntoIterator<Item = S>,
69 S: AsRef<str>,
70 {
71 let mut prefixes = Vec::new();
72 for proxy in proxies {
73 let raw = proxy.as_ref().trim();
74 let prefix = match raw.split_once('/') {
75 Some((addr, bits)) => {
76 let addr: IpAddr = addr.parse().map_err(|_| InvalidProxy(raw.to_string()))?;
77 let bits: u8 = bits.parse().map_err(|_| InvalidProxy(raw.to_string()))?;
78 if bits > width(addr) {
79 return Err(InvalidProxy(raw.to_string()));
80 }
81 Prefix { addr, bits }
82 }
83 None => {
84 let addr: IpAddr = raw.parse().map_err(|_| InvalidProxy(raw.to_string()))?;
85 Prefix {
86 addr,
87 bits: width(addr),
88 }
89 }
90 };
91 prefixes.push(prefix);
92 }
93 Ok(TrustedProxies { prefixes })
94 }
95
96 fn trusted(&self, ip: IpAddr) -> bool {
97 let ip = ip.to_canonical();
98 self.prefixes.iter().any(|p| p.contains(ip))
99 }
100
101 pub fn resolve(&self, headers: &HeaderMap, peer: Option<IpAddr>) -> Option<IpAddr> {
103 let peer = peer?;
104 if !self.trusted(peer) {
105 return Some(peer);
106 }
107
108 let hops: Vec<&str> = headers
109 .get_all("x-forwarded-for")
110 .iter()
111 .filter_map(|v| v.to_str().ok())
112 .flat_map(|v| v.split(','))
113 .map(str::trim)
114 .filter(|hop| !hop.is_empty())
115 .collect();
116
117 for hop in hops.iter().rev() {
118 match hop.parse::<IpAddr>() {
119 Ok(ip) if self.trusted(ip) => continue,
120 Ok(ip) => return Some(ip),
121 Err(_) => return None,
124 }
125 }
126 match hops.first() {
127 Some(first) => first.parse().ok(),
128 None => Some(peer),
129 }
130 }
131}
132
133fn width(addr: IpAddr) -> u8 {
134 match addr {
135 IpAddr::V4(_) => 32,
136 IpAddr::V6(_) => 128,
137 }
138}
139
140pub(crate) fn canonical(ip: IpAddr) -> String {
142 ip.to_canonical().to_string()
143}
144
145pub(crate) fn client_user_agent(headers: &HeaderMap) -> Option<Vec<u8>> {
147 let raw = headers.get(USER_AGENT)?.as_bytes();
148 let trimmed = raw.trim_ascii();
149 if trimmed.is_empty() {
150 return None;
151 }
152 Some(trimmed[..trimmed.len().min(MAX_USER_AGENT_LENGTH)].to_vec())
153}