1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
use crate::args::Args;
use std::collections::HashSet;
use crate::errors::*;
use serde::de::DeserializeOwned;
use serde::Deserialize;
use std::ffi::OsStr;
use std::fs;
use std::path::Path;
use std::path::PathBuf;

pub const DEFAULT_RENEW_IF_DAYS_LEFT: i64 = 30;

#[derive(Debug, PartialEq, Deserialize)]
pub struct ConfigFile {
    #[serde(default)]
    pub acme: AcmeConfig,
}

#[derive(Debug, Default, PartialEq, Deserialize)]
pub struct AcmeConfig {
    pub acme_email: Option<String>,
    pub acme_url: Option<String>,
    pub renew_if_days_left: Option<i64>,
}

#[derive(Debug, PartialEq, Deserialize)]
pub struct CertConfigFile {
    cert: CertConfig,
}

fn load_str<T: DeserializeOwned>(s: &str) -> Result<T> {
    let conf = toml::from_str(&s).context("Failed to load config")?;
    Ok(conf)
}

fn load_file<P: AsRef<Path>, T: DeserializeOwned>(path: P) -> Result<T> {
    let buf = fs::read_to_string(path.as_ref()).context("Failed to read file")?;
    load_str(&buf)
}

fn load_from_folder<P: AsRef<Path>>(path: P) -> Result<Vec<CertConfigFile>> {
    let mut configs = Vec::new();
    let iter = fs::read_dir(path.as_ref())
        .with_context(|| anyhow!("Failed to list directory: {:?}", path.as_ref()))?;

    for file in iter {
        let file = file?;
        let path = file.path();

        if path.extension() == Some(OsStr::new("conf")) {
            let c = load_file(&path)
                .with_context(|| anyhow!("Failed to load config file {:?}", path))?;
            configs.push(c);
        } else {
            debug!("skipping non-config file {:?}", path);
        }
    }
    Ok(configs)
}

#[derive(Debug, PartialEq, Clone, Deserialize)]
pub struct CertConfig {
    pub name: String,
    pub dns_names: Vec<String>,
    #[serde(default)]
    pub must_staple: bool,
    #[serde(default)]
    pub exec: Vec<String>,
}

#[derive(Debug, Clone)]
pub struct Config {
    pub certs: Vec<CertConfig>,
    pub acme_email: Option<String>,
    pub acme_url: String,
    pub renew_if_days_left: i64,
    pub data_dir: PathBuf,
    pub chall_dir: PathBuf,
}

impl Config {
    pub fn filter_certs<'a>(&'a self, filter: &'a HashSet<String>) -> impl Iterator<Item = &'a CertConfig> {
        self.certs.iter().filter(move |cert| {
            filter.is_empty() || filter.contains(&cert.name)
        })
    }
}

pub fn load(args: &Args) -> Result<Config> {
    // TODO: none of this is applied yet, we need to change all the arg parsing code for that
    let path = &args.config;
    let mut config = load_file::<_, ConfigFile>(path)
        .with_context(|| anyhow!("Failed to load config file {:?}", path))?;

    if args.acme_email.is_some() {
        config.acme.acme_email = args.acme_email.clone();
    }

    let certs = load_from_folder(&args.config_dir)?
        .into_iter()
        .map(|c| c.cert)
        .collect();
    Ok(Config {
        acme_email: args.acme_email.clone(),
        acme_url: args.acme_url.to_string(),
        renew_if_days_left: config
            .acme
            .renew_if_days_left
            .unwrap_or(DEFAULT_RENEW_IF_DAYS_LEFT),
        data_dir: PathBuf::from(&args.data_dir),
        chall_dir: PathBuf::from(&args.chall_dir),
        certs,
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn minimal_cert_conf() {
        let conf = load_str::<CertConfigFile>(
            r#"
            [cert]
            name = "example.com"
            dns_names = ["example.com", "www.example.com"]
        "#,
        )
        .unwrap();

        assert_eq!(
            conf,
            CertConfigFile {
                cert: CertConfig {
                    name: "example.com".to_string(),
                    dns_names: vec!["example.com".to_string(), "www.example.com".to_string(),],
                    must_staple: false,
                    exec: vec![],
                },
            }
        );
    }
}