apub-reqwest 0.2.0

Utilities for building activitypub servers
Documentation
//! A Reqwest-backed Repo and Client implementation for apub

#![deny(missing_docs)]
use apub_core::{
    deliver::Deliver,
    digest::{Digest, DigestBuilder, DigestFactory},
    repo::{Dereference, Repo},
    session::{Session, SessionError},
    signature::{PrivateKey, Sign},
};
use http_signature_normalization_reqwest::{
    digest::{DigestCreate, SignExt},
    prelude::{Sign as _, SignError},
};
use reqwest::header::{ACCEPT, CONTENT_TYPE, DATE};
use reqwest_middleware::ClientWithMiddleware;
use std::time::SystemTime;
use url::Url;

pub use http_signature_normalization_reqwest::Config as SignatureConfig;

/// A Repo and Deliver type backed by reqwest
///
/// 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_reqwest::{ReqwestClient, SignatureConfig};
/// use apub_rustcrypto::Rustcrypto;
/// use reqwest_middleware::ClientBuilder;
/// use rsa::RsaPrivateKey;
///
/// fn main() -> Result<(), Box<dyn std::error::Error>> {
///     let private_key = RsaPrivateKey::new(&mut rand::thread_rng(), 1024)?;
///     let crypto = Rustcrypto::new("key-id".to_string(), private_key);
///     let signature_config = SignatureConfig::default();
///
///     let client = reqwest::Client::new();
///     let client = ClientBuilder::new(client).build();
///
///     let reqwest_client = ReqwestClient::new(client, signature_config, &crypto);
///     Ok(())
/// }
/// ```
pub struct ReqwestClient<Crypto> {
    client: ClientWithMiddleware,
    config: SignatureConfig,
    crypto: Crypto,
}

/// The error produced hile signing a request
#[derive(Debug, thiserror::Error)]
pub enum SignatureError<E: std::error::Error + Send> {
    /// Signature errors
    #[error(transparent)]
    Sign(#[from] SignError),

    /// Reqwest errors
    #[error(transparent)]
    Reqwest(#[from] reqwest::Error),

    /// Cryptography-specified signing errors
    #[error(transparent)]
    Signer(E),
}

/// Errors produced when sending requests with the client
#[derive(Debug, thiserror::Error)]
pub enum ReqwestError<E: std::error::Error + Send> {
    /// The session prevented the request from continuing
    #[error("Session indicated request should not procede")]
    Session(#[from] SessionError),

    /// Reqwest errors
    #[error(transparent)]
    Reqwest(#[from] reqwest::Error),

    /// Middleware errors
    #[error(transparent)]
    Middleware(#[from] reqwest_middleware::Error),

    /// Failed to serialize the json request body
    #[error(transparent)]
    Json(#[from] serde_json::Error),

    /// Request failed with non-2xx status
    #[error("Invalid response code: {0}")]
    Status(u16),

    /// Errors signing the request
    #[error(transparent)]
    SignatureError(#[from] SignatureError<E>),
}

type SignTraitError<S> = <<S as PrivateKey>::Signer as Sign>::Error;

struct DigestWrapper<D>(D);

impl<D> DigestCreate for DigestWrapper<D>
where
    D: Digest + Clone,
{
    const NAME: &'static str = D::NAME;

    fn compute(&mut self, input: &[u8]) -> String {
        self.0.clone().digest(input)
    }
}

impl<Crypto> ReqwestClient<Crypto>
where
    Crypto: PrivateKey,
    SignTraitError<Crypto>: std::error::Error,
{
    /// Create a new Client & Repo implementation backed by the reqwest client
    pub fn new(client: ClientWithMiddleware, config: SignatureConfig, crypto: Crypto) -> Self {
        Self {
            client,
            config,
            crypto,
        }
    }

    async fn do_fetch<Id>(
        &self,
        url: &Url,
    ) -> Result<Option<<Id as Dereference>::Output>, ReqwestError<SignTraitError<Crypto>>>
    where
        Id: Dereference,
    {
        let request = self
            .client
            .get(url.as_str())
            .header(ACCEPT, "application/activity+json")
            .header(DATE, httpdate::fmt_http_date(SystemTime::now()))
            .signature(&self.config, self.crypto.key_id(), {
                let sign = self.crypto.signer();

                move |signing_string| sign.sign(signing_string).map_err(SignatureError::Signer)
            })?;

        let response = self.client.execute(request).await?;

        Ok(Some(response.json().await?))
    }
}

#[async_trait::async_trait(?Send)]
impl<Crypto> Repo for ReqwestClient<Crypto>
where
    Crypto: PrivateKey + Send + Sync,
    SignTraitError<Crypto>: std::error::Error,
{
    type Error = ReqwestError<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 ReqwestClient<Crypto>
where
    Crypto: DigestFactory + PrivateKey + Send + Sync,
    <Crypto as DigestFactory>::Digest: DigestBuilder + Clone,
    SignTraitError<Crypto>: std::error::Error,
{
    type Error = ReqwestError<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 request = self
                    .client
                    .post(inbox.as_str())
                    .header(CONTENT_TYPE, "application/activity+json")
                    .header(ACCEPT, "application/activity+json")
                    .header(DATE, httpdate::fmt_http_date(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?;

                let response = self.client.execute(request).await?;

                if !response.status().is_success() {
                    return Err(ReqwestError::Status(response.status().as_u16()));
                }

                Ok(())
            },
            inbox,
            session,
        )
        .await
    }
}