]> git.proxmox.com Git - proxmox-acme-rs.git/blob - src/client.rs
78c83a20e700635ede536802c2e2d3d0e4ebafdc
[proxmox-acme-rs.git] / src / client.rs
1 //! A blocking higher-level ACME client implementation using 'curl'.
2
3 use std::io::Read;
4 use std::sync::Arc;
5
6 use serde::{Deserialize, Serialize};
7
8 use crate::b64u;
9 use crate::error;
10 use crate::order::OrderData;
11 use crate::request::ErrorResponse;
12 use crate::{Account, Authorization, Challenge, Directory, Error, Order, Request};
13
14 macro_rules! format_err {
15 ($($fmt:tt)*) => { Error::Client(format!($($fmt)*)) };
16 }
17
18 macro_rules! bail {
19 ($($fmt:tt)*) => {{ return Err(format_err!($($fmt)*)); }}
20 }
21
22 /// Low level HTTP response structure.
23 pub struct HttpResponse {
24 /// The raw HTTP response body as a byte vector.
25 pub body: Vec<u8>,
26
27 /// The http status code.
28 pub status: u16,
29
30 /// The headers relevant to the ACME protocol.
31 pub headers: Headers,
32 }
33
34 impl HttpResponse {
35 /// Check the HTTP status code for a success code (200..299).
36 pub fn is_success(&self) -> bool {
37 self.status >= 200 && self.status < 300
38 }
39
40 /// Convenience shortcut to perform json deserialization of the returned body.
41 pub fn json<T: for<'a> Deserialize<'a>>(&self) -> Result<T, Error> {
42 Ok(serde_json::from_slice(&self.body)?)
43 }
44
45 /// Access the raw body as bytes.
46 pub fn bytes(&self) -> &[u8] {
47 &self.body
48 }
49
50 /// Get the returned location header. Borrowing shortcut to `self.headers.location`.
51 pub fn location(&self) -> Option<&str> {
52 self.headers.location.as_deref()
53 }
54
55 /// Convenience helper to assert that a location header was part of the response.
56 pub fn location_required(&mut self) -> Result<String, Error> {
57 self.headers
58 .location
59 .take()
60 .ok_or_else(|| format_err!("missing Location header"))
61 }
62 }
63
64 /// Contains headers from the HTTP response which are relevant parts of the Acme API.
65 ///
66 /// Note that access to the `nonce` header is internal to this crate only, since a nonce will
67 /// always be moved out of the response into the `Client` whenever a new nonce is received.
68 #[derive(Default)]
69 pub struct Headers {
70 /// The 'Location' header usually encodes the URL where an account or order can be queried from
71 /// after they were created.
72 pub location: Option<String>,
73 nonce: Option<String>,
74 }
75
76 struct Inner {
77 agent: Option<ureq::Agent>,
78 nonce: Option<String>,
79 proxy: Option<String>,
80 }
81
82 impl Inner {
83 fn agent(&mut self) -> Result<&mut ureq::Agent, Error> {
84 if self.agent.is_none() {
85 let connector = Arc::new(
86 native_tls::TlsConnector::new()
87 .map_err(|err| format_err!("failed to create tls connector: {}", err))?,
88 );
89
90 let mut builder = ureq::AgentBuilder::new().tls_connector(connector);
91
92 if let Some(proxy) = self.proxy.as_deref() {
93 builder = builder.proxy(
94 ureq::Proxy::new(proxy)
95 .map_err(|err| format_err!("failed to set proxy: {}", err))?,
96 );
97 }
98
99 self.agent = Some(builder.build());
100 }
101
102 Ok(self.agent.as_mut().unwrap())
103 }
104
105 fn new() -> Self {
106 Self {
107 agent: None,
108 nonce: None,
109 proxy: None,
110 }
111 }
112
113 fn execute(
114 &mut self,
115 method: &[u8],
116 url: &str,
117 request_body: Option<(&str, &[u8])>, // content-type and body
118 ) -> Result<HttpResponse, Error> {
119 let agent = self.agent()?;
120 let req = match method {
121 b"POST" => agent.post(url),
122 b"GET" => agent.get(url),
123 b"HEAD" => agent.head(url),
124 other => bail!("invalid http method: {:?}", other),
125 };
126
127 let response = if let Some((content_type, body)) = request_body {
128 req.set("Content-Type", content_type)
129 .set("Content-Length", &body.len().to_string())
130 .send_bytes(body)
131 } else {
132 req.call()
133 }
134 .map_err(|err| format_err!("http request failed: {}", err))?;
135
136 let mut headers = Headers::default();
137 if let Some(value) = response.header(crate::LOCATION) {
138 headers.location = Some(value.to_owned());
139 }
140
141 if let Some(value) = response.header(crate::REPLAY_NONCE) {
142 headers.nonce = Some(value.to_owned());
143 }
144
145 let status = response.status();
146
147 let mut body = Vec::new();
148 response
149 .into_reader()
150 .take(16 * 1024 * 1024) // arbitrary limit
151 .read_to_end(&mut body)
152 .map_err(|err| format_err!("failed to read response body: {}", err))?;
153
154 Ok(HttpResponse {
155 status,
156 headers,
157 body,
158 })
159 }
160
161 pub fn set_proxy(&mut self, proxy: String) {
162 self.proxy = Some(proxy);
163 self.agent = None;
164 }
165
166 /// Low-level API to run an API request. This automatically updates the current nonce!
167 fn run_request(&mut self, request: Request) -> Result<HttpResponse, Error> {
168 let body = if request.body.is_empty() {
169 None
170 } else {
171 Some((request.content_type, request.body.as_bytes()))
172 };
173
174 let mut response = self
175 .execute(request.method.as_bytes(), &request.url, body)
176 .map_err({
177 // borrow fixup:
178 let method = &request.method;
179 let url = &request.url;
180 move |err| format_err!("failed to execute {} request to {}: {}", method, url, err)
181 })?;
182
183 let got_nonce = self.update_nonce(&mut response)?;
184
185 if response.is_success() {
186 if response.status != request.expected {
187 return Err(Error::InvalidApi(format!(
188 "API server responded with unexpected status code: {:?}",
189 response.status
190 )));
191 }
192 return Ok(response);
193 }
194
195 let error: ErrorResponse = response.json().map_err(|err| {
196 format_err!("error status with improper error ACME response: {}", err)
197 })?;
198
199 if error.ty == error::BAD_NONCE {
200 if !got_nonce {
201 return Err(Error::InvalidApi(
202 "badNonce without a new Replay-Nonce header".to_string(),
203 ));
204 }
205 return Err(Error::BadNonce);
206 }
207
208 Err(Error::Api(error))
209 }
210
211 /// If the response contained a nonce, update our nonce and return `true`, otherwise return
212 /// `false`.
213 fn update_nonce(&mut self, response: &mut HttpResponse) -> Result<bool, Error> {
214 match response.headers.nonce.take() {
215 Some(nonce) => {
216 self.nonce = Some(nonce);
217 Ok(true)
218 }
219 None => Ok(false),
220 }
221 }
222
223 /// Update the nonce, if there isn't one it is an error.
224 fn must_update_nonce(&mut self, response: &mut HttpResponse) -> Result<(), Error> {
225 if !self.update_nonce(response)? {
226 bail!("newNonce URL did not return a nonce");
227 }
228 Ok(())
229 }
230
231 /// Update the Nonce.
232 fn new_nonce(&mut self, new_nonce_url: &str) -> Result<(), Error> {
233 let mut response = self.execute(b"HEAD", new_nonce_url, None).map_err(|err| {
234 Error::InvalidApi(format!("failed to get HEAD of newNonce URL: {}", err))
235 })?;
236
237 if !response.is_success() {
238 bail!("HEAD on newNonce URL returned error");
239 }
240
241 self.must_update_nonce(&mut response)?;
242
243 Ok(())
244 }
245
246 /// Make sure a nonce is available without forcing renewal.
247 fn nonce(&mut self, new_nonce_url: &str) -> Result<&str, Error> {
248 if self.nonce.is_none() {
249 self.new_nonce(new_nonce_url)?;
250 }
251 self.nonce
252 .as_deref()
253 .ok_or_else(|| format_err!("failed to get nonce"))
254 }
255 }
256
257 /// A blocking Acme client using curl's `Easy` interface.
258 pub struct Client {
259 inner: Inner,
260 directory: Option<Directory>,
261 account: Option<Account>,
262 directory_url: String,
263 }
264
265 impl Client {
266 /// Create a new Client. This has no account associated with it yet, so the next step is to
267 /// either attach an existing `Account` or create a new one.
268 pub fn new(directory_url: String) -> Self {
269 Self {
270 inner: Inner::new(),
271 directory: None,
272 account: None,
273 directory_url,
274 }
275 }
276
277 /// Get the directory URL without querying the `Directory` structure.
278 ///
279 /// The difference to [`directory`](Client::directory()) is that this does not
280 /// attempt to fetch the directory data from the ACME server.
281 pub fn directory_url(&self) -> &str {
282 &self.directory_url
283 }
284
285 /// Set the account this client should use.
286 pub fn set_account(&mut self, account: Account) {
287 self.account = Some(account);
288 }
289
290 /// Get the Directory information.
291 pub fn directory(&mut self) -> Result<&Directory, Error> {
292 Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)
293 }
294
295 /// Get the Directory information.
296 fn get_directory<'a>(
297 inner: &'_ mut Inner,
298 directory: &'a mut Option<Directory>,
299 directory_url: &str,
300 ) -> Result<&'a Directory, Error> {
301 if let Some(d) = directory {
302 return Ok(d);
303 }
304
305 let response = inner
306 .execute(b"GET", directory_url, None)
307 .map_err(|err| Error::InvalidApi(format!("failed to get directory info: {}", err)))?;
308
309 if !response.is_success() {
310 bail!(
311 "GET on the directory URL returned error status ({})",
312 response.status
313 );
314 }
315
316 *directory = Some(Directory::from_parts(
317 directory_url.to_string(),
318 response.json()?,
319 ));
320 Ok(directory.as_ref().unwrap())
321 }
322
323 /// Get the current account, if there is one.
324 pub fn account(&self) -> Option<&Account> {
325 self.account.as_ref()
326 }
327
328 /// Convenience method to get the ToS URL from the contained `Directory`.
329 ///
330 /// This requires mutable self as the directory information may be lazily loaded, which can
331 /// fail.
332 pub fn terms_of_service_url(&mut self) -> Result<Option<&str>, Error> {
333 Ok(self.directory()?.terms_of_service_url())
334 }
335
336 /// Get a fresh nonce (this should normally not be required as nonces are updated
337 /// automatically, even when a `badNonce` error occurs, which according to the ACME API
338 /// specification should include a new valid nonce in its headers anyway).
339 pub fn new_nonce(&mut self) -> Result<(), Error> {
340 let was_none = self.inner.nonce.is_none();
341 let directory =
342 Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)?;
343 if was_none && self.inner.nonce.is_some() {
344 // this was the first call and we already got a nonce from querying the directory
345 return Ok(());
346 }
347
348 // otherwise actually call up to get a new nonce
349 self.inner.new_nonce(directory.new_nonce_url())
350 }
351
352 /// borrow helper
353 fn nonce<'a>(inner: &'a mut Inner, directory: &'_ Directory) -> Result<&'a str, Error> {
354 inner.nonce(directory.new_nonce_url())
355 }
356
357 /// Convenience method to create a new account with a list of ACME compatible contact strings
358 /// (eg. `mailto:someone@example.com`).
359 ///
360 /// Please remember to persist the returned `Account` structure somewhere to not lose access to
361 /// the account!
362 ///
363 /// If an RSA key size is provided, an RSA key will be generated. Otherwise an EC key using the
364 /// P-256 curve will be generated.
365 pub fn new_account(
366 &mut self,
367 contact: Vec<String>,
368 tos_agreed: bool,
369 rsa_bits: Option<u32>,
370 ) -> Result<&Account, Error> {
371 let account = Account::creator()
372 .set_contacts(contact)
373 .agree_to_tos(tos_agreed);
374 let account = if let Some(bits) = rsa_bits {
375 account.generate_rsa_key(bits)?
376 } else {
377 account.generate_ec_key()?
378 };
379
380 self.register_account(account)
381 }
382
383 /// Register an ACME account.
384 ///
385 /// This uses an [`AccountCreator`](crate::account::AccountCreator) since it may need to build
386 /// the request multiple times in case the we get a `BadNonce` error.
387 pub fn register_account(
388 &mut self,
389 account: crate::account::AccountCreator,
390 ) -> Result<&Account, Error> {
391 let mut retry = retry();
392 let mut response = loop {
393 retry.tick()?;
394
395 let directory =
396 Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)?;
397 let nonce = Self::nonce(&mut self.inner, directory)?;
398 let request = account.request(directory, nonce)?;
399 match self.run_request(request) {
400 Ok(response) => break response,
401 Err(err) if err.is_bad_nonce() => continue,
402 Err(err) => return Err(err),
403 }
404 };
405
406 let account = account.response(response.location_required()?, response.bytes().as_ref())?;
407
408 self.account = Some(account);
409 Ok(self.account.as_ref().unwrap())
410 }
411
412 fn need_account(account: &Option<Account>) -> Result<&Account, Error> {
413 account
414 .as_ref()
415 .ok_or_else(|| format_err!("cannot use client without an account"))
416 }
417
418 /// Update account data.
419 ///
420 /// Low-level version: we allow arbitrary data to be passed to the remote here, it's up to the
421 /// user to know what to do for now.
422 pub fn update_account<T: Serialize>(&mut self, data: &T) -> Result<&Account, Error> {
423 let account = Self::need_account(&self.account)?;
424
425 let mut retry = retry();
426 let response = loop {
427 retry.tick()?;
428 let directory =
429 Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)?;
430 let nonce = Self::nonce(&mut self.inner, directory)?;
431 let request = account.post_request(&account.location, nonce, data)?;
432 let response = match self.inner.run_request(request) {
433 Ok(response) => response,
434 Err(err) if err.is_bad_nonce() => continue,
435 Err(err) => return Err(err),
436 };
437
438 break response;
439 };
440
441 // unwrap: we asserted we have an account at the top of the method!
442 let account = self.account.as_mut().unwrap();
443 account.data = response.json()?;
444 Ok(account)
445 }
446
447 /// Method to create a new order for a set of domains.
448 ///
449 /// Please remember to persist the order somewhere (ideally along with the account data) in
450 /// order to finish & query it later on.
451 pub fn new_order(&mut self, domains: Vec<String>) -> Result<Order, Error> {
452 let account = Self::need_account(&self.account)?;
453
454 let order = domains
455 .into_iter()
456 .fold(OrderData::new(), |order, domain| order.domain(domain));
457
458 let mut retry = retry();
459 loop {
460 retry.tick()?;
461
462 let directory =
463 Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)?;
464 let nonce = Self::nonce(&mut self.inner, directory)?;
465 let mut new_order = account.new_order(&order, directory, nonce)?;
466 let mut response = match self.inner.run_request(new_order.request.take().unwrap()) {
467 Ok(response) => response,
468 Err(err) if err.is_bad_nonce() => continue,
469 Err(err) => return Err(err),
470 };
471
472 return new_order.response(response.location_required()?, response.bytes().as_ref());
473 }
474 }
475
476 /// Assuming the provided URL is an 'Authorization' URL, get and deserialize it.
477 pub fn get_authorization(&mut self, url: &str) -> Result<Authorization, Error> {
478 self.post_as_get(url)?.json()
479 }
480
481 /// Assuming the provided URL is an 'Order' URL, get and deserialize it.
482 pub fn get_order(&mut self, url: &str) -> Result<OrderData, Error> {
483 self.post_as_get(url)?.json()
484 }
485
486 /// Low level "POST-as-GET" request.
487 pub fn post_as_get(&mut self, url: &str) -> Result<HttpResponse, Error> {
488 let account = Self::need_account(&self.account)?;
489
490 let mut retry = retry();
491 loop {
492 retry.tick()?;
493
494 let directory =
495 Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)?;
496 let nonce = Self::nonce(&mut self.inner, directory)?;
497 let request = account.get_request(url, nonce)?;
498 match self.inner.run_request(request) {
499 Ok(response) => return Ok(response),
500 Err(err) if err.is_bad_nonce() => continue,
501 Err(err) => return Err(err),
502 }
503 }
504 }
505
506 /// Low level POST request.
507 pub fn post<T: Serialize>(&mut self, url: &str, data: &T) -> Result<HttpResponse, Error> {
508 let account = Self::need_account(&self.account)?;
509
510 let mut retry = retry();
511 loop {
512 retry.tick()?;
513
514 let directory =
515 Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)?;
516 let nonce = Self::nonce(&mut self.inner, directory)?;
517 let request = account.post_request(url, nonce, data)?;
518 match self.inner.run_request(request) {
519 Ok(response) => return Ok(response),
520 Err(err) if err.is_bad_nonce() => continue,
521 Err(err) => return Err(err),
522 }
523 }
524 }
525
526 /// Request challenge validation. Afterwards, the challenge should be polled.
527 pub fn request_challenge_validation(&mut self, url: &str) -> Result<Challenge, Error> {
528 self.post(url, &serde_json::json!({}))?.json()
529 }
530
531 /// Shortcut to `account().ok_or_else(...).key_authorization()`.
532 pub fn key_authorization(&self, token: &str) -> Result<String, Error> {
533 Self::need_account(&self.account)?.key_authorization(token)
534 }
535
536 /// Shortcut to `account().ok_or_else(...).dns_01_txt_value()`.
537 /// the key authorization value.
538 pub fn dns_01_txt_value(&self, token: &str) -> Result<String, Error> {
539 Self::need_account(&self.account)?.dns_01_txt_value(token)
540 }
541
542 /// Low-level API to run an n API request. This automatically updates the current nonce!
543 pub fn run_request(&mut self, request: Request) -> Result<HttpResponse, Error> {
544 self.inner.run_request(request)
545 }
546
547 /// Finalize an Order via its `finalize` URL property and the DER encoded CSR.
548 pub fn finalize(&mut self, url: &str, csr: &[u8]) -> Result<(), Error> {
549 let csr = b64u::encode(csr);
550 let data = serde_json::json!({ "csr": csr });
551 self.post(url, &data)?;
552 Ok(())
553 }
554
555 /// Download a certificate via its 'certificate' URL property.
556 ///
557 /// The certificate will be a PEM certificate chain.
558 pub fn get_certificate(&mut self, url: &str) -> Result<Vec<u8>, Error> {
559 Ok(self.post_as_get(url)?.body)
560 }
561
562 /// Revoke an existing certificate (PEM or DER formatted).
563 pub fn revoke_certificate(
564 &mut self,
565 certificate: &[u8],
566 reason: Option<u32>,
567 ) -> Result<(), Error> {
568 // TODO: This can also work without an account.
569 let account = Self::need_account(&self.account)?;
570
571 let revocation = account.revoke_certificate(certificate, reason)?;
572
573 let mut retry = retry();
574 loop {
575 retry.tick()?;
576
577 let directory =
578 Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)?;
579 let nonce = Self::nonce(&mut self.inner, directory)?;
580 let request = revocation.request(directory, nonce)?;
581 match self.inner.run_request(request) {
582 Ok(_response) => return Ok(()),
583 Err(err) if err.is_bad_nonce() => continue,
584 Err(err) => return Err(err),
585 }
586 }
587 }
588
589 /// Set a proxy
590 pub fn set_proxy(&mut self, proxy: String) {
591 self.inner.set_proxy(proxy)
592 }
593 }
594
595 /// bad nonce retry count helper
596 struct Retry(usize);
597
598 const fn retry() -> Retry {
599 Retry(0)
600 }
601
602 impl Retry {
603 fn tick(&mut self) -> Result<(), Error> {
604 if self.0 >= 3 {
605 bail!("kept getting a badNonce error!");
606 }
607 self.0 += 1;
608 Ok(())
609 }
610 }