const HOP_BY_HOP: &[&str] = &[
"connection",
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"te",
"trailer",
"transfer-encoding",
"upgrade",
"proxy-connection",
];
const REQUEST_ONLY: &[&str] = &[
"host",
"user-agent",
"accept",
"accept-encoding",
"accept-language",
"accept-charset",
"accept-datetime",
"authorization",
"cookie",
"expect",
"from",
"if-match",
"if-modified-since",
"if-none-match",
"if-range",
"if-unmodified-since",
"max-forwards",
"range",
"referer",
];
const SERVER_OWNED: &[&str] = &["date"];
const fn is_tchar(c: u8) -> bool {
matches!(c, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9')
|| matches!(
c,
b'!' | b'#'
| b'$'
| b'%'
| b'&'
| b'\''
| b'*'
| b'+'
| b'-'
| b'.'
| b'^'
| b'_'
| b'`'
| b'|'
| b'~'
)
}
fn is_valid_token(token: &str) -> bool {
!token.is_empty() && token.bytes().all(is_tchar)
}
pub(crate) fn connection_tokens<'a, I>(connection_header_values: I) -> Vec<String>
where
I: IntoIterator<Item = &'a str>,
{
let mut seen = Vec::new();
for value in connection_header_values {
for raw in value.split(',') {
let candidate = raw.trim().to_ascii_lowercase();
if is_valid_token(&candidate) && !seen.contains(&candidate) {
seen.push(candidate);
}
}
}
seen
}
pub(crate) fn excluded_outbound(name: &str, connection_tokens: &[String]) -> bool {
let lower = name.to_ascii_lowercase();
HOP_BY_HOP.contains(&lower.as_str())
|| lower == "content-length"
|| lower == "host"
|| connection_tokens.contains(&lower)
}
pub(crate) fn excluded_response(name: &str, connection_tokens: &[String]) -> bool {
let lower = name.to_ascii_lowercase();
HOP_BY_HOP.contains(&lower.as_str())
|| REQUEST_ONLY.contains(&lower.as_str())
|| SERVER_OWNED.contains(&lower.as_str())
|| lower == "content-length"
|| lower == "content-type"
|| connection_tokens.contains(&lower)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn excluded_outbound_strips_hop_by_hop() {
for name in ["Connection", "Transfer-Encoding", "Upgrade"] {
assert!(
excluded_outbound(name, &[]),
"{name} should be excluded outbound"
);
}
}
#[test]
fn excluded_outbound_excludes_host_and_content_length() {
assert!(excluded_outbound("Host", &[]));
assert!(excluded_outbound("Content-Length", &[]));
}
#[test]
fn excluded_outbound_forwards_request_only() {
assert!(!excluded_outbound("Accept", &[]));
assert!(!excluded_outbound("User-Agent", &[]));
}
#[test]
fn excluded_outbound_dynamic_connection() {
let tokens = connection_tokens(["X-Custom, Keep-Alive"]);
assert!(excluded_outbound("X-Custom", &tokens));
assert!(!excluded_outbound("X-Other", &tokens));
}
#[test]
fn connection_tokens_casefold_dedup() {
let tokens = connection_tokens(["X-Custom, x-custom, X-Custom "]);
assert_eq!(tokens, ["x-custom"]);
}
#[test]
fn connection_tokens_malformed_no_panic() {
let tokens = connection_tokens(["X-Custom, bad token, ,"]);
assert_eq!(tokens, ["x-custom"]);
assert!(!excluded_outbound("X-Unrelated", &tokens));
assert!(!excluded_response("X-Unrelated", &tokens));
}
#[test]
fn excluded_response_strips_hop_by_hop() {
for name in ["Connection", "Transfer-Encoding", "Upgrade"] {
assert!(
excluded_response(name, &[]),
"{name} should be excluded from response"
);
}
}
#[test]
fn excluded_response_keeps_cache_control_pragma_warning_via() {
for name in ["Cache-Control", "Pragma", "Warning", "Via"] {
assert!(
!excluded_response(name, &[]),
"{name} should be kept on response"
);
}
}
#[test]
fn excluded_response_excludes_request_only_and_server_owned() {
assert!(excluded_response("User-Agent", &[]));
assert!(excluded_response("Accept", &[]));
assert!(excluded_response("Date", &[]));
}
#[test]
fn excluded_response_excludes_content_length_and_type() {
assert!(excluded_response("Content-Length", &[]));
assert!(excluded_response("Content-Type", &[]));
}
#[test]
fn excluded_response_dynamic_connection() {
let tokens = connection_tokens(["X-Custom"]);
assert!(excluded_response("X-Custom", &tokens));
}
}