Skip to main content

rust_serv/auto_tls/
client.rs

1//! ACME Client for certificate management
2//!
3//! Note: Full ACME implementation requires careful handling of various edge cases.
4//! This module provides the core structure with placeholder implementations.
5
6use std::path::Path;
7use std::sync::Arc;
8
9use super::challenge::ChallengeHandler;
10use super::store::CertificateStore;
11use super::{AutoTlsConfig, AutoTlsError, AutoTlsResult};
12
13/// ACME Client for certificate operations
14pub struct AcmeClient {
15    config: AutoTlsConfig,
16    challenge_handler: Arc<ChallengeHandler>,
17    store: CertificateStore,
18}
19
20impl AcmeClient {
21    /// Create a new ACME client
22    pub fn new(config: AutoTlsConfig) -> Self {
23        let store = CertificateStore::new(Path::new(&config.cache_dir));
24        Self {
25            config,
26            challenge_handler: Arc::new(ChallengeHandler::new()),
27            store,
28        }
29    }
30
31    /// Create client with existing challenge handler
32    pub fn with_challenge_handler(
33        config: AutoTlsConfig,
34        challenge_handler: Arc<ChallengeHandler>,
35    ) -> Self {
36        let store = CertificateStore::new(Path::new(&config.cache_dir));
37        Self {
38            config,
39            challenge_handler,
40            store,
41        }
42    }
43
44    /// Get the challenge handler
45    pub fn challenge_handler(&self) -> Arc<ChallengeHandler> {
46        self.challenge_handler.clone()
47    }
48
49    /// Initialize ACME account
50    pub async fn initialize(&mut self) -> AutoTlsResult<()> {
51        // Ensure cache directory exists
52        self.store.init().await?;
53        tracing::info!("ACME client initialized");
54        Ok(())
55    }
56
57    /// Request a certificate for configured domains
58    ///
59    /// TODO: Full implementation using instant-acme
60    /// For now, this returns an error indicating manual certificate setup is needed
61    pub async fn request_certificate(&mut self) -> AutoTlsResult<Vec<String>> {
62        if self.config.domains.is_empty() {
63            return Err(AutoTlsError::ConfigError("No domains configured".to_string()));
64        }
65
66        // Placeholder: In production, this would:
67        // 1. Create ACME account
68        // 2. Create order with Let's Encrypt
69        // 3. Complete HTTP-01 challenges
70        // 4. Generate CSR
71        // 5. Finalize order
72        // 6. Download certificate
73
74        Err(AutoTlsError::AcmeError(
75            "Auto TLS not yet fully implemented. Please use manual certificate setup or certbot.".to_string()
76        ))
77    }
78
79    /// Check if certificate needs renewal
80    pub async fn needs_renewal(&self) -> AutoTlsResult<bool> {
81        let domain = match self.config.domains.first() {
82            Some(d) => d,
83            None => return Ok(false),
84        };
85
86        match self.store.load_certificate(domain).await? {
87            Some(cert) => Ok(cert.is_expired() || cert.days_until_expiry() <= self.config.renew_before_days as i64),
88            None => Ok(true),
89        }
90    }
91
92    /// Get current certificate if exists
93    pub async fn get_certificate(&self) -> AutoTlsResult<Option<super::store::StoredCertificate>> {
94        let domain = match self.config.domains.first() {
95            Some(d) => d,
96            None => return Ok(None),
97        };
98        self.store.load_certificate(domain).await
99    }
100
101    /// Import existing certificate (from certbot or other source)
102    pub async fn import_certificate(
103        &self,
104        certificate: String,
105        private_key: String,
106        expires_at: Option<u64>,
107    ) -> AutoTlsResult<()> {
108        let domain = match self.config.domains.first() {
109            Some(d) => d,
110            None => return Err(AutoTlsError::ConfigError("No domains configured".to_string())),
111        };
112
113        let expires = expires_at.unwrap_or_else(|| {
114            // Default to 90 days from now
115            std::time::SystemTime::now()
116                .duration_since(std::time::UNIX_EPOCH)
117                .unwrap()
118                .as_secs()
119                + (90 * 24 * 60 * 60)
120        });
121
122        self.store
123            .save_certificate_with_expiry(domain, &certificate, &private_key, expires)
124            .await?;
125
126        tracing::info!("Certificate imported for {}", domain);
127        Ok(())
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[tokio::test]
136    async fn test_acme_client_creation() {
137        let config = AutoTlsConfig {
138            enabled: true,
139            domains: vec!["example.com".to_string()],
140            email: "admin@example.com".to_string(),
141            cache_dir: std::env::temp_dir().to_string_lossy().to_string(),
142            ..Default::default()
143        };
144
145        let client = AcmeClient::new(config);
146        assert_eq!(client.challenge_handler().challenge_count().await, 0);
147    }
148
149    #[test]
150    fn test_acme_client_with_challenge_handler() {
151        let config = AutoTlsConfig::default();
152        let handler = Arc::new(ChallengeHandler::new());
153        let client = AcmeClient::with_challenge_handler(config, handler.clone());
154        
155        // Both should share the same handler
156        assert!(Arc::ptr_eq(&client.challenge_handler(), &handler));
157    }
158
159    #[tokio::test]
160    async fn test_needs_renewal_no_certificate() {
161        let config = AutoTlsConfig {
162            domains: vec!["example.com".to_string()],
163            cache_dir: std::env::temp_dir().to_string_lossy().to_string(),
164            ..Default::default()
165        };
166        let client = AcmeClient::new(config);
167        
168        // Without certificate, should return true
169        let result = client.needs_renewal().await.unwrap();
170        assert!(result);
171    }
172
173    #[tokio::test]
174    async fn test_initialize() {
175        let temp_dir = tempfile::tempdir().unwrap();
176        let config = AutoTlsConfig {
177            cache_dir: temp_dir.path().to_string_lossy().to_string(),
178            ..Default::default()
179        };
180        let mut client = AcmeClient::new(config);
181        
182        client.initialize().await.unwrap();
183    }
184}