diff --git a/crates/client-api/src/util.rs b/crates/client-api/src/util.rs index 901a79f7d39..5b69fb0c713 100644 --- a/crates/client-api/src/util.rs +++ b/crates/client-api/src/util.rs @@ -48,8 +48,10 @@ impl headers::Header for XForwardedFor { fn decode<'i, I: Iterator>(values: &mut I) -> Result { let val = values.next().ok_or_else(headers::Error::invalid)?; let val = val.to_str().map_err(|_| headers::Error::invalid())?; - let (first, _) = val.split_once(',').ok_or_else(headers::Error::invalid)?; - let ip = first.trim().parse().map_err(|_| headers::Error::invalid())?; + // X-Forwarded-For is a comma-separated chain. For a single-hop + // proxy there is no comma; take the first IP either way. + let first = val.split(',').next().unwrap_or(val).trim(); + let ip = first.parse().map_err(|_| headers::Error::invalid())?; Ok(XForwardedFor(ip)) } @@ -183,3 +185,41 @@ impl FromRequest for EmptyBody { Ok(Self) } } + +#[cfg(test)] +mod tests { + use super::*; + use headers::Header; + + fn decode_one(raw: &str) -> Result { + let val = HeaderValue::from_str(raw).unwrap(); + let values = [val]; + XForwardedFor::decode(&mut values.iter()) + } + + #[test] + fn decodes_single_ip() { + // Single-hop proxies (e.g. nginx inserting the client IP) emit + // a value with no comma. This must succeed. + let got = decode_one("10.0.0.1").expect("single IP should decode"); + assert_eq!(got.0, "10.0.0.1".parse::().unwrap()); + } + + #[test] + fn decodes_chain_takes_first() { + let got = decode_one("10.0.0.1, 192.168.1.1, 172.16.0.1").expect("chain should decode"); + assert_eq!(got.0, "10.0.0.1".parse::().unwrap()); + } + + #[test] + fn decodes_chain_trims_whitespace() { + let got = decode_one(" 10.0.0.1 , 192.168.1.1").expect("chain should decode"); + assert_eq!(got.0, "10.0.0.1".parse::().unwrap()); + } + + #[test] + fn rejects_non_ip() { + assert!(decode_one("not-an-ip").is_err()); + assert!(decode_one("not-an-ip, 10.0.0.1").is_err()); + } +}