1use parking_lot::RwLock;
7use std::path::{Path, PathBuf};
8use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
9use std::sync::Arc;
10use std::time::Instant;
11use tracing::{error, info, warn};
12
13use crate::config::{ConfigError, ConfigFile, ConfigLoader};
14use crate::site_waf::SiteWafManager;
15use crate::tls::TlsManager;
16use crate::vhost::VhostMatcher;
17
18#[derive(Debug, Default)]
20pub struct ReloadStats {
21 pub attempts: AtomicU64,
23 pub successes: AtomicU64,
25 pub failures: AtomicU64,
27 pub last_reload_time: AtomicU64,
29 pub last_success: AtomicBool,
31}
32
33impl ReloadStats {
34 pub fn record(&self, success: bool) {
36 self.attempts.fetch_add(1, Ordering::Relaxed);
37 if success {
38 self.successes.fetch_add(1, Ordering::Relaxed);
39 } else {
40 self.failures.fetch_add(1, Ordering::Relaxed);
41 }
42 self.last_success.store(success, Ordering::Relaxed);
43 self.last_reload_time.store(
44 std::time::SystemTime::now()
45 .duration_since(std::time::UNIX_EPOCH)
46 .map(|d| d.as_secs())
47 .unwrap_or(0),
48 Ordering::Relaxed,
49 );
50 }
51}
52
53#[derive(Debug)]
55pub struct ReloadResult {
56 pub success: bool,
58 pub error: Option<String>,
60 pub sites_loaded: usize,
62 pub certs_loaded: usize,
64 pub duration_ms: u64,
66}
67
68pub struct ConfigReloader {
70 config_path: PathBuf,
72 current_config: Arc<RwLock<ConfigFile>>,
74 vhost_matcher: Arc<RwLock<VhostMatcher>>,
76 tls_manager: Arc<TlsManager>,
78 waf_manager: Arc<RwLock<SiteWafManager>>,
80 stats: ReloadStats,
82 reload_in_progress: AtomicBool,
84}
85
86impl ConfigReloader {
87 pub fn new<P: AsRef<Path>>(config_path: P) -> Result<Self, ConfigError> {
89 let config_path = config_path.as_ref().to_path_buf();
90
91 let config = ConfigLoader::load(&config_path)?;
93 let sites = ConfigLoader::to_site_configs(&config);
94
95 let vhost_matcher = VhostMatcher::new(sites.clone())
97 .map_err(|e| ConfigError::ValidationError(e.to_string()))?;
98
99 let tls_manager = TlsManager::default();
101
102 let mut waf_manager = SiteWafManager::new();
104 for site in &config.sites {
105 if let Some(waf_config) = &site.waf {
106 let site_waf = crate::site_waf::SiteWafConfig {
107 enabled: waf_config.enabled,
108 threshold: waf_config.threshold.unwrap_or(config.server.waf_threshold),
109 ..Default::default()
110 };
111 waf_manager.add_site(&site.hostname, site_waf);
112 }
113 }
114
115 info!(
116 "Configuration reloader initialized with {} sites",
117 config.sites.len()
118 );
119
120 Ok(Self {
121 config_path,
122 current_config: Arc::new(RwLock::new(config)),
123 vhost_matcher: Arc::new(RwLock::new(vhost_matcher)),
124 tls_manager: Arc::new(tls_manager),
125 waf_manager: Arc::new(RwLock::new(waf_manager)),
126 stats: ReloadStats::default(),
127 reload_in_progress: AtomicBool::new(false),
128 })
129 }
130
131 pub fn reload(&self) -> ReloadResult {
136 if self.reload_in_progress.swap(true, Ordering::SeqCst) {
138 warn!("Reload already in progress, skipping");
139 return ReloadResult {
140 success: false,
141 error: Some("Reload already in progress".to_string()),
142 sites_loaded: 0,
143 certs_loaded: 0,
144 duration_ms: 0,
145 };
146 }
147
148 let start = Instant::now();
149 let result = self.do_reload();
150 let duration_ms = start.elapsed().as_millis() as u64;
151
152 self.stats.record(result.success);
153 self.reload_in_progress.store(false, Ordering::SeqCst);
154
155 ReloadResult {
156 duration_ms,
157 ..result
158 }
159 }
160
161 fn do_reload(&self) -> ReloadResult {
163 info!("Starting configuration reload from {:?}", self.config_path);
164
165 let new_config = match ConfigLoader::load(&self.config_path) {
167 Ok(config) => config,
168 Err(e) => {
169 error!("Failed to load configuration: {}", e);
170 return ReloadResult {
171 success: false,
172 error: Some(format!("Config load error: {}", e)),
173 sites_loaded: 0,
174 certs_loaded: 0,
175 duration_ms: 0,
176 };
177 }
178 };
179
180 let sites = ConfigLoader::to_site_configs(&new_config);
182 let sites_count = sites.len();
183
184 let new_matcher = match VhostMatcher::new(sites.clone()) {
186 Ok(matcher) => matcher,
187 Err(e) => {
188 error!("Failed to create vhost matcher: {}", e);
189 return ReloadResult {
190 success: false,
191 error: Some(format!("Vhost matcher error: {}", e)),
192 sites_loaded: 0,
193 certs_loaded: 0,
194 duration_ms: 0,
195 };
196 }
197 };
198
199 let mut new_waf_manager = SiteWafManager::new();
201 for site in &new_config.sites {
202 if let Some(waf_config) = &site.waf {
203 let site_waf = crate::site_waf::SiteWafConfig {
204 enabled: waf_config.enabled,
205 threshold: waf_config
206 .threshold
207 .unwrap_or(new_config.server.waf_threshold),
208 ..Default::default()
209 };
210 new_waf_manager.add_site(&site.hostname, site_waf);
211 }
212 }
213
214 {
216 let mut config = self.current_config.write();
217 *config = new_config;
218 }
219 {
220 let mut matcher = self.vhost_matcher.write();
221 *matcher = new_matcher;
222 }
223 {
224 let mut waf = self.waf_manager.write();
225 *waf = new_waf_manager;
226 }
227
228 let tls_result = self.tls_manager.reload_all();
230 if !tls_result.is_success() {
231 warn!(
232 "TLS reload completed with errors: {} succeeded, {} failed",
233 tls_result.succeeded, tls_result.failed
234 );
235 for (domain, error) in &tls_result.errors {
236 warn!(" Failed to reload cert for {}: {}", domain, error);
237 }
238 }
239
240 info!(
241 "Configuration reload complete: {} sites loaded",
242 sites_count
243 );
244
245 ReloadResult {
246 success: true,
247 error: None,
248 sites_loaded: sites_count,
249 certs_loaded: self.tls_manager.cert_count(),
250 duration_ms: 0,
251 }
252 }
253
254 pub fn config(&self) -> Arc<RwLock<ConfigFile>> {
256 Arc::clone(&self.current_config)
257 }
258
259 pub fn vhost_matcher(&self) -> Arc<RwLock<VhostMatcher>> {
261 Arc::clone(&self.vhost_matcher)
262 }
263
264 pub fn tls_manager(&self) -> Arc<TlsManager> {
266 Arc::clone(&self.tls_manager)
267 }
268
269 pub fn waf_manager(&self) -> Arc<RwLock<SiteWafManager>> {
271 Arc::clone(&self.waf_manager)
272 }
273
274 pub fn stats(&self) -> &ReloadStats {
276 &self.stats
277 }
278
279 pub fn is_reloading(&self) -> bool {
281 self.reload_in_progress.load(Ordering::Relaxed)
282 }
283}
284
285#[cfg(unix)]
290pub fn setup_sighup_handler(_reloader: Arc<ConfigReloader>) {
291 use std::thread;
292
293 thread::spawn(move || {
294 info!("SIGHUP handler ready for configuration reload");
297
298 loop {
307 std::thread::sleep(std::time::Duration::from_secs(60));
308 }
311 });
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317 use std::io::Write;
318 use tempfile::NamedTempFile;
319
320 fn create_temp_config(content: &str) -> NamedTempFile {
321 let mut file = NamedTempFile::new().unwrap();
322 file.write_all(content.as_bytes()).unwrap();
323 file
324 }
325
326 const MINIMAL_CONFIG: &str = r#"
327sites:
328 - hostname: example.com
329 upstreams:
330 - host: 127.0.0.1
331 port: 8080
332"#;
333
334 #[test]
335 fn test_reloader_creation() {
336 let file = create_temp_config(MINIMAL_CONFIG);
337 let reloader = ConfigReloader::new(file.path()).unwrap();
338
339 assert!(!reloader.is_reloading());
340 assert_eq!(reloader.stats.attempts.load(Ordering::Relaxed), 0);
341 }
342
343 #[test]
344 fn test_reload_success() {
345 let file = create_temp_config(MINIMAL_CONFIG);
346 let reloader = ConfigReloader::new(file.path()).unwrap();
347
348 let result = reloader.reload();
349
350 assert!(result.success);
351 assert!(result.error.is_none());
352 assert_eq!(result.sites_loaded, 1);
353 assert_eq!(reloader.stats.successes.load(Ordering::Relaxed), 1);
354 }
355
356 #[test]
357 fn test_reload_failure() {
358 let file = create_temp_config(MINIMAL_CONFIG);
359 let reloader = ConfigReloader::new(file.path()).unwrap();
360
361 drop(file);
363
364 let result = reloader.reload();
365
366 assert!(!result.success);
367 assert!(result.error.is_some());
368 assert_eq!(reloader.stats.failures.load(Ordering::Relaxed), 1);
369 }
370
371 #[test]
372 fn test_reload_stats() {
373 let stats = ReloadStats::default();
374
375 stats.record(true);
376 stats.record(true);
377 stats.record(false);
378
379 assert_eq!(stats.attempts.load(Ordering::Relaxed), 3);
380 assert_eq!(stats.successes.load(Ordering::Relaxed), 2);
381 assert_eq!(stats.failures.load(Ordering::Relaxed), 1);
382 assert!(!stats.last_success.load(Ordering::Relaxed));
383 }
384
385 #[test]
386 fn test_concurrent_reload_prevention() {
387 let file = create_temp_config(MINIMAL_CONFIG);
388 let reloader = Arc::new(ConfigReloader::new(file.path()).unwrap());
389
390 reloader.reload_in_progress.store(true, Ordering::SeqCst);
392
393 let result = reloader.reload();
394
395 assert!(!result.success);
396 assert!(result
397 .error
398 .as_ref()
399 .unwrap()
400 .contains("already in progress"));
401 }
402
403 #[test]
404 fn test_config_access() {
405 let file = create_temp_config(MINIMAL_CONFIG);
406 let reloader = ConfigReloader::new(file.path()).unwrap();
407
408 let config = reloader.config();
409 let config_read = config.read();
410
411 assert_eq!(config_read.sites.len(), 1);
412 assert_eq!(config_read.sites[0].hostname, "example.com");
413 }
414
415 #[test]
416 fn test_vhost_matcher_access() {
417 let file = create_temp_config(MINIMAL_CONFIG);
418 let reloader = ConfigReloader::new(file.path()).unwrap();
419
420 let matcher = reloader.vhost_matcher();
421 let matcher_read = matcher.read();
422
423 assert!(matcher_read.match_host("example.com").is_some());
424 }
425}