#[derive(Clone, Copy)]
pub struct RequestContext<'a> {
pub method: &'a str,
pub uri: &'a str,
pub body: Option<&'a [u8]>,
pub headers: &'a [(&'a str, &'a str)],
pub peer_addr: Option<std::net::SocketAddr>,
}
impl<'a> RequestContext<'a> {
pub fn new(method: &'a str, uri: &'a str) -> Self {
RequestContext {
method,
uri,
body: Some(&[]),
headers: &[],
peer_addr: None,
}
}
pub fn with_body(mut self, body: &'a [u8]) -> Self {
self.body = Some(body);
self
}
pub fn with_headers(mut self, headers: &'a [(&'a str, &'a str)]) -> Self {
self.headers = headers;
self
}
pub fn with_peer_addr(mut self, peer_addr: std::net::SocketAddr) -> Self {
self.peer_addr = Some(peer_addr);
self
}
pub fn header(&self, name: &str) -> Option<&'a str> {
for &(k, v) in self.headers {
if k.eq_ignore_ascii_case(name) {
return Some(v);
}
}
None
}
}
impl core::fmt::Debug for RequestContext<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
struct Headers<'a>(&'a [(&'a str, &'a str)]);
impl core::fmt::Debug for Headers<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_list()
.entries(self.0.iter().map(|(name, value)| {
let redact = name.eq_ignore_ascii_case("authorization")
|| name.eq_ignore_ascii_case("proxy-authorization");
(*name, if redact { "<redacted>" } else { value })
}))
.finish()
}
}
f.debug_struct("RequestContext")
.field("method", &self.method)
.field("uri", &self.uri)
.field("body", &self.body)
.field("headers", &Headers(self.headers))
.field("peer_addr", &self.peer_addr)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn header_lookup_is_case_insensitive() {
let headers: &[(&str, &str)] = &[("X-Forwarded-User", "alice")];
let ctx = RequestContext::new("GET", "/x").with_headers(headers);
assert_eq!(ctx.header("x-forwarded-user"), Some("alice"));
assert_eq!(ctx.header("X-FORWARDED-USER"), Some("alice"));
assert_eq!(ctx.header("x-forwarded-for"), None);
}
#[test]
fn new_defaults_to_no_headers_and_no_peer() {
let ctx = RequestContext::new("GET", "/x");
assert_eq!(ctx.header("authorization"), None);
assert_eq!(ctx.peer_addr, None);
}
#[test]
fn with_peer_addr_round_trips() {
let addr: std::net::SocketAddr = "127.0.0.1:8080".parse().unwrap();
let ctx = RequestContext::new("GET", "/x").with_peer_addr(addr);
assert_eq!(ctx.peer_addr, Some(addr));
}
#[test]
fn debug_redacts_authorization_header_value_but_keeps_other_fields() {
let auth_value = "Basic YWRtaW46aHVudGVyMi1zdXBlci1zZWNyZXQ=";
let headers: &[(&str, &str)] =
&[("Authorization", auth_value), ("X-Forwarded-User", "alice")];
let ctx = RequestContext::new("DESCRIBE", "rtsp://cam/live").with_headers(headers);
let debug = format!("{ctx:?}");
assert!(
!debug.contains("YWRtaW46aHVudGVyMi1zdXBlci1zZWNyZXQ"),
"leaked base64 secret: {debug}"
);
assert!(
!debug.contains("hunter2"),
"leaked password substring: {debug}"
);
assert!(
debug.contains("<redacted>"),
"expected redaction marker: {debug}"
);
assert!(debug.contains("DESCRIBE"), "method missing: {debug}");
assert!(debug.contains("rtsp://cam/live"), "uri missing: {debug}");
assert!(
debug.contains("Authorization"),
"header name should still be shown: {debug}"
);
assert!(
debug.contains("X-Forwarded-User") && debug.contains("alice"),
"non-secret header should render normally: {debug}"
);
}
#[test]
fn debug_redacts_proxy_authorization_header_case_insensitively() {
let headers: &[(&str, &str)] = &[("proxy-AUTHORIZATION", "Basic c2VjcmV0LXBhc3N3b3Jk")];
let ctx = RequestContext::new("GET", "/x").with_headers(headers);
let debug = format!("{ctx:?}");
assert!(
!debug.contains("c2VjcmV0LXBhc3N3b3Jk"),
"leaked base64 secret: {debug}"
);
assert!(
debug.contains("<redacted>"),
"expected redaction marker: {debug}"
);
}
}