use std::sync::{Arc, RwLock};
use guard_core_engine::cloud_provider::{CloudIpTable, parse_cloud_selectors};
use guard_core_engine::geo::{GeoIpHandler, parse_country_lists};
use guard_core_engine::ip_ban::{IpBanConfig, IpBanManager};
use guard_core_engine::rate_limit::{RateLimitConfig, RateLimiter};
use guard_core_engine::security_config::SecurityConfig;
use guard_core_engine::user_agent::UserAgentFilter;
use crate::cloud_provider::{CloudProviderStage, CloudProviderStageConfig};
use crate::dynamic_rules::SnapshotConfig;
use crate::emergency_mode::{EmergencyModeStage, EmergencyModeStageConfig};
use crate::geo::{GeoStage, GeoStageConfig};
use crate::user_agent::{UserAgentStage, UserAgentStageConfig};
use crate::wiring::{WiringError, rate_limit_config};
#[derive(Clone)]
#[allow(clippy::struct_excessive_bools)]
pub struct LiveStages {
pub penetration_detection: bool,
pub geo: Option<GeoStage>,
pub cloud: Option<CloudProviderStage>,
pub user_agent: Option<UserAgentStage>,
pub emergency: Option<EmergencyModeStage>,
pub rate_limiter: RateLimiter,
pub rate_limit: RateLimitConfig,
pub ip_ban: IpBanConfig,
pub bans: IpBanManager,
}
#[derive(Clone)]
pub struct LiveContext {
config: Arc<RwLock<SecurityConfig>>,
geo_handler: Option<Arc<dyn GeoIpHandler>>,
cloud_table: Arc<CloudIpTable>,
limiter: RateLimiter,
bans: IpBanManager,
}
impl LiveContext {
pub(crate) const fn new(
config: Arc<RwLock<SecurityConfig>>,
geo_handler: Option<Arc<dyn GeoIpHandler>>,
cloud_table: Arc<CloudIpTable>,
limiter: RateLimiter,
bans: IpBanManager,
) -> Self {
Self {
config,
geo_handler,
cloud_table,
limiter,
bans,
}
}
#[must_use]
pub const fn config(&self) -> &Arc<RwLock<SecurityConfig>> {
&self.config
}
#[must_use]
pub const fn bans(&self) -> &IpBanManager {
&self.bans
}
pub fn build(&self, config: &SecurityConfig) -> Result<LiveStages, WiringError> {
let gate = parse_country_lists(&config.whitelist_countries, &config.blocked_countries);
let geo = gate.has_rules().then(|| {
GeoStage::new(GeoStageConfig {
gate,
handler: self.geo_handler.clone(),
passive_mode: config.passive_mode,
})
});
let cloud = match config
.block_cloud_providers
.as_ref()
.filter(|providers| !providers.is_empty())
{
Some(providers) => Some(CloudProviderStage::new(CloudProviderStageConfig {
block_cloud_providers: parse_cloud_selectors(providers)?,
table: (*self.cloud_table).clone(),
passive_mode: config.passive_mode,
})),
None => None,
};
let user_agent = if config.blocked_user_agents.is_empty() {
None
} else {
Some(
UserAgentStage::builder(UserAgentStageConfig {
blocked_user_agents: UserAgentFilter::from_trusted_patterns(
config.blocked_user_agents.iter().cloned(),
)?,
ip_ban: config.ip_ban_config(),
passive_mode: config.passive_mode,
})
.build()?,
)
};
let emergency = if config.emergency_mode || !config.emergency_whitelist.is_empty() {
Some(
EmergencyModeStage::builder(EmergencyModeStageConfig {
emergency_mode: config.emergency_mode,
passive_mode: config.passive_mode,
})
.emergency_whitelist(config.emergency_whitelist.iter().cloned())
.build()?,
)
} else {
None
};
let rate_limit = rate_limit_config(config)?;
Ok(LiveStages {
penetration_detection: config.enable_penetration_detection,
geo,
cloud,
user_agent,
emergency,
rate_limiter: self.limiter.with_config(rate_limit.clone())?,
rate_limit,
ip_ban: config.ip_ban_config(),
bans: self.bans.clone(),
})
}
pub fn cell<T, F>(&self, derive: F) -> Result<LiveCell<T>, WiringError>
where
F: Fn(&Arc<LiveStages>) -> T + Send + Sync + 'static,
{
let config = self.config.read().expect("live config");
let stages = Arc::new(self.build(&config)?);
let state = LiveState {
snapshot: SnapshotConfig::capture(&config),
value: Arc::new(derive(&stages)),
stages,
};
drop(config);
Ok(LiveCell {
context: self.clone(),
derive: Box::new(derive),
state: RwLock::new(state),
})
}
}
type Derive<T> = Box<dyn Fn(&Arc<LiveStages>) -> T + Send + Sync>;
struct LiveState<T> {
snapshot: SnapshotConfig,
stages: Arc<LiveStages>,
value: Arc<T>,
}
pub struct LiveCell<T> {
context: LiveContext,
derive: Derive<T>,
state: RwLock<LiveState<T>>,
}
impl<T> LiveCell<T> {
#[must_use]
pub fn get(&self) -> Arc<T> {
let Ok(config) = self.context.config.read() else {
return Arc::clone(&self.state.read().expect("live state").value);
};
{
let state = self.state.read().expect("live state");
if state.snapshot.matches(&config) {
return Arc::clone(&state.value);
}
}
let mut state = self.state.write().expect("live state");
state.snapshot = SnapshotConfig::capture(&config);
if let Ok(stages) = self.context.build(&config) {
let stages = Arc::new(stages);
state.value = Arc::new((self.derive)(&stages));
state.stages = stages;
}
Arc::clone(&state.value)
}
#[must_use]
pub fn stages(&self) -> Arc<LiveStages> {
let _current = self.get();
Arc::clone(&self.state.read().expect("live state").stages)
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use std::net::IpAddr;
use super::*;
use crate::wiring::Wiring;
struct Germany;
impl GeoIpHandler for Germany {
fn get_country(&self, _ip: IpAddr) -> Option<String> {
Some("DE".to_owned())
}
}
fn wired() -> (Wiring, LiveCell<LiveStages>) {
let config = SecurityConfig {
enable_redis: false,
enable_rate_limiting: true,
rate_limit: 3,
geo_ip_handler: Some(Arc::new(Germany)),
..SecurityConfig::default()
};
let wiring = Wiring::from_security_config(&config).expect("wired");
let cell = wiring
.live
.cell(|stages| LiveStages::clone(stages))
.expect("cell");
(wiring, cell)
}
fn mutate(wiring: &Wiring, change: impl FnOnce(&mut SecurityConfig)) {
change(&mut wiring.live_config.write().expect("live config"));
}
#[test]
fn an_unchanged_config_returns_the_cached_value() {
let (_wiring, cell) = wired();
assert!(Arc::ptr_eq(&cell.get(), &cell.get()));
}
#[test]
fn a_rate_limit_change_reaches_the_rebuilt_limiter_and_keeps_the_windows() {
let (wiring, cell) = wired();
let ip: IpAddr = "198.51.100.7".parse().unwrap();
let before = cell.get();
assert_eq!(before.rate_limit.rate_limit, 3);
for _ in 0..3 {
assert!(before.rate_limiter.check(ip, None).allowed);
}
assert!(!before.rate_limiter.check(ip, None).allowed);
mutate(&wiring, |config| config.rate_limit = 50);
let after = cell.get();
assert!(!Arc::ptr_eq(&before, &after));
assert_eq!(after.rate_limit.rate_limit, 50);
assert!(after.rate_limiter.check(ip, None).allowed);
assert_eq!(after.rate_limiter.tracked_windows(), 1);
}
#[test]
fn country_agent_emergency_and_ban_changes_rebuild_their_stages() {
let (wiring, cell) = wired();
let before = cell.get();
assert!(before.geo.is_none() && before.user_agent.is_none());
assert!(before.emergency.is_none());
mutate(&wiring, |config| {
config.blocked_countries = BTreeSet::from(["DE".to_owned()]);
config.blocked_user_agents = vec!["badbot".to_owned()];
config.emergency_mode = true;
config.enable_penetration_detection = false;
config.auto_ban_threshold = 2;
});
let after = cell.get();
let geo = after.geo.as_ref().expect("geo stage");
assert!(
geo.decide("203.0.113.9".parse().ok(), None).is_some(),
"the new blocklist blocks DE"
);
assert!(after.user_agent.is_some());
assert!(after.emergency.is_some());
assert!(!after.penetration_detection);
assert_eq!(after.ip_ban.auto_ban_threshold, 2);
}
#[test]
fn a_rejected_rebuild_keeps_the_previous_value_and_is_not_retried() {
let (wiring, cell) = wired();
let before = cell.get();
mutate(&wiring, |config| {
config.block_cloud_providers = Some(BTreeSet::from(["nope".to_owned()]));
});
let kept = cell.get();
assert!(Arc::ptr_eq(&before, &kept));
assert!(Arc::ptr_eq(&kept, &cell.get()));
}
#[test]
fn a_cloud_provider_rule_builds_the_stage_and_an_uncompilable_agent_is_rejected() {
let (wiring, cell) = wired();
mutate(&wiring, |config| {
config.block_cloud_providers = Some(BTreeSet::from(["AWS".to_owned()]));
});
assert!(cell.get().cloud.is_some());
let kept = cell.get();
mutate(&wiring, |config| {
config.blocked_user_agents = vec!["(".to_owned()];
});
assert!(Arc::ptr_eq(&kept, &cell.get()));
}
#[test]
fn the_stages_accessor_follows_the_value() {
let (wiring, cell) = wired();
mutate(&wiring, |config| config.rate_limit = 9);
assert_eq!(cell.stages().rate_limit.rate_limit, 9);
assert_eq!(wiring.live.bans().banned_count(), 0);
}
#[test]
fn a_poisoned_config_lock_serves_the_cached_value() {
let (wiring, cell) = wired();
let before = cell.get();
let handle = Arc::clone(&wiring.live_config);
let _ = std::thread::spawn(move || {
let _guard = handle.write().expect("lock");
panic!("poison the live config");
})
.join();
assert!(Arc::ptr_eq(&before, &cell.get()));
assert!(wiring.live.config().read().is_err());
}
}