use http::HeaderMap;
use sha2::{Digest, Sha256};
use crate::rfc9530::format_digest;
pub const MAX_HEADER_NAMES: usize = 32;
const FORBIDDEN_HEADER_NAMES: &[&str] = &[
"authorization",
"connection",
"host",
"keep-alive",
"proxy-authorization",
"proxy-connection",
"te",
"trailer",
"transfer-encoding",
"upgrade",
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HeaderError {
TooMany,
BadName(String),
Forbidden(String),
Duplicate(String),
Absent(String),
BadValue(String),
}
impl std::fmt::Display for HeaderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::TooMany => write!(f, "at most {MAX_HEADER_NAMES} header names"),
Self::BadName(n) => write!(f, "header name {n:?} is not a lowercase token"),
Self::Forbidden(n) => write!(f, "header {n:?} must not be bound"),
Self::Duplicate(n) => write!(f, "header name {n:?} is repeated"),
Self::Absent(n) => write!(f, "bound header {n:?} is absent from the request"),
Self::BadValue(n) => write!(
f,
"header {n:?} has a byte outside visible ASCII, space, and tab"
),
}
}
}
pub(crate) fn is_tchar_except_alpha(b: u8) -> bool {
b.is_ascii_digit()
|| matches!(
b,
b'!' | b'#'
| b'$'
| b'%'
| b'&'
| b'\''
| b'*'
| b'+'
| b'-'
| b'.'
| b'^'
| b'_'
| b'`'
| b'|'
| b'~'
)
}
fn check_header_name(name: &str) -> Result<(), HeaderError> {
if name.starts_with(':') || FORBIDDEN_HEADER_NAMES.contains(&name) {
return Err(HeaderError::Forbidden(name.to_owned()));
}
if name.is_empty()
|| !name
.bytes()
.all(|b| b.is_ascii_lowercase() || is_tchar_except_alpha(b))
{
return Err(HeaderError::BadName(name.to_owned()));
}
Ok(())
}
pub fn check_header_names<S: AsRef<str>>(names: &[S]) -> Result<(), HeaderError> {
if names.len() > MAX_HEADER_NAMES {
return Err(HeaderError::TooMany);
}
for (i, n) in names.iter().enumerate() {
let n = n.as_ref();
check_header_name(n)?;
if names.iter().take(i).any(|m| m.as_ref() == n) {
return Err(HeaderError::Duplicate(n.to_owned()));
}
}
Ok(())
}
fn check_value(name: &str, bytes: &[u8]) -> Result<(), HeaderError> {
if bytes
.iter()
.all(|b| (0x20..=0x7e).contains(b) || *b == b'\t')
{
Ok(())
} else {
Err(HeaderError::BadValue(name.to_owned()))
}
}
pub fn header_value(name: &str, headers: &HeaderMap) -> Result<String, HeaderError> {
let mut out = String::new();
join_bound_lines(name, headers, |b| {
out.extend(b.iter().map(|&c| char::from(c)))
})?;
Ok(out)
}
fn join_bound_lines(
name: &str,
headers: &HeaderMap,
mut sink: impl FnMut(&[u8]),
) -> Result<(), HeaderError> {
if !headers.contains_key(name) {
return Err(HeaderError::Absent(name.to_owned()));
}
for (i, v) in headers.get_all(name).iter().enumerate() {
check_value(name, v.as_bytes())?;
if i > 0 {
sink(b", ");
}
sink(v.as_bytes().trim_ascii());
}
Ok(())
}
pub fn header_digest(name: &str, headers: &HeaderMap) -> Result<String, HeaderError> {
let mut ctx = Sha256::new();
join_bound_lines(name, headers, |b| ctx.update(b))?;
Ok(format_digest(&ctx.finalize()))
}
pub fn header_digests<S: AsRef<str>>(
names: &[S],
headers: &HeaderMap,
) -> Result<Vec<String>, HeaderError> {
names
.iter()
.map(|n| header_digest(n.as_ref(), headers))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rfc9530::tests::VECTORS;
use http::{HeaderName, HeaderValue};
fn header_map(received: &serde_json::Value) -> HeaderMap {
let mut h = HeaderMap::new();
for pair in received.as_array().unwrap() {
let name = HeaderName::from_bytes(pair[0].as_str().unwrap().as_bytes()).unwrap();
let value = HeaderValue::from_bytes(pair[1].as_str().unwrap().as_bytes()).unwrap();
h.append(name, value);
}
h
}
#[test]
fn header_value_and_digest_match_shared_vectors() {
let v: serde_json::Value = serde_json::from_str(VECTORS).unwrap();
let cases = v["headers"].as_array().unwrap();
assert!(cases.len() >= 8);
for case in cases {
let name = case["name"].as_str().unwrap();
let bound = case["header"].as_str().unwrap();
let received = header_map(&case["received"]);
let got = header_value(bound, &received)
.and_then(|value| Ok((value, header_digest(bound, &received)?)));
match case.get("value").and_then(|c| c.as_str()) {
Some(want) => {
let (value, digest) = got.unwrap_or_else(|e| panic!("{name}: {e}"));
assert_eq!(value, want, "{name}");
assert_eq!(digest, case["digest"].as_str().unwrap(), "{name}");
}
None => assert!(got.is_err(), "{name} should fail, got {got:?}"),
}
}
}
#[test]
fn check_header_names_accepts_and_rejects_shared_name_lists() {
let v: serde_json::Value = serde_json::from_str(VECTORS).unwrap();
for case in v["header_names"].as_array().unwrap() {
let name = case["name"].as_str().unwrap();
let names: Vec<String> = serde_json::from_value(case["names"].clone()).unwrap();
assert_eq!(
check_header_names(&names).is_ok(),
case["ok"].as_bool().unwrap(),
"{name}"
);
}
}
#[test]
fn rejects_duplicate_oversized_and_forbidden_header_names() {
assert_eq!(
check_header_names(&["a", "a"]),
Err(HeaderError::Duplicate("a".into()))
);
let many: Vec<String> = (0..=MAX_HEADER_NAMES)
.map(|i| format!("x-{i:02}"))
.collect();
assert_eq!(check_header_names(&many), Err(HeaderError::TooMany));
for f in FORBIDDEN_HEADER_NAMES {
assert_eq!(
check_header_names(&[*f]),
Err(HeaderError::Forbidden((*f).to_owned()))
);
}
}
}