//! A blocking higher-level ACME client implementation using 'curl'. use std::io::Read; use std::sync::Arc; use serde::{Deserialize, Serialize}; use crate::b64u; use crate::error; use crate::order::OrderData; use crate::request::ErrorResponse; use crate::{Account, Authorization, Challenge, Directory, Error, Order, Request}; macro_rules! format_err { ($($fmt:tt)*) => { Error::Client(format!($($fmt)*)) }; } macro_rules! bail { ($($fmt:tt)*) => {{ return Err(format_err!($($fmt)*)); }} } /// Low level HTTP response structure. pub struct HttpResponse { /// The raw HTTP response body as a byte vector. pub body: Vec, /// The http status code. pub status: u16, /// The headers relevant to the ACME protocol. pub headers: Headers, } impl HttpResponse { /// Check the HTTP status code for a success code (200..299). pub fn is_success(&self) -> bool { self.status >= 200 && self.status < 300 } /// Convenience shortcut to perform json deserialization of the returned body. pub fn json Deserialize<'a>>(&self) -> Result { Ok(serde_json::from_slice(&self.body)?) } /// Access the raw body as bytes. pub fn bytes(&self) -> &[u8] { &self.body } /// Get the returned location header. Borrowing shortcut to `self.headers.location`. pub fn location(&self) -> Option<&str> { self.headers.location.as_deref() } /// Convenience helper to assert that a location header was part of the response. pub fn location_required(&mut self) -> Result { self.headers .location .take() .ok_or_else(|| format_err!("missing Location header")) } } /// Contains headers from the HTTP response which are relevant parts of the Acme API. /// /// Note that access to the `nonce` header is internal to this crate only, since a nonce will /// always be moved out of the response into the `Client` whenever a new nonce is received. #[derive(Default)] pub struct Headers { /// The 'Location' header usually encodes the URL where an account or order can be queried from /// after they were created. pub location: Option, nonce: Option, } struct Inner { agent: Option, nonce: Option, proxy: Option, } impl Inner { fn agent(&mut self) -> Result<&mut ureq::Agent, Error> { if self.agent.is_none() { let connector = Arc::new( native_tls::TlsConnector::new() .map_err(|err| format_err!("failed to create tls connector: {}", err))?, ); let mut builder = ureq::AgentBuilder::new().tls_connector(connector); if let Some(proxy) = self.proxy.as_deref() { builder = builder.proxy( ureq::Proxy::new(proxy) .map_err(|err| format_err!("failed to set proxy: {}", err))?, ); } self.agent = Some(builder.build()); } Ok(self.agent.as_mut().unwrap()) } fn new() -> Self { Self { agent: None, nonce: None, proxy: None, } } fn execute( &mut self, method: &[u8], url: &str, request_body: Option<(&str, &[u8])>, // content-type and body ) -> Result { let agent = self.agent()?; let req = match method { b"POST" => agent.post(url), b"GET" => agent.get(url), b"HEAD" => agent.head(url), other => bail!("invalid http method: {:?}", other), }; let response = if let Some((content_type, body)) = request_body { req.set("Content-Type", content_type) .set("Content-Length", &body.len().to_string()) .send_bytes(body) } else { req.call() } .map_err(|err| format_err!("http request failed: {}", err))?; let mut headers = Headers::default(); if let Some(value) = response.header(crate::LOCATION) { headers.location = Some(value.to_owned()); } if let Some(value) = response.header(crate::REPLAY_NONCE) { headers.nonce = Some(value.to_owned()); } let status = response.status(); let mut body = Vec::new(); response .into_reader() .take(16 * 1024 * 1024) // arbitrary limit .read_to_end(&mut body) .map_err(|err| format_err!("failed to read response body: {}", err))?; Ok(HttpResponse { status, headers, body, }) } pub fn set_proxy(&mut self, proxy: String) { self.proxy = Some(proxy); self.agent = None; } /// Low-level API to run an API request. This automatically updates the current nonce! fn run_request(&mut self, request: Request) -> Result { let body = if request.body.is_empty() { None } else { Some((request.content_type, request.body.as_bytes())) }; let mut response = self .execute(request.method.as_bytes(), &request.url, body) .map_err({ // borrow fixup: let method = &request.method; let url = &request.url; move |err| format_err!("failed to execute {} request to {}: {}", method, url, err) })?; let got_nonce = self.update_nonce(&mut response)?; if response.is_success() { if response.status != request.expected { return Err(Error::InvalidApi(format!( "API server responded with unexpected status code: {:?}", response.status ))); } return Ok(response); } let error: ErrorResponse = response.json().map_err(|err| { format_err!("error status with improper error ACME response: {}", err) })?; if error.ty == error::BAD_NONCE { if !got_nonce { return Err(Error::InvalidApi( "badNonce without a new Replay-Nonce header".to_string(), )); } return Err(Error::BadNonce); } Err(Error::Api(error)) } /// If the response contained a nonce, update our nonce and return `true`, otherwise return /// `false`. fn update_nonce(&mut self, response: &mut HttpResponse) -> Result { match response.headers.nonce.take() { Some(nonce) => { self.nonce = Some(nonce); Ok(true) } None => Ok(false), } } /// Update the nonce, if there isn't one it is an error. fn must_update_nonce(&mut self, response: &mut HttpResponse) -> Result<(), Error> { if !self.update_nonce(response)? { bail!("newNonce URL did not return a nonce"); } Ok(()) } /// Update the Nonce. fn new_nonce(&mut self, new_nonce_url: &str) -> Result<(), Error> { let mut response = self.execute(b"HEAD", new_nonce_url, None).map_err(|err| { Error::InvalidApi(format!("failed to get HEAD of newNonce URL: {}", err)) })?; if !response.is_success() { bail!("HEAD on newNonce URL returned error"); } self.must_update_nonce(&mut response)?; Ok(()) } /// Make sure a nonce is available without forcing renewal. fn nonce(&mut self, new_nonce_url: &str) -> Result<&str, Error> { if self.nonce.is_none() { self.new_nonce(new_nonce_url)?; } self.nonce .as_deref() .ok_or_else(|| format_err!("failed to get nonce")) } } /// A blocking Acme client using curl's `Easy` interface. pub struct Client { inner: Inner, directory: Option, account: Option, directory_url: String, } impl Client { /// Create a new Client. This has no account associated with it yet, so the next step is to /// either attach an existing `Account` or create a new one. pub fn new(directory_url: String) -> Self { Self { inner: Inner::new(), directory: None, account: None, directory_url, } } /// Get the directory URL without querying the `Directory` structure. /// /// The difference to [`directory`](Client::directory()) is that this does not /// attempt to fetch the directory data from the ACME server. pub fn directory_url(&self) -> &str { &self.directory_url } /// Set the account this client should use. pub fn set_account(&mut self, account: Account) { self.account = Some(account); } /// Get the Directory information. pub fn directory(&mut self) -> Result<&Directory, Error> { Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url) } /// Get the Directory information. fn get_directory<'a>( inner: &'_ mut Inner, directory: &'a mut Option, directory_url: &str, ) -> Result<&'a Directory, Error> { if let Some(d) = directory { return Ok(d); } let response = inner .execute(b"GET", directory_url, None) .map_err(|err| Error::InvalidApi(format!("failed to get directory info: {}", err)))?; if !response.is_success() { bail!( "GET on the directory URL returned error status ({})", response.status ); } *directory = Some(Directory::from_parts( directory_url.to_string(), response.json()?, )); Ok(directory.as_ref().unwrap()) } /// Get the current account, if there is one. pub fn account(&self) -> Option<&Account> { self.account.as_ref() } /// Convenience method to get the ToS URL from the contained `Directory`. /// /// This requires mutable self as the directory information may be lazily loaded, which can /// fail. pub fn terms_of_service_url(&mut self) -> Result, Error> { Ok(self.directory()?.terms_of_service_url()) } /// Get a fresh nonce (this should normally not be required as nonces are updated /// automatically, even when a `badNonce` error occurs, which according to the ACME API /// specification should include a new valid nonce in its headers anyway). pub fn new_nonce(&mut self) -> Result<(), Error> { let was_none = self.inner.nonce.is_none(); let directory = Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)?; if was_none && self.inner.nonce.is_some() { // this was the first call and we already got a nonce from querying the directory return Ok(()); } // otherwise actually call up to get a new nonce self.inner.new_nonce(directory.new_nonce_url()) } /// borrow helper fn nonce<'a>(inner: &'a mut Inner, directory: &'_ Directory) -> Result<&'a str, Error> { inner.nonce(directory.new_nonce_url()) } /// Convenience method to create a new account with a list of ACME compatible contact strings /// (eg. `mailto:someone@example.com`). /// /// Please remember to persist the returned `Account` structure somewhere to not lose access to /// the account! /// /// If an RSA key size is provided, an RSA key will be generated. Otherwise an EC key using the /// P-256 curve will be generated. pub fn new_account( &mut self, contact: Vec, tos_agreed: bool, rsa_bits: Option, eab_creds: Option<(String, String)>, ) -> Result<&Account, Error> { let mut account = Account::creator() .set_contacts(contact) .agree_to_tos(tos_agreed); if let Some((eab_kid, eab_hmac_key)) = eab_creds { account = account.set_eab_credentials(eab_kid, eab_hmac_key)?; } let account = if let Some(bits) = rsa_bits { account.generate_rsa_key(bits)? } else { account.generate_ec_key()? }; self.register_account(account) } /// Register an ACME account. /// /// This uses an [`AccountCreator`](crate::account::AccountCreator) since it may need to build /// the request multiple times in case the we get a `BadNonce` error. pub fn register_account( &mut self, account: crate::account::AccountCreator, ) -> Result<&Account, Error> { let mut retry = retry(); let mut response = loop { retry.tick()?; let directory = Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)?; let nonce = Self::nonce(&mut self.inner, directory)?; let request = account.request(directory, nonce)?; match self.run_request(request) { Ok(response) => break response, Err(err) if err.is_bad_nonce() => continue, Err(err) => return Err(err), } }; let account = account.response(response.location_required()?, response.bytes().as_ref())?; self.account = Some(account); Ok(self.account.as_ref().unwrap()) } fn need_account(account: &Option) -> Result<&Account, Error> { account .as_ref() .ok_or_else(|| format_err!("cannot use client without an account")) } /// Update account data. /// /// Low-level version: we allow arbitrary data to be passed to the remote here, it's up to the /// user to know what to do for now. pub fn update_account(&mut self, data: &T) -> Result<&Account, Error> { let account = Self::need_account(&self.account)?; let mut retry = retry(); let response = loop { retry.tick()?; let directory = Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)?; let nonce = Self::nonce(&mut self.inner, directory)?; let request = account.post_request(&account.location, nonce, data)?; let response = match self.inner.run_request(request) { Ok(response) => response, Err(err) if err.is_bad_nonce() => continue, Err(err) => return Err(err), }; break response; }; // unwrap: we asserted we have an account at the top of the method! let account = self.account.as_mut().unwrap(); account.data = response.json()?; Ok(account) } /// Method to create a new order for a set of domains. /// /// Please remember to persist the order somewhere (ideally along with the account data) in /// order to finish & query it later on. pub fn new_order(&mut self, domains: Vec) -> Result { let account = Self::need_account(&self.account)?; let order = domains .into_iter() .fold(OrderData::new(), |order, domain| order.domain(domain)); let mut retry = retry(); loop { retry.tick()?; let directory = Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)?; let nonce = Self::nonce(&mut self.inner, directory)?; let mut new_order = account.new_order(&order, directory, nonce)?; let mut response = match self.inner.run_request(new_order.request.take().unwrap()) { Ok(response) => response, Err(err) if err.is_bad_nonce() => continue, Err(err) => return Err(err), }; return new_order.response(response.location_required()?, response.bytes().as_ref()); } } /// Assuming the provided URL is an 'Authorization' URL, get and deserialize it. pub fn get_authorization(&mut self, url: &str) -> Result { self.post_as_get(url)?.json() } /// Assuming the provided URL is an 'Order' URL, get and deserialize it. pub fn get_order(&mut self, url: &str) -> Result { self.post_as_get(url)?.json() } /// Low level "POST-as-GET" request. pub fn post_as_get(&mut self, url: &str) -> Result { let account = Self::need_account(&self.account)?; let mut retry = retry(); loop { retry.tick()?; let directory = Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)?; let nonce = Self::nonce(&mut self.inner, directory)?; let request = account.get_request(url, nonce)?; match self.inner.run_request(request) { Ok(response) => return Ok(response), Err(err) if err.is_bad_nonce() => continue, Err(err) => return Err(err), } } } /// Low level POST request. pub fn post(&mut self, url: &str, data: &T) -> Result { let account = Self::need_account(&self.account)?; let mut retry = retry(); loop { retry.tick()?; let directory = Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)?; let nonce = Self::nonce(&mut self.inner, directory)?; let request = account.post_request(url, nonce, data)?; match self.inner.run_request(request) { Ok(response) => return Ok(response), Err(err) if err.is_bad_nonce() => continue, Err(err) => return Err(err), } } } /// Request challenge validation. Afterwards, the challenge should be polled. pub fn request_challenge_validation(&mut self, url: &str) -> Result { self.post(url, &serde_json::json!({}))?.json() } /// Shortcut to `account().ok_or_else(...).key_authorization()`. pub fn key_authorization(&self, token: &str) -> Result { Self::need_account(&self.account)?.key_authorization(token) } /// Shortcut to `account().ok_or_else(...).dns_01_txt_value()`. /// the key authorization value. pub fn dns_01_txt_value(&self, token: &str) -> Result { Self::need_account(&self.account)?.dns_01_txt_value(token) } /// Low-level API to run an n API request. This automatically updates the current nonce! pub fn run_request(&mut self, request: Request) -> Result { self.inner.run_request(request) } /// Finalize an Order via its `finalize` URL property and the DER encoded CSR. pub fn finalize(&mut self, url: &str, csr: &[u8]) -> Result<(), Error> { let csr = b64u::encode(csr); let data = serde_json::json!({ "csr": csr }); self.post(url, &data)?; Ok(()) } /// Download a certificate via its 'certificate' URL property. /// /// The certificate will be a PEM certificate chain. pub fn get_certificate(&mut self, url: &str) -> Result, Error> { Ok(self.post_as_get(url)?.body) } /// Revoke an existing certificate (PEM or DER formatted). pub fn revoke_certificate( &mut self, certificate: &[u8], reason: Option, ) -> Result<(), Error> { // TODO: This can also work without an account. let account = Self::need_account(&self.account)?; let revocation = account.revoke_certificate(certificate, reason)?; let mut retry = retry(); loop { retry.tick()?; let directory = Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)?; let nonce = Self::nonce(&mut self.inner, directory)?; let request = revocation.request(directory, nonce)?; match self.inner.run_request(request) { Ok(_response) => return Ok(()), Err(err) if err.is_bad_nonce() => continue, Err(err) => return Err(err), } } } /// Set a proxy pub fn set_proxy(&mut self, proxy: String) { self.inner.set_proxy(proxy) } } /// bad nonce retry count helper struct Retry(usize); const fn retry() -> Retry { Retry(0) } impl Retry { fn tick(&mut self) -> Result<(), Error> { if self.0 >= 3 { bail!("kept getting a badNonce error!"); } self.0 += 1; Ok(()) } }