acme2_eab/
directory.rs

1use crate::error::*;
2use crate::jws::jws;
3use hyper::body::Bytes;
4use openssl::pkey::PKey;
5use openssl::pkey::Private;
6use serde::de::DeserializeOwned;
7use serde::Deserialize;
8use serde::Serialize;
9use std::sync::Arc;
10use std::sync::Mutex;
11use tracing::debug;
12use tracing::field;
13use tracing::instrument;
14use tracing::Level;
15use tracing::Span;
16
17/// An builder that is used create a [`Directory`].
18pub struct DirectoryBuilder {
19    url: String,
20    http_client: Option<reqwest::Client>,
21}
22
23impl DirectoryBuilder {
24    /// Creates a new builder with the specified directory root URL.
25    ///
26    /// Let's Encrypt: `https://acme-v02.api.letsencrypt.org/directory`
27    ///
28    /// Let's Encrypt Staging: `https://acme-staging-v02.api.letsencrypt.org/directory`
29    pub fn new(url: String) -> Self {
30        DirectoryBuilder {
31            url,
32            http_client: None,
33        }
34    }
35
36    /// Specify a custom [`reqwest::Client`] to use for all outbound HTTP
37    /// requests to the ACME server.
38    pub fn http_client(&mut self, http_client: reqwest::Client) -> &mut Self {
39        self.http_client = Some(http_client);
40        self
41    }
42
43    /// Build a [`Directory`] using the given parameters.
44    ///
45    /// If no http client is specified, a default client will be created using
46    /// the webpki trust roots.
47    #[instrument(
48    level = Level::INFO,
49    name = "acme2::DirectoryBuilder::build",
50    err,
51    skip(self),
52    fields(url = %self.url, custom_http_client = self.http_client.is_some(), dir = field::Empty)
53  )]
54    pub async fn build(&mut self) -> Result<Arc<Directory>, Error> {
55        let http_client = self.http_client.clone().unwrap_or_default();
56
57        let resp = http_client.get(&self.url).send().await?;
58
59        let res: Result<Directory, Error> = resp.json::<ServerResult<Directory>>().await?.into();
60        let mut dir = res?;
61        Span::current().record("dir", &field::debug(&dir));
62
63        dir.http_client = http_client;
64        dir.nonce = Mutex::new(None);
65
66        Ok(Arc::new(dir))
67    }
68}
69
70/// A directory is the resource representing how to reach an ACME server.
71///
72/// Must be created through a [`DirectoryBuilder`].
73#[derive(Deserialize, Debug)]
74#[serde(rename_all = "camelCase")]
75#[allow(unused)]
76pub struct Directory {
77    #[serde(skip)]
78    pub(crate) http_client: reqwest::Client,
79    #[serde(skip)]
80    pub(crate) nonce: Mutex<Option<String>>,
81    #[serde(rename = "newNonce")]
82    pub(crate) new_nonce_url: String,
83    #[serde(rename = "newAccount")]
84    pub(crate) new_account_url: String,
85    #[serde(rename = "newOrder")]
86    pub(crate) new_order_url: String,
87    #[serde(rename = "revokeCert")]
88    pub(crate) revoke_cert_url: String,
89    #[serde(rename = "keyChange")]
90    pub(crate) key_change_url: Option<String>,
91    #[serde(rename = "newAuthz")]
92    pub(crate) new_authz_url: Option<String>,
93    /// Optional metadata describing a directory.
94    pub meta: Option<DirectoryMeta>,
95}
96
97/// This is some metadata about a directory.
98///
99/// Directories are not required to provide this information.
100#[derive(Deserialize, Clone, Debug)]
101#[serde(rename_all = "camelCase")]
102pub struct DirectoryMeta {
103    pub terms_of_service: Option<String>,
104    pub website: Option<String>,
105    pub caa_identities: Option<Vec<String>>,
106    pub external_account_required: Option<bool>,
107}
108
109fn extract_nonce_from_response(resp: &reqwest::Response) -> Result<Option<String>, Error> {
110    let headers = resp.headers();
111    let maybe_nonce_res = headers
112        .get("replay-nonce")
113        .map::<Result<String, Error>, _>(|hv| Ok(map_transport_err(hv.to_str())?.to_string()));
114    match maybe_nonce_res {
115        Some(Ok(n)) => Ok(Some(n)),
116        Some(Err(err)) => Err(err),
117        None => Ok(None),
118    }
119}
120
121impl Directory {
122    #[instrument(
123    level = Level::DEBUG,
124    name = "acme2::Directory::get_nonce",
125    err,
126    skip(self),
127    fields(cached = field::Empty)
128  )]
129    pub(crate) async fn get_nonce(&self) -> Result<String, Error> {
130        let maybe_nonce = {
131            let mut guard = self.nonce.lock().unwrap();
132            (*guard).take()
133        };
134        let span = Span::current();
135        span.record("cached", maybe_nonce.is_some());
136        if let Some(nonce) = maybe_nonce {
137            return Ok(nonce);
138        }
139        let resp = self.http_client.get(&self.new_nonce_url).send().await?;
140        let maybe_nonce = extract_nonce_from_response(&resp)?;
141        match maybe_nonce {
142            Some(nonce) => Ok(nonce),
143            None => Err(transport_err("newNonce request must return a nonce")),
144        }
145    }
146
147    #[instrument(level = Level::DEBUG, name = "acme2::Directory::authenticated_request_raw", err, skip(self, payload, pkey))]
148    async fn authenticated_request_raw(
149        &self,
150        url: &str,
151        payload: &str,
152        pkey: &PKey<Private>,
153        account_id: &Option<String>,
154    ) -> Result<reqwest::Response, Error> {
155        let nonce = self.get_nonce().await?;
156        let body = jws(url, Some(nonce), payload, pkey, account_id.clone())?;
157        let body = serde_json::to_vec(&body)?;
158        let resp = self
159            .http_client
160            .post(url)
161            .header(reqwest::header::CONTENT_TYPE, "application/jose+json")
162            .body(body)
163            .send()
164            .await?;
165
166        if let Some(nonce) = extract_nonce_from_response(&resp)? {
167            let mut guard = self.nonce.lock().unwrap();
168            *guard = Some(nonce);
169        }
170
171        Ok(resp)
172    }
173
174    #[instrument(
175    level = Level::DEBUG,
176    name = "acme2::Directory::authenticated_request_bytes",
177    err,
178    skip(self, payload, pkey),
179    fields()
180  )]
181    pub(crate) async fn authenticated_request_bytes(
182        &self,
183        url: &str,
184        payload: &str,
185        pkey: &PKey<Private>,
186        account_id: &Option<String>,
187    ) -> Result<(Result<Bytes, ServerError>, reqwest::header::HeaderMap), Error> {
188        let mut attempt = 0;
189
190        loop {
191            attempt += 1;
192
193            let resp = self
194                .authenticated_request_raw(url, payload, pkey, account_id)
195                .await?;
196
197            let headers = resp.headers().clone();
198
199            if resp.status().is_success() {
200                return Ok((Ok(resp.bytes().await?), headers));
201            }
202
203            let err: ServerError = resp.json().await?;
204
205            if let Some(typ) = err.r#type.clone() {
206                if &typ == "urn:ietf:params:acme:error:badNonce" && attempt <= 3 {
207                    debug!({ attempt }, "bad nonce, retrying");
208                    continue;
209                }
210            }
211
212            return Ok((Err(err), headers));
213        }
214    }
215
216    #[instrument(
217    level = Level::DEBUG,
218    name = "acme2::Directory::authenticated_request",
219    err,
220    skip(self, payload, pkey),
221    fields()
222  )]
223    pub(crate) async fn authenticated_request<T, R>(
224        &self,
225        url: &str,
226        payload: T,
227        pkey: PKey<Private>,
228        account_id: Option<String>,
229    ) -> Result<(ServerResult<R>, reqwest::header::HeaderMap), Error>
230    where
231        T: Serialize,
232        R: DeserializeOwned,
233    {
234        let payload = serde_json::to_string(&payload)?;
235        let payload = if payload == "\"\"" {
236            "".to_string()
237        } else {
238            payload
239        };
240
241        let (res, headers) = self
242            .authenticated_request_bytes(url, &payload, &pkey, &account_id)
243            .await?;
244
245        let bytes = match res {
246            Ok(bytes) => bytes,
247            Err(err) => return Ok((ServerResult::Err(err), headers)),
248        };
249
250        let val: R = serde_json::from_slice(&bytes)?;
251
252        Ok((ServerResult::Ok(val), headers))
253    }
254}