230 lines
6.7 KiB
Rust
230 lines
6.7 KiB
Rust
//! Implementation of Repo and Client backed by awc
|
|
|
|
#![deny(missing_docs)]
|
|
|
|
use actix_http::error::BlockingError;
|
|
use apub_core::{
|
|
deliver::Deliver,
|
|
digest::{Digest, DigestBuilder, DigestFactory},
|
|
repo::{Dereference, Repo},
|
|
session::{Session, SessionError},
|
|
signature::{PrivateKey, Sign},
|
|
};
|
|
use awc::{http::header::HttpDate, Client};
|
|
use http_signature_normalization_actix::{
|
|
digest::DigestName,
|
|
prelude::{DigestCreate, Sign as _, SignExt},
|
|
};
|
|
use std::time::SystemTime;
|
|
use tracing_awc::Propagate;
|
|
use url::Url;
|
|
|
|
pub use http_signature_normalization_actix::{
|
|
prelude::{InvalidHeaderValue, PrepareSignError},
|
|
Config as SignatureConfig,
|
|
};
|
|
|
|
/// A Repo and Deliver type backed by awc
|
|
///
|
|
/// This client is generic over it's Cryptography. It signs it's requests with HTTP Signatures, and
|
|
/// computes digests of it's request bodies. It also propagates the OpenTelemetry Trace ID if that
|
|
/// feature is enabled in tracing-awc, and wraps the request and response body in tracing spans.
|
|
///
|
|
/// ```rust
|
|
/// use apub_awc::{AwcClient, SignatureConfig};
|
|
/// use apub_openssl::OpenSsl;
|
|
/// use openssl::{pkey::PKey, rsa::Rsa};
|
|
///
|
|
/// fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
/// let private_key = PKey::from_rsa(Rsa::generate(1024)?)?;
|
|
/// let crypto = OpenSsl::new("key-id".to_string(), private_key);
|
|
/// let signature_config = SignatureConfig::default();
|
|
///
|
|
/// let client = awc::Client::new();
|
|
///
|
|
/// let awc_client = AwcClient::new(client, signature_config, &crypto);
|
|
/// Ok(())
|
|
/// }
|
|
/// ```
|
|
pub struct AwcClient<Crypto> {
|
|
client: Client,
|
|
config: SignatureConfig,
|
|
crypto: Crypto,
|
|
}
|
|
|
|
/// Errors produced while signing requests
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum SignatureError<E: std::error::Error + Send> {
|
|
/// Failed to create the signature header
|
|
#[error(transparent)]
|
|
Header(#[from] InvalidHeaderValue),
|
|
|
|
/// Failed to prepare for signing
|
|
#[error(transparent)]
|
|
Sign(#[from] PrepareSignError),
|
|
|
|
/// Signature operation panicked
|
|
#[error(transparent)]
|
|
Blocking(#[from] BlockingError),
|
|
|
|
/// Cryptography-provided signature error
|
|
#[error(transparent)]
|
|
Signer(E),
|
|
}
|
|
|
|
/// Errors produced while sending requests
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum AwcError<E: std::error::Error + Send> {
|
|
/// The session indicated to stop requesting
|
|
#[error("Session indicated request should not procede")]
|
|
Session(#[from] SessionError),
|
|
|
|
/// awc failed to send the request
|
|
#[error(transparent)]
|
|
Request(#[from] awc::error::SendRequestError),
|
|
|
|
/// awc failed to parse json from the response
|
|
#[error(transparent)]
|
|
Response(#[from] awc::error::JsonPayloadError),
|
|
|
|
/// failed to serialize the json payload
|
|
#[error(transparent)]
|
|
Json(#[from] serde_json::Error),
|
|
|
|
/// The request failed with a non-2xx status
|
|
#[error("Invalid response code: {0}")]
|
|
Status(u16),
|
|
|
|
/// Failed to sign the request
|
|
#[error(transparent)]
|
|
SignatureError(#[from] SignatureError<E>),
|
|
}
|
|
|
|
type SignTraitError<S> = <<S as PrivateKey>::Signer as Sign>::Error;
|
|
|
|
struct DigestWrapper<D>(D);
|
|
|
|
impl<D> DigestName for DigestWrapper<D>
|
|
where
|
|
D: Digest,
|
|
{
|
|
const NAME: &'static str = D::NAME;
|
|
}
|
|
|
|
impl<D> DigestCreate for DigestWrapper<D>
|
|
where
|
|
D: Digest + Clone,
|
|
{
|
|
fn compute(&mut self, input: &[u8]) -> String {
|
|
self.0.digest(input)
|
|
}
|
|
}
|
|
|
|
impl<Crypto> AwcClient<Crypto>
|
|
where
|
|
Crypto: PrivateKey,
|
|
SignTraitError<Crypto>: std::error::Error,
|
|
{
|
|
/// Creates a new Client and Repo implementation backed by awc
|
|
pub fn new(client: Client, config: SignatureConfig, crypto: Crypto) -> Self {
|
|
Self {
|
|
client,
|
|
config,
|
|
crypto,
|
|
}
|
|
}
|
|
|
|
async fn do_fetch<Id: Dereference>(
|
|
&self,
|
|
url: &Url,
|
|
) -> Result<Option<<Id as Dereference>::Output>, AwcError<SignTraitError<Crypto>>> {
|
|
let mut response = self
|
|
.client
|
|
.get(url.as_str())
|
|
.insert_header(("Accept", "application/activity+json"))
|
|
.insert_header(("Date", HttpDate::from(SystemTime::now())))
|
|
.signature(self.config.clone(), self.crypto.key_id(), {
|
|
let sign = self.crypto.signer();
|
|
|
|
move |signing_string| sign.sign(signing_string).map_err(SignatureError::Signer)
|
|
})
|
|
.await?
|
|
.propagate()
|
|
.send()
|
|
.await?;
|
|
|
|
Ok(Some(response.json().await?))
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait(?Send)]
|
|
impl<Crypto> Repo for AwcClient<Crypto>
|
|
where
|
|
Crypto: PrivateKey,
|
|
SignTraitError<Crypto>: std::error::Error,
|
|
{
|
|
type Error = AwcError<SignTraitError<Crypto>>;
|
|
|
|
async fn fetch<D: Dereference, S: Session>(
|
|
&self,
|
|
id: D,
|
|
session: S,
|
|
) -> Result<Option<D::Output>, Self::Error> {
|
|
apub_core::session::guard(self.do_fetch::<D>(id.url()), id.url(), session).await
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait(?Send)]
|
|
impl<Crypto> Deliver for AwcClient<Crypto>
|
|
where
|
|
Crypto: DigestFactory + PrivateKey,
|
|
<Crypto as DigestFactory>::Digest: DigestBuilder + Clone,
|
|
SignTraitError<Crypto>: std::error::Error,
|
|
{
|
|
type Error = AwcError<SignTraitError<Crypto>>;
|
|
|
|
async fn deliver<T: serde::ser::Serialize, S: Session>(
|
|
&self,
|
|
inbox: &Url,
|
|
activity: &T,
|
|
session: S,
|
|
) -> Result<(), Self::Error> {
|
|
apub_core::session::guard(
|
|
async move {
|
|
let activity_string = serde_json::to_string(activity)?;
|
|
let (req, body) = self
|
|
.client
|
|
.post(inbox.as_str())
|
|
.content_type("application/activity+json")
|
|
.insert_header(("Accept", "application/activity+json"))
|
|
.insert_header(("Date", HttpDate::from(SystemTime::now())))
|
|
.signature_with_digest(
|
|
self.config.clone(),
|
|
self.crypto.key_id(),
|
|
DigestWrapper(Crypto::Digest::build()),
|
|
activity_string,
|
|
{
|
|
let signer = self.crypto.signer();
|
|
move |signing_string| {
|
|
signer.sign(signing_string).map_err(SignatureError::Signer)
|
|
}
|
|
},
|
|
)
|
|
.await?
|
|
.split();
|
|
|
|
let response = req.propagate().send_body(body).await?;
|
|
|
|
if !response.status().is_success() {
|
|
return Err(AwcError::Status(response.status().as_u16()));
|
|
}
|
|
|
|
Ok(())
|
|
},
|
|
inbox,
|
|
session,
|
|
)
|
|
.await
|
|
}
|
|
}
|