use std::collections::HashSet;
use std::net::IpAddr;
use std::str::FromStr;
use hyper::header::{HeaderName, HeaderValue};
use hyper::http::Uri;
use hyper::http::Version;
use hyper::http::uri::{Authority, Scheme};
use plecto_control::{Header, HttpRequest, TrustedProxyTrust};
const HOP_BY_HOP: &[&str] = &[
"connection",
"keep-alive",
"proxy-connection",
"transfer-encoding",
"te",
"trailer",
"upgrade",
"proxy-authorization",
"proxy-authenticate",
];
fn is_hop_by_hop(name: &str) -> bool {
HOP_BY_HOP.iter().any(|h| name.eq_ignore_ascii_case(h))
}
fn connection_named(map: &hyper::HeaderMap) -> HashSet<String> {
let mut named = HashSet::new();
for value in map.get_all(hyper::header::CONNECTION).iter() {
if let Ok(s) = value.to_str() {
for token in s.split(',') {
let token = token.trim();
if !token.is_empty()
&& !token.eq_ignore_ascii_case("close")
&& !token.eq_ignore_ascii_case("keep-alive")
{
named.insert(token.to_ascii_lowercase());
}
}
}
}
named
}
pub(crate) fn upgrade_request_header(map: &hyper::HeaderMap) -> Option<&str> {
if !connection_named(map).contains("upgrade") {
return None;
}
map.get(hyper::header::UPGRADE)?.to_str().ok()
}
pub(crate) fn te_requests_trailers(map: &hyper::HeaderMap) -> bool {
map.get_all(hyper::header::TE).iter().any(|value| {
value.to_str().is_ok_and(|s| {
s.split(',').any(|token| {
token
.split(';')
.next()
.is_some_and(|name| name.trim().eq_ignore_ascii_case("trailers"))
})
})
})
}
const FORWARDED_HEADERS: &[&str] = &[
"forwarded",
"x-forwarded-for",
"x-forwarded-proto",
"x-forwarded-host",
"x-real-ip",
"true-client-ip",
"cf-connecting-ip",
"fastly-client-ip",
"x-client-ip",
"x-cluster-client-ip",
];
const MAX_FORWARDED_ELEMENTS: usize = 32;
fn forwarded_client(headers: &hyper::HeaderMap, trusted: &TrustedProxyTrust) -> Option<IpAddr> {
let mut budget = MAX_FORWARDED_ELEMENTS;
for value in headers.get_all("x-forwarded-for").iter().rev() {
for element in value.to_str().ok()?.rsplit(',') {
budget = budget.checked_sub(1)?;
let ip: IpAddr = element.trim().parse().ok()?;
if !trusted.contains(ip) {
return Some(ip.to_canonical());
}
}
}
None
}
pub(crate) fn set_forwarded(
headers: &mut hyper::HeaderMap,
peer: IpAddr,
scheme: &str,
trusted: Option<&TrustedProxyTrust>,
) -> IpAddr {
let client = trusted
.filter(|trusted| trusted.contains(peer))
.and_then(|trusted| forwarded_client(headers, trusted))
.unwrap_or_else(|| peer.to_canonical());
for name in FORWARDED_HEADERS {
headers.remove(*name);
}
if let Ok(ip_value) = HeaderValue::from_str(&client.to_string()) {
headers.insert("x-forwarded-for", ip_value.clone());
headers.insert("x-real-ip", ip_value);
}
if let Ok(proto) = HeaderValue::from_str(scheme) {
headers.insert("x-forwarded-proto", proto);
}
client
}
pub(crate) fn normalize_ingress_authority(
parts: &mut hyper::http::request::Parts,
wire_scheme: &str,
) -> Result<(), ()> {
let uri_authority = parts
.uri
.authority()
.map(|authority| parse_authority(authority.as_str()))
.transpose()?;
let mut hosts = parts.headers.get_all(hyper::header::HOST).iter();
let host_authority = match hosts.next() {
Some(value) => Some(parse_authority(value.to_str().map_err(|_| ())?)?),
None => None,
};
if hosts.next().is_some() {
return Err(());
}
let scheme = parts.uri.scheme_str().unwrap_or(wire_scheme);
let transport_host_fallback = !matches!(parts.version, Version::HTTP_10 | Version::HTTP_11)
&& uri_authority.is_none()
&& host_authority.is_some();
let selected = match parts.version {
Version::HTTP_10 => match (uri_authority.as_deref(), host_authority.as_deref()) {
(Some(uri), Some(host)) if !authorities_match(uri, host, scheme) => return Err(()),
(Some(uri), _) => Some(uri),
(None, Some(host)) => Some(host),
(None, None) => None,
},
Version::HTTP_11 => {
let host = host_authority.as_deref().ok_or(())?;
match uri_authority.as_deref() {
Some(uri) if !authorities_match(uri, host, scheme) => return Err(()),
Some(uri) => Some(uri),
None => Some(host),
}
}
_ => match (uri_authority.as_deref(), host_authority.as_deref()) {
(Some(uri), Some(host)) if !authorities_match(uri, host, scheme) => return Err(()),
(Some(uri), _) => Some(uri),
(None, Some(host)) => Some(host),
(None, None) => return Err(()),
},
};
match selected {
Some(authority) => {
let value = HeaderValue::from_str(authority).map_err(|_| ())?;
parts.headers.insert(hyper::header::HOST, value);
if transport_host_fallback {
let mut uri_parts = parts.uri.clone().into_parts();
uri_parts.scheme = Some(Scheme::from_str(wire_scheme).map_err(|_| ())?);
uri_parts.authority = Some(Authority::from_str(authority).map_err(|_| ())?);
parts.uri = Uri::from_parts(uri_parts).map_err(|_| ())?;
}
}
None => {
parts.headers.remove(hyper::header::HOST);
}
}
Ok(())
}
fn parse_authority(value: &str) -> Result<String, ()> {
if value.is_empty() || value.contains('@') {
return Err(());
}
let authority = Authority::from_str(value).map_err(|_| ())?;
if !has_valid_port_syntax(value)
|| (authority.port().is_some() && authority.port_u16().is_none())
{
return Err(());
}
Ok(authority.to_string())
}
fn has_valid_port_syntax(authority: &str) -> bool {
let port = if let Some(bracketed) = authority.strip_prefix('[') {
let Some((_, suffix)) = bracketed.split_once(']') else {
return false;
};
match suffix.strip_prefix(':') {
Some(port) => port,
None if suffix.is_empty() => return true,
None => return false,
}
} else {
match authority.rsplit_once(':') {
Some((_, port)) => port,
None => return true,
}
};
!port.is_empty() && port.parse::<u16>().is_ok()
}
fn authorities_match(left: &str, right: &str, scheme: &str) -> bool {
let Ok(left) = Authority::from_str(left) else {
return false;
};
let Ok(right) = Authority::from_str(right) else {
return false;
};
let left_host = left.host().strip_suffix('.').unwrap_or(left.host());
let right_host = right.host().strip_suffix('.').unwrap_or(right.host());
if !left_host.eq_ignore_ascii_case(right_host) {
return false;
}
match (left.port_u16(), right.port_u16()) {
(Some(left), Some(right)) => left == right,
(None, None) => true,
(Some(port), None) | (None, Some(port)) => {
(scheme.eq_ignore_ascii_case("http") && port == 80)
|| (scheme.eq_ignore_ascii_case("https") && port == 443)
}
}
}
pub(crate) fn to_http_request(parts: &hyper::http::request::Parts, scheme: &str) -> HttpRequest {
let path = parts
.uri
.path_and_query()
.map(|pq| pq.as_str().to_string())
.unwrap_or_else(|| "/".to_string());
HttpRequest {
method: parts.method.as_str().to_string(),
path_with_query: path,
authority: request_authority(parts),
scheme: scheme.to_string(),
headers: headers_to_vec(&parts.headers),
}
}
pub(crate) fn request_authority(parts: &hyper::http::request::Parts) -> String {
parts
.uri
.authority()
.map(|a| a.to_string())
.or_else(|| {
parts
.headers
.get(hyper::header::HOST)
.and_then(|v| v.to_str().ok())
.map(str::to_string)
})
.unwrap_or_default()
}
pub(crate) fn headers_to_vec(map: &hyper::HeaderMap) -> Vec<Header> {
let named = connection_named(map);
map.iter()
.filter(|(name, _)| {
!is_hop_by_hop(name.as_str()) && (named.is_empty() || !named.contains(name.as_str()))
})
.map(|(name, value)| Header {
name: name.as_str().to_string(),
value: value.as_bytes().to_vec(),
})
.collect()
}
pub(crate) fn copy_headers(dst: Option<&mut hyper::HeaderMap>, headers: &[Header]) {
copy_headers_filtered(dst, headers, false);
}
pub(crate) fn copy_headers_synth(dst: Option<&mut hyper::HeaderMap>, headers: &[Header]) {
copy_headers_filtered(dst, headers, true);
}
fn copy_headers_filtered(
dst: Option<&mut hyper::HeaderMap>,
headers: &[Header],
strip_content_length: bool,
) {
let Some(dst) = dst else { return };
for h in headers {
if is_hop_by_hop(&h.name) {
continue;
}
if strip_content_length && h.name.eq_ignore_ascii_case("content-length") {
continue;
}
if let (Ok(name), Ok(value)) = (
HeaderName::from_bytes(h.name.as_bytes()),
HeaderValue::from_bytes(&h.value),
) {
dst.append(name, value);
}
}
}
pub(crate) fn copy_headers_direct(dst: Option<&mut hyper::HeaderMap>, src: &hyper::HeaderMap) {
let Some(dst) = dst else { return };
let named = connection_named(src);
for (name, value) in src {
if is_hop_by_hop(name.as_str()) || (!named.is_empty() && named.contains(name.as_str())) {
continue;
}
dst.append(name.clone(), value.clone());
}
}
#[cfg(test)]
mod tests {
use super::*;
use hyper::Request;
fn parts(authority_in_uri: bool) -> hyper::http::request::Parts {
let uri = if authority_in_uri {
"https://h2.example/api/x"
} else {
"/api/x"
};
Request::builder()
.method("GET")
.uri(uri)
.header("host", "h1.example")
.body(())
.unwrap()
.into_parts()
.0
}
#[test]
fn scheme_reflects_tls_termination_not_a_hardcoded_value() {
assert_eq!(to_http_request(&parts(false), "https").scheme, "https");
assert_eq!(to_http_request(&parts(false), "http").scheme, "http");
}
#[test]
fn authority_prefers_h2_uri_authority_then_falls_back_to_host() {
assert_eq!(
to_http_request(&parts(true), "https").authority,
"h2.example"
);
assert_eq!(
to_http_request(&parts(false), "http").authority,
"h1.example"
);
}
fn request_parts(version: Version, uri: &str, hosts: &[&str]) -> hyper::http::request::Parts {
let mut builder = Request::builder().method("GET").version(version).uri(uri);
for host in hosts {
builder = builder.header(hyper::header::HOST, *host);
}
builder.body(()).unwrap().into_parts().0
}
#[test]
fn ingress_authority_rejects_ambiguous_or_invalid_http11_host() {
for (uri, hosts) in [
("http://public.example/", vec!["protected.example"]),
("/", vec!["public.example", "public.example"]),
("/", vec![]),
("/", vec!["user@public.example"]),
("/", vec!["public.example:abc"]),
("/", vec!["public.example:65536"]),
("/", vec!["public.example:"]),
] {
let mut parts = request_parts(Version::HTTP_11, uri, &hosts);
assert!(
normalize_ingress_authority(&mut parts, "http").is_err(),
"{uri:?} with {hosts:?} must be rejected"
);
}
}
#[test]
fn ingress_authority_canonicalizes_matching_authorities_for_all_transports() {
let cases = [
(
Version::HTTP_11,
"http://EXAMPLE.test./",
"example.test:80",
"EXAMPLE.test.",
),
(
Version::HTTP_2,
"https://[2001:db8::1]/",
"[2001:DB8::1]:443",
"[2001:db8::1]",
),
];
for (version, uri, host, want) in cases {
let mut parts = request_parts(version, uri, &[host]);
normalize_ingress_authority(&mut parts, "https").unwrap();
assert_eq!(
parts
.headers
.get(hyper::header::HOST)
.and_then(|v| v.to_str().ok()),
Some(want),
"{uri:?} must replace its matching literal Host with the URI authority"
);
}
let mut h2_without_host = request_parts(Version::HTTP_2, "https://h2.example/", &[]);
normalize_ingress_authority(&mut h2_without_host, "https").unwrap();
assert_eq!(
h2_without_host
.headers
.get(hyper::header::HOST)
.and_then(|v| v.to_str().ok()),
Some("h2.example")
);
for version in [Version::HTTP_2, Version::HTTP_3] {
let mut origin_form = request_parts(version, "/from-adapter", &["h2.example"]);
normalize_ingress_authority(&mut origin_form, "https").unwrap();
assert_eq!(
origin_form
.headers
.get(hyper::header::HOST)
.and_then(|v| v.to_str().ok()),
Some("h2.example"),
"a single Host remains the authority when {version:?} adapter URI is origin-form"
);
assert_eq!(
origin_form.uri.authority().map(Authority::as_str),
Some("h2.example")
);
}
}
#[test]
fn ingress_authority_rejects_non_default_port_conflicts_and_preserves_http10_compatibility() {
let mut non_default = request_parts(
Version::HTTP_11,
"http://example.test/",
&["example.test:8080"],
);
assert!(normalize_ingress_authority(&mut non_default, "http").is_err());
let mut legacy = request_parts(Version::HTTP_10, "/", &[]);
normalize_ingress_authority(&mut legacy, "http").unwrap();
assert!(legacy.headers.get(hyper::header::HOST).is_none());
assert_eq!(request_authority(&legacy), "");
}
fn header(name: &str, value: &str) -> Header {
Header {
name: name.to_string(),
value: value.as_bytes().to_vec(),
}
}
#[test]
fn hop_by_hop_set_is_recognised_case_insensitively() {
for h in [
"connection",
"Keep-Alive",
"PROXY-CONNECTION",
"Transfer-Encoding",
"te",
"Trailer",
"upgrade",
"Proxy-Authorization",
"Proxy-Authenticate",
] {
assert!(is_hop_by_hop(h), "{h} must be treated as hop-by-hop");
}
assert!(!is_hop_by_hop("x-api-key"));
assert!(!is_hop_by_hop("content-type"));
}
#[test]
fn hop_by_hop_list_matches_the_host_contract_strip_list() {
assert_eq!(HOP_BY_HOP, plecto_host::HOP_BY_HOP_GUEST_HEADERS);
}
#[test]
fn te_requests_trailers_matches_the_token_not_the_whole_value() {
for value in [
"trailers",
"Trailers",
"gzip, trailers",
"trailers;q=1",
"gzip;q=0.5, trailers",
] {
let mut map = hyper::HeaderMap::new();
map.insert(hyper::header::TE, HeaderValue::from_str(value).unwrap());
assert!(te_requests_trailers(&map), "{value:?} requests trailers");
}
for value in ["gzip", "compress, deflate", "trailersx"] {
let mut map = hyper::HeaderMap::new();
map.insert(hyper::header::TE, HeaderValue::from_str(value).unwrap());
assert!(!te_requests_trailers(&map), "{value:?} must not match");
}
assert!(
!te_requests_trailers(&hyper::HeaderMap::new()),
"no TE header, no trailers request"
);
}
#[test]
fn headers_to_vec_strips_hop_by_hop_on_ingress() {
let mut map = hyper::HeaderMap::new();
map.insert("x-keep", HeaderValue::from_static("1"));
map.insert(
hyper::header::CONNECTION,
HeaderValue::from_static("keep-alive"),
);
map.insert(
hyper::header::TRANSFER_ENCODING,
HeaderValue::from_static("chunked"),
);
map.insert(hyper::header::TE, HeaderValue::from_static("trailers"));
let out = headers_to_vec(&map);
assert!(
out.iter().all(|h| !is_hop_by_hop(&h.name)),
"no hop-by-hop header may survive the ingress conversion"
);
assert!(
out.iter().any(|h| h.name == "x-keep"),
"an end-to-end header is preserved"
);
}
#[test]
fn headers_to_vec_strips_connection_named_headers() {
let mut map = hyper::HeaderMap::new();
map.insert(
hyper::header::CONNECTION,
HeaderValue::from_static("X-Secret, close"),
);
map.append("x-secret", HeaderValue::from_static("leak"));
map.insert("x-keep", HeaderValue::from_static("1"));
let out = headers_to_vec(&map);
assert!(
!out.iter().any(|h| h.name.eq_ignore_ascii_case("x-secret")),
"a Connection-named header must be stripped"
);
assert!(
!out.iter()
.any(|h| h.name.eq_ignore_ascii_case("connection")),
"Connection itself is hop-by-hop"
);
assert!(
out.iter().any(|h| h.name == "x-keep"),
"an unrelated end-to-end header survives"
);
}
#[test]
fn copy_headers_forwards_non_utf8_bytes() {
let raw: &[u8] = &[0xC3, 0x28]; let contract = vec![Header {
name: "x-blob".to_string(),
value: raw.to_vec(),
}];
let mut dst = hyper::HeaderMap::new();
copy_headers(Some(&mut dst), &contract);
assert_eq!(
dst.get("x-blob").map(|v| v.as_bytes()),
Some(raw),
"a byte-valued header forwards byte-for-byte"
);
}
#[test]
fn set_forwarded_overwrites_spoofed_client_headers() {
let mut headers = hyper::HeaderMap::new();
headers.append("x-forwarded-for", HeaderValue::from_static("9.9.9.9"));
headers.append("x-forwarded-for", HeaderValue::from_static("8.8.4.4"));
headers.insert("forwarded", HeaderValue::from_static("for=10.0.0.1"));
headers.insert("x-real-ip", HeaderValue::from_static("9.9.9.9"));
headers.insert("cf-connecting-ip", HeaderValue::from_static("8.8.8.8"));
headers.insert("x-keep", HeaderValue::from_static("1"));
set_forwarded(&mut headers, "203.0.113.5".parse().unwrap(), "https", None);
let xff: Vec<&HeaderValue> = headers.get_all("x-forwarded-for").iter().collect();
assert_eq!(
xff,
vec!["203.0.113.5"],
"the spoofed XFF (all values) is replaced by the real peer (one value, not appended)"
);
assert_eq!(
headers.get("x-real-ip").map(HeaderValue::as_bytes),
Some(b"203.0.113.5".as_ref()),
"the spoofed X-Real-IP is replaced by the real peer (re-issued)"
);
assert!(
!headers.contains_key("forwarded"),
"a spoofed Forwarded header is stripped"
);
assert!(
!headers.contains_key("cf-connecting-ip"),
"a spoofed CDN client-IP header is stripped and not re-issued"
);
assert_eq!(
headers
.get("x-forwarded-proto")
.and_then(|v| v.to_str().ok()),
Some("https"),
"X-Forwarded-Proto reflects the connection scheme"
);
assert!(
headers.contains_key("x-keep"),
"an unrelated header is left intact"
);
}
#[test]
fn set_forwarded_normalises_ipv4_mapped_peer() {
let mut headers = hyper::HeaderMap::new();
set_forwarded(
&mut headers,
"::ffff:203.0.113.5".parse().unwrap(),
"https",
None,
);
for name in ["x-forwarded-for", "x-real-ip"] {
assert_eq!(
headers.get(name).and_then(|v| v.to_str().ok()),
Some("203.0.113.5"),
"an IPv4-mapped peer normalises to dotted IPv4 in {name}"
);
}
let mut headers = hyper::HeaderMap::new();
set_forwarded(&mut headers, "2001:db8::1".parse().unwrap(), "https", None);
assert_eq!(
headers.get("x-forwarded-for").and_then(|v| v.to_str().ok()),
Some("2001:db8::1"),
"a real IPv6 peer is kept as-is"
);
}
fn trusted(cidrs: &str) -> plecto_control::TrustedProxyTrust {
plecto_control::Manifest::from_toml(&format!(
"[listen.trusted_proxy]\ntrusted = [{cidrs}]\n"
))
.unwrap()
.listen
.trusted_proxy_trust()
.unwrap()
.expect("the section is declared")
}
fn xff(headers: &hyper::HeaderMap) -> Option<&str> {
headers.get("x-forwarded-for").and_then(|v| v.to_str().ok())
}
#[test]
fn set_forwarded_restores_the_client_named_by_a_trusted_proxy() {
let trust = trusted(r#""10.0.0.0/8", "192.0.2.0/24""#);
let mut headers = hyper::HeaderMap::new();
headers.insert(
"x-forwarded-for",
HeaderValue::from_static("198.51.100.7, 192.0.2.5, 10.0.0.1"),
);
headers.insert("x-real-ip", HeaderValue::from_static("9.9.9.9"));
let client = set_forwarded(
&mut headers,
"10.0.0.1".parse().unwrap(),
"https",
Some(&trust),
);
assert_eq!(client, "198.51.100.7".parse::<IpAddr>().unwrap());
assert_eq!(
xff(&headers),
Some("198.51.100.7"),
"the re-issued XFF carries the restored client, not the inbound list"
);
assert_eq!(
headers.get("x-real-ip").and_then(|v| v.to_str().ok()),
Some("198.51.100.7"),
"X-Real-IP is re-issued from the same restored client (ADR 000022 stays)"
);
}
#[test]
fn set_forwarded_reads_repeated_forwarded_headers_as_one_list() {
let trust = trusted(r#""10.0.0.0/8""#);
let mut headers = hyper::HeaderMap::new();
headers.append("x-forwarded-for", HeaderValue::from_static("203.0.113.9"));
headers.append(
"x-forwarded-for",
HeaderValue::from_static("198.51.100.7, 10.0.0.2"),
);
let client = set_forwarded(
&mut headers,
"10.0.0.1".parse().unwrap(),
"http",
Some(&trust),
);
assert_eq!(client, "198.51.100.7".parse::<IpAddr>().unwrap());
assert_eq!(xff(&headers), Some("198.51.100.7"));
}
#[test]
fn set_forwarded_falls_back_to_the_peer_when_every_element_is_trusted() {
let trust = trusted(r#""10.0.0.0/8""#);
let mut headers = hyper::HeaderMap::new();
headers.insert(
"x-forwarded-for",
HeaderValue::from_static("10.0.0.9, 10.0.0.1"),
);
let client = set_forwarded(
&mut headers,
"10.0.0.1".parse().unwrap(),
"http",
Some(&trust),
);
assert_eq!(client, "10.0.0.1".parse::<IpAddr>().unwrap());
assert_eq!(xff(&headers), Some("10.0.0.1"));
}
#[test]
fn set_forwarded_falls_back_to_the_peer_on_a_malformed_or_absent_list() {
let trust = trusted(r#""10.0.0.0/8""#);
for value in [
Some("not-an-ip"),
Some(""),
Some("198.51.100.7,"),
Some("198.51.100.7, , 10.0.0.1"),
Some("198.51.100.7:443"),
Some("for=198.51.100.7"),
None,
] {
let mut headers = hyper::HeaderMap::new();
if let Some(value) = value {
headers.insert("x-forwarded-for", HeaderValue::from_str(value).unwrap());
}
let client = set_forwarded(
&mut headers,
"10.0.0.1".parse().unwrap(),
"http",
Some(&trust),
);
assert_eq!(
client,
"10.0.0.1".parse::<IpAddr>().unwrap(),
"{value:?} must fall back to the peer"
);
assert_eq!(xff(&headers), Some("10.0.0.1"), "{value:?}");
}
}
#[test]
fn set_forwarded_bounds_the_elements_it_scans() {
let trust = trusted(r#""10.0.0.0/8""#);
let stuffed = std::iter::repeat_n("10.0.0.1", MAX_FORWARDED_ELEMENTS + 1)
.collect::<Vec<_>>()
.join(",");
let mut headers = hyper::HeaderMap::new();
headers.insert(
"x-forwarded-for",
HeaderValue::from_str(&format!("198.51.100.7,{stuffed}")).unwrap(),
);
let client = set_forwarded(
&mut headers,
"10.0.0.1".parse().unwrap(),
"http",
Some(&trust),
);
assert_eq!(client, "10.0.0.1".parse::<IpAddr>().unwrap());
}
#[test]
fn set_forwarded_ignores_x_forwarded_for_from_an_untrusted_peer() {
let trust = trusted(r#""10.0.0.0/8""#);
for trust_arg in [Some(&trust), None] {
let mut headers = hyper::HeaderMap::new();
headers.insert(
"x-forwarded-for",
HeaderValue::from_static("198.51.100.7, 10.0.0.1"),
);
headers.insert("x-real-ip", HeaderValue::from_static("198.51.100.7"));
let client = set_forwarded(
&mut headers,
"203.0.113.5".parse().unwrap(),
"https",
trust_arg,
);
assert_eq!(client, "203.0.113.5".parse::<IpAddr>().unwrap());
assert_eq!(
xff(&headers),
Some("203.0.113.5"),
"an untrusted peer cannot name its own client"
);
assert_eq!(
headers.get("x-real-ip").and_then(|v| v.to_str().ok()),
Some("203.0.113.5")
);
}
}
#[test]
fn set_forwarded_normalises_an_ipv4_mapped_restored_client() {
let trust = trusted(r#""10.0.0.0/8""#);
let mut headers = hyper::HeaderMap::new();
headers.insert(
"x-forwarded-for",
HeaderValue::from_static("::ffff:198.51.100.7, ::ffff:10.0.0.2"),
);
let client = set_forwarded(
&mut headers,
"::ffff:10.0.0.1".parse().unwrap(),
"https",
Some(&trust),
);
assert_eq!(client, "198.51.100.7".parse::<IpAddr>().unwrap());
assert_eq!(xff(&headers), Some("198.51.100.7"));
}
#[test]
fn copy_headers_drops_hop_by_hop_crlf_and_malformed_names() {
let mut dst = hyper::HeaderMap::new();
copy_headers(
Some(&mut dst),
&[
header("x-ok", "fine"),
header("transfer-encoding", "chunked"), header("x-evil", "a\r\nInjected: pwned"), header("bad name", "x"), header("", "x"), ],
);
assert_eq!(
dst.get("x-ok").and_then(|v| v.to_str().ok()),
Some("fine"),
"a valid end-to-end header is copied"
);
assert!(
!dst.contains_key("transfer-encoding"),
"a filter cannot re-introduce a hop-by-hop header"
);
assert!(
!dst.contains_key("x-evil") && !dst.contains_key("injected"),
"a CRLF-bearing value is rejected, not split into a second header"
);
assert_eq!(dst.len(), 1, "only the one valid header survives");
}
}