pub trait LeashRequest {
fn cookie(&self, name: &str) -> Option<String>;
fn header(&self, name: &str) -> Option<String>;
}
fn parse_cookie_header<'a>(header: &'a str, name: &str) -> Option<&'a str> {
for pair in header.split(';') {
let pair = pair.trim();
if let Some(rest) = pair.strip_prefix(name) {
if let Some(value) = rest.strip_prefix('=') {
let value = value.trim();
if !value.is_empty() {
return Some(value);
}
}
}
}
None
}
fn header_value<T>(req: &http::Request<T>, name: &str) -> Option<String> {
req.headers()
.get(name)
.or_else(|| req.headers().get(name.to_ascii_lowercase().as_str()))
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
}
impl<T> LeashRequest for &http::Request<T> {
fn cookie(&self, name: &str) -> Option<String> {
let raw = header_value(self, "cookie")?;
parse_cookie_header(&raw, name).map(|s| s.to_string())
}
fn header(&self, name: &str) -> Option<String> {
header_value(self, name)
}
}
impl<T> LeashRequest for http::Request<T> {
fn cookie(&self, name: &str) -> Option<String> {
let raw = header_value(self, "cookie")?;
parse_cookie_header(&raw, name).map(|s| s.to_string())
}
fn header(&self, name: &str) -> Option<String> {
header_value(self, name)
}
}
impl LeashRequest for &http::request::Parts {
fn cookie(&self, name: &str) -> Option<String> {
let raw = self
.headers
.get("cookie")
.or_else(|| self.headers.get("Cookie"))
.and_then(|v| v.to_str().ok())?;
parse_cookie_header(raw, name).map(|s| s.to_string())
}
fn header(&self, name: &str) -> Option<String> {
self.headers
.get(name)
.or_else(|| self.headers.get(name.to_ascii_lowercase().as_str()))
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
}
}
impl LeashRequest for http::request::Parts {
fn cookie(&self, name: &str) -> Option<String> {
(&self).cookie(name)
}
fn header(&self, name: &str) -> Option<String> {
(&self).header(name)
}
}
impl LeashRequest for &http::HeaderMap {
fn cookie(&self, name: &str) -> Option<String> {
let raw = self
.get("cookie")
.or_else(|| self.get("Cookie"))
.and_then(|v| v.to_str().ok())?;
parse_cookie_header(raw, name).map(|s| s.to_string())
}
fn header(&self, name: &str) -> Option<String> {
self.get(name)
.or_else(|| self.get(name.to_ascii_lowercase().as_str()))
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
}
}
#[cfg(feature = "axum")]
mod axum_impl {
use super::*;
#[doc(hidden)]
#[allow(dead_code)]
pub fn extract_with_request<B>(_req: &http::Request<B>) -> &dyn LeashRequest
where
for<'a> &'a http::Request<B>: LeashRequest,
{
unreachable!("doc-only helper, never call directly")
}
}
#[cfg(feature = "actix-web")]
mod actix_impl {
use super::*;
use actix_web::HttpRequest;
impl LeashRequest for &HttpRequest {
fn cookie(&self, name: &str) -> Option<String> {
HttpRequest::cookie(self, name).map(|c| c.value().to_string())
}
fn header(&self, name: &str) -> Option<String> {
self.headers()
.get(name)
.or_else(|| self.headers().get(name.to_ascii_lowercase().as_str()))
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
}
}
impl LeashRequest for HttpRequest {
fn cookie(&self, name: &str) -> Option<String> {
HttpRequest::cookie(self, name).map(|c| c.value().to_string())
}
fn header(&self, name: &str) -> Option<String> {
self.headers()
.get(name)
.or_else(|| self.headers().get(name.to_ascii_lowercase().as_str()))
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn req_with(cookie: Option<&str>, auth: Option<&str>) -> http::Request<()> {
let mut builder = http::Request::builder().uri("/");
if let Some(c) = cookie {
builder = builder.header("cookie", c);
}
if let Some(a) = auth {
builder = builder.header("authorization", a);
}
builder.body(()).unwrap()
}
#[test]
fn cookie_extraction_works() {
let req = req_with(Some("other=foo; leash-auth=tok123; bar=baz"), None);
assert_eq!(req.cookie("leash-auth"), Some("tok123".to_string()));
assert_eq!(req.cookie("bar"), Some("baz".to_string()));
assert_eq!(req.cookie("missing"), None);
}
#[test]
fn cookie_returns_none_when_header_missing() {
let req = req_with(None, None);
assert_eq!(req.cookie("leash-auth"), None);
}
#[test]
fn similar_prefix_does_not_match() {
let req = req_with(Some("leash-auth-extra=nope"), None);
assert_eq!(req.cookie("leash-auth"), None);
}
#[test]
fn header_lookup_is_case_insensitive() {
let req = req_with(None, Some("Bearer abc.def"));
assert_eq!(req.header("authorization"), Some("Bearer abc.def".to_string()));
assert_eq!(req.header("Authorization"), Some("Bearer abc.def".to_string()));
}
#[test]
fn parts_impl_round_trips_cookies() {
let req = req_with(Some("leash-auth=parts"), None);
let (parts, _) = req.into_parts();
assert_eq!(parts.cookie("leash-auth"), Some("parts".to_string()));
}
#[test]
fn headermap_impl_round_trips() {
let req = req_with(Some("leash-auth=hdr"), Some("Bearer xyz"));
let headers = req.headers().clone();
assert_eq!((&headers).cookie("leash-auth"), Some("hdr".to_string()));
assert_eq!((&headers).header("authorization"), Some("Bearer xyz".to_string()));
}
#[test]
fn owned_request_implements_leash_request() {
let req = req_with(Some("leash-auth=owned"), None);
let v: &dyn LeashRequest = &req;
assert_eq!(v.cookie("leash-auth"), Some("owned".to_string()));
}
}