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 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 }
80 }
81
82 tokio::time::sleep(Duration::from_secs(5)).await;
84 }
85
86 Ok(())
87 }
88
89 fn should_schedule_renewal(&self, cert: &Certificate) -> bool {
90 if !cert.auto_renew {
92 return false;
93 }
94
95 if cert.days_until_expiry() <= self.renewal_days_before_expiry {
97 return true;
98 }
99
100 cert.last_renewal_check.is_none_or(|last_check| {
103 let hours_since_check = (Utc::now() - last_check).num_hours();
104 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 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 let success = self.ssl_manager.ensure_certificate(&cert.domain).await?;
126
127 if success {
128 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 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
211pub 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 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#[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, High, Medium, Low, }
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 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), issuer: "Test CA".to_string(),
325 san_domains: vec![],
326 auto_renew: true,
327 last_renewal_check: None,
328 };
329
330 let urgency = calculate_renewal_urgency(cert.days_until_expiry());
333 assert_eq!(urgency, RenewalUrgency::High);
334 assert!(urgency.should_renew_now());
335 }
336}