Browse Source

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.
pull/5/head
Rodolphe Breard 6 years ago
parent
commit
2c7b716584
  1. 1
      CHANGELOG.md
  2. 1
      README.md
  3. 124
      acmed/src/acme_proto.rs
  4. 7
      acmed/src/acme_proto/account.rs
  5. 198
      acmed/src/acme_proto/http.rs
  6. 1
      acmed/src/acme_proto/structs.rs
  7. 157
      acmed/src/error.rs
  8. 4
      acmed/src/main.rs

1
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.

1
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

124
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<structs::Challenge> for Challenge {
}
}
fn pool<T, F, G>(
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::<Order>(&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::<Authorization>(&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::<Authorization>(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::<Order>(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>(&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::<Order>(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(", "));

7
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::<AccountResponse>(&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,

198
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<Self, Self::Err> {
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<String, Error> {
@ -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<Directory, Error> {
let uri = url.parse::<Uri>()?;
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<T, G>(
url: &str,
data_builder: &G,
nonce: &str,
accept_type: &str,
) -> Result<(T, String, String), Error>
where
T: std::str::FromStr<Err = Error>,
G: Fn(&str) -> Result<String, Error>,
{
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<String, Error> {
let uri = url.parse::<Uri>()?;
let request = new_request(&uri, Method::HEAD);
let (res, _) = send_request(&request)?;
check_response(&res)?;
nonce_from_response(&res)
fn fetch_obj<T, G>(url: &str, data_builder: &G, nonce: &str) -> Result<(T, String, String), Error>
where
T: std::str::FromStr<Err = Error>,
G: Fn(&str) -> Result<String, Error>,
{
fetch_obj_type(url, data_builder, nonce, CONTENT_TYPE_JSON)
}
pub fn get_obj<T>(url: &str, data: &[u8]) -> Result<(T, String), Error>
pub fn get_obj_loc<T, G>(
url: &str,
data_builder: &G,
nonce: &str,
) -> Result<(T, String, String), Error>
where
T: std::str::FromStr<Err = Error>,
G: Fn(&str) -> Result<String, Error>,
{
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<T, G>(url: &str, data_builder: &G, nonce: &str) -> Result<(T, String), Error>
where
T: std::str::FromStr<Err = Error>,
G: Fn(&str) -> Result<String, Error>,
{
let (obj, _, nonce) = fetch_obj(url, data_builder, nonce)?;
Ok((obj, nonce))
}
pub fn get_obj_loc<T>(url: &str, data: &[u8]) -> Result<(T, String, String), Error>
pub fn pool_obj<T, G, S>(
url: &str,
data_builder: &G,
break_fn: &S,
nonce: &str,
) -> Result<(T, String), Error>
where
T: std::str::FromStr<Err = Error>,
G: Fn(&str) -> Result<String, Error>,
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<String, Error> {
let (res, _) = post_jose(url, data)?;
let nonce = nonce_from_response(&res)?;
pub fn post_challenge_response<G>(url: &str, data_builder: &G, nonce: &str) -> Result<String, Error>
where
G: Fn(&str) -> Result<String, Error>,
{
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<G>(
url: &str,
data_builder: &G,
nonce: &str,
) -> Result<(String, String), Error>
where
G: Fn(&str) -> Result<String, Error>,
{
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<Directory, Error> {
let uri = url.parse::<Uri>()?;
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<String, Error> {
let uri = url.parse::<Uri>()?;
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)]

1
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};

157
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<AcmeError> for Error {
fn from(error: AcmeError) -> Self {
error.to_string().into()
}
}
impl From<std::io::Error> for Error {
fn from(error: std::io::Error) -> Self {
format!("IO error: {}", error).into()
@ -85,3 +93,152 @@ impl From<nix::Error> 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<String> 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<Error> for AcmeError {
fn from(_error: Error) -> Self {
AcmeError::Unknown
}
}
#[derive(Deserialize)]
pub struct HttpApiError {
#[serde(rename = "type")]
error_type: Option<String>,
// title: Option<String>,
// status: Option<usize>,
detail: Option<String>,
// instance: Option<String>,
// 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()
}
}

4
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)

Loading…
Cancel
Save