1use crate::RuntimeError;
2use serde::Deserialize;
3use serde::de::DeserializeOwned;
4use std::path::Path;
5
6#[derive(Debug, Deserialize)]
9pub struct TlsConfig {
10 pub cert: Option<Box<str>>,
12 pub key: Option<Box<str>>,
14 pub auto: Option<bool>,
16 pub email: Option<Box<str>>,
18 pub staging: Option<bool>,
20 pub cache_dir: Option<Box<str>>,
22 pub dns_provider: Option<Box<str>>,
24 pub dns_api_token_env: Option<Box<str>>,
26 pub dns_api_token_file: Option<Box<str>>,
28}
29
30impl TlsConfig {
31 pub fn validate(&self) -> Result<(), RuntimeError> {
33 let is_auto = self.auto.unwrap_or(false);
34 let has_cert = self.cert.is_some();
35 let has_key = self.key.is_some();
36 let has_email = self.email.is_some();
37 let has_dns = self.dns_provider.is_some()
38 || self.dns_api_token_env.is_some()
39 || self.dns_api_token_file.is_some();
40
41 match (
42 is_auto,
43 has_cert || has_key,
44 has_email,
45 has_cert,
46 has_key,
47 has_dns,
48 ) {
49 (true, true, _, _, _, _) => Err(RuntimeError::Config(
50 "tls: auto and cert/key are mutually exclusive".into(),
51 )),
52 (true, false, false, _, _, _) => Err(RuntimeError::Config(
53 "tls: auto = true requires email".into(),
54 )),
55 (true, false, true, _, _, _) => self.validate_dns(),
56 (false, _, _, _, _, true) => Err(RuntimeError::Config(
57 "tls: DNS settings require auto = true".into(),
58 )),
59 (false, true, _, true, true, false) => Ok(()),
60 (false, true, _, _, _, false) => Err(RuntimeError::Config(
61 "tls: both cert and key must be provided".into(),
62 )),
63 (false, false, _, _, _, false) => Err(RuntimeError::Config(
64 "tls: must specify either auto = true or cert/key paths".into(),
65 )),
66 }
67 }
68
69 fn validate_dns(&self) -> Result<(), RuntimeError> {
70 let has_env = self.dns_api_token_env.is_some();
71 let has_file = self.dns_api_token_file.is_some();
72
73 match (self.dns_provider.is_some(), has_env, has_file) {
74 (false, true, _) | (false, _, true) => Err(RuntimeError::Config(
75 "tls: dns_api_token_env/dns_api_token_file requires dns_provider".into(),
76 )),
77 (true, true, true) => Err(RuntimeError::Config(
78 "tls: dns_api_token_env and dns_api_token_file are mutually exclusive".into(),
79 )),
80 (true, false, false) => Err(RuntimeError::Config(
81 "tls: dns_provider requires dns_api_token_env or dns_api_token_file".into(),
82 )),
83 _ => Ok(()),
84 }
85 }
86
87 pub fn auto(&self) -> bool {
89 self.auto.unwrap_or(false)
90 }
91
92 pub fn email(&self) -> Option<&str> {
94 self.email.as_deref()
95 }
96
97 pub fn staging(&self) -> bool {
99 self.staging.unwrap_or(false)
100 }
101
102 pub fn cert(&self) -> Option<&str> {
104 self.cert.as_deref()
105 }
106
107 pub fn key(&self) -> Option<&str> {
109 self.key.as_deref()
110 }
111
112 pub fn cache_dir(&self) -> Option<&str> {
114 self.cache_dir.as_deref()
115 }
116
117 pub fn dns_provider(&self) -> Option<&str> {
119 self.dns_provider.as_deref()
120 }
121
122 pub fn dns_api_token_env(&self) -> Option<&str> {
124 self.dns_api_token_env.as_deref()
125 }
126
127 pub fn dns_api_token_file(&self) -> Option<&str> {
129 self.dns_api_token_file.as_deref()
130 }
131}
132
133#[cfg(any(feature = "acme", feature = "dns01"))]
135pub(crate) fn default_cache_dir(tool: &str) -> std::path::PathBuf {
136 home_dir().join(".config").join(tool).join("certs")
137}
138
139#[cfg(any(feature = "acme", feature = "dns01"))]
140pub(crate) fn home_dir() -> std::path::PathBuf {
141 std::env::var_os("HOME")
142 .map(std::path::PathBuf::from)
143 .unwrap_or_else(|| std::path::PathBuf::from("."))
144}
145
146#[cfg(any(feature = "acme", feature = "dns01"))]
148#[derive(Debug, Clone)]
149pub struct AcmeBase {
150 pub(crate) domains: std::sync::Arc<[Box<str>]>,
151 pub(crate) email: Option<Box<str>>,
152 pub(crate) cache_dir: std::path::PathBuf,
153 pub(crate) staging: bool,
154}
155
156#[cfg(any(feature = "acme", feature = "dns01"))]
157impl AcmeBase {
158 pub fn new(tool_name: &str, domains: impl IntoIterator<Item = impl Into<Box<str>>>) -> Self {
162 Self {
163 domains: domains.into_iter().map(Into::into).collect(),
164 email: None,
165 cache_dir: default_cache_dir(tool_name),
166 staging: false,
167 }
168 }
169
170 pub fn email(mut self, email: impl Into<Box<str>>) -> Self {
172 self.email = Some(email.into());
173 self
174 }
175
176 pub fn cache_dir(mut self, path: impl Into<std::path::PathBuf>) -> Self {
178 self.cache_dir = path.into();
179 self
180 }
181
182 pub fn staging(mut self, staging: bool) -> Self {
184 self.staging = staging;
185 self
186 }
187
188 pub fn cache_path(&self) -> &std::path::Path {
190 &self.cache_dir
191 }
192}
193
194pub fn load_config<T: DeserializeOwned>(path: &Path) -> Result<T, RuntimeError> {
196 let contents = std::fs::read_to_string(path)?;
197 toml::from_str(&contents)
198 .map_err(|e| RuntimeError::Config(format!("failed to parse config: {e}").into()))
199}