Skip to main content

http_signature_normalization_reqwest/
lib.rs

1use http_signature_normalization::create::Signed;
2use httpdate::HttpDate;
3use reqwest::{
4    header::{InvalidHeaderValue, ToStrError},
5    Request, RequestBuilder,
6};
7use std::{
8    convert::TryInto,
9    fmt::Display,
10    time::{Duration, SystemTime},
11};
12
13pub use http_signature_normalization::RequiredError;
14
15#[cfg(feature = "digest")]
16pub mod digest;
17
18pub mod prelude {
19    pub use crate::{Config, Sign, SignError};
20
21    #[cfg(feature = "default-spawner")]
22    pub use crate::default_spawner::DefaultSpawner;
23
24    #[cfg(feature = "digest")]
25    pub use crate::digest::{DigestCreate, SignExt};
26}
27
28#[cfg(feature = "default-spawner")]
29pub use default_spawner::DefaultSpawner;
30
31#[cfg(feature = "default-spawner")]
32#[derive(Clone, Debug, Default)]
33/// Configuration for signing and verifying signatures
34///
35/// By default, the config is set up to create and verify signatures that expire after 10 seconds,
36/// and use the `(created)` and `(expires)` fields that were introduced in draft 11
37pub struct Config<Spawner = DefaultSpawner> {
38    /// The inner config type
39    config: http_signature_normalization::Config,
40
41    /// Whether to set the Host header
42    set_host: bool,
43
44    /// Whether to set the Date header
45    set_date: bool,
46
47    /// How to spawn blocking tasks
48    spawner: Spawner,
49}
50
51#[cfg(not(feature = "default-spawner"))]
52#[derive(Clone, Debug, Default)]
53/// Configuration for signing and verifying signatures
54///
55/// By default, the config is set up to create and verify signatures that expire after 10 seconds,
56/// and use the `(created)` and `(expires)` fields that were introduced in draft 11
57pub struct Config<Spawner> {
58    /// The inner config type
59    config: http_signature_normalization::Config,
60
61    /// Whether to set the Host header
62    set_host: bool,
63
64    /// Whether to set the Date header
65    set_date: bool,
66
67    /// How to spawn blocking tasks
68    spawner: Spawner,
69}
70
71#[cfg(feature = "default-spawner")]
72mod default_spawner {
73    use super::{Canceled, Config, Spawn};
74
75    impl Config<DefaultSpawner> {
76        /// Create a new config with the default spawner
77        pub fn new() -> Self {
78            Default::default()
79        }
80    }
81
82    /// A default implementation of Spawner for spawning blocking operations
83    #[derive(Clone, Copy, Debug, Default)]
84    pub struct DefaultSpawner;
85
86    /// The future returned by DefaultSpawner when spawning blocking operations on the tokio
87    /// blocking threadpool
88    pub struct DefaultSpawnerFuture<Out> {
89        inner: tokio::task::JoinHandle<Out>,
90    }
91
92    impl Spawn for DefaultSpawner {
93        type Future<T>
94            = DefaultSpawnerFuture<T>
95        where
96            T: Send;
97
98        fn spawn_blocking<Func, Out>(&self, func: Func) -> Self::Future<Out>
99        where
100            Func: FnOnce() -> Out + Send + 'static,
101            Out: Send + 'static,
102        {
103            DefaultSpawnerFuture {
104                inner: tokio::task::spawn_blocking(func),
105            }
106        }
107    }
108
109    impl<Out> std::future::Future for DefaultSpawnerFuture<Out> {
110        type Output = Result<Out, Canceled>;
111
112        fn poll(
113            mut self: std::pin::Pin<&mut Self>,
114            cx: &mut std::task::Context<'_>,
115        ) -> std::task::Poll<Self::Output> {
116            let res = std::task::ready!(std::pin::Pin::new(&mut self.inner).poll(cx));
117
118            std::task::Poll::Ready(res.map_err(|_| Canceled))
119        }
120    }
121}
122
123/// An error that indicates a blocking operation panicked and cannot return a response
124#[derive(Debug)]
125pub struct Canceled;
126
127impl std::fmt::Display for Canceled {
128    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129        write!(f, "Operation was canceled")
130    }
131}
132
133impl std::error::Error for Canceled {}
134
135/// A trait dictating how to spawn a future onto a blocking threadpool. By default,
136/// http-signature-normalization-actix will use tokio's built-in blocking threadpool, but this
137/// can be customized
138pub trait Spawn {
139    /// The future type returned by spawn_blocking
140    type Future<T>: std::future::Future<Output = Result<T, Canceled>> + Send
141    where
142        T: Send;
143
144    /// Spawn the blocking function onto the threadpool
145    fn spawn_blocking<Func, Out>(&self, func: Func) -> Self::Future<Out>
146    where
147        Func: FnOnce() -> Out + Send + 'static,
148        Out: Send + 'static;
149}
150
151/// A trait implemented by the reqwest RequestBuilder type to add an HTTP Signature to the request
152#[async_trait::async_trait]
153pub trait Sign {
154    /// Add an Authorization Signature to the request
155    async fn authorization_signature<F, E, K, S>(
156        self,
157        config: &Config<S>,
158        key_id: K,
159        f: F,
160    ) -> Result<Request, E>
161    where
162        Self: Sized,
163        F: FnOnce(&str) -> Result<String, E> + Send + 'static,
164        E: From<SignError> + From<reqwest::Error> + Send + 'static,
165        K: Display + Send,
166        S: Spawn + Send + Sync;
167
168    /// Add a Signature to the request
169    async fn signature<F, E, K, S>(self, config: &Config<S>, key_id: K, f: F) -> Result<Request, E>
170    where
171        Self: Sized,
172        F: FnOnce(&str) -> Result<String, E> + Send + 'static,
173        E: From<SignError> + From<reqwest::Error> + Send + 'static,
174        K: Display + Send,
175        S: Spawn + Send + Sync;
176}
177
178#[derive(Debug)]
179pub enum SignError {
180    /// An error occurred when reading the request's headers
181    Header(ToStrError),
182
183    /// An error occured when adding a new header
184    NewHeader(InvalidHeaderValue),
185
186    /// Some headers were marked as required, but are missing
187    RequiredError(RequiredError),
188
189    /// Missing host
190    Host(String),
191
192    /// Request Body already exists
193    BodyPresent,
194
195    /// Panic in blocking operation
196    Canceled,
197}
198
199impl std::fmt::Display for SignError {
200    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201        match self {
202            Self::Header(_) => f.write_str("Failed to read header"),
203            Self::NewHeader(_) => f.write_str("Failed to write header"),
204            Self::RequiredError(_) => f.write_str("Missing required field"),
205            Self::Host(s) => write!(f, "No host provided for URL, {s}"),
206            Self::BodyPresent => f.write_str("Cannog sign request with body already present"),
207            Self::Canceled => f.write_str("Panic in spawn blocking"),
208        }
209    }
210}
211
212impl std::error::Error for SignError {
213    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
214        match self {
215            Self::Header(h) => Some(h),
216            Self::NewHeader(n) => Some(n),
217            Self::RequiredError(r) => Some(r),
218            Self::Host(_) | Self::BodyPresent | Self::Canceled => None,
219        }
220    }
221}
222
223impl<Spawner> Config<Spawner> {
224    /// Create a new config with the provided spawner
225    pub fn new_with_spawner(spawner: Spawner) -> Self {
226        Config {
227            config: Default::default(),
228            set_host: Default::default(),
229            set_date: Default::default(),
230            spawner,
231        }
232    }
233
234    /// This method can be used to include the Host header in the HTTP Signature without
235    /// interfering with Reqwest's built-in Host mechanisms
236    pub fn set_host_header(self) -> Self {
237        Config {
238            config: self.config,
239            set_host: true,
240            set_date: self.set_date,
241            spawner: self.spawner,
242        }
243    }
244
245    /// Enable mastodon compatibility
246    ///
247    /// This is the same as disabling the use of `(created)` and `(expires)` signature fields,
248    /// requiring the Date header, and requiring the Host header
249    pub fn mastodon_compat(self) -> Self {
250        Config {
251            config: self.config.mastodon_compat(),
252            set_host: true,
253            set_date: true,
254            spawner: self.spawner,
255        }
256    }
257
258    /// Require the Digest header be set
259    ///
260    /// This is useful for POST, PUT, and PATCH requests, but doesn't make sense for GET or DELETE.
261    pub fn require_digest(self) -> Self {
262        Config {
263            config: self.config.require_digest(),
264            set_host: self.set_host,
265            set_date: self.set_date,
266            spawner: self.spawner,
267        }
268    }
269
270    /// Opt out of using the (created) and (expires) fields introduced in draft 11
271    ///
272    /// Note that by enabling this, the Date header becomes required on requests. This is to
273    /// prevent replay attacks
274    pub fn dont_use_created_field(self) -> Self {
275        Config {
276            config: self.config.dont_use_created_field(),
277            set_host: self.set_host,
278            set_date: self.set_date,
279            spawner: self.spawner,
280        }
281    }
282
283    /// Set the expiration to a custom duration
284    pub fn set_expiration(self, expiries_after: Duration) -> Self {
285        Config {
286            config: self.config.set_expiration(expiries_after),
287            set_host: self.set_host,
288            set_date: self.set_date,
289            spawner: self.spawner,
290        }
291    }
292
293    /// Require a header on signed requests
294    pub fn require_header(self, header: &str) -> Self {
295        Config {
296            config: self.config.require_header(header),
297            set_host: self.set_host,
298            set_date: self.set_date,
299            spawner: self.spawner,
300        }
301    }
302
303    pub fn set_spawner<NewSpawner: Spawn>(self, spawner: NewSpawner) -> Config<NewSpawner> {
304        Config {
305            config: self.config,
306            set_host: self.set_host,
307            set_date: self.set_date,
308            spawner,
309        }
310    }
311}
312
313#[async_trait::async_trait]
314impl Sign for RequestBuilder {
315    async fn authorization_signature<F, E, K, S>(
316        self,
317        config: &Config<S>,
318        key_id: K,
319        f: F,
320    ) -> Result<Request, E>
321    where
322        F: FnOnce(&str) -> Result<String, E> + Send + 'static,
323        E: From<SignError> + From<reqwest::Error> + Send + 'static,
324        K: Display + Send,
325        S: Spawn + Send + Sync,
326    {
327        let mut request = self.build()?;
328        let signed = prepare(&mut request, config, key_id, f).await?;
329
330        let auth_header = signed.authorization_header();
331        request.headers_mut().insert(
332            "Authorization",
333            auth_header.parse().map_err(SignError::NewHeader)?,
334        );
335
336        Ok(request)
337    }
338
339    async fn signature<F, E, K, S>(self, config: &Config<S>, key_id: K, f: F) -> Result<Request, E>
340    where
341        F: FnOnce(&str) -> Result<String, E> + Send + 'static,
342        E: From<SignError> + From<reqwest::Error> + Send + 'static,
343        K: Display + Send,
344        S: Spawn + Send + Sync,
345    {
346        let mut request = self.build()?;
347        let signed = prepare(&mut request, config, key_id, f).await?;
348
349        let sig_header = signed.signature_header();
350
351        request.headers_mut().insert(
352            "Signature",
353            sig_header.parse().map_err(SignError::NewHeader)?,
354        );
355
356        Ok(request)
357    }
358}
359
360async fn prepare<F, E, K, S>(
361    req: &mut Request,
362    config: &Config<S>,
363    key_id: K,
364    f: F,
365) -> Result<Signed, E>
366where
367    F: FnOnce(&str) -> Result<String, E> + Send + 'static,
368    E: From<SignError> + Send + 'static,
369    K: Display + Send,
370    S: Spawn,
371{
372    if config.set_date && !req.headers().contains_key("date") {
373        req.headers_mut().insert(
374            "date",
375            HttpDate::from(SystemTime::now())
376                .to_string()
377                .try_into()
378                .map_err(SignError::NewHeader)?,
379        );
380    }
381    let mut bt = std::collections::BTreeMap::new();
382    for (k, v) in req.headers().iter() {
383        bt.insert(
384            k.as_str().to_owned(),
385            v.to_str().map_err(SignError::Header)?.to_owned(),
386        );
387    }
388    if config.set_host {
389        let header_string = req
390            .url()
391            .host()
392            .ok_or_else(|| SignError::Host(req.url().to_string()))?
393            .to_string();
394
395        let header_string = match req.url().port() {
396            None | Some(443) | Some(80) => header_string,
397            Some(port) => format!("{}:{}", header_string, port),
398        };
399
400        bt.insert("Host".to_string(), header_string);
401    }
402    let path_and_query = if let Some(query) = req.url().query() {
403        format!("{}?{}", req.url().path(), query)
404    } else {
405        req.url().path().to_string()
406    };
407    let unsigned = config
408        .config
409        .begin_sign(req.method().as_str(), &path_and_query, bt)
410        .map_err(SignError::RequiredError)?;
411
412    let key_string = key_id.to_string();
413    let signed = config
414        .spawner
415        .spawn_blocking(move || unsigned.sign(key_string, f))
416        .await
417        .map_err(|_| SignError::Canceled)??;
418    Ok(signed)
419}
420
421#[cfg(feature = "middleware")]
422mod middleware {
423    use super::{prepare, Config, Sign, SignError, Spawn};
424    use reqwest::Request;
425    use reqwest_middleware::RequestBuilder;
426    use std::fmt::Display;
427
428    #[async_trait::async_trait]
429    impl Sign for RequestBuilder {
430        async fn authorization_signature<F, E, K, S>(
431            self,
432            config: &Config<S>,
433            key_id: K,
434            f: F,
435        ) -> Result<Request, E>
436        where
437            F: FnOnce(&str) -> Result<String, E> + Send + 'static,
438            E: From<SignError> + From<reqwest::Error> + Send + 'static,
439            K: Display + Send,
440            S: Spawn + Send + Sync,
441        {
442            let mut request = self.build()?;
443            let signed = prepare(&mut request, config, key_id, f).await?;
444
445            let auth_header = signed.authorization_header();
446            request.headers_mut().insert(
447                "Authorization",
448                auth_header.parse().map_err(SignError::NewHeader)?,
449            );
450
451            Ok(request)
452        }
453
454        async fn signature<F, E, K, S>(
455            self,
456            config: &Config<S>,
457            key_id: K,
458            f: F,
459        ) -> Result<Request, E>
460        where
461            F: FnOnce(&str) -> Result<String, E> + Send + 'static,
462            E: From<SignError> + From<reqwest::Error> + Send + 'static,
463            K: Display + Send,
464            S: Spawn + Send + Sync,
465        {
466            let mut request = self.build()?;
467            let signed = prepare(&mut request, config, key_id, f).await?;
468
469            let sig_header = signed.signature_header();
470
471            request.headers_mut().insert(
472                "Signature",
473                sig_header.parse().map_err(SignError::NewHeader)?,
474            );
475
476            Ok(request)
477        }
478    }
479}