1use http::Request;
25
26use crate::error::Error;
27use crate::http_shared::collect_canonical_header_value;
28
29#[derive(Debug, Clone, PartialEq, Eq, Hash)]
35#[non_exhaustive]
36pub enum Component {
37 Method,
39 TargetUri,
41 Authority,
43 Scheme,
45 Path,
47 Query,
53 RequestTarget,
62 Header(String),
64}
65
66impl Component {
67 #[must_use]
70 pub fn lexical(&self) -> String {
71 format!(r#""{}""#, self.identifier())
72 }
73
74 #[must_use]
76 pub fn identifier(&self) -> &str {
77 match self {
78 Self::Method => "@method",
79 Self::TargetUri => "@target-uri",
80 Self::Authority => "@authority",
81 Self::Scheme => "@scheme",
82 Self::Path => "@path",
83 Self::Query => "@query",
84 Self::RequestTarget => "@request-target",
85 Self::Header(name) => name,
86 }
87 }
88
89 pub fn parse(identifier: &str) -> Result<Self, Error> {
99 if !identifier.starts_with('@') {
100 return Ok(Self::Header(identifier.to_ascii_lowercase()));
101 }
102 Ok(match identifier {
103 "@method" => Self::Method,
104 "@target-uri" => Self::TargetUri,
105 "@authority" => Self::Authority,
106 "@scheme" => Self::Scheme,
107 "@path" => Self::Path,
108 "@query" => Self::Query,
109 "@request-target" => Self::RequestTarget,
110 other => {
111 return Err(Error::UnsupportedAlgorithm(format!(
112 "derived component `{other}` is not supported"
113 )));
114 }
115 })
116 }
117}
118
119pub(crate) fn canonical_value<B>(component: &Component, req: &Request<B>) -> Result<String, Error> {
128 match component {
129 Component::Method => Ok(req.method().as_str().to_uppercase()),
130 Component::TargetUri => target_uri(req),
131 Component::Authority => authority(req),
132 Component::Scheme => Ok(scheme(req)),
133 Component::Path => Ok(req.uri().path().to_owned()),
134 Component::Query => Ok(query_with_leading_q(req)),
135 Component::RequestTarget => Ok(request_target(req)),
136 Component::Header(name) => header_value(req, name),
137 }
138}
139
140fn target_uri<B>(req: &Request<B>) -> Result<String, Error> {
141 let scheme = scheme(req);
142 let authority = authority(req)?;
143 let path_and_query = req
144 .uri()
145 .path_and_query()
146 .map_or_else(|| req.uri().path().to_owned(), ToString::to_string);
147 Ok(format!("{scheme}://{authority}{path_and_query}"))
148}
149
150fn authority<B>(req: &Request<B>) -> Result<String, Error> {
151 if let Some(auth) = req.uri().authority() {
157 return Ok(auth.as_str().to_ascii_lowercase());
158 }
159 req.headers()
160 .get(http::header::HOST)
161 .and_then(|v| v.to_str().ok())
162 .map(|s| s.trim().to_ascii_lowercase())
163 .filter(|s| !s.is_empty())
164 .ok_or_else(|| Error::RequiredHeaderAbsent(http::header::HOST.as_str().to_owned()))
165}
166
167fn scheme<B>(req: &Request<B>) -> String {
168 req.uri()
169 .scheme_str()
170 .map_or_else(|| "https".to_owned(), str::to_ascii_lowercase)
171}
172
173fn query_with_leading_q<B>(req: &Request<B>) -> String {
174 req.uri()
177 .query()
178 .map_or_else(|| "?".to_owned(), |q| format!("?{q}"))
179}
180
181fn request_target<B>(req: &Request<B>) -> String {
182 req.uri()
185 .path_and_query()
186 .map_or_else(|| req.uri().path().to_owned(), ToString::to_string)
187}
188
189fn header_value<B>(req: &Request<B>, lower_name: &str) -> Result<String, Error> {
190 collect_canonical_header_value(req, lower_name)
191 .ok_or_else(|| Error::RequiredHeaderAbsent(lower_name.to_owned()))
192}
193
194#[allow(
203 clippy::expect_used,
204 clippy::unwrap_in_result,
205 reason = "writing to an owned String via core::fmt::Write is infallible; the Result on write! only exists to satisfy the trait"
206)]
207pub(crate) fn build_signature_base<B>(
208 req: &Request<B>,
209 components: &[Component],
210 signature_params_inner_list: &str,
211) -> Result<String, Error> {
212 use core::fmt::Write as _;
213 let mut out = String::new();
214 let infallible = "writing to an owned String is infallible";
215 for component in components {
216 let line = canonical_value(component, req)?;
217 writeln!(out, "{}: {line}", component.lexical()).expect(infallible);
218 }
219 write!(out, r#""@signature-params": {signature_params_inner_list}"#).expect(infallible);
220 Ok(out)
221}
222
223#[cfg(test)]
224mod tests {
225 use http::{Method, Request};
226 use pretty_assertions::assert_eq;
227
228 use super::*;
229
230 fn sample() -> Request<Vec<u8>> {
231 Request::builder()
232 .method(Method::POST)
233 .uri("https://example.com/inbox?a=1")
234 .header("host", "example.com")
235 .header("date", "Sun, 05 Jan 2014 21:31:40 GMT")
236 .body(Vec::new())
237 .expect("valid")
238 }
239
240 #[test]
241 fn method_is_uppercase() {
242 assert_eq!(
243 canonical_value(&Component::Method, &sample()).unwrap(),
244 "POST"
245 );
246 }
247
248 #[test]
249 fn target_uri_includes_scheme_authority_path_and_query() {
250 assert_eq!(
251 canonical_value(&Component::TargetUri, &sample()).unwrap(),
252 "https://example.com/inbox?a=1",
253 );
254 }
255
256 #[test]
257 fn authority_is_lowercase() {
258 let req = Request::builder()
259 .method(Method::POST)
260 .uri("https://EXAMPLE.COM/inbox")
261 .body(Vec::<u8>::new())
262 .expect("valid");
263 assert_eq!(
264 canonical_value(&Component::Authority, &req).unwrap(),
265 "example.com"
266 );
267 }
268
269 #[test]
270 fn path_and_query_are_separate() {
271 let req = sample();
272 assert_eq!(canonical_value(&Component::Path, &req).unwrap(), "/inbox");
273 assert_eq!(canonical_value(&Component::Query, &req).unwrap(), "?a=1");
274 }
275
276 #[test]
277 fn empty_query_canonicalises_to_single_question_mark() {
278 let req = Request::builder()
280 .method(Method::POST)
281 .uri("https://example.com/inbox")
282 .body(Vec::<u8>::new())
283 .expect("valid");
284 assert_eq!(canonical_value(&Component::Query, &req).unwrap(), "?");
285 }
286
287 #[test]
288 fn request_target_excludes_method_per_rfc9421() {
289 let req = sample();
292 assert_eq!(
293 canonical_value(&Component::RequestTarget, &req).unwrap(),
294 "/inbox?a=1",
295 );
296 }
297
298 #[test]
299 fn request_target_is_just_path_when_query_absent() {
300 let req = Request::builder()
301 .method(Method::POST)
302 .uri("https://example.com/inbox")
303 .body(Vec::<u8>::new())
304 .expect("valid");
305 assert_eq!(
306 canonical_value(&Component::RequestTarget, &req).unwrap(),
307 "/inbox",
308 );
309 }
310
311 #[test]
312 fn missing_header_reports_required_header_absent() {
313 let req = sample();
314 let err = canonical_value(&Component::Header("authorization".into()), &req)
315 .expect_err("missing header must error");
316 assert!(matches!(err, Error::RequiredHeaderAbsent(name) if name == "authorization"));
317 }
318
319 #[test]
320 fn parse_roundtrips_known_identifiers() {
321 for ident in [
322 "@method",
323 "@target-uri",
324 "@authority",
325 "@scheme",
326 "@path",
327 "@query",
328 "@request-target",
329 "date",
330 ] {
331 let c = Component::parse(ident).expect("known identifier");
332 assert_eq!(c.identifier(), ident);
333 }
334 }
335
336 #[test]
337 fn parse_rejects_unknown_derived_component() {
338 let err = Component::parse("@future").expect_err("unknown derived");
339 assert!(matches!(err, Error::UnsupportedAlgorithm(_)));
340 }
341
342 #[test]
343 fn authority_errors_when_request_has_no_host_and_no_uri_authority() {
344 let req = Request::builder()
347 .method(Method::POST)
348 .uri("/inbox")
349 .body(Vec::<u8>::new())
350 .expect("valid");
351 let err = canonical_value(&Component::Authority, &req)
352 .expect_err("missing authority must surface as an error");
353 assert!(matches!(err, Error::RequiredHeaderAbsent(name) if name == "host"));
354 }
355
356 #[test]
357 fn target_uri_errors_when_request_has_no_host_and_no_uri_authority() {
358 let req = Request::builder()
359 .method(Method::POST)
360 .uri("/inbox")
361 .body(Vec::<u8>::new())
362 .expect("valid");
363 let err = canonical_value(&Component::TargetUri, &req)
364 .expect_err("missing authority must propagate through @target-uri");
365 assert!(matches!(err, Error::RequiredHeaderAbsent(_)));
366 }
367
368 #[test]
369 fn full_signature_base_matches_expected_shape() {
370 let req = sample();
371 let components = [
372 Component::Method,
373 Component::TargetUri,
374 Component::Header("host".into()),
375 Component::Header("date".into()),
376 ];
377 let base = build_signature_base(
378 &req,
379 &components,
380 r#"("@method" "@target-uri" "host" "date");created=1704464900;keyid="kid""#,
381 )
382 .unwrap();
383 assert_eq!(
384 base,
385 concat!(
386 "\"@method\": POST\n",
387 "\"@target-uri\": https://example.com/inbox?a=1\n",
388 "\"host\": example.com\n",
389 "\"date\": Sun, 05 Jan 2014 21:31:40 GMT\n",
390 "\"@signature-params\": (\"@method\" \"@target-uri\" \"host\" \"date\");created=1704464900;keyid=\"kid\"",
391 ),
392 );
393 }
394}