use std::fmt;
use std::net::IpAddr;
use axum::http::HeaderMap;
use axum::http::header::USER_AGENT;
const MAX_USER_AGENT_LENGTH: usize = 512;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Prefix {
addr: IpAddr,
bits: u8,
}
impl Prefix {
fn contains(&self, ip: IpAddr) -> bool {
match (self.addr, ip) {
(IpAddr::V4(net), IpAddr::V4(ip)) => {
masked(u32::from(net).into(), self.bits, 32)
== masked(u32::from(ip).into(), self.bits, 32)
}
(IpAddr::V6(net), IpAddr::V6(ip)) => {
masked(u128::from(net), self.bits, 128) == masked(u128::from(ip), self.bits, 128)
}
_ => false,
}
}
}
fn masked(value: u128, bits: u8, width: u8) -> u128 {
if bits == 0 {
return 0;
}
value >> (width - bits)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InvalidProxy(pub String);
impl fmt::Display for InvalidProxy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"seamless-auth: {:?} is not an address or CIDR subnet",
self.0
)
}
}
impl std::error::Error for InvalidProxy {}
#[derive(Clone, Debug)]
pub struct TrustedProxies {
prefixes: Vec<Prefix>,
}
impl TrustedProxies {
pub fn new<I, S>(proxies: I) -> Result<Self, InvalidProxy>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut prefixes = Vec::new();
for proxy in proxies {
let raw = proxy.as_ref().trim();
let prefix = match raw.split_once('/') {
Some((addr, bits)) => {
let addr: IpAddr = addr.parse().map_err(|_| InvalidProxy(raw.to_string()))?;
let bits: u8 = bits.parse().map_err(|_| InvalidProxy(raw.to_string()))?;
if bits > width(addr) {
return Err(InvalidProxy(raw.to_string()));
}
Prefix { addr, bits }
}
None => {
let addr: IpAddr = raw.parse().map_err(|_| InvalidProxy(raw.to_string()))?;
Prefix {
addr,
bits: width(addr),
}
}
};
prefixes.push(prefix);
}
Ok(TrustedProxies { prefixes })
}
fn trusted(&self, ip: IpAddr) -> bool {
let ip = ip.to_canonical();
self.prefixes.iter().any(|p| p.contains(ip))
}
pub fn resolve(&self, headers: &HeaderMap, peer: Option<IpAddr>) -> Option<IpAddr> {
let peer = peer?;
if !self.trusted(peer) {
return Some(peer);
}
let hops: Vec<&str> = headers
.get_all("x-forwarded-for")
.iter()
.filter_map(|v| v.to_str().ok())
.flat_map(|v| v.split(','))
.map(str::trim)
.filter(|hop| !hop.is_empty())
.collect();
for hop in hops.iter().rev() {
match hop.parse::<IpAddr>() {
Ok(ip) if self.trusted(ip) => continue,
Ok(ip) => return Some(ip),
Err(_) => return None,
}
}
match hops.first() {
Some(first) => first.parse().ok(),
None => Some(peer),
}
}
}
fn width(addr: IpAddr) -> u8 {
match addr {
IpAddr::V4(_) => 32,
IpAddr::V6(_) => 128,
}
}
pub(crate) fn canonical(ip: IpAddr) -> String {
ip.to_canonical().to_string()
}
pub(crate) fn client_user_agent(headers: &HeaderMap) -> Option<Vec<u8>> {
let raw = headers.get(USER_AGENT)?.as_bytes();
let trimmed = raw.trim_ascii();
if trimmed.is_empty() {
return None;
}
Some(trimmed[..trimmed.len().min(MAX_USER_AGENT_LENGTH)].to_vec())
}