asonix
133e081740
Update actix library with new methods Build reqwest client with feature parity to actix client
213 lines
6.3 KiB
Rust
213 lines
6.3 KiB
Rust
use chrono::Duration;
|
|
use http_signature_normalization::create::Signed;
|
|
use reqwest::{
|
|
header::{InvalidHeaderValue, ToStrError},
|
|
Request, RequestBuilder,
|
|
};
|
|
use std::fmt::Display;
|
|
|
|
pub use http_signature_normalization::RequiredError;
|
|
|
|
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<F, E, K>(self, config: &Config, key_id: K, f: F) -> Result<Self, E>
|
|
where
|
|
Self: Sized,
|
|
F: FnOnce(&str) -> Result<String, E>,
|
|
E: From<SignError> + From<reqwest::Error>,
|
|
K: Display;
|
|
|
|
/// Add a Signature to the request
|
|
fn signature<F, E, K>(self, config: &Config, key_id: K, f: F) -> Result<Self, E>
|
|
where
|
|
Self: Sized,
|
|
F: FnOnce(&str) -> Result<String, E>,
|
|
E: From<SignError> + From<reqwest::Error>,
|
|
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,
|
|
}
|
|
|
|
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<F, E, K>(self, config: &Config, key_id: K, f: F) -> Result<Self, E>
|
|
where
|
|
F: FnOnce(&str) -> Result<String, E>,
|
|
E: From<SignError> + From<reqwest::Error>,
|
|
K: Display,
|
|
{
|
|
if let Some(builder) = self.try_clone() {
|
|
let request = builder.build()?;
|
|
let signed = prepare(&request, config, key_id, f)?;
|
|
|
|
let auth_header = signed.authorization_header();
|
|
return Ok(self.header("Authorization", auth_header));
|
|
}
|
|
|
|
Err(SignError::BodyPresent.into())
|
|
}
|
|
|
|
fn signature<F, E, K>(self, config: &Config, key_id: K, f: F) -> Result<Self, E>
|
|
where
|
|
F: FnOnce(&str) -> Result<String, E>,
|
|
E: From<SignError> + From<reqwest::Error>,
|
|
K: Display,
|
|
{
|
|
if let Some(builder) = self.try_clone() {
|
|
let request = builder.build()?;
|
|
let signed = prepare(&request, config, key_id, f)?;
|
|
|
|
let sig_header = signed.signature_header();
|
|
return Ok(self.header("Signature", sig_header));
|
|
}
|
|
|
|
Err(SignError::BodyPresent.into())
|
|
}
|
|
}
|
|
|
|
fn prepare<F, E, K>(req: &Request, config: &Config, key_id: K, f: F) -> Result<Signed, E>
|
|
where
|
|
F: FnOnce(&str) -> Result<String, E>,
|
|
E: From<SignError>,
|
|
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)
|
|
}
|