http-signature-normalization/reqwest/src/lib.rs

457 lines
13 KiB
Rust

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 = "default-spawner")]
pub use crate::default_spawner::DefaultSpawner;
#[cfg(feature = "digest")]
pub use crate::digest::{DigestCreate, SignExt};
}
#[cfg(feature = "default-spawner")]
pub use default_spawner::DefaultSpawner;
#[cfg(feature = "default-spawner")]
#[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<Spawner = DefaultSpawner> {
/// 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,
/// How to spawn blocking tasks
spawner: Spawner,
}
#[cfg(not(feature = "default-spawner"))]
#[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<Spawner> {
/// 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,
/// How to spawn blocking tasks
spawner: Spawner,
}
#[cfg(feature = "default-spawner")]
mod default_spawner {
use super::{Canceled, Config, Spawn};
impl Config<DefaultSpawner> {
/// Create a new config with the default spawner
pub fn new() -> Self {
Default::default()
}
}
/// A default implementation of Spawner for spawning blocking operations
#[derive(Clone, Copy, Debug, Default)]
pub struct DefaultSpawner;
/// The future returned by DefaultSpawner when spawning blocking operations on the tokio
/// blocking threadpool
pub struct DefaultSpawnerFuture<Out> {
inner: tokio::task::JoinHandle<Out>,
}
impl Spawn for DefaultSpawner {
type Future<T> = DefaultSpawnerFuture<T> where T: Send;
fn spawn_blocking<Func, Out>(&self, func: Func) -> Self::Future<Out>
where
Func: FnOnce() -> Out + Send + 'static,
Out: Send + 'static,
{
DefaultSpawnerFuture {
inner: tokio::task::spawn_blocking(func),
}
}
}
impl<Out> std::future::Future for DefaultSpawnerFuture<Out> {
type Output = Result<Out, Canceled>;
fn poll(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
let res = std::task::ready!(std::pin::Pin::new(&mut self.inner).poll(cx));
std::task::Poll::Ready(res.map_err(|_| Canceled))
}
}
}
/// An error that indicates a blocking operation panicked and cannot return a response
#[derive(Debug)]
pub struct Canceled;
impl std::fmt::Display for Canceled {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Operation was canceled")
}
}
impl std::error::Error for Canceled {}
/// A trait dictating how to spawn a future onto a blocking threadpool. By default,
/// http-signature-normalization-actix will use tokio's built-in blocking threadpool, but this
/// can be customized
pub trait Spawn {
/// The future type returned by spawn_blocking
type Future<T>: std::future::Future<Output = Result<T, Canceled>> + Send
where
T: Send;
/// Spawn the blocking function onto the threadpool
fn spawn_blocking<Func, Out>(&self, func: Func) -> Self::Future<Out>
where
Func: FnOnce() -> Out + Send + 'static,
Out: Send + 'static;
}
/// 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<F, E, K, S>(
self,
config: &Config<S>,
key_id: K,
f: F,
) -> Result<Request, E>
where
Self: Sized,
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
E: From<SignError> + From<reqwest::Error> + Send + 'static,
K: Display + Send,
S: Spawn + Send + Sync;
/// Add a Signature to the request
async fn signature<F, E, K, S>(self, config: &Config<S>, key_id: K, f: F) -> Result<Request, E>
where
Self: Sized,
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
E: From<SignError> + From<reqwest::Error> + Send + 'static,
K: Display + Send,
S: Spawn + Send + Sync;
}
#[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<Spawner> Config<Spawner> {
/// Create a new config with the provided spawner
pub fn new_with_spawner(spawner: Spawner) -> Self {
Config {
config: Default::default(),
set_host: Default::default(),
set_date: Default::default(),
spawner,
}
}
/// 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,
spawner: self.spawner,
}
}
/// 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,
spawner: self.spawner,
}
}
/// 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,
spawner: self.spawner,
}
}
/// 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,
spawner: self.spawner,
}
}
/// 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,
spawner: self.spawner,
}
}
/// 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,
spawner: self.spawner,
}
}
pub fn set_spawner<NewSpawner: Spawn>(self, spawner: NewSpawner) -> Config<NewSpawner> {
Config {
config: self.config,
set_host: self.set_host,
set_date: self.set_date,
spawner,
}
}
}
#[async_trait::async_trait]
impl Sign for RequestBuilder {
async fn authorization_signature<F, E, K, S>(
self,
config: &Config<S>,
key_id: K,
f: F,
) -> Result<Request, E>
where
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
E: From<SignError> + From<reqwest::Error> + Send + 'static,
K: Display + Send,
S: Spawn + Send + Sync,
{
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<F, E, K, S>(self, config: &Config<S>, key_id: K, f: F) -> Result<Request, E>
where
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
E: From<SignError> + From<reqwest::Error> + Send + 'static,
K: Display + Send,
S: Spawn + Send + Sync,
{
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<F, E, K, S>(
req: &mut Request,
config: &Config<S>,
key_id: K,
f: F,
) -> Result<Signed, E>
where
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
E: From<SignError> + Send + 'static,
K: Display + Send,
S: Spawn,
{
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 = config
.spawner
.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, Spawn};
use reqwest::Request;
use reqwest_middleware::RequestBuilder;
use std::fmt::Display;
#[async_trait::async_trait]
impl Sign for RequestBuilder {
async fn authorization_signature<F, E, K, S>(
self,
config: &Config<S>,
key_id: K,
f: F,
) -> Result<Request, E>
where
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
E: From<SignError> + From<reqwest::Error> + Send + 'static,
K: Display + Send,
S: Spawn + Send + Sync,
{
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<F, E, K, S>(
self,
config: &Config<S>,
key_id: K,
f: F,
) -> Result<Request, E>
where
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
E: From<SignError> + From<reqwest::Error> + Send + 'static,
K: Display + Send,
S: Spawn + Send + Sync,
{
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)
}
}
}