diff --git a/Cargo.lock b/Cargo.lock index 02e6177..5714db7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,6 +10,7 @@ dependencies = [ "clap", "config", "daemonize", + "nom", "serde", "serde_derive", "syslog-tracing", diff --git a/Cargo.toml b/Cargo.toml index 24f9063..ae5902b 100644 --- a/Cargo.toml +++ b/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"] } config = { version = "0.14.0", default-features = false, features = ["toml"] } 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_derive = { version = "1.0.216", default-features = false } syslog-tracing = { version = "0.3.1", default-features = false } diff --git a/src/config.rs b/src/config.rs index 2219371..e4457cf 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,6 @@ mod account; mod certificate; +mod duration; mod endpoint; mod global; mod hook; @@ -7,6 +8,7 @@ mod rate_limit; pub use account::*; pub use certificate::*; +pub use duration::*; pub use endpoint::*; pub use global::*; pub use hook::*; diff --git a/src/config/certificate.rs b/src/config/certificate.rs index 168043a..77fd54d 100644 --- a/src/config/certificate.rs +++ b/src/config/certificate.rs @@ -1,3 +1,4 @@ +use crate::config::Duration; use anyhow::Result; use serde::{de, Deserialize, Deserializer}; use serde_derive::Deserialize; @@ -24,8 +25,8 @@ pub struct Certificate { #[serde(default)] pub(in crate::config) kp_reuse: bool, pub(in crate::config) name: Option, - pub(in crate::config) random_early_renew: Option, - pub(in crate::config) renew_delay: Option, + pub(in crate::config) random_early_renew: Option, + pub(in crate::config) renew_delay: Option, #[serde(default)] 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.kp_reuse, true); 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!(c.subject_attributes.generation_qualifier.is_none()); assert!(c.subject_attributes.given_name.is_none()); diff --git a/src/config/duration.rs b/src/config/duration.rs new file mode 100644 index 0000000..250f85f --- /dev/null +++ b/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(deserializer: D) -> Result + 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::())(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)); + } +} diff --git a/src/config/endpoint.rs b/src/config/endpoint.rs index 9a55598..7c822f0 100644 --- a/src/config/endpoint.rs +++ b/src/config/endpoint.rs @@ -1,3 +1,4 @@ +use crate::config::Duration; use serde_derive::Deserialize; use std::path::PathBuf; @@ -6,10 +7,10 @@ use std::path::PathBuf; pub struct Endpoint { pub(in crate::config) file_name_format: Option, pub(in crate::config) name: String, - pub(in crate::config) random_early_renew: Option, + pub(in crate::config) random_early_renew: Option, #[serde(default)] pub(in crate::config) rate_limits: Vec, - pub(in crate::config) renew_delay: Option, + pub(in crate::config) renew_delay: Option, #[serde(default)] pub(in crate::config) root_certificates: Vec, #[serde(default)] @@ -65,9 +66,9 @@ tos_agreed = true Some("{{ key_type }} {{ file_type }} {{ name }}.{{ ext }}".to_string()) ); 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.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.tos_agreed, true); assert_eq!(e.url, "https://acme-v02.api.example.com/directory"); diff --git a/src/config/global.rs b/src/config/global.rs index f60d6cb..bf6f9ab 100644 --- a/src/config/global.rs +++ b/src/config/global.rs @@ -1,3 +1,4 @@ +use crate::config::Duration; use serde_derive::Deserialize; use std::collections::HashMap; use std::path::PathBuf; @@ -23,9 +24,9 @@ pub struct GlobalOptions { pub(in crate::config) pk_file_user: Option, #[serde(default = "get_default_pk_file_ext")] pub(in crate::config) pk_file_ext: String, - pub(in crate::config) random_early_renew: Option, + pub(in crate::config) random_early_renew: Option, #[serde(default = "get_default_renew_delay")] - pub(in crate::config) renew_delay: String, + pub(in crate::config) renew_delay: Duration, #[serde(default)] pub(in crate::config) root_certificates: Vec, } @@ -63,8 +64,8 @@ fn get_default_pk_file_ext() -> 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)] @@ -135,8 +136,8 @@ root_certificates = ["root_cert.pem"] assert_eq!(go.pk_file_mode, Some(0o644)); assert_eq!(go.pk_file_user, Some("acme_test".to_string())); 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")]); } } diff --git a/src/config/rate_limit.rs b/src/config/rate_limit.rs index ada0e83..f2557aa 100644 --- a/src/config/rate_limit.rs +++ b/src/config/rate_limit.rs @@ -1,3 +1,4 @@ +use crate::config::Duration; use serde_derive::Deserialize; #[derive(Clone, Debug, Deserialize)] @@ -5,7 +6,7 @@ use serde_derive::Deserialize; pub struct RateLimit { pub(in crate::config) name: String, pub(in crate::config) number: usize, - pub(in crate::config) period: String, + pub(in crate::config) period: Duration, } #[cfg(test)] @@ -30,7 +31,7 @@ period = "20s" let rl: RateLimit = load_str(cfg).unwrap(); assert_eq!(rl.name, "test"); assert_eq!(rl.number, 20); - assert_eq!(rl.period, "20s"); + assert_eq!(rl.period, Duration::from_secs(20)); } #[test]