use std::fmt;
use zenith_api::{
CanonicalRequest, CanonicalResponse, Method, Protocol, Transport, MAX_HEADER_COUNT,
MAX_PATH_LEN,
};
use zenith_http1::response::{Http1ResponseEncoder, ResponseSerializeError};
use zenith_http1::types::HttpRequest as Http1Request;
use zenith_http2::error::Http2Error;
use zenith_http2::hpack::HeaderField;
use zenith_http2::response::Http2ResponseEncoder;
use zenith_http3::encoder::Http3ResponseEncoder;
use zenith_http3::frame::Http3Error;
const MAX_REQUEST_TARGET_LEN: usize = MAX_PATH_LEN;
const MAX_BODY_SIZE: usize = 16_777_216;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProtocolNormalizeError {
UnsupportedMethod(String),
HeaderTooLong(String),
TooManyHeaders,
InvalidHeaderName(String),
InvalidHeaderValue(String),
RequestTargetTooLong,
BodyTooLarge {
size: usize,
max: usize,
},
ProtocolViolation(String),
Internal(String),
}
impl fmt::Display for ProtocolNormalizeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnsupportedMethod(m) => write!(f, "unsupported HTTP method: {m}"),
Self::HeaderTooLong(h) => write!(f, "header too long: {h}"),
Self::TooManyHeaders => write!(f, "too many headers (max {MAX_HEADER_COUNT})"),
Self::InvalidHeaderName(n) => write!(f, "invalid header name: {n}"),
Self::InvalidHeaderValue(v) => write!(f, "invalid header value: {v}"),
Self::RequestTargetTooLong => {
write!(f, "request target too long (max {MAX_REQUEST_TARGET_LEN})")
}
Self::BodyTooLarge { size, max } => {
write!(f, "body too large: {size} bytes (max {max})")
}
Self::ProtocolViolation(m) => write!(f, "protocol violation: {m}"),
Self::Internal(m) => write!(f, "internal normalization error: {m}"),
}
}
}
impl std::error::Error for ProtocolNormalizeError {}
#[inline]
pub fn parse_method(s: &str) -> Result<Method, ProtocolNormalizeError> {
s.parse::<Method>()
.map_err(|_| ProtocolNormalizeError::UnsupportedMethod(s.to_string()))
}
fn map_header_err(
msg: &'static str,
name: &str,
value: &str,
) -> ProtocolNormalizeError {
match msg {
"header count exceeded" => ProtocolNormalizeError::TooManyHeaders,
"header name contains CRLF" => ProtocolNormalizeError::InvalidHeaderName(name.to_string()),
"header value contains CRLF" => ProtocolNormalizeError::InvalidHeaderValue(value.to_string()),
"header name too long" | "header name or value too long" => {
ProtocolNormalizeError::InvalidHeaderName(name.to_string())
}
"header value too long" => ProtocolNormalizeError::InvalidHeaderValue(value.to_string()),
other => ProtocolNormalizeError::Internal(other.to_string()),
}
}
pub fn normalize_http1_request(
req: &Http1Request,
transport: Transport,
) -> Result<CanonicalRequest, ProtocolNormalizeError> {
let method = parse_method(&req.line.method)?;
if req.line.target.len() > MAX_REQUEST_TARGET_LEN {
return Err(ProtocolNormalizeError::RequestTargetTooLong);
}
let mut canonical = CanonicalRequest::empty();
canonical.method = method;
canonical.protocol = Protocol::Http1;
canonical.transport = transport;
let normalized_path = zenith_api::normalize::normalize_path(&req.line.target, false)
.map_err(|e| ProtocolNormalizeError::ProtocolViolation(format!("invalid path: {}", e.message)))?;
if !canonical.set_path(&normalized_path) {
return Err(ProtocolNormalizeError::ProtocolViolation(
"path canonicalization rejected".to_string(),
));
}
for (name, value) in &req.headers {
canonical
.add_header(name.as_bytes(), value.as_bytes())
.map_err(|e| map_header_err(e, name, value))?;
}
if req.body.len() > MAX_BODY_SIZE {
return Err(ProtocolNormalizeError::BodyTooLarge {
size: req.body.len(),
max: MAX_BODY_SIZE,
});
}
canonical.set_body(req.body.clone());
Ok(canonical)
}
const FORBIDDEN_H2_H3_HEADERS: &[&[u8]] = &[
b"connection",
b"keep-alive",
b"proxy-connection",
b"transfer-encoding",
b"upgrade",
];
fn is_forbidden_connection_header(name: &[u8]) -> bool {
FORBIDDEN_H2_H3_HEADERS
.iter()
.any(|forbidden| forbidden.eq_ignore_ascii_case(name))
}
fn normalize_h23_request_core<'a, I>(
pairs: I,
total_len: usize,
transport: Transport,
protocol: Protocol,
) -> Result<CanonicalRequest, ProtocolNormalizeError>
where
I: Iterator<Item = (&'a str, &'a str)>,
{
let mut past_pseudo = false;
let mut method: Option<Method> = None;
let mut path: Option<&str> = None;
let mut scheme: Option<&str> = None;
let mut authority: Option<&str> = None;
let cap = total_len.min(MAX_HEADER_COUNT);
let mut regular_headers: Vec<(&str, &str)> = Vec::with_capacity(cap);
for (name, value) in pairs {
if name.starts_with(':') {
if past_pseudo {
return Err(ProtocolNormalizeError::ProtocolViolation(
"pseudo-header after regular header".to_string(),
));
}
match name {
":method" => {
if method.is_some() {
return Err(ProtocolNormalizeError::ProtocolViolation(
"duplicate pseudo-header: :method".to_string(),
));
}
let m = parse_method(value)?;
method = Some(m);
}
":path" => {
if path.is_some() {
return Err(ProtocolNormalizeError::ProtocolViolation(
"duplicate pseudo-header: :path".to_string(),
));
}
if value.is_empty() {
return Err(ProtocolNormalizeError::ProtocolViolation(
"empty :path pseudo-header".to_string(),
));
}
if value.len() > MAX_REQUEST_TARGET_LEN {
return Err(ProtocolNormalizeError::RequestTargetTooLong);
}
path = Some(value);
}
":scheme" => {
if scheme.is_some() {
return Err(ProtocolNormalizeError::ProtocolViolation(
"duplicate pseudo-header: :scheme".to_string(),
));
}
scheme = Some(value);
}
":authority" => {
if authority.is_some() {
return Err(ProtocolNormalizeError::ProtocolViolation(
"duplicate pseudo-header: :authority".to_string(),
));
}
authority = Some(value);
}
other => {
return Err(ProtocolNormalizeError::ProtocolViolation(format!(
"unknown pseudo-header: {other}"
)));
}
}
} else {
past_pseudo = true;
if name.as_bytes().iter().any(|b| b.is_ascii_uppercase()) {
return Err(ProtocolNormalizeError::ProtocolViolation(format!(
"uppercase header name rejected (must be lowercase): {name}"
)));
}
if is_forbidden_connection_header(name.as_bytes()) {
return Err(ProtocolNormalizeError::ProtocolViolation(format!(
"forbidden connection-specific header: {name}"
)));
}
if name.eq_ignore_ascii_case("te") && !value.eq_ignore_ascii_case("trailers") {
return Err(ProtocolNormalizeError::ProtocolViolation(format!(
"te header must be 'trailers', got: {value}"
)));
}
if regular_headers.len() >= MAX_HEADER_COUNT {
return Err(ProtocolNormalizeError::TooManyHeaders);
}
regular_headers.push((name, value));
}
}
let method =
method.ok_or_else(|| ProtocolNormalizeError::ProtocolViolation("missing :method".to_string()))?;
let path =
path.ok_or_else(|| ProtocolNormalizeError::ProtocolViolation("missing :path".to_string()))?;
let scheme_val =
scheme.ok_or_else(|| ProtocolNormalizeError::ProtocolViolation("missing :scheme".to_string()))?;
let asterisk_form = method == Method::Options && path == "*";
if method != Method::Connect
&& !asterisk_form
&& authority.is_none()
{
return Err(ProtocolNormalizeError::ProtocolViolation(
"missing :authority".to_string(),
));
}
let schemes_match = matches!(
(transport, scheme_val),
(Transport::Plaintext, "http") | (Transport::Tls13, "https")
);
if !schemes_match {
return Err(ProtocolNormalizeError::ProtocolViolation(format!(
"transport/scheme mismatch: transport={transport:?} scheme={scheme_val}"
)));
}
let mut canonical = CanonicalRequest::empty();
canonical.method = method;
canonical.protocol = protocol;
canonical.transport = transport;
let normalized_path = zenith_api::normalize::normalize_path(path, false)
.map_err(|e| ProtocolNormalizeError::ProtocolViolation(format!("invalid path: {}", e.message)))?;
if !canonical.set_path(&normalized_path) {
return Err(ProtocolNormalizeError::ProtocolViolation(
"path canonicalization rejected".to_string(),
));
}
if let Some(auth) = authority {
if !canonical.set_authority(auth) {
return Err(ProtocolNormalizeError::ProtocolViolation(
"authority canonicalization rejected".to_string(),
));
}
}
for (name, value) in regular_headers {
canonical
.add_header(name.as_bytes(), value.as_bytes())
.map_err(|e| map_header_err(e, name, value))?;
}
Ok(canonical)
}
pub fn normalize_http2_request(
headers: &[HeaderField],
transport: Transport,
) -> Result<CanonicalRequest, ProtocolNormalizeError> {
normalize_h23_request_core(
headers.iter().map(|h| (h.name.as_str(), h.value.as_str())),
headers.len(),
transport,
Protocol::Http2,
)
}
pub fn normalize_http3_request(
headers: &[(Vec<u8>, Vec<u8>)],
transport: Transport,
) -> Result<CanonicalRequest, ProtocolNormalizeError> {
let mut pairs: Vec<(&str, &str)> = Vec::with_capacity(headers.len().min(MAX_HEADER_COUNT));
for (name_raw, value_raw) in headers {
let name = std::str::from_utf8(name_raw).map_err(|_| {
ProtocolNormalizeError::InvalidHeaderName(String::from_utf8_lossy(name_raw).to_string())
})?;
let value = std::str::from_utf8(value_raw).map_err(|_| {
ProtocolNormalizeError::InvalidHeaderValue(String::from_utf8_lossy(value_raw).to_string())
})?;
pairs.push((name, value));
}
normalize_h23_request_core(pairs.into_iter(), headers.len(), transport, Protocol::Http3)
}
pub fn encode_response_http1(
resp: &CanonicalResponse,
out: &mut Vec<u8>,
) -> Result<usize, ProtocolNormalizeError> {
Http1ResponseEncoder::encode(resp, out).map_err(|e| match e {
ResponseSerializeError::CrlfInjection => {
ProtocolNormalizeError::InvalidHeaderValue("CRLF injection detected".to_string())
}
ResponseSerializeError::InvalidHeaderName => {
ProtocolNormalizeError::InvalidHeaderName("invalid header name character".to_string())
}
})
}
pub fn encode_response_http2(
encoder: &mut Http2ResponseEncoder,
resp: &CanonicalResponse,
stream_id: u32,
) -> Result<Vec<u8>, ProtocolNormalizeError> {
encoder.encode(resp, stream_id).map_err(|e| match e {
Http2Error::ProtocolError(m) => ProtocolNormalizeError::ProtocolViolation(m),
Http2Error::FrameFormatError(m) => ProtocolNormalizeError::ProtocolViolation(m),
Http2Error::ConnectionError(code) => {
ProtocolNormalizeError::ProtocolViolation(format!("connection error: {code}"))
}
Http2Error::StreamError(id, info) => ProtocolNormalizeError::ProtocolViolation(format!(
"stream {id} error ({}): {}",
info.code, info.message
)),
Http2Error::CompressionError(m) => ProtocolNormalizeError::ProtocolViolation(m),
Http2Error::RapidReset(n) => {
ProtocolNormalizeError::ProtocolViolation(format!("rapid reset: {n}"))
}
Http2Error::FrameTooShort
| Http2Error::FrameTooLarge
| Http2Error::UnknownFrameType(_)
| Http2Error::FlowControlError(_)
| Http2Error::SettingsError(_)
| Http2Error::UnknownSettingId(_)
| Http2Error::IntegerOverflow(_)
| Http2Error::Internal(_) => ProtocolNormalizeError::Internal(e.to_string()),
})
}
pub fn encode_response_http3(
encoder: &mut Http3ResponseEncoder,
resp: &CanonicalResponse,
_stream_id: u64,
) -> Result<Vec<u8>, ProtocolNormalizeError> {
let raw_headers_cap = resp.header_count() as usize;
let mut raw_headers: Vec<(Vec<u8>, Vec<u8>)> = Vec::with_capacity(raw_headers_cap);
for h in resp.headers_iter() {
raw_headers.push((
h.name_str().as_bytes().to_vec(),
h.value_str().as_bytes().to_vec(),
));
}
encoder
.encode_response(resp.status_code, &raw_headers, resp.body())
.map_err(|e| match e {
Http3Error::ProtocolError(m) => ProtocolNormalizeError::ProtocolViolation(m),
Http3Error::FrameFormatError(m) => ProtocolNormalizeError::ProtocolViolation(m),
Http3Error::StreamError(id, code) => {
ProtocolNormalizeError::ProtocolViolation(format!("stream {id} error: {code}"))
}
Http3Error::FrameTooShort
| Http3Error::FrameTooLarge
| Http3Error::UnknownFrameType(_)
| Http3Error::InternalError(_) => ProtocolNormalizeError::Internal(e.to_string()),
})
}
#[inline]
pub fn alpn_to_protocol(alpn: &[u8]) -> Option<Protocol> {
match zenith_tls::sni::Alpn::from_bytes(alpn) {
zenith_tls::sni::Alpn::Http1 => Some(Protocol::Http1),
zenith_tls::sni::Alpn::Http2 => Some(Protocol::Http2),
zenith_tls::sni::Alpn::Http3 => Some(Protocol::Http3),
zenith_tls::sni::Alpn::Unknown => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use zenith_api::MAX_HEADER_COUNT;
#[test]
fn parse_method_all_standard() {
assert_eq!(parse_method("GET"), Ok(Method::Get));
assert_eq!(parse_method("POST"), Ok(Method::Post));
assert_eq!(parse_method("PUT"), Ok(Method::Put));
assert_eq!(parse_method("DELETE"), Ok(Method::Delete));
assert_eq!(parse_method("PATCH"), Ok(Method::Patch));
assert_eq!(parse_method("HEAD"), Ok(Method::Head));
assert_eq!(parse_method("OPTIONS"), Ok(Method::Options));
assert_eq!(parse_method("TRACE"), Ok(Method::Trace));
assert_eq!(parse_method("CONNECT"), Ok(Method::Connect));
}
#[test]
fn parse_method_unknown() {
let err = parse_method("FOOBAR").unwrap_err();
assert!(matches!(err, ProtocolNormalizeError::UnsupportedMethod(ref m) if m == "FOOBAR"));
}
#[test]
fn parse_method_case_sensitive() {
assert!(parse_method("get").is_err());
assert!(parse_method("Get").is_err());
assert!(parse_method("Post ").is_err());
assert!(parse_method(" GET").is_err());
}
fn make_http1(method: &str, target: &str) -> Http1Request {
let mut r = Http1Request::new(method.into(), target.into(), "HTTP/1.1".into());
r.keep_alive = true;
r
}
#[test]
fn normalize_http1_simple_get() {
let mut req = make_http1("GET", "/");
req.headers
.push(("content-type".into(), "application/json".into()));
let c = normalize_http1_request(&req, Transport::Plaintext).unwrap();
assert_eq!(c.method, Method::Get);
assert_eq!(c.protocol, Protocol::Http1);
assert_eq!(c.transport, Transport::Plaintext);
assert_eq!(c.path_str(), "/");
assert_eq!(c.header_count(), 1);
let h = c.find_header("content-type").unwrap();
assert_eq!(h.value_str(), "application/json");
}
#[test]
fn normalize_http1_body_within_limit_ok() {
let mut req = make_http1("POST", "/submit");
req.body = b"hello world".to_vec();
let c = normalize_http1_request(&req, Transport::Tls13).unwrap();
assert_eq!(c.method, Method::Post);
assert_eq!(c.transport, Transport::Tls13);
}
#[test]
fn normalize_http1_body_too_large() {
let mut req = make_http1("POST", "/submit");
req.body = vec![b'x'; MAX_BODY_SIZE + 1];
let err = normalize_http1_request(&req, Transport::Plaintext).unwrap_err();
assert!(matches!(
err,
ProtocolNormalizeError::BodyTooLarge { size, max }
if size == MAX_BODY_SIZE + 1 && max == MAX_BODY_SIZE
));
}
#[test]
fn normalize_http1_too_many_headers() {
let mut req = make_http1("GET", "/");
for i in 0..MAX_HEADER_COUNT + 1 {
req.headers
.push((format!("h{i}").into(), format!("v{i}").into()));
}
let err = normalize_http1_request(&req, Transport::Plaintext).unwrap_err();
assert!(matches!(err, ProtocolNormalizeError::TooManyHeaders));
}
#[test]
fn normalize_http1_crlf_in_header_value() {
let mut req = make_http1("GET", "/");
req.headers
.push(("x-evil".into(), "val\r\nInjected: yes".into()));
let err = normalize_http1_request(&req, Transport::Plaintext).unwrap_err();
assert!(matches!(
err,
ProtocolNormalizeError::InvalidHeaderValue(ref v) if v.contains("val")
));
}
#[test]
fn normalize_http1_unsupported_method() {
let req = make_http1("FOO", "/");
let err = normalize_http1_request(&req, Transport::Plaintext).unwrap_err();
assert!(matches!(err, ProtocolNormalizeError::UnsupportedMethod(_)));
}
#[test]
fn normalize_http1_target_too_long() {
let long_target = "a".repeat(MAX_REQUEST_TARGET_LEN + 1);
let req = make_http1("GET", &long_target);
let err = normalize_http1_request(&req, Transport::Plaintext).unwrap_err();
assert!(matches!(err, ProtocolNormalizeError::RequestTargetTooLong));
}
#[test]
fn normalize_http1_keep_alive_header_preserved() {
let mut req = make_http1("GET", "/");
req.headers
.push(("keep-alive".into(), "timeout=5".into()));
let c = normalize_http1_request(&req, Transport::Plaintext).unwrap();
assert!(c.find_header("keep-alive").is_some());
}
#[test]
fn normalize_http2_valid() {
let headers = vec![
HeaderField::new(":method", "GET"),
HeaderField::new(":path", "/api"),
HeaderField::new(":scheme", "https"),
HeaderField::new(":authority", "example.com"),
HeaderField::new("content-type", "application/json"),
];
let c = normalize_http2_request(&headers, Transport::Tls13).unwrap();
assert_eq!(c.method, Method::Get);
assert_eq!(c.protocol, Protocol::Http2);
assert_eq!(c.path_str(), "/api");
assert_eq!(c.authority_str(), "example.com");
assert_eq!(c.header_count(), 1);
}
#[test]
fn normalize_http2_pseudo_after_regular() {
let headers = vec![
HeaderField::new("content-type", "text/plain"),
HeaderField::new(":method", "GET"),
HeaderField::new(":path", "/"),
HeaderField::new(":scheme", "http"),
];
let err = normalize_http2_request(&headers, Transport::Plaintext).unwrap_err();
assert!(matches!(
err,
ProtocolNormalizeError::ProtocolViolation(ref m) if m.contains("pseudo-header after regular")
));
}
#[test]
fn normalize_http2_missing_method() {
let headers = vec![
HeaderField::new(":path", "/"),
HeaderField::new(":scheme", "http"),
];
let err = normalize_http2_request(&headers, Transport::Plaintext).unwrap_err();
assert!(matches!(
err,
ProtocolNormalizeError::ProtocolViolation(ref m) if m.contains("missing :method")
));
}
#[test]
fn normalize_http2_forbidden_connection_header() {
let headers = vec![
HeaderField::new(":method", "GET"),
HeaderField::new(":path", "/"),
HeaderField::new(":scheme", "http"),
HeaderField::new("connection", "keep-alive"),
];
let err = normalize_http2_request(&headers, Transport::Plaintext).unwrap_err();
assert!(matches!(
err,
ProtocolNormalizeError::ProtocolViolation(ref m) if m.contains("connection")
));
}
#[test]
fn normalize_http2_unknown_method() {
let headers = vec![
HeaderField::new(":method", "FOOBAR"),
HeaderField::new(":path", "/"),
HeaderField::new(":scheme", "http"),
];
let err = normalize_http2_request(&headers, Transport::Plaintext).unwrap_err();
assert!(matches!(err, ProtocolNormalizeError::UnsupportedMethod(_)));
}
#[test]
fn normalize_http2_missing_authority_rejected() {
let headers = vec![
HeaderField::new(":method", "GET"),
HeaderField::new(":path", "/"),
HeaderField::new(":scheme", "http"),
];
let err = normalize_http2_request(&headers, Transport::Plaintext).unwrap_err();
assert!(matches!(
err,
ProtocolNormalizeError::ProtocolViolation(ref m) if m.contains("missing :authority")
));
}
#[test]
fn normalize_http2_connect_without_authority_ok() {
let headers = vec![
HeaderField::new(":method", "CONNECT"),
HeaderField::new(":path", "/"),
HeaderField::new(":scheme", "http"),
];
let c = normalize_http2_request(&headers, Transport::Plaintext).unwrap();
assert_eq!(c.method, Method::Connect);
assert_eq!(c.authority_str(), "");
}
#[test]
fn normalize_http2_asterisk_form_without_authority_ok() {
let headers = vec![
HeaderField::new(":method", "OPTIONS"),
HeaderField::new(":path", "*"),
HeaderField::new(":scheme", "http"),
];
let c = normalize_http2_request(&headers, Transport::Plaintext).unwrap();
assert_eq!(c.method, Method::Options);
}
#[test]
fn normalize_http3_valid() {
let headers: Vec<(Vec<u8>, Vec<u8>)> = vec![
(b":method".to_vec(), b"POST".to_vec()),
(b":path".to_vec(), b"/submit".to_vec()),
(b":scheme".to_vec(), b"https".to_vec()),
(b":authority".to_vec(), b"example.com".to_vec()),
(b"accept".to_vec(), b"*/*".to_vec()),
];
let c = normalize_http3_request(&headers, Transport::Tls13).unwrap();
assert_eq!(c.method, Method::Post);
assert_eq!(c.protocol, Protocol::Http3);
assert_eq!(c.path_str(), "/submit");
assert_eq!(c.authority_str(), "example.com");
assert_eq!(c.header_count(), 1);
}
#[test]
fn normalize_http3_transfer_encoding_forbidden() {
let headers: Vec<(Vec<u8>, Vec<u8>)> = vec![
(b":method".to_vec(), b"GET".to_vec()),
(b":path".to_vec(), b"/".to_vec()),
(b":scheme".to_vec(), b"http".to_vec()),
(b"transfer-encoding".to_vec(), b"chunked".to_vec()),
];
let err = normalize_http3_request(&headers, Transport::Plaintext).unwrap_err();
assert!(matches!(
err,
ProtocolNormalizeError::ProtocolViolation(ref m) if m.contains("transfer-encoding")
));
}
#[test]
fn normalize_http3_missing_authority_rejected() {
let headers: Vec<(Vec<u8>, Vec<u8>)> = vec![
(b":method".to_vec(), b"GET".to_vec()),
(b":path".to_vec(), b"/".to_vec()),
(b":scheme".to_vec(), b"http".to_vec()),
];
let err = normalize_http3_request(&headers, Transport::Plaintext).unwrap_err();
assert!(matches!(
err,
ProtocolNormalizeError::ProtocolViolation(ref m) if m.contains("missing :authority")
));
}
#[test]
fn normalize_http3_connect_without_authority_ok() {
let headers: Vec<(Vec<u8>, Vec<u8>)> = vec![
(b":method".to_vec(), b"CONNECT".to_vec()),
(b":path".to_vec(), b"/".to_vec()),
(b":scheme".to_vec(), b"https".to_vec()),
];
let c = normalize_http3_request(&headers, Transport::Tls13).unwrap();
assert_eq!(c.method, Method::Connect);
}
#[test]
fn encode_response_http1_round_trip_format() {
let mut resp = CanonicalResponse::new(200);
resp.add_header(b"content-type", b"text/plain").unwrap();
resp.set_body(b"Hello");
let mut out = Vec::new();
let n = encode_response_http1(&resp, &mut out).unwrap();
assert_eq!(n, out.len());
let s = String::from_utf8(out).unwrap();
assert!(s.starts_with("HTTP/1.1 200 OK\r\n"));
assert!(s.contains("content-type: text/plain\r\n"));
assert!(s.ends_with("\r\nHello"));
}
#[test]
fn alpn_matches() {
assert_eq!(alpn_to_protocol(b"h2"), Some(Protocol::Http2));
assert_eq!(alpn_to_protocol(b"http/1.1"), Some(Protocol::Http1));
assert_eq!(alpn_to_protocol(b"h3"), Some(Protocol::Http3));
}
#[test]
fn alpn_unknown_is_none() {
assert_eq!(alpn_to_protocol(b"http/0.9"), None);
assert_eq!(alpn_to_protocol(b""), None);
assert_eq!(alpn_to_protocol(b"h2c"), None);
assert_eq!(alpn_to_protocol(b"H2"), None);
}
#[test]
fn normalize_error_display_variants() {
let cases: Vec<(ProtocolNormalizeError, &str)> = vec![
(
ProtocolNormalizeError::UnsupportedMethod("FOO".into()),
"unsupported HTTP method: FOO",
),
(
ProtocolNormalizeError::HeaderTooLong("x-big".into()),
"header too long: x-big",
),
(ProtocolNormalizeError::TooManyHeaders, "too many headers"),
(
ProtocolNormalizeError::InvalidHeaderName("bad name".into()),
"invalid header name: bad name",
),
(
ProtocolNormalizeError::InvalidHeaderValue("bad val".into()),
"invalid header value: bad val",
),
(
ProtocolNormalizeError::RequestTargetTooLong,
"request target too long",
),
(
ProtocolNormalizeError::BodyTooLarge {
size: 100,
max: 50,
},
"body too large: 100 bytes (max 50)",
),
(
ProtocolNormalizeError::ProtocolViolation("oops".into()),
"protocol violation: oops",
),
(
ProtocolNormalizeError::Internal("bug".into()),
"internal normalization error: bug",
),
];
for (err, expected) in cases {
let msg = err.to_string();
assert!(
msg.contains(expected),
"display mismatch:\n expected to contain: {expected}\n actual: {msg}"
);
}
}
#[test]
fn h23_duplicate_pseudo_header_rejected() {
fn h3(pairs: Vec<(&str, &str)>) -> Result<CanonicalRequest, ProtocolNormalizeError> {
let bytes: Vec<(Vec<u8>, Vec<u8>)> = pairs
.into_iter()
.map(|(n, v)| (n.as_bytes().to_vec(), v.as_bytes().to_vec()))
.collect();
normalize_http3_request(&bytes, Transport::Tls13)
}
for dup in [":method", ":path", ":scheme", ":authority"] {
let err = h3(vec![
(":method", "GET"),
(":path", "/"),
(":scheme", "https"),
(":authority", "a.com"),
(dup, "x"),
])
.unwrap_err();
assert!(
matches!(err, ProtocolNormalizeError::ProtocolViolation(ref m) if m.contains("duplicate pseudo-header")),
"duplicate {dup} not rejected: {err}"
);
}
let ok = h3(vec![
(":method", "GET"), (":path", "/"), (":scheme", "https"), (":authority", "a.com"),
]);
assert!(ok.is_ok());
}
#[test]
fn h23_uppercase_header_name_rejected() {
let bytes = vec![
(b":method".to_vec(), b"GET".to_vec()),
(b":path".to_vec(), b"/".to_vec()),
(b":scheme".to_vec(), b"https".to_vec()),
(b":authority".to_vec(), b"a.com".to_vec()),
(b"X-Custom".to_vec(), b"v".to_vec()),
];
let err = normalize_http3_request(&bytes, Transport::Tls13).unwrap_err();
assert!(matches!(
err,
ProtocolNormalizeError::ProtocolViolation(ref m) if m.contains("uppercase header name")
));
let ok = vec![
(b":method".to_vec(), b"GET".to_vec()),
(b":path".to_vec(), b"/".to_vec()),
(b":scheme".to_vec(), b"https".to_vec()),
(b":authority".to_vec(), b"a.com".to_vec()),
(b"x-custom-1".to_vec(), b"v".to_vec()),
];
assert!(normalize_http3_request(&ok, Transport::Tls13).is_ok());
}
#[test]
fn h23_te_must_be_trailers() {
let ok = vec![
(b":method".to_vec(), b"GET".to_vec()),
(b":path".to_vec(), b"/".to_vec()),
(b":scheme".to_vec(), b"https".to_vec()),
(b":authority".to_vec(), b"a.com".to_vec()),
(b"te".to_vec(), b"trailers".to_vec()),
];
assert!(normalize_http3_request(&ok, Transport::Tls13).is_ok());
for bad in ["chunked", "identity", "trailers, chunked"] {
let bytes = vec![
(b":method".to_vec(), b"GET".to_vec()),
(b":path".to_vec(), b"/".to_vec()),
(b":scheme".to_vec(), b"https".to_vec()),
(b":authority".to_vec(), b"a.com".to_vec()),
(b"te".to_vec(), bad.as_bytes().to_vec()),
];
let err = normalize_http3_request(&bytes, Transport::Tls13).unwrap_err();
assert!(
matches!(err, ProtocolNormalizeError::ProtocolViolation(ref m) if m.contains("te header")),
"te={bad} not rejected: {err}"
);
}
}
#[test]
fn h2_duplicate_authority_rejected() {
use zenith_http2::hpack::HeaderField;
let fields = vec![
HeaderField::new(":method", "GET"),
HeaderField::new(":path", "/"),
HeaderField::new(":scheme", "https"),
HeaderField::new(":authority", "a.com"),
HeaderField::new(":authority", "b.com"),
];
let err = normalize_http2_request(&fields, Transport::Tls13).unwrap_err();
assert!(matches!(
err,
ProtocolNormalizeError::ProtocolViolation(ref m) if m.contains("duplicate pseudo-header")
));
}
}