mirror of https://github.com/breard-r/acmed.git
Rodolphe Bréard
4 weeks ago
Failed to extract signature
22 changed files with 1539 additions and 215 deletions
-
4.gitignore
-
75CHANGELOG.md
-
160Cargo.lock
-
6Cargo.toml
-
4Makefile
-
9config/acmed.toml
-
157config/default_hooks.toml
-
22man/en/acmed.8
-
16man/en/acmed.toml.5
-
31src/cli.rs
-
161src/config.rs
-
243src/config/account.rs
-
334src/config/certificate.rs
-
83src/config/endpoint.rs
-
142src/config/global.rs
-
196src/config/hook.rs
-
68src/config/rate_limit.rs
-
23src/main.rs
-
0tests/config/empty/.dummy
-
9tests/config/override/00_initial.toml
-
1tests/config/override/01_override.toml
-
10tests/config/simple/simple.toml
@ -1,9 +0,0 @@ |
|||||
# ------------------------------------------------------------------------ |
|
||||
# Base configruation for ACMEd |
|
||||
# You should adapt this file to include/enhance with your custom toml's |
|
||||
# ------------------------------------------------------------------------ |
|
||||
|
|
||||
include = [ |
|
||||
"default_hooks.toml", |
|
||||
"letsencrypt.toml", |
|
||||
] |
|
@ -1,157 +0,0 @@ |
|||||
# Copyright (c) 2019-2020 Rodolphe Bréard <rodolphe@breard.tf> |
|
||||
# |
|
||||
# Copying and distribution of this file, with or without modification, |
|
||||
# are permitted in any medium without royalty provided the copyright |
|
||||
# notice and this notice are preserved. This file is offered as-is, |
|
||||
# without any warranty. |
|
||||
|
|
||||
# ------------------------------------------------------------------------ |
|
||||
# Default hooks for ACMEd |
|
||||
# You should not edit this file since it may be overridden by a newer one. |
|
||||
# ------------------------------------------------------------------------ |
|
||||
|
|
||||
|
|
||||
# |
|
||||
# http-01 challenge in "/var/www/{{ identifier }}/" |
|
||||
# |
|
||||
|
|
||||
[[hook]] |
|
||||
name = "http-01-echo-mkdir" |
|
||||
type = ["challenge-http-01"] |
|
||||
cmd = "mkdir" |
|
||||
args = [ |
|
||||
"-m", "0755", |
|
||||
"-p", "{{ env.HTTP_ROOT | default('/var/www') }}/{{ identifier }}/.well-known/acme-challenge" |
|
||||
] |
|
||||
allow_failure = true |
|
||||
|
|
||||
[[hook]] |
|
||||
name = "http-01-echo-echo" |
|
||||
type = ["challenge-http-01"] |
|
||||
cmd = "echo" |
|
||||
args = ["{{ proof }}"] |
|
||||
stdout = "{{ env.HTTP_ROOT | default('/var/www') }}/{{ identifier }}/.well-known/acme-challenge/{{ file_name }}" |
|
||||
|
|
||||
[[hook]] |
|
||||
name = "http-01-echo-chmod" |
|
||||
type = ["challenge-http-01"] |
|
||||
cmd = "chmod" |
|
||||
args = [ |
|
||||
"a+r", |
|
||||
"{{ env.HTTP_ROOT | default('/var/www') }}/{{ identifier }}/.well-known/acme-challenge/{{ file_name }}" |
|
||||
] |
|
||||
allow_failure = true |
|
||||
|
|
||||
[[hook]] |
|
||||
name = "http-01-echo-clean" |
|
||||
type = ["challenge-http-01-clean"] |
|
||||
cmd = "rm" |
|
||||
args = [ |
|
||||
"-f", |
|
||||
"{{ env.HTTP_ROOT | default('/var/www') }}/{{ identifier }}/.well-known/acme-challenge/{{ file_name }}" |
|
||||
] |
|
||||
allow_failure = true |
|
||||
|
|
||||
[[group]] |
|
||||
name = "http-01-echo" |
|
||||
hooks = [ |
|
||||
"http-01-echo-mkdir", |
|
||||
"http-01-echo-echo", |
|
||||
"http-01-echo-chmod", |
|
||||
"http-01-echo-clean" |
|
||||
] |
|
||||
|
|
||||
|
|
||||
# |
|
||||
# tls-alpn-01 challenge with tacd |
|
||||
# |
|
||||
|
|
||||
[[hook]] |
|
||||
name = "tls-alpn-01-tacd-start-tcp" |
|
||||
type = ["challenge-tls-alpn-01"] |
|
||||
cmd = "tacd" |
|
||||
args = [ |
|
||||
"--pid-file", "{{ env.TACD_PID_ROOT | default('/run') }}/tacd_{{ identifier }}.pid", |
|
||||
"--domain", "{{ identifier_tls_alpn }}", |
|
||||
"--acme-ext", "{{ proof }}", |
|
||||
"--listen", "{{ env.TACD_PORT | default('5001') }}" |
|
||||
] |
|
||||
|
|
||||
[[hook]] |
|
||||
name = "tls-alpn-01-tacd-start-unix" |
|
||||
type = ["challenge-tls-alpn-01"] |
|
||||
cmd = "tacd" |
|
||||
args = [ |
|
||||
"--pid-file", "{{ env.TACD_PID_ROOT | default('/run') }}/tacd_{{ identifier }}.pid", |
|
||||
"--domain", "{{ identifier_tls_alpn }}", |
|
||||
"--acme-ext", "{{ proof }}", |
|
||||
"--listen", "unix:{{ env.TACD_SOCK_ROOT | default('/run') }}/tacd_{{ identifier }}.sock" |
|
||||
] |
|
||||
|
|
||||
[[hook]] |
|
||||
name = "tls-alpn-01-tacd-kill" |
|
||||
type = ["challenge-tls-alpn-01-clean"] |
|
||||
cmd = "pkill" |
|
||||
args = [ |
|
||||
"-F", "{{ env.TACD_PID_ROOT | default('/run') }}/tacd_{{ identifier }}.pid", |
|
||||
] |
|
||||
allow_failure = true |
|
||||
|
|
||||
[[hook]] |
|
||||
name = "tls-alpn-01-tacd-rm" |
|
||||
type = ["challenge-tls-alpn-01-clean"] |
|
||||
cmd = "rm" |
|
||||
args = [ |
|
||||
"-f", "{{ env.TACD_PID_ROOT | default('/run') }}/tacd_{{ identifier }}.pid", |
|
||||
] |
|
||||
allow_failure = true |
|
||||
|
|
||||
[[group]] |
|
||||
name = "tls-alpn-01-tacd-tcp" |
|
||||
hooks = ["tls-alpn-01-tacd-start-tcp", "tls-alpn-01-tacd-kill", "tls-alpn-01-tacd-rm"] |
|
||||
|
|
||||
[[group]] |
|
||||
name = "tls-alpn-01-tacd-unix" |
|
||||
hooks = ["tls-alpn-01-tacd-start-unix", "tls-alpn-01-tacd-kill", "tls-alpn-01-tacd-rm"] |
|
||||
|
|
||||
|
|
||||
# |
|
||||
# Git storage hook |
|
||||
# |
|
||||
|
|
||||
[[hook]] |
|
||||
name = "git-init" |
|
||||
type = ["file-pre-create", "file-pre-edit"] |
|
||||
cmd = "git" |
|
||||
args = [ |
|
||||
"init", |
|
||||
"{{ file_directory }}" |
|
||||
] |
|
||||
|
|
||||
[[hook]] |
|
||||
name = "git-add" |
|
||||
type = ["file-post-create", "file-post-edit"] |
|
||||
cmd = "git" |
|
||||
args = [ |
|
||||
"-C", "{{ file_directory }}", |
|
||||
"add", "{{ file_name }}" |
|
||||
] |
|
||||
allow_failure = true |
|
||||
|
|
||||
[[hook]] |
|
||||
name = "git-commit" |
|
||||
type = ["file-post-create", "file-post-edit"] |
|
||||
cmd = "git" |
|
||||
args = [ |
|
||||
"-C", "{{ file_directory }}", |
|
||||
"-c", "user.name='{{ env.GIT_USERNAME | default('ACMEd') }}'", |
|
||||
"-c", "user.email='{{ env.GIT_EMAIL | default('acmed@localhost') }}'", |
|
||||
"commit", |
|
||||
"-m", "{{ file_name }}", |
|
||||
"--only", "{{ file_name }}" |
|
||||
] |
|
||||
allow_failure = true |
|
||||
|
|
||||
[[group]] |
|
||||
name = "git" |
|
||||
hooks = ["git-init", "git-add", "git-commit"] |
|
@ -0,0 +1,161 @@ |
|||||
|
mod account;
|
||||
|
mod certificate;
|
||||
|
mod endpoint;
|
||||
|
mod global;
|
||||
|
mod hook;
|
||||
|
mod rate_limit;
|
||||
|
|
||||
|
pub use account::*;
|
||||
|
pub use certificate::*;
|
||||
|
pub use endpoint::*;
|
||||
|
pub use global::*;
|
||||
|
pub use hook::*;
|
||||
|
pub use rate_limit::*;
|
||||
|
|
||||
|
use anyhow::{Context, Result};
|
||||
|
use config::{Config, File};
|
||||
|
use serde_derive::Deserialize;
|
||||
|
use std::path::{Path, PathBuf};
|
||||
|
use walkdir::WalkDir;
|
||||
|
|
||||
|
const ALLOWED_FILE_EXT: &[&str] = &["toml"];
|
||||
|
|
||||
|
#[derive(Debug, Deserialize)]
|
||||
|
#[serde(deny_unknown_fields)]
|
||||
|
pub struct AcmedConfig {
|
||||
|
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>,
|
||||
|
}
|
||||
|
|
||||
|
pub fn load<P: AsRef<Path>>(config_dir: P) -> Result<AcmedConfig> {
|
||||
|
let config_dir = config_dir.as_ref();
|
||||
|
tracing::debug!("loading config directory: {config_dir:?}");
|
||||
|
let settings = Config::builder()
|
||||
|
.add_source(
|
||||
|
get_files(config_dir)?
|
||||
|
.iter()
|
||||
|
.map(|path| File::from(path.as_path()))
|
||||
|
.collect::<Vec<_>>(),
|
||||
|
)
|
||||
|
.build()?;
|
||||
|
tracing::trace!("loaded config: {settings:?}");
|
||||
|
let config: AcmedConfig = settings.try_deserialize().context("invalid setting")?;
|
||||
|
tracing::debug!("computed config: {config:?}");
|
||||
|
Ok(config)
|
||||
|
}
|
||||
|
|
||||
|
fn get_files(config_dir: &Path) -> Result<Vec<PathBuf>> {
|
||||
|
let mut file_lst = Vec::new();
|
||||
|
for entry in WalkDir::new(config_dir).follow_links(true) {
|
||||
|
let path = entry?.path().to_path_buf();
|
||||
|
if path.is_file() {
|
||||
|
if let Some(ext) = path.extension() {
|
||||
|
if ALLOWED_FILE_EXT.iter().any(|&e| e == ext) {
|
||||
|
std::fs::File::open(&path).with_context(|| path.display().to_string())?;
|
||||
|
file_lst.push(path);
|
||||
|
}
|
||||
|
}
|
||||
|
}
|
||||
|
}
|
||||
|
file_lst.sort();
|
||||
|
tracing::debug!("configuration files found: {file_lst:?}");
|
||||
|
Ok(file_lst)
|
||||
|
}
|
||||
|
|
||||
|
#[cfg(test)]
|
||||
|
fn load_str<'de, T: serde::de::Deserialize<'de>>(config_str: &str) -> Result<T> {
|
||||
|
let settings = Config::builder()
|
||||
|
.add_source(File::from_str(config_str, config::FileFormat::Toml))
|
||||
|
.build()?;
|
||||
|
let config: T = settings.try_deserialize().context("invalid setting")?;
|
||||
|
Ok(config)
|
||||
|
}
|
||||
|
|
||||
|
#[cfg(test)]
|
||||
|
mod tests {
|
||||
|
use super::*;
|
||||
|
|
||||
|
#[test]
|
||||
|
fn empty() {
|
||||
|
let cfg = load("tests/config/empty").unwrap();
|
||||
|
assert!(cfg.global.is_none());
|
||||
|
assert!(cfg.rate_limit.is_empty());
|
||||
|
assert!(cfg.endpoint.is_empty());
|
||||
|
assert!(cfg.hook.is_empty());
|
||||
|
assert!(cfg.group.is_empty());
|
||||
|
assert!(cfg.account.is_empty());
|
||||
|
assert!(cfg.certificate.is_empty());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn simple() {
|
||||
|
let cfg = load("tests/config/simple").unwrap();
|
||||
|
assert!(cfg.global.is_some());
|
||||
|
let global = cfg.global.unwrap();
|
||||
|
assert_eq!(
|
||||
|
global.accounts_directory,
|
||||
|
PathBuf::from("/tmp/example/account/dir")
|
||||
|
);
|
||||
|
assert_eq!(
|
||||
|
global.certificates_directory,
|
||||
|
PathBuf::from("/tmp/example/cert/dir")
|
||||
|
);
|
||||
|
assert!(cfg.rate_limit.is_empty());
|
||||
|
assert!(cfg.endpoint.is_empty());
|
||||
|
assert!(cfg.hook.is_empty());
|
||||
|
assert!(cfg.group.is_empty());
|
||||
|
assert_eq!(cfg.account.len(), 1);
|
||||
|
let account = cfg.account.first().unwrap();
|
||||
|
assert_eq!(account.contacts.len(), 1);
|
||||
|
assert!(account.env.is_empty());
|
||||
|
assert!(account.external_account.is_none());
|
||||
|
assert!(account.hooks.is_empty());
|
||||
|
assert_eq!(account.key_type, AccountKeyType::EcDsaP256);
|
||||
|
assert_eq!(account.name, "example");
|
||||
|
assert_eq!(
|
||||
|
account.signature_algorithm,
|
||||
|
Some(AccountSignatureAlgorithm::Hs384)
|
||||
|
);
|
||||
|
assert!(cfg.certificate.is_empty());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn setting_override() {
|
||||
|
let cfg = load("tests/config/override").unwrap();
|
||||
|
assert!(cfg.global.is_some());
|
||||
|
let global = cfg.global.unwrap();
|
||||
|
assert_eq!(
|
||||
|
global.accounts_directory,
|
||||
|
PathBuf::from("/tmp/other/account/dir")
|
||||
|
);
|
||||
|
assert_eq!(
|
||||
|
global.certificates_directory,
|
||||
|
PathBuf::from("/tmp/example/cert/dir")
|
||||
|
);
|
||||
|
assert!(cfg.rate_limit.is_empty());
|
||||
|
assert!(cfg.endpoint.is_empty());
|
||||
|
assert!(cfg.hook.is_empty());
|
||||
|
assert!(cfg.group.is_empty());
|
||||
|
assert_eq!(cfg.account.len(), 1);
|
||||
|
let account = cfg.account.first().unwrap();
|
||||
|
assert_eq!(account.contacts.len(), 1);
|
||||
|
assert!(account.env.is_empty());
|
||||
|
assert!(account.external_account.is_none());
|
||||
|
assert!(account.hooks.is_empty());
|
||||
|
assert_eq!(account.key_type, AccountKeyType::EcDsaP256);
|
||||
|
assert_eq!(account.name, "example");
|
||||
|
assert!(account.signature_algorithm.is_none());
|
||||
|
assert!(cfg.certificate.is_empty());
|
||||
|
}
|
||||
|
}
|
@ -0,0 +1,243 @@ |
|||||
|
use serde::{de, Deserialize, Deserializer};
|
||||
|
use serde_derive::Deserialize;
|
||||
|
use std::collections::HashMap;
|
||||
|
|
||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||
|
#[serde(remote = "Self")]
|
||||
|
#[serde(deny_unknown_fields)]
|
||||
|
pub struct Account {
|
||||
|
pub contacts: Vec<AccountContact>,
|
||||
|
#[serde(default)]
|
||||
|
pub env: HashMap<String, String>,
|
||||
|
pub external_account: Option<ExternalAccount>,
|
||||
|
#[serde(default)]
|
||||
|
pub hooks: Vec<String>,
|
||||
|
#[serde(default)]
|
||||
|
pub key_type: AccountKeyType,
|
||||
|
pub name: String,
|
||||
|
#[serde(default)]
|
||||
|
pub signature_algorithm: Option<AccountSignatureAlgorithm>,
|
||||
|
}
|
||||
|
|
||||
|
impl<'de> Deserialize<'de> for Account {
|
||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
|
where
|
||||
|
D: Deserializer<'de>,
|
||||
|
{
|
||||
|
let unchecked = Account::deserialize(deserializer)?;
|
||||
|
if unchecked.contacts.is_empty() {
|
||||
|
return Err(de::Error::custom("at least one contact must be specified"));
|
||||
|
}
|
||||
|
Ok(unchecked)
|
||||
|
}
|
||||
|
}
|
||||
|
|
||||
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
|
||||
|
#[serde(deny_unknown_fields)]
|
||||
|
pub struct AccountContact {
|
||||
|
pub mailto: String,
|
||||
|
}
|
||||
|
|
||||
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
|
||||
|
#[serde(deny_unknown_fields)]
|
||||
|
pub struct ExternalAccount {
|
||||
|
pub identifier: String,
|
||||
|
pub key: String,
|
||||
|
#[serde(default)]
|
||||
|
pub signature_algorithm: ExternalAccountSignatureAlgorithm,
|
||||
|
}
|
||||
|
|
||||
|
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
|
||||
|
pub enum AccountKeyType {
|
||||
|
Ed25519,
|
||||
|
Ed448,
|
||||
|
#[default]
|
||||
|
EcDsaP256,
|
||||
|
EcDsaP384,
|
||||
|
EcDsaP521,
|
||||
|
Rsa2048,
|
||||
|
Rsa4096,
|
||||
|
}
|
||||
|
|
||||
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
|
||||
|
pub enum AccountSignatureAlgorithm {
|
||||
|
Hs256,
|
||||
|
Hs384,
|
||||
|
Hs512,
|
||||
|
Rs256,
|
||||
|
}
|
||||
|
|
||||
|
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
|
||||
|
pub enum ExternalAccountSignatureAlgorithm {
|
||||
|
#[default]
|
||||
|
Hs256,
|
||||
|
Hs384,
|
||||
|
Hs512,
|
||||
|
}
|
||||
|
|
||||
|
#[cfg(test)]
|
||||
|
mod tests {
|
||||
|
use super::*;
|
||||
|
use crate::config::load_str;
|
||||
|
|
||||
|
#[test]
|
||||
|
fn empty_account() {
|
||||
|
let res = load_str::<Account>("");
|
||||
|
assert!(res.is_err());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn account_minimal() {
|
||||
|
let cfg = r#"
|
||||
|
name = "test"
|
||||
|
contacts = [
|
||||
|
{ mailto = "acme@example.org" }
|
||||
|
]
|
||||
|
"#;
|
||||
|
|
||||
|
let a: Account = load_str(cfg).unwrap();
|
||||
|
assert_eq!(
|
||||
|
a.contacts,
|
||||
|
vec![AccountContact {
|
||||
|
mailto: "acme@example.org".to_string()
|
||||
|
}]
|
||||
|
);
|
||||
|
assert!(a.env.is_empty());
|
||||
|
assert!(a.external_account.is_none());
|
||||
|
assert!(a.hooks.is_empty());
|
||||
|
assert_eq!(a.key_type, AccountKeyType::EcDsaP256);
|
||||
|
assert_eq!(a.name, "test");
|
||||
|
assert!(a.signature_algorithm.is_none());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn account_full() {
|
||||
|
let cfg = r#"
|
||||
|
name = "test"
|
||||
|
contacts = [
|
||||
|
{ mailto = "acme@example.org" }
|
||||
|
]
|
||||
|
env.TEST = "Test"
|
||||
|
external_account.identifier = "toto"
|
||||
|
external_account.key = "VGhpcyBpcyBhIHRlc3Q="
|
||||
|
hooks = ["hook name"]
|
||||
|
key_type = "rsa2048"
|
||||
|
signature_algorithm = "HS512"
|
||||
|
"#;
|
||||
|
let mut env = HashMap::with_capacity(2);
|
||||
|
env.insert("test".to_string(), "Test".to_string());
|
||||
|
let ea = ExternalAccount {
|
||||
|
identifier: "toto".to_string(),
|
||||
|
key: "VGhpcyBpcyBhIHRlc3Q=".to_string(),
|
||||
|
signature_algorithm: ExternalAccountSignatureAlgorithm::Hs256,
|
||||
|
};
|
||||
|
let a: Account = load_str(cfg).unwrap();
|
||||
|
assert_eq!(
|
||||
|
a.contacts,
|
||||
|
vec![AccountContact {
|
||||
|
mailto: "acme@example.org".to_string()
|
||||
|
}]
|
||||
|
);
|
||||
|
assert_eq!(a.env, env);
|
||||
|
assert_eq!(a.external_account, Some(ea));
|
||||
|
assert_eq!(a.hooks, vec!["hook name".to_string()]);
|
||||
|
assert_eq!(a.key_type, AccountKeyType::Rsa2048);
|
||||
|
assert_eq!(a.name, "test");
|
||||
|
assert_eq!(
|
||||
|
a.signature_algorithm,
|
||||
|
Some(AccountSignatureAlgorithm::Hs512)
|
||||
|
);
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn account_missing_name() {
|
||||
|
let cfg = r#"
|
||||
|
contacts = [
|
||||
|
{ mailto = "acme@example.org" }
|
||||
|
]
|
||||
|
"#;
|
||||
|
let res = load_str::<Account>(cfg);
|
||||
|
assert!(res.is_err());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn account_missing_contact() {
|
||||
|
let cfg = r#"name = "test""#;
|
||||
|
let res = load_str::<Account>(cfg);
|
||||
|
assert!(res.is_err());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn account_empty_contact() {
|
||||
|
let cfg = r#"
|
||||
|
name = "test"
|
||||
|
contacts = []
|
||||
|
"#;
|
||||
|
let res = load_str::<Account>(cfg);
|
||||
|
assert!(res.is_err());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn empty_account_contact() {
|
||||
|
let res = load_str::<AccountContact>("");
|
||||
|
assert!(res.is_err());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn account_contact_mailto() {
|
||||
|
let cfg = r#"mailto = "test.acme@example.org""#;
|
||||
|
let ac: AccountContact = load_str(cfg).unwrap();
|
||||
|
assert_eq!(ac.mailto, "test.acme@example.org");
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn empty_external_account() {
|
||||
|
let res = load_str::<ExternalAccount>("");
|
||||
|
assert!(res.is_err());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn external_account_minimal() {
|
||||
|
let cfg = r#"
|
||||
|
identifier = "toto"
|
||||
|
key = "VGhpcyBpcyBhIHRlc3Q="
|
||||
|
"#;
|
||||
|
let ea: ExternalAccount = load_str(cfg).unwrap();
|
||||
|
assert_eq!(ea.identifier, "toto");
|
||||
|
assert_eq!(ea.key, "VGhpcyBpcyBhIHRlc3Q=");
|
||||
|
assert_eq!(
|
||||
|
ea.signature_algorithm,
|
||||
|
ExternalAccountSignatureAlgorithm::Hs256
|
||||
|
);
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn external_account_full() {
|
||||
|
let cfg = r#"
|
||||
|
identifier = "toto"
|
||||
|
key = "VGhpcyBpcyBhIHRlc3Q="
|
||||
|
signature_algorithm = "HS384"
|
||||
|
"#;
|
||||
|
let ea: ExternalAccount = load_str(cfg).unwrap();
|
||||
|
assert_eq!(ea.identifier, "toto");
|
||||
|
assert_eq!(ea.key, "VGhpcyBpcyBhIHRlc3Q=");
|
||||
|
assert_eq!(
|
||||
|
ea.signature_algorithm,
|
||||
|
ExternalAccountSignatureAlgorithm::Hs384
|
||||
|
);
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn external_account_missing_identifier() {
|
||||
|
let cfg = r#"key = "VGhpcyBpcyBhIHRlc3Q=""#;
|
||||
|
let res = load_str::<ExternalAccount>(cfg);
|
||||
|
assert!(res.is_err());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn external_account_missing_key() {
|
||||
|
let cfg = r#"identifier = "toto""#;
|
||||
|
let res = load_str::<ExternalAccount>(cfg);
|
||||
|
assert!(res.is_err());
|
||||
|
}
|
||||
|
}
|
@ -0,0 +1,334 @@ |
|||||
|
use anyhow::Result;
|
||||
|
use serde::{de, Deserialize, Deserializer};
|
||||
|
use serde_derive::Deserialize;
|
||||
|
use std::collections::HashMap;
|
||||
|
use std::fmt;
|
||||
|
use std::path::PathBuf;
|
||||
|
|
||||
|
#[derive(Debug, Deserialize)]
|
||||
|
#[serde(remote = "Self")]
|
||||
|
#[serde(deny_unknown_fields)]
|
||||
|
pub struct Certificate {
|
||||
|
pub account: String,
|
||||
|
#[serde(default)]
|
||||
|
pub csr_digest: CsrDigest,
|
||||
|
pub directory: Option<PathBuf>,
|
||||
|
pub endpoint: String,
|
||||
|
#[serde(default)]
|
||||
|
pub env: HashMap<String, String>,
|
||||
|
pub file_name_format: Option<String>,
|
||||
|
pub hooks: Vec<String>,
|
||||
|
pub identifiers: Vec<Identifier>,
|
||||
|
#[serde(default)]
|
||||
|
pub key_type: KeyType,
|
||||
|
#[serde(default)]
|
||||
|
pub kp_reuse: bool,
|
||||
|
pub name: Option<String>,
|
||||
|
pub random_early_renew: Option<String>,
|
||||
|
pub renew_delay: Option<String>,
|
||||
|
#[serde(default)]
|
||||
|
pub subject_attributes: SubjectAttributes,
|
||||
|
}
|
||||
|
|
||||
|
impl<'de> Deserialize<'de> for Certificate {
|
||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
|
where
|
||||
|
D: Deserializer<'de>,
|
||||
|
{
|
||||
|
let unchecked = Certificate::deserialize(deserializer)?;
|
||||
|
if unchecked.hooks.is_empty() {
|
||||
|
return Err(de::Error::custom("at least one hook must be specified"));
|
||||
|
}
|
||||
|
if unchecked.identifiers.is_empty() {
|
||||
|
return Err(de::Error::custom(
|
||||
|
"at least one identifier must be specified",
|
||||
|
));
|
||||
|
}
|
||||
|
Ok(unchecked)
|
||||
|
}
|
||||
|
}
|
||||
|
|
||||
|
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
|
||||
|
pub enum CsrDigest {
|
||||
|
#[default]
|
||||
|
Sha256,
|
||||
|
Sha384,
|
||||
|
Sha512,
|
||||
|
}
|
||||
|
|
||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||
|
#[serde(remote = "Self")]
|
||||
|
#[serde(deny_unknown_fields)]
|
||||
|
pub struct Identifier {
|
||||
|
pub challenge: AcmeChallenge,
|
||||
|
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}")
|
||||
|
}
|
||||
|
}
|
||||
|
|
||||
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
|
||||
|
pub enum AcmeChallenge {
|
||||
|
#[serde(rename = "dns-01")]
|
||||
|
Dns01,
|
||||
|
#[serde(rename = "http-01")]
|
||||
|
Http01,
|
||||
|
#[serde(rename = "tls-alpn-01")]
|
||||
|
TlsAlpn01,
|
||||
|
}
|
||||
|
|
||||
|
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
|
||||
|
pub enum KeyType {
|
||||
|
#[serde(rename = "ed25519")]
|
||||
|
Ed25519,
|
||||
|
#[serde(rename = "ed448")]
|
||||
|
Ed448,
|
||||
|
#[serde(rename = "ecdsa_p256")]
|
||||
|
EcDsaP256,
|
||||
|
#[serde(rename = "ecdsa_p384")]
|
||||
|
EcDsaP384,
|
||||
|
#[serde(rename = "ecdsa_p521")]
|
||||
|
EcDsaP521,
|
||||
|
#[default]
|
||||
|
#[serde(rename = "rsa2048")]
|
||||
|
Rsa2048,
|
||||
|
#[serde(rename = "rsa4096")]
|
||||
|
Rsa4096,
|
||||
|
}
|
||||
|
|
||||
|
#[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>,
|
||||
|
}
|
||||
|
|
||||
|
#[cfg(test)]
|
||||
|
mod tests {
|
||||
|
use super::*;
|
||||
|
use crate::config::load_str;
|
||||
|
|
||||
|
#[test]
|
||||
|
fn empty_certificate() {
|
||||
|
let res = load_str::<Certificate>("");
|
||||
|
assert!(res.is_err());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn cert_minimal() {
|
||||
|
let cfg = r#"
|
||||
|
account = "test"
|
||||
|
endpoint = "dummy"
|
||||
|
hooks = ["hook 01"]
|
||||
|
identifiers = [
|
||||
|
{ dns = "example.org", challenge = "http-01"},
|
||||
|
]
|
||||
|
"#;
|
||||
|
let c = load_str::<Certificate>(cfg).unwrap();
|
||||
|
assert_eq!(c.account, "test");
|
||||
|
assert_eq!(c.csr_digest, CsrDigest::Sha256);
|
||||
|
assert!(c.directory.is_none());
|
||||
|
assert_eq!(c.endpoint, "dummy");
|
||||
|
assert!(c.env.is_empty());
|
||||
|
assert!(c.file_name_format.is_none());
|
||||
|
assert_eq!(c.hooks, vec!["hook 01".to_string()]);
|
||||
|
assert_eq!(c.identifiers.len(), 1);
|
||||
|
assert_eq!(c.key_type, KeyType::Rsa2048);
|
||||
|
assert_eq!(c.kp_reuse, false);
|
||||
|
assert!(c.name.is_none());
|
||||
|
assert!(c.random_early_renew.is_none());
|
||||
|
assert!(c.renew_delay.is_none());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn cert_full() {
|
||||
|
let cfg = r#"
|
||||
|
account = "test"
|
||||
|
csr_digest = "sha512"
|
||||
|
directory = "/tmp/certs"
|
||||
|
endpoint = "dummy"
|
||||
|
env.TEST = "some env"
|
||||
|
file_name_format = "test.pem"
|
||||
|
hooks = ["hook 01"]
|
||||
|
identifiers = [
|
||||
|
{ dns = "example.org", challenge = "http-01"},
|
||||
|
]
|
||||
|
key_type = "ecdsa_p256"
|
||||
|
kp_reuse = true
|
||||
|
name = "test"
|
||||
|
random_early_renew = "1d"
|
||||
|
renew_delay = "30d"
|
||||
|
subject_attributes.country_name = "FR"
|
||||
|
subject_attributes.organization_name = "ACME Inc."
|
||||
|
"#;
|
||||
|
let c = load_str::<Certificate>(cfg).unwrap();
|
||||
|
assert_eq!(c.account, "test");
|
||||
|
assert_eq!(c.csr_digest, CsrDigest::Sha512);
|
||||
|
assert_eq!(c.directory, Some(PathBuf::from("/tmp/certs")));
|
||||
|
assert_eq!(c.endpoint, "dummy");
|
||||
|
assert_eq!(c.env.len(), 1);
|
||||
|
assert_eq!(c.file_name_format, Some("test.pem".to_string()));
|
||||
|
assert_eq!(c.hooks, vec!["hook 01".to_string()]);
|
||||
|
assert_eq!(c.identifiers.len(), 1);
|
||||
|
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.subject_attributes.country_name, Some("FR".to_string()));
|
||||
|
assert!(c.subject_attributes.generation_qualifier.is_none());
|
||||
|
assert!(c.subject_attributes.given_name.is_none());
|
||||
|
assert!(c.subject_attributes.initials.is_none());
|
||||
|
assert!(c.subject_attributes.locality_name.is_none());
|
||||
|
assert!(c.subject_attributes.name.is_none());
|
||||
|
assert_eq!(
|
||||
|
c.subject_attributes.organization_name,
|
||||
|
Some("ACME Inc.".to_string())
|
||||
|
);
|
||||
|
assert!(c.subject_attributes.organizational_unit_name.is_none());
|
||||
|
assert!(c.subject_attributes.pkcs9_email_address.is_none());
|
||||
|
assert!(c.subject_attributes.postal_address.is_none());
|
||||
|
assert!(c.subject_attributes.postal_code.is_none());
|
||||
|
assert!(c.subject_attributes.state_or_province_name.is_none());
|
||||
|
assert!(c.subject_attributes.street.is_none());
|
||||
|
assert!(c.subject_attributes.surname.is_none());
|
||||
|
assert!(c.subject_attributes.title.is_none());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn cert_empty_hooks() {
|
||||
|
let cfg = r#"
|
||||
|
account = "test"
|
||||
|
endpoint = "dummy"
|
||||
|
hooks = []
|
||||
|
identifiers = [
|
||||
|
{ dns = "example.org", challenge = "http-01"},
|
||||
|
]
|
||||
|
"#;
|
||||
|
let res = load_str::<Certificate>(cfg);
|
||||
|
assert!(res.is_err());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn cert_empty_identifiers() {
|
||||
|
let cfg = r#"
|
||||
|
account = "test"
|
||||
|
endpoint = "dummy"
|
||||
|
hooks = ["hook 01"]
|
||||
|
identifiers = []
|
||||
|
"#;
|
||||
|
let res = load_str::<Certificate>(cfg);
|
||||
|
assert!(res.is_err());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn empty_identifier() {
|
||||
|
let res = load_str::<Identifier>("");
|
||||
|
assert!(res.is_err());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn identifier_dns() {
|
||||
|
let cfg = r#"
|
||||
|
challenge = "dns-01"
|
||||
|
dns = "example.org"
|
||||
|
"#;
|
||||
|
let i = load_str::<Identifier>(cfg).unwrap();
|
||||
|
assert_eq!(i.challenge, AcmeChallenge::Dns01);
|
||||
|
assert_eq!(i.dns, Some("example.org".to_string()));
|
||||
|
assert!(i.env.is_empty());
|
||||
|
assert!(i.ip.is_none());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn identifier_ipv4() {
|
||||
|
let cfg = r#"
|
||||
|
challenge = "http-01"
|
||||
|
ip = "203.0.113.42"
|
||||
|
"#;
|
||||
|
let i = load_str::<Identifier>(cfg).unwrap();
|
||||
|
assert_eq!(i.challenge, AcmeChallenge::Http01);
|
||||
|
assert!(i.dns.is_none());
|
||||
|
assert!(i.env.is_empty());
|
||||
|
assert_eq!(i.ip, Some("203.0.113.42".to_string()));
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn identifier_ipv6() {
|
||||
|
let cfg = r#"
|
||||
|
challenge = "tls-alpn-01"
|
||||
|
ip = "2001:db8::42"
|
||||
|
"#;
|
||||
|
let i = load_str::<Identifier>(cfg).unwrap();
|
||||
|
assert_eq!(i.challenge, AcmeChallenge::TlsAlpn01);
|
||||
|
assert!(i.dns.is_none());
|
||||
|
assert!(i.env.is_empty());
|
||||
|
assert_eq!(i.ip, Some("2001:db8::42".to_string()));
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn identifier_dns_and_ip() {
|
||||
|
let cfg = r#"
|
||||
|
challenge = "http-01"
|
||||
|
dns = "example.org"
|
||||
|
ip = "203.0.113.42"
|
||||
|
"#;
|
||||
|
let res = load_str::<Identifier>(cfg);
|
||||
|
assert!(res.is_err());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn identifier_missing_challenge() {
|
||||
|
let cfg = r#"ip = "2001:db8::42""#;
|
||||
|
let res = load_str::<Identifier>(cfg);
|
||||
|
assert!(res.is_err());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn identifier_missing_dns_and_ip() {
|
||||
|
let cfg = r#"challenge = "http-01""#;
|
||||
|
let res = load_str::<Identifier>(cfg);
|
||||
|
assert!(res.is_err());
|
||||
|
}
|
||||
|
}
|
@ -0,0 +1,83 @@ |
|||||
|
use serde_derive::Deserialize;
|
||||
|
use std::path::PathBuf;
|
||||
|
|
||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||
|
#[serde(deny_unknown_fields)]
|
||||
|
pub struct Endpoint {
|
||||
|
pub file_name_format: Option<String>,
|
||||
|
pub name: String,
|
||||
|
pub random_early_renew: Option<String>,
|
||||
|
#[serde(default)]
|
||||
|
pub rate_limits: Vec<String>,
|
||||
|
pub renew_delay: Option<String>,
|
||||
|
#[serde(default)]
|
||||
|
pub root_certificates: Vec<PathBuf>,
|
||||
|
#[serde(default)]
|
||||
|
pub tos_agreed: bool,
|
||||
|
pub url: String,
|
||||
|
}
|
||||
|
|
||||
|
#[cfg(test)]
|
||||
|
mod tests {
|
||||
|
use super::*;
|
||||
|
use crate::config::load_str;
|
||||
|
|
||||
|
#[test]
|
||||
|
fn empty() {
|
||||
|
let res = load_str::<Endpoint>("");
|
||||
|
assert!(res.is_err());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn minimal() {
|
||||
|
let cfg = r#"
|
||||
|
name = "test"
|
||||
|
url = "https://acme-v02.api.example.com/directory"
|
||||
|
"#;
|
||||
|
|
||||
|
let e: Endpoint = load_str(cfg).unwrap();
|
||||
|
assert!(e.file_name_format.is_none());
|
||||
|
assert_eq!(e.name, "test");
|
||||
|
assert!(e.random_early_renew.is_none());
|
||||
|
assert!(e.rate_limits.is_empty());
|
||||
|
assert!(e.renew_delay.is_none());
|
||||
|
assert!(e.root_certificates.is_empty());
|
||||
|
assert_eq!(e.tos_agreed, false);
|
||||
|
assert_eq!(e.url, "https://acme-v02.api.example.com/directory");
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn full() {
|
||||
|
let cfg = r#"
|
||||
|
name = "test"
|
||||
|
url = "https://acme-v02.api.example.com/directory"
|
||||
|
file_name_format = "{{ key_type }} {{ file_type }} {{ name }}.{{ ext }}"
|
||||
|
random_early_renew = "1d"
|
||||
|
rate_limits = ["rl 1", "rl 2"]
|
||||
|
renew_delay = "21d"
|
||||
|
root_certificates = ["root_cert.pem"]
|
||||
|
tos_agreed = true
|
||||
|
"#;
|
||||
|
|
||||
|
let e: Endpoint = load_str(cfg).unwrap();
|
||||
|
assert_eq!(
|
||||
|
e.file_name_format,
|
||||
|
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.rate_limits, vec!["rl 1", "rl 2"]);
|
||||
|
assert_eq!(e.renew_delay, Some("21d".to_string()));
|
||||
|
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");
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn missing_name() {
|
||||
|
let cfg = r#""#;
|
||||
|
|
||||
|
let res = load_str::<Endpoint>(cfg);
|
||||
|
assert!(res.is_err());
|
||||
|
}
|
||||
|
}
|
@ -0,0 +1,142 @@ |
|||||
|
use serde_derive::Deserialize;
|
||||
|
use std::collections::HashMap;
|
||||
|
use std::path::PathBuf;
|
||||
|
|
||||
|
#[derive(Debug, Deserialize)]
|
||||
|
#[serde(deny_unknown_fields)]
|
||||
|
pub struct GlobalOptions {
|
||||
|
#[serde(default = "get_default_accounts_directory")]
|
||||
|
pub accounts_directory: PathBuf,
|
||||
|
pub cert_file_group: Option<String>,
|
||||
|
pub cert_file_mode: Option<u32>,
|
||||
|
pub cert_file_user: Option<String>,
|
||||
|
#[serde(default = "get_default_cert_file_ext")]
|
||||
|
pub cert_file_ext: String,
|
||||
|
#[serde(default = "get_default_certificates_directory")]
|
||||
|
pub certificates_directory: PathBuf,
|
||||
|
#[serde(default)]
|
||||
|
pub env: HashMap<String, String>,
|
||||
|
#[serde(default = "get_default_file_name_format")]
|
||||
|
pub file_name_format: String,
|
||||
|
pub pk_file_group: Option<String>,
|
||||
|
pub pk_file_mode: Option<u32>,
|
||||
|
pub pk_file_user: Option<String>,
|
||||
|
#[serde(default = "get_default_pk_file_ext")]
|
||||
|
pub pk_file_ext: String,
|
||||
|
pub random_early_renew: Option<String>,
|
||||
|
#[serde(default = "get_default_renew_delay")]
|
||||
|
pub renew_delay: String,
|
||||
|
#[serde(default)]
|
||||
|
pub root_certificates: Vec<PathBuf>,
|
||||
|
}
|
||||
|
|
||||
|
fn get_default_lib_dir() -> PathBuf {
|
||||
|
let mut path = match option_env!("VARLIBDIR") {
|
||||
|
Some(s) => PathBuf::from(s),
|
||||
|
None => PathBuf::from("/var/lib"),
|
||||
|
};
|
||||
|
path.push("acmed");
|
||||
|
path
|
||||
|
}
|
||||
|
|
||||
|
fn get_default_accounts_directory() -> PathBuf {
|
||||
|
let mut path = get_default_lib_dir();
|
||||
|
path.push("accounts");
|
||||
|
path
|
||||
|
}
|
||||
|
|
||||
|
fn get_default_cert_file_ext() -> String {
|
||||
|
"pem".to_string()
|
||||
|
}
|
||||
|
|
||||
|
fn get_default_certificates_directory() -> PathBuf {
|
||||
|
let mut path = get_default_lib_dir();
|
||||
|
path.push("certs");
|
||||
|
path
|
||||
|
}
|
||||
|
|
||||
|
fn get_default_file_name_format() -> String {
|
||||
|
"{{ name }}_{{ key_type }}.{{ file_type }}.{{ ext }}".to_string()
|
||||
|
}
|
||||
|
|
||||
|
fn get_default_pk_file_ext() -> String {
|
||||
|
"pem".to_string()
|
||||
|
}
|
||||
|
|
||||
|
fn get_default_renew_delay() -> String {
|
||||
|
"30d".to_string()
|
||||
|
}
|
||||
|
|
||||
|
#[cfg(test)]
|
||||
|
mod tests {
|
||||
|
use super::*;
|
||||
|
use crate::config::load_str;
|
||||
|
|
||||
|
#[test]
|
||||
|
fn empty() {
|
||||
|
let go: GlobalOptions = load_str("").unwrap();
|
||||
|
assert_eq!(go.accounts_directory, get_default_accounts_directory());
|
||||
|
assert!(go.cert_file_group.is_none());
|
||||
|
assert!(go.cert_file_mode.is_none());
|
||||
|
assert!(go.cert_file_user.is_none());
|
||||
|
assert_eq!(go.cert_file_ext, get_default_cert_file_ext());
|
||||
|
assert_eq!(
|
||||
|
go.certificates_directory,
|
||||
|
get_default_certificates_directory()
|
||||
|
);
|
||||
|
assert!(go.env.is_empty());
|
||||
|
assert_eq!(go.file_name_format, get_default_file_name_format());
|
||||
|
assert!(go.pk_file_group.is_none());
|
||||
|
assert!(go.pk_file_mode.is_none());
|
||||
|
assert!(go.pk_file_user.is_none());
|
||||
|
assert_eq!(go.pk_file_ext, get_default_pk_file_ext());
|
||||
|
assert!(go.random_early_renew.is_none());
|
||||
|
assert_eq!(go.renew_delay, get_default_renew_delay());
|
||||
|
assert!(go.root_certificates.is_empty());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn full() {
|
||||
|
let cfg = r#"
|
||||
|
accounts_directory = "/tmp/accounts"
|
||||
|
cert_file_group = "acme_test"
|
||||
|
cert_file_mode = 0o644
|
||||
|
cert_file_user = "acme_test"
|
||||
|
cert_file_ext = "pem.txt"
|
||||
|
certificates_directory = "/tmp/certs"
|
||||
|
env.HTTP_ROOT = "/srv/http"
|
||||
|
env.TEST = "Test"
|
||||
|
file_name_format = "{{ key_type }} {{ file_type }} {{ name }}.{{ ext }}"
|
||||
|
pk_file_group = "acme_test"
|
||||
|
pk_file_mode = 0o644
|
||||
|
pk_file_user = "acme_test"
|
||||
|
pk_file_ext = "pem.txt"
|
||||
|
random_early_renew = "2d"
|
||||
|
renew_delay = "21d"
|
||||
|
root_certificates = ["root_cert.pem"]
|
||||
|
"#;
|
||||
|
|
||||
|
let mut env = HashMap::with_capacity(2);
|
||||
|
env.insert("test".to_string(), "Test".to_string());
|
||||
|
env.insert("http_root".to_string(), "/srv/http".to_string());
|
||||
|
let go: GlobalOptions = load_str(cfg).unwrap();
|
||||
|
assert_eq!(go.accounts_directory, PathBuf::from("/tmp/accounts"));
|
||||
|
assert_eq!(go.cert_file_group, Some("acme_test".to_string()));
|
||||
|
assert_eq!(go.cert_file_mode, Some(0o644));
|
||||
|
assert_eq!(go.cert_file_user, Some("acme_test".to_string()));
|
||||
|
assert_eq!(go.cert_file_ext, "pem.txt");
|
||||
|
assert_eq!(go.certificates_directory, PathBuf::from("/tmp/certs"));
|
||||
|
assert_eq!(go.env, env);
|
||||
|
assert_eq!(
|
||||
|
go.file_name_format,
|
||||
|
"{{ key_type }} {{ file_type }} {{ name }}.{{ ext }}"
|
||||
|
);
|
||||
|
assert_eq!(go.pk_file_group, Some("acme_test".to_string()));
|
||||
|
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.root_certificates, vec![PathBuf::from("root_cert.pem")]);
|
||||
|
}
|
||||
|
}
|
@ -0,0 +1,196 @@ |
|||||
|
use serde::{de, Deserialize, Deserializer};
|
||||
|
use serde_derive::Deserialize;
|
||||
|
use std::path::PathBuf;
|
||||
|
|
||||
|
#[derive(Debug, Deserialize)]
|
||||
|
#[serde(remote = "Self")]
|
||||
|
#[serde(deny_unknown_fields)]
|
||||
|
pub struct Hook {
|
||||
|
#[serde(default)]
|
||||
|
pub allow_failure: bool,
|
||||
|
#[serde(default)]
|
||||
|
pub args: Vec<String>,
|
||||
|
pub cmd: String,
|
||||
|
pub name: String,
|
||||
|
pub stderr: Option<PathBuf>,
|
||||
|
pub stdin: Option<PathBuf>,
|
||||
|
pub stdin_str: Option<String>,
|
||||
|
pub stdout: Option<PathBuf>,
|
||||
|
#[serde(rename = "type")]
|
||||
|
pub hook_type: Vec<HookType>,
|
||||
|
}
|
||||
|
|
||||
|
impl<'de> Deserialize<'de> for Hook {
|
||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
|
where
|
||||
|
D: Deserializer<'de>,
|
||||
|
{
|
||||
|
let unchecked = Hook::deserialize(deserializer)?;
|
||||
|
if unchecked.hook_type.is_empty() {
|
||||
|
return Err(de::Error::custom(
|
||||
|
"at least one hook type must be specified",
|
||||
|
));
|
||||
|
}
|
||||
|
if unchecked.stdin.is_some() && unchecked.stdin_str.is_some() {
|
||||
|
return Err(de::Error::custom(
|
||||
|
"the `stdin` and `stdin_str` directives cannot be both specified within the same hook",
|
||||
|
));
|
||||
|
}
|
||||
|
Ok(unchecked)
|
||||
|
}
|
||||
|
}
|
||||
|
|
||||
|
#[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(Debug, Deserialize)]
|
||||
|
#[serde(deny_unknown_fields)]
|
||||
|
pub struct Group {
|
||||
|
pub hooks: Vec<String>,
|
||||
|
pub name: String,
|
||||
|
}
|
||||
|
|
||||
|
#[cfg(test)]
|
||||
|
mod tests {
|
||||
|
use super::*;
|
||||
|
use crate::config::load_str;
|
||||
|
|
||||
|
#[test]
|
||||
|
fn empty_group() {
|
||||
|
let res = load_str::<Group>("");
|
||||
|
assert!(res.is_err());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn valid_group() {
|
||||
|
let cfg = r#"
|
||||
|
name = "test"
|
||||
|
hooks = ["h1", "H 2"]
|
||||
|
"#;
|
||||
|
let rl: Group = load_str(cfg).unwrap();
|
||||
|
assert_eq!(rl.name, "test");
|
||||
|
assert_eq!(rl.hooks, vec!["h1".to_string(), "H 2".to_string()]);
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn empty_hook() {
|
||||
|
let res = load_str::<Hook>("");
|
||||
|
assert!(res.is_err());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn hook_minimal() {
|
||||
|
let cfg = r#"
|
||||
|
name = "test"
|
||||
|
cmd = "cat"
|
||||
|
type = ["file-pre-edit"]
|
||||
|
"#;
|
||||
|
let h = load_str::<Hook>(cfg).unwrap();
|
||||
|
assert_eq!(h.allow_failure, false);
|
||||
|
assert!(h.args.is_empty());
|
||||
|
assert_eq!(h.cmd, "cat");
|
||||
|
assert_eq!(h.name, "test");
|
||||
|
assert!(h.stderr.is_none());
|
||||
|
assert!(h.stdin.is_none());
|
||||
|
assert!(h.stdin_str.is_none());
|
||||
|
assert!(h.stdout.is_none());
|
||||
|
assert_eq!(h.hook_type, vec![HookType::FilePreEdit]);
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn hook_full() {
|
||||
|
let cfg = r#"
|
||||
|
name = "test"
|
||||
|
cmd = "cat"
|
||||
|
args = ["-e"]
|
||||
|
type = ["file-pre-edit"]
|
||||
|
allow_failure = true
|
||||
|
stdin = "/tmp/in.txt"
|
||||
|
stdout = "/tmp/out.log"
|
||||
|
stderr = "/tmp/err.log"
|
||||
|
"#;
|
||||
|
let h = load_str::<Hook>(cfg).unwrap();
|
||||
|
assert_eq!(h.allow_failure, true);
|
||||
|
assert_eq!(h.args, vec!["-e".to_string()]);
|
||||
|
assert_eq!(h.cmd, "cat");
|
||||
|
assert_eq!(h.name, "test");
|
||||
|
assert_eq!(h.stderr, Some(PathBuf::from("/tmp/err.log")));
|
||||
|
assert_eq!(h.stdin, Some(PathBuf::from("/tmp/in.txt")));
|
||||
|
assert!(h.stdin_str.is_none());
|
||||
|
assert_eq!(h.stdout, Some(PathBuf::from("/tmp/out.log")));
|
||||
|
assert_eq!(h.hook_type, vec![HookType::FilePreEdit]);
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn hook_both_stdin() {
|
||||
|
let cfg = r#"
|
||||
|
name = "test"
|
||||
|
cmd = "cat"
|
||||
|
type = ["file-pre-edit"]
|
||||
|
stdin = "/tmp/in.txt"
|
||||
|
stdin_str = "some input"
|
||||
|
"#;
|
||||
|
let res = load_str::<Hook>(cfg);
|
||||
|
assert!(res.is_err());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn hook_missing_name() {
|
||||
|
let cfg = r#"
|
||||
|
cmd = "cat"
|
||||
|
type = ["file-pre-edit"]
|
||||
|
"#;
|
||||
|
let res = load_str::<Hook>(cfg);
|
||||
|
assert!(res.is_err());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn hook_missing_cmd() {
|
||||
|
let cfg = r#"
|
||||
|
name = "test"
|
||||
|
type = ["file-pre-edit"]
|
||||
|
"#;
|
||||
|
let res = load_str::<Hook>(cfg);
|
||||
|
assert!(res.is_err());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn hook_missing_type() {
|
||||
|
let cfg = r#"
|
||||
|
name = "test"
|
||||
|
cmd = "cat"
|
||||
|
"#;
|
||||
|
let res = load_str::<Hook>(cfg);
|
||||
|
assert!(res.is_err());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn hook_empty_type() {
|
||||
|
let cfg = r#"
|
||||
|
name = "test"
|
||||
|
cmd = "cat"
|
||||
|
type = []
|
||||
|
"#;
|
||||
|
let res = load_str::<Hook>(cfg);
|
||||
|
assert!(res.is_err());
|
||||
|
}
|
||||
|
}
|
@ -0,0 +1,68 @@ |
|||||
|
use serde_derive::Deserialize;
|
||||
|
|
||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||
|
#[serde(deny_unknown_fields)]
|
||||
|
pub struct RateLimit {
|
||||
|
pub name: String,
|
||||
|
pub number: usize,
|
||||
|
pub period: String,
|
||||
|
}
|
||||
|
|
||||
|
#[cfg(test)]
|
||||
|
mod tests {
|
||||
|
use super::*;
|
||||
|
use crate::config::load_str;
|
||||
|
|
||||
|
#[test]
|
||||
|
fn empty() {
|
||||
|
let res = load_str::<RateLimit>("");
|
||||
|
assert!(res.is_err());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn ok() {
|
||||
|
let cfg = r#"
|
||||
|
name = "test"
|
||||
|
number = 20
|
||||
|
period = "20s"
|
||||
|
"#;
|
||||
|
|
||||
|
let rl: RateLimit = load_str(cfg).unwrap();
|
||||
|
assert_eq!(rl.name, "test");
|
||||
|
assert_eq!(rl.number, 20);
|
||||
|
assert_eq!(rl.period, "20s");
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn missing_name() {
|
||||
|
let cfg = r#"
|
||||
|
number = 20
|
||||
|
period = "20s"
|
||||
|
"#;
|
||||
|
|
||||
|
let res = load_str::<RateLimit>(cfg);
|
||||
|
assert!(res.is_err());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn missing_number() {
|
||||
|
let cfg = r#"
|
||||
|
name = "test"
|
||||
|
period = "20s"
|
||||
|
"#;
|
||||
|
|
||||
|
let res = load_str::<RateLimit>(cfg);
|
||||
|
assert!(res.is_err());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn missing_period() {
|
||||
|
let cfg = r#"
|
||||
|
name = "test"
|
||||
|
number = 20
|
||||
|
"#;
|
||||
|
|
||||
|
let res = load_str::<RateLimit>(cfg);
|
||||
|
assert!(res.is_err());
|
||||
|
}
|
||||
|
}
|
@ -0,0 +1,9 @@ |
|||||
|
[global] |
||||
|
accounts_directory = "/tmp/example/account/dir" |
||||
|
certificates_directory = "/tmp/example/cert/dir/" |
||||
|
|
||||
|
[[account]] |
||||
|
name = "example" |
||||
|
contacts = [ |
||||
|
{ mailto = "acme@example.org" }, |
||||
|
] |
@ -0,0 +1 @@ |
|||||
|
global.accounts_directory = "/tmp/other/account/dir" |
@ -0,0 +1,10 @@ |
|||||
|
[global] |
||||
|
accounts_directory = "/tmp/example/account/dir" |
||||
|
certificates_directory = "/tmp/example/cert/dir/" |
||||
|
|
||||
|
[[account]] |
||||
|
name = "example" |
||||
|
contacts = [ |
||||
|
{ mailto = "acme@example.org" }, |
||||
|
] |
||||
|
signature_algorithm = "HS384" |
Write
Preview
Loading…
Cancel
Save
Reference in new issue