#![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 url::Url;
pub use http_signature_normalization_actix::{
prelude::{InvalidHeaderValue, PrepareSignError},
Config as SignatureConfig,
};
pub struct AwcClient<Crypto> {
client: Client,
config: SignatureConfig,
crypto: Crypto,
}
#[derive(Debug, thiserror::Error)]
pub enum SignatureError<E: std::error::Error + Send> {
#[error(transparent)]
Header(#[from] InvalidHeaderValue),
#[error(transparent)]
Sign(#[from] PrepareSignError),
#[error(transparent)]
Blocking(#[from] BlockingError),
#[error(transparent)]
Signer(E),
}
#[derive(Debug, thiserror::Error)]
pub enum AwcError<E: std::error::Error + Send> {
#[error("Session indicated request should not procede")]
Session(#[from] SessionError),
#[error(transparent)]
Request(#[from] awc::error::SendRequestError),
#[error(transparent)]
Response(#[from] awc::error::JsonPayloadError),
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error("Invalid response code: {0}")]
Status(u16),
#[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,
{
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?
.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.send_body(body).await?;
if !response.status().is_success() {
return Err(AwcError::Status(response.status().as_u16()));
}
Ok(())
},
inbox,
session,
)
.await
}
}