use alloc::vec::Vec;
use core::cmp::Ordering;
use crate::head::{Headers, ListElements, RequestHead, ResponseHead};
const HOP_BY_HOP: &[&str] = &[
"connection",
"keep-alive",
"proxy-connection",
"te",
"transfer-encoding",
"upgrade",
"trailer",
];
#[must_use]
pub fn is_hop_by_hop(name: &str, mut connection_options: ListElements<'_, '_>) -> bool {
HOP_BY_HOP.iter().any(|h| h.eq_ignore_ascii_case(name))
|| connection_options.any(|o| o.eq_ignore_ascii_case(name.as_bytes()))
}
fn cmp_ignore_case(a: &[u8], b: &[u8]) -> Ordering {
a.iter()
.map(u8::to_ascii_lowercase)
.cmp(b.iter().map(u8::to_ascii_lowercase))
}
pub(crate) struct HopByHop<'a> {
listed: Vec<&'a [u8]>,
}
impl<'a> HopByHop<'a> {
pub(crate) fn new(options: ListElements<'a, '_>) -> Self {
let mut listed: Vec<&'a [u8]> = options.collect();
listed.sort_unstable_by(|a, b| cmp_ignore_case(a, b));
listed.dedup_by(|a, b| a.eq_ignore_ascii_case(b));
Self { listed }
}
pub(crate) fn contains(&self, name: &str) -> bool {
HOP_BY_HOP.iter().any(|h| h.eq_ignore_ascii_case(name))
|| self
.listed
.binary_search_by(|o| cmp_ignore_case(o, name.as_bytes()))
.is_ok()
}
}
fn end_to_end<'a>(
headers: Headers<'a>,
hop: HopByHop<'a>,
) -> impl Iterator<Item = (&'a str, &'a [u8])> + use<'a> {
headers.filter(move |(name, _)| !hop.contains(name))
}
impl<'a> RequestHead<'a> {
pub fn end_to_end_headers(&self) -> impl Iterator<Item = (&'a str, &'a [u8])> + use<'a> {
end_to_end(self.headers(), HopByHop::new(self.connection_options()))
}
}
impl<'a> ResponseHead<'a> {
pub fn end_to_end_headers(&self) -> impl Iterator<Item = (&'a str, &'a [u8])> + use<'a> {
end_to_end(
self.headers(),
HopByHop::new(self.header_list("connection")),
)
}
}
#[cfg(test)]
mod tests {
use crate::{ServerConn, ServerEvent};
use super::*;
#[test]
fn test_is_hop_by_hop_fixed_set_and_connection_options() {
let mut conn = ServerConn::new();
let _ = conn.feed(
b"GET / HTTP/1.1\r\nHost: a\r\nConnection: close, X-A\r\nConnection: x-b\r\n\r\n",
);
let Ok(ServerEvent::Request(req)) = conn.poll() else {
panic!("request");
};
for name in HOP_BY_HOP {
assert!(is_hop_by_hop(name, req.connection_options()), "{name}");
let upper = name.to_ascii_uppercase();
assert!(is_hop_by_hop(&upper, req.connection_options()), "{upper}");
}
assert!(is_hop_by_hop("X-A", req.connection_options()));
assert!(is_hop_by_hop("x-b", req.connection_options()));
assert!(is_hop_by_hop("close", req.connection_options()));
for name in ["host", "accept", "x-c", "content-length", "trailers"] {
assert!(!is_hop_by_hop(name, req.connection_options()), "{name}");
}
}
#[test]
fn test_hop_by_hop_set_agrees_with_is_hop_by_hop() {
let mut conn = ServerConn::new();
let _ = conn.feed(
b"GET / HTTP/1.1\r\nHost: a\r\nConnection: close, X-A, x-a, Zed\r\nConnection: x-b, , \"q\"\r\n\r\n",
);
let Ok(ServerEvent::Request(req)) = conn.poll() else {
panic!("request");
};
let set = HopByHop::new(req.connection_options());
for name in [
"connection",
"TE",
"x-a",
"X-A",
"zed",
"ZED",
"x-b",
"close",
"host",
"accept",
"x",
"x-a2",
"",
"\"q\"",
"trailer",
"zee",
] {
assert_eq!(
set.contains(name),
is_hop_by_hop(name, req.connection_options()),
"{name}"
);
}
}
}