Browse Source

Add rate limits for HTTPS requests

pull/5/head
Rodolphe Breard 6 years ago
parent
commit
1e6aba52dc
  1. 1
      CHANGELOG.md
  2. 3
      README.md
  3. 6
      acme_common/src/error.rs
  4. 1
      acmed/Cargo.toml
  5. 7
      acmed/config/acmed.toml
  6. 51
      acmed/src/acme_proto/http.rs
  7. 5
      acmed/src/certificate.rs
  8. 60
      acmed/src/config.rs
  9. 3
      acmed/src/main.rs
  10. 12
      acmed/src/main_event_loop.rs
  11. 188
      acmed/src/rate_limits.rs
  12. 51
      man/en/acmed.toml.5

1
CHANGELOG.md

@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Hooks now have the optional `allow_failure` field. - Hooks now have the optional `allow_failure` field.
- In hooks, the `stdin_str` has been added in replacement of the previous `stdin` behavior. - In hooks, the `stdin_str` has been added in replacement of the previous `stdin` behavior.
- HTTPS request rate limits.
### Changed ### Changed
- Certificates are renewed in parallel. - Certificates are renewed in parallel.

3
README.md

@ -25,7 +25,8 @@ The Automatic Certificate Management Environment (ACME), is an internet standard
- Nice and simple configuration file - Nice and simple configuration file
- Run as a deamon: no need to set-up timers, crontab or other time-triggered process - Run as a deamon: no need to set-up timers, crontab or other time-triggered process
- Retry of HTTPS request rejected with a badNonce or other recoverable errors - Retry of HTTPS request rejected with a badNonce or other recoverable errors
- Optional private-key reuse (useful for [HPKP](https://en.wikipedia.org/wiki/HTTP_Public_Key_Pinning))
- Customizable HTTPS requests rate limits.
- Optional key pair reuse (useful for [HPKP](https://en.wikipedia.org/wiki/HTTP_Public_Key_Pinning))
- For a given certificate, each domain names may be validated using a different challenge. - For a given certificate, each domain names may be validated using a different challenge.
- A standalone server dedicated to the tls-alpn-01 challenge validation - A standalone server dedicated to the tls-alpn-01 challenge validation

6
acme_common/src/error.rs

@ -51,6 +51,12 @@ impl From<std::string::FromUtf8Error> for Error {
} }
} }
impl From<std::sync::mpsc::RecvError> for Error {
fn from(error: std::sync::mpsc::RecvError) -> Self {
format!("MSPC receiver error: {}", error).into()
}
}
impl From<syslog::Error> for Error { impl From<syslog::Error> for Error {
fn from(error: syslog::Error) -> Self { fn from(error: syslog::Error) -> Self {
format!("syslog error: {}", error).into() format!("syslog error: {}", error).into()

1
acmed/Cargo.toml

@ -17,6 +17,7 @@ clap = "2.32"
handlebars = "2.0.0-beta.1" handlebars = "2.0.0-beta.1"
http_req = "0.4" http_req = "0.4"
log = "0.4" log = "0.4"
nom = "5.0.0-beta2"
openssl = "0.10" openssl = "0.10"
openssl-sys = "0.9" openssl-sys = "0.9"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }

7
acmed/config/acmed.toml

@ -2,12 +2,19 @@ include = [
"default_hooks.toml" "default_hooks.toml"
] ]
[[rate-limit]]
name = "LE min"
number = 20
period = "1s"
[[endpoint]] [[endpoint]]
name = "letsencrypt v2 prod" name = "letsencrypt v2 prod"
url = "https://acme-v02.api.letsencrypt.org/directory" url = "https://acme-v02.api.letsencrypt.org/directory"
rate_limits = ["LE min"]
tos_agreed = false tos_agreed = false
[[endpoint]] [[endpoint]]
name = "letsencrypt v2 staging" name = "letsencrypt v2 staging"
url = "https://acme-staging-v02.api.letsencrypt.org/directory" url = "https://acme-staging-v02.api.letsencrypt.org/directory"
rate_limits = ["LE min"]
tos_agreed = false tos_agreed = false

51
acmed/src/acme_proto/http.rs

@ -1,7 +1,8 @@
use crate::acme_proto::structs::{AcmeError, ApiError, Directory, HttpApiError}; use crate::acme_proto::structs::{AcmeError, ApiError, Directory, HttpApiError};
use crate::certificate::Certificate; use crate::certificate::Certificate;
use crate::rate_limits;
use acme_common::error::Error; use acme_common::error::Error;
use http_req::request::{Method, Request};
use http_req::request::{self, Method};
use http_req::response::Response; use http_req::response::Response;
use http_req::uri::Uri; use http_req::uri::Uri;
use std::path::Path; use std::path::Path;
@ -11,6 +12,12 @@ use std::{thread, time};
const CONTENT_TYPE_JOSE: &str = "application/jose+json"; const CONTENT_TYPE_JOSE: &str = "application/jose+json";
const CONTENT_TYPE_JSON: &str = "application/json"; const CONTENT_TYPE_JSON: &str = "application/json";
struct Request<'a> {
r: request::Request<'a>,
uri: &'a Uri,
method: Method,
}
struct DummyString { struct DummyString {
pub content: String, pub content: String,
} }
@ -25,13 +32,7 @@ impl FromStr for DummyString {
} }
} }
fn new_request<'a>(
cert: &Certificate,
root_certs: &'a [String],
uri: &'a Uri,
method: Method,
) -> Request<'a> {
cert.debug(&format!("{}: {}", method, uri));
fn new_request<'a>(root_certs: &'a [String], uri: &'a Uri, method: Method) -> Request<'a> {
let useragent = format!( let useragent = format!(
"{}/{} ({}) {}", "{}/{} ({}) {}",
crate::APP_NAME, crate::APP_NAME,
@ -39,27 +40,31 @@ fn new_request<'a>(
env!("ACMED_TARGET"), env!("ACMED_TARGET"),
env!("ACMED_HTTP_LIB_AGENT") env!("ACMED_HTTP_LIB_AGENT")
); );
let mut rb = Request::new(uri);
let mut rb = request::Request::new(uri);
for file_name in root_certs.iter() { for file_name in root_certs.iter() {
rb.root_cert_file_pem(&Path::new(file_name)); rb.root_cert_file_pem(&Path::new(file_name));
} }
rb.method(method);
rb.method(method.to_owned());
rb.header("User-Agent", &useragent); rb.header("User-Agent", &useragent);
// TODO: allow to configure the language // TODO: allow to configure the language
rb.header("Accept-Language", "en-US,en;q=0.5"); rb.header("Accept-Language", "en-US,en;q=0.5");
rb
Request { r: rb, method, uri }
} }
fn send_request(request: &Request) -> Result<(Response, String), Error> {
fn send_request(cert: &Certificate, request: &Request) -> Result<(Response, String), Error> {
let mut buffer = Vec::new(); let mut buffer = Vec::new();
let res = request.send(&mut buffer)?;
cert.https_throttle
.send(rate_limits::Request::HttpsRequest)
.unwrap();
cert.debug(&format!("{}: {}", request.method, request.uri));
let res = request.r.send(&mut buffer)?;
let res_str = String::from_utf8(buffer)?; let res_str = String::from_utf8(buffer)?;
Ok((res, res_str)) Ok((res, res_str))
} }
fn send_request_retry(cert: &Certificate, request: &Request) -> Result<(Response, String), Error> { fn send_request_retry(cert: &Certificate, request: &Request) -> Result<(Response, String), Error> {
for _ in 0..crate::DEFAULT_HTTP_FAIL_NB_RETRY { for _ in 0..crate::DEFAULT_HTTP_FAIL_NB_RETRY {
let (res, res_body) = send_request(request)?;
let (res, res_body) = send_request(cert, request)?;
match check_response(&res, &res_body) { match check_response(&res, &res_body) {
Ok(()) => { Ok(()) => {
return Ok((res, res_body)); return Ok((res, res_body));
@ -110,14 +115,14 @@ fn post_jose_type(
accept_type: &str, accept_type: &str,
) -> Result<(Response, String), Error> { ) -> Result<(Response, String), Error> {
let uri = url.parse::<Uri>()?; let uri = url.parse::<Uri>()?;
let mut request = new_request(cert, root_certs, &uri, Method::POST);
request.header("Content-Type", CONTENT_TYPE_JOSE);
request.header("Content-Length", &data.len().to_string());
request.header("Accept", accept_type);
request.body(data);
let mut request = new_request(root_certs, &uri, Method::POST);
request.r.header("Content-Type", CONTENT_TYPE_JOSE);
request.r.header("Content-Length", &data.len().to_string());
request.r.header("Accept", accept_type);
request.r.body(data);
let rstr = String::from_utf8_lossy(data); let rstr = String::from_utf8_lossy(data);
cert.trace(&format!("request body: {}", rstr)); cert.trace(&format!("request body: {}", rstr));
let (res, res_body) = send_request(&request)?;
let (res, res_body) = send_request(cert, &request)?;
cert.trace(&format!("response body: {}", res_body)); cert.trace(&format!("response body: {}", res_body));
Ok((res, res_body)) Ok((res, res_body))
} }
@ -287,8 +292,8 @@ pub fn get_directory(
url: &str, url: &str,
) -> Result<Directory, Error> { ) -> Result<Directory, Error> {
let uri = url.parse::<Uri>()?; let uri = url.parse::<Uri>()?;
let mut request = new_request(cert, root_certs, &uri, Method::GET);
request.header("Accept", CONTENT_TYPE_JSON);
let mut request = new_request(root_certs, &uri, Method::GET);
request.r.header("Accept", CONTENT_TYPE_JSON);
let (r, s) = send_request_retry(cert, &request)?; let (r, s) = send_request_retry(cert, &request)?;
check_response(&r, &s)?; check_response(&r, &s)?;
Directory::from_str(&s) Directory::from_str(&s)
@ -296,7 +301,7 @@ pub fn get_directory(
pub fn get_nonce(cert: &Certificate, root_certs: &[String], url: &str) -> Result<String, Error> { pub fn get_nonce(cert: &Certificate, root_certs: &[String], url: &str) -> Result<String, Error> {
let uri = url.parse::<Uri>()?; let uri = url.parse::<Uri>()?;
let request = new_request(cert, root_certs, &uri, Method::HEAD);
let request = new_request(root_certs, &uri, Method::HEAD);
let (res, res_body) = send_request_retry(cert, &request)?; let (res, res_body) = send_request_retry(cert, &request)?;
check_response(&res, &res_body)?; check_response(&res, &res_body)?;
nonce_from_response(cert, &res) nonce_from_response(cert, &res)

5
acmed/src/certificate.rs

@ -7,6 +7,7 @@ use log::{debug, info, trace, warn};
use openssl::x509::X509; use openssl::x509::X509;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::fmt; use std::fmt;
use std::sync::mpsc::SyncSender;
use time::{strptime, Duration}; use time::{strptime, Duration};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -49,6 +50,7 @@ pub struct Certificate {
pub kp_reuse: bool, pub kp_reuse: bool,
pub remote_url: String, pub remote_url: String,
pub tos_agreed: bool, pub tos_agreed: bool,
pub https_throttle: SyncSender<crate::rate_limits::Request>,
pub hooks: Vec<Hook>, pub hooks: Vec<Hook>,
pub account_directory: String, pub account_directory: String,
pub crt_directory: String, pub crt_directory: String,
@ -245,8 +247,10 @@ impl Certificate {
mod tests { mod tests {
use super::{Algorithm, Certificate}; use super::{Algorithm, Certificate};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::mpsc::sync_channel;
fn get_dummy_certificate() -> Certificate { fn get_dummy_certificate() -> Certificate {
let (https_throttle, _) = sync_channel(0);
Certificate { Certificate {
account: crate::config::Account { account: crate::config::Account {
name: String::new(), name: String::new(),
@ -257,6 +261,7 @@ mod tests {
kp_reuse: false, kp_reuse: false,
remote_url: String::new(), remote_url: String::new(),
tos_agreed: false, tos_agreed: false,
https_throttle,
hooks: Vec::new(), hooks: Vec::new(),
account_directory: String::new(), account_directory: String::new(),
crt_directory: String::new(), crt_directory: String::new(),

60
acmed/src/config.rs

@ -1,13 +1,15 @@
use crate::certificate::Algorithm; use crate::certificate::Algorithm;
use crate::hooks; use crate::hooks;
use crate::rate_limits;
use acme_common::error::Error; use acme_common::error::Error;
use log::info; use log::info;
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt;
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::prelude::*; use std::io::prelude::*;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::mpsc;
use std::{fmt, thread};
macro_rules! set_cfg_attr { macro_rules! set_cfg_attr {
($to: expr, $from: expr) => { ($to: expr, $from: expr) => {
@ -42,6 +44,8 @@ pub struct Config {
pub global: Option<GlobalOptions>, pub global: Option<GlobalOptions>,
#[serde(default)] #[serde(default)]
pub endpoint: Vec<Endpoint>, pub endpoint: Vec<Endpoint>,
#[serde(default, rename = "rate-limit")]
pub rate_limit: Vec<RateLimit>,
#[serde(default)] #[serde(default)]
pub hook: Vec<Hook>, pub hook: Vec<Hook>,
#[serde(default)] #[serde(default)]
@ -55,6 +59,15 @@ pub struct Config {
} }
impl Config { impl Config {
fn get_rate_limit(&self, name: &str) -> Result<(usize, String), Error> {
for rl in self.rate_limit.iter() {
if rl.name == name {
return Ok((rl.number, rl.period.to_owned()));
}
}
Err(format!("{}: rate limit not found", name).into())
}
pub fn get_account_dir(&self) -> String { pub fn get_account_dir(&self) -> String {
let account_dir = match &self.global { let account_dir = match &self.global {
Some(g) => match &g.accounts_directory { Some(g) => match &g.accounts_directory {
@ -167,6 +180,27 @@ pub struct Endpoint {
pub name: String, pub name: String,
pub url: String, pub url: String,
pub tos_agreed: bool, pub tos_agreed: bool,
#[serde(default)]
pub rate_limits: Vec<String>,
}
impl Endpoint {
fn is_used(&self, config: &Config) -> bool {
for crt in config.certificate.iter() {
if crt.endpoint == self.name {
return true;
}
}
false
}
}
#[derive(Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RateLimit {
pub name: String,
pub number: usize,
pub period: String,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -304,6 +338,11 @@ impl Certificate {
Ok(ep.url) Ok(ep.url)
} }
pub fn get_endpoint_name(&self, cnf: &Config) -> Result<String, Error> {
let ep = self.get_endpoint(cnf)?;
Ok(ep.name)
}
pub fn get_tos_agreement(&self, cnf: &Config) -> Result<bool, Error> { pub fn get_tos_agreement(&self, cnf: &Config) -> Result<bool, Error> {
let ep = self.get_endpoint(cnf)?; let ep = self.get_endpoint(cnf)?;
Ok(ep.tos_agreed) Ok(ep.tos_agreed)
@ -368,6 +407,7 @@ fn read_cnf(path: &PathBuf) -> Result<Config, Error> {
let cnf_path = get_cnf_path(path, cnf_name); let cnf_path = get_cnf_path(path, cnf_name);
let mut add_cnf = read_cnf(&cnf_path)?; let mut add_cnf = read_cnf(&cnf_path)?;
config.endpoint.append(&mut add_cnf.endpoint); config.endpoint.append(&mut add_cnf.endpoint);
config.rate_limit.append(&mut add_cnf.rate_limit);
config.hook.append(&mut add_cnf.hook); config.hook.append(&mut add_cnf.hook);
config.group.append(&mut add_cnf.group); config.group.append(&mut add_cnf.group);
config.account.append(&mut add_cnf.account); config.account.append(&mut add_cnf.account);
@ -407,6 +447,24 @@ fn dispatch_global_env_vars(config: &mut Config) {
} }
} }
pub fn init_rate_limits(
config: &Config,
) -> Result<HashMap<String, mpsc::SyncSender<rate_limits::Request>>, Error> {
let mut corresp = HashMap::new();
for endpoint in config.endpoint.iter() {
if endpoint.is_used(config) {
let mut limits = Vec::new();
for l in endpoint.rate_limits.iter() {
limits.push(config.get_rate_limit(l)?);
}
let mut rl = rate_limits::RateLimit::new(&limits)?;
corresp.insert(endpoint.name.to_owned(), rl.get_sender());
thread::spawn(move || rl.run());
}
}
Ok(corresp)
}
pub fn from_file(file_name: &str) -> Result<Config, Error> { pub fn from_file(file_name: &str) -> Result<Config, Error> {
let path = PathBuf::from(file_name); let path = PathBuf::from(file_name);
let mut config = read_cnf(&path)?; let mut config = read_cnf(&path)?;

3
acmed/src/main.rs

@ -8,6 +8,7 @@ mod certificate;
mod config; mod config;
mod hooks; mod hooks;
mod main_event_loop; mod main_event_loop;
mod rate_limits;
mod storage; mod storage;
pub const APP_NAME: &str = "ACMEd"; pub const APP_NAME: &str = "ACMEd";
@ -30,6 +31,8 @@ pub const DEFAULT_POOL_WAIT_SEC: u64 = 5;
pub const DEFAULT_HTTP_FAIL_NB_RETRY: usize = 10; pub const DEFAULT_HTTP_FAIL_NB_RETRY: usize = 10;
pub const DEFAULT_HTTP_FAIL_WAIT_SEC: u64 = 1; pub const DEFAULT_HTTP_FAIL_WAIT_SEC: u64 = 1;
pub const DEFAULT_HOOK_ALLOW_FAILURE: bool = false; pub const DEFAULT_HOOK_ALLOW_FAILURE: bool = false;
pub const MAX_RATE_LIMIT_SLEEP_MILISEC: u64 = 3_600_000;
pub const MIN_RATE_LIMIT_SLEEP_MILISEC: u64 = 100;
fn main() { fn main() {
let full_version = format!( let full_version = format!(

12
acmed/src/main_event_loop.rs

@ -31,9 +31,20 @@ pub struct MainEventLoop {
impl MainEventLoop { impl MainEventLoop {
pub fn new(config_file: &str, root_certs: &[&str]) -> Result<Self, Error> { pub fn new(config_file: &str, root_certs: &[&str]) -> Result<Self, Error> {
let cnf = config::from_file(config_file)?; let cnf = config::from_file(config_file)?;
let rate_limits_corresp = config::init_rate_limits(&cnf)?;
let mut certs = Vec::new(); let mut certs = Vec::new();
for (i, crt) in cnf.certificate.iter().enumerate() { for (i, crt) in cnf.certificate.iter().enumerate() {
let ep_name = crt.get_endpoint_name(&cnf)?;
let https_throttle = rate_limits_corresp
.get(&ep_name)
.ok_or_else(|| {
Error::from(format!(
"{}: rate limit not found for this endpoint",
ep_name
))
})?
.to_owned();
let cert = Certificate { let cert = Certificate {
account: crt.get_account(&cnf)?, account: crt.get_account(&cnf)?,
domains: crt.domains.to_owned(), domains: crt.domains.to_owned(),
@ -41,6 +52,7 @@ impl MainEventLoop {
kp_reuse: crt.get_kp_reuse(), kp_reuse: crt.get_kp_reuse(),
remote_url: crt.get_remote_url(&cnf)?, remote_url: crt.get_remote_url(&cnf)?,
tos_agreed: crt.get_tos_agreement(&cnf)?, tos_agreed: crt.get_tos_agreement(&cnf)?,
https_throttle,
hooks: crt.get_hooks(&cnf)?, hooks: crt.get_hooks(&cnf)?,
account_directory: cnf.get_account_dir(), account_directory: cnf.get_account_dir(),
crt_directory: crt.get_crt_dir(&cnf), crt_directory: crt.get_crt_dir(&cnf),

188
acmed/src/rate_limits.rs

@ -0,0 +1,188 @@
use acme_common::error::Error;
use nom::bytes::complete::take_while_m_n;
use nom::character::complete::digit1;
use nom::combinator::map_res;
use nom::multi::fold_many1;
use nom::IResult;
use std::cmp;
use std::sync::mpsc;
use std::thread;
use std::time::{Duration, Instant};
pub enum Request {
HttpsRequest,
}
pub struct RateLimit {
limits: Vec<(usize, Duration)>,
sender: mpsc::SyncSender<Request>,
receiver: mpsc::Receiver<Request>,
log: Vec<Instant>,
}
impl RateLimit {
pub fn new(limits: &[(usize, String)]) -> Result<Self, Error> {
let mut max_size = 0;
let mut parsed_limits = Vec::new();
for (nb, raw_duration) in limits.iter() {
if *nb > max_size {
max_size = *nb;
}
let parsed_duration = parse_duration(raw_duration)?;
parsed_limits.push((*nb, parsed_duration));
}
parsed_limits.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
parsed_limits.reverse();
let (sender, receiver) = mpsc::sync_channel::<Request>(0);
Ok(RateLimit {
limits: parsed_limits,
sender,
receiver,
log: Vec::with_capacity(max_size),
})
}
pub fn get_sender(&self) -> mpsc::SyncSender<Request> {
self.sender.clone()
}
pub fn run(&mut self) -> Result<(), Error> {
let sleep_duration = self.get_sleep_duration();
loop {
self.prune_log();
if self.request_allowed() {
match self.receiver.recv()? {
Request::HttpsRequest => {
if !self.limits.is_empty() {
self.log.push(Instant::now());
}
}
}
} else {
// TODO: find a better sleep duration
thread::sleep(sleep_duration);
}
}
}
fn get_sleep_duration(&self) -> Duration {
let (nb_req, min_duration) = match self.limits.last() {
Some((n, d)) => (*n as u64, *d),
None => {
return Duration::from_millis(0);
}
};
let nb_mili = match min_duration.as_secs() {
0 | 1 => crate::MIN_RATE_LIMIT_SLEEP_MILISEC,
n => {
let a = n * 200 / nb_req;
let a = cmp::min(a, crate::MAX_RATE_LIMIT_SLEEP_MILISEC);
cmp::max(a, crate::MIN_RATE_LIMIT_SLEEP_MILISEC)
}
};
Duration::from_millis(nb_mili)
}
fn request_allowed(&self) -> bool {
for (max_allowed, duration) in self.limits.iter() {
let max_date = Instant::now() - *duration;
let nb_req = self.log.iter().filter(move |x| **x > max_date).count();
if nb_req >= *max_allowed {
return false;
}
}
true
}
fn prune_log(&mut self) {
if let Some((_, max_limit)) = self.limits.first() {
let prune_date = Instant::now() - *max_limit;
self.log.retain(move |&d| d > prune_date);
}
}
}
fn is_duration_chr(c: char) -> bool {
c == 's' || c == 'm' || c == 'h' || c == 'd' || c == 'w'
}
fn get_multiplicator(input: &str) -> IResult<&str, u64> {
let (input, nb) = take_while_m_n(1, 1, is_duration_chr)(input)?;
let mult = match nb.chars().nth(0) {
Some('s') => 1,
Some('m') => 60,
Some('h') => 3_600,
Some('d') => 86_400,
Some('w') => 604_800,
_ => 0,
};
Ok((input, mult))
}
fn get_duration_part(input: &str) -> IResult<&str, Duration> {
let (input, nb) = map_res(digit1, |s: &str| s.parse::<u64>())(input)?;
let (input, mult) = get_multiplicator(input)?;
Ok((input, Duration::from_secs(nb * mult)))
}
fn get_duration(input: &str) -> IResult<&str, Duration> {
fold_many1(
get_duration_part,
Duration::new(0, 0),
|mut acc: Duration, item| {
acc += item;
acc
},
)(input)
}
fn parse_duration(input: &str) -> Result<Duration, Error> {
match get_duration(input) {
Ok((r, d)) => match r.len() {
0 => Ok(d),
_ => Err(format!("{}: invalid duration", input).into()),
},
Err(_) => Err(format!("{}: invalid duration", input).into()),
}
}
#[cfg(test)]
mod tests {
use super::{parse_duration, RateLimit};
#[test]
fn test_rate_limit_build() {
let l = vec![
(5, String::from("5s")),
(12, String::from("2m")),
(8, String::from("5m")),
(1, String::from("1s")),
(2, String::from("1m")),
];
let rl = RateLimit::new(l.as_slice()).unwrap();
let ref_t = (8_usize, parse_duration("5m").unwrap());
assert_eq!(rl.limits.first(), Some(&ref_t));
assert_eq!(rl.log.len(), 0);
assert_eq!(rl.log.capacity(), 12);
}
#[test]
fn test_parse_duration() {
let lst = [
("42s", 42),
("21m", 1_260),
("3h", 10_800),
("2d", 172_800),
("1w", 604_800),
("42m30s", 2_550),
("30s42m", 2_550),
("3h5m12s", 11_112),
("40s2s", 42),
];
for (fmt, ref_sec) in lst.iter() {
let d = parse_duration(fmt);
assert!(d.is_ok());
assert_eq!(d.unwrap().as_secs(), *ref_sec);
}
}
}

51
man/en/acmed.toml.5

@ -63,6 +63,18 @@ Specify the group who will own newly-created private-key files. See
.Xr chown 2 .Xr chown 2
for more details. for more details.
.El .El
.It Ic rate-limit
Array of table where each element defines a HTTPS rate limit.
.Bl -tag
.It Cm name Ar string
The name the rate limit is registered under. Must be unique.
.It Cm number Ar integer
Number of requests authorized withing the time period.
.It Cm period Ar string
Period of time during which a maximal number of requests is authorized. The format is described in the
.Sx TIME PERIODS
section.
.El
.It Ic endpoint .It Ic endpoint
Array of table where each element defines a Certificate Authority Array of table where each element defines a Certificate Authority
.Pq CA .Pq CA
@ -70,11 +82,13 @@ which may be used to request certificates.
.Bl -tag .Bl -tag
.It Cm name Ar string .It Cm name Ar string
The name the endpoint is registered under. Must be unique. The name the endpoint is registered under. Must be unique.
.It Cm url Ar string
The endpoint's directory URL.
.It Cm rate_limits Ar array
Array containing the names of the HTTPS rate limits to apply.
.It Cm tos_agreed Ar boolean .It Cm tos_agreed Ar boolean
Set whether or not the user agrees to the Terms Of Service Set whether or not the user agrees to the Terms Of Service
.Pq TOS . .Pq TOS .
.It Cm url Ar string
The endpoint's directory URL.
.El .El
.It Ic hook .It Ic hook
Array of table where each element defines a command that will be launched at a defined point. See section Array of table where each element defines a command that will be launched at a defined point. See section
@ -442,6 +456,39 @@ If
is not specified, it will be set to is not specified, it will be set to
.Pa /run . .Pa /run .
.El .El
.Sh TIME PERIODS
ACMEd uses its own time period format, which is vaguely inspired by the ISO 8601 one. Periods are formatted as
.Ar PM[PM...]
where
.Ar M
is case sensitive character representing a length and
.Ar P
is an integer representing a multiplayer for the following length. The authorized length are:
.Bl -dash -compact
.It
.Ar s :
second
.It
.Ar m :
minute
.It
.Ar h :
hour
.It
.Ar d :
day
.It
.Ar w :
week
.El
The
.Ar PM
couples can be specified multiple times and in any order.
.Pp
For example,
.Dq 1d42s and
.Dq 40s20h4h2s
both represents a period of one day and forty-two seconds.
.Sh FILES .Sh FILES
.Bl -tag .Bl -tag
.It Pa /etc/acmed/acmed.toml .It Pa /etc/acmed/acmed.toml

Loading…
Cancel
Save