use http_signature_normalization::create::Signed; use httpdate::HttpDate; use reqwest::{ header::{InvalidHeaderValue, ToStrError}, Request, RequestBuilder, }; use std::{ convert::TryInto, fmt::Display, time::{Duration, SystemTime}, }; pub use http_signature_normalization::RequiredError; #[cfg(feature = "digest")] pub mod digest; pub mod prelude { pub use crate::{Config, Sign, SignError}; #[cfg(feature = "digest")] pub use crate::digest::{DigestCreate, SignExt}; } #[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, } /// 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( 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; /// Add a Signature to the request 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; } #[derive(Debug, thiserror::Error)] pub enum SignError { #[error("Failed to read header, {0}")] /// An error occurred when reading the request's headers Header(#[from] ToStrError), #[error("Failed to write header, {0}")] /// An error occured when adding a new header NewHeader(#[from] InvalidHeaderValue), #[error("{0}")] /// Some headers were marked as required, but are missing RequiredError(#[from] RequiredError), #[error("No host provided for URL, {0}")] /// Missing host Host(String), #[error("Cannot sign request with body already present")] BodyPresent, #[error("Panic in spawn blocking")] Canceled, } impl Config { pub fn new() -> Self { Default::default() } /// This method can be used to include the Host header in the HTTP Signature without /// interfering with Reqwest's built-in Host mechanisms pub fn set_host_header(self) -> Self { Config { config: self.config, set_host: true, set_date: self.set_date, } } /// Enable mastodon compatibility /// /// This is the same as disabling the use of `(created)` and `(expires)` signature fields, /// requiring the Date header, and requiring the Host header pub fn mastodon_compat(self) -> Self { Config { config: self.config.mastodon_compat(), set_host: true, set_date: true, } } /// Require the Digest header be set /// /// This is useful for POST, PUT, and PATCH requests, but doesn't make sense for GET or DELETE. pub fn require_digest(self) -> Self { Config { config: self.config.require_digest(), set_host: self.set_host, set_date: self.set_date, } } /// Opt out of using the (created) and (expires) fields introduced in draft 11 /// /// Note that by enabling this, the Date header becomes required on requests. This is to /// prevent replay attacks pub fn dont_use_created_field(self) -> Self { Config { config: self.config.dont_use_created_field(), set_host: self.set_host, set_date: self.set_date, } } /// Set the expiration to a custom duration pub fn set_expiration(self, expiries_after: Duration) -> Self { Config { config: self.config.set_expiration(expiries_after), set_host: self.set_host, set_date: self.set_date, } } /// Require a header on signed requests pub fn require_header(self, header: &str) -> Self { Config { config: self.config.require_header(header), set_host: self.set_host, set_date: self.set_date, } } } #[async_trait::async_trait] impl Sign for RequestBuilder { async fn authorization_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, { let mut request = self.build()?; let signed = prepare(&mut request, config, key_id, f).await?; let auth_header = signed.authorization_header(); request.headers_mut().insert( "Authorization", auth_header.parse().map_err(SignError::NewHeader)?, ); Ok(request) } 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, { let mut request = self.build()?; let signed = prepare(&mut request, config, key_id, f).await?; let sig_header = signed.signature_header(); request.headers_mut().insert( "Signature", sig_header.parse().map_err(SignError::NewHeader)?, ); Ok(request) } } 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, { if config.set_date && !req.headers().contains_key("date") { req.headers_mut().insert( "date", HttpDate::from(SystemTime::now()) .to_string() .try_into() .map_err(SignError::from)?, ); } let mut bt = std::collections::BTreeMap::new(); for (k, v) in req.headers().iter() { bt.insert( k.as_str().to_owned(), v.to_str().map_err(SignError::from)?.to_owned(), ); } if config.set_host { let header_string = req .url() .host() .ok_or_else(|| SignError::Host(req.url().to_string()))? .to_string(); let header_string = match req.url().port() { None | Some(443) | Some(80) => header_string, Some(port) => format!("{}:{}", header_string, port), }; bt.insert("Host".to_string(), header_string); } let path_and_query = if let Some(query) = req.url().query() { format!("{}?{}", req.url().path(), query) } else { req.url().path().to_string() }; let unsigned = config .config .begin_sign(req.method().as_str(), &path_and_query, bt) .map_err(SignError::from)?; let key_string = key_id.to_string(); let signed = tokio::task::spawn_blocking(move || unsigned.sign(key_string, f)) .await .map_err(|_| SignError::Canceled)??; Ok(signed) } #[cfg(feature = "middleware")] mod middleware { use super::{prepare, Config, Sign, SignError}; use reqwest::Request; use reqwest_middleware::RequestBuilder; use std::fmt::Display; #[async_trait::async_trait] impl Sign for RequestBuilder { async fn authorization_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, { let mut request = self.build()?; let signed = prepare(&mut request, config, key_id, f).await?; let auth_header = signed.authorization_header(); request.headers_mut().insert( "Authorization", auth_header.parse().map_err(SignError::NewHeader)?, ); Ok(request) } 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, { let mut request = self.build()?; let signed = prepare(&mut request, config, key_id, f).await?; let sig_header = signed.signature_header(); request.headers_mut().insert( "Signature", sig_header.parse().map_err(SignError::NewHeader)?, ); Ok(request) } } }