Skip to main content

reqsign_core/
request.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use 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/// A service-local canonicalization view and signed-header staging area.
49///
50/// The method and URI-derived fields are read-only working values. They do not own
51/// the wire URI and must not be mutated to express signing output. The URI supplied
52/// to [`Self::build`] is already the final caller-provided representation; services
53/// derive canonical values locally and construct query authentication from the
54/// original URI.
55#[derive(Debug)]
56pub struct SigningRequest {
57    /// Read-only HTTP method used for canonicalization.
58    pub method: Method,
59    /// Read-only HTTP scheme used for canonicalization.
60    pub scheme: Scheme,
61    /// Read-only HTTP authority used for canonicalization.
62    pub authority: Authority,
63    /// Read-only, percent-encoded wire path.
64    ///
65    /// Services may derive a canonical path from this value, but must not decode it
66    /// and write the result back to the request URI.
67    pub path: String,
68    /// HTTP query parameters decoded once from the wire query for canonicalization.
69    ///
70    /// Percent escapes are decoded once, literal `+` remains `+`, and duplicate order
71    /// is retained. This working view does not own or rebuild the wire URI.
72    pub query: Vec<(String, String)>,
73    /// Staged HTTP headers committed by [`Self::apply`].
74    pub headers: HeaderMap,
75}
76
77impl SigningRequest {
78    /// Build a read-only request-target working view from http::request::Parts.
79    ///
80    /// The URI path and query must already be percent-encoded for transport, and the
81    /// URI must contain an authority. This method clones the URI-derived values and
82    /// headers; the input request head remains unchanged on success or error.
83    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    /// Commit staged headers back to http::request::Parts.
102    ///
103    /// In debug builds, this method verifies that the method, scheme, authority, path,
104    /// and decoded query working view still match `parts`. A mismatch returns an error
105    /// without changing `parts`. Release builds omit this implementation check.
106    ///
107    /// On success, only headers are committed. This method never writes the method or
108    /// URI. Query signers must construct their final URI from the original wire URI and
109    /// assign it separately after all fallible signing work succeeds.
110    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    /// Return the entire working path percent-decoded.
146    ///
147    /// This is a canonicalization helper, not a wire URI builder. Decoding the entire
148    /// path turns encoded slashes such as `%2F` into `/`; services where encoded slash
149    /// is data must decode path segments separately.
150    pub fn path_percent_decoded(&self) -> Cow<'_, str> {
151        percent_encoding::percent_decode_str(&self.path).decode_utf8_lossy()
152    }
153
154    /// Return the combined key and value length of the decoded working query.
155    ///
156    /// This is not the byte length of the wire query.
157    #[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    /// Push a new query pair into the working query view.
166    ///
167    /// This does not modify the wire URI. [`Self::apply`] rejects a modified
168    /// request-target view in debug builds and ignores it in release builds. Query
169    /// signers must construct their final URI from the original wire URI instead.
170    #[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    /// Push a query string into the working query view.
176    ///
177    /// This does not modify the wire URI; see [`Self::query_push`].
178    #[inline]
179    pub fn query_append(&mut self, query: &str) {
180        self.query.push((query.to_string(), "".to_string()));
181    }
182
183    /// Clone working query pairs whose keys match a canonicalization filter.
184    pub fn query_to_vec_with_filter(&self, filter: impl Fn(&str) -> bool) -> Vec<(String, String)> {
185        self.query
186            .iter()
187            // Filter all queries
188            .filter(|(k, _)| filter(k))
189            // Clone all queries
190            .map(|(k, v)| (k.to_string(), v.to_string()))
191            .collect()
192    }
193
194    /// Convert sorted query pairs to a canonical string.
195    ///
196    /// This helper does not produce or modify a wire URI.
197    ///
198    /// ```shell
199    /// [(a, b), (c, d)] => "a:b\nc:d"
200    /// ```
201    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        // Sort via header name.
205        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    /// Convert sorted query pairs to a string after percent-decoding each value.
223    ///
224    /// Values from [`Self::query`] have already been decoded once. Passing them to
225    /// this helper performs an additional decode and is only correct when a service
226    /// protocol explicitly requires it. This helper does not produce a wire URI.
227    ///
228    /// ```shell
229    /// [(a, b), (c, d)] => "a:b\nc:d"
230    /// ```
231    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        // Sort via header name.
239        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    /// Get header value by name.
257    ///
258    /// Returns empty string if header not found.
259    #[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    /// Normalize a header value for canonicalization.
270    ///
271    /// Normalize a clone when the protocol does not require changing the wire header;
272    /// mutating a value inside [`Self::headers`] changes the header committed by
273    /// [`Self::apply`].
274    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        // This can't fail because we started with a valid HeaderValue and then only trimmed spaces
282        *v = HeaderValue::from_bytes(&bs[starting_index..ending_index])
283            .expect("invalid header value")
284    }
285
286    /// Get header names as sorted vector.
287    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    /// Get header names with given prefix.
299    pub fn header_to_vec_with_prefix(&self, prefix: &str) -> Vec<(String, String)> {
300        self.headers
301            .iter()
302            // Filter all header that starts with prefix
303            .filter(|(k, _)| k.as_str().starts_with(prefix))
304            // Convert all header name to lowercase
305            .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    /// Convert sorted headers to string.
315    ///
316    /// ```shell
317    /// [(a, b), (c, d)] => "a:b\nc:d"
318    /// ```
319    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        // Sort via header name.
323        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/// A service-selected authentication placement.
340///
341/// This type does not define a universal mapping from `expires_in` to query
342/// authentication. Services and credential types decide which placement applies.
343#[derive(Copy, Clone, PartialEq, Eq)]
344pub enum SigningMethod {
345    /// Signing with header.
346    Header,
347    /// Signing with query.
348    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&amp=%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}