use core::{
net::{IpAddr, Ipv4Addr, Ipv6Addr},
str::FromStr,
};
use crate::{
class,
error::{ErrorKind, ParseError, Result, WriteError, err},
host::{Host, HostPort, host_port},
limits::Limits,
list,
param::{MAX_CHECKED_PARAMS, Names},
scan::control_kind,
text::{Word, is_token, parse_word, skip_ows, trim_ows},
write::{CountSink, Sink, reserve},
};
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,
Ws,
Wss,
Other,
}
fn proto(v: &[u8]) -> Option<Proto> {
const NAMES: [(&[u8], Proto); 4] = [
(b"http", Proto::Http),
(b"https", Proto::Https),
(b"ws", Proto::Ws),
(b"wss", Proto::Wss),
];
if let Some(&(_, p)) = NAMES.iter().find(|(n, _)| v.eq_ignore_ascii_case(n)) {
return Some(p);
}
let (first, rest) = v.split_first()?;
let scheme = first.is_ascii_alphabetic()
&& rest
.iter()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'+' | b'-' | b'.'));
scheme.then_some(Proto::Other)
}
#[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>,
}
const RELAXED: Limits = Limits::DEFAULT
.max_list_members(u16::MAX)
.max_empty_elements(u16::MAX)
.max_comment_depth(u8::MAX);
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> {
if v6 {
let t = core::str::from_utf8(text).ok()?;
Ipv6Addr::from_str(t).ok().map(IpAddr::V6)
} else {
ipv4(text).map(IpAddr::V4)
}
}
fn ipv4_prefix(mut s: &[u8]) -> Option<(Ipv4Addr, &[u8])> {
let mut octets = [0u8; 4];
for (i, o) in octets.iter_mut().enumerate() {
if i > 0 {
let [b'.', rest @ ..] = s else {
return None;
};
s = rest;
}
let [first @ b'0'..=b'9', rest @ ..] = s else {
return None;
};
let (mut n, mut len) = (u16::from(*first - b'0'), 1usize);
s = rest;
while let [d @ b'0'..=b'9', rest @ ..] = s {
len += 1;
if len > 3 || *first == b'0' {
return None;
}
n = n * 10 + u16::from(*d - b'0');
s = rest;
}
*o = u8::try_from(n).ok()?;
}
Some((Ipv4Addr::from(octets), s))
}
fn ipv4(text: &[u8]) -> Option<Ipv4Addr> {
match ipv4_prefix(text)? {
(a, []) => Some(a),
_ => None,
}
}
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.eq_ignore_ascii_case(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 names, mut pairs) = (Names::new(), 0usize);
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;
pairs += 1;
if pairs > MAX_CHECKED_PARAMS {
return err(ErrorKind::TooManyMembers, name_at);
}
if names.repeats(e, name_at, name, class::is_tchar) {
return err(ErrorKind::DuplicateParameter, name_at);
}
let slot = [&b"for"[..], b"by", b"host", b"proto"]
.iter()
.position(|k| name.eq_ignore_ascii_case(k));
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) => match proto(v) {
Some(p) => out.proto = Some(p),
None => 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, &RELAXED).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,
};
}
match ipv4_prefix(m) {
Some((a, [])) => Some((IpAddr::V4(a), None)),
Some((a, [b':', p @ ..])) => Some((IpAddr::V4(a), port(p)?)),
Some(_) => None,
None => ip(m, true).map(|a| (a, 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, &[u8])> = 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 Some(p) = proto(t) else {
return err(ErrorKind::Malformed, at);
};
if found.is_some_and(|(_, f)| !f.eq_ignore_ascii_case(t)) {
return err(ErrorKind::Malformed, at);
}
found = Some((p, t));
}
found
.map(|(p, _)| p)
.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)
}
fn xff_client(
lines: &[&[u8]],
limits: &Limits,
trust: &impl Trust,
) -> Option<Option<ClientAddress>> {
for line in lines {
crate::scan::check_field_value(line).ok()?;
}
let hops = |n: usize| u8::try_from(n).unwrap_or(u8::MAX);
let (mut count, mut found, mut leftmost) = (0usize, None, None);
for line in lines.iter().rev() {
for m in line.rsplit(|&b| b == b',') {
let (addr, _) = xff_member(trim_ows(m))?;
count += 1;
if count > usize::from(limits.hops) {
return None;
}
if found.is_none() {
if trust.is_trusted(addr) {
leftmost = Some(addr);
} else {
found = Some(ClientAddress::Forwarded {
addr,
hops: hops(count),
});
}
}
}
}
Some(found.or(leftmost.map(|addr| ClientAddress::Forwarded {
addr,
hops: hops(count),
})))
}
fn forwarded_client(elements: &ForwardedElements<'_>, trust: &impl Trust) -> Option<ClientAddress> {
let count = elements.len();
let hops = |n: usize| u8::try_from(n).unwrap_or(u8::MAX);
let (mut last, mut leftmost) = (None, None);
for (i, e) in elements.iter().enumerate() {
match e.for_ {
Some(Node::Addr(a, _)) if trust.is_trusted(a) => {
leftmost = leftmost.or(Some(a));
}
Some(Node::Addr(addr, _)) => {
last = Some(ClientAddress::Forwarded {
addr,
hops: hops(count - i),
});
}
_ => {
last = Some(ClientAddress::Unknown {
hops: hops(count - i),
});
}
}
}
last.or(leftmost.map(|addr| ClientAddress::Forwarded {
addr,
hops: hops(count),
}))
}
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) => match xff_client(lines, &limits, trust) {
Some(found) => found,
None => {
let _ = parse_x_forwarded_for(lines, &limits).map_err(as_malformed)?;
return err(ErrorKind::Malformed, 0);
}
},
ForwardedSource::Forwarded(lines) => {
let elements = parse(lines, &limits).map_err(as_malformed)?;
forwarded_client(&elements, trust)
}
};
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()),
})
}
fn put_node(n: &Node<'_>, out: &mut impl Sink) -> core::result::Result<(), WriteError> {
let port = match *n {
Node::Addr(ip, port) => {
let host = match ip {
IpAddr::V4(a) => Host::V4(a),
IpAddr::V6(a) => Host::V6(a),
};
HostPort {
host,
port: None,
empty_port: false,
}
.write_to(out)?;
port
}
Node::Unknown(port) => {
out.put(b"unknown")?;
port
}
Node::Obfuscated(name, port) => {
if !is_obfuscated(name) {
return Err(WriteError::NotToken);
}
out.put(name)?;
port
}
};
match port {
None => {}
Some(NodePort::Number(p)) => {
out.put(b":")?;
crate::write::write_decimal_u64(u64::from(p), out)?;
}
Some(NodePort::Obfuscated(p)) => {
if !is_obfuscated(p) {
return Err(WriteError::NotToken);
}
out.put(b":")?;
out.put(p)?;
}
}
Ok(())
}
fn put_pair(
name: &[u8],
n: &Node<'_>,
first: &mut bool,
out: &mut impl Sink,
) -> core::result::Result<(), WriteError> {
let quote = matches!(n, Node::Addr(IpAddr::V6(_), _))
|| matches!(
n,
Node::Addr(_, Some(_)) | Node::Unknown(Some(_)) | Node::Obfuscated(_, Some(_))
);
if !*first {
out.put(b";")?;
}
*first = false;
out.put(name)?;
out.put(b"=")?;
if quote {
out.put(b"\"")?;
}
put_node(n, out)?;
if quote {
out.put(b"\"")?;
}
Ok(())
}
fn put_element(e: &Element<'_>, out: &mut impl Sink) -> core::result::Result<(), WriteError> {
let mut first = true;
if let Some(n) = &e.for_ {
put_pair(b"for", n, &mut first, out)?;
}
if let Some(p) = e.proto {
if !first {
out.put(b";")?;
}
first = false;
out.put(match p {
Proto::Http => b"proto=http",
Proto::Https => b"proto=https",
Proto::Ws => b"proto=ws",
Proto::Wss => b"proto=wss",
Proto::Other => return Err(WriteError::NotToken),
})?;
}
if let Some(n) = &e.by {
put_pair(b"by", n, &mut first, out)?;
}
if let Some(h) = &e.host {
if !first {
out.put(b";")?;
}
out.put(b"host=")?;
let quote = h.port.is_some()
|| match h.host {
Host::V4(_) => false,
Host::V6(_) | Host::Future(_) => true,
Host::Name(n) => !is_token(n),
};
if quote {
out.put(b"\"")?;
}
h.write_to(out)?;
if quote {
out.put(b"\"")?;
}
}
Ok(())
}
pub fn write_element(e: &Element<'_>, out: &mut impl Sink) -> core::result::Result<(), WriteError> {
let mut count = CountSink::new();
put_element(e, &mut count)?;
reserve(out, count.count())?;
put_element(e, out)
}
#[cfg(test)]
mod tests {
use std::vec::Vec;
use super::*;
#[test]
fn test_write_element_round_trip() {
let v6: IpAddr = "2001:db8:cafe::17".parse().unwrap();
let cases = [
Element {
for_: Some(Node::Addr(ip4("192.0.2.43"), None)),
by: None,
host: None,
proto: None,
},
Element {
for_: Some(Node::Addr(v6, Some(NodePort::Number(4711)))),
by: Some(Node::Obfuscated(
b"_hidden",
Some(NodePort::Obfuscated(b"_p1")),
)),
host: Some(crate::parse_host_port(b"example.com:8080").unwrap()),
proto: Some(Proto::Http),
},
Element {
for_: Some(Node::Unknown(Some(NodePort::Number(80)))),
by: Some(Node::Addr(v6, None)),
host: Some(crate::parse_host_port(b"[::1]").unwrap()),
proto: Some(Proto::Https),
},
];
for e in cases {
let mut buf = [0u8; 128];
let mut out = crate::SliceSink::new(&mut buf);
write_element(&e, &mut out).unwrap();
let lines = [out.written()];
let parsed = parse(&lines, &Limits::DEFAULT).unwrap();
assert_eq!(
parsed.iter().next(),
Some(e),
"{:?}",
core::str::from_utf8(out.written())
);
}
let bad = Element {
for_: Some(Node::Obfuscated(b"hidden", None)),
by: None,
host: None,
proto: None,
};
let mut buf = [0u8; 8];
let mut out = crate::SliceSink::new(&mut buf);
assert_eq!(write_element(&bad, &mut out), Err(WriteError::NotToken));
let big = Element {
for_: Some(Node::Addr(v6, None)),
by: None,
host: None,
proto: None,
};
assert_eq!(write_element(&big, &mut out), Err(WriteError::Full));
assert!(out.is_empty());
}
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=1x"), 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_eq!(parse_x_forwarded_proto(b"wss"), Ok(Proto::Wss));
assert_eq!(parse_x_forwarded_proto(b"ftp"), Ok(Proto::Other));
assert!(parse_x_forwarded_proto(b"ftp, sftp").is_err());
assert!(parse_x_forwarded_proto(b"1x").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
);
}
mod reference {
use super::super::*;
pub(super) 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,
};
}
let v4 = |t: &[u8]| {
core::str::from_utf8(t)
.ok()?
.parse::<Ipv4Addr>()
.ok()
.map(IpAddr::V4)
};
let mut colons = m.iter().enumerate().filter(|&(_, &b)| b == b':');
match (colons.next(), colons.next()) {
(None, _) => Some((v4(m)?, None)),
(Some((c, _)), None) => Some((v4(m.get(..c)?)?, port(m.get(c + 1..)?)?)),
(Some(_), Some(_)) => Some((ip(m, true)?, None)),
}
}
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(super) 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 v4(s: &str) -> IpAddr {
s.parse().unwrap()
}
proptest::proptest! {
#![proptest_config(proptest::prelude::ProptestConfig::with_cases(4096))]
#[test]
fn test_xff_member_agrees_with_reference(
text in "[0-9a-f.:\\[\\]]{0,28}",
shaped in "(25[0-9]|1?[0-9]{1,2})\\.(0|[0-9]{1,3})\\.[0-9]{1,3}\\.[0-9]{1,3}(:[0-9_a]{0,6})?",
) {
proptest::prop_assert_eq!(xff_member(text.as_bytes()), reference::xff_member(text.as_bytes()));
proptest::prop_assert_eq!(xff_member(shaped.as_bytes()), reference::xff_member(shaped.as_bytes()));
}
#[test]
fn test_client_address_agrees_with_reference(
xff in proptest::collection::vec(proptest::sample::select(&[
"10.0.0.1", "10.0.0.2", "1.2.3.4", "198.51.100.7:80", "[2001:db8::1]:443",
"2001:db8::2", "::1", "", "bad", "01.2.3.4", "1.2.3", "1.2.3.4:99999", "[::1",
][..]), 0..12),
fwd in proptest::collection::vec(proptest::sample::select(&[
"for=10.0.0.1", "for=10.0.0.2;proto=http", "for=1.2.3.4", "for=unknown",
"for=_hidden", "proto=https", "for=\"[2001:db8::1]:80\"", "for=\"::1\"",
"by=10.0.0.1;for=198.51.100.7", "bad", "for=", "",
][..]), 0..12),
cuts in proptest::collection::vec(proptest::bool::ANY, 12),
max_hops in 0u8..10,
trusted_peer in proptest::bool::ANY,
) {
fn lines<'a>(members: &[&str], cuts: &[bool], store: &'a mut Vec<std::string::String>) -> Vec<&'a [u8]> {
let mut cur = std::string::String::new();
for (i, m) in members.iter().enumerate() {
if i > 0 && cuts[i] {
store.push(core::mem::take(&mut cur));
} else if i > 0 {
cur.push_str(", ");
}
cur.push_str(m);
}
if !members.is_empty() {
store.push(cur);
}
store.iter().map(|s| s.as_bytes()).collect()
}
let peer = if trusted_peer { v4("10.0.0.1") } else { v4("203.0.113.1") };
let trust = |a: IpAddr| a == v4("10.0.0.1") || a == v4("10.0.0.2") || a == v4("::1");
let shape = |r: Result<ClientAddress>| r.map_err(|e| (e.kind(), e.offset(), e.line()));
let mut store = Vec::new();
let ls = lines(&xff, &cuts, &mut store);
proptest::prop_assert_eq!(
shape(client_address(peer, ForwardedSource::XForwardedFor(&ls), &trust, max_hops)),
shape(reference::client_address(peer, ForwardedSource::XForwardedFor(&ls), &trust, max_hops))
);
let mut store = Vec::new();
let ls = lines(&fwd, &cuts, &mut store);
proptest::prop_assert_eq!(
shape(client_address(peer, ForwardedSource::Forwarded(&ls), &trust, max_hops)),
shape(reference::client_address(peer, ForwardedSource::Forwarded(&ls), &trust, max_hops))
);
}
}
}