diff --git a/http-signature-normalization-reqwest/Cargo.toml b/http-signature-normalization-reqwest/Cargo.toml index ee06492..d7bd210 100644 --- a/http-signature-normalization-reqwest/Cargo.toml +++ b/http-signature-normalization-reqwest/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "http-signature-normalization-reqwest" description = "An HTTP Signatures library that leaves the signing to you" -version = "0.2.1" +version = "0.3.0" authors = ["asonix "] license-file = "LICENSE" readme = "README.md" @@ -12,6 +12,7 @@ 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"] @@ -28,12 +29,13 @@ chrono = "0.4.10" http = "0.2.0" http-signature-normalization = { version = "0.5.1", path = ".." } reqwest = { version = "0.11", default-features = false, features = ["json"] } +reqwest-middleware = { version = "0.1.2", optional = true } sha2 = { version = "0.9", optional = true } sha3 = { version = "0.9", optional = true } thiserror = "1.0" tokio = { version = "1", default-features = false, features = ["rt"], optional = true } [dev-dependencies] +httpdate = "1.0.2" pretty_env_logger = "0.4" tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "macros"] } -time = "0.2" diff --git a/http-signature-normalization-reqwest/examples/client.rs b/http-signature-normalization-reqwest/examples/client.rs index 5d6499e..79f92fd 100644 --- a/http-signature-normalization-reqwest/examples/client.rs +++ b/http-signature-normalization-reqwest/examples/client.rs @@ -1,26 +1,27 @@ +use std::time::SystemTime; + use http_signature_normalization_reqwest::prelude::*; +use httpdate::HttpDate; use reqwest::{header::DATE, Client}; use sha2::{Digest, Sha256}; -use time::OffsetDateTime; async fn request(config: Config) -> Result<(), Box> { let digest = Sha256::new(); - let response = Client::new() + let client = Client::new(); + + let request = client .post("http://127.0.0.1:8010/") .header("User-Agent", "Reqwest") .header("Accept", "text/plain") - .header( - DATE, - OffsetDateTime::now_utc().format("%a, %d %b %Y %H:%M:%S GMT"), - ) + .header(DATE, HttpDate::from(SystemTime::now()).to_string()) .signature_with_digest(config, "my-key-id", digest, "Hewwo-owo", |s| { println!("Signing String\n{}", s); Ok(base64::encode(s)) as Result<_, MyError> }) .await?; - let body = response.bytes().await.map_err(MyError::Body)?; + let body = client.execute(request).await?.bytes().await?; println!("{:?}", body); Ok(()) diff --git a/http-signature-normalization-reqwest/src/digest/mod.rs b/http-signature-normalization-reqwest/src/digest/mod.rs index a650efb..7a08f4b 100644 --- a/http-signature-normalization-reqwest/src/digest/mod.rs +++ b/http-signature-normalization-reqwest/src/digest/mod.rs @@ -1,5 +1,5 @@ use crate::{Config, Sign, SignError}; -use reqwest::{Body, RequestBuilder}; +use reqwest::{Body, Request, RequestBuilder}; use std::{fmt::Display, future::Future, pin::Pin}; #[cfg(feature = "sha-2")] @@ -29,7 +29,7 @@ pub trait SignExt: Sign { digest: D, v: V, f: F, - ) -> Pin>>> + ) -> Pin>>> where F: FnOnce(&str) -> Result + Send + 'static, E: From + From, @@ -45,7 +45,7 @@ pub trait SignExt: Sign { digest: D, v: V, f: F, - ) -> Pin>>> + ) -> Pin>>> where F: FnOnce(&str) -> Result + Send + 'static, E: From + From, @@ -63,7 +63,7 @@ impl SignExt for RequestBuilder { mut digest: D, v: V, f: F, - ) -> Pin>>> + ) -> Pin>>> where F: FnOnce(&str) -> Result + Send + 'static, E: From + From, @@ -80,11 +80,13 @@ impl SignExt for RequestBuilder { .await .map_err(|_| SignError::Canceled)?; - let c = self + let mut req = self .header("Digest", format!("{}={}", D::NAME, digest)) .authorization_signature(&config, key_id, f)?; - c.body(v).send().await.map_err(E::from) + *req.body_mut() = Some(Body::from(v.as_ref().to_vec())); + + Ok(req) }) } @@ -95,7 +97,7 @@ impl SignExt for RequestBuilder { mut digest: D, v: V, f: F, - ) -> Pin>>> + ) -> Pin>>> where F: FnOnce(&str) -> Result + Send + 'static, E: From + From, @@ -112,11 +114,91 @@ impl SignExt for RequestBuilder { .await .map_err(|_| SignError::Canceled)?; - let c = self + let mut req = self .header("Digest", format!("{}={}", D::NAME, digest)) .signature(&config, key_id, f)?; - c.body(v).send().await.map_err(E::from) + *req.body_mut() = Some(Body::from(v.as_ref().to_vec())); + + Ok(req) }) } } + +#[cfg(feature = "middleware")] +mod middleware { + use super::{Config, DigestCreate, Sign, SignError, SignExt}; + use reqwest::{Body, Request}; + use reqwest_middleware::RequestBuilder; + use std::{fmt::Display, future::Future, pin::Pin}; + + impl SignExt for RequestBuilder { + fn authorization_signature_with_digest( + self, + config: Config, + key_id: K, + mut digest: D, + v: V, + f: F, + ) -> Pin>>> + where + F: FnOnce(&str) -> Result + Send + 'static, + E: From + From, + K: Display + 'static, + D: DigestCreate + Send + 'static, + V: AsRef<[u8]> + Into + Send + 'static, + Self: Sized, + { + Box::pin(async move { + let (v, digest) = tokio::task::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)) + .authorization_signature(&config, key_id, f)?; + + *req.body_mut() = Some(Body::from(v.as_ref().to_vec())); + + Ok(req) + }) + } + + fn signature_with_digest( + self, + config: Config, + key_id: K, + mut digest: D, + v: V, + f: F, + ) -> Pin>>> + where + F: FnOnce(&str) -> Result + Send + 'static, + E: From + From, + K: Display + 'static, + D: DigestCreate + Send + 'static, + V: AsRef<[u8]> + Into + Send + 'static, + Self: Sized, + { + Box::pin(async move { + let (v, digest) = tokio::task::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)) + .signature(&config, key_id, f)?; + + *req.body_mut() = Some(Body::from(v.as_ref().to_vec())); + + Ok(req) + }) + } + } +} diff --git a/http-signature-normalization-reqwest/src/lib.rs b/http-signature-normalization-reqwest/src/lib.rs index 80819cd..81e4816 100644 --- a/http-signature-normalization-reqwest/src/lib.rs +++ b/http-signature-normalization-reqwest/src/lib.rs @@ -34,7 +34,12 @@ pub struct Config { /// A trait implemented by the reqwest RequestBuilder type to add an HTTP Signature to the request pub trait Sign { /// Add an Authorization Signature to the request - fn authorization_signature(self, config: &Config, key_id: K, f: F) -> Result + fn authorization_signature( + self, + config: &Config, + key_id: K, + f: F, + ) -> Result where Self: Sized, F: FnOnce(&str) -> Result, @@ -42,7 +47,7 @@ pub trait Sign { K: Display; /// Add a Signature to the request - fn signature(self, config: &Config, key_id: K, f: F) -> Result + fn signature(self, config: &Config, key_id: K, f: F) -> Result where Self: Sized, F: FnOnce(&str) -> Result, @@ -139,38 +144,46 @@ impl Config { } impl Sign for RequestBuilder { - fn authorization_signature(self, config: &Config, key_id: K, f: F) -> Result + fn authorization_signature( + self, + config: &Config, + key_id: K, + f: F, + ) -> Result where F: FnOnce(&str) -> Result, E: From + From, K: Display, { - if let Some(builder) = self.try_clone() { - let request = builder.build()?; - let signed = prepare(&request, config, key_id, f)?; + let mut request = self.build()?; + let signed = prepare(&request, config, key_id, f)?; - let auth_header = signed.authorization_header(); - return Ok(self.header("Authorization", auth_header)); - } + let auth_header = signed.authorization_header(); + request.headers_mut().insert( + "Authorization", + auth_header.parse().map_err(SignError::NewHeader)?, + ); - Err(SignError::BodyPresent.into()) + Ok(request) } - fn signature(self, config: &Config, key_id: K, f: F) -> Result + fn signature(self, config: &Config, key_id: K, f: F) -> Result where F: FnOnce(&str) -> Result, E: From + From, K: Display, { - if let Some(builder) = self.try_clone() { - let request = builder.build()?; - let signed = prepare(&request, config, key_id, f)?; + let mut request = self.build()?; + let signed = prepare(&request, config, key_id, f)?; - let sig_header = signed.signature_header(); - return Ok(self.header("Signature", sig_header)); - } + let sig_header = signed.signature_header(); - Err(SignError::BodyPresent.into()) + request.headers_mut().insert( + "Signature", + sig_header.parse().map_err(SignError::NewHeader)?, + ); + + Ok(request) } } @@ -214,3 +227,55 @@ where let signed = unsigned.sign(key_id.to_string(), f)?; Ok(signed) } + +#[cfg(feature = "middleware")] +mod middleware { + use super::{prepare, Config, Sign, SignError}; + use reqwest::Request; + use reqwest_middleware::RequestBuilder; + use std::fmt::Display; + + impl Sign for RequestBuilder { + fn authorization_signature( + self, + config: &Config, + key_id: K, + f: F, + ) -> Result + where + F: FnOnce(&str) -> Result, + E: From + From, + K: Display, + { + let mut request = self.build()?; + let signed = prepare(&request, config, key_id, f)?; + + let auth_header = signed.authorization_header(); + request.headers_mut().insert( + "Authorization", + auth_header.parse().map_err(SignError::NewHeader)?, + ); + + Ok(request) + } + + fn signature(self, config: &Config, key_id: K, f: F) -> Result + where + F: FnOnce(&str) -> Result, + E: From + From, + K: Display, + { + let mut request = self.build()?; + let signed = prepare(&request, config, key_id, f)?; + + let sig_header = signed.signature_header(); + + request.headers_mut().insert( + "Signature", + sig_header.parse().map_err(SignError::NewHeader)?, + ); + + Ok(request) + } + } +}