mirror of https://github.com/breard-r/acmed.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
753 lines
19 KiB
753 lines
19 KiB
use crate::duration::parse_duration;
|
|
use crate::hooks;
|
|
use crate::identifier::IdentifierType;
|
|
use crate::storage::FileManager;
|
|
use acme_common::b64_decode;
|
|
use acme_common::crypto::{HashFunction, JwsSignatureAlgorithm, KeyType, SubjectAttribute};
|
|
use acme_common::error::Error;
|
|
use glob::glob;
|
|
use log::info;
|
|
use serde::{de, Deserialize, Deserializer};
|
|
use std::collections::{BTreeSet, HashMap};
|
|
use std::fmt;
|
|
use std::fs::{self, File};
|
|
use std::io::prelude::*;
|
|
use std::path::{Path, PathBuf};
|
|
use std::result::Result;
|
|
use std::time::Duration;
|
|
|
|
macro_rules! set_cfg_attr {
|
|
($to: expr, $from: expr) => {
|
|
if let Some(v) = $from {
|
|
$to = Some(v);
|
|
};
|
|
};
|
|
}
|
|
|
|
macro_rules! push_subject_attr {
|
|
($hm: expr, $attr: expr, $attr_type: ident) => {
|
|
if let Some(v) = &$attr {
|
|
$hm.insert(SubjectAttribute::$attr_type, v.to_owned());
|
|
}
|
|
};
|
|
}
|
|
|
|
fn get_stdin(hook: &Hook) -> Result<hooks::HookStdin, Error> {
|
|
match &hook.stdin {
|
|
Some(file) => match &hook.stdin_str {
|
|
Some(_) => {
|
|
let msg = format!(
|
|
"{}: a hook cannot have both stdin and stdin_str",
|
|
&hook.name
|
|
);
|
|
Err(msg.into())
|
|
}
|
|
None => Ok(hooks::HookStdin::File(file.to_string())),
|
|
},
|
|
None => match &hook.stdin_str {
|
|
Some(s) => Ok(hooks::HookStdin::Str(s.to_string())),
|
|
None => Ok(hooks::HookStdin::None),
|
|
},
|
|
}
|
|
}
|
|
|
|
#[derive(Default, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct Config {
|
|
pub global: Option<GlobalOptions>,
|
|
#[serde(default)]
|
|
pub endpoint: Vec<Endpoint>,
|
|
#[serde(default, rename = "rate-limit")]
|
|
pub rate_limit: Vec<RateLimit>,
|
|
#[serde(default)]
|
|
pub hook: Vec<Hook>,
|
|
#[serde(default)]
|
|
pub group: Vec<Group>,
|
|
#[serde(default)]
|
|
pub account: Vec<Account>,
|
|
#[serde(default)]
|
|
pub certificate: Vec<Certificate>,
|
|
#[serde(default)]
|
|
pub include: Vec<String>,
|
|
}
|
|
|
|
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 {
|
|
let account_dir = match &self.global {
|
|
Some(g) => match &g.accounts_directory {
|
|
Some(d) => d,
|
|
None => crate::DEFAULT_ACCOUNTS_DIR,
|
|
},
|
|
None => crate::DEFAULT_ACCOUNTS_DIR,
|
|
};
|
|
account_dir.to_string()
|
|
}
|
|
|
|
pub fn get_hook(&self, name: &str) -> Result<Vec<hooks::Hook>, Error> {
|
|
for hook in self.hook.iter() {
|
|
if name == hook.name {
|
|
let h = hooks::Hook {
|
|
name: hook.name.to_owned(),
|
|
hook_type: hook.hook_type.iter().map(|e| e.to_owned()).collect(),
|
|
cmd: hook.cmd.to_owned(),
|
|
args: hook.args.to_owned(),
|
|
stdin: get_stdin(hook)?,
|
|
stdout: hook.stdout.to_owned(),
|
|
stderr: hook.stderr.to_owned(),
|
|
allow_failure: hook
|
|
.allow_failure
|
|
.unwrap_or(crate::DEFAULT_HOOK_ALLOW_FAILURE),
|
|
};
|
|
return Ok(vec![h]);
|
|
}
|
|
}
|
|
for grp in self.group.iter() {
|
|
if name == grp.name {
|
|
let mut ret = vec![];
|
|
for hook_name in grp.hooks.iter() {
|
|
let mut h = self.get_hook(hook_name)?;
|
|
ret.append(&mut h);
|
|
}
|
|
return Ok(ret);
|
|
}
|
|
}
|
|
Err(format!("{}: hook not found", name).into())
|
|
}
|
|
|
|
pub fn get_cert_file_mode(&self) -> u32 {
|
|
match &self.global {
|
|
Some(g) => match g.cert_file_mode {
|
|
Some(m) => m,
|
|
None => crate::DEFAULT_CERT_FILE_MODE,
|
|
},
|
|
None => crate::DEFAULT_CERT_FILE_MODE,
|
|
}
|
|
}
|
|
|
|
pub fn get_cert_file_user(&self) -> Option<String> {
|
|
match &self.global {
|
|
Some(g) => g.cert_file_user.to_owned(),
|
|
None => None,
|
|
}
|
|
}
|
|
|
|
pub fn get_cert_file_group(&self) -> Option<String> {
|
|
match &self.global {
|
|
Some(g) => g.cert_file_group.to_owned(),
|
|
None => None,
|
|
}
|
|
}
|
|
|
|
pub fn get_pk_file_mode(&self) -> u32 {
|
|
match &self.global {
|
|
Some(g) => match g.pk_file_mode {
|
|
Some(m) => m,
|
|
None => crate::DEFAULT_PK_FILE_MODE,
|
|
},
|
|
None => crate::DEFAULT_PK_FILE_MODE,
|
|
}
|
|
}
|
|
|
|
pub fn get_pk_file_user(&self) -> Option<String> {
|
|
match &self.global {
|
|
Some(g) => g.pk_file_user.to_owned(),
|
|
None => None,
|
|
}
|
|
}
|
|
|
|
pub fn get_pk_file_group(&self) -> Option<String> {
|
|
match &self.global {
|
|
Some(g) => g.pk_file_group.to_owned(),
|
|
None => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct GlobalOptions {
|
|
pub accounts_directory: Option<String>,
|
|
pub cert_file_group: Option<String>,
|
|
pub cert_file_mode: Option<u32>,
|
|
pub cert_file_user: Option<String>,
|
|
pub certificates_directory: Option<String>,
|
|
#[serde(default)]
|
|
pub env: HashMap<String, String>,
|
|
pub file_name_format: Option<String>,
|
|
pub pk_file_group: Option<String>,
|
|
pub pk_file_mode: Option<u32>,
|
|
pub pk_file_user: Option<String>,
|
|
pub renew_delay: Option<String>,
|
|
pub root_certificates: Option<Vec<String>>,
|
|
}
|
|
|
|
impl GlobalOptions {
|
|
pub fn get_renew_delay(&self) -> Result<Duration, Error> {
|
|
match &self.renew_delay {
|
|
Some(d) => parse_duration(d),
|
|
None => Ok(Duration::new(crate::DEFAULT_CERT_RENEW_DELAY, 0)),
|
|
}
|
|
}
|
|
|
|
pub fn get_crt_name_format(&self) -> String {
|
|
match &self.file_name_format {
|
|
Some(n) => n.to_string(),
|
|
None => crate::DEFAULT_CERT_FORMAT.to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct Endpoint {
|
|
pub file_name_format: Option<String>,
|
|
pub name: String,
|
|
#[serde(default)]
|
|
pub rate_limits: Vec<String>,
|
|
pub renew_delay: Option<String>,
|
|
pub root_certificates: Option<Vec<String>>,
|
|
pub tos_agreed: bool,
|
|
pub url: String,
|
|
}
|
|
|
|
impl Endpoint {
|
|
pub fn get_renew_delay(&self, cnf: &Config) -> Result<Duration, Error> {
|
|
match &self.renew_delay {
|
|
Some(d) => parse_duration(d),
|
|
None => match &cnf.global {
|
|
Some(g) => g.get_renew_delay(),
|
|
None => Ok(Duration::new(crate::DEFAULT_CERT_RENEW_DELAY, 0)),
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn get_crt_name_format(&self, cnf: &Config) -> String {
|
|
match &self.file_name_format {
|
|
Some(n) => n.to_string(),
|
|
None => match &cnf.global {
|
|
Some(g) => g.get_crt_name_format(),
|
|
None => crate::DEFAULT_CERT_FORMAT.to_string(),
|
|
},
|
|
}
|
|
}
|
|
|
|
fn to_generic(
|
|
&self,
|
|
cnf: &Config,
|
|
root_certs: &[&str],
|
|
) -> Result<crate::endpoint::Endpoint, Error> {
|
|
let mut limits = vec![];
|
|
for rl_name in self.rate_limits.iter() {
|
|
let (nb, timeframe) = cnf.get_rate_limit(rl_name)?;
|
|
limits.push((nb, timeframe));
|
|
}
|
|
let mut root_lst: Vec<String> = vec![];
|
|
root_lst.extend(root_certs.iter().map(|v| v.to_string()));
|
|
if let Some(crt_lst) = &self.root_certificates {
|
|
root_lst.extend(crt_lst.iter().map(|v| v.to_owned()));
|
|
}
|
|
if let Some(glob) = &cnf.global {
|
|
if let Some(crt_lst) = &glob.root_certificates {
|
|
root_lst.extend(crt_lst.iter().map(|v| v.to_owned()));
|
|
}
|
|
}
|
|
crate::endpoint::Endpoint::new(
|
|
&self.name,
|
|
&self.url,
|
|
self.tos_agreed,
|
|
&limits,
|
|
root_lst.as_slice(),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct RateLimit {
|
|
pub name: String,
|
|
pub number: usize,
|
|
pub period: String,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct Hook {
|
|
pub allow_failure: Option<bool>,
|
|
pub args: Option<Vec<String>>,
|
|
pub cmd: String,
|
|
pub name: String,
|
|
pub stderr: Option<String>,
|
|
pub stdin: Option<String>,
|
|
pub stdin_str: Option<String>,
|
|
pub stdout: Option<String>,
|
|
#[serde(rename = "type")]
|
|
pub hook_type: Vec<HookType>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub enum HookType {
|
|
FilePreCreate,
|
|
FilePostCreate,
|
|
FilePreEdit,
|
|
FilePostEdit,
|
|
#[serde(rename = "challenge-http-01")]
|
|
ChallengeHttp01,
|
|
#[serde(rename = "challenge-http-01-clean")]
|
|
ChallengeHttp01Clean,
|
|
#[serde(rename = "challenge-dns-01")]
|
|
ChallengeDns01,
|
|
#[serde(rename = "challenge-dns-01-clean")]
|
|
ChallengeDns01Clean,
|
|
#[serde(rename = "challenge-tls-alpn-01")]
|
|
ChallengeTlsAlpn01,
|
|
#[serde(rename = "challenge-tls-alpn-01-clean")]
|
|
ChallengeTlsAlpn01Clean,
|
|
PostOperation,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct Group {
|
|
pub hooks: Vec<String>,
|
|
pub name: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct ExternalAccount {
|
|
pub identifier: String,
|
|
pub key: String,
|
|
pub signature_algorithm: Option<String>,
|
|
}
|
|
|
|
impl ExternalAccount {
|
|
pub fn to_generic(&self) -> Result<crate::account::ExternalAccount, Error> {
|
|
let signature_algorithm = match &self.signature_algorithm {
|
|
Some(a) => a.parse()?,
|
|
None => crate::DEFAULT_EXTERNAL_ACCOUNT_JWA,
|
|
};
|
|
match signature_algorithm {
|
|
JwsSignatureAlgorithm::Hs256
|
|
| JwsSignatureAlgorithm::Hs384
|
|
| JwsSignatureAlgorithm::Hs512 => {}
|
|
_ => {
|
|
return Err(format!(
|
|
"{}: invalid signature algorithm for external account binding",
|
|
signature_algorithm
|
|
)
|
|
.into());
|
|
}
|
|
};
|
|
Ok(crate::account::ExternalAccount {
|
|
identifier: self.identifier.to_owned(),
|
|
key: b64_decode(&self.key)?,
|
|
signature_algorithm,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct Account {
|
|
pub contacts: Vec<AccountContact>,
|
|
#[serde(default)]
|
|
pub env: HashMap<String, String>,
|
|
pub external_account: Option<ExternalAccount>,
|
|
pub hooks: Option<Vec<String>>,
|
|
pub key_type: Option<String>,
|
|
pub name: String,
|
|
pub signature_algorithm: Option<String>,
|
|
}
|
|
|
|
impl Account {
|
|
pub fn get_hooks(&self, cnf: &Config) -> Result<Vec<hooks::Hook>, Error> {
|
|
let lst = match &self.hooks {
|
|
Some(h) => {
|
|
let mut res = vec![];
|
|
for name in h.iter() {
|
|
let mut h = cnf.get_hook(name)?;
|
|
res.append(&mut h);
|
|
}
|
|
res
|
|
}
|
|
None => vec![],
|
|
};
|
|
Ok(lst)
|
|
}
|
|
|
|
pub fn to_generic(&self, file_manager: &FileManager) -> Result<crate::account::Account, Error> {
|
|
let contacts: Vec<(String, String)> = self
|
|
.contacts
|
|
.iter()
|
|
.map(|e| (e.get_type(), e.get_value()))
|
|
.collect();
|
|
let external_account = match &self.external_account {
|
|
Some(a) => Some(a.to_generic()?),
|
|
None => None,
|
|
};
|
|
crate::account::Account::load(
|
|
file_manager,
|
|
&self.name,
|
|
&contacts,
|
|
&self.key_type,
|
|
&self.signature_algorithm,
|
|
&external_account,
|
|
)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct AccountContact {
|
|
pub mailto: String,
|
|
}
|
|
|
|
impl AccountContact {
|
|
pub fn get_type(&self) -> String {
|
|
"mailto".to_string()
|
|
}
|
|
|
|
pub fn get_value(&self) -> String {
|
|
self.mailto.clone()
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct Certificate {
|
|
pub account: String,
|
|
pub csr_digest: Option<String>,
|
|
pub directory: Option<String>,
|
|
pub endpoint: String,
|
|
#[serde(default)]
|
|
pub env: HashMap<String, String>,
|
|
pub file_name_format: Option<String>,
|
|
pub hooks: Vec<String>,
|
|
pub identifiers: Vec<Identifier>,
|
|
pub key_type: Option<String>,
|
|
pub kp_reuse: Option<bool>,
|
|
pub name: Option<String>,
|
|
pub renew_delay: Option<String>,
|
|
#[serde(default)]
|
|
pub subject_attributes: SubjectAttributes,
|
|
}
|
|
|
|
impl Certificate {
|
|
pub fn get_key_type(&self) -> Result<KeyType, Error> {
|
|
match &self.key_type {
|
|
Some(a) => a.parse(),
|
|
None => Ok(crate::DEFAULT_CERT_KEY_TYPE),
|
|
}
|
|
}
|
|
|
|
pub fn get_csr_digest(&self) -> Result<HashFunction, Error> {
|
|
match &self.csr_digest {
|
|
Some(d) => d.parse(),
|
|
None => Ok(crate::DEFAULT_CSR_DIGEST),
|
|
}
|
|
}
|
|
|
|
pub fn get_identifiers(&self) -> Result<Vec<crate::identifier::Identifier>, Error> {
|
|
let mut ret = vec![];
|
|
for id in self.identifiers.iter() {
|
|
ret.push(id.to_generic()?);
|
|
}
|
|
Ok(ret)
|
|
}
|
|
|
|
pub fn get_kp_reuse(&self) -> bool {
|
|
match self.kp_reuse {
|
|
Some(b) => b,
|
|
None => crate::DEFAULT_KP_REUSE,
|
|
}
|
|
}
|
|
|
|
pub fn get_crt_name(&self) -> Result<String, Error> {
|
|
let name = match &self.name {
|
|
Some(n) => n.to_string(),
|
|
None => {
|
|
let id = self
|
|
.identifiers
|
|
.first()
|
|
.ok_or_else(|| Error::from("certificate has no identifiers"))?;
|
|
id.to_string()
|
|
}
|
|
};
|
|
let name = name.replace(['*', ':', '/'], "_");
|
|
Ok(name)
|
|
}
|
|
|
|
pub fn get_crt_name_format(&self, cnf: &Config) -> Result<String, Error> {
|
|
match &self.file_name_format {
|
|
Some(n) => Ok(n.to_string()),
|
|
None => {
|
|
let ep = self.do_get_endpoint(cnf)?;
|
|
Ok(ep.get_crt_name_format(cnf))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn get_crt_dir(&self, cnf: &Config) -> String {
|
|
let crt_directory = match &self.directory {
|
|
Some(d) => d,
|
|
None => match &cnf.global {
|
|
Some(g) => match &g.certificates_directory {
|
|
Some(d) => d,
|
|
None => crate::DEFAULT_CERT_DIR,
|
|
},
|
|
None => crate::DEFAULT_CERT_DIR,
|
|
},
|
|
};
|
|
crt_directory.to_string()
|
|
}
|
|
|
|
fn do_get_endpoint(&self, cnf: &Config) -> Result<Endpoint, Error> {
|
|
for endpoint in cnf.endpoint.iter() {
|
|
if endpoint.name == self.endpoint {
|
|
return Ok(endpoint.clone());
|
|
}
|
|
}
|
|
Err(format!("{}: unknown endpoint", self.endpoint).into())
|
|
}
|
|
|
|
pub fn get_endpoint(
|
|
&self,
|
|
cnf: &Config,
|
|
root_certs: &[&str],
|
|
) -> Result<crate::endpoint::Endpoint, Error> {
|
|
let endpoint = self.do_get_endpoint(cnf)?;
|
|
endpoint.to_generic(cnf, root_certs)
|
|
}
|
|
|
|
pub fn get_hooks(&self, cnf: &Config) -> Result<Vec<hooks::Hook>, Error> {
|
|
let mut res = vec![];
|
|
for name in self.hooks.iter() {
|
|
let mut h = cnf.get_hook(name)?;
|
|
res.append(&mut h);
|
|
}
|
|
Ok(res)
|
|
}
|
|
|
|
pub fn get_renew_delay(&self, cnf: &Config) -> Result<Duration, Error> {
|
|
match &self.renew_delay {
|
|
Some(d) => parse_duration(d),
|
|
None => {
|
|
let endpoint = self.do_get_endpoint(cnf)?;
|
|
endpoint.get_renew_delay(cnf)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
#[serde(remote = "Self")]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct Identifier {
|
|
pub challenge: String,
|
|
pub dns: Option<String>,
|
|
#[serde(default)]
|
|
pub env: HashMap<String, String>,
|
|
pub ip: Option<String>,
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for Identifier {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
let unchecked = Identifier::deserialize(deserializer)?;
|
|
let filled_nb: u8 = [unchecked.dns.is_some(), unchecked.ip.is_some()]
|
|
.iter()
|
|
.copied()
|
|
.map(u8::from)
|
|
.sum();
|
|
if filled_nb != 1 {
|
|
return Err(de::Error::custom(
|
|
"one and only one of `dns` or `ip` must be specified",
|
|
));
|
|
}
|
|
Ok(unchecked)
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Identifier {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
let s = String::new();
|
|
let msg = self.dns.as_ref().or(self.ip.as_ref()).unwrap_or(&s);
|
|
write!(f, "{}", msg)
|
|
}
|
|
}
|
|
|
|
impl Identifier {
|
|
fn to_generic(&self) -> Result<crate::identifier::Identifier, Error> {
|
|
let (t, v) = match &self.dns {
|
|
Some(d) => (IdentifierType::Dns, d),
|
|
None => match &self.ip {
|
|
Some(ip) => (IdentifierType::Ip, ip),
|
|
None => {
|
|
return Err("no identifier found".into());
|
|
}
|
|
},
|
|
};
|
|
crate::identifier::Identifier::new(t, v, &self.challenge, &self.env)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct SubjectAttributes {
|
|
pub country_name: Option<String>,
|
|
pub generation_qualifier: Option<String>,
|
|
pub given_name: Option<String>,
|
|
pub initials: Option<String>,
|
|
pub locality_name: Option<String>,
|
|
pub name: Option<String>,
|
|
pub organization_name: Option<String>,
|
|
pub organizational_unit_name: Option<String>,
|
|
pub pkcs9_email_address: Option<String>,
|
|
pub postal_address: Option<String>,
|
|
pub postal_code: Option<String>,
|
|
pub state_or_province_name: Option<String>,
|
|
pub street: Option<String>,
|
|
pub surname: Option<String>,
|
|
pub title: Option<String>,
|
|
}
|
|
|
|
impl SubjectAttributes {
|
|
pub fn to_generic(&self) -> HashMap<SubjectAttribute, String> {
|
|
let mut ret = HashMap::new();
|
|
push_subject_attr!(ret, self.country_name, CountryName);
|
|
push_subject_attr!(ret, self.generation_qualifier, GenerationQualifier);
|
|
push_subject_attr!(ret, self.given_name, GivenName);
|
|
push_subject_attr!(ret, self.initials, Initials);
|
|
push_subject_attr!(ret, self.locality_name, LocalityName);
|
|
push_subject_attr!(ret, self.name, Name);
|
|
push_subject_attr!(ret, self.organization_name, OrganizationName);
|
|
push_subject_attr!(ret, self.organizational_unit_name, OrganizationalUnitName);
|
|
push_subject_attr!(ret, self.pkcs9_email_address, Pkcs9EmailAddress);
|
|
push_subject_attr!(ret, self.postal_address, PostalAddress);
|
|
push_subject_attr!(ret, self.postal_code, PostalCode);
|
|
push_subject_attr!(ret, self.state_or_province_name, StateOrProvinceName);
|
|
push_subject_attr!(ret, self.street, Street);
|
|
push_subject_attr!(ret, self.surname, Surname);
|
|
push_subject_attr!(ret, self.title, Title);
|
|
ret
|
|
}
|
|
}
|
|
|
|
fn create_dir(path: &str) -> Result<(), Error> {
|
|
if Path::new(path).is_dir() {
|
|
Ok(())
|
|
} else {
|
|
fs::create_dir_all(path)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn init_directories(config: &Config) -> Result<(), Error> {
|
|
create_dir(&config.get_account_dir())?;
|
|
for crt in config.certificate.iter() {
|
|
create_dir(&crt.get_crt_dir(config))?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn get_cnf_path(from: &Path, file: &str) -> Result<Vec<PathBuf>, Error> {
|
|
let mut path = from.to_path_buf().canonicalize()?;
|
|
path.pop();
|
|
path.push(file);
|
|
let err = format!("{:?}: invalid UTF-8 path", path);
|
|
let raw_path = path.to_str().ok_or(err)?;
|
|
let g = glob(raw_path)?
|
|
.filter_map(Result::ok)
|
|
.collect::<Vec<PathBuf>>();
|
|
if g.is_empty() {
|
|
log::warn!(
|
|
"pattern `{}` (expanded as `{}`): no matching configuration file found",
|
|
file,
|
|
raw_path
|
|
);
|
|
}
|
|
Ok(g)
|
|
}
|
|
|
|
fn read_cnf(path: &Path, loaded_files: &mut BTreeSet<PathBuf>) -> Result<Config, Error> {
|
|
let path = path
|
|
.canonicalize()
|
|
.map_err(|e| Error::from(e).prefix(&path.display().to_string()))?;
|
|
if loaded_files.contains(&path) {
|
|
info!("{}: configuration file already loaded", path.display());
|
|
return Ok(Config::default());
|
|
}
|
|
loaded_files.insert(path.clone());
|
|
info!("{}: loading configuration file", &path.display());
|
|
let mut file =
|
|
File::open(&path).map_err(|e| Error::from(e).prefix(&path.display().to_string()))?;
|
|
let mut contents = String::new();
|
|
file.read_to_string(&mut contents)
|
|
.map_err(|e| Error::from(e).prefix(&path.display().to_string()))?;
|
|
let mut config: Config = toml::from_str(&contents)
|
|
.map_err(|e| Error::from(e).prefix(&path.display().to_string()))?;
|
|
for cnf_name in config.include.iter() {
|
|
for cnf_path in get_cnf_path(&path, cnf_name)? {
|
|
let mut add_cnf = read_cnf(&cnf_path, loaded_files)?;
|
|
config.endpoint.append(&mut add_cnf.endpoint);
|
|
config.rate_limit.append(&mut add_cnf.rate_limit);
|
|
config.hook.append(&mut add_cnf.hook);
|
|
config.group.append(&mut add_cnf.group);
|
|
config.account.append(&mut add_cnf.account);
|
|
config.certificate.append(&mut add_cnf.certificate);
|
|
if config.global.is_none() {
|
|
config.global = add_cnf.global;
|
|
} else if let Some(new_glob) = add_cnf.global {
|
|
let mut tmp_glob = config.global.clone().unwrap();
|
|
set_cfg_attr!(tmp_glob.accounts_directory, new_glob.accounts_directory);
|
|
set_cfg_attr!(
|
|
tmp_glob.certificates_directory,
|
|
new_glob.certificates_directory
|
|
);
|
|
set_cfg_attr!(tmp_glob.cert_file_mode, new_glob.cert_file_mode);
|
|
set_cfg_attr!(tmp_glob.cert_file_user, new_glob.cert_file_user);
|
|
set_cfg_attr!(tmp_glob.cert_file_group, new_glob.cert_file_group);
|
|
set_cfg_attr!(tmp_glob.pk_file_mode, new_glob.pk_file_mode);
|
|
set_cfg_attr!(tmp_glob.pk_file_user, new_glob.pk_file_user);
|
|
set_cfg_attr!(tmp_glob.pk_file_group, new_glob.pk_file_group);
|
|
config.global = Some(tmp_glob);
|
|
}
|
|
}
|
|
}
|
|
Ok(config)
|
|
}
|
|
|
|
fn dispatch_global_env_vars(config: &mut Config) {
|
|
if let Some(glob) = &config.global {
|
|
if !glob.env.is_empty() {
|
|
for mut cert in config.certificate.iter_mut() {
|
|
let mut new_vars = glob.env.clone();
|
|
for (k, v) in cert.env.iter() {
|
|
new_vars.insert(k.to_string(), v.to_string());
|
|
}
|
|
cert.env = new_vars;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn from_file(file_name: &str) -> Result<Config, Error> {
|
|
let path = PathBuf::from(file_name);
|
|
let mut loaded_files = BTreeSet::new();
|
|
let mut config = read_cnf(&path, &mut loaded_files)?;
|
|
dispatch_global_env_vars(&mut config);
|
|
init_directories(&config)?;
|
|
Ok(config)
|
|
}
|