1use std::collections::HashMap;
7use std::fs;
8use std::path::PathBuf;
9use std::sync::Arc;
10
11use parking_lot::RwLock;
12use serde::{Deserialize, Serialize};
13use sha2::{Digest, Sha256};
14use tracing::{debug, info, warn};
15
16use crate::access::AccessListManager;
17use crate::config::{AccessControlConfig, ConfigFile};
18use crate::ratelimit::RateLimitManager;
19use crate::site_waf::SiteWafManager;
20use crate::validation::{
21 validate_cidr, validate_hostname, validate_rate_limit, validate_upstream,
22 validate_waf_threshold, ValidationError,
23};
24use crate::vhost::{SiteConfig, VhostMatcher};
25use crate::waf::Synapse;
26
27#[path = "rules.rs"]
28mod rules;
29pub use rules::{
30 CustomRuleAction, CustomRuleCondition, CustomRuleInput, CustomRuleUpdate, RuleMetadata,
31 RuleView, StoredRule,
32};
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct CreateSiteRequest {
41 pub hostname: String,
42 pub upstreams: Vec<String>,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub waf: Option<SiteWafRequest>,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub rate_limit: Option<RateLimitRequest>,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub access_list: Option<AccessListRequest>,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, Default)]
53pub struct UpdateSiteRequest {
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub upstreams: Option<Vec<String>>,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub waf: Option<SiteWafRequest>,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub rate_limit: Option<RateLimitRequest>,
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub access_list: Option<AccessListRequest>,
62 #[serde(skip_serializing_if = "Option::is_none")]
63 pub shadow_mirror: Option<crate::shadow::ShadowMirrorConfig>,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct SiteWafRequest {
69 pub enabled: bool,
70 #[serde(skip_serializing_if = "Option::is_none")]
71 pub threshold: Option<f64>,
72 #[serde(skip_serializing_if = "Option::is_none")]
73 pub rule_overrides: Option<HashMap<String, bool>>,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct RateLimitRequest {
79 pub requests_per_second: u64,
80 pub burst: u64,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize, Default)]
85pub struct AccessListRequest {
86 #[serde(default)]
87 pub allow: Vec<String>,
88 #[serde(default)]
89 pub deny: Vec<String>,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct MutationResult {
99 pub applied: bool,
100 pub persisted: bool,
101 pub rebuild_required: bool,
102 #[serde(default)]
103 pub warnings: Vec<String>,
104}
105
106impl MutationResult {
107 fn new() -> Self {
108 Self {
109 applied: false,
110 persisted: false,
111 rebuild_required: false,
112 warnings: Vec::new(),
113 }
114 }
115
116 fn with_applied(mut self) -> Self {
117 self.applied = true;
118 self
119 }
120
121 fn with_persisted(mut self) -> Self {
122 self.persisted = true;
123 self
124 }
125
126 fn with_rebuild(mut self) -> Self {
127 self.rebuild_required = true;
128 self
129 }
130
131 fn add_warning(&mut self, warning: impl Into<String>) {
132 self.warnings.push(warning.into());
133 }
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct SiteDetailResponse {
139 pub hostname: String,
140 pub upstreams: Vec<String>,
141 pub tls_enabled: bool,
142 pub waf: Option<SiteWafResponse>,
143 pub rate_limit: Option<RateLimitResponse>,
144 pub access_list: Option<AccessListResponse>,
145 pub shadow_mirror: Option<crate::shadow::ShadowMirrorConfig>,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct SiteWafResponse {
151 pub enabled: bool,
152 pub threshold: u8,
153 pub rule_overrides: HashMap<String, String>,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct RateLimitResponse {
159 pub requests_per_second: u32,
160 pub burst: u32,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct AccessListResponse {
166 pub allow: Vec<String>,
167 pub deny: Vec<String>,
168}
169
170#[derive(Debug, thiserror::Error)]
176pub enum ConfigManagerError {
177 #[error("site not found: {0}")]
178 SiteNotFound(String),
179
180 #[error("site already exists: {0}")]
181 SiteExists(String),
182
183 #[error("validation error: {0}")]
184 Validation(#[from] ValidationError),
185
186 #[error("persistence error: {0}")]
187 Persistence(String),
188
189 #[error("rebuild error: {0}")]
190 RebuildError(String),
191
192 #[error("at least one upstream is required")]
193 NoUpstreams,
194
195 #[error("rule not found: {0}")]
196 RuleNotFound(String),
197
198 #[error("rule already exists: {0}")]
199 RuleExists(String),
200}
201
202pub struct ConfigManager {
208 config: Arc<RwLock<ConfigFile>>,
209 sites: Arc<RwLock<Vec<SiteConfig>>>,
210 vhost: Arc<RwLock<VhostMatcher>>,
211 waf: Arc<RwLock<SiteWafManager>>,
212 rate_limiter: Arc<RwLock<RateLimitManager>>,
213 access_lists: Arc<RwLock<AccessListManager>>,
214 config_path: Option<PathBuf>,
215 rules_store: Arc<RwLock<Vec<StoredRule>>>,
216 rules_engine: Option<Arc<RwLock<Synapse>>>,
217 rules_path: Option<PathBuf>,
218 rules_hash: Option<Arc<RwLock<String>>>,
219}
220
221impl ConfigManager {
222 pub fn new(
224 config: Arc<RwLock<ConfigFile>>,
225 sites: Arc<RwLock<Vec<SiteConfig>>>,
226 vhost: Arc<RwLock<VhostMatcher>>,
227 waf: Arc<RwLock<SiteWafManager>>,
228 rate_limiter: Arc<RwLock<RateLimitManager>>,
229 access_lists: Arc<RwLock<AccessListManager>>,
230 ) -> Self {
231 Self {
232 config,
233 sites,
234 vhost,
235 waf,
236 rate_limiter,
237 access_lists,
238 config_path: None,
239 rules_store: Arc::new(RwLock::new(Vec::new())),
240 rules_engine: None,
241 rules_path: None,
242 rules_hash: None,
243 }
244 }
245
246 pub fn with_persistence(mut self, path: impl AsRef<std::path::Path>) -> Self {
248 self.config_path = Some(path.as_ref().to_path_buf());
249 self
250 }
251
252 pub fn with_rules(
254 mut self,
255 engine: Arc<RwLock<Synapse>>,
256 rules_path: Option<PathBuf>,
257 rules_hash: Option<Arc<RwLock<String>>>,
258 ) -> Self {
259 self.rules_engine = Some(engine);
260 self.rules_path = rules_path;
261 self.rules_hash = rules_hash;
262
263 if let Err(err) = self.load_rules_from_disk() {
264 warn!("Failed to load rules from disk: {}", err);
265 }
266
267 self
268 }
269
270 pub fn create_site(
276 &self,
277 req: CreateSiteRequest,
278 ) -> Result<MutationResult, ConfigManagerError> {
279 let mut result = MutationResult::new();
280
281 validate_hostname(&req.hostname)?;
283
284 if req.upstreams.is_empty() {
286 return Err(ConfigManagerError::NoUpstreams);
287 }
288 for upstream in &req.upstreams {
289 validate_upstream(upstream)?;
290 }
291
292 if let Some(ref waf) = req.waf {
294 if let Some(threshold) = waf.threshold {
295 validate_waf_threshold(threshold)?;
296 }
297 }
298
299 if let Some(ref rl) = req.rate_limit {
301 validate_rate_limit(rl.requests_per_second, rl.burst)?;
302 }
303
304 if let Some(ref al) = req.access_list {
306 for cidr in al.allow.iter().chain(al.deny.iter()) {
307 validate_cidr(cidr)?;
308 }
309 }
310
311 {
313 let sites = self.sites.read();
314 if sites
315 .iter()
316 .any(|s| s.hostname.to_lowercase() == req.hostname.to_lowercase())
317 {
318 return Err(ConfigManagerError::SiteExists(req.hostname.clone()));
319 }
320 }
321
322 let site_config = SiteConfig {
324 hostname: req.hostname.clone(),
325 upstreams: req.upstreams.clone(),
326 tls_enabled: false,
327 tls_cert: None,
328 tls_key: None,
329 waf_threshold: req
330 .waf
331 .as_ref()
332 .and_then(|w| w.threshold.map(|t| (t * 100.0) as u8)),
333 waf_enabled: req.waf.as_ref().map(|w| w.enabled).unwrap_or(true),
334 access_control: req
335 .access_list
336 .as_ref()
337 .map(|access_list| AccessControlConfig {
338 allow: access_list.allow.clone(),
339 deny: access_list.deny.clone(),
340 default_action: "allow".to_string(),
341 }),
342 headers: None,
343 shadow_mirror: None,
344 };
345
346 {
348 let mut sites = self.sites.write();
349 let site_id = sites.len();
350 sites.push(site_config);
351
352 let mut waf = self.waf.write();
354
355 if let Some(waf_req) = &req.waf {
356 let rule_overrides = waf_req
357 .rule_overrides
358 .as_ref()
359 .map(|overrides| {
360 overrides
361 .iter()
362 .map(|(rule_id, _enabled)| {
363 (
364 rule_id.clone(),
365 crate::site_waf::RuleOverride {
366 rule_id: rule_id.clone(),
367 action: crate::site_waf::WafAction::Block,
368 threshold: None,
369 enabled: *_enabled,
370 },
371 )
372 })
373 .collect()
374 })
375 .unwrap_or_default();
376
377 let waf_config = crate::site_waf::SiteWafConfig {
378 enabled: waf_req.enabled,
379 threshold: waf_req.threshold.map(|t| (t * 100.0) as u8).unwrap_or(70),
380 rule_overrides,
381 custom_block_page: None,
382 default_action: crate::site_waf::WafAction::Block,
383 };
384 waf.add_site(&req.hostname, waf_config);
385 }
386
387 if let Some(rl_req) = &req.rate_limit {
388 let rl_config = crate::ratelimit::RateLimitConfig {
389 rps: rl_req.requests_per_second as u32,
390 burst: rl_req.burst as u32,
391 enabled: true,
392 window_secs: 1,
393 };
394 self.rate_limiter.write().add_site(&req.hostname, rl_config);
395 }
396
397 if let Some(al_req) = &req.access_list {
398 let mut access_list = crate::access::AccessList::allow_all();
399
400 for cidr in &al_req.allow {
401 if let Err(e) = access_list.allow(cidr) {
402 warn!("failed to add allow rule '{}': {}", cidr, e);
403 }
404 }
405
406 for cidr in &al_req.deny {
407 if let Err(e) = access_list.deny(cidr) {
408 warn!("failed to add deny rule '{}': {}", cidr, e);
409 }
410 }
411
412 self.access_lists
413 .write()
414 .add_site(&req.hostname, access_list);
415 }
416
417 info!(hostname = %req.hostname, site_id = site_id, "created new site");
418 }
419
420 result = result.with_applied();
421
422 self.rebuild_vhost()?;
424 result = result.with_rebuild();
425
426 if self.config_path.is_some() {
428 match self.persist_config() {
429 Ok(()) => result = result.with_persisted(),
430 Err(e) => {
431 result.add_warning(format!("failed to persist config: {}", e));
432 warn!(error = %e, "failed to persist config after create_site");
433 }
434 }
435 }
436
437 Ok(result)
438 }
439
440 pub fn get_site(&self, hostname: &str) -> Result<SiteDetailResponse, ConfigManagerError> {
442 let sites = self.sites.read();
443 let waf = self.waf.read();
444
445 let site = sites
446 .iter()
447 .find(|s| s.hostname.to_lowercase() == hostname.to_lowercase())
448 .ok_or_else(|| ConfigManagerError::SiteNotFound(hostname.to_string()))?;
449
450 let waf_config = waf.get_config(hostname);
451 let waf_response = Some(SiteWafResponse {
452 enabled: waf_config.enabled,
453 threshold: waf_config.threshold,
454 rule_overrides: waf_config
455 .rule_overrides
456 .iter()
457 .map(|(k, v)| (k.clone(), format!("{:?}", v.action)))
458 .collect(),
459 });
460
461 Ok(SiteDetailResponse {
462 hostname: site.hostname.clone(),
463 upstreams: site.upstreams.clone(),
464 tls_enabled: site.tls_enabled,
465 waf: waf_response,
466 rate_limit: None,
467 access_list: None,
468 shadow_mirror: site.shadow_mirror.clone(),
469 })
470 }
471
472 pub fn list_sites(&self) -> Vec<String> {
474 let sites = self.sites.read();
475 sites.iter().map(|s| s.hostname.clone()).collect()
476 }
477
478 pub fn get_sites_info(&self) -> Vec<crate::api::SiteInfo> {
480 let sites = self.sites.read();
481 sites
482 .iter()
483 .map(|s| crate::api::SiteInfo {
484 hostname: s.hostname.clone(),
485 upstreams: s.upstreams.clone(),
486 tls_enabled: s.tls_enabled,
487 waf_enabled: s.waf_enabled,
488 })
489 .collect()
490 }
491
492 pub fn update_site(
494 &self,
495 hostname: &str,
496 req: UpdateSiteRequest,
497 ) -> Result<MutationResult, ConfigManagerError> {
498 let mut result = MutationResult::new();
499
500 if let Some(ref upstreams) = req.upstreams {
502 if upstreams.is_empty() {
503 return Err(ConfigManagerError::NoUpstreams);
504 }
505 for upstream in upstreams {
506 validate_upstream(upstream)?;
507 }
508 }
509
510 if let Some(ref waf) = req.waf {
512 if let Some(threshold) = waf.threshold {
513 validate_waf_threshold(threshold)?;
514 }
515 }
516
517 if let Some(ref rl) = req.rate_limit {
519 validate_rate_limit(rl.requests_per_second, rl.burst)?;
520 }
521
522 if let Some(ref al) = req.access_list {
524 for cidr in al.allow.iter().chain(al.deny.iter()) {
525 validate_cidr(cidr)?;
526 }
527 }
528
529 {
531 let mut sites = self.sites.write();
532 let mut waf = self.waf.write();
533
534 let (_site_id, site) = sites
535 .iter_mut()
536 .enumerate()
537 .find(|(_, s)| s.hostname.to_lowercase() == hostname.to_lowercase())
538 .ok_or_else(|| ConfigManagerError::SiteNotFound(hostname.to_string()))?;
539
540 if let Some(upstreams) = req.upstreams {
542 site.upstreams = upstreams;
543 debug!(hostname = %hostname, "updated upstreams");
544 }
545
546 if let Some(waf_req) = req.waf {
548 site.waf_enabled = waf_req.enabled;
549 site.waf_threshold = waf_req.threshold.map(|t| (t * 100.0) as u8);
550
551 let rule_overrides = waf_req
552 .rule_overrides
553 .as_ref()
554 .map(|overrides| {
555 overrides
556 .iter()
557 .map(|(rule_id, _enabled)| {
558 (
559 rule_id.clone(),
560 crate::site_waf::RuleOverride {
561 rule_id: rule_id.clone(),
562 action: crate::site_waf::WafAction::Block,
563 threshold: None,
564 enabled: *_enabled,
565 },
566 )
567 })
568 .collect()
569 })
570 .unwrap_or_default();
571
572 if let Some(config) = waf.get_config_mut(hostname) {
573 config.enabled = waf_req.enabled;
574 config.threshold = waf_req.threshold.map(|t| (t * 100.0) as u8).unwrap_or(70);
575 config.rule_overrides = rule_overrides;
576 } else {
577 let waf_config = crate::site_waf::SiteWafConfig {
578 enabled: waf_req.enabled,
579 threshold: waf_req.threshold.map(|t| (t * 100.0) as u8).unwrap_or(70),
580 rule_overrides,
581 custom_block_page: None,
582 default_action: crate::site_waf::WafAction::Block,
583 };
584 waf.add_site(hostname, waf_config);
585 }
586 debug!(hostname = %hostname, "updated WAF config");
587 }
588
589 if let Some(rl_req) = req.rate_limit {
591 let rl_config = crate::ratelimit::RateLimitConfig {
592 rps: rl_req.requests_per_second as u32,
593 burst: rl_req.burst as u32,
594 enabled: true,
595 window_secs: 1,
596 };
597 self.rate_limiter.write().add_site(hostname, rl_config);
598 debug!(hostname = %hostname, "updated rate limit config");
599 }
600
601 if let Some(al_req) = req.access_list {
603 site.access_control = Some(AccessControlConfig {
604 allow: al_req.allow.clone(),
605 deny: al_req.deny.clone(),
606 default_action: "allow".to_string(),
607 });
608 let mut access_list = crate::access::AccessList::allow_all();
609
610 for cidr in &al_req.allow {
611 if let Err(e) = access_list.allow(cidr) {
612 warn!("failed to add allow rule '{}': {}", cidr, e);
613 }
614 }
615
616 for cidr in &al_req.deny {
617 if let Err(e) = access_list.deny(cidr) {
618 warn!("failed to add deny rule '{}': {}", cidr, e);
619 }
620 }
621
622 self.access_lists.write().add_site(hostname, access_list);
623 debug!(hostname = %hostname, "updated access list config");
624 }
625
626 if let Some(shadow_mirror_config) = req.shadow_mirror {
628 site.shadow_mirror = Some(shadow_mirror_config);
629 debug!(hostname = %hostname, "updated shadow mirror config");
630 }
631
632 info!(hostname = %hostname, "updated site configuration");
633 }
634
635 result = result.with_applied();
636
637 if self.config_path.is_some() {
639 match self.persist_config() {
640 Ok(()) => result = result.with_persisted(),
641 Err(e) => {
642 result.add_warning(format!("failed to persist config: {}", e));
643 warn!(error = %e, "failed to persist config after update_site");
644 }
645 }
646 }
647
648 Ok(result)
649 }
650
651 pub fn delete_site(&self, hostname: &str) -> Result<MutationResult, ConfigManagerError> {
653 let mut result = MutationResult::new();
654
655 {
656 let mut sites = self.sites.write();
657
658 let _site_id = sites
659 .iter()
660 .position(|s| s.hostname.to_lowercase() == hostname.to_lowercase())
661 .ok_or_else(|| ConfigManagerError::SiteNotFound(hostname.to_string()))?;
662
663 sites.remove(_site_id);
664 info!(hostname = %hostname, "deleted site");
668 }
669
670 result = result.with_applied();
671
672 self.rebuild_vhost()?;
674 result = result.with_rebuild();
675
676 if self.config_path.is_some() {
678 match self.persist_config() {
679 Ok(()) => result = result.with_persisted(),
680 Err(e) => {
681 result.add_warning(format!("failed to persist config: {}", e));
682 warn!(error = %e, "failed to persist config after delete_site");
683 }
684 }
685 }
686
687 Ok(result)
688 }
689
690 pub fn get_full_config(&self) -> ConfigFile {
692 let config = self.config.read();
693 config.clone()
694 }
695
696 pub fn config_hash(&self) -> String {
698 let config = self.config.read();
699 let payload = serde_json::to_vec(&*config).unwrap_or_default();
700 let mut hasher = Sha256::new();
701 hasher.update(payload);
702 let digest = format!("{:x}", hasher.finalize());
703 digest.get(..16).unwrap_or(&digest).to_string()
704 }
705
706 pub fn rules_hash(&self) -> String {
708 if let Some(hash) = self.rules_hash.as_ref() {
709 return hash.read().clone();
710 }
711 let rules = self.rules_store.read();
712 rules::rules_hash(&rules)
713 }
714
715 pub fn update_full_config(
720 &self,
721 new_config: ConfigFile,
722 ) -> Result<MutationResult, ConfigManagerError> {
723 let mut result = MutationResult::new();
724
725 if new_config.sites.is_empty() {
727 result.add_warning("Configuration has no sites defined");
729 }
730
731 let mut seen_hostnames: std::collections::HashSet<String> =
733 std::collections::HashSet::new();
734 for (idx, site) in new_config.sites.iter().enumerate() {
735 if let Err(e) = validate_hostname(&site.hostname) {
737 return Err(ConfigManagerError::Validation(
738 ValidationError::InvalidDomain(format!(
739 "Site[{}] hostname '{}': {}",
740 idx, site.hostname, e
741 )),
742 ));
743 }
744
745 let normalized = site.hostname.to_lowercase();
747 if seen_hostnames.contains(&normalized) {
748 return Err(ConfigManagerError::Validation(
749 ValidationError::InvalidDomain(format!(
750 "Site[{}] hostname '{}' is duplicated",
751 idx, site.hostname
752 )),
753 ));
754 }
755 seen_hostnames.insert(normalized);
756
757 if site.upstreams.is_empty() {
759 return Err(ConfigManagerError::Validation(
760 ValidationError::InvalidDomain(format!(
761 "Site[{}] '{}' has no upstreams defined",
762 idx, site.hostname
763 )),
764 ));
765 }
766 for (u_idx, upstream) in site.upstreams.iter().enumerate() {
767 let upstream_str = format!("{}:{}", upstream.host, upstream.port);
769 if let Err(e) = validate_upstream(&upstream_str) {
770 return Err(ConfigManagerError::Validation(
771 ValidationError::InvalidDomain(format!(
772 "Site[{}] '{}' upstream[{}] '{}:{}': {}",
773 idx, site.hostname, u_idx, upstream.host, upstream.port, e
774 )),
775 ));
776 }
777 }
778
779 if let Some(ref waf) = site.waf {
781 if let Some(threshold) = waf.threshold {
782 if let Err(e) = validate_waf_threshold(threshold as f64) {
784 return Err(ConfigManagerError::Validation(
785 ValidationError::InvalidDomain(format!(
786 "Site[{}] '{}' WAF threshold: {}",
787 idx, site.hostname, e
788 )),
789 ));
790 }
791 }
792 }
793
794 if let Some(ref rl) = site.rate_limit {
796 let burst = rl.burst.unwrap_or(rl.rps.saturating_mul(2));
799 if let Err(e) = validate_rate_limit(rl.rps as u64, burst as u64) {
800 return Err(ConfigManagerError::Validation(
801 ValidationError::InvalidDomain(format!(
802 "Site[{}] '{}' rate limit: {}",
803 idx, site.hostname, e
804 )),
805 ));
806 }
807 }
808
809 if let Some(ref ac) = site.access_control {
811 for (c_idx, cidr) in ac.allow.iter().enumerate() {
812 if let Err(e) = validate_cidr(cidr) {
813 return Err(ConfigManagerError::Validation(
814 ValidationError::InvalidDomain(format!(
815 "Site[{}] '{}' access_control.allow[{}] '{}': {}",
816 idx, site.hostname, c_idx, cidr, e
817 )),
818 ));
819 }
820 }
821 for (c_idx, cidr) in ac.deny.iter().enumerate() {
822 if let Err(e) = validate_cidr(cidr) {
823 return Err(ConfigManagerError::Validation(
824 ValidationError::InvalidDomain(format!(
825 "Site[{}] '{}' access_control.deny[{}] '{}': {}",
826 idx, site.hostname, c_idx, cidr, e
827 )),
828 ));
829 }
830 }
831 }
832 }
833
834 {
836 let mut config = self.config.write();
838 *config = new_config.clone();
839
840 let mut sites = self.sites.write();
842 *sites = new_config
843 .sites
844 .iter()
845 .map(|s| crate::vhost::SiteConfig::from(s.clone()))
846 .collect();
847
848 let new_hostnames: std::collections::HashSet<String> = new_config
851 .sites
852 .iter()
853 .map(|s| s.hostname.to_lowercase())
854 .collect();
855
856 let mut waf = self.waf.write();
857
858 let old_hostnames = waf.hostnames();
860 for old_host in old_hostnames {
861 if !new_hostnames.contains(&old_host.to_lowercase()) {
862 waf.remove_site(&old_host);
863 info!(
864 hostname = %old_host,
865 "Removed site WAF configuration (no longer in config)"
866 );
867 }
868 }
869
870 for site in &new_config.sites {
872 if let Some(ref waf_yaml) = site.waf {
873 if let Some(threshold) = waf_yaml.threshold {
874 let waf_config = crate::site_waf::SiteWafConfig {
875 enabled: waf_yaml.enabled,
876 threshold,
877 rule_overrides: HashMap::new(),
878 custom_block_page: None,
879 default_action: crate::site_waf::WafAction::Block,
880 };
881 waf.add_site(&site.hostname, waf_config);
882 }
883 }
884 }
885
886 let _rate_limiter = self.rate_limiter.write();
890 let mut access_lists = self.access_lists.write();
901
902 let old_access_sites = access_lists.list_sites();
904 for old_host in old_access_sites {
905 if !new_hostnames.contains(&old_host.to_lowercase()) {
906 access_lists.remove_site(&old_host);
907 info!(
908 hostname = %old_host,
909 "Removed site access list (no longer in config)"
910 );
911 }
912 }
913
914 for site in &new_config.sites {
916 if let Some(ac) = &site.access_control {
917 let mut list = crate::access::AccessList::allow_all();
918 for cidr in &ac.allow {
919 let _ = list.allow(cidr);
920 }
921 for cidr in &ac.deny {
922 let _ = list.deny(cidr);
923 }
924 access_lists.add_site(&site.hostname, list);
925 }
926 }
927
928 info!("Full configuration updated with {} sites", sites.len());
929 }
930
931 result = result.with_applied();
932
933 self.rebuild_vhost()?;
935 result = result.with_rebuild();
936
937 if self.config_path.is_some() {
939 match self.persist_config() {
940 Ok(()) => result = result.with_persisted(),
941 Err(e) => {
942 result.add_warning(format!("failed to persist config: {}", e));
943 warn!(error = %e, "failed to persist config after update_full_config");
944 }
945 }
946 }
947
948 Ok(result)
949 }
950
951 pub fn update_site_waf(
957 &self,
958 hostname: &str,
959 waf_req: SiteWafRequest,
960 ) -> Result<MutationResult, ConfigManagerError> {
961 self.update_site(
962 hostname,
963 UpdateSiteRequest {
964 waf: Some(waf_req),
965 ..Default::default()
966 },
967 )
968 }
969
970 pub fn update_site_rate_limit(
972 &self,
973 hostname: &str,
974 rate_limit: RateLimitRequest,
975 ) -> Result<MutationResult, ConfigManagerError> {
976 self.update_site(
977 hostname,
978 UpdateSiteRequest {
979 rate_limit: Some(rate_limit),
980 ..Default::default()
981 },
982 )
983 }
984
985 pub fn update_site_access_list(
987 &self,
988 hostname: &str,
989 access_list: AccessListRequest,
990 ) -> Result<MutationResult, ConfigManagerError> {
991 self.update_site(
992 hostname,
993 UpdateSiteRequest {
994 access_list: Some(access_list),
995 ..Default::default()
996 },
997 )
998 }
999
1000 pub fn list_rules(&self) -> Vec<StoredRule> {
1006 self.rules_store.read().clone()
1007 }
1008
1009 pub fn create_rule(&self, rule: StoredRule) -> Result<StoredRule, ConfigManagerError> {
1011 let mut rules = self.rules_store.read().clone();
1012 let rule_id = rules::rule_identifier(&rule);
1013
1014 if rules
1015 .iter()
1016 .any(|existing| rules::matches_rule_id(existing, &rule_id))
1017 {
1018 return Err(ConfigManagerError::RuleExists(rule_id));
1019 }
1020
1021 rules.push(rule.clone());
1022 self.apply_rules(rules, true, None)?;
1023 Ok(rule)
1024 }
1025
1026 pub fn update_rule(
1028 &self,
1029 rule_id: &str,
1030 update: CustomRuleUpdate,
1031 ) -> Result<StoredRule, ConfigManagerError> {
1032 let mut rules = self.rules_store.read().clone();
1033 let Some(index) = rules
1034 .iter()
1035 .position(|rule| rules::matches_rule_id(rule, rule_id))
1036 else {
1037 return Err(ConfigManagerError::RuleNotFound(rule_id.to_string()));
1038 };
1039
1040 let updated = rules::merge_rule_update(&rules[index], update)
1041 .map_err(ConfigManagerError::Persistence)?;
1042 rules[index] = updated.clone();
1043 self.apply_rules(rules, true, None)?;
1044 Ok(updated)
1045 }
1046
1047 pub fn delete_rule(&self, rule_id: &str) -> Result<(), ConfigManagerError> {
1049 let mut rules = self.rules_store.read().clone();
1050 let original_len = rules.len();
1051 rules.retain(|rule| !rules::matches_rule_id(rule, rule_id));
1052
1053 if rules.len() == original_len {
1054 return Err(ConfigManagerError::RuleNotFound(rule_id.to_string()));
1055 }
1056
1057 self.apply_rules(rules, true, None)?;
1058 Ok(())
1059 }
1060
1061 pub fn replace_rules(
1063 &self,
1064 rules: Vec<StoredRule>,
1065 hash_override: Option<String>,
1066 ) -> Result<usize, ConfigManagerError> {
1067 self.apply_rules(rules, true, hash_override)
1068 }
1069
1070 pub fn update_waf_rules(
1084 &self,
1085 rules_json: &[u8],
1086 hash_override: Option<&str>,
1087 ) -> Result<usize, ConfigManagerError> {
1088 let value: serde_json::Value = serde_json::from_slice(rules_json)
1089 .map_err(|e| ConfigManagerError::Persistence(format!("Invalid rules JSON: {}", e)))?;
1090
1091 let rules = rules::parse_rules_payload(value).map_err(ConfigManagerError::Persistence)?;
1092
1093 let rule_count = rules.len();
1094
1095 if rule_count == 0 {
1096 warn!("Received empty rules update from Horizon Hub");
1097 return Ok(0);
1098 }
1099
1100 info!(rule_count, "Received WAF rules update from Horizon Hub");
1101
1102 let applied = self.apply_rules(rules, true, hash_override.map(|s| s.to_string()))?;
1103
1104 info!(
1105 rules_received = rule_count,
1106 rules_applied = applied,
1107 sites_affected = self.waf.read().site_count(),
1108 "WAF rules synchronized from Horizon Hub"
1109 );
1110
1111 Ok(rule_count)
1112 }
1113
1114 fn load_rules_from_disk(&self) -> Result<usize, ConfigManagerError> {
1119 let Some(path) = self.rules_path.clone() else {
1120 return Ok(0);
1121 };
1122
1123 if let Err(err) = recover_rules_from_wal(&path) {
1124 warn!("Failed to recover rules WAL: {}", err);
1125 }
1126
1127 if !path.exists() {
1128 return Ok(0);
1129 }
1130
1131 let rules_json = fs::read(&path)
1132 .map_err(|e| ConfigManagerError::Persistence(format!("failed to read rules: {}", e)))?;
1133 let value: serde_json::Value = serde_json::from_slice(&rules_json)
1134 .map_err(|e| ConfigManagerError::Persistence(format!("invalid rules JSON: {}", e)))?;
1135 let rules = rules::parse_rules_payload(value).map_err(ConfigManagerError::Persistence)?;
1136
1137 if rules.is_empty() {
1138 return Ok(0);
1139 }
1140
1141 self.apply_rules(rules, false, None)
1142 }
1143
1144 fn apply_rules(
1145 &self,
1146 rules: Vec<StoredRule>,
1147 persist: bool,
1148 hash_override: Option<String>,
1149 ) -> Result<usize, ConfigManagerError> {
1150 let engine = self.rules_engine.as_ref().ok_or_else(|| {
1151 ConfigManagerError::Persistence("rules engine not configured".to_string())
1152 })?;
1153
1154 let mut active_rules: Vec<&StoredRule> = rules
1155 .iter()
1156 .filter(|rule| rule.meta.enabled.unwrap_or(true))
1157 .collect();
1158
1159 active_rules.sort_by(|a, b| {
1160 let a_priority = a.meta.priority.unwrap_or(100);
1161 let b_priority = b.meta.priority.unwrap_or(100);
1162 a_priority
1163 .cmp(&b_priority)
1164 .then_with(|| a.rule.id.cmp(&b.rule.id))
1165 });
1166
1167 let waf_rules: Vec<_> = active_rules.iter().map(|rule| rule.rule.clone()).collect();
1168 let waf_json = serde_json::to_vec(&waf_rules).map_err(|e| {
1169 ConfigManagerError::Persistence(format!("failed to serialize waf rules: {}", e))
1170 })?;
1171
1172 let applied = engine.write().load_rules(&waf_json).map_err(|e| {
1173 ConfigManagerError::Persistence(format!("failed to load waf rules: {}", e))
1174 })?;
1175
1176 *self.rules_store.write() = rules.clone();
1177
1178 if persist {
1179 self.persist_rules(&rules)?;
1180 }
1181
1182 self.update_rules_hash(hash_override.unwrap_or_else(|| rules::rules_hash(&rules)));
1183
1184 Ok(applied)
1185 }
1186
1187 fn persist_rules(&self, rules: &[StoredRule]) -> Result<(), ConfigManagerError> {
1188 let Some(path) = self.rules_path.clone() else {
1189 return Ok(());
1190 };
1191
1192 if let Some(parent) = path.parent() {
1193 if let Err(err) = fs::create_dir_all(parent) {
1194 return Err(ConfigManagerError::Persistence(format!(
1195 "failed to create rules directory: {}",
1196 err
1197 )));
1198 }
1199 }
1200
1201 let payload = serde_json::to_vec_pretty(rules).map_err(|e| {
1202 ConfigManagerError::Persistence(format!("failed to serialize rules: {}", e))
1203 })?;
1204
1205 let wal_path = path.with_extension("wal");
1206 append_wal_entry(
1207 &wal_path,
1208 serde_json::json!({
1209 "timestamp_ms": current_timestamp_ms(),
1210 "type": "rules_update",
1211 "rules": rules,
1212 }),
1213 )?;
1214
1215 write_file_with_fsync(&path, &payload).map_err(|e| {
1216 ConfigManagerError::Persistence(format!("failed to write rules: {}", e))
1217 })?;
1218
1219 clear_wal(&wal_path)?;
1220
1221 info!(path = %path.display(), "persisted rules");
1222 Ok(())
1223 }
1224
1225 fn update_rules_hash(&self, value: String) {
1226 if let Some(hash_lock) = self.rules_hash.as_ref() {
1227 *hash_lock.write() = value;
1228 }
1229 }
1230
1231 fn rebuild_vhost(&self) -> Result<(), ConfigManagerError> {
1232 let sites = self.sites.read();
1233 let new_vhost = VhostMatcher::new(sites.clone())
1234 .map_err(|e| ConfigManagerError::RebuildError(e.to_string()))?;
1235
1236 let mut vhost = self.vhost.write();
1237 *vhost = new_vhost;
1238
1239 debug!("rebuilt VhostMatcher with {} sites", sites.len());
1240 Ok(())
1241 }
1242
1243 fn persist_config(&self) -> Result<(), ConfigManagerError> {
1244 let path = self.config_path.as_ref().ok_or_else(|| {
1245 ConfigManagerError::Persistence("no config path configured".to_string())
1246 })?;
1247
1248 let config = self.config.read();
1249 let yaml = serde_yaml::to_string(&*config).map_err(|e| {
1250 ConfigManagerError::Persistence(format!("failed to serialize config: {}", e))
1251 })?;
1252
1253 write_file_with_fsync(path, yaml.as_bytes()).map_err(|e| {
1254 ConfigManagerError::Persistence(format!("failed to write config: {}", e))
1255 })?;
1256
1257 info!(path = %path.display(), "persisted configuration");
1258 Ok(())
1259 }
1260}
1261
1262fn current_timestamp_ms() -> u64 {
1263 use std::time::{SystemTime, UNIX_EPOCH};
1264
1265 SystemTime::now()
1266 .duration_since(UNIX_EPOCH)
1267 .map(|duration| duration.as_millis() as u64)
1268 .unwrap_or(0)
1269}
1270
1271fn recover_rules_from_wal(path: &std::path::Path) -> Result<bool, ConfigManagerError> {
1272 let wal_path = path.with_extension("wal");
1273 if !wal_path.exists() {
1274 return Ok(false);
1275 }
1276
1277 let contents = fs::read_to_string(&wal_path).map_err(|err| {
1278 ConfigManagerError::Persistence(format!("failed to read WAL file: {}", err))
1279 })?;
1280 if contents.trim().is_empty() {
1281 return Ok(false);
1282 }
1283
1284 let mut last_rules: Option<Vec<StoredRule>> = None;
1285 for line in contents.lines() {
1286 let value: serde_json::Value = match serde_json::from_str(line) {
1287 Ok(value) => value,
1288 Err(err) => {
1289 warn!("Skipping invalid WAL entry: {}", err);
1290 continue;
1291 }
1292 };
1293 if value.get("type").and_then(|t| t.as_str()) != Some("rules_update") {
1294 continue;
1295 }
1296 let rules_value = value
1297 .get("rules")
1298 .cloned()
1299 .unwrap_or(serde_json::Value::Null);
1300 match serde_json::from_value::<Vec<StoredRule>>(rules_value) {
1301 Ok(rules) if !rules.is_empty() => {
1302 last_rules = Some(rules);
1303 }
1304 Ok(_) => {}
1305 Err(err) => {
1306 warn!("Skipping WAL rules entry: {}", err);
1307 }
1308 }
1309 }
1310
1311 let Some(rules) = last_rules else {
1312 return Ok(false);
1313 };
1314
1315 if let Some(parent) = path.parent() {
1316 if let Err(err) = fs::create_dir_all(parent) {
1317 return Err(ConfigManagerError::Persistence(format!(
1318 "failed to create rules directory: {}",
1319 err
1320 )));
1321 }
1322 }
1323
1324 let payload = serde_json::to_vec_pretty(&rules).map_err(|err| {
1325 ConfigManagerError::Persistence(format!("failed to serialize WAL rules: {}", err))
1326 })?;
1327
1328 write_file_with_fsync(path, &payload).map_err(|err| {
1329 ConfigManagerError::Persistence(format!("failed to apply WAL rules: {}", err))
1330 })?;
1331 clear_wal(&wal_path)?;
1332 info!(path = %path.display(), "recovered rules from WAL");
1333 Ok(true)
1334}
1335
1336fn append_wal_entry(
1337 path: &std::path::Path,
1338 entry: serde_json::Value,
1339) -> Result<(), ConfigManagerError> {
1340 use std::io::Write;
1341 let Some(parent) = path.parent() else {
1342 return Err(ConfigManagerError::Persistence(
1343 "invalid WAL path".to_string(),
1344 ));
1345 };
1346
1347 if let Err(err) = fs::create_dir_all(parent) {
1348 return Err(ConfigManagerError::Persistence(format!(
1349 "failed to create WAL directory: {}",
1350 err
1351 )));
1352 }
1353
1354 let mut file = fs::OpenOptions::new()
1355 .create(true)
1356 .append(true)
1357 .open(path)
1358 .map_err(|err| {
1359 ConfigManagerError::Persistence(format!("failed to open WAL file: {}", err))
1360 })?;
1361
1362 let payload = serde_json::to_vec(&entry).map_err(|err| {
1363 ConfigManagerError::Persistence(format!("failed to serialize WAL entry: {}", err))
1364 })?;
1365 file.write_all(&payload)
1366 .and_then(|_| file.write_all(b"\n"))
1367 .and_then(|_| file.sync_all())
1368 .map_err(|err| {
1369 ConfigManagerError::Persistence(format!("failed to persist WAL entry: {}", err))
1370 })?;
1371
1372 Ok(())
1373}
1374
1375fn write_file_with_fsync(path: &std::path::Path, contents: &[u8]) -> Result<(), std::io::Error> {
1376 use std::io::Write;
1377
1378 let mut file = fs::OpenOptions::new()
1379 .create(true)
1380 .truncate(true)
1381 .write(true)
1382 .open(path)?;
1383 file.write_all(contents)?;
1384 file.sync_all()?;
1385 Ok(())
1386}
1387
1388fn clear_wal(path: &std::path::Path) -> Result<(), ConfigManagerError> {
1389 use std::io::Write;
1390
1391 let mut file = fs::OpenOptions::new()
1392 .create(true)
1393 .truncate(true)
1394 .write(true)
1395 .open(path)
1396 .map_err(|err| {
1397 ConfigManagerError::Persistence(format!("failed to open WAL file: {}", err))
1398 })?;
1399 file.write_all(b"")
1400 .and_then(|_| file.sync_all())
1401 .map_err(|err| {
1402 ConfigManagerError::Persistence(format!("failed to clear WAL file: {}", err))
1403 })?;
1404 Ok(())
1405}
1406
1407#[cfg(test)]
1408mod tests {
1409 use super::*;
1410 use crate::waf::{MatchCondition, MatchValue, WafRule};
1411
1412 #[test]
1413 fn test_mutation_result_builder() {
1414 let result = MutationResult::new().with_applied().with_rebuild();
1415
1416 assert!(result.applied);
1417 assert!(result.rebuild_required);
1418 assert!(!result.persisted);
1419 }
1420
1421 #[test]
1422 fn test_create_site_request_serialization() {
1423 let req = CreateSiteRequest {
1424 hostname: "api.example.com".to_string(),
1425 upstreams: vec!["10.0.0.1:8080".to_string()],
1426 waf: Some(SiteWafRequest {
1427 enabled: true,
1428 threshold: Some(0.7),
1429 rule_overrides: None,
1430 }),
1431 rate_limit: None,
1432 access_list: None,
1433 };
1434
1435 let json = serde_json::to_string(&req).unwrap();
1436 assert!(json.contains("api.example.com"));
1437 assert!(json.contains("10.0.0.1:8080"));
1438 }
1439
1440 #[test]
1441 fn test_update_site_request_default() {
1442 let req = UpdateSiteRequest::default();
1443 assert!(req.upstreams.is_none());
1444 assert!(req.waf.is_none());
1445 assert!(req.rate_limit.is_none());
1446 assert!(req.access_list.is_none());
1447 }
1448
1449 #[test]
1450 fn test_site_detail_response_serialization() {
1451 let response = SiteDetailResponse {
1452 hostname: "api.example.com".to_string(),
1453 upstreams: vec!["10.0.0.1:8080".to_string()],
1454 tls_enabled: false,
1455 waf: Some(SiteWafResponse {
1456 enabled: true,
1457 threshold: 70,
1458 rule_overrides: HashMap::new(),
1459 }),
1460 rate_limit: Some(RateLimitResponse {
1461 requests_per_second: 100,
1462 burst: 200,
1463 }),
1464 access_list: None,
1465 shadow_mirror: None,
1466 };
1467
1468 let json = serde_json::to_string(&response).unwrap();
1469 assert!(json.contains("api.example.com"));
1470 assert!(json.contains("\"threshold\":70"));
1471 }
1472
1473 fn test_rule(id: u32) -> StoredRule {
1474 StoredRule {
1475 rule: WafRule {
1476 id,
1477 description: format!("rule-{}", id),
1478 contributing_score: None,
1479 risk: None,
1480 blocking: None,
1481 matches: vec![MatchCondition {
1482 kind: "match".to_string(),
1483 match_value: Some(MatchValue::Str("test".to_string())),
1484 op: None,
1485 field: None,
1486 direction: None,
1487 field_type: None,
1488 name: None,
1489 selector: None,
1490 cleanup_after: None,
1491 count: None,
1492 timeframe: None,
1493 }],
1494 },
1495 meta: RuleMetadata::default(),
1496 }
1497 }
1498
1499 #[test]
1500 fn test_recover_rules_from_wal_overwrites_rules_file() {
1501 let dir = tempfile::tempdir().unwrap();
1502 let rules_path = dir.path().join("rules.json");
1503 let wal_path = rules_path.with_extension("wal");
1504
1505 let old_rules = vec![test_rule(1)];
1506 let new_rules = vec![test_rule(42)];
1507
1508 fs::write(&rules_path, serde_json::to_vec_pretty(&old_rules).unwrap()).unwrap();
1509
1510 let wal_entry = serde_json::json!({
1511 "timestamp_ms": 1,
1512 "type": "rules_update",
1513 "rules": new_rules,
1514 });
1515 fs::write(&wal_path, format!("{}\n", wal_entry)).unwrap();
1516
1517 let recovered = recover_rules_from_wal(&rules_path).unwrap();
1518 assert!(recovered);
1519
1520 let persisted: Vec<StoredRule> =
1521 serde_json::from_slice(&fs::read(&rules_path).unwrap()).unwrap();
1522 assert_eq!(persisted.len(), 1);
1523 assert_eq!(persisted[0].rule.id, 42);
1524
1525 let wal_contents = fs::read_to_string(&wal_path).unwrap();
1526 assert!(wal_contents.trim().is_empty());
1527 }
1528}