dpop_verifier/
actix_helpers.rs1#[cfg(feature = "actix-web")]
2use actix_web::HttpRequest;
3
4#[cfg(feature = "actix-web")]
5use crate::DpopError;
6
7#[cfg(feature = "actix-web")]
8pub fn dpop_header_str(req: &HttpRequest) -> Result<&str, DpopError> {
10 let mut it = req.headers().get_all("DPoP");
12 let first = it.next().ok_or(DpopError::MalformedJws)?;
13 if it.next().is_some() {
14 return Err(DpopError::MalformedJws);
15 }
16
17 let s = first.to_str().map_err(|_| DpopError::MalformedJws)?;
20 if s.contains(',') || s.contains(' ') || s.contains('\t') || s.contains('=') {
21 return Err(DpopError::MalformedJws);
22 }
23 Ok(s)
24}
25
26#[cfg(feature = "actix-web")]
27pub fn expected_htu_from_actix(req: &actix_web::HttpRequest, trust_proxies: bool) -> String {
29 let scheme = if trust_proxies {
30 req.headers()
31 .get("x-forwarded-proto")
32 .and_then(|v| v.to_str().ok())
33 .map(|s| s.trim().to_ascii_lowercase())
34 } else {
35 None
36 }
37 .unwrap_or_else(|| req.connection_info().scheme().to_ascii_lowercase());
38
39 let host = if trust_proxies {
41 req.headers()
42 .get("x-forwarded-host")
43 .and_then(|v| v.to_str().ok())
44 .and_then(|s| s.split(',').next())
45 .map(|s| s.trim().to_string())
46 } else {
47 None
48 }
49 .unwrap_or_else(|| req.connection_info().host().to_string());
50
51 let port_opt = if trust_proxies {
53 req.headers()
54 .get("x-forwarded-port")
55 .and_then(|v| v.to_str().ok())
56 .and_then(|s| s.parse::<u16>().ok())
57 } else {
58 None
59 };
60
61 let path = {
62 let p = req.uri().path();
63 if p.is_empty() {
64 "/"
65 } else {
66 p
67 }
68 };
69
70 let is_default =
72 (scheme == "http" && port_opt == Some(80)) || (scheme == "https" && port_opt == Some(443));
73 let host_lc = host.to_ascii_lowercase();
74 if let Some(p) = port_opt {
75 if !is_default {
76 return format!("{scheme}://{host_lc}:{p}{path}");
77 }
78 }
79 format!("{scheme}://{host_lc}{path}")
80}
81
82#[cfg(all(test, feature = "actix-web"))]
83mod actix_helper_tests {
84 use super::{dpop_header_str, expected_htu_from_actix};
85 use actix_web::test;
86 use actix_web::test::TestRequest;
87
88 #[actix_web::test]
91 async fn dpop_header_ok() {
92 let req = test::TestRequest::default()
93 .insert_header(("DPoP", "abc.def.ghi"))
94 .to_http_request();
95 assert_eq!(dpop_header_str(&req).unwrap(), "abc.def.ghi");
96 }
97
98 #[actix_web::test]
99 async fn dpop_header_missing() {
100 let req = test::TestRequest::default().to_http_request();
101 assert!(dpop_header_str(&req).is_err());
102 }
103
104 #[test]
105 async fn dpop_header_multiple() {
106 let req = TestRequest::default()
108 .append_header(("DPoP", "v1.header.payload")) .append_header(("DPoP", "v2.header.payload"))
110 .to_http_request();
111 assert!(dpop_header_str(&req).is_err());
112 }
113
114 #[test]
115 async fn dpop_header_single_field_comma_list_rejected() {
116 let req = TestRequest::default()
118 .insert_header(("DPoP", "a.b.c,d.e.f"))
119 .to_http_request();
120 assert!(dpop_header_str(&req).is_err());
121 }
122
123 #[test]
124 async fn dpop_header_invalid_token68_ascii() {
125 let req = TestRequest::default()
127 .insert_header(("DPoP", "abc==")) .to_http_request();
129 assert!(dpop_header_str(&req).is_err());
130
131 let req2 = TestRequest::default()
132 .insert_header(("DPoP", "abc def")) .to_http_request();
134 assert!(dpop_header_str(&req2).is_err());
135
136 let req3 = TestRequest::default()
137 .insert_header(("DPoP", "abc,def")) .to_http_request();
139 assert!(dpop_header_str(&req3).is_err());
140 }
141
142 #[actix_web::test]
145 async fn canonicalize_basic_no_proxy() {
146 let req = test::TestRequest::default()
148 .insert_header(("Host", "api.example.com:8080"))
149 .uri("/a")
150 .to_http_request();
151
152 let got = expected_htu_from_actix(&req, false);
153 assert_eq!(got, "http://api.example.com:8080/a");
154 }
155
156 #[actix_web::test]
157 async fn canonicalize_uses_x_forwarded_and_drops_default_port() {
158 let req = test::TestRequest::default()
161 .insert_header(("Host", "ignored.local:1234"))
162 .insert_header(("X-Forwarded-Proto", "HTTPS"))
163 .insert_header(("X-Forwarded-Host", "EXAMPLE.COM"))
164 .insert_header(("X-Forwarded-Port", "443"))
165 .uri("/a/../b?x=1#frag")
166 .to_http_request();
167
168 let got = expected_htu_from_actix(&req, true);
169 assert_eq!(got, "https://example.com/a/../b");
171 }
172}