use http_net::{ClientConn, ClientEvent, Request, Response, ServerConn, ServerEvent};
const SPELLINGS: &[&str] = &[
"authorization",
"Authorization",
"AUTHORIZATION",
"aUtHoRiZaTiOn",
"proxy-authorization",
"Proxy-Authorization",
"PROXY-AUTHORIZATION",
"cookie",
"Cookie",
"COOKIE",
"cOoKiE",
"set-cookie",
"Set-Cookie",
"SET-COOKIE",
"sEt-CoOkIe",
];
fn secret(name: &str) -> String {
format!("s3cr3t-{}-0123456789", name.len())
}
fn assert_redacted(what: &str, shown: &str, secret: &str) {
assert!(!shown.contains(secret), "{what}: secret printed in {shown}");
let marker = format!("<redacted {} bytes>", secret.len());
assert!(shown.contains(&marker), "{what}: no {marker} in {shown}");
}
fn both(value: &impl std::fmt::Debug) -> [String; 2] {
[format!("{value:?}"), format!("{value:#?}")]
}
fn request_with(name: &str, value: &str) -> Vec<u8> {
format!("GET / HTTP/1.1\r\nHost: a\r\nX-Visible: shown\r\n{name}: {value}\r\n\r\n").into_bytes()
}
fn response_with(name: &str, value: &str) -> Vec<u8> {
format!("HTTP/1.1 200 OK\r\nX-Visible: shown\r\n{name}: {value}\r\nContent-Length: 0\r\n\r\n")
.into_bytes()
}
#[test]
fn test_request_head_headers_and_event_debug_redact_every_spelling() {
for name in SPELLINGS {
let value = secret(name);
let mut conn = ServerConn::new();
let _ = conn.feed(&request_with(name, &value));
let event = conn.poll().unwrap();
for shown in both(&event) {
assert_redacted(&format!("ServerEvent::Request / {name}"), &shown, &value);
assert!(
shown.contains("shown"),
"other fields stay visible: {shown}"
);
}
let ServerEvent::Request(req) = event else {
panic!("expected a request");
};
for shown in both(&req) {
assert_redacted(&format!("RequestHead / {name}"), &shown, &value);
}
for shown in both(&req.headers()) {
assert_redacted(&format!("Headers / {name}"), &shown, &value);
}
for shown in both(&req.header_all(name)) {
assert_redacted(&format!("HeaderValues / {name}"), &shown, &value);
}
assert_eq!(req.header(name), Some(value.as_bytes()));
assert_eq!(req.header_all(name).next(), Some(value.as_bytes()));
assert!(req.headers().any(|(_, v)| v == value.as_bytes()));
}
}
#[test]
fn test_response_head_and_client_event_debug_redact_every_spelling() {
for name in SPELLINGS {
let value = secret(name);
let mut conn = ClientConn::new();
conn.send_request(&Request::get("/").header("host", "a"), &mut Vec::new())
.unwrap();
let _ = conn.feed(&response_with(name, &value));
let event = conn.poll().unwrap();
for shown in both(&event) {
assert_redacted(&format!("ClientEvent::Response / {name}"), &shown, &value);
}
let ClientEvent::Response(resp) = event else {
panic!("expected a response");
};
for shown in both(&resp) {
assert_redacted(&format!("ResponseHead / {name}"), &shown, &value);
assert!(shown.contains("shown"));
}
for shown in both(&resp.headers()) {
assert_redacted(&format!("Headers / {name}"), &shown, &value);
}
for shown in both(&resp.header_all(name)) {
assert_redacted(&format!("HeaderValues / {name}"), &shown, &value);
}
assert_eq!(resp.header(name), Some(value.as_bytes()));
}
}
#[test]
fn test_builders_debug_redact_every_spelling() {
for name in SPELLINGS {
let value = secret(name);
let request = Request::get("/")
.header("host", "a")
.header("x-visible", "shown")
.header(name, &value);
for shown in both(&request) {
assert_redacted(&format!("Request / {name}"), &shown, &value);
assert!(shown.contains("shown"));
}
assert_eq!(request.header_value(name), Some(value.as_bytes()));
let response = Response::new(200)
.header("x-visible", "shown")
.header(name, &value);
for shown in both(&response) {
assert_redacted(&format!("Response / {name}"), &shown, &value);
}
assert_eq!(response.header_value(name), Some(value.as_bytes()));
}
}
#[test]
fn test_repeated_credential_fields_are_each_redacted() {
let mut conn = ServerConn::new();
let _ = conn
.feed(b"GET / HTTP/1.1\r\nHost: a\r\nCookie: first=aaaa\r\ncookie: second=bbbbbb\r\n\r\n");
let ServerEvent::Request(req) = conn.poll().unwrap() else {
panic!("expected a request");
};
for shown in both(&req)
.into_iter()
.chain(both(&req.header_all("cookie")))
{
assert!(
!shown.contains("first=aaaa") && !shown.contains("second=bbbbbb"),
"{shown}"
);
assert!(
shown.contains("<redacted 10 bytes>") && shown.contains("<redacted 13 bytes>"),
"{shown}"
);
}
}
#[test]
fn test_empty_credential_value_is_redacted_as_zero_bytes() {
let mut conn = ServerConn::new();
let _ = conn.feed(b"GET / HTTP/1.1\r\nHost: a\r\nAuthorization:\r\n\r\n");
let ServerEvent::Request(req) = conn.poll().unwrap() else {
panic!("expected a request");
};
assert!(format!("{req:?}").contains("<redacted 0 bytes>"));
}
#[cfg(feature = "std")]
#[test]
fn test_blocking_client_response_debug_redacts_set_cookie() {
use http_net::blocking;
let server = blocking::Server::bind("127.0.0.1:0").unwrap();
let addr = server.local_addr();
let stop = server.shutdown_handle();
let thread = std::thread::spawn(move || {
server.serve(|_req, _body| {
Response::text(200, "ok")
.header("Set-Cookie", "sid=s3cr3t-session-value; HttpOnly")
.header("set-cookie", "csrf=s3cr3t-csrf-token")
.header("x-visible", "shown")
})
});
let resp = blocking::get(&format!("http://{addr}/")).unwrap();
for shown in both(&resp) {
assert!(!shown.contains("s3cr3t"), "secret printed in {shown}");
assert!(shown.contains("<redacted 34 bytes>"), "{shown}");
assert!(shown.contains("<redacted 22 bytes>"), "{shown}");
assert!(shown.contains("shown"), "{shown}");
}
assert_eq!(
resp.header("set-cookie"),
Some(&b"sid=s3cr3t-session-value; HttpOnly"[..])
);
stop.shutdown();
thread.join().unwrap().unwrap();
}