bws_web_server/ssl/
renewal.rs

1use crate::ssl::{
2    certificate::Certificate,
3    manager::{SslConfig, SslManager},
4};
5use chrono::{DateTime, Utc};
6use std::sync::Arc;
7use tokio::time::{interval, Duration};
8
9#[derive(Debug, Clone)]
10pub struct RenewalScheduler {
11    ssl_manager: Arc<SslManager>,
12    check_interval_hours: u64,
13    renewal_days_before_expiry: i64,
14}
15
16impl RenewalScheduler {
17    #[must_use]
18    pub const fn new(
19        ssl_manager: Arc<SslManager>,
20        check_interval_hours: u64,
21        renewal_days_before_expiry: i64,
22    ) -> Self {
23        Self {
24            ssl_manager,
25            check_interval_hours,
26            renewal_days_before_expiry,
27        }
28    }
29
30    pub async fn start(&self) {
31        let mut interval_timer = interval(Duration::from_secs(self.check_interval_hours * 3600));
32
33        log::info!(
34            "Starting certificate renewal scheduler (check every {} hours, renew {} days before expiry)",
35            self.check_interval_hours,
36            self.renewal_days_before_expiry
37        );
38
39        loop {
40            interval_timer.tick().await;
41
42            if let Err(e) = self.check_and_schedule_renewals().await {
43                log::error!("Error during renewal check: {e}");
44            }
45        }
46    }
47
48    async fn check_and_schedule_renewals(&self) -> Result<(), Box<dyn std::error::Error>> {
49        log::info!("Checking certificates for renewal eligibility");
50
51        let certificates = self.ssl_manager.list_certificates().await;
52        let mut renewal_tasks = Vec::new();
53
54        for cert in certificates {
55            if self.should_schedule_renewal(&cert) {
56                renewal_tasks.push(cert);
57            }
58        }
59
60        if renewal_tasks.is_empty() {
61            log::info!("No certificates require renewal at this time");
62            return Ok(());
63        }
64
65        log::info!(
66            "Scheduling renewal for {} certificates",
67            renewal_tasks.len()
68        );
69
70        // Process renewals sequentially to avoid overwhelming ACME servers
71        for cert in renewal_tasks {
72            match self.attempt_renewal(&cert).await {
73                Ok(()) => {
74                    log::info!("Successfully renewed certificate for {}", cert.domain);
75                }
76                Err(e) => {
77                    log::error!("Failed to renew certificate for {}: {}", cert.domain, e);
78                    // Continue with other certificates
79                }
80            }
81
82            // Add a small delay between renewals to be respectful to ACME servers
83            tokio::time::sleep(Duration::from_secs(5)).await;
84        }
85
86        Ok(())
87    }
88
89    fn should_schedule_renewal(&self, cert: &Certificate) -> bool {
90        // Check if auto-renewal is enabled
91        if !cert.auto_renew {
92            return false;
93        }
94
95        // Check if certificate is expired or close to expiry
96        if cert.days_until_expiry() <= self.renewal_days_before_expiry {
97            return true;
98        }
99
100        // Check if this is a fresh certificate that we haven't checked recently
101        // This helps catch certificates that were manually installed
102        cert.last_renewal_check.is_none_or(|last_check| {
103            let hours_since_check = (Utc::now() - last_check).num_hours();
104            // Only check again if it's been more than the check interval
105            hours_since_check >= i64::try_from(self.check_interval_hours).unwrap_or(24)
106        })
107    }
108
109    async fn attempt_renewal(&self, cert: &Certificate) -> Result<(), Box<dyn std::error::Error>> {
110        log::info!(
111            "Attempting renewal for {} (expires in {} days)",
112            cert.domain,
113            cert.days_until_expiry()
114        );
115
116        // Validate current certificate files before attempting renewal
117        if !cert.validate_certificate_files().await.unwrap_or(false) {
118            log::warn!(
119                "Current certificate files for {} are invalid, forcing renewal",
120                cert.domain
121            );
122        }
123
124        // Attempt to obtain/renew the certificate
125        let success = self.ssl_manager.ensure_certificate(&cert.domain).await?;
126
127        if success {
128            // Update the last renewal check timestamp
129            // This would be handled by the SSL manager internally
130            log::info!(
131                "Certificate renewal completed successfully for {}",
132                cert.domain
133            );
134        } else {
135            return Err(format!("Failed to renew certificate for {}", cert.domain).into());
136        }
137
138        Ok(())
139    }
140
141    /// Force certificate renewal for a domain
142    ///
143    /// # Errors
144    ///
145    /// Returns an error if certificate renewal fails.
146    pub async fn force_renewal(&self, domain: &str) -> Result<(), Box<dyn std::error::Error>> {
147        log::info!("Forcing certificate renewal for domain: {domain}");
148
149        let success = self.ssl_manager.ensure_certificate(domain).await?;
150
151        if success {
152            log::info!("Forced renewal completed successfully for {domain}");
153            Ok(())
154        } else {
155            Err(format!("Failed to force renewal for domain: {domain}").into())
156        }
157    }
158
159    pub async fn get_renewal_status(&self) -> RenewalStatus {
160        let certificates = self.ssl_manager.list_certificates().await;
161        let mut status = RenewalStatus::default();
162
163        for cert in certificates {
164            status.total_certificates += 1;
165
166            if cert.is_expired() {
167                status.expired_certificates.push(cert.domain.clone());
168            } else if cert.needs_renewal(self.renewal_days_before_expiry) {
169                status.renewal_needed.push(CertificateRenewalInfo {
170                    domain: cert.domain.clone(),
171                    days_until_expiry: cert.days_until_expiry(),
172                    auto_renew_enabled: cert.auto_renew,
173                    last_renewal_check: cert.last_renewal_check,
174                });
175            } else {
176                status.valid_certificates.push(CertificateValidInfo {
177                    domain: cert.domain.clone(),
178                    days_until_expiry: cert.days_until_expiry(),
179                    expires_at: cert.expires_at,
180                });
181            }
182        }
183
184        status
185    }
186}
187
188#[derive(Debug, Clone, Default)]
189pub struct RenewalStatus {
190    pub total_certificates: usize,
191    pub valid_certificates: Vec<CertificateValidInfo>,
192    pub renewal_needed: Vec<CertificateRenewalInfo>,
193    pub expired_certificates: Vec<String>,
194}
195
196#[derive(Debug, Clone)]
197pub struct CertificateValidInfo {
198    pub domain: String,
199    pub days_until_expiry: i64,
200    pub expires_at: DateTime<Utc>,
201}
202
203#[derive(Debug, Clone)]
204pub struct CertificateRenewalInfo {
205    pub domain: String,
206    pub days_until_expiry: i64,
207    pub auto_renew_enabled: bool,
208    pub last_renewal_check: Option<DateTime<Utc>>,
209}
210
211// Background renewal service that can be spawned as a task
212pub struct RenewalService {
213    scheduler: RenewalScheduler,
214}
215
216impl RenewalService {
217    #[must_use]
218    pub fn new(ssl_manager: Arc<SslManager>, config: &SslConfig) -> Self {
219        let scheduler = RenewalScheduler::new(
220            ssl_manager,
221            config.renewal_check_interval_hours,
222            config.renewal_days_before_expiry,
223        );
224
225        Self { scheduler }
226    }
227
228    pub async fn run(self) {
229        self.scheduler.start().await;
230    }
231
232    /// Force certificate renewal for a domain
233    ///
234    /// # Errors
235    ///
236    /// Returns an error if certificate renewal fails.
237    pub async fn force_renewal(&self, domain: &str) -> Result<(), Box<dyn std::error::Error>> {
238        self.scheduler.force_renewal(domain).await
239    }
240
241    pub async fn get_status(&self) -> RenewalStatus {
242        self.scheduler.get_renewal_status().await
243    }
244}
245
246// Helper functions for renewal logic
247#[must_use]
248pub const fn calculate_renewal_urgency(days_until_expiry: i64) -> RenewalUrgency {
249    match days_until_expiry {
250        d if d <= 0 => RenewalUrgency::Expired,
251        d if d <= 7 => RenewalUrgency::Critical,
252        d if d <= 30 => RenewalUrgency::High,
253        d if d <= 60 => RenewalUrgency::Medium,
254        _ => RenewalUrgency::Low,
255    }
256}
257
258#[derive(Debug, Clone, PartialEq, Eq)]
259pub enum RenewalUrgency {
260    Expired,
261    Critical, // <= 7 days
262    High,     // <= 30 days
263    Medium,   // <= 60 days
264    Low,      // > 60 days
265}
266
267impl RenewalUrgency {
268    #[must_use]
269    pub const fn as_str(&self) -> &'static str {
270        match self {
271            Self::Expired => "expired",
272            Self::Critical => "critical",
273            Self::High => "high",
274            Self::Medium => "medium",
275            Self::Low => "low",
276        }
277    }
278
279    #[must_use]
280    pub const fn should_renew_now(&self) -> bool {
281        matches!(self, Self::Expired | Self::Critical | Self::High)
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288    use chrono::Duration as ChronoDuration;
289
290    #[test]
291    fn test_renewal_urgency() {
292        assert_eq!(calculate_renewal_urgency(-1), RenewalUrgency::Expired);
293        assert_eq!(calculate_renewal_urgency(0), RenewalUrgency::Expired);
294        assert_eq!(calculate_renewal_urgency(5), RenewalUrgency::Critical);
295        assert_eq!(calculate_renewal_urgency(15), RenewalUrgency::High);
296        assert_eq!(calculate_renewal_urgency(45), RenewalUrgency::Medium);
297        assert_eq!(calculate_renewal_urgency(90), RenewalUrgency::Low);
298
299        assert!(RenewalUrgency::Critical.should_renew_now());
300        assert!(RenewalUrgency::High.should_renew_now());
301        assert!(!RenewalUrgency::Low.should_renew_now());
302    }
303
304    #[test]
305    fn test_renewal_status_default() {
306        let status = RenewalStatus::default();
307        assert_eq!(status.total_certificates, 0);
308        assert!(status.valid_certificates.is_empty());
309        assert!(status.renewal_needed.is_empty());
310        assert!(status.expired_certificates.is_empty());
311    }
312
313    #[tokio::test]
314    async fn test_should_schedule_renewal() {
315        use std::path::PathBuf;
316
317        // Create a test certificate that needs renewal
318        let cert = Certificate {
319            domain: "example.com".to_string(),
320            cert_path: PathBuf::from("test.crt"),
321            key_path: PathBuf::from("test.key"),
322            issued_at: Utc::now() - ChronoDuration::days(60),
323            expires_at: Utc::now() + ChronoDuration::days(15), // Expires in 15 days
324            issuer: "Test CA".to_string(),
325            san_domains: vec![],
326            auto_renew: true,
327            last_renewal_check: None,
328        };
329
330        // Mock SSL manager - in a real test, you'd use a proper mock
331        // For now, we'll just test the renewal urgency calculation
332        let urgency = calculate_renewal_urgency(cert.days_until_expiry());
333        assert_eq!(urgency, RenewalUrgency::High);
334        assert!(urgency.should_renew_now());
335    }
336}