use std::net::IpAddr;
use http::{HeaderMap, HeaderName, Request};
use crate::{Error, header::extract_single_header_text};
pub const FORWARDED: HeaderName = HeaderName::from_static("forwarded");
pub fn extract_header_forwarded_for(headers: &HeaderMap) -> Result<Option<Vec<IpAddr>>, Error> {
extract_single_header_text(headers, &FORWARDED)?
.map(parse_forwarded_for)
.transpose()
}
pub fn extract_request_forwarded_for<B>(
request: &Request<B>,
) -> Result<Option<Vec<IpAddr>>, Error> {
extract_header_forwarded_for(request.headers())
}
pub fn extract_rightmost_forwarded(headers: &HeaderMap) -> Result<Option<IpAddr>, Error> {
Ok(extract_header_forwarded_for(headers)?.and_then(|ips| ips.last().copied()))
}
fn parse_forwarded_for(value: &str) -> Result<Vec<IpAddr>, Error> {
split_quoted(value, ',')?
.into_iter()
.map(parse_forwarded_element_for)
.collect()
}
fn parse_forwarded_element_for(element: &str) -> Result<IpAddr, Error> {
if trim_ows(element).is_empty() {
return Err(invalid());
}
let mut forwarded_for = None;
for parameter in split_quoted(element, ';')? {
let parameter = trim_ows(parameter);
if parameter.is_empty() {
return Err(invalid());
}
let Some((name, value)) = parameter.split_once('=') else {
continue;
};
if !trim_ows(name).eq_ignore_ascii_case("for") {
continue;
}
if forwarded_for.is_some() {
return Err(invalid());
}
let (value, quoted) = parse_parameter_value(value)?;
forwarded_for = Some(parse_forwarded_node(&value, quoted)?);
}
forwarded_for.ok_or_else(invalid)
}
fn parse_forwarded_node(value: &str, quoted: bool) -> Result<IpAddr, Error> {
if !quoted {
return value
.parse::<std::net::Ipv4Addr>()
.map(IpAddr::V4)
.map_err(|_| invalid());
}
if let Some(rest) = value.strip_prefix('[') {
let (address, suffix) = rest.split_once(']').ok_or_else(invalid)?;
if !suffix.is_empty() {
let port = suffix.strip_prefix(':').ok_or_else(invalid)?;
validate_node_port(port)?;
}
return address
.parse::<std::net::Ipv6Addr>()
.map(IpAddr::V6)
.map_err(|_| invalid());
}
let (address, port) = value
.split_once(':')
.map_or((value, None), |(address, port)| (address, Some(port)));
if let Some(port) = port {
validate_node_port(port)?;
}
address
.parse::<std::net::Ipv4Addr>()
.map(IpAddr::V4)
.map_err(|_| invalid())
}
fn validate_node_port(value: &str) -> Result<(), Error> {
let numeric =
!value.is_empty() && value.len() <= 5 && value.bytes().all(|byte| byte.is_ascii_digit());
let obfuscated =
value.len() > 1 && value.starts_with('_') && value.bytes().all(is_obfuscated_byte);
if numeric || obfuscated {
Ok(())
} else {
Err(invalid())
}
}
fn split_quoted(value: &str, delimiter: char) -> Result<Vec<&str>, Error> {
let mut output = Vec::new();
let mut quoted = false;
let mut escaped = false;
let mut start = 0;
for (index, character) in value.char_indices() {
if escaped {
escaped = false;
} else if quoted && character == '\\' {
escaped = true;
} else if character == '"' {
quoted = !quoted;
} else if !quoted && character == delimiter {
output.push(&value[start..index]);
start = index + character.len_utf8();
}
}
if quoted || escaped {
return Err(invalid());
}
output.push(&value[start..]);
Ok(output)
}
fn parse_parameter_value(value: &str) -> Result<(String, bool), Error> {
let value = trim_ows(value);
if !value.starts_with('"') {
if value.is_empty() || !value.bytes().all(is_token_byte) {
return Err(invalid());
}
return Ok((value.to_owned(), false));
}
if value.len() < 2 || !value.ends_with('"') {
return Err(invalid());
}
let mut output = String::with_capacity(value.len() - 2);
let mut escaped = false;
for character in value[1..value.len() - 1].chars() {
if escaped {
if !is_quoted_pair_character(character) {
return Err(invalid());
}
output.push(character);
escaped = false;
} else if character == '\\' {
escaped = true;
} else if is_quoted_text_character(character) {
output.push(character);
} else {
return Err(invalid());
}
}
if escaped {
return Err(invalid());
}
Ok((output, true))
}
fn trim_ows(value: &str) -> &str {
value.trim_matches([' ', '\t'])
}
const fn is_token_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric()
|| matches!(
byte,
b'!' | b'#'
| b'$'
| b'%'
| b'&'
| b'\''
| b'*'
| b'+'
| b'-'
| b'.'
| b'^'
| b'_'
| b'`'
| b'|'
| b'~'
)
}
const fn is_obfuscated_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-')
}
const fn is_quoted_text_character(character: char) -> bool {
matches!(character, '\t' | ' ' | '!' | '#'..='[' | ']'..='~')
}
const fn is_quoted_pair_character(character: char) -> bool {
matches!(character, '\t' | ' '..='~')
}
const fn invalid() -> Error {
Error::invalid_header(FORWARDED)
}
#[cfg(test)]
mod tests {
use std::net::IpAddr;
use http::{HeaderMap, HeaderValue};
use super::*;
#[test]
fn returns_none_when_forwarded_is_absent() {
assert_eq!(
extract_header_forwarded_for(&HeaderMap::new()).unwrap(),
None
);
}
#[test]
fn extracts_every_for_ip_in_wire_order() {
let mut headers = HeaderMap::new();
headers.insert(
&FORWARDED,
"for=192.0.2.60;proto=https, For=\"[2001:db8:cafe::17]:4711\";by=_edge, for=\"198.51.100.4:_port\";ext=\"a,b;c\""
.parse()
.unwrap(),
);
assert_eq!(
extract_header_forwarded_for(&headers).unwrap().unwrap(),
vec![
"192.0.2.60".parse::<IpAddr>().unwrap(),
"2001:db8:cafe::17".parse::<IpAddr>().unwrap(),
"198.51.100.4".parse::<IpAddr>().unwrap(),
]
);
}
#[test]
fn extracts_for_ip_without_parsing_other_parameters() {
let mut headers = HeaderMap::new();
headers.insert(
&FORWARDED,
"for=52.159.243.17;host=example.vercel.app;proto=https;sig=c2lnbmF0dXJlCg==;exp=1786417338"
.parse()
.unwrap(),
);
assert_eq!(
extract_header_forwarded_for(&headers).unwrap().unwrap(),
vec!["52.159.243.17".parse::<IpAddr>().unwrap()]
);
}
#[test]
fn ignores_malformed_parameters_other_than_for() {
for parameter in ["broken", "=value", "bad name=value", "proto="] {
let mut headers = HeaderMap::new();
headers.insert(
&FORWARDED,
format!("for=192.0.2.1;{parameter}").parse().unwrap(),
);
assert_eq!(
extract_header_forwarded_for(&headers).unwrap().unwrap(),
vec!["192.0.2.1".parse::<IpAddr>().unwrap()],
"unexpectedly rejected {parameter:?}"
);
}
}
#[test]
fn request_entry_point_delegates_to_headers() {
let request = Request::builder()
.header(&FORWARDED, "for=192.0.2.1")
.body(())
.unwrap();
assert_eq!(
extract_request_forwarded_for(&request).unwrap(),
Some(vec!["192.0.2.1".parse().unwrap()])
);
}
#[test]
fn rejects_a_duplicate_or_non_text_field() {
let mut headers = HeaderMap::new();
headers.append(&FORWARDED, "for=192.0.2.1".parse().unwrap());
headers.append(&FORWARDED, "for=198.51.100.2".parse().unwrap());
assert!(matches!(
extract_header_forwarded_for(&headers),
Err(Error::DuplicateHeader { .. })
));
headers.clear();
headers.insert(&FORWARDED, HeaderValue::from_bytes(&[0xff]).unwrap());
assert!(matches!(
extract_header_forwarded_for(&headers),
Err(Error::InvalidHeader { .. })
));
}
#[test]
fn rejects_any_element_that_cannot_extend_a_continuous_ip_chain() {
for value in [
"",
"for=192.0.2.1,",
"for=192.0.2.1,,for=198.51.100.2",
"proto=https",
"for=unknown",
"for=_hidden",
"for=example.com",
"for=192.0.2.1:443",
"for=\"[not-an-ip]\"",
"for=192.0.2.1;for=198.51.100.2",
"for=192.0.2.1;proto=\"unterminated",
] {
let mut headers = HeaderMap::new();
headers.insert(&FORWARDED, value.parse().unwrap());
assert!(
matches!(
extract_header_forwarded_for(&headers),
Err(Error::InvalidHeader { .. })
),
"unexpectedly accepted {value:?}"
);
}
}
}