acme_lib/
api.rs

1//! Low level API JSON objects.
2//!
3//! Unstable and not to be used directly. Provided to aid debugging.
4#![allow(non_snake_case)]
5#![allow(non_camel_case_types)]
6
7use serde::{
8    ser::{SerializeMap, Serializer},
9    Deserialize, Serialize,
10};
11
12/// Serializes to `""`
13pub struct ApiEmptyString;
14impl Serialize for ApiEmptyString {
15    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
16    where
17        S: Serializer,
18    {
19        serializer.serialize_str("")
20    }
21}
22
23/// Serializes to `{}`
24pub struct ApiEmptyObject;
25impl Serialize for ApiEmptyObject {
26    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
27    where
28        S: Serializer,
29    {
30        let m = serializer.serialize_map(Some(0))?;
31        m.end()
32    }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
36pub struct ApiProblem {
37    #[serde(rename = "type")]
38    pub _type: String,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub detail: Option<String>,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub subproblems: Option<Vec<ApiSubproblem>>,
43}
44
45impl ApiProblem {
46    pub fn is_bad_nonce(&self) -> bool {
47        self._type == "badNonce"
48    }
49    pub fn is_jwt_verification_error(&self) -> bool {
50        (self._type == "urn:acme:error:malformed"
51            || self._type == "urn:ietf:params:acme:error:malformed")
52            && self
53                .detail
54                .as_ref()
55                .map(|s| s == "JWS verification error")
56                .unwrap_or(false)
57    }
58}
59
60impl std::fmt::Display for ApiProblem {
61    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
62        if let Some(detail) = &self.detail {
63            write!(f, "{}: {}", self._type, detail)
64        } else {
65            write!(f, "{}", self._type)
66        }
67    }
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
71pub struct ApiSubproblem {
72    #[serde(rename = "type")]
73    pub _type: String,
74    pub detail: Option<String>,
75    pub identifier: Option<ApiIdentifier>,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
79pub struct ApiDirectory {
80    pub newNonce: String,
81    pub newAccount: String,
82    pub newOrder: String,
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub newAuthz: Option<String>,
85    pub revokeCert: String,
86    pub keyChange: String,
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub meta: Option<ApiDirectoryMeta>,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
92pub struct ApiDirectoryMeta {
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub termsOfService: Option<String>,
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub website: Option<String>,
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub caaIdentities: Option<Vec<String>>,
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub externalAccountRequired: Option<bool>,
101}
102
103impl ApiDirectoryMeta {
104    pub fn externalAccountRequired(&self) -> bool {
105        self.externalAccountRequired.unwrap_or(false)
106    }
107}
108
109//    {
110//      "status": "valid",
111//      "contact": [
112//        "mailto:cert-admin@example.com",
113//        "mailto:admin@example.com"
114//      ],
115//      "termsOfServiceAgreed": true,
116//      "orders": "https://example.com/acme/acct/evOfKhNU60wg/orders"
117//    }
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
119pub struct ApiAccount {
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub status: Option<String>,
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub contact: Option<Vec<String>>,
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub termsOfServiceAgreed: Option<bool>,
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub orders: Option<String>,
128}
129
130impl ApiAccount {
131    pub fn is_status_valid(&self) -> bool {
132        self.status.as_ref().map(|s| s.as_ref()) == Some("valid")
133    }
134    pub fn is_status_deactivated(&self) -> bool {
135        self.status.as_ref().map(|s| s.as_ref()) == Some("deactivated")
136    }
137    pub fn is_status_revoked(&self) -> bool {
138        self.status.as_ref().map(|s| s.as_ref()) == Some("revoked")
139    }
140    pub fn termsOfServiceAgreed(&self) -> bool {
141        self.termsOfServiceAgreed.unwrap_or(false)
142    }
143}
144
145// {
146//   "status": "pending",
147//   "expires": "2019-01-09T08:26:43.570360537Z",
148//   "identifiers": [
149//     {
150//       "type": "dns",
151//       "value": "acmetest.algesten.se"
152//     }
153//   ],
154//   "authorizations": [
155//     "https://example.com/acme/authz/YTqpYUthlVfwBncUufE8IRA2TkzZkN4eYWWLMSRqcSs"
156//   ],
157//   "finalize": "https://example.com/acme/finalize/7738992/18234324"
158// }
159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
160pub struct ApiOrder {
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub status: Option<String>,
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub expires: Option<String>,
165    pub identifiers: Vec<ApiIdentifier>,
166    pub notBefore: Option<String>,
167    pub notAfter: Option<String>,
168    pub error: Option<ApiProblem>,
169    pub authorizations: Option<Vec<String>>,
170    pub finalize: String,
171    pub certificate: Option<String>,
172}
173
174impl ApiOrder {
175    /// As long as there are outstanding authorizations.
176    pub fn is_status_pending(&self) -> bool {
177        self.status.as_ref().map(|s| s.as_ref()) == Some("pending")
178    }
179    /// When all authorizations are finished, and we need to call
180    /// "finalize".
181    pub fn is_status_ready(&self) -> bool {
182        self.status.as_ref().map(|s| s.as_ref()) == Some("ready")
183    }
184    /// On "finalize" the server is processing to sign CSR.
185    pub fn is_status_processing(&self) -> bool {
186        self.status.as_ref().map(|s| s.as_ref()) == Some("processing")
187    }
188    /// Once the certificate is issued and can be downloaded.
189    pub fn is_status_valid(&self) -> bool {
190        self.status.as_ref().map(|s| s.as_ref()) == Some("valid")
191    }
192    /// If the order failed and can't be used again.
193    pub fn is_status_invalid(&self) -> bool {
194        self.status.as_ref().map(|s| s.as_ref()) == Some("invalid")
195    }
196    /// Return all domains
197    pub fn domains(&self) -> Vec<&str> {
198        self.identifiers.iter().map(|i| i.value.as_ref()).collect()
199    }
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
203pub struct ApiIdentifier {
204    #[serde(rename = "type")]
205    pub _type: String,
206    pub value: String,
207}
208
209impl ApiIdentifier {
210    pub fn is_type_dns(&self) -> bool {
211        self._type == "dns"
212    }
213}
214
215// {
216//   "identifier": {
217//     "type": "dns",
218//     "value": "acmetest.algesten.se"
219//   },
220//   "status": "pending",
221//   "expires": "2019-01-09T08:26:43Z",
222//   "challenges": [
223//     {
224//       "type": "http-01",
225//       "status": "pending",
226//       "url": "https://example.com/acme/challenge/YTqpYUthlVfwBncUufE8IRA2TkzZkN4eYWWLMSRqcSs/216789597",
227//       "token": "MUi-gqeOJdRkSb_YR2eaMxQBqf6al8dgt_dOttSWb0w"
228//     },
229//     {
230//       "type": "tls-alpn-01",
231//       "status": "pending",
232//       "url": "https://example.com/acme/challenge/YTqpYUthlVfwBncUufE8IRA2TkzZkN4eYWWLMSRqcSs/216789598",
233//       "token": "WCdRWkCy4THTD_j5IH4ISAzr59lFIg5wzYmKxuOJ1lU"
234//     },
235//     {
236//       "type": "dns-01",
237//       "status": "pending",
238//       "url": "https://example.com/acme/challenge/YTqpYUthlVfwBncUufE8IRA2TkzZkN4eYWWLMSRqcSs/216789599",
239//       "token": "RRo2ZcXAEqxKvMH8RGcATjSK1KknLEUmauwfQ5i3gG8"
240//     }
241//   ]
242// }
243
244// on incorrect challenge, something like:
245//
246//   "challenges": [
247//     {
248//       "type": "dns-01",
249//       "status": "invalid",
250//       "error": {
251//         "type": "urn:ietf:params:acme:error:dns",
252//         "detail": "DNS problem: NXDOMAIN looking up TXT for _acme-challenge.martintest.foobar.com",
253//         "status": 400
254//       },
255//       "url": "https://example.com/acme/challenge/afyChhlFB8GLLmIqEnqqcXzX0Ss3GBw6oUlKAGDG6lY/221695600",
256//       "token": "YsNqBWZnyYjDun3aUC2CkCopOaqZRrI5hp3tUjxPLQU"
257//     },
258// "Incorrect TXT record \"caOh44dp9eqXNRkd0sYrKVF8dBl0L8h8-kFpIBje-2c\" found at _acme-challenge.martintest.foobar.com
259#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
260pub struct ApiAuth {
261    pub identifier: ApiIdentifier,
262    pub status: Option<String>,
263    pub expires: Option<String>,
264    pub challenges: Vec<ApiChallenge>,
265    pub wildcard: Option<bool>,
266}
267
268impl ApiAuth {
269    pub fn is_status_pending(&self) -> bool {
270        self.status.as_ref().map(|s| s.as_ref()) == Some("pending")
271    }
272    pub fn is_status_valid(&self) -> bool {
273        self.status.as_ref().map(|s| s.as_ref()) == Some("valid")
274    }
275    pub fn is_status_invalid(&self) -> bool {
276        self.status.as_ref().map(|s| s.as_ref()) == Some("invalid")
277    }
278    pub fn is_status_deactivated(&self) -> bool {
279        self.status.as_ref().map(|s| s.as_ref()) == Some("deactivated")
280    }
281    pub fn is_status_expired(&self) -> bool {
282        self.status.as_ref().map(|s| s.as_ref()) == Some("expired")
283    }
284    pub fn is_status_revoked(&self) -> bool {
285        self.status.as_ref().map(|s| s.as_ref()) == Some("revoked")
286    }
287    pub fn wildcard(&self) -> bool {
288        self.wildcard.unwrap_or(false)
289    }
290    pub fn http_challenge(&self) -> Option<&ApiChallenge> {
291        self.challenges.iter().find(|c| c._type == "http-01")
292    }
293    pub fn dns_challenge(&self) -> Option<&ApiChallenge> {
294        self.challenges.iter().find(|c| c._type == "dns-01")
295    }
296    pub fn tls_alpn_challenge(&self) -> Option<&ApiChallenge> {
297        self.challenges.iter().find(|c| c._type == "tls-alpn-01")
298    }
299}
300
301#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
302pub struct ApiChallenge {
303    pub url: String,
304    #[serde(rename = "type")]
305    pub _type: String,
306    pub status: String,
307    pub token: String,
308    pub validated: Option<String>,
309    pub error: Option<ApiProblem>,
310}
311
312// {
313//   "type": "http-01",
314//   "status": "pending",
315//   "url": "https://acme-staging-v02.api.letsencrypt.org/acme/challenge/YTqpYUthlVfwBncUufE8IRA2TkzZkN4eYWWLMSRqcSs/216789597",
316//   "token": "MUi-gqeOJdRkSb_YR2eaMxQBqf6al8dgt_dOttSWb0w"
317// }
318impl ApiChallenge {
319    pub fn is_status_pending(&self) -> bool {
320        &self.status == "pending"
321    }
322    pub fn is_status_processing(&self) -> bool {
323        &self.status == "processing"
324    }
325    pub fn is_status_valid(&self) -> bool {
326        &self.status == "valid"
327    }
328    pub fn is_status_invalid(&self) -> bool {
329        &self.status == "invalid"
330    }
331}
332
333#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
334pub struct ApiFinalize {
335    pub csr: String,
336}
337
338#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
339pub struct ApiRevocation {
340    pub certificate: String,
341    pub reason: usize,
342}
343
344#[cfg(test)]
345mod test {
346    use super::*;
347
348    #[test]
349    fn test_api_empty_string() {
350        let x = serde_json::to_string(&ApiEmptyString).unwrap();
351        assert_eq!("\"\"", x);
352    }
353
354    #[test]
355    fn test_api_empty_object() {
356        let x = serde_json::to_string(&ApiEmptyObject).unwrap();
357        assert_eq!("{}", x);
358    }
359}