#[cfg(feature = "actix-web")]
use actix_web::HttpRequest;
#[cfg(feature = "actix-web")]
use crate::DpopError;
#[cfg(feature = "actix-web")]
pub fn dpop_header_str(req: &HttpRequest) -> Result<&str, DpopError> {
let mut it = req.headers().get_all("DPoP");
let first = it.next().ok_or(DpopError::MalformedJws)?;
if it.next().is_some() {
return Err(DpopError::MalformedJws);
}
let s = first.to_str().map_err(|_| DpopError::MalformedJws)?;
if s.contains(',') || s.contains(' ') || s.contains('\t') || s.contains('=') {
return Err(DpopError::MalformedJws);
}
Ok(s)
}
#[cfg(feature = "actix-web")]
pub fn expected_htu_from_actix(req: &actix_web::HttpRequest, trust_proxies: bool) -> String {
let scheme = if trust_proxies {
req.headers()
.get("x-forwarded-proto")
.and_then(|v| v.to_str().ok())
.map(|s| s.trim().to_ascii_lowercase())
} else {
None
}
.unwrap_or_else(|| req.connection_info().scheme().to_ascii_lowercase());
let host = if trust_proxies {
req.headers()
.get("x-forwarded-host")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.split(',').next())
.map(|s| s.trim().to_string())
} else {
None
}
.unwrap_or_else(|| req.connection_info().host().to_string());
let port_opt = if trust_proxies {
req.headers()
.get("x-forwarded-port")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse::<u16>().ok())
} else {
None
};
let path = {
let p = req.uri().path();
if p.is_empty() {
"/"
} else {
p
}
};
let is_default =
(scheme == "http" && port_opt == Some(80)) || (scheme == "https" && port_opt == Some(443));
let host_lc = host.to_ascii_lowercase();
if let Some(p) = port_opt {
if !is_default {
return format!("{scheme}://{host_lc}:{p}{path}");
}
}
format!("{scheme}://{host_lc}{path}")
}
#[cfg(all(test, feature = "actix-web"))]
mod actix_helper_tests {
use super::{dpop_header_str, expected_htu_from_actix};
use actix_web::test;
use actix_web::test::TestRequest;
#[actix_web::test]
async fn dpop_header_ok() {
let req = test::TestRequest::default()
.insert_header(("DPoP", "abc.def.ghi"))
.to_http_request();
assert_eq!(dpop_header_str(&req).unwrap(), "abc.def.ghi");
}
#[actix_web::test]
async fn dpop_header_missing() {
let req = test::TestRequest::default().to_http_request();
assert!(dpop_header_str(&req).is_err());
}
#[test]
async fn dpop_header_multiple() {
let req = TestRequest::default()
.append_header(("DPoP", "v1.header.payload")) .append_header(("DPoP", "v2.header.payload"))
.to_http_request();
assert!(dpop_header_str(&req).is_err());
}
#[test]
async fn dpop_header_single_field_comma_list_rejected() {
let req = TestRequest::default()
.insert_header(("DPoP", "a.b.c,d.e.f"))
.to_http_request();
assert!(dpop_header_str(&req).is_err());
}
#[test]
async fn dpop_header_invalid_token68_ascii() {
let req = TestRequest::default()
.insert_header(("DPoP", "abc==")) .to_http_request();
assert!(dpop_header_str(&req).is_err());
let req2 = TestRequest::default()
.insert_header(("DPoP", "abc def")) .to_http_request();
assert!(dpop_header_str(&req2).is_err());
let req3 = TestRequest::default()
.insert_header(("DPoP", "abc,def")) .to_http_request();
assert!(dpop_header_str(&req3).is_err());
}
#[actix_web::test]
async fn canonicalize_basic_no_proxy() {
let req = test::TestRequest::default()
.insert_header(("Host", "api.example.com:8080"))
.uri("/a")
.to_http_request();
let got = expected_htu_from_actix(&req, false);
assert_eq!(got, "http://api.example.com:8080/a");
}
#[actix_web::test]
async fn canonicalize_uses_x_forwarded_and_drops_default_port() {
let req = test::TestRequest::default()
.insert_header(("Host", "ignored.local:1234"))
.insert_header(("X-Forwarded-Proto", "HTTPS"))
.insert_header(("X-Forwarded-Host", "EXAMPLE.COM"))
.insert_header(("X-Forwarded-Port", "443"))
.uri("/a/../b?x=1#frag")
.to_http_request();
let got = expected_htu_from_actix(&req, true);
assert_eq!(got, "https://example.com/a/../b");
}
}