http-signature-normalization/http-signature-normalization-reqwest/src/lib.rs
asonix 133e081740 Add methods for explicit mastodon compat, requiring digest
Update actix library with new methods
Build reqwest client with feature parity to actix client
2020-09-29 18:58:06 -05:00

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)
}