Skip to main content

http_sig/
canonicalize.rs

1use http::HeaderValue;
2use itertools::{Either, Itertools};
3use thiserror::Error;
4
5use crate::header::{Header, PseudoHeader};
6
7/// The types of error which may occur whilst computing the canonical "signature string"
8/// for a request.
9#[derive(Debug, Error)]
10#[non_exhaustive]
11pub enum CanonicalizeError {
12    /// One or more headers required to be part of the signature was not present
13    /// on the request, and the `skip_missing` configuration option
14    /// was disabled.
15    #[error("Missing headers required for signature: {0:?}")]
16    MissingHeaders(Vec<Header>),
17}
18
19/// Base trait for all request types
20pub trait RequestLike {
21    /// Returns an existing header on the request. This method *must* reflect changes made
22    /// be the `ClientRequestLike::set_header` method, with the possible exception of the
23    /// `Authorization` header itself.
24    fn header(&self, header: &Header) -> Option<HeaderValue>;
25
26    /// Returns true if this request contains a value for the specified header. If this
27    /// returns true, following requests to `header()` for the same name must return a
28    /// value.
29    fn has_header(&self, header: &Header) -> bool {
30        self.header(header).is_some()
31    }
32}
33
34impl<T: RequestLike> RequestLike for &T {
35    fn header(&self, header: &Header) -> Option<HeaderValue> {
36        (**self).header(header)
37    }
38}
39
40/// Configuration for computing the canonical "signature string" of a request.
41#[derive(Default)]
42pub struct CanonicalizeConfig {
43    headers: Option<Vec<Header>>,
44    signature_created: Option<HeaderValue>,
45    signature_expires: Option<HeaderValue>,
46}
47
48impl CanonicalizeConfig {
49    /// Creates a new canonicalization configuration using the default values.
50    pub fn new() -> Self {
51        Self::default()
52    }
53    /// Set the headers to include in the signature
54    pub fn with_headers(mut self, headers: Vec<Header>) -> Self {
55        self.headers = Some(headers);
56        self
57    }
58    /// Set the headers to include in the signature
59    pub fn set_headers(&mut self, headers: Vec<Header>) -> &mut Self {
60        self.headers = Some(headers);
61        self
62    }
63    /// Get the headers to include in the signature
64    pub fn headers(&self) -> Option<impl IntoIterator<Item = &Header>> {
65        self.headers.as_ref()
66    }
67    /// Set the "signature created" pseudo-header
68    pub fn with_signature_created(mut self, signature_created: HeaderValue) -> Self {
69        self.signature_created = Some(signature_created);
70        self
71    }
72    /// Set the "signature created" pseudo-header
73    pub fn set_signature_created(&mut self, signature_created: HeaderValue) -> &mut Self {
74        self.signature_created = Some(signature_created);
75        self
76    }
77    /// Get the "signature created" pseudo-header
78    pub fn signature_created(&self) -> Option<&HeaderValue> {
79        self.signature_created.as_ref()
80    }
81    /// Set the "signature expires" pseudo-header
82    pub fn with_signature_expires(mut self, signature_expires: HeaderValue) -> Self {
83        self.signature_expires = Some(signature_expires);
84        self
85    }
86    /// Set the "signature expires" pseudo-header
87    pub fn set_signature_expires(&mut self, signature_expires: HeaderValue) -> &mut Self {
88        self.signature_expires = Some(signature_expires);
89        self
90    }
91    /// Get the "signature expires" pseudo-header
92    pub fn signature_expires(&self) -> Option<&HeaderValue> {
93        self.signature_expires.as_ref()
94    }
95}
96
97/// Extension method for computing the canonical "signature string" of a request.
98pub trait CanonicalizeExt {
99    /// Compute the canonical representation of this request
100    fn canonicalize(
101        &self,
102        config: &CanonicalizeConfig,
103    ) -> Result<SignatureString, CanonicalizeError>;
104}
105
106const DEFAULT_HEADERS: &[Header] = &[Header::Pseudo(PseudoHeader::Created)];
107
108/// Opaque struct storing a computed signature string.
109pub struct SignatureString {
110    content: Vec<u8>,
111    pub(crate) headers: Vec<(Header, HeaderValue)>,
112}
113
114impl SignatureString {
115    /// Obtain a view of this signature string as a byte slice
116    pub fn as_bytes(&self) -> &[u8] {
117        &self.content
118    }
119}
120
121impl From<SignatureString> for Vec<u8> {
122    fn from(other: SignatureString) -> Self {
123        other.content
124    }
125}
126
127impl<T: RequestLike> CanonicalizeExt for T {
128    fn canonicalize(
129        &self,
130        config: &CanonicalizeConfig,
131    ) -> Result<SignatureString, CanonicalizeError> {
132        // Find value of each header
133        let (headers, missing_headers): (Vec<_>, Vec<_>) = config
134            .headers
135            .as_deref()
136            .unwrap_or(DEFAULT_HEADERS)
137            .iter()
138            .cloned()
139            .partition_map(|header| {
140                if let Some(header_value) = match header {
141                    Header::Pseudo(PseudoHeader::Created) => config.signature_created.clone(),
142                    Header::Pseudo(PseudoHeader::Expires) => config.signature_expires.clone(),
143                    _ => self.header(&header),
144                } {
145                    Either::Left((header, header_value))
146                } else {
147                    Either::Right(header)
148                }
149            });
150
151        // Check for missing headers
152        if !missing_headers.is_empty() {
153            return Err(CanonicalizeError::MissingHeaders(missing_headers));
154        }
155
156        // Build signature string block
157        let mut content = Vec::new();
158        for (name, value) in &headers {
159            if !content.is_empty() {
160                content.push(b'\n');
161            }
162            content.extend(name.as_str().as_bytes());
163            content.extend(b": ");
164            content.extend(value.as_bytes());
165        }
166
167        Ok(SignatureString { content, headers })
168    }
169}