use http::{HeaderMap, HeaderName, HeaderValue};
use std::net::IpAddr;
const HOP_BY_HOP: &[&str] = &[
"connection", "keep-alive", "transfer-encoding", "te",
"trailer", "upgrade", "proxy-authorization", "proxy-authenticate",
];
const NOMINATION_EXEMPT: &[&str] = &["host", "content-length"];
pub fn strip_hop_by_hop(headers: &mut HeaderMap) {
let nominated: Vec<String> = headers
.get_all(http::header::CONNECTION)
.iter()
.filter_map(|v| v.to_str().ok())
.flat_map(|s| s.split(','))
.map(|t| t.trim().to_ascii_lowercase())
.filter(|t| !t.is_empty() && !NOMINATION_EXEMPT.contains(&t.as_str()))
.collect();
for token in nominated {
if let Ok(name) = HeaderName::from_bytes(token.as_bytes()) {
headers.remove(&name);
}
}
for &name in HOP_BY_HOP {
headers.remove(name);
}
}
const ATTRIBUTION: &[&str] = &[
"x-forwarded-for",
"forwarded",
"x-real-ip",
"x-forwarded-host",
"x-forwarded-proto",
];
pub fn apply_forwarding_policy(
headers: &mut HeaderMap,
client: IpAddr,
scheme: &str,
original_host: Option<&HeaderValue>,
trusted_proxies: &[ipnetwork::IpNetwork],
) {
let trusted = trusted_proxies.iter().any(|n| n.contains(client));
let prior_xff: Option<String> = if trusted {
let joined = headers
.get_all("x-forwarded-for")
.iter()
.filter_map(|v| v.to_str().ok())
.collect::<Vec<_>>()
.join(", ");
(!joined.is_empty()).then_some(joined)
} else {
for &name in ATTRIBUTION {
headers.remove(name);
}
None
};
let xff = match prior_xff {
Some(prev) => format!("{}, {}", prev, client),
None => client.to_string(),
};
if let Ok(v) = HeaderValue::from_str(&xff) {
headers.insert("x-forwarded-for", v);
}
if !headers.contains_key("x-real-ip") {
if let Ok(v) = HeaderValue::from_str(&client.to_string()) {
headers.insert("x-real-ip", v);
}
}
if !headers.contains_key("x-forwarded-proto") {
if let Ok(v) = HeaderValue::from_str(scheme) {
headers.insert("x-forwarded-proto", v);
}
}
if !headers.contains_key("x-forwarded-host") {
if let Some(host) = original_host {
headers.insert("x-forwarded-host", host.clone());
}
}
}
pub fn normalize_headers(headers: &mut HeaderMap) {
strip_hop_by_hop(headers);
}
pub fn redact_sensitive_headers(headers: &HeaderMap) -> Vec<(String, String)> {
const SENSITIVE: &[&str] = &[
"authorization", "cookie", "proxy-authorization", "set-cookie",
];
headers
.iter()
.map(|(name, value)| {
let name_str = name.as_str().to_lowercase();
let value_str = if SENSITIVE.contains(&name_str.as_str()) {
"[REDACTED]".to_string()
} else {
value.to_str().unwrap_or("[INVALID]").to_string()
};
(name_str, value_str)
})
.collect()
}
pub fn validate_header_values(headers: &HeaderMap) -> bool {
headers.iter().all(|(_, v)| {
v.to_str().map(|s| !s.contains('\r') && !s.contains('\n')).unwrap_or(false)
})
}
#[cfg(test)]
mod tests {
use super::*;
fn hm(pairs: &[(&str, &str)]) -> HeaderMap {
let mut m = HeaderMap::new();
for (k, v) in pairs {
m.append(
HeaderName::from_bytes(k.as_bytes()).unwrap(),
HeaderValue::from_str(v).unwrap(),
);
}
m
}
#[test]
fn connection_nominated_headers_are_stripped() {
let mut h = hm(&[
("connection", "close, x-internal-secret"),
("x-internal-secret", "token"),
("x-kept", "yes"),
]);
strip_hop_by_hop(&mut h);
assert!(h.get("x-internal-secret").is_none());
assert!(h.get("connection").is_none());
assert_eq!(h.get("x-kept").unwrap(), "yes");
}
#[test]
fn nomination_cannot_remove_host_or_content_length() {
let mut h = hm(&[
("connection", "host, content-length"),
("host", "example.com"),
("content-length", "5"),
]);
strip_hop_by_hop(&mut h);
assert!(h.get("host").is_some());
assert!(h.get("content-length").is_some());
}
#[test]
fn standard_hop_by_hop_removed() {
let mut h = hm(&[
("transfer-encoding", "chunked"),
("keep-alive", "timeout=5"),
("upgrade", "h2c"),
("proxy-authorization", "Basic xxx"),
]);
strip_hop_by_hop(&mut h);
assert!(h.is_empty());
}
#[test]
fn untrusted_client_attribution_rewritten() {
let mut h = hm(&[
("x-forwarded-for", "1.2.3.4"),
("x-real-ip", "1.2.3.4"),
("x-forwarded-proto", "https"),
]);
let client: IpAddr = "203.0.113.7".parse().unwrap();
apply_forwarding_policy(&mut h, client, "http", None, &[]);
assert_eq!(h.get("x-forwarded-for").unwrap(), "203.0.113.7");
assert_eq!(h.get("x-real-ip").unwrap(), "203.0.113.7");
assert_eq!(h.get("x-forwarded-proto").unwrap(), "http");
}
#[test]
fn trusted_proxy_xff_appended() {
let mut h = hm(&[("x-forwarded-for", "1.2.3.4")]);
let client: IpAddr = "10.0.0.1".parse().unwrap();
let trusted = vec!["10.0.0.0/8".parse().unwrap()];
apply_forwarding_policy(&mut h, client, "https", None, &trusted);
assert_eq!(h.get("x-forwarded-for").unwrap(), "1.2.3.4, 10.0.0.1");
}
#[test]
fn forwarded_host_set_from_original() {
let mut h = HeaderMap::new();
let host = HeaderValue::from_static("example.com");
let client: IpAddr = "203.0.113.7".parse().unwrap();
apply_forwarding_policy(&mut h, client, "https", Some(&host), &[]);
assert_eq!(h.get("x-forwarded-host").unwrap(), "example.com");
}
}