use core::{
net::{IpAddr, Ipv4Addr, Ipv6Addr},
str::FromStr,
};
use crate::{
class,
error::{ErrorKind, ParseError, Result, err},
host::{HostPort, host_port},
limits::Limits,
list,
scan::control_kind,
text::{Word, parse_word, skip_ows, trim_ows},
};
pub trait Trust {
fn is_trusted(&self, addr: IpAddr) -> bool;
}
impl<F: Fn(IpAddr) -> bool> Trust for F {
fn is_trusted(&self, addr: IpAddr) -> bool {
self(addr)
}
}
#[derive(Clone, Copy, Debug)]
pub enum ForwardedSource<'a> {
Forwarded(&'a [&'a [u8]]),
XForwardedFor(&'a [&'a [u8]]),
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ClientAddress {
Peer(IpAddr),
Forwarded {
addr: IpAddr,
hops: u8,
},
Unknown {
hops: u8,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum NodePort<'a> {
Number(u16),
Obfuscated(&'a [u8]),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Node<'a> {
Addr(IpAddr, Option<NodePort<'a>>),
Unknown(Option<NodePort<'a>>),
Obfuscated(&'a [u8], Option<NodePort<'a>>),
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Proto {
Http,
Https,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Element<'a> {
pub for_: Option<Node<'a>>,
pub by: Option<Node<'a>>,
pub host: Option<HostPort<'a>>,
pub proto: Option<Proto>,
}
fn is_obfuscated(s: &[u8]) -> bool {
matches!(s, [b'_', rest @ ..] if !rest.is_empty()
&& rest.iter().all(|&b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-')))
}
fn node_port(s: &[u8]) -> Option<NodePort<'_>> {
if is_obfuscated(s) {
return Some(NodePort::Obfuscated(s));
}
if s.is_empty() || s.len() > 5 || !s.iter().all(u8::is_ascii_digit) {
return None;
}
let n = s.iter().fold(0u32, |n, &d| n * 10 + u32::from(d - b'0'));
u16::try_from(n).ok().map(NodePort::Number)
}
fn ip(text: &[u8], v6: bool) -> Option<IpAddr> {
let t = core::str::from_utf8(text).ok()?;
if v6 {
Ipv6Addr::from_str(t).ok().map(IpAddr::V6)
} else {
Ipv4Addr::from_str(t).ok().map(IpAddr::V4)
}
}
fn node(v: &[u8]) -> Result<Node<'_>> {
let (name, rest) = if v.first() == Some(&b'[') {
match v.iter().position(|&b| b == b']') {
Some(close) => (
v.get(..=close).unwrap_or(&[]),
v.get(close + 1..).unwrap_or(&[]),
),
None => return err(ErrorKind::InvalidAddress, 0),
}
} else {
match v.iter().position(|&b| b == b':') {
Some(c) => (v.get(..c).unwrap_or(&[]), v.get(c..).unwrap_or(&[])),
None => (v, &[][..]),
}
};
let port = match rest.split_first() {
None => None,
Some((b':', p)) => match node_port(p) {
Some(p) => Some(p),
None => return err(ErrorKind::InvalidPort, name.len() + 1),
},
Some(_) => return err(ErrorKind::InvalidAddress, name.len()),
};
if name == b"unknown" {
return Ok(Node::Unknown(port));
}
if is_obfuscated(name) {
return Ok(Node::Obfuscated(name, port));
}
let addr = match name {
[b'[', inner @ .., b']'] => ip(inner, true),
_ => ip(name, false),
};
match addr {
Some(a) => Ok(Node::Addr(a, port)),
None => err(ErrorKind::InvalidAddress, 0),
}
}
fn element(e: &[u8]) -> Result<Element<'_>> {
let mut out = Element {
for_: None,
by: None,
host: None,
proto: None,
};
let mut seen = [false; 4];
let mut i = 0usize;
loop {
i += skip_ows(e.get(i..).unwrap_or(&[]));
match e.get(i) {
None => return Ok(out),
Some(b';') => {
i += 1;
continue;
}
Some(_) => {}
}
let name_len = e
.get(i..)
.unwrap_or(&[])
.iter()
.take_while(|&&b| class::is_tchar(b))
.count();
if name_len == 0 {
return Err(refused(e, i, ErrorKind::NotToken));
}
let name_at = i;
let name = e.get(i..i + name_len).unwrap_or(&[]);
i += name_len;
if e.get(i) != Some(&b'=') {
return Err(refused(e, i, ErrorKind::MissingValue));
}
i += 1;
let (w, used) = parse_word(e.get(i..).unwrap_or(&[])).map_err(|x| match x.kind() {
ErrorKind::Empty => refused(e, i, ErrorKind::InvalidByte),
_ => x.shifted(i),
})?;
let Some(v) = w.as_plain() else {
return err(ErrorKind::InvalidQuotedPair, i);
};
let value_at = i + usize::from(matches!(w, Word::Quoted(_)));
i += used;
let slot = [&b"for"[..], b"by", b"host", b"proto"]
.iter()
.position(|k| name.eq_ignore_ascii_case(k));
if let Some(s) = slot {
if seen.get(s).copied().unwrap_or(false) {
return err(ErrorKind::DuplicateParameter, name_at);
}
if let Some(flag) = seen.get_mut(s) {
*flag = true;
}
}
match slot {
Some(0) => out.for_ = Some(node(v).map_err(|x| x.shifted(value_at))?),
Some(1) => out.by = Some(node(v).map_err(|x| x.shifted(value_at))?),
Some(2) => out.host = Some(host_port(v).map_err(|x| x.shifted(value_at))?),
Some(3) => {
out.proto = Some(if v.eq_ignore_ascii_case(b"http") {
Proto::Http
} else if v.eq_ignore_ascii_case(b"https") {
Proto::Https
} else {
return err(ErrorKind::Malformed, value_at);
});
}
_ => {}
}
i += skip_ows(e.get(i..).unwrap_or(&[]));
match e.get(i) {
None => return Ok(out),
Some(b';') => i += 1,
Some(_) => return Err(refused(e, i, ErrorKind::ExpectedSeparator)),
}
}
}
fn refused(e: &[u8], at: usize, kind: ErrorKind) -> ParseError {
match e.get(at) {
Some(&b) if crate::scan::is_control(b) => ParseError::new(control_kind(b), at),
_ => ParseError::new(kind, at),
}
}
#[derive(Clone, Copy, Debug)]
pub struct ForwardedElements<'a> {
lines: &'a [&'a [u8]],
count: usize,
}
impl<'a> ForwardedElements<'a> {
pub fn iter(&self) -> impl Iterator<Item = Element<'a>> + use<'a> {
let lines = self.lines;
lines
.iter()
.filter_map(|line| list::parse(line, &Limits::DEFAULT).ok())
.flat_map(|l| l.into_iter())
.filter_map(|e| element(e).ok())
}
#[must_use]
pub const fn len(&self) -> usize {
self.count
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.count == 0
}
}
pub fn parse<'a>(lines: &'a [&'a [u8]], limits: &Limits) -> Result<ForwardedElements<'a>> {
let mut count = 0usize;
for (n, line) in lines.iter().enumerate() {
let l = list::parse(line, limits).map_err(|e| e.on_line(n))?;
for e in l.iter() {
let at = (e.as_ptr() as usize).saturating_sub(line.as_ptr() as usize);
element(e)
.map(|_| ())
.map_err(|x| x.shifted(at).on_line(n))?;
count += 1;
if count > usize::from(limits.hops) {
return Err(ParseError::new(ErrorKind::TooManyHops, at).on_line(n));
}
}
}
Ok(ForwardedElements { lines, count })
}
fn xff_member(m: &[u8]) -> Option<(IpAddr, Option<u16>)> {
let port = |p: &[u8]| -> Option<Option<u16>> {
match node_port(p)? {
NodePort::Number(n) => Some(Some(n)),
NodePort::Obfuscated(_) => None,
}
};
if let [b'[', ..] = m {
let close = m.iter().position(|&b| b == b']')?;
let a = ip(m.get(1..close)?, true)?;
return match m.get(close + 1..)? {
[] => Some((a, None)),
[b':', p @ ..] => Some((a, port(p)?)),
_ => None,
};
}
if let Some(a) = ip(m, true) {
return Some((a, None));
}
match m.iter().position(|&b| b == b':') {
Some(c) => Some((ip(m.get(..c)?, false)?, port(m.get(c + 1..)?)?)),
None => Some((ip(m, false)?, None)),
}
}
fn xff_line(line: &[u8]) -> impl Iterator<Item = &[u8]> {
line.split(|&b| b == b',').map(trim_ows)
}
#[derive(Clone, Copy, Debug)]
pub struct XffChain<'a> {
lines: &'a [&'a [u8]],
count: usize,
}
impl<'a> XffChain<'a> {
pub fn iter(&self) -> impl Iterator<Item = (IpAddr, Option<u16>)> + use<'a> {
self.lines
.iter()
.flat_map(|line| xff_line(line))
.filter_map(xff_member)
}
#[must_use]
pub const fn len(&self) -> usize {
self.count
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.count == 0
}
}
pub fn parse_x_forwarded_for<'a>(lines: &'a [&'a [u8]], limits: &Limits) -> Result<XffChain<'a>> {
let mut count = 0usize;
for (n, line) in lines.iter().enumerate() {
crate::scan::check_field_value(line).map_err(|e| e.on_line(n))?;
for m in xff_line(line) {
let at = (m.as_ptr() as usize).saturating_sub(line.as_ptr() as usize);
if xff_member(m).is_none() {
return Err(ParseError::new(ErrorKind::Malformed, at).on_line(n));
}
count += 1;
if count > usize::from(limits.hops) {
return Err(ParseError::new(ErrorKind::TooManyHops, at).on_line(n));
}
}
}
Ok(XffChain { lines, count })
}
pub fn parse_x_forwarded_proto(value: &[u8]) -> Result<Proto> {
let mut found: Option<Proto> = None;
for m in value.split(|&b| b == b',') {
let at = (m.as_ptr() as usize).saturating_sub(value.as_ptr() as usize);
let t = trim_ows(m);
let p = if t.eq_ignore_ascii_case(b"http") {
Proto::Http
} else if t.eq_ignore_ascii_case(b"https") {
Proto::Https
} else {
return err(ErrorKind::Malformed, at);
};
if found.is_some_and(|f| f != p) {
return err(ErrorKind::Malformed, at);
}
found = Some(p);
}
found.ok_or(ParseError::new(ErrorKind::Malformed, 0))
}
pub fn parse_x_forwarded_host(value: &[u8]) -> Result<HostPort<'_>> {
if let Some(at) = crate::scan::find_byte(b',', value) {
return err(ErrorKind::Malformed, at);
}
crate::host::parse_host_port(value)
}
enum Step {
Addr(IpAddr),
Unknown,
}
fn walk(
count: usize,
trust: &impl Trust,
mut step: impl FnMut(usize) -> Step,
) -> Option<ClientAddress> {
let mut leftmost: Option<IpAddr> = None;
for (walked, i) in (0..count).rev().enumerate() {
let hops = u8::try_from(walked + 1).unwrap_or(u8::MAX);
match step(i) {
Step::Addr(a) if trust.is_trusted(a) => leftmost = Some(a),
Step::Addr(addr) => return Some(ClientAddress::Forwarded { addr, hops }),
Step::Unknown => return Some(ClientAddress::Unknown { hops }),
}
}
leftmost.map(|addr| ClientAddress::Forwarded {
addr,
hops: u8::try_from(count).unwrap_or(u8::MAX),
})
}
fn nth_member<'a, I: Iterator<Item = &'a [u8]>>(
lines: &[&'a [u8]],
i: usize,
members: impl Fn(&'a [u8]) -> I,
) -> Option<&'a [u8]> {
lines.iter().flat_map(|line| members(line)).nth(i)
}
pub fn client_address(
peer: IpAddr,
source: ForwardedSource<'_>,
trust: &impl Trust,
max_hops: u8,
) -> Result<ClientAddress> {
if !trust.is_trusted(peer) {
return Ok(ClientAddress::Peer(peer));
}
let limits = Limits::DEFAULT.max_hops(max_hops);
let found = match source {
ForwardedSource::XForwardedFor(lines) => {
let chain = parse_x_forwarded_for(lines, &limits).map_err(as_malformed)?;
walk(chain.len(), trust, |i| {
match nth_member(lines, i, xff_line).and_then(xff_member) {
Some((a, _)) => Step::Addr(a),
None => Step::Unknown,
}
})
}
ForwardedSource::Forwarded(lines) => {
let elements = parse(lines, &limits).map_err(as_malformed)?;
walk(elements.len(), trust, |i| {
let e = nth_member(lines, i, |line| {
list::parse(line, &Limits::DEFAULT)
.ok()
.into_iter()
.flat_map(|l| l.into_iter())
})
.and_then(|e| element(e).ok());
match e.and_then(|e| e.for_) {
Some(Node::Addr(a, _)) => Step::Addr(a),
_ => Step::Unknown,
}
})
}
};
Ok(found.unwrap_or(ClientAddress::Peer(peer)))
}
fn as_malformed(e: ParseError) -> ParseError {
if e.kind() == ErrorKind::TooManyHops {
e
} else {
ParseError::new(ErrorKind::Malformed, e.offset()).on_line(e.line())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Resolved {
pub address: ClientAddress,
pub other_present: bool,
}
pub fn client_address_detailed(
peer: IpAddr,
source: ForwardedSource<'_>,
other_lines: &[&[u8]],
trust: &impl Trust,
max_hops: u8,
) -> Result<Resolved> {
Ok(Resolved {
address: client_address(peer, source, trust, max_hops)?,
other_present: other_lines.iter().any(|l| !trim_ows(l).is_empty()),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn ip4(s: &str) -> IpAddr {
s.parse().unwrap()
}
#[test]
fn test_forwarded_rfc_examples() {
let l = Limits::DEFAULT;
let one = |v: &'static [u8]| -> Element<'static> {
let lines: &'static [&'static [u8]] = std::boxed::Box::leak(std::boxed::Box::new([v]));
parse(lines, &l).unwrap().iter().next().unwrap()
};
let e = one(b"for=\"_gazonk\"");
assert_eq!(e.for_, Some(Node::Obfuscated(b"_gazonk", None)));
let e = one(b"For=\"[2001:db8:cafe::17]:4711\"");
assert!(matches!(
e.for_,
Some(Node::Addr(IpAddr::V6(_), Some(NodePort::Number(4711))))
));
let e = one(b"for=192.0.2.60;proto=http;by=203.0.113.43");
assert_eq!(
(e.proto, e.by),
(
Some(Proto::Http),
Some(Node::Addr(ip4("203.0.113.43"), None))
)
);
assert_eq!(one(b"for=unknown").for_, Some(Node::Unknown(None)));
assert_eq!(
one(b"for=\"10.0.0.1:_p1\"").for_,
Some(Node::Addr(
ip4("10.0.0.1"),
Some(NodePort::Obfuscated(b"_p1"))
))
);
assert_eq!(
one(b"host=example.com; proto=https").proto,
Some(Proto::Https)
);
assert_eq!(
one(b";;for=1.2.3.4;").for_,
Some(Node::Addr(ip4("1.2.3.4"), None))
);
}
#[test]
fn test_forwarded_refusals() {
let l = Limits::DEFAULT;
let k = |v: &[u8]| parse(&[v], &l).unwrap_err().kind();
assert_ne!(k(b"for=2001:db8::1"), ErrorKind::TooManyHops);
assert_eq!(k(b"for=1.2.3.4;for=5.6.7.8"), ErrorKind::DuplicateParameter);
assert_eq!(k(b"for=1.2.3"), ErrorKind::InvalidAddress);
assert_eq!(k(b"for=01.2.3.4"), ErrorKind::InvalidAddress);
assert_eq!(k(b"for=\"[::1]:99999\""), ErrorKind::InvalidPort);
assert_eq!(k(b"proto=ftp"), ErrorKind::Malformed);
assert_eq!(k(b"for"), ErrorKind::MissingValue);
assert_eq!(k(b"for=\"a\\\"b\""), ErrorKind::InvalidQuotedPair);
assert_eq!(k(b"for=1.2.3.4 x"), ErrorKind::ExpectedSeparator);
let seventeen = [b"for=1.1.1.1,".repeat(17)].concat();
assert_eq!(
parse(&[&seventeen[..]], &l).unwrap_err().kind(),
ErrorKind::TooManyHops
);
let sixteen = [b"for=1.1.1.1,".repeat(16)].concat();
assert_eq!(parse(&[&sixteen[..]], &l).unwrap().len(), 16);
}
#[test]
fn test_xff_shapes() {
let l = Limits::DEFAULT;
let ok = |v: &[u8]| parse_x_forwarded_for(&[v], &l).map(|c| c.len());
assert_eq!(ok(b"1.2.3.4"), Ok(1));
assert_eq!(ok(b" 1.2.3.4 , 5.6.7.8:80 "), Ok(2));
assert_eq!(ok(b"2001:db8::1, [2001:db8::2]:443, [::1]"), Ok(3));
for bad in [
&b"1.2.3.4, , 5.6.7.8"[..],
b"",
b"garbage",
b"1.2.3.4:x",
b"2001:db8::1:80:",
b"[::1",
b"1.2.3.4\r",
] {
assert!(
ok(bad).is_err(),
"{}",
std::string::String::from_utf8_lossy(bad)
);
}
}
#[test]
fn test_xff_untrusted_peer_ignored() {
let t = |a: IpAddr| a == ip4("10.0.0.1");
let lines: [&[u8]; 1] = [b"garbage that would be refused"];
assert_eq!(
client_address(
ip4("192.0.2.9"),
ForwardedSource::XForwardedFor(&lines),
&t,
16
),
Ok(ClientAddress::Peer(ip4("192.0.2.9")))
);
}
#[test]
fn test_xff_leftmost_spoof_ignored() {
let t = |a: IpAddr| a == ip4("10.0.0.1") || a == ip4("10.0.0.2");
let lines: [&[u8]; 1] = [b"1.1.1.1, 203.0.113.7, 10.0.0.2"];
assert_eq!(
client_address(
ip4("10.0.0.1"),
ForwardedSource::XForwardedFor(&lines),
&t,
16
),
Ok(ClientAddress::Forwarded {
addr: ip4("203.0.113.7"),
hops: 2
})
);
}
#[test]
fn test_xff_malformed_fails_closed() {
let t = |_: IpAddr| true;
let lines: [&[u8]; 2] = [b"1.1.1.1", b"10.0.0.2, nope"];
let e = client_address(
ip4("10.0.0.1"),
ForwardedSource::XForwardedFor(&lines),
&t,
16,
)
.unwrap_err();
assert_eq!((e.kind(), e.line()), (ErrorKind::Malformed, 1));
let too_many: [&[u8]; 1] = [b"1.1.1.1, 1.1.1.2, 1.1.1.3"];
assert_eq!(
client_address(
ip4("10.0.0.1"),
ForwardedSource::XForwardedFor(&too_many),
&t,
2
)
.unwrap_err()
.kind(),
ErrorKind::TooManyHops
);
}
#[test]
fn test_client_address_walk_cases() {
let t = |a: IpAddr| a.is_loopback() || a == ip4("10.0.0.1");
let peer = ip4("10.0.0.1");
let x = |v: &'static [u8]| {
client_address(
peer,
ForwardedSource::XForwardedFor(std::boxed::Box::leak(std::boxed::Box::new([v]))),
&t,
16,
)
};
assert_eq!(
x(b"127.0.0.1, 10.0.0.1"),
Ok(ClientAddress::Forwarded {
addr: ip4("127.0.0.1"),
hops: 2
})
);
let none: [&[u8]; 0] = [];
assert_eq!(
client_address(peer, ForwardedSource::XForwardedFor(&none), &t, 16),
Ok(ClientAddress::Peer(peer))
);
let f = |v: &'static [u8]| {
client_address(
peer,
ForwardedSource::Forwarded(std::boxed::Box::leak(std::boxed::Box::new([v]))),
&t,
16,
)
};
assert_eq!(
f(b"for=192.0.2.1, for=unknown, for=127.0.0.1"),
Ok(ClientAddress::Unknown { hops: 2 })
);
assert_eq!(
f(b"for=192.0.2.1, for=_hidden"),
Ok(ClientAddress::Unknown { hops: 1 })
);
assert_eq!(
f(b"for=192.0.2.1, proto=https"),
Ok(ClientAddress::Unknown { hops: 1 })
);
assert_eq!(
f(b"for=\"[2001:db8::7]\""),
Ok(ClientAddress::Forwarded {
addr: "2001:db8::7".parse().unwrap(),
hops: 1
})
);
assert_eq!(
f(b"for=2001:db8::7").unwrap_err().kind(),
ErrorKind::Malformed
);
}
#[test]
fn test_both_forwarded_headers_reported() {
let t = |_: IpAddr| true;
let xff: [&[u8]; 1] = [b"192.0.2.1"];
let fwd: [&[u8]; 1] = [b"for=198.51.100.1"];
let r = client_address_detailed(
ip4("10.0.0.1"),
ForwardedSource::XForwardedFor(&xff),
&fwd,
&t,
16,
)
.unwrap();
assert!(r.other_present);
assert_eq!(
r.address,
ClientAddress::Forwarded {
addr: ip4("192.0.2.1"),
hops: 1
}
);
let empty: [&[u8]; 1] = [b" "];
assert!(
!client_address_detailed(
ip4("10.0.0.1"),
ForwardedSource::XForwardedFor(&xff),
&empty,
&t,
16
)
.unwrap()
.other_present
);
}
#[test]
fn test_x_forwarded_proto_and_host() {
assert_eq!(parse_x_forwarded_proto(b"http, HTTP"), Ok(Proto::Http));
assert!(parse_x_forwarded_proto(b"").is_err());
assert!(parse_x_forwarded_proto(b"wss").is_err());
assert!(parse_x_forwarded_host(b"[::1]:8080").is_ok());
assert_eq!(
parse_x_forwarded_host(b"a,b").unwrap_err().kind(),
ErrorKind::Malformed
);
}
}