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> {
126 match component {
127 Component::Method => Ok(req.method().as_str().to_uppercase()),
128 Component::TargetUri => Ok(target_uri(req)),
129 Component::Authority => Ok(authority(req)),
130 Component::Scheme => Ok(scheme(req)),
131 Component::Path => Ok(req.uri().path().to_owned()),
132 Component::Query => Ok(query_with_leading_q(req)),
133 Component::RequestTarget => Ok(request_target(req)),
134 Component::Header(name) => header_value(req, name),
135 }
136}
137
138fn target_uri<B>(req: &Request<B>) -> String {
139 let scheme = scheme(req);
140 let authority = authority(req);
141 let path_and_query = req
142 .uri()
143 .path_and_query()
144 .map_or_else(|| req.uri().path().to_owned(), ToString::to_string);
145 format!("{scheme}://{authority}{path_and_query}")
146}
147
148fn authority<B>(req: &Request<B>) -> String {
149 if let Some(auth) = req.uri().authority() {
152 return auth.as_str().to_ascii_lowercase();
153 }
154 req.headers()
155 .get(http::header::HOST)
156 .and_then(|v| v.to_str().ok())
157 .map(|s| s.trim().to_ascii_lowercase())
158 .unwrap_or_default()
159}
160
161fn scheme<B>(req: &Request<B>) -> String {
162 req.uri()
163 .scheme_str()
164 .map_or_else(|| "https".to_owned(), str::to_ascii_lowercase)
165}
166
167fn query_with_leading_q<B>(req: &Request<B>) -> String {
168 req.uri()
171 .query()
172 .map_or_else(|| "?".to_owned(), |q| format!("?{q}"))
173}
174
175fn request_target<B>(req: &Request<B>) -> String {
176 req.uri()
179 .path_and_query()
180 .map_or_else(|| req.uri().path().to_owned(), ToString::to_string)
181}
182
183fn header_value<B>(req: &Request<B>, lower_name: &str) -> Result<String, Error> {
184 collect_canonical_header_value(req, lower_name)
185 .ok_or_else(|| Error::RequiredHeaderAbsent(lower_name.to_owned()))
186}
187
188#[allow(
197 clippy::expect_used,
198 clippy::unwrap_in_result,
199 reason = "writing to an owned String via core::fmt::Write is infallible; the Result on write! only exists to satisfy the trait"
200)]
201pub(crate) fn build_signature_base<B>(
202 req: &Request<B>,
203 components: &[Component],
204 signature_params_inner_list: &str,
205) -> Result<String, Error> {
206 use core::fmt::Write as _;
207 let mut out = String::new();
208 let infallible = "writing to an owned String is infallible";
209 for component in components {
210 let line = canonical_value(component, req)?;
211 writeln!(out, "{}: {line}", component.lexical()).expect(infallible);
212 }
213 write!(out, r#""@signature-params": {signature_params_inner_list}"#).expect(infallible);
214 Ok(out)
215}
216
217#[cfg(test)]
218mod tests {
219 use http::{Method, Request};
220 use pretty_assertions::assert_eq;
221
222 use super::*;
223
224 fn sample() -> Request<Vec<u8>> {
225 Request::builder()
226 .method(Method::POST)
227 .uri("https://example.com/inbox?a=1")
228 .header("host", "example.com")
229 .header("date", "Sun, 05 Jan 2014 21:31:40 GMT")
230 .body(Vec::new())
231 .expect("valid")
232 }
233
234 #[test]
235 fn method_is_uppercase() {
236 assert_eq!(
237 canonical_value(&Component::Method, &sample()).unwrap(),
238 "POST"
239 );
240 }
241
242 #[test]
243 fn target_uri_includes_scheme_authority_path_and_query() {
244 assert_eq!(
245 canonical_value(&Component::TargetUri, &sample()).unwrap(),
246 "https://example.com/inbox?a=1",
247 );
248 }
249
250 #[test]
251 fn authority_is_lowercase() {
252 let req = Request::builder()
253 .method(Method::POST)
254 .uri("https://EXAMPLE.COM/inbox")
255 .body(Vec::<u8>::new())
256 .expect("valid");
257 assert_eq!(
258 canonical_value(&Component::Authority, &req).unwrap(),
259 "example.com"
260 );
261 }
262
263 #[test]
264 fn path_and_query_are_separate() {
265 let req = sample();
266 assert_eq!(canonical_value(&Component::Path, &req).unwrap(), "/inbox");
267 assert_eq!(canonical_value(&Component::Query, &req).unwrap(), "?a=1");
268 }
269
270 #[test]
271 fn empty_query_canonicalises_to_single_question_mark() {
272 let req = Request::builder()
274 .method(Method::POST)
275 .uri("https://example.com/inbox")
276 .body(Vec::<u8>::new())
277 .expect("valid");
278 assert_eq!(canonical_value(&Component::Query, &req).unwrap(), "?");
279 }
280
281 #[test]
282 fn request_target_excludes_method_per_rfc9421() {
283 let req = sample();
286 assert_eq!(
287 canonical_value(&Component::RequestTarget, &req).unwrap(),
288 "/inbox?a=1",
289 );
290 }
291
292 #[test]
293 fn request_target_is_just_path_when_query_absent() {
294 let req = Request::builder()
295 .method(Method::POST)
296 .uri("https://example.com/inbox")
297 .body(Vec::<u8>::new())
298 .expect("valid");
299 assert_eq!(
300 canonical_value(&Component::RequestTarget, &req).unwrap(),
301 "/inbox",
302 );
303 }
304
305 #[test]
306 fn missing_header_reports_required_header_absent() {
307 let req = sample();
308 let err = canonical_value(&Component::Header("authorization".into()), &req)
309 .expect_err("missing header must error");
310 assert!(matches!(err, Error::RequiredHeaderAbsent(name) if name == "authorization"));
311 }
312
313 #[test]
314 fn parse_roundtrips_known_identifiers() {
315 for ident in [
316 "@method",
317 "@target-uri",
318 "@authority",
319 "@scheme",
320 "@path",
321 "@query",
322 "@request-target",
323 "date",
324 ] {
325 let c = Component::parse(ident).expect("known identifier");
326 assert_eq!(c.identifier(), ident);
327 }
328 }
329
330 #[test]
331 fn parse_rejects_unknown_derived_component() {
332 let err = Component::parse("@future").expect_err("unknown derived");
333 assert!(matches!(err, Error::UnsupportedAlgorithm(_)));
334 }
335
336 #[test]
337 fn full_signature_base_matches_expected_shape() {
338 let req = sample();
339 let components = [
340 Component::Method,
341 Component::TargetUri,
342 Component::Header("host".into()),
343 Component::Header("date".into()),
344 ];
345 let base = build_signature_base(
346 &req,
347 &components,
348 r#"("@method" "@target-uri" "host" "date");created=1704464900;keyid="kid""#,
349 )
350 .unwrap();
351 assert_eq!(
352 base,
353 concat!(
354 "\"@method\": POST\n",
355 "\"@target-uri\": https://example.com/inbox?a=1\n",
356 "\"host\": example.com\n",
357 "\"date\": Sun, 05 Jan 2014 21:31:40 GMT\n",
358 "\"@signature-params\": (\"@method\" \"@target-uri\" \"host\" \"date\");created=1704464900;keyid=\"kid\"",
359 ),
360 );
361 }
362}