use std::net::IpAddr;
use crate::ip_gate::{IpGateError, IpNet, canonical, parse_network_entry};
pub const EMERGENCY_MODE_CHECK_NAME: &str = "emergency_mode";
pub const EMERGENCY_BLOCK_STATUS: u16 = 503;
pub const EMERGENCY_BLOCK_BODY: &str = "Service temporarily unavailable";
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct EmergencyModeConfig {
whitelist: Vec<IpNet>,
}
impl EmergencyModeConfig {
pub fn new<I>(entries: I) -> Result<Self, IpGateError>
where
I: IntoIterator,
I::Item: AsRef<str>,
{
let mut whitelist = Vec::new();
for entry in entries {
let entry = entry.as_ref();
match parse_network_entry(entry) {
Some(network) => whitelist.push(network),
None => {
return Err(IpGateError {
list: "emergency_whitelist",
entry: entry.to_owned(),
});
}
}
}
Ok(Self { whitelist })
}
#[must_use]
pub const fn len(&self) -> usize {
self.whitelist.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.whitelist.is_empty()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EmergencyVerdict {
Allowed,
Denied,
}
#[must_use]
pub fn decide(client_ip: Option<&str>, config: &EmergencyModeConfig) -> EmergencyVerdict {
if let Some(raw) = client_ip
&& let Ok(addr) = raw.parse::<IpAddr>()
&& config
.whitelist
.iter()
.any(|network| network.contains(canonical(addr)) || network.contains(addr))
{
return EmergencyVerdict::Allowed;
}
EmergencyVerdict::Denied
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn whitelist_members_pass_everyone_else_is_denied() {
let config = EmergencyModeConfig::new(["192.0.2.40"]).unwrap();
assert_eq!(
decide(Some("192.0.2.40"), &config),
EmergencyVerdict::Allowed
);
assert_eq!(
decide(Some("192.0.2.41"), &config),
EmergencyVerdict::Denied
);
}
#[test]
fn missing_or_unparseable_ip_is_denied() {
let config = EmergencyModeConfig::new(["192.0.2.40"]).unwrap();
assert_eq!(decide(None, &config), EmergencyVerdict::Denied);
assert_eq!(decide(Some("not-an-ip"), &config), EmergencyVerdict::Denied);
assert_eq!(decide(Some(""), &config), EmergencyVerdict::Denied);
}
#[test]
fn cidr_entries_and_families_match_like_the_global_lists() {
let config = EmergencyModeConfig::new(["198.51.100.0/24", "2001:db8:ffff::/48"]).unwrap();
assert_eq!(
decide(Some("198.51.100.7"), &config),
EmergencyVerdict::Allowed
);
assert_eq!(
decide(Some("::ffff:198.51.100.7"), &config),
EmergencyVerdict::Allowed,
"a v4-mapped request matches its IPv4 entry"
);
assert_eq!(
decide(Some("2001:db8:ffff::9"), &config),
EmergencyVerdict::Allowed
);
assert_eq!(
decide(Some("198.51.101.7"), &config),
EmergencyVerdict::Denied,
"families and ranges never stretch"
);
}
#[test]
fn an_empty_whitelist_denies_everything() {
let config = EmergencyModeConfig::new(Vec::<String>::new()).unwrap();
assert!(config.is_empty());
assert_eq!(config.len(), 0);
assert_eq!(
decide(Some("192.0.2.40"), &config),
EmergencyVerdict::Denied
);
}
#[test]
fn config_fails_closed_on_a_bad_entry() {
let error = EmergencyModeConfig::new(["junk"]).unwrap_err();
assert_eq!(error.list, "emergency_whitelist");
assert_eq!(error.entry, "junk");
}
}