mirror of
https://git.asonix.dog/asonix/http-signature-normalization.git
synced 2024-11-22 01:11:00 +00:00
Add methods for explicit mastodon compat, requiring digest
Update actix library with new methods Build reqwest client with feature parity to actix client
This commit is contained in:
parent
baceb6fa50
commit
133e081740
11 changed files with 498 additions and 55 deletions
|
@ -1,7 +1,7 @@
|
||||||
[package]
|
[package]
|
||||||
name = "http-signature-normalization"
|
name = "http-signature-normalization"
|
||||||
description = "An HTTP Signatures library that leaves the signing to you"
|
description = "An HTTP Signatures library that leaves the signing to you"
|
||||||
version = "0.5.2"
|
version = "0.5.3"
|
||||||
authors = ["asonix <asonix@asonix.dog>"]
|
authors = ["asonix <asonix@asonix.dog>"]
|
||||||
license-file = "LICENSE"
|
license-file = "LICENSE"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
|
@ -37,10 +37,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
std::env::set_var("RUST_LOG", "info");
|
std::env::set_var("RUST_LOG", "info");
|
||||||
pretty_env_logger::init();
|
pretty_env_logger::init();
|
||||||
|
|
||||||
let config = Config::default().require_header("accept").set_host_header();
|
let config = Config::default().require_header("accept").require_digest();
|
||||||
|
|
||||||
request(config.clone()).await?;
|
request(config.clone()).await?;
|
||||||
request(config.dont_use_created_field()).await?;
|
request(config.mastodon_compat()).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
std::env::set_var("RUST_LOG", "info");
|
std::env::set_var("RUST_LOG", "info");
|
||||||
pretty_env_logger::init();
|
pretty_env_logger::init();
|
||||||
|
|
||||||
let config = Config::default().require_header("accept");
|
let config = Config::default().require_header("accept").require_digest();
|
||||||
|
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
App::new()
|
App::new()
|
||||||
|
|
|
@ -359,9 +359,28 @@ impl Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opt out of using the (created) and (expires) fields introduced in draft 11
|
/// Enable mastodon compatibility
|
||||||
///
|
///
|
||||||
/// Use this for compatibility with mastodon
|
/// 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
|
/// Note that by enabling this, the Date header becomes required on requests. This is to
|
||||||
/// prevent replay attacks
|
/// prevent replay attacks
|
||||||
|
|
|
@ -6,20 +6,25 @@ edition = "2018"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
[features]
|
[features]
|
||||||
default = ["sha-2"]
|
default = ["sha-2", "sha-3"]
|
||||||
digest = ["base64", "serde", "serde_json", "serde_urlencoded", "thiserror"]
|
digest = ["base64", "tokio"]
|
||||||
sha-2 = ["digest", "sha2"]
|
sha-2 = ["digest", "sha2"]
|
||||||
|
sha-3 = ["digest", "sha3"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
base64 = { version = "0.11.0", optional = true }
|
base64 = { version = "0.12", optional = true }
|
||||||
bytes = "0.5.3"
|
bytes = "0.5.3"
|
||||||
futures = "0.3.1"
|
futures = "0.3.1"
|
||||||
chrono = "0.4.10"
|
chrono = "0.4.10"
|
||||||
http = "0.2.0"
|
http = "0.2.0"
|
||||||
http-signature-normalization = { version = "0.5.0", path = ".." }
|
http-signature-normalization = { version = "0.5.0", path = ".." }
|
||||||
reqwest = "0.10.1"
|
reqwest = "0.10.8"
|
||||||
serde = { version = "1.0.104", features = ["derive"], optional = true }
|
sha2 = { version = "0.9", optional = true }
|
||||||
serde_json = { version = "1.0.44", optional = true }
|
sha3 = { version = "0.9", optional = true }
|
||||||
serde_urlencoded = { version = "0.6.1", optional = true }
|
thiserror = "1.0"
|
||||||
sha2 = { version = "0.8.1", optional = true }
|
tokio = { version = "0.2", default-features = false, features = ["rt-threaded", "blocking"], optional = true }
|
||||||
thiserror = { version = "1.0.9", optional = true }
|
|
||||||
|
[dev-dependencies]
|
||||||
|
pretty_env_logger = "0.4"
|
||||||
|
tokio = { version = "0.2", default-features = false, features = ["rt-threaded", "blocking", "macros"] }
|
||||||
|
time = "0.2"
|
||||||
|
|
51
http-signature-normalization-reqwest/examples/client.rs
Normal file
51
http-signature-normalization-reqwest/examples/client.rs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
use http_signature_normalization_reqwest::prelude::*;
|
||||||
|
use reqwest::{header::DATE, Client};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
async fn request(config: Config) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let digest = Sha256::new();
|
||||||
|
|
||||||
|
let response = Client::new()
|
||||||
|
.post("http://127.0.0.1:8010/")
|
||||||
|
.header("User-Agent", "Reqwest")
|
||||||
|
.header("Accept", "text/plain")
|
||||||
|
.header(
|
||||||
|
DATE,
|
||||||
|
OffsetDateTime::now_utc().format("%a, %d %b %Y %H:%M:%S GMT"),
|
||||||
|
)
|
||||||
|
.signature_with_digest(config, "my-key-id", digest, "Hewwo-owo", |s| {
|
||||||
|
println!("Signing String\n{}", s);
|
||||||
|
Ok(base64::encode(s)) as Result<_, MyError>
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let body = response.bytes().await.map_err(MyError::Body)?;
|
||||||
|
|
||||||
|
println!("{:?}", body);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
std::env::set_var("RUST_LOG", "info");
|
||||||
|
pretty_env_logger::init();
|
||||||
|
|
||||||
|
let config = Config::default().require_header("accept");
|
||||||
|
|
||||||
|
request(config.clone()).await?;
|
||||||
|
request(config.mastodon_compat()).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum MyError {
|
||||||
|
#[error("Failed to create signing string, {0}")]
|
||||||
|
Convert(#[from] SignError),
|
||||||
|
|
||||||
|
#[error("Failed to send request")]
|
||||||
|
SendRequest(#[from] reqwest::Error),
|
||||||
|
|
||||||
|
#[error("Failed to retrieve request body")]
|
||||||
|
Body(reqwest::Error),
|
||||||
|
}
|
|
@ -1,16 +1,110 @@
|
||||||
use reqwest::Request;
|
use crate::{Config, Sign, SignError};
|
||||||
use std::{future::Future, pin::Pin};
|
use reqwest::{Body, RequestBuilder};
|
||||||
|
use std::{fmt::Display, future::Future, pin::Pin};
|
||||||
|
|
||||||
pub trait CreateDigest {
|
mod sha2;
|
||||||
fn create_digest(&mut self, payload: &[u8]) -> String;
|
mod sha3;
|
||||||
|
|
||||||
|
/// A trait for creating digests of an array of bytes
|
||||||
|
pub trait DigestCreate {
|
||||||
|
/// The name of the digest algorithm
|
||||||
|
const NAME: &'static str;
|
||||||
|
|
||||||
|
/// Compute the digest of the input bytes
|
||||||
|
fn compute(&mut self, input: &[u8]) -> String;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait WithDigest: Sized {
|
/// Extend the Sign trait with support for adding Digest Headers to the request
|
||||||
type Future: Future<Output = Self>;
|
///
|
||||||
|
/// It generates HTTP Signatures after the Digest header has been added, in order to have
|
||||||
|
/// verification that the body has not been tampered with, or that the request can't be replayed by
|
||||||
|
/// a malicious entity
|
||||||
|
pub trait SignExt: Sign {
|
||||||
|
fn authorization_signature_with_digest<F, E, K, D, V>(
|
||||||
|
self,
|
||||||
|
config: Config,
|
||||||
|
key_id: K,
|
||||||
|
digest: D,
|
||||||
|
v: V,
|
||||||
|
f: F,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<reqwest::Response, E>>>>
|
||||||
|
where
|
||||||
|
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
|
||||||
|
E: From<SignError> + From<reqwest::Error>,
|
||||||
|
K: Display + 'static,
|
||||||
|
D: DigestCreate + Send + 'static,
|
||||||
|
V: AsRef<[u8]> + Into<Body> + Send + 'static,
|
||||||
|
Self: Sized;
|
||||||
|
|
||||||
fn with_digest<T>(&mut self, creator: T) -> Self::Future;
|
fn signature_with_digest<F, E, K, D, V>(
|
||||||
|
self,
|
||||||
|
config: Config,
|
||||||
|
key_id: K,
|
||||||
|
digest: D,
|
||||||
|
v: V,
|
||||||
|
f: F,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<reqwest::Response, E>>>>
|
||||||
|
where
|
||||||
|
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
|
||||||
|
E: From<SignError> + From<reqwest::Error>,
|
||||||
|
K: Display + 'static,
|
||||||
|
D: DigestCreate + Send + 'static,
|
||||||
|
V: AsRef<[u8]> + Into<Body> + Send + 'static,
|
||||||
|
Self: Sized;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WithDigest for Request {
|
impl SignExt for RequestBuilder {
|
||||||
type Future = Pin<Box<dyn Future<Output = Self> + Send>>;
|
fn authorization_signature_with_digest<F, E, K, D, V>(
|
||||||
|
self,
|
||||||
|
config: Config,
|
||||||
|
key_id: K,
|
||||||
|
mut digest: D,
|
||||||
|
v: V,
|
||||||
|
f: F,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<reqwest::Response, E>>>>
|
||||||
|
where
|
||||||
|
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
|
||||||
|
E: From<SignError> + From<reqwest::Error>,
|
||||||
|
K: Display + 'static,
|
||||||
|
D: DigestCreate + Send + 'static,
|
||||||
|
V: AsRef<[u8]> + Into<Body> + Send + 'static,
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
Box::pin(async move {
|
||||||
|
let digest = tokio::task::block_in_place(|| digest.compute(v.as_ref()));
|
||||||
|
|
||||||
|
let c = self
|
||||||
|
.header("Digest", format!("{}={}", D::NAME, digest))
|
||||||
|
.authorization_signature(&config, key_id, f)?;
|
||||||
|
|
||||||
|
c.body(v).send().await.map_err(E::from)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature_with_digest<F, E, K, D, V>(
|
||||||
|
self,
|
||||||
|
config: Config,
|
||||||
|
key_id: K,
|
||||||
|
mut digest: D,
|
||||||
|
v: V,
|
||||||
|
f: F,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<reqwest::Response, E>>>>
|
||||||
|
where
|
||||||
|
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
|
||||||
|
E: From<SignError> + From<reqwest::Error>,
|
||||||
|
K: Display + 'static,
|
||||||
|
D: DigestCreate + Send + 'static,
|
||||||
|
V: AsRef<[u8]> + Into<Body> + Send + 'static,
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
Box::pin(async move {
|
||||||
|
let digest = tokio::task::block_in_place(|| digest.compute(v.as_ref()));
|
||||||
|
|
||||||
|
let c = self
|
||||||
|
.header("Digest", format!("{}={}", D::NAME, digest))
|
||||||
|
.signature(&config, key_id, f)?;
|
||||||
|
|
||||||
|
c.body(v).send().await.map_err(E::from)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
56
http-signature-normalization-reqwest/src/digest/sha2.rs
Normal file
56
http-signature-normalization-reqwest/src/digest/sha2.rs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
use sha2::{Sha224, Sha256, Sha384, Sha512, Sha512Trunc224, Sha512Trunc256};
|
||||||
|
|
||||||
|
use super::DigestCreate;
|
||||||
|
|
||||||
|
fn create(digest: &mut impl sha2::Digest, input: &[u8]) -> String {
|
||||||
|
digest.update(input);
|
||||||
|
base64::encode(&digest.finalize_reset())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DigestCreate for Sha224 {
|
||||||
|
const NAME: &'static str = "SHA-224";
|
||||||
|
|
||||||
|
fn compute(&mut self, input: &[u8]) -> String {
|
||||||
|
create(self, input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DigestCreate for Sha256 {
|
||||||
|
const NAME: &'static str = "SHA-256";
|
||||||
|
|
||||||
|
fn compute(&mut self, input: &[u8]) -> String {
|
||||||
|
create(self, input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DigestCreate for Sha384 {
|
||||||
|
const NAME: &'static str = "SHA-384";
|
||||||
|
|
||||||
|
fn compute(&mut self, input: &[u8]) -> String {
|
||||||
|
create(self, input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DigestCreate for Sha512 {
|
||||||
|
const NAME: &'static str = "SHA-512";
|
||||||
|
|
||||||
|
fn compute(&mut self, input: &[u8]) -> String {
|
||||||
|
create(self, input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DigestCreate for Sha512Trunc224 {
|
||||||
|
const NAME: &'static str = "SHA-512-224";
|
||||||
|
|
||||||
|
fn compute(&mut self, input: &[u8]) -> String {
|
||||||
|
create(self, input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DigestCreate for Sha512Trunc256 {
|
||||||
|
const NAME: &'static str = "SHA-512-256";
|
||||||
|
|
||||||
|
fn compute(&mut self, input: &[u8]) -> String {
|
||||||
|
create(self, input)
|
||||||
|
}
|
||||||
|
}
|
83
http-signature-normalization-reqwest/src/digest/sha3.rs
Normal file
83
http-signature-normalization-reqwest/src/digest/sha3.rs
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
use sha3::{
|
||||||
|
Keccak224, Keccak256, Keccak256Full, Keccak384, Keccak512, Sha3_224, Sha3_256, Sha3_384,
|
||||||
|
Sha3_512,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::DigestCreate;
|
||||||
|
|
||||||
|
fn create(digest: &mut impl sha2::Digest, input: &[u8]) -> String {
|
||||||
|
digest.update(input);
|
||||||
|
base64::encode(&digest.finalize_reset())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DigestCreate for Sha3_224 {
|
||||||
|
const NAME: &'static str = "SHA3-224";
|
||||||
|
|
||||||
|
fn compute(&mut self, input: &[u8]) -> String {
|
||||||
|
create(self, input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DigestCreate for Sha3_256 {
|
||||||
|
const NAME: &'static str = "SHA3-256";
|
||||||
|
|
||||||
|
fn compute(&mut self, input: &[u8]) -> String {
|
||||||
|
create(self, input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DigestCreate for Sha3_384 {
|
||||||
|
const NAME: &'static str = "SHA3-384";
|
||||||
|
|
||||||
|
fn compute(&mut self, input: &[u8]) -> String {
|
||||||
|
create(self, input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DigestCreate for Sha3_512 {
|
||||||
|
const NAME: &'static str = "SHA3-512";
|
||||||
|
|
||||||
|
fn compute(&mut self, input: &[u8]) -> String {
|
||||||
|
create(self, input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DigestCreate for Keccak224 {
|
||||||
|
const NAME: &'static str = "keccak-224";
|
||||||
|
|
||||||
|
fn compute(&mut self, input: &[u8]) -> String {
|
||||||
|
create(self, input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DigestCreate for Keccak256 {
|
||||||
|
const NAME: &'static str = "keccak-256";
|
||||||
|
|
||||||
|
fn compute(&mut self, input: &[u8]) -> String {
|
||||||
|
create(self, input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DigestCreate for Keccak256Full {
|
||||||
|
const NAME: &'static str = "keccak-256-full";
|
||||||
|
|
||||||
|
fn compute(&mut self, input: &[u8]) -> String {
|
||||||
|
create(self, input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DigestCreate for Keccak384 {
|
||||||
|
const NAME: &'static str = "keccak-384";
|
||||||
|
|
||||||
|
fn compute(&mut self, input: &[u8]) -> String {
|
||||||
|
create(self, input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DigestCreate for Keccak512 {
|
||||||
|
const NAME: &'static str = "keccak-512";
|
||||||
|
|
||||||
|
fn compute(&mut self, input: &[u8]) -> String {
|
||||||
|
create(self, input)
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,79 +2,200 @@ use chrono::Duration;
|
||||||
use http_signature_normalization::create::Signed;
|
use http_signature_normalization::create::Signed;
|
||||||
use reqwest::{
|
use reqwest::{
|
||||||
header::{InvalidHeaderValue, ToStrError},
|
header::{InvalidHeaderValue, ToStrError},
|
||||||
Request,
|
Request, RequestBuilder,
|
||||||
};
|
};
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
pub use http_signature_normalization::RequiredError;
|
||||||
|
|
||||||
pub mod digest;
|
pub mod digest;
|
||||||
|
|
||||||
pub struct Config(http_signature_normalization::Config);
|
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 {
|
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>
|
fn authorization_signature<F, E, K>(self, config: &Config, key_id: K, f: F) -> Result<Self, E>
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
F: FnOnce(&str) -> Result<String, E>,
|
F: FnOnce(&str) -> Result<String, E>,
|
||||||
E: From<ToStrError> + From<InvalidHeaderValue>,
|
E: From<SignError> + From<reqwest::Error>,
|
||||||
K: Display;
|
K: Display;
|
||||||
|
|
||||||
|
/// Add a Signature to the request
|
||||||
fn signature<F, E, K>(self, config: &Config, key_id: K, f: F) -> Result<Self, E>
|
fn signature<F, E, K>(self, config: &Config, key_id: K, f: F) -> Result<Self, E>
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
F: FnOnce(&str) -> Result<String, E>,
|
F: FnOnce(&str) -> Result<String, E>,
|
||||||
E: From<ToStrError> + From<InvalidHeaderValue>,
|
E: From<SignError> + From<reqwest::Error>,
|
||||||
K: Display;
|
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 {
|
impl Config {
|
||||||
pub fn new(expires_after: Duration) -> Self {
|
pub fn new() -> Self {
|
||||||
Config(http_signature_normalization::Config { expires_after })
|
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 Request {
|
impl Sign for RequestBuilder {
|
||||||
fn authorization_signature<F, E, K>(
|
fn authorization_signature<F, E, K>(self, config: &Config, key_id: K, f: F) -> Result<Self, E>
|
||||||
mut self,
|
|
||||||
config: &Config,
|
|
||||||
key_id: K,
|
|
||||||
f: F,
|
|
||||||
) -> Result<Self, E>
|
|
||||||
where
|
where
|
||||||
F: FnOnce(&str) -> Result<String, E>,
|
F: FnOnce(&str) -> Result<String, E>,
|
||||||
E: From<ToStrError> + From<InvalidHeaderValue>,
|
E: From<SignError> + From<reqwest::Error>,
|
||||||
K: Display,
|
K: Display,
|
||||||
{
|
{
|
||||||
let signed = prepare(&self, config, key_id, f)?;
|
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();
|
let auth_header = signed.authorization_header();
|
||||||
self.headers_mut()
|
return Ok(self.header("Authorization", auth_header));
|
||||||
.insert("Authorization", auth_header.parse()?);
|
}
|
||||||
Ok(self)
|
|
||||||
|
Err(SignError::BodyPresent.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn signature<F, E, K>(mut self, config: &Config, key_id: K, f: F) -> Result<Self, E>
|
fn signature<F, E, K>(self, config: &Config, key_id: K, f: F) -> Result<Self, E>
|
||||||
where
|
where
|
||||||
F: FnOnce(&str) -> Result<String, E>,
|
F: FnOnce(&str) -> Result<String, E>,
|
||||||
E: From<ToStrError> + From<InvalidHeaderValue>,
|
E: From<SignError> + From<reqwest::Error>,
|
||||||
K: Display,
|
K: Display,
|
||||||
{
|
{
|
||||||
let signed = prepare(&self, config, key_id, f)?;
|
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();
|
let sig_header = signed.signature_header();
|
||||||
self.headers_mut().insert("Signature", sig_header.parse()?);
|
return Ok(self.header("Signature", sig_header));
|
||||||
Ok(self)
|
}
|
||||||
|
|
||||||
|
Err(SignError::BodyPresent.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare<F, E, K>(req: &Request, config: &Config, key_id: K, f: F) -> Result<Signed, E>
|
fn prepare<F, E, K>(req: &Request, config: &Config, key_id: K, f: F) -> Result<Signed, E>
|
||||||
where
|
where
|
||||||
F: FnOnce(&str) -> Result<String, E>,
|
F: FnOnce(&str) -> Result<String, E>,
|
||||||
E: From<ToStrError> + From<InvalidHeaderValue>,
|
E: From<SignError>,
|
||||||
K: Display,
|
K: Display,
|
||||||
{
|
{
|
||||||
let mut bt = std::collections::BTreeMap::new();
|
let mut bt = std::collections::BTreeMap::new();
|
||||||
for (k, v) in req.headers().iter() {
|
for (k, v) in req.headers().iter() {
|
||||||
bt.insert(k.as_str().to_owned(), v.to_str()?.to_owned());
|
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() {
|
let path_and_query = if let Some(query) = req.url().query() {
|
||||||
format!("{}?{}", req.url().path(), query)
|
format!("{}?{}", req.url().path(), query)
|
||||||
|
@ -82,8 +203,9 @@ where
|
||||||
req.url().path().to_string()
|
req.url().path().to_string()
|
||||||
};
|
};
|
||||||
let unsigned = config
|
let unsigned = config
|
||||||
.0
|
.config
|
||||||
.begin_sign(req.method().as_str(), &path_and_query, bt);
|
.begin_sign(req.method().as_str(), &path_and_query, bt)
|
||||||
|
.map_err(SignError::from)?;
|
||||||
|
|
||||||
let signed = unsigned.sign(key_id.to_string(), f)?;
|
let signed = unsigned.sign(key_id.to_string(), f)?;
|
||||||
Ok(signed)
|
Ok(signed)
|
||||||
|
|
17
src/lib.rs
17
src/lib.rs
|
@ -105,9 +105,22 @@ impl Config {
|
||||||
Config::default()
|
Config::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opt out of using the (created) and (expires) fields introduced in draft 11
|
/// Enable mastodon compatibility
|
||||||
///
|
///
|
||||||
/// Use this for compatibility with mastodon.
|
/// 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 {
|
||||||
|
self.dont_use_created_field().require_header("host")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
self.require_header("Digest")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opt out of using the (created) and (expires) fields introduced in draft 11
|
||||||
///
|
///
|
||||||
/// Note that by not requiring the created field, the Date header becomes required. This is to
|
/// Note that by not requiring the created field, the Date header becomes required. This is to
|
||||||
/// prevent replay attacks.
|
/// prevent replay attacks.
|
||||||
|
|
Loading…
Reference in a new issue