1use std::borrow::Cow;
19use std::time::Duration;
20
21use crate::{Error, Result};
22use http::HeaderMap;
23use http::HeaderValue;
24use http::Method;
25use http::header::HeaderName;
26use http::uri::Authority;
27use http::uri::PathAndQuery;
28use http::uri::Scheme;
29
30fn parse_query(query: &str) -> Vec<(String, String)> {
31 query
32 .split('&')
33 .filter(|pair| !pair.is_empty())
34 .map(|pair| {
35 let (key, value) = pair.split_once('=').unwrap_or((pair, ""));
36 (
37 percent_encoding::percent_decode_str(key)
38 .decode_utf8_lossy()
39 .into_owned(),
40 percent_encoding::percent_decode_str(value)
41 .decode_utf8_lossy()
42 .into_owned(),
43 )
44 })
45 .collect()
46}
47
48#[derive(Debug)]
56pub struct SigningRequest {
57 pub method: Method,
59 pub scheme: Scheme,
61 pub authority: Authority,
63 pub path: String,
68 pub query: Vec<(String, String)>,
73 pub headers: HeaderMap,
75}
76
77impl SigningRequest {
78 pub fn build(parts: &mut http::request::Parts) -> Result<Self> {
84 let uri = parts.uri.clone().into_parts();
85 let paq = uri
86 .path_and_query
87 .unwrap_or_else(|| PathAndQuery::from_static("/"));
88
89 Ok(SigningRequest {
90 method: parts.method.clone(),
91 scheme: uri.scheme.unwrap_or(Scheme::HTTP),
92 authority: uri.authority.ok_or_else(|| {
93 Error::request_invalid("request without authority is invalid for signing")
94 })?,
95 path: paq.path().to_string(),
96 query: paq.query().map(parse_query).unwrap_or_default(),
97 headers: parts.headers.clone(),
98 })
99 }
100
101 pub fn apply(self, parts: &mut http::request::Parts) -> Result<()> {
111 #[cfg(debug_assertions)]
112 self.validate_request_view(parts)?;
113
114 parts.headers = self.headers;
115
116 Ok(())
117 }
118
119 #[cfg(debug_assertions)]
120 fn validate_request_view(&self, parts: &http::request::Parts) -> Result<()> {
121 let uri = parts.uri.clone().into_parts();
122 let paq = uri
123 .path_and_query
124 .unwrap_or_else(|| PathAndQuery::from_static("/"));
125 let scheme = uri.scheme.unwrap_or(Scheme::HTTP);
126 let authority = uri.authority.ok_or_else(|| {
127 Error::request_invalid("request without authority is invalid for signing")
128 })?;
129 let query = paq.query().map(parse_query).unwrap_or_default();
130
131 if self.method != parts.method
132 || self.scheme != scheme
133 || self.authority != authority
134 || self.path != paq.path()
135 || self.query != query
136 {
137 return Err(Error::request_invalid(
138 "signing request method or URI working view was modified",
139 ));
140 }
141
142 Ok(())
143 }
144
145 pub fn path_percent_decoded(&self) -> Cow<'_, str> {
151 percent_encoding::percent_decode_str(&self.path).decode_utf8_lossy()
152 }
153
154 #[inline]
158 pub fn query_size(&self) -> usize {
159 self.query
160 .iter()
161 .map(|(k, v)| k.len() + v.len())
162 .sum::<usize>()
163 }
164
165 #[inline]
171 pub fn query_push(&mut self, key: impl Into<String>, value: impl Into<String>) {
172 self.query.push((key.into(), value.into()));
173 }
174
175 #[inline]
179 pub fn query_append(&mut self, query: &str) {
180 self.query.push((query.to_string(), "".to_string()));
181 }
182
183 pub fn query_to_vec_with_filter(&self, filter: impl Fn(&str) -> bool) -> Vec<(String, String)> {
185 self.query
186 .iter()
187 .filter(|(k, _)| filter(k))
189 .map(|(k, v)| (k.to_string(), v.to_string()))
191 .collect()
192 }
193
194 pub fn query_to_string(mut query: Vec<(String, String)>, sep: &str, join: &str) -> String {
202 let mut s = String::with_capacity(16);
203
204 query.sort();
206
207 for (idx, (k, v)) in query.into_iter().enumerate() {
208 if idx != 0 {
209 s.push_str(join);
210 }
211
212 s.push_str(&k);
213 if !v.is_empty() {
214 s.push_str(sep);
215 s.push_str(&v);
216 }
217 }
218
219 s
220 }
221
222 pub fn query_to_percent_decoded_string(
232 mut query: Vec<(String, String)>,
233 sep: &str,
234 join: &str,
235 ) -> String {
236 let mut s = String::with_capacity(16);
237
238 query.sort();
240
241 for (idx, (k, v)) in query.into_iter().enumerate() {
242 if idx != 0 {
243 s.push_str(join);
244 }
245
246 s.push_str(&k);
247 if !v.is_empty() {
248 s.push_str(sep);
249 s.push_str(&percent_encoding::percent_decode_str(&v).decode_utf8_lossy());
250 }
251 }
252
253 s
254 }
255
256 #[inline]
260 pub fn header_get_or_default(&self, key: &HeaderName) -> Result<&str> {
261 match self.headers.get(key) {
262 Some(v) => v
263 .to_str()
264 .map_err(|e| Error::request_invalid("invalid header value").with_source(e)),
265 None => Ok(""),
266 }
267 }
268
269 pub fn header_value_normalize(v: &mut HeaderValue) {
275 let bs = v.as_bytes();
276
277 let starting_index = bs.iter().position(|b| *b != b' ').unwrap_or(0);
278 let ending_offset = bs.iter().rev().position(|b| *b != b' ').unwrap_or(0);
279 let ending_index = bs.len() - ending_offset;
280
281 *v = HeaderValue::from_bytes(&bs[starting_index..ending_index])
283 .expect("invalid header value")
284 }
285
286 pub fn header_name_to_vec_sorted(&self) -> Vec<&str> {
288 let mut h = self
289 .headers
290 .keys()
291 .map(|k| k.as_str())
292 .collect::<Vec<&str>>();
293 h.sort_unstable();
294
295 h
296 }
297
298 pub fn header_to_vec_with_prefix(&self, prefix: &str) -> Vec<(String, String)> {
300 self.headers
301 .iter()
302 .filter(|(k, _)| k.as_str().starts_with(prefix))
304 .map(|(k, v)| {
306 (
307 k.as_str().to_lowercase(),
308 v.to_str().expect("must be valid header").to_string(),
309 )
310 })
311 .collect()
312 }
313
314 pub fn header_to_string(mut headers: Vec<(String, String)>, sep: &str, join: &str) -> String {
320 let mut s = String::with_capacity(16);
321
322 headers.sort();
324
325 for (idx, (k, v)) in headers.into_iter().enumerate() {
326 if idx != 0 {
327 s.push_str(join);
328 }
329
330 s.push_str(&k);
331 s.push_str(sep);
332 s.push_str(&v);
333 }
334
335 s
336 }
337}
338
339#[derive(Copy, Clone, PartialEq, Eq)]
344pub enum SigningMethod {
345 Header,
347 Query(Duration),
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354 use http::{HeaderValue, Request};
355
356 const RAW_QUERY: &str = "slash=%2F&hash=%23&=%26&equals=%3D&space=%20&encoded-plus=%2B&literal-plus=+&double=%252F&dup=first&dup=second&=empty-key&empty=&flag&flag=&";
357
358 fn request_parts() -> http::request::Parts {
359 Request::get(format!("https://example.com/object%2Fname?{RAW_QUERY}"))
360 .header("x-original", " value ")
361 .body(())
362 .expect("request must build")
363 .into_parts()
364 .0
365 }
366
367 #[test]
368 fn build_is_read_only_and_parses_wire_query_once() {
369 let mut parts = request_parts();
370 let original = parts.clone();
371
372 let signing = SigningRequest::build(&mut parts).expect("signing request must build");
373
374 assert_eq!(parts.method, original.method);
375 assert_eq!(parts.uri, original.uri);
376 assert_eq!(parts.version, original.version);
377 assert_eq!(parts.headers, original.headers);
378 assert_eq!(signing.path, "/object%2Fname");
379 assert_eq!(
380 signing.query,
381 vec![
382 ("slash".to_string(), "/".to_string()),
383 ("hash".to_string(), "#".to_string()),
384 ("amp".to_string(), "&".to_string()),
385 ("equals".to_string(), "=".to_string()),
386 ("space".to_string(), " ".to_string()),
387 ("encoded-plus".to_string(), "+".to_string()),
388 ("literal-plus".to_string(), "+".to_string()),
389 ("double".to_string(), "%2F".to_string()),
390 ("dup".to_string(), "first".to_string()),
391 ("dup".to_string(), "second".to_string()),
392 (String::new(), "empty-key".to_string()),
393 ("empty".to_string(), String::new()),
394 ("flag".to_string(), String::new()),
395 ("flag".to_string(), String::new()),
396 ]
397 );
398 }
399
400 #[test]
401 fn build_error_leaves_request_unchanged() {
402 let mut parts = Request::get("/relative")
403 .header("x-original", "value")
404 .body(())
405 .expect("request must build")
406 .into_parts()
407 .0;
408 let original = parts.clone();
409
410 assert!(SigningRequest::build(&mut parts).is_err());
411 assert_eq!(parts.method, original.method);
412 assert_eq!(parts.uri, original.uri);
413 assert_eq!(parts.version, original.version);
414 assert_eq!(parts.headers, original.headers);
415 }
416
417 #[test]
418 fn apply_commits_only_headers() {
419 let mut parts = request_parts();
420 let original = parts.clone();
421 let mut signing =
422 SigningRequest::build(&mut parts).expect("signing request must build successfully");
423 signing
424 .headers
425 .insert("authorization", HeaderValue::from_static("signed"));
426
427 signing.apply(&mut parts).expect("apply must succeed");
428
429 assert_eq!(parts.method, original.method);
430 assert_eq!(parts.uri, original.uri);
431 assert_eq!(parts.version, original.version);
432 assert_eq!(
433 parts.headers.get("authorization"),
434 Some(&HeaderValue::from_static("signed"))
435 );
436 }
437
438 #[cfg(debug_assertions)]
439 #[test]
440 fn apply_rejects_modified_request_view_atomically() {
441 type ViewMutation = Box<dyn Fn(&mut SigningRequest)>;
442
443 let mutations: Vec<ViewMutation> = vec![
444 Box::new(|signing| signing.method = Method::POST),
445 Box::new(|signing| signing.scheme = Scheme::HTTP),
446 Box::new(|signing| signing.authority = "other.example.com".parse().unwrap()),
447 Box::new(|signing| signing.path.push_str("/changed")),
448 Box::new(|signing| signing.query_push("auth", "value")),
449 ];
450
451 for mutate in mutations {
452 let mut parts = request_parts();
453 let original = parts.clone();
454 let mut signing =
455 SigningRequest::build(&mut parts).expect("signing request must build successfully");
456 signing
457 .headers
458 .insert("authorization", HeaderValue::from_static("signed"));
459 mutate(&mut signing);
460
461 assert!(signing.apply(&mut parts).is_err());
462 assert_eq!(parts.method, original.method);
463 assert_eq!(parts.uri, original.uri);
464 assert_eq!(parts.version, original.version);
465 assert_eq!(parts.headers, original.headers);
466 }
467 }
468
469 #[cfg(not(debug_assertions))]
470 #[test]
471 fn apply_omits_request_view_validation_in_release() {
472 let mut parts = request_parts();
473 let original = parts.clone();
474 let mut signing =
475 SigningRequest::build(&mut parts).expect("signing request must build successfully");
476 signing.method = Method::POST;
477 signing.scheme = Scheme::HTTP;
478 signing.authority = "other.example.com".parse().unwrap();
479 signing.path.push_str("/changed");
480 signing.query_push("auth", "value");
481 signing
482 .headers
483 .insert("authorization", HeaderValue::from_static("signed"));
484
485 signing.apply(&mut parts).expect("apply must succeed");
486
487 assert_eq!(parts.method, original.method);
488 assert_eq!(parts.uri, original.uri);
489 assert_eq!(parts.version, original.version);
490 assert_eq!(
491 parts.headers.get("authorization"),
492 Some(&HeaderValue::from_static("signed"))
493 );
494 }
495}