diff --git a/reqwest/Cargo.toml b/reqwest/Cargo.toml index 75e1521..4ff9497 100644 --- a/reqwest/Cargo.toml +++ b/reqwest/Cargo.toml @@ -11,15 +11,16 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["sha-2", "sha-3"] -middleware = ["reqwest-middleware"] -digest = ["base64", "tokio"] -sha-2 = ["digest", "sha2"] -sha-3 = ["digest", "sha3"] +default = ["sha-2", "default-spawner"] +middleware = ["dep:reqwest-middleware"] +default-spawner = ["dep:tokio"] +digest = ["dep:base64", "dep:tokio"] +sha-2 = ["digest", "dep:sha2"] +sha-3 = ["digest", "dep:sha3"] [[example]] name = "client" -required-features = ["sha-2"] +required-features = ["default-spawner", "sha-2"] [dependencies] async-trait = "0.1.71" diff --git a/reqwest/src/digest/mod.rs b/reqwest/src/digest/mod.rs index 0957253..4342d05 100644 --- a/reqwest/src/digest/mod.rs +++ b/reqwest/src/digest/mod.rs @@ -1,4 +1,4 @@ -use crate::{Config, Sign, SignError}; +use crate::{Config, Sign, SignError, Spawn}; use reqwest::{Body, Request, RequestBuilder}; use std::fmt::Display; @@ -23,9 +23,9 @@ pub trait DigestCreate { /// a malicious entity #[async_trait::async_trait] pub trait SignExt: Sign { - async fn authorization_signature_with_digest( + async fn authorization_signature_with_digest( self, - config: Config, + config: Config, key_id: K, digest: D, v: V, @@ -37,11 +37,12 @@ pub trait SignExt: Sign { K: Display + Send + 'static, D: DigestCreate + Send + 'static, V: AsRef<[u8]> + Into + Send + 'static, + S: Spawn + Send + Sync, Self: Sized; - async fn signature_with_digest( + async fn signature_with_digest( self, - config: Config, + config: Config, key_id: K, digest: D, v: V, @@ -53,14 +54,15 @@ pub trait SignExt: Sign { K: Display + Send + 'static, D: DigestCreate + Send + 'static, V: AsRef<[u8]> + Into + Send + 'static, + S: Spawn + Send + Sync, Self: Sized; } #[async_trait::async_trait] impl SignExt for RequestBuilder { - async fn authorization_signature_with_digest( + async fn authorization_signature_with_digest( self, - config: Config, + config: Config, key_id: K, mut digest: D, v: V, @@ -72,13 +74,16 @@ impl SignExt for RequestBuilder { K: Display + Send + 'static, D: DigestCreate + Send + 'static, V: AsRef<[u8]> + Into + Send + 'static, + S: Spawn + Send + Sync, { - let (v, digest) = tokio::task::spawn_blocking(move || { - let digest = digest.compute(v.as_ref()); - (v, digest) - }) - .await - .map_err(|_| SignError::Canceled)?; + let (v, digest) = config + .spawner + .spawn_blocking(move || { + let digest = digest.compute(v.as_ref()); + (v, digest) + }) + .await + .map_err(|_| SignError::Canceled)?; let mut req = self .header("Digest", format!("{}={}", D::NAME, digest)) @@ -90,9 +95,9 @@ impl SignExt for RequestBuilder { Ok(req) } - async fn signature_with_digest( + async fn signature_with_digest( self, - config: Config, + config: Config, key_id: K, mut digest: D, v: V, @@ -104,13 +109,16 @@ impl SignExt for RequestBuilder { K: Display + Send + 'static, D: DigestCreate + Send + 'static, V: AsRef<[u8]> + Into + Send + 'static, + S: Spawn + Send + Sync, { - let (v, digest) = tokio::task::spawn_blocking(move || { - let digest = digest.compute(v.as_ref()); - (v, digest) - }) - .await - .map_err(|_| SignError::Canceled)?; + let (v, digest) = config + .spawner + .spawn_blocking(move || { + let digest = digest.compute(v.as_ref()); + (v, digest) + }) + .await + .map_err(|_| SignError::Canceled)?; let mut req = self .header("Digest", format!("{}={}", D::NAME, digest)) @@ -125,16 +133,16 @@ impl SignExt for RequestBuilder { #[cfg(feature = "middleware")] mod middleware { - use super::{Config, DigestCreate, Sign, SignError, SignExt}; + use super::{Config, DigestCreate, Sign, SignError, SignExt, Spawn}; use reqwest::{Body, Request}; use reqwest_middleware::RequestBuilder; use std::fmt::Display; #[async_trait::async_trait] impl SignExt for RequestBuilder { - async fn authorization_signature_with_digest( + async fn authorization_signature_with_digest( self, - config: Config, + config: Config, key_id: K, mut digest: D, v: V, @@ -146,14 +154,17 @@ mod middleware { K: Display + Send + 'static, D: DigestCreate + Send + 'static, V: AsRef<[u8]> + Into + Send + 'static, + S: Spawn + Send + Sync, Self: Sized, { - let (v, digest) = tokio::task::spawn_blocking(move || { - let digest = digest.compute(v.as_ref()); - (v, digest) - }) - .await - .map_err(|_| SignError::Canceled)?; + let (v, digest) = config + .spawner + .spawn_blocking(move || { + let digest = digest.compute(v.as_ref()); + (v, digest) + }) + .await + .map_err(|_| SignError::Canceled)?; let mut req = self .header("Digest", format!("{}={}", D::NAME, digest)) @@ -165,9 +176,9 @@ mod middleware { Ok(req) } - async fn signature_with_digest( + async fn signature_with_digest( self, - config: Config, + config: Config, key_id: K, mut digest: D, v: V, @@ -179,14 +190,17 @@ mod middleware { K: Display + Send + 'static, D: DigestCreate + Send + 'static, V: AsRef<[u8]> + Into + Send + 'static, + S: Spawn + Send + Sync, Self: Sized, { - let (v, digest) = tokio::task::spawn_blocking(move || { - let digest = digest.compute(v.as_ref()); - (v, digest) - }) - .await - .map_err(|_| SignError::Canceled)?; + let (v, digest) = config + .spawner + .spawn_blocking(move || { + let digest = digest.compute(v.as_ref()); + (v, digest) + }) + .await + .map_err(|_| SignError::Canceled)?; let mut req = self .header("Digest", format!("{}={}", D::NAME, digest)) diff --git a/reqwest/src/lib.rs b/reqwest/src/lib.rs index 9b23c33..0a2bfe5 100644 --- a/reqwest/src/lib.rs +++ b/reqwest/src/lib.rs @@ -18,16 +18,23 @@ pub mod digest; pub mod prelude { pub use crate::{Config, Sign, SignError}; + #[cfg(feature = "default-spawner")] + pub use crate::default_spawner::DefaultSpawner; + #[cfg(feature = "digest")] pub use crate::digest::{DigestCreate, SignExt}; } +#[cfg(feature = "default-spawner")] +pub use default_spawner::DefaultSpawner; + +#[cfg(feature = "default-spawner")] #[derive(Clone, Debug, Default)] /// Configuration for signing and verifying signatures /// /// By default, the config is set up to create and verify signatures that expire after 10 seconds, /// and use the `(created)` and `(expires)` fields that were introduced in draft 11 -pub struct Config { +pub struct Config { /// The inner config type config: http_signature_normalization::Config, @@ -36,15 +43,115 @@ pub struct Config { /// Whether to set the Date header set_date: bool, + + /// How to spawn blocking tasks + spawner: Spawner, +} + +#[cfg(not(feature = "default-spawner"))] +#[derive(Clone, Debug, Default)] +/// Configuration for signing and verifying signatures +/// +/// By default, the config is set up to create and verify signatures that expire after 10 seconds, +/// and use the `(created)` and `(expires)` fields that were introduced in draft 11 +pub struct Config { + /// The inner config type + config: http_signature_normalization::Config, + + /// Whether to set the Host header + set_host: bool, + + /// Whether to set the Date header + set_date: bool, + + /// How to spawn blocking tasks + spawner: Spawner, +} + +#[cfg(feature = "default-spawner")] +mod default_spawner { + use super::{Canceled, Config, Spawn}; + + impl Config { + /// Create a new config with the default spawner + pub fn new() -> Self { + Default::default() + } + } + + /// A default implementation of Spawner for spawning blocking operations + #[derive(Clone, Copy, Debug, Default)] + pub struct DefaultSpawner; + + /// The future returned by DefaultSpawner when spawning blocking operations on the tokio + /// blocking threadpool + pub struct DefaultSpawnerFuture { + inner: tokio::task::JoinHandle, + } + + impl Spawn for DefaultSpawner { + type Future = DefaultSpawnerFuture where T: Send; + + fn spawn_blocking(&self, func: Func) -> Self::Future + where + Func: FnOnce() -> Out + Send + 'static, + Out: Send + 'static, + { + DefaultSpawnerFuture { + inner: tokio::task::spawn_blocking(func), + } + } + } + + impl std::future::Future for DefaultSpawnerFuture { + type Output = Result; + + fn poll( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + let res = std::task::ready!(std::pin::Pin::new(&mut self.inner).poll(cx)); + + std::task::Poll::Ready(res.map_err(|_| Canceled)) + } + } +} + +/// An error that indicates a blocking operation panicked and cannot return a response +#[derive(Debug)] +pub struct Canceled; + +impl std::fmt::Display for Canceled { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Operation was canceled") + } +} + +impl std::error::Error for Canceled {} + +/// A trait dictating how to spawn a future onto a blocking threadpool. By default, +/// http-signature-normalization-actix will use tokio's built-in blocking threadpool, but this +/// can be customized +pub trait Spawn { + /// The future type returned by spawn_blocking + type Future: std::future::Future> + Send + where + T: Send; + + /// Spawn the blocking function onto the threadpool + fn spawn_blocking(&self, func: Func) -> Self::Future + where + Func: FnOnce() -> Out + Send + 'static, + Out: Send + 'static; } /// A trait implemented by the reqwest RequestBuilder type to add an HTTP Signature to the request #[async_trait::async_trait] pub trait Sign { /// Add an Authorization Signature to the request - async fn authorization_signature( + async fn authorization_signature( self, - config: &Config, + config: &Config, key_id: K, f: F, ) -> Result @@ -52,15 +159,17 @@ pub trait Sign { Self: Sized, F: FnOnce(&str) -> Result + Send + 'static, E: From + From + Send + 'static, - K: Display + Send; + K: Display + Send, + S: Spawn + Send + Sync; /// Add a Signature to the request - async fn signature(self, config: &Config, key_id: K, f: F) -> Result + async fn signature(self, config: &Config, key_id: K, f: F) -> Result where Self: Sized, F: FnOnce(&str) -> Result + Send + 'static, E: From + From + Send + 'static, - K: Display + Send; + K: Display + Send, + S: Spawn + Send + Sync; } #[derive(Debug, thiserror::Error)] @@ -88,9 +197,15 @@ pub enum SignError { Canceled, } -impl Config { - pub fn new() -> Self { - Default::default() +impl Config { + /// Create a new config with the provided spawner + pub fn new_with_spawner(spawner: Spawner) -> Self { + Config { + config: Default::default(), + set_host: Default::default(), + set_date: Default::default(), + spawner, + } } /// This method can be used to include the Host header in the HTTP Signature without @@ -100,6 +215,7 @@ impl Config { config: self.config, set_host: true, set_date: self.set_date, + spawner: self.spawner, } } @@ -112,6 +228,7 @@ impl Config { config: self.config.mastodon_compat(), set_host: true, set_date: true, + spawner: self.spawner, } } @@ -123,6 +240,7 @@ impl Config { config: self.config.require_digest(), set_host: self.set_host, set_date: self.set_date, + spawner: self.spawner, } } @@ -135,6 +253,7 @@ impl Config { config: self.config.dont_use_created_field(), set_host: self.set_host, set_date: self.set_date, + spawner: self.spawner, } } @@ -144,6 +263,7 @@ impl Config { config: self.config.set_expiration(expiries_after), set_host: self.set_host, set_date: self.set_date, + spawner: self.spawner, } } @@ -153,15 +273,25 @@ impl Config { config: self.config.require_header(header), set_host: self.set_host, set_date: self.set_date, + spawner: self.spawner, + } + } + + pub fn set_spawner(self, spawner: NewSpawner) -> Config { + Config { + config: self.config, + set_host: self.set_host, + set_date: self.set_date, + spawner, } } } #[async_trait::async_trait] impl Sign for RequestBuilder { - async fn authorization_signature( + async fn authorization_signature( self, - config: &Config, + config: &Config, key_id: K, f: F, ) -> Result @@ -169,6 +299,7 @@ impl Sign for RequestBuilder { F: FnOnce(&str) -> Result + Send + 'static, E: From + From + Send + 'static, K: Display + Send, + S: Spawn + Send + Sync, { let mut request = self.build()?; let signed = prepare(&mut request, config, key_id, f).await?; @@ -182,11 +313,12 @@ impl Sign for RequestBuilder { Ok(request) } - async fn signature(self, config: &Config, key_id: K, f: F) -> Result + async fn signature(self, config: &Config, key_id: K, f: F) -> Result where F: FnOnce(&str) -> Result + Send + 'static, E: From + From + Send + 'static, K: Display + Send, + S: Spawn + Send + Sync, { let mut request = self.build()?; let signed = prepare(&mut request, config, key_id, f).await?; @@ -202,11 +334,17 @@ impl Sign for RequestBuilder { } } -async fn prepare(req: &mut Request, config: &Config, key_id: K, f: F) -> Result +async fn prepare( + req: &mut Request, + config: &Config, + key_id: K, + f: F, +) -> Result where F: FnOnce(&str) -> Result + Send + 'static, E: From + Send + 'static, K: Display + Send, + S: Spawn, { if config.set_date && !req.headers().contains_key("date") { req.headers_mut().insert( @@ -249,7 +387,9 @@ where .map_err(SignError::from)?; let key_string = key_id.to_string(); - let signed = tokio::task::spawn_blocking(move || unsigned.sign(key_string, f)) + let signed = config + .spawner + .spawn_blocking(move || unsigned.sign(key_string, f)) .await .map_err(|_| SignError::Canceled)??; Ok(signed) @@ -257,16 +397,16 @@ where #[cfg(feature = "middleware")] mod middleware { - use super::{prepare, Config, Sign, SignError}; + use super::{prepare, Config, Sign, SignError, Spawn}; use reqwest::Request; use reqwest_middleware::RequestBuilder; use std::fmt::Display; #[async_trait::async_trait] impl Sign for RequestBuilder { - async fn authorization_signature( + async fn authorization_signature( self, - config: &Config, + config: &Config, key_id: K, f: F, ) -> Result @@ -274,6 +414,7 @@ mod middleware { F: FnOnce(&str) -> Result + Send + 'static, E: From + From + Send + 'static, K: Display + Send, + S: Spawn + Send + Sync, { let mut request = self.build()?; let signed = prepare(&mut request, config, key_id, f).await?; @@ -287,11 +428,17 @@ mod middleware { Ok(request) } - async fn signature(self, config: &Config, key_id: K, f: F) -> Result + async fn signature( + self, + config: &Config, + key_id: K, + f: F, + ) -> Result where F: FnOnce(&str) -> Result + Send + 'static, E: From + From + Send + 'static, K: Display + Send, + S: Spawn + Send + Sync, { let mut request = self.build()?; let signed = prepare(&mut request, config, key_id, f).await?;