use http_signature_normalization::create::Signed; use reqwest::{ header::{InvalidHeaderValue, ToStrError}, Request, RequestBuilder, }; use std::{fmt::Display, time::Duration}; 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, } /// 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 where Self: Sized, F: FnOnce(&str) -> Result, E: From + From, K: Display; /// Add a Signature to the request fn signature(self, config: &Config, key_id: K, f: F) -> Result where Self: Sized, F: FnOnce(&str) -> Result, E: From + From, K: Display; } #[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, } } /// 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, } } /// 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, } } /// 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 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, } } /// 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, } } } 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) } } fn prepare(req: &Request, config: &Config, key_id: K, f: F) -> Result where F: FnOnce(&str) -> Result, E: From, K: Display, { 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 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) } } }