use std::net::{IpAddr, SocketAddr};
use http::{HeaderMap, HeaderName, Request};
use crate::Error;
pub const X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for");
pub const X_FORWARDED_PROTO: HeaderName = HeaderName::from_static("x-forwarded-proto");
pub fn extract_header_x_forwarded_for(headers: &HeaderMap) -> Result<Option<Vec<IpAddr>>, Error> {
extract_comma_values(headers, &X_FORWARDED_FOR, |value| {
parse_ip(value, X_FORWARDED_FOR)
})
}
pub fn extract_request_x_forwarded_for<B>(
request: &Request<B>,
) -> Result<Option<Vec<IpAddr>>, Error> {
extract_header_x_forwarded_for(request.headers())
}
pub fn extract_header_x_forwarded_proto(headers: &HeaderMap) -> Result<Option<Vec<String>>, Error> {
extract_comma_values(headers, &X_FORWARDED_PROTO, |value| {
if is_scheme(value) {
Ok(value.to_ascii_lowercase())
} else {
Err(Error::invalid_header(X_FORWARDED_PROTO))
}
})
}
pub fn extract_request_x_forwarded_proto<B>(
request: &Request<B>,
) -> Result<Option<Vec<String>>, Error> {
extract_header_x_forwarded_proto(request.headers())
}
pub fn extract_rightmost_x_forwarded_for(headers: &HeaderMap) -> Result<Option<IpAddr>, Error> {
Ok(extract_header_x_forwarded_for(headers)?.and_then(|ips| ips.last().copied()))
}
fn parse_ip(value: &str, name: HeaderName) -> Result<IpAddr, Error> {
if let Ok(address) = value.parse() {
return Ok(address);
}
if let Ok(address) = value.parse::<SocketAddr>() {
return Ok(address.ip());
}
Err(Error::invalid_header(name))
}
fn extract_comma_values<T>(
headers: &HeaderMap,
name: &HeaderName,
mut parse: impl FnMut(&str) -> Result<T, Error>,
) -> Result<Option<Vec<T>>, Error> {
let mut output = Vec::new();
let mut present = false;
for value in headers.get_all(name) {
present = true;
let value = value
.to_str()
.map_err(|_| Error::invalid_header(name.clone()))?;
for item in value.split(',') {
let item = item.trim();
if item.is_empty() {
return Err(Error::invalid_header(name.clone()));
}
output.push(parse(item)?);
}
}
Ok(present.then_some(output))
}
fn is_scheme(value: &str) -> bool {
let mut bytes = value.bytes();
matches!(bytes.next(), Some(byte) if byte.is_ascii_alphabetic())
&& bytes.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'-' | b'.'))
}
#[cfg(test)]
mod tests {
use std::net::{IpAddr, Ipv4Addr};
use http::{HeaderMap, HeaderValue};
use super::*;
#[test]
fn extracts_x_forwarded_for_across_field_lines() {
let mut headers = HeaderMap::new();
headers.append(&X_FORWARDED_FOR, "192.0.2.1, 198.51.100.2".parse().unwrap());
headers.append(&X_FORWARDED_FOR, "203.0.113.3".parse().unwrap());
assert_eq!(
extract_header_x_forwarded_for(&headers).unwrap().unwrap(),
vec![
IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1)),
IpAddr::V4(Ipv4Addr::new(198, 51, 100, 2)),
IpAddr::V4(Ipv4Addr::new(203, 0, 113, 3)),
]
);
}
#[test]
fn rejects_invalid_x_forwarded_for_values() {
let mut headers = HeaderMap::new();
for value in [
"[2001:db8::1]",
"[2001:db8::1]junk",
"[2001:db8::1]:65536",
"[2001:db8::1]:99999",
] {
headers.insert(&X_FORWARDED_FOR, value.parse().unwrap());
assert!(
matches!(
extract_header_x_forwarded_for(&headers),
Err(Error::InvalidHeader { .. })
),
"unexpectedly accepted {value:?}",
);
}
headers.insert(&X_FORWARDED_FOR, HeaderValue::from_bytes(&[0xff]).unwrap());
assert!(matches!(
extract_header_x_forwarded_for(&headers),
Err(Error::InvalidHeader { .. })
));
}
#[test]
fn accepts_bracketed_ipv6_with_valid_port() {
let mut headers = HeaderMap::new();
headers.insert(&X_FORWARDED_FOR, "[2001:db8::1]:65535".parse().unwrap());
assert_eq!(
extract_header_x_forwarded_for(&headers).unwrap(),
Some(vec!["2001:db8::1".parse().unwrap()]),
);
}
#[test]
fn extracts_and_normalizes_x_forwarded_proto() {
let mut headers = HeaderMap::new();
assert_eq!(extract_header_x_forwarded_proto(&headers).unwrap(), None);
headers.append(&X_FORWARDED_PROTO, "HTTPS, Web+TLS".parse().unwrap());
assert_eq!(
extract_header_x_forwarded_proto(&headers).unwrap().unwrap(),
vec!["https".to_owned(), "web+tls".to_owned()]
);
headers.insert(&X_FORWARDED_PROTO, "http_2".parse().unwrap());
assert!(matches!(
extract_header_x_forwarded_proto(&headers),
Err(Error::InvalidHeader { .. })
));
}
#[test]
fn request_entry_points_delegate_to_headers() {
let request = Request::builder()
.header(&X_FORWARDED_FOR, "192.0.2.1")
.header(&X_FORWARDED_PROTO, "HTTPS")
.body(())
.unwrap();
assert_eq!(
extract_request_x_forwarded_for(&request).unwrap(),
Some(vec!["192.0.2.1".parse().unwrap()])
);
assert_eq!(
extract_request_x_forwarded_proto(&request).unwrap(),
Some(vec!["https".to_owned()])
);
}
}