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

304 lines
8.6 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 = "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,
/// Whether to set the Date header
set_date: 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<Request, 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<Request, 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,
#[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,
set_date: self.set_date,
}
}
/// 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,
}
}
/// 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,
}
}
/// 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,
}
}
/// 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,
}
}
/// 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,
}
}
}
impl Sign for RequestBuilder {
fn authorization_signature<F, E, K>(
self,
config: &Config,
key_id: K,
f: F,
) -> Result<Request, E>
where
F: FnOnce(&str) -> Result<String, E>,
E: From<SignError> + From<reqwest::Error>,
K: Display,
{
let mut request = self.build()?;
let signed = prepare(&mut 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<F, E, K>(self, config: &Config, key_id: K, f: F) -> Result<Request, E>
where
F: FnOnce(&str) -> Result<String, E>,
E: From<SignError> + From<reqwest::Error>,
K: Display,
{
let mut request = self.build()?;
let signed = prepare(&mut 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<F, E, K>(req: &mut Request, config: &Config, key_id: K, f: F) -> Result<Signed, E>
where
F: FnOnce(&str) -> Result<String, E>,
E: From<SignError>,
K: Display,
{
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 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<F, E, K>(
self,
config: &Config,
key_id: K,
f: F,
) -> Result<Request, E>
where
F: FnOnce(&str) -> Result<String, E>,
E: From<SignError> + From<reqwest::Error>,
K: Display,
{
let mut request = self.build()?;
let signed = prepare(&mut 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<F, E, K>(self, config: &Config, key_id: K, f: F) -> Result<Request, E>
where
F: FnOnce(&str) -> Result<String, E>,
E: From<SignError> + From<reqwest::Error>,
K: Display,
{
let mut request = self.build()?;
let signed = prepare(&mut 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)
}
}
}