apub_reqwest/
lib.rs

1//! A Reqwest-backed Repo and Client implementation for apub
2
3#![deny(missing_docs)]
4use apub_core::{
5    deliver::Deliver,
6    digest::{Digest, DigestBuilder, DigestFactory},
7    repo::{Dereference, Repo},
8    session::{Session, SessionError},
9    signature::{PrivateKey, Sign},
10};
11use http_signature_normalization_reqwest::{
12    digest::{DigestCreate, SignExt},
13    prelude::{Sign as _, SignError},
14};
15use reqwest::header::{ACCEPT, CONTENT_TYPE, DATE};
16use reqwest_middleware::ClientWithMiddleware;
17use std::time::SystemTime;
18use url::Url;
19
20pub use http_signature_normalization_reqwest::Config as SignatureConfig;
21
22/// A Repo and Deliver type backed by reqwest
23///
24/// This client is generic over it's Cryptography. It signs it's requests with HTTP Signatures, and
25/// computes digests of it's request bodies.
26///
27/// ```rust
28/// use apub_reqwest::{ReqwestClient, SignatureConfig};
29/// use apub_rustcrypto::Rustcrypto;
30/// use reqwest_middleware::ClientBuilder;
31/// use rsa::RsaPrivateKey;
32///
33/// fn main() -> Result<(), Box<dyn std::error::Error>> {
34///     let private_key = RsaPrivateKey::new(&mut rand::thread_rng(), 1024)?;
35///     let crypto = Rustcrypto::new("key-id".to_string(), private_key);
36///     let signature_config = SignatureConfig::default();
37///
38///     let client = reqwest::Client::new();
39///     let client = ClientBuilder::new(client).build();
40///
41///     let reqwest_client = ReqwestClient::new(client, signature_config, &crypto);
42///     Ok(())
43/// }
44/// ```
45pub struct ReqwestClient<Crypto> {
46    client: ClientWithMiddleware,
47    config: SignatureConfig,
48    crypto: Crypto,
49}
50
51/// The error produced hile signing a request
52#[derive(Debug, thiserror::Error)]
53pub enum SignatureError<E: std::error::Error + Send> {
54    /// Signature errors
55    #[error(transparent)]
56    Sign(#[from] SignError),
57
58    /// Reqwest errors
59    #[error(transparent)]
60    Reqwest(#[from] reqwest::Error),
61
62    /// Cryptography-specified signing errors
63    #[error(transparent)]
64    Signer(E),
65}
66
67/// Errors produced when sending requests with the client
68#[derive(Debug, thiserror::Error)]
69pub enum ReqwestError<E: std::error::Error + Send> {
70    /// The session prevented the request from continuing
71    #[error("Session indicated request should not procede")]
72    Session(#[from] SessionError),
73
74    /// Reqwest errors
75    #[error(transparent)]
76    Reqwest(#[from] reqwest::Error),
77
78    /// Middleware errors
79    #[error(transparent)]
80    Middleware(#[from] reqwest_middleware::Error),
81
82    /// Failed to serialize the json request body
83    #[error(transparent)]
84    Json(#[from] serde_json::Error),
85
86    /// Request failed with non-2xx status
87    #[error("Invalid response code: {0}")]
88    Status(u16),
89
90    /// Errors signing the request
91    #[error(transparent)]
92    SignatureError(#[from] SignatureError<E>),
93}
94
95type SignTraitError<S> = <<S as PrivateKey>::Signer as Sign>::Error;
96
97struct DigestWrapper<D>(D);
98
99impl<D> DigestCreate for DigestWrapper<D>
100where
101    D: Digest + Clone,
102{
103    const NAME: &'static str = D::NAME;
104
105    fn compute(&mut self, input: &[u8]) -> String {
106        self.0.clone().digest(input)
107    }
108}
109
110impl<Crypto> ReqwestClient<Crypto>
111where
112    Crypto: PrivateKey,
113    SignTraitError<Crypto>: std::error::Error,
114{
115    /// Create a new Client & Repo implementation backed by the reqwest client
116    pub fn new(client: ClientWithMiddleware, config: SignatureConfig, crypto: Crypto) -> Self {
117        Self {
118            client,
119            config,
120            crypto,
121        }
122    }
123
124    async fn do_fetch<Id>(
125        &self,
126        url: &Url,
127    ) -> Result<Option<<Id as Dereference>::Output>, ReqwestError<SignTraitError<Crypto>>>
128    where
129        Id: Dereference,
130    {
131        let request = self
132            .client
133            .get(url.as_str())
134            .header(ACCEPT, "application/activity+json")
135            .header(DATE, httpdate::fmt_http_date(SystemTime::now()))
136            .signature(&self.config, self.crypto.key_id(), {
137                let sign = self.crypto.signer();
138
139                move |signing_string| sign.sign(signing_string).map_err(SignatureError::Signer)
140            })?;
141
142        let response = self.client.execute(request).await?;
143
144        Ok(Some(response.json().await?))
145    }
146}
147
148#[async_trait::async_trait(?Send)]
149impl<Crypto> Repo for ReqwestClient<Crypto>
150where
151    Crypto: PrivateKey + Send + Sync,
152    SignTraitError<Crypto>: std::error::Error,
153{
154    type Error = ReqwestError<SignTraitError<Crypto>>;
155
156    async fn fetch<D: Dereference, S: Session>(
157        &self,
158        id: D,
159        session: S,
160    ) -> Result<Option<D::Output>, Self::Error> {
161        apub_core::session::guard(self.do_fetch::<D>(id.url()), id.url(), session).await
162    }
163}
164
165#[async_trait::async_trait(?Send)]
166impl<Crypto> Deliver for ReqwestClient<Crypto>
167where
168    Crypto: DigestFactory + PrivateKey + Send + Sync,
169    <Crypto as DigestFactory>::Digest: DigestBuilder + Clone,
170    SignTraitError<Crypto>: std::error::Error,
171{
172    type Error = ReqwestError<SignTraitError<Crypto>>;
173
174    async fn deliver<T: serde::ser::Serialize, S: Session>(
175        &self,
176        inbox: &Url,
177        activity: &T,
178        session: S,
179    ) -> Result<(), Self::Error> {
180        apub_core::session::guard(
181            async move {
182                let activity_string = serde_json::to_string(activity)?;
183
184                let request = self
185                    .client
186                    .post(inbox.as_str())
187                    .header(CONTENT_TYPE, "application/activity+json")
188                    .header(ACCEPT, "application/activity+json")
189                    .header(DATE, httpdate::fmt_http_date(SystemTime::now()))
190                    .signature_with_digest(
191                        self.config.clone(),
192                        self.crypto.key_id(),
193                        DigestWrapper(Crypto::Digest::build()),
194                        activity_string,
195                        {
196                            let signer = self.crypto.signer();
197                            move |signing_string| {
198                                signer.sign(signing_string).map_err(SignatureError::Signer)
199                            }
200                        },
201                    )
202                    .await?;
203
204                let response = self.client.execute(request).await?;
205
206                if !response.status().is_success() {
207                    return Err(ReqwestError::Status(response.status().as_u16()));
208                }
209
210                Ok(())
211            },
212            inbox,
213            session,
214        )
215        .await
216    }
217}