From 2c7b716584a0d2cd9d0fac89d70890bc506d63c1 Mon Sep 17 00:00:00 2001 From: Rodolphe Breard Date: Wed, 24 Apr 2019 19:22:29 +0200 Subject: [PATCH] Retry request rejected with a recoverable error Some errors, like the badNonce one, are recoverable. Hence, the client is expected to retry. ACMEd will now re-send the associated request until it succeed or the max retries number is reached. Each retry is preceded by a small waiting time in order to let the server recover in case it was faulty. --- CHANGELOG.md | 1 + README.md | 1 + acmed/src/acme_proto.rs | 124 ++++++-------------- acmed/src/acme_proto/account.rs | 7 +- acmed/src/acme_proto/http.rs | 198 ++++++++++++++++++++++++-------- acmed/src/acme_proto/structs.rs | 1 + acmed/src/error.rs | 157 +++++++++++++++++++++++++ acmed/src/main.rs | 4 +- 8 files changed, 355 insertions(+), 138 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98e8ca2..737edbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - An account object has been added in the configuration. +- Failure recovery: HTTPS requests rejected by the server that are recoverable, like the badNonce error, are now retried several times before being considered a hard failure. ### Changed - In the configuration, the `email` certificate field has been replaced by the `account` field which matches an account object. diff --git a/README.md b/README.md index 6a358d5..5dabfea 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ The Automatic Certificate Management Environment (ACME), is an internet standard - Fully customizable archiving method (yes, you can use git or anything else) - Run as a deamon: no need to set-up timers, crontab or other time-triggered process - Nice and simple configuration file +- Retry HTTPS request rejected with a badNonce or other recoverable errors ## Planned features diff --git a/acmed/src/acme_proto.rs b/acmed/src/acme_proto.rs index d03df6b..a5f7b2e 100644 --- a/acmed/src/acme_proto.rs +++ b/acmed/src/acme_proto.rs @@ -7,13 +7,13 @@ use crate::certificate::Certificate; use crate::error::Error; use crate::storage; use log::info; -use std::{fmt, thread, time}; +use std::fmt; mod account; mod certificate; mod http; pub mod jws; -mod structs; +pub mod structs; #[derive(Clone, Debug, PartialEq)] pub enum Challenge { @@ -51,29 +51,15 @@ impl PartialEq for Challenge { } } -fn pool( - account: &AccountManager, - url: &str, - nonce: &str, - get_fn: F, - break_fn: G, -) -> Result<(T, String), Error> -where - F: Fn(&str, &[u8]) -> Result<(T, String), Error>, - G: Fn(&T) -> bool, -{ - let mut nonce: String = nonce.to_string(); - for _ in 0..crate::DEFAULT_POOL_NB_TRIES { - thread::sleep(time::Duration::from_secs(crate::DEFAULT_POOL_WAIT_SEC)); - let data = encode_kid(&account.priv_key, &account.account_url, b"", url, &nonce)?; - let (obj, new_nonce) = get_fn(url, data.as_bytes())?; - if break_fn(&obj) { - return Ok((obj, new_nonce)); - } - nonce = new_nonce; - } - let msg = format!("Pooling failed for {}", url); - Err(msg.into()) +macro_rules! set_data_builder { + ($account: ident, $data: expr, $url: expr) => { + |n: &str| encode_kid(&$account.priv_key, &$account.account_url, $data, &$url, n) + }; +} +macro_rules! set_empty_data_builder { + ($account: ident, $url: expr) => { + set_data_builder!($account, b"", $url) + }; } pub fn request_certificate(cert: &Certificate) -> Result<(), Error> { @@ -89,26 +75,15 @@ pub fn request_certificate(cert: &Certificate) -> Result<(), Error> { // 4. Create a new order let new_order = NewOrder::new(&cert.domains); let new_order = serde_json::to_string(&new_order)?; - let new_order = encode_kid( - &account.priv_key, - &account.account_url, - new_order.as_bytes(), - &directory.new_order, - &nonce, - )?; - let (order, order_url, mut nonce) = - http::get_obj_loc::(&directory.new_order, new_order.as_bytes())?; + let data_builder = set_data_builder!(account, new_order.as_bytes(), directory.new_order); + let (order, order_url, mut nonce): (Order, String, String) = + http::get_obj_loc(&directory.new_order, &data_builder, &nonce)?; // 5. Get all the required authorizations for auth_url in order.authorizations.iter() { - let auth_data = encode_kid( - &account.priv_key, - &account.account_url, - b"", - &auth_url, - &nonce, - )?; - let (auth, new_nonce) = http::get_obj::(&auth_url, auth_data.as_bytes())?; + let data_builder = set_empty_data_builder!(account, auth_url); + let (auth, new_nonce): (Authorization, String) = + http::get_obj(&auth_url, &data_builder, &nonce)?; nonce = new_nonce; if auth.status == AuthorizationStatus::Valid { @@ -134,73 +109,44 @@ pub fn request_certificate(cert: &Certificate) -> Result<(), Error> { // 8. Tell the server the challenge has been completed let chall_url = challenge.get_url(); - let chall_resp_data = encode_kid( - &account.priv_key, - &account.account_url, - b"{}", - &chall_url, - &nonce, - )?; - let new_nonce = - http::post_challenge_response(&chall_url, chall_resp_data.as_bytes())?; + let data_builder = set_data_builder!(account, b"{}", chall_url); + let new_nonce = http::post_challenge_response(&chall_url, &data_builder, &nonce)?; nonce = new_nonce; } } // 9. Pool the authorization in order to see whether or not it is valid - let (_, new_nonce) = pool( - &account, - &auth_url, - &nonce, - |u, d| http::get_obj::(u, d), - |a| a.status == AuthorizationStatus::Valid, - )?; + let data_builder = set_empty_data_builder!(account, auth_url); + let break_fn = |a: &Authorization| a.status == AuthorizationStatus::Valid; + let (_, new_nonce): (Authorization, String) = + http::pool_obj(&auth_url, &data_builder, &break_fn, &nonce)?; nonce = new_nonce; } // 10. Pool the order in order to see whether or not it is ready - let (order, nonce) = pool( - &account, - &order_url, - &nonce, - |u, d| http::get_obj::(u, d), - |a| a.status == OrderStatus::Ready, - )?; + let data_builder = set_empty_data_builder!(account, order_url); + let break_fn = |o: &Order| o.status == OrderStatus::Ready; + let (order, nonce): (Order, String) = + http::pool_obj(&order_url, &data_builder, &break_fn, &nonce)?; // 11. Finalize the order by sending the CSR let (priv_key, pub_key) = certificate::get_key_pair(cert)?; let csr = certificate::generate_csr(cert, &priv_key, &pub_key)?; - let csr_data = encode_kid( - &account.priv_key, - &account.account_url, - csr.as_bytes(), - &order.finalize, - &nonce, - )?; - let (_, nonce) = http::get_obj::(&order.finalize, &csr_data.as_bytes())?; + let data_builder = set_data_builder!(account, csr.as_bytes(), order.finalize); + let (_, nonce): (Order, String) = http::get_obj(&order.finalize, &data_builder, &nonce)?; // 12. Pool the order in order to see whether or not it is valid - let (order, nonce) = pool( - &account, - &order_url, - &nonce, - |u, d| http::get_obj::(u, d), - |a| a.status == OrderStatus::Valid, - )?; + let data_builder = set_empty_data_builder!(account, order_url); + let break_fn = |o: &Order| o.status == OrderStatus::Valid; + let (order, nonce): (Order, String) = + http::pool_obj(&order_url, &data_builder, &break_fn, &nonce)?; // 13. Download the certificate - // TODO: implement let crt_url = order .certificate .ok_or_else(|| Error::from("No certificate available for download."))?; - let crt_data = encode_kid( - &account.priv_key, - &account.account_url, - b"", - &crt_url, - &nonce, - )?; - let (crt, _) = http::get_certificate(&crt_url, &crt_data.as_bytes())?; + let data_builder = set_empty_data_builder!(account, crt_url); + let (crt, _) = http::get_certificate(&crt_url, &data_builder, &nonce)?; storage::write_certificate(cert, &crt.as_bytes())?; info!("Certificate renewed for {}", cert.domains.join(", ")); diff --git a/acmed/src/acme_proto/account.rs b/acmed/src/acme_proto/account.rs index a6792da..1799268 100644 --- a/acmed/src/acme_proto/account.rs +++ b/acmed/src/acme_proto/account.rs @@ -39,9 +39,10 @@ impl AccountManager { }; let account = Account::new(cert); let account = serde_json::to_string(&account)?; - let data = encode_jwk(&priv_key, account.as_bytes(), &directory.new_account, nonce)?; - let (acc_rep, account_url, nonce) = - http::get_obj_loc::(&directory.new_account, data.as_bytes())?; + let data_builder = + |n: &str| encode_jwk(&priv_key, account.as_bytes(), &directory.new_account, n); + let (acc_rep, account_url, nonce): (AccountResponse, String, String) = + http::get_obj_loc(&directory.new_account, &data_builder, &nonce)?; let ac = AccountManager { priv_key, pub_key, diff --git a/acmed/src/acme_proto/http.rs b/acmed/src/acme_proto/http.rs index 05acb8a..6ccc889 100644 --- a/acmed/src/acme_proto/http.rs +++ b/acmed/src/acme_proto/http.rs @@ -1,14 +1,29 @@ use crate::acme_proto::structs::Directory; -use crate::error::Error; +use crate::error::{AcmeError, Error, HttpApiError}; use http_req::request::{Method, Request}; use http_req::response::Response; use http_req::uri::Uri; -use log::{debug, trace}; +use log::{debug, trace, warn}; use std::str::FromStr; +use std::{thread, time}; const CONTENT_TYPE_JOSE: &str = "application/jose+json"; const CONTENT_TYPE_JSON: &str = "application/json"; +struct DummyString { + pub content: String, +} + +impl FromStr for DummyString { + type Err = Error; + + fn from_str(data: &str) -> Result { + Ok(DummyString { + content: data.to_string(), + }) + } +} + fn new_request(uri: &Uri, method: Method) -> Request { debug!("{}: {}", method, uri); let useragent = format!( @@ -30,17 +45,27 @@ fn send_request(request: &Request) -> Result<(Response, String), Error> { let mut buffer = Vec::new(); let res = request.send(&mut buffer)?; let res_str = String::from_utf8(buffer)?; - if !res.status_code().is_success() { - debug!("Response: {}", res_str); - let msg = format!("HTTP error: {}: {}", res.status_code(), res.reason()); - return Err(msg.into()); - } Ok((res, res_str)) } -fn check_response(_res: &Response) -> Result<(), Error> { - // TODO: implement - Ok(()) +fn send_request_retry(request: &Request) -> Result<(Response, String), Error> { + for _ in 0..crate::DEFAULT_HTTP_FAIL_NB_RETRY { + let (res, res_body) = send_request(request)?; + match check_response(&res, &res_body) { + Ok(()) => { + return Ok((res, res_body)); + } + Err(e) => { + if !e.is_recoverable() { + let msg = format!("HTTP error: {}: {}", res.status_code(), res.reason()); + return Err(msg.into()); + } + warn!("{}", e); + } + }; + thread::sleep(time::Duration::from_secs(crate::DEFAULT_HTTP_FAIL_WAIT_SEC)); + } + Err("Too much errors, will not retry".into()) } fn get_header(res: &Response, name: &str) -> Result { @@ -76,65 +101,148 @@ fn post_jose_type(url: &str, data: &[u8], accept_type: &str) -> Result<(Response request.header("Accept", accept_type); request.body(data); let rstr = String::from_utf8_lossy(data); - trace!("post_jose: request body: {}", rstr); + trace!("request body: {}", rstr); let (res, res_body) = send_request(&request)?; - trace!("post_jose: response body: {}", res_body); - check_response(&res)?; + trace!("response body: {}", res_body); Ok((res, res_body)) } -fn post_jose(url: &str, data: &[u8]) -> Result<(Response, String), Error> { - post_jose_type(url, data, CONTENT_TYPE_JSON) +fn check_response(res: &Response, body: &str) -> Result<(), AcmeError> { + if res.status_code().is_success() { + Ok(()) + } else { + Err(HttpApiError::from_str(body)?.get_acme_type()) + } } -pub fn get_directory(url: &str) -> Result { - let uri = url.parse::()?; - let mut request = new_request(&uri, Method::GET); - request.header("Accept", CONTENT_TYPE_JSON); - let (r, s) = send_request(&request)?; - check_response(&r)?; - Directory::from_str(&s) +fn fetch_obj_type( + url: &str, + data_builder: &G, + nonce: &str, + accept_type: &str, +) -> Result<(T, String, String), Error> +where + T: std::str::FromStr, + G: Fn(&str) -> Result, +{ + let mut nonce = nonce.to_string(); + for _ in 0..crate::DEFAULT_HTTP_FAIL_NB_RETRY { + let data = data_builder(&nonce)?; + let (res, res_body) = post_jose_type(url, data.as_bytes(), accept_type)?; + nonce = nonce_from_response(&res)?; + + match check_response(&res, &res_body) { + Ok(()) => { + let obj = T::from_str(&res_body)?; + let location = get_header(&res, "Location").unwrap_or_else(|_| String::new()); + return Ok((obj, location, nonce)); + } + Err(e) => { + if !e.is_recoverable() { + let msg = format!("HTTP error: {}: {}", res.status_code(), res.reason()); + return Err(msg.into()); + } + warn!("{}", e); + } + }; + thread::sleep(time::Duration::from_secs(crate::DEFAULT_HTTP_FAIL_WAIT_SEC)); + } + Err("Too much errors, will not retry".into()) } -pub fn get_nonce(url: &str) -> Result { - let uri = url.parse::()?; - let request = new_request(&uri, Method::HEAD); - let (res, _) = send_request(&request)?; - check_response(&res)?; - nonce_from_response(&res) +fn fetch_obj(url: &str, data_builder: &G, nonce: &str) -> Result<(T, String, String), Error> +where + T: std::str::FromStr, + G: Fn(&str) -> Result, +{ + fetch_obj_type(url, data_builder, nonce, CONTENT_TYPE_JSON) } -pub fn get_obj(url: &str, data: &[u8]) -> Result<(T, String), Error> +pub fn get_obj_loc( + url: &str, + data_builder: &G, + nonce: &str, +) -> Result<(T, String, String), Error> where T: std::str::FromStr, + G: Fn(&str) -> Result, { - let (res, res_body) = post_jose(url, data)?; - let obj = T::from_str(&res_body)?; - let nonce = nonce_from_response(&res)?; + let (obj, location, nonce) = fetch_obj(url, data_builder, nonce)?; + if location.is_empty() { + Err("Location header not found.".into()) + } else { + Ok((obj, location, nonce)) + } +} + +pub fn get_obj(url: &str, data_builder: &G, nonce: &str) -> Result<(T, String), Error> +where + T: std::str::FromStr, + G: Fn(&str) -> Result, +{ + let (obj, _, nonce) = fetch_obj(url, data_builder, nonce)?; Ok((obj, nonce)) } -pub fn get_obj_loc(url: &str, data: &[u8]) -> Result<(T, String, String), Error> +pub fn pool_obj( + url: &str, + data_builder: &G, + break_fn: &S, + nonce: &str, +) -> Result<(T, String), Error> where T: std::str::FromStr, + G: Fn(&str) -> Result, + S: Fn(&T) -> bool, { - let (res, res_body) = post_jose(url, data)?; - let obj = T::from_str(&res_body)?; - let location = get_header(&res, "Location")?; - let nonce = nonce_from_response(&res)?; - Ok((obj, location, nonce)) + let mut nonce: String = nonce.to_string(); + for _ in 0..crate::DEFAULT_POOL_NB_TRIES { + thread::sleep(time::Duration::from_secs(crate::DEFAULT_POOL_WAIT_SEC)); + let (obj, _, new_nonce) = fetch_obj(url, data_builder, &nonce)?; + if break_fn(&obj) { + return Ok((obj, new_nonce)); + } + nonce = new_nonce; + } + let msg = format!("Pooling failed for {}", url); + Err(msg.into()) } -pub fn post_challenge_response(url: &str, data: &[u8]) -> Result { - let (res, _) = post_jose(url, data)?; - let nonce = nonce_from_response(&res)?; +pub fn post_challenge_response(url: &str, data_builder: &G, nonce: &str) -> Result +where + G: Fn(&str) -> Result, +{ + let (_, _, nonce): (DummyString, String, String) = fetch_obj(url, data_builder, nonce)?; Ok(nonce) } -pub fn get_certificate(url: &str, data: &[u8]) -> Result<(String, String), Error> { - let (res, res_body) = post_jose_type(url, data, CONTENT_TYPE_JSON)?; - let nonce = nonce_from_response(&res)?; - Ok((res_body, nonce)) +pub fn get_certificate( + url: &str, + data_builder: &G, + nonce: &str, +) -> Result<(String, String), Error> +where + G: Fn(&str) -> Result, +{ + let (res_body, _, nonce): (DummyString, String, String) = fetch_obj(url, data_builder, nonce)?; + Ok((res_body.content, nonce)) +} + +pub fn get_directory(url: &str) -> Result { + let uri = url.parse::()?; + let mut request = new_request(&uri, Method::GET); + request.header("Accept", CONTENT_TYPE_JSON); + let (r, s) = send_request_retry(&request)?; + check_response(&r, &s)?; + Directory::from_str(&s) +} + +pub fn get_nonce(url: &str) -> Result { + let uri = url.parse::()?; + let request = new_request(&uri, Method::HEAD); + let (res, res_body) = send_request_retry(&request)?; + check_response(&res, &res_body)?; + nonce_from_response(&res) } #[cfg(test)] diff --git a/acmed/src/acme_proto/structs.rs b/acmed/src/acme_proto/structs.rs index 4f132dc..df2488d 100644 --- a/acmed/src/acme_proto/structs.rs +++ b/acmed/src/acme_proto/structs.rs @@ -19,5 +19,6 @@ mod order; pub use account::{Account, AccountDeactivation, AccountResponse, AccountUpdate}; pub use authorization::{Authorization, AuthorizationStatus, Challenge}; +pub use deserialize_from_str; pub use directory::Directory; pub use order::{Identifier, IdentifierType, NewOrder, Order, OrderStatus}; diff --git a/acmed/src/error.rs b/acmed/src/error.rs index 3d8c355..91fe5e1 100644 --- a/acmed/src/error.rs +++ b/acmed/src/error.rs @@ -1,4 +1,6 @@ +use serde::Deserialize; use std::fmt; +use std::str::FromStr; #[derive(Debug)] pub struct Error { @@ -31,6 +33,12 @@ impl From<&String> for Error { } } +impl From for Error { + fn from(error: AcmeError) -> Self { + error.to_string().into() + } +} + impl From for Error { fn from(error: std::io::Error) -> Self { format!("IO error: {}", error).into() @@ -85,3 +93,152 @@ impl From for Error { format!("{}", error).into() } } + +#[derive(PartialEq)] +pub enum AcmeError { + AccountDoesNotExist, + AlreadyRevoked, + BadCSR, + BadNonce, + BadPublicKey, + BadRevocationReason, + BadSignatureAlgorithm, + Caa, + Compound, + Connection, + Dns, + ExternalAccountRequired, + IncorrectResponse, + InvalidContact, + Malformed, + OrderNotReady, + RateLimited, + RejectedIdentifier, + ServerInternal, + Tls, + Unauthorized, + UnsupportedContact, + UnsupportedIdentifier, + UserActionRequired, + Unknown, +} + +impl From for AcmeError { + fn from(error: String) -> Self { + match error.as_str() { + "urn:ietf:params:acme:error:accountDoesNotExist" => AcmeError::AccountDoesNotExist, + "urn:ietf:params:acme:error:alreadyRevoked" => AcmeError::AlreadyRevoked, + "urn:ietf:params:acme:error:badCSR" => AcmeError::BadCSR, + "urn:ietf:params:acme:error:badNonce" => AcmeError::BadNonce, + "urn:ietf:params:acme:error:badPublicKey" => AcmeError::BadPublicKey, + "urn:ietf:params:acme:error:badRevocationReason" => AcmeError::BadRevocationReason, + "urn:ietf:params:acme:error:badSignatureAlgorithm" => AcmeError::BadSignatureAlgorithm, + "urn:ietf:params:acme:error:caa" => AcmeError::Caa, + "urn:ietf:params:acme:error:compound" => AcmeError::Compound, + "urn:ietf:params:acme:error:connection" => AcmeError::Connection, + "urn:ietf:params:acme:error:dns" => AcmeError::Dns, + "urn:ietf:params:acme:error:externalAccountRequired" => { + AcmeError::ExternalAccountRequired + } + "urn:ietf:params:acme:error:incorrectResponse" => AcmeError::IncorrectResponse, + "urn:ietf:params:acme:error:invalidContact" => AcmeError::InvalidContact, + "urn:ietf:params:acme:error:malformed" => AcmeError::Malformed, + "urn:ietf:params:acme:error:orderNotReady" => AcmeError::OrderNotReady, + "urn:ietf:params:acme:error:rateLimited" => AcmeError::RateLimited, + "urn:ietf:params:acme:error:rejectedIdentifier" => AcmeError::RejectedIdentifier, + "urn:ietf:params:acme:error:serverInternal" => AcmeError::ServerInternal, + "urn:ietf:params:acme:error:tls" => AcmeError::Tls, + "urn:ietf:params:acme:error:unauthorized" => AcmeError::Unauthorized, + "urn:ietf:params:acme:error:unsupportedContact" => AcmeError::UnsupportedContact, + "urn:ietf:params:acme:error:unsupportedIdentifier" => AcmeError::UnsupportedIdentifier, + "urn:ietf:params:acme:error:userActionRequired" => AcmeError::UserActionRequired, + _ => AcmeError::Unknown, + } + } +} + +impl fmt::Display for AcmeError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let msg = match self { + AcmeError::AccountDoesNotExist => "The request specified an account that does not exist", + AcmeError::AlreadyRevoked => "The request specified a certificate to be revoked that has already been revoked", + AcmeError::BadCSR => "The CSR is unacceptable (e.g., due to a short key)", + AcmeError::BadNonce => "The client sent an unacceptable anti-replay nonce", + AcmeError::BadPublicKey => "The JWS was signed by a public key the server does not support", + AcmeError::BadRevocationReason => "The revocation reason provided is not allowed by the server", + AcmeError::BadSignatureAlgorithm => "The JWS was signed with an algorithm the server does not support", + AcmeError::Caa => "Certification Authority Authorization (CAA) records forbid the CA from issuing a certificate", + AcmeError::Compound => "Specific error conditions are indicated in the \"subproblems\" array", + AcmeError::Connection => "The server could not connect to validation target", + AcmeError::Dns => "There was a problem with a DNS query during identifier validation", + AcmeError::ExternalAccountRequired => "The request must include a value for the \"externalAccountBinding\" field", + AcmeError::IncorrectResponse => "Response received didn't match the challenge's requirements", + AcmeError::InvalidContact => "A contact URL for an account was invalid", + AcmeError::Malformed => "The request message was malformed", + AcmeError::OrderNotReady => "The request attempted to finalize an order that is not ready to be finalized", + AcmeError::RateLimited => "The request exceeds a rate limit", + AcmeError::RejectedIdentifier => "The server will not issue certificates for the identifier", + AcmeError::ServerInternal => "The server experienced an internal error", + AcmeError::Tls => "The server received a TLS error during validation", + AcmeError::Unauthorized => "The client lacks sufficient authorization", + AcmeError::UnsupportedContact => "A contact URL for an account used an unsupported protocol scheme", + AcmeError::UnsupportedIdentifier => "An identifier is of an unsupported type", + AcmeError::UserActionRequired => "Visit the \"instance\" URL and take actions specified there", + AcmeError::Unknown => "Unknown error", + }; + write!(f, "{}", msg) + } +} + +impl AcmeError { + pub fn is_recoverable(&self) -> bool { + *self == AcmeError::BadNonce + || *self == AcmeError::Connection + || *self == AcmeError::Dns + || *self == AcmeError::Malformed + || *self == AcmeError::RateLimited + || *self == AcmeError::ServerInternal + || *self == AcmeError::Tls + } +} + +impl From for AcmeError { + fn from(_error: Error) -> Self { + AcmeError::Unknown + } +} + +#[derive(Deserialize)] +pub struct HttpApiError { + #[serde(rename = "type")] + error_type: Option, + // title: Option, + // status: Option, + detail: Option, + // instance: Option, + // TODO: implement subproblems +} + +crate::acme_proto::structs::deserialize_from_str!(HttpApiError); + +impl fmt::Display for HttpApiError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let msg = self + .detail + .to_owned() + .unwrap_or_else(|| self.get_acme_type().to_string()); + write!(f, "{}", msg) + } +} + +impl HttpApiError { + pub fn get_type(&self) -> String { + self.error_type + .to_owned() + .unwrap_or_else(|| String::from("about:blank")) + } + + pub fn get_acme_type(&self) -> AcmeError { + self.get_type().into() + } +} diff --git a/acmed/src/main.rs b/acmed/src/main.rs index bf9751b..11af373 100644 --- a/acmed/src/main.rs +++ b/acmed/src/main.rs @@ -30,8 +30,10 @@ pub const DEFAULT_KP_REUSE: bool = false; pub const DEFAULT_LOG_SYSTEM: logs::LogSystem = logs::LogSystem::SysLog; pub const DEFAULT_LOG_LEVEL: LevelFilter = LevelFilter::Warn; pub const DEFAULT_JWS_SIGN_ALGO: &str = "ES256"; -pub const DEFAULT_POOL_NB_TRIES: usize = 10; +pub const DEFAULT_POOL_NB_TRIES: usize = 20; pub const DEFAULT_POOL_WAIT_SEC: u64 = 5; +pub const DEFAULT_HTTP_FAIL_NB_RETRY: usize = 10; +pub const DEFAULT_HTTP_FAIL_WAIT_SEC: u64 = 1; fn main() { let matches = App::new(APP_NAME)