Skip to main content

camber/
config.rs

1use crate::RuntimeError;
2use serde::Deserialize;
3use serde::de::DeserializeOwned;
4use std::path::Path;
5
6/// Shared TLS configuration parsed from TOML.
7/// Used by all suspension-stack tools (Camber, Kingpin, Damper).
8#[derive(Debug, Deserialize)]
9pub struct TlsConfig {
10    /// Path to a PEM-encoded certificate file for manual TLS.
11    pub cert: Option<Box<str>>,
12    /// Path to a PEM-encoded private key file for manual TLS.
13    pub key: Option<Box<str>>,
14    /// Enable automatic certificate provisioning.
15    pub auto: Option<bool>,
16    /// Contact email for ACME registration.
17    pub email: Option<Box<str>>,
18    /// Use the ACME staging environment instead of production.
19    pub staging: Option<bool>,
20    /// Directory used to cache ACME account and certificate data.
21    pub cache_dir: Option<Box<str>>,
22    /// DNS provider name for DNS-01 challenges.
23    pub dns_provider: Option<Box<str>>,
24    /// Environment variable containing the DNS API token.
25    pub dns_api_token_env: Option<Box<str>>,
26    /// File containing the DNS API token.
27    pub dns_api_token_file: Option<Box<str>>,
28}
29
30impl TlsConfig {
31    /// Validate that the configured TLS mode is internally consistent.
32    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    /// Return whether automatic TLS is enabled.
88    pub fn auto(&self) -> bool {
89        self.auto.unwrap_or(false)
90    }
91
92    /// Return the configured ACME contact email.
93    pub fn email(&self) -> Option<&str> {
94        self.email.as_deref()
95    }
96
97    /// Return whether ACME staging mode is enabled.
98    pub fn staging(&self) -> bool {
99        self.staging.unwrap_or(false)
100    }
101
102    /// Return the configured certificate path for manual TLS.
103    pub fn cert(&self) -> Option<&str> {
104        self.cert.as_deref()
105    }
106
107    /// Return the configured private key path for manual TLS.
108    pub fn key(&self) -> Option<&str> {
109        self.key.as_deref()
110    }
111
112    /// Return the configured ACME cache directory.
113    pub fn cache_dir(&self) -> Option<&str> {
114        self.cache_dir.as_deref()
115    }
116
117    /// Return the configured DNS provider name.
118    pub fn dns_provider(&self) -> Option<&str> {
119        self.dns_provider.as_deref()
120    }
121
122    /// Return the environment variable name holding the DNS API token.
123    pub fn dns_api_token_env(&self) -> Option<&str> {
124        self.dns_api_token_env.as_deref()
125    }
126
127    /// Return the file path holding the DNS API token.
128    pub fn dns_api_token_file(&self) -> Option<&str> {
129        self.dns_api_token_file.as_deref()
130    }
131}
132
133/// Return the default cache directory: `~/.config/{tool}/certs/`.
134#[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/// Shared ACME configuration fields used by both HTTP-01 and DNS-01 flows.
147#[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    /// Create a new ACME base configuration.
159    ///
160    /// `tool_name` sets the default cache directory to `~/.config/{tool_name}/certs/`.
161    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    /// Set the contact email for ACME registration.
171    pub fn email(mut self, email: impl Into<Box<str>>) -> Self {
172        self.email = Some(email.into());
173        self
174    }
175
176    /// Set the directory for caching certificates and account keys.
177    pub fn cache_dir(mut self, path: impl Into<std::path::PathBuf>) -> Self {
178        self.cache_dir = path.into();
179        self
180    }
181
182    /// Use Let's Encrypt staging directory (for testing).
183    pub fn staging(mut self, staging: bool) -> Self {
184        self.staging = staging;
185        self
186    }
187
188    /// Return the configured cache directory path.
189    pub fn cache_path(&self) -> &std::path::Path {
190        &self.cache_dir
191    }
192}
193
194/// Load and parse a TOML configuration file into the given type.
195pub 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}