457 lines
13 KiB
Rust
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)
|
|
}
|
|
}
|
|
}
|