apub-awc 0.2.0

Utilities for building activitypub servers
//! 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 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.
///
/// ```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?
            .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
    }
}