Browse Source

Implement durations

ng
Rodolphe Bréard 1 month ago
parent
commit
05197029e2
Failed to extract signature
  1. 1
      Cargo.lock
  2. 1
      Cargo.toml
  3. 2
      src/config.rs
  4. 9
      src/config/certificate.rs
  5. 125
      src/config/duration.rs
  6. 9
      src/config/endpoint.rs
  7. 13
      src/config/global.rs
  8. 5
      src/config/rate_limit.rs

1
Cargo.lock

@ -10,6 +10,7 @@ dependencies = [
"clap", "clap",
"config", "config",
"daemonize", "daemonize",
"nom",
"serde", "serde",
"serde_derive", "serde_derive",
"syslog-tracing", "syslog-tracing",

1
Cargo.toml

@ -26,6 +26,7 @@ anyhow = { version = "1.0.94", default-features = false, features = ["std"] }
clap = { version = "4.5.23", default-features = false, features = ["color", "derive", "help", "std", "string"] } clap = { version = "4.5.23", default-features = false, features = ["color", "derive", "help", "std", "string"] }
config = { version = "0.14.0", default-features = false, features = ["toml"] } config = { version = "0.14.0", default-features = false, features = ["toml"] }
daemonize = { version = "0.5.0", default-features = false } daemonize = { version = "0.5.0", default-features = false }
nom = { version = "7.1.3", default-features = false }
serde = { version = "1.0.216", default-features = false, features = ["std"] } serde = { version = "1.0.216", default-features = false, features = ["std"] }
serde_derive = { version = "1.0.216", default-features = false } serde_derive = { version = "1.0.216", default-features = false }
syslog-tracing = { version = "0.3.1", default-features = false } syslog-tracing = { version = "0.3.1", default-features = false }

2
src/config.rs

@ -1,5 +1,6 @@
mod account; mod account;
mod certificate; mod certificate;
mod duration;
mod endpoint; mod endpoint;
mod global; mod global;
mod hook; mod hook;
@ -7,6 +8,7 @@ mod rate_limit;
pub use account::*; pub use account::*;
pub use certificate::*; pub use certificate::*;
pub use duration::*;
pub use endpoint::*; pub use endpoint::*;
pub use global::*; pub use global::*;
pub use hook::*; pub use hook::*;

9
src/config/certificate.rs

@ -1,3 +1,4 @@
use crate::config::Duration;
use anyhow::Result; use anyhow::Result;
use serde::{de, Deserialize, Deserializer}; use serde::{de, Deserialize, Deserializer};
use serde_derive::Deserialize; use serde_derive::Deserialize;
@ -24,8 +25,8 @@ pub struct Certificate {
#[serde(default)] #[serde(default)]
pub(in crate::config) kp_reuse: bool, pub(in crate::config) kp_reuse: bool,
pub(in crate::config) name: Option<String>, pub(in crate::config) name: Option<String>,
pub(in crate::config) random_early_renew: Option<String>,
pub(in crate::config) renew_delay: Option<String>,
pub(in crate::config) random_early_renew: Option<Duration>,
pub(in crate::config) renew_delay: Option<Duration>,
#[serde(default)] #[serde(default)]
pub(in crate::config) subject_attributes: SubjectAttributes, pub(in crate::config) subject_attributes: SubjectAttributes,
} }
@ -214,8 +215,8 @@ subject_attributes.organization_name = "ACME Inc."
assert_eq!(c.key_type, KeyType::EcDsaP256); assert_eq!(c.key_type, KeyType::EcDsaP256);
assert_eq!(c.kp_reuse, true); assert_eq!(c.kp_reuse, true);
assert_eq!(c.name, Some("test".to_string())); assert_eq!(c.name, Some("test".to_string()));
assert_eq!(c.random_early_renew, Some("1d".to_string()));
assert_eq!(c.renew_delay, Some("30d".to_string()));
assert_eq!(c.random_early_renew, Some(Duration::from_days(1)));
assert_eq!(c.renew_delay, Some(Duration::from_days(30)));
assert_eq!(c.subject_attributes.country_name, Some("FR".to_string())); assert_eq!(c.subject_attributes.country_name, Some("FR".to_string()));
assert!(c.subject_attributes.generation_qualifier.is_none()); assert!(c.subject_attributes.generation_qualifier.is_none());
assert!(c.subject_attributes.given_name.is_none()); assert!(c.subject_attributes.given_name.is_none());

125
src/config/duration.rs

@ -0,0 +1,125 @@
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 serde::{de, Deserialize, Deserializer};
type StdDuration = std::time::Duration;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Duration(StdDuration);
impl Duration {
pub(in crate::config) fn from_secs(nb_secs: u64) -> Self {
Self(std::time::Duration::from_secs(nb_secs))
}
pub(in crate::config) fn from_days(nb_days: u64) -> Self {
Self(std::time::Duration::from_secs(nb_days * 24 * 60 * 60))
}
pub(in crate::config) fn get_std(&self) -> StdDuration {
self.0
}
}
impl<'de> Deserialize<'de> for Duration {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let (_, duration) =
parse_duration(&s).map_err(|_| de::Error::custom("invalid duration"))?;
Ok(duration)
}
}
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().next() {
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, StdDuration> {
let (input, nb) = map_res(digit1, |s: &str| s.parse::<u64>())(input)?;
let (input, mult) = get_multiplicator(input)?;
Ok((input, StdDuration::from_secs(nb * mult)))
}
fn parse_duration(input: &str) -> IResult<&str, Duration> {
let (input, std_duration) = fold_many1(
get_duration_part,
|| StdDuration::new(0, 0),
|mut acc: StdDuration, item| {
acc += item;
acc
},
)(input)?;
Ok((input, Duration(std_duration)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_duration() {
let res = parse_duration("");
assert!(res.is_err());
}
#[test]
fn single_second() {
let (_, d) = parse_duration("1s").unwrap();
assert_eq!(d.get_std(), StdDuration::from_secs(1));
}
#[test]
fn single_minute() {
let (_, d) = parse_duration("123m").unwrap();
assert_eq!(d.get_std(), StdDuration::from_secs(123 * 60));
}
#[test]
fn single_hour() {
let (_, d) = parse_duration("10h").unwrap();
assert_eq!(d.get_std(), StdDuration::from_secs(10 * 60 * 60));
}
#[test]
fn single_day() {
let (_, d) = parse_duration("3d").unwrap();
assert_eq!(d.get_std(), StdDuration::from_secs(3 * 24 * 60 * 60));
}
#[test]
fn single_week() {
let (_, d) = parse_duration("1w").unwrap();
assert_eq!(d.get_std(), StdDuration::from_secs(7 * 24 * 60 * 60));
}
#[test]
fn mixed() {
let (_, d) = parse_duration("1d42s").unwrap();
assert_eq!(d.get_std(), StdDuration::from_secs(24 * 60 * 60 + 42));
}
#[test]
fn duplicated() {
let (_, d) = parse_duration("40s20h4h2s").unwrap();
assert_eq!(d.get_std(), StdDuration::from_secs(24 * 60 * 60 + 42));
}
}

9
src/config/endpoint.rs

@ -1,3 +1,4 @@
use crate::config::Duration;
use serde_derive::Deserialize; use serde_derive::Deserialize;
use std::path::PathBuf; use std::path::PathBuf;
@ -6,10 +7,10 @@ use std::path::PathBuf;
pub struct Endpoint { pub struct Endpoint {
pub(in crate::config) file_name_format: Option<String>, pub(in crate::config) file_name_format: Option<String>,
pub(in crate::config) name: String, pub(in crate::config) name: String,
pub(in crate::config) random_early_renew: Option<String>,
pub(in crate::config) random_early_renew: Option<Duration>,
#[serde(default)] #[serde(default)]
pub(in crate::config) rate_limits: Vec<String>, pub(in crate::config) rate_limits: Vec<String>,
pub(in crate::config) renew_delay: Option<String>,
pub(in crate::config) renew_delay: Option<Duration>,
#[serde(default)] #[serde(default)]
pub(in crate::config) root_certificates: Vec<PathBuf>, pub(in crate::config) root_certificates: Vec<PathBuf>,
#[serde(default)] #[serde(default)]
@ -65,9 +66,9 @@ tos_agreed = true
Some("{{ key_type }} {{ file_type }} {{ name }}.{{ ext }}".to_string()) Some("{{ key_type }} {{ file_type }} {{ name }}.{{ ext }}".to_string())
); );
assert_eq!(e.name, "test"); assert_eq!(e.name, "test");
assert_eq!(e.random_early_renew, Some("1d".to_string()));
assert_eq!(e.random_early_renew, Some(Duration::from_days(1)));
assert_eq!(e.rate_limits, vec!["rl 1", "rl 2"]); assert_eq!(e.rate_limits, vec!["rl 1", "rl 2"]);
assert_eq!(e.renew_delay, Some("21d".to_string()));
assert_eq!(e.renew_delay, Some(Duration::from_days(21)));
assert_eq!(e.root_certificates, vec![PathBuf::from("root_cert.pem")]); assert_eq!(e.root_certificates, vec![PathBuf::from("root_cert.pem")]);
assert_eq!(e.tos_agreed, true); assert_eq!(e.tos_agreed, true);
assert_eq!(e.url, "https://acme-v02.api.example.com/directory"); assert_eq!(e.url, "https://acme-v02.api.example.com/directory");

13
src/config/global.rs

@ -1,3 +1,4 @@
use crate::config::Duration;
use serde_derive::Deserialize; use serde_derive::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
@ -23,9 +24,9 @@ pub struct GlobalOptions {
pub(in crate::config) pk_file_user: Option<String>, pub(in crate::config) pk_file_user: Option<String>,
#[serde(default = "get_default_pk_file_ext")] #[serde(default = "get_default_pk_file_ext")]
pub(in crate::config) pk_file_ext: String, pub(in crate::config) pk_file_ext: String,
pub(in crate::config) random_early_renew: Option<String>,
pub(in crate::config) random_early_renew: Option<Duration>,
#[serde(default = "get_default_renew_delay")] #[serde(default = "get_default_renew_delay")]
pub(in crate::config) renew_delay: String,
pub(in crate::config) renew_delay: Duration,
#[serde(default)] #[serde(default)]
pub(in crate::config) root_certificates: Vec<PathBuf>, pub(in crate::config) root_certificates: Vec<PathBuf>,
} }
@ -63,8 +64,8 @@ fn get_default_pk_file_ext() -> String {
"pem".to_string() "pem".to_string()
} }
fn get_default_renew_delay() -> String {
"30d".to_string()
fn get_default_renew_delay() -> Duration {
Duration::from_days(3)
} }
#[cfg(test)] #[cfg(test)]
@ -135,8 +136,8 @@ root_certificates = ["root_cert.pem"]
assert_eq!(go.pk_file_mode, Some(0o644)); assert_eq!(go.pk_file_mode, Some(0o644));
assert_eq!(go.pk_file_user, Some("acme_test".to_string())); assert_eq!(go.pk_file_user, Some("acme_test".to_string()));
assert_eq!(go.pk_file_ext, "pem.txt"); assert_eq!(go.pk_file_ext, "pem.txt");
assert_eq!(go.random_early_renew, Some("2d".to_string()));
assert_eq!(go.renew_delay, "21d");
assert_eq!(go.random_early_renew, Some(Duration::from_days(2)));
assert_eq!(go.renew_delay, Duration::from_days(21));
assert_eq!(go.root_certificates, vec![PathBuf::from("root_cert.pem")]); assert_eq!(go.root_certificates, vec![PathBuf::from("root_cert.pem")]);
} }
} }

5
src/config/rate_limit.rs

@ -1,3 +1,4 @@
use crate::config::Duration;
use serde_derive::Deserialize; use serde_derive::Deserialize;
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
@ -5,7 +6,7 @@ use serde_derive::Deserialize;
pub struct RateLimit { pub struct RateLimit {
pub(in crate::config) name: String, pub(in crate::config) name: String,
pub(in crate::config) number: usize, pub(in crate::config) number: usize,
pub(in crate::config) period: String,
pub(in crate::config) period: Duration,
} }
#[cfg(test)] #[cfg(test)]
@ -30,7 +31,7 @@ period = "20s"
let rl: RateLimit = load_str(cfg).unwrap(); let rl: RateLimit = load_str(cfg).unwrap();
assert_eq!(rl.name, "test"); assert_eq!(rl.name, "test");
assert_eq!(rl.number, 20); assert_eq!(rl.number, 20);
assert_eq!(rl.period, "20s");
assert_eq!(rl.period, Duration::from_secs(20));
} }
#[test] #[test]

Loading…
Cancel
Save