use http::HeaderName;
use http::request::Builder;
pub fn header_raw(mut builder: Builder, name: &str, value: &[u8]) -> Builder {
let index = builder
.headers_ref()
.map(|h| h.get_all(name).iter().count());
builder = builder.header(name, value);
let (Some(index), Some(extensions)) = (index, builder.extensions_mut()) else {
return builder;
};
if extensions.get::<HeaderCasing>().is_none() {
extensions.insert(HeaderCasing::default());
}
let header_casing = extensions.get_mut::<HeaderCasing>().unwrap();
let key = HeaderName::from_bytes(name.as_bytes()).unwrap();
if let Some((_, _, casing)) = header_casing
.0
.iter_mut()
.find(|(n, i, _)| *n == key && *i == index)
{
*casing = name.into();
} else {
header_casing.0.push((key, index, name.into()));
}
builder
}
#[derive(Clone, Default)]
pub(super) struct HeaderCasing(Vec<(HeaderName, usize, String)>);
impl HeaderCasing {
pub(super) fn get<'a>(&'a self, name: &'a HeaderName, index: usize) -> &'a str {
self.0
.iter()
.find(|(n, i, _)| n == name && *i == index)
.map(|(_, _, casing)| casing.as_str())
.unwrap_or_else(|| name.as_str())
}
}
#[cfg(test)]
mod tests {
use super::{HeaderCasing, header_raw};
use crate::Error;
use crate::client::state::Redirect;
use crate::client::{Call, RedirectAuthHeaders};
use http::{HeaderValue, Request, StatusCode, Version, header};
fn output(request: Request<()>, size: usize) -> Vec<u8> {
let mut call = Call::new(request).unwrap().proceed();
let mut result = Vec::new();
while !call.can_proceed() {
let mut buffer = vec![0; size];
let n = call.write(&mut buffer).unwrap();
result.extend_from_slice(&buffer[..n]);
}
result
}
#[test]
fn typed_and_raw_headers_keep_map_order_incrementally() {
for raw in [false, true] {
let mut builder = Request::get("http://q.test");
for (name, value) in [
("x-first", "one"),
("x-remove", "gone"),
("x-second", "middle"),
("x-first", "two"),
("x-third", "last"),
("x-second", "end"),
] {
builder = if raw {
header_raw(builder, name, value.as_bytes())
} else {
builder.header(name, value)
};
}
builder.headers_mut().unwrap().remove("x-remove");
let request = builder.body(()).unwrap();
let mut expected = b"GET / HTTP/1.1\r\nhost: q.test\r\n".to_vec();
for (name, value) in request.headers() {
expected.extend_from_slice(name.as_str().as_bytes());
expected.extend_from_slice(b": ");
expected.extend_from_slice(value.as_bytes());
expected.extend_from_slice(b"\r\n");
}
expected.extend_from_slice(b"\r\n");
assert_eq!(output(request, 24), expected);
}
}
#[test]
fn preserves_mixed_duplicates_and_typed_headers_incrementally() {
let builder = Request::get("http://q.test").header("X-SECRET", "typed");
let builder = header_raw(builder, "X-SECRET", b"one");
let builder = header_raw(builder, "x-SeCrEt", &[0xff]);
let request = builder.header("X-SECRET", "last").body(()).unwrap();
assert_eq!(output(request, 24), b"GET / HTTP/1.1\r\nhost: q.test\r\nx-secret: typed\r\nX-SECRET: one\r\nx-SeCrEt: \xff\r\nx-secret: last\r\n\r\n");
}
#[test]
fn remove_and_reappend_uses_latest_explicit_casing() {
let mut builder = header_raw(Request::get("http://q.test"), "X-SECRET", b"old");
for name in ["x-SeCrEt", "X-secret", "X-SeCrEt"] {
builder.headers_mut().unwrap().remove("x-secret");
builder = header_raw(builder, name, b"new");
let casing = builder
.extensions_ref()
.unwrap()
.get::<HeaderCasing>()
.unwrap();
assert_eq!(casing.0.len(), 1);
assert_eq!(casing.0[0].1, 0);
assert_eq!(casing.0[0].2, name);
}
assert_eq!(
output(builder.body(()).unwrap(), 24),
b"GET / HTTP/1.1\r\nhost: q.test\r\nX-SeCrEt: new\r\n\r\n"
);
}
#[test]
fn validates_names_and_values() {
for name in ["", "X Bad", "X:Bad", "X\r\nInjected", "é"] {
assert!(
header_raw(Request::get("http://q.test"), name, b"ok")
.body(())
.is_err()
);
}
for value in [b"bad\r\nInjected: yes".as_slice(), b"bad\n", b"bad\0"] {
assert!(
header_raw(Request::get("http://q.test"), "X-Good", value)
.body(())
.is_err()
);
}
let builder = Request::get("bad uri");
assert!(header_raw(builder, "X-Good", b"ok").body(()).is_err());
}
#[test]
fn amendments_do_not_consume_original_ordinals() {
let request = header_raw(Request::get("http://q.test"), "X-SECRET", b"original")
.body(())
.unwrap();
let mut call = Call::new(request).unwrap();
call.header("X-SECRET", "amended").unwrap();
let mut call = call.proceed();
let mut buffer = [0; 256];
let n = call.write(&mut buffer).unwrap();
assert_eq!(
&buffer[..n],
b"GET / HTTP/1.1\r\nx-secret: amended\r\nhost: q.test\r\nX-SECRET: original\r\n\r\n"
);
}
#[test]
fn raw_headers_still_participate_in_protocol_validation() {
let builder = header_raw(Request::get("http://q.test"), "HOST", b"q.test");
let request = builder.header("host", "other.test").body(()).unwrap();
let mut call = Call::new(request).unwrap().proceed();
assert_eq!(
call.write(&mut [0; 256]).unwrap_err(),
Error::TooManyHostHeaders
);
let builder = header_raw(Request::post("http://q.test"), "Content-Length", b"1");
let request = builder.header("content-length", "2").body(()).unwrap();
let mut call = Call::new(request).unwrap().proceed();
assert_eq!(
call.write(&mut [0; 256]).unwrap_err(),
Error::TooManyContentLengthHeaders
);
let request = header_raw(Request::post("http://q.test"), "CONTENT-LENGTH", b"1, 2")
.body(())
.unwrap();
let mut call = Call::new(request).unwrap().proceed();
assert_eq!(
call.write(&mut [0; 256]).unwrap_err(),
Error::BadContentLengthHeader
);
let request = header_raw(Request::get("http://q.test"), "X-SECRET", b"ok")
.version(Version::HTTP_2)
.body(())
.unwrap();
let mut call = Call::new(request).unwrap().proceed();
assert_eq!(
call.write(&mut [0; 256]).unwrap_err(),
Error::UnsupportedVersion
);
}
#[test]
fn redirect_removes_sensitive_values_and_keeps_remaining_casing() {
let mut builder = Request::get("http://q.test");
for (name, value) in [
("AUTHORIZATION", b"secret".as_slice()),
("COOKIE", b"secret"),
("CONTENT-LENGTH", b"0"),
("X-SECRET", b"kept"),
] {
builder = header_raw(builder, name, value);
}
let call = Call::new(builder.body(()).unwrap()).unwrap();
let mut redirect: Call<Redirect> = Call::wrap(call.inner);
redirect.inner.status = Some(StatusCode::FOUND);
redirect.inner.location = Some(HeaderValue::from_static("http://other.test/"));
let mut next = redirect
.as_new_call(RedirectAuthHeaders::Never)
.unwrap()
.unwrap()
.proceed();
let mut buffer = [0; 256];
let n = next.write(&mut buffer).unwrap();
assert_eq!(
&buffer[..n],
b"GET / HTTP/1.1\r\nhost: other.test\r\nX-SECRET: kept\r\n\r\n"
);
}
#[test]
fn removed_metadata_does_not_affect_regenerated_headers() {
let mut request = header_raw(Request::get("http://q.test"), "HOST", b"old.test")
.body(())
.unwrap();
request.headers_mut().remove(header::HOST);
assert_eq!(
output(request, 24),
b"GET / HTTP/1.1\r\nhost: q.test\r\n\r\n"
);
}
}