use crate::constants::{
MAX_API_KEY_LENGTH, MAX_BAN_REASON_LENGTH, MAX_HEADER_VALUE_LENGTH, MAX_IP_ADDRESS_LENGTH,
MAX_MAC_ADDRESS_LENGTH, MAX_PATH_LENGTH, MAX_USER_ID_LENGTH,
};
use crate::error::LimiteronError;
#[cfg(feature = "ban-manager")]
use crate::storage::BanTarget;
pub fn validate_ip_address(ip: &str) -> Result<(), LimiteronError> {
if ip.is_empty() {
return Err(LimiteronError::ValidationError(
"IP address cannot be empty".to_string(),
));
}
if ip.len() > MAX_IP_ADDRESS_LENGTH {
return Err(LimiteronError::ValidationError(format!(
"IP address exceeds maximum length (max: {}, actual: {})",
MAX_IP_ADDRESS_LENGTH,
ip.len()
)));
}
let ip_part = extract_ip_part(ip)?;
ip_part.parse::<std::net::IpAddr>().map_err(|_| {
LimiteronError::ValidationError(format!("Invalid IP address format: {}", ip))
})?;
Ok(())
}
fn extract_ip_part(ip: &str) -> Result<&str, LimiteronError> {
if ip.starts_with('[') {
if let Some(close_bracket) = ip.find(']') {
return Ok(&ip[1..close_bracket]);
}
return Err(LimiteronError::ValidationError(
"Invalid IPv6 address format: missing closing bracket".to_string(),
));
}
if let Some(last_colon) = ip.rfind(':') {
let colon_count = ip.chars().filter(|&c| c == ':').count();
if colon_count == 1 {
let potential_ip = &ip[..last_colon];
if potential_ip.contains('.') {
return Ok(potential_ip);
}
}
}
Ok(ip)
}
pub fn validate_user_id(user_id: &str) -> Result<(), LimiteronError> {
if user_id.is_empty() {
return Err(LimiteronError::ValidationError(
"User ID cannot be empty".to_string(),
));
}
if user_id.len() > MAX_USER_ID_LENGTH {
return Err(LimiteronError::ConfigError(format!(
"User ID exceeds maximum length ({})",
MAX_USER_ID_LENGTH
)));
}
if !user_id
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '@' || c == '.')
{
return Err(LimiteronError::ValidationError(
"User ID contains invalid characters".to_string(),
));
}
Ok(())
}
pub fn validate_mac_address(mac: &str) -> Result<(), LimiteronError> {
if mac.is_empty() {
return Err(LimiteronError::ValidationError(
"MAC address cannot be empty".to_string(),
));
}
if mac.len() > MAX_MAC_ADDRESS_LENGTH {
return Err(LimiteronError::ConfigError(format!(
"MAC address exceeds maximum length ({})",
MAX_MAC_ADDRESS_LENGTH
)));
}
let cleaned = mac.replace([':', '-', '.'], "");
if cleaned.len() != 12 {
return Err(LimiteronError::ValidationError(
"Invalid MAC address format".to_string(),
));
}
if !cleaned.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(LimiteronError::ValidationError(
"MAC address contains invalid characters".to_string(),
));
}
Ok(())
}
pub fn validate_length(
value: &str,
max_length: usize,
field_name: &str,
) -> Result<(), LimiteronError> {
if value.len() > max_length {
return Err(LimiteronError::ConfigError(format!(
"{} exceeds maximum length ({})",
field_name, max_length
)));
}
Ok(())
}
pub fn validate_ban_reason(reason: &str) -> Result<(), LimiteronError> {
validate_length(reason, MAX_BAN_REASON_LENGTH, "Ban reason")
}
pub fn validate_api_key(api_key: &str) -> Result<(), LimiteronError> {
validate_length(api_key, MAX_API_KEY_LENGTH, "API key")
}
pub fn validate_header_value(value: &str) -> Result<(), LimiteronError> {
validate_length(value, MAX_HEADER_VALUE_LENGTH, "Header value")
}
pub fn validate_path(path: &str) -> Result<(), LimiteronError> {
validate_length(path, MAX_PATH_LENGTH, "Path")
}
#[cfg(feature = "ban-manager")]
pub fn validate_ban_target(target: &BanTarget) -> Result<(), LimiteronError> {
match target {
BanTarget::Ip(ip) => validate_ip_address(ip),
BanTarget::UserId(user_id) => validate_user_id(user_id),
BanTarget::Mac(mac) => validate_mac_address(mac),
BanTarget::Geo { country_code } => validate_geo_country_code(country_code),
}
}
pub fn validate_geo_country_code(code: &str) -> Result<(), LimiteronError> {
if code.is_empty() {
return Err(LimiteronError::ValidationError(
"国家代码不能为空".to_string(),
));
}
if code.len() != 2 {
return Err(LimiteronError::ValidationError(format!(
"国家代码必须是 2 字母(ISO 3166-1 alpha-2),got {} 字符: {}",
code.len(),
code
)));
}
if !code.chars().all(|c| c.is_ascii_uppercase()) {
return Err(LimiteronError::ValidationError(format!(
"国家代码必须是大写字母: {}",
code
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_ip_address_ipv4() {
assert!(validate_ip_address("192.168.1.1").is_ok());
assert!(validate_ip_address("10.0.0.1").is_ok());
assert!(validate_ip_address("255.255.255.255").is_ok());
assert!(validate_ip_address("0.0.0.0").is_ok());
assert!(validate_ip_address("192.168.1.1:8080").is_ok());
assert!(validate_ip_address("127.0.0.1:443").is_ok());
}
#[test]
fn test_validate_ip_address_ipv4_invalid() {
assert!(validate_ip_address("").is_err());
assert!(validate_ip_address("abc").is_err());
assert!(validate_ip_address("256.1.1.1").is_err()); assert!(validate_ip_address("192.168.1").is_err()); assert!(validate_ip_address("192.168.1.1.1").is_err()); assert!(validate_ip_address("192.168.1.256").is_err()); assert!(validate_ip_address("192.168.1.-1").is_err()); }
#[test]
fn test_ipv6_full_format() {
assert!(validate_ip_address("2001:0db8:85a3:0000:0000:8a2e:0370:7334").is_ok());
assert!(validate_ip_address("2001:0db8:85a3:0000:0000:8a2e:0370:7334").is_ok());
assert!(validate_ip_address("2001:0db8:85a3:0000:0000:8a2e:0370:7334").is_ok());
assert!(validate_ip_address("2001:0DB8:85A3:0000:0000:8A2E:0370:7334").is_ok());
}
#[test]
fn test_ipv6_compressed_format() {
assert!(validate_ip_address("2001:db8:85a3::8a2e:370:7334").is_ok());
assert!(validate_ip_address("::1").is_ok()); assert!(validate_ip_address("::").is_ok()); assert!(validate_ip_address("fe80::1").is_ok()); assert!(validate_ip_address("::ffff:0:0").is_ok()); assert!(validate_ip_address("::1:2:3:4:5:6:7").is_ok());
assert!(validate_ip_address("1:2:3:4:5:6:7::").is_ok());
assert!(validate_ip_address("1:2::3:4:5:6").is_ok());
}
#[test]
fn test_ipv6_mixed_format() {
assert!(validate_ip_address("::ffff:192.168.1.1").is_ok());
assert!(validate_ip_address("::ffff:10.0.0.1").is_ok());
assert!(validate_ip_address("::ffff:127.0.0.1").is_ok());
assert!(validate_ip_address("::192.168.1.1").is_ok());
}
#[test]
fn test_ipv6_with_port() {
assert!(validate_ip_address("[::1]:8080").is_ok());
assert!(validate_ip_address("[2001:db8::1]:443").is_ok());
assert!(validate_ip_address("[fe80::1]:80").is_ok());
assert!(validate_ip_address("[::ffff:192.168.1.1]:8080").is_ok());
}
#[test]
fn test_ipv6_special_addresses() {
assert!(validate_ip_address("::1").is_ok());
assert!(validate_ip_address("::").is_ok());
assert!(validate_ip_address("fe80::1").is_ok());
assert!(validate_ip_address("fc00::1").is_ok());
assert!(validate_ip_address("fd00::1").is_ok());
assert!(validate_ip_address("ff00::1").is_ok());
assert!(validate_ip_address("2001:db8::1").is_ok());
}
#[test]
fn test_ipv6_invalid() {
assert!(validate_ip_address(":::1").is_err()); assert!(validate_ip_address("1:2:3:4:5:6:7:8:9").is_err()); assert!(validate_ip_address("1:2:3:4:5:6").is_err()); assert!(validate_ip_address("gggg::1").is_err()); assert!(validate_ip_address("1::2::3").is_err()); assert!(validate_ip_address("[::1").is_err()); assert!(validate_ip_address("::1]:8080").is_err()); }
#[test]
fn test_validate_user_id() {
assert!(validate_user_id("user123").is_ok());
assert!(validate_user_id("user-name_123").is_ok());
assert!(validate_user_id("user@example.com").is_ok());
assert!(validate_user_id("a").is_ok());
}
#[test]
fn test_validate_user_id_invalid() {
assert!(validate_user_id("").is_err());
assert!(validate_user_id("user name").is_err()); assert!(validate_user_id("user@#$%").is_err()); }
#[test]
fn test_validate_mac_address() {
assert!(validate_mac_address("00:1A:2B:3C:4D:5E").is_ok());
assert!(validate_mac_address("001A2B3C4D5E").is_ok()); assert!(validate_mac_address("aa:bb:cc:dd:ee:ff").is_ok());
}
#[test]
fn test_validate_mac_address_invalid() {
assert!(validate_mac_address("").is_err());
assert!(validate_mac_address("00:1A:2B:3C:4D").is_err()); assert!(validate_mac_address("00:1A:2B:3C:4D:5E:6F").is_err()); assert!(validate_mac_address("00:1A:2B:3C:4D:GG").is_err()); }
#[test]
fn test_validate_length() {
assert!(validate_length("test", 10, "test").is_ok());
assert!(validate_length("", 10, "test").is_ok());
assert!(validate_length("test", 4, "test").is_ok());
}
#[test]
fn test_validate_length_exceeds() {
assert!(validate_length("test_value", 5, "test").is_err());
}
#[test]
fn test_validate_ban_reason() {
assert!(validate_ban_reason("Spam behavior").is_ok());
assert!(validate_ban_reason(&"x".repeat(500)).is_ok()); assert!(validate_ban_reason(&"x".repeat(501)).is_err()); }
#[test]
fn test_validate_api_key() {
assert!(validate_api_key("sk-abc123xyz").is_ok());
assert!(validate_api_key(&"a".repeat(512)).is_ok()); assert!(validate_api_key(&"a".repeat(513)).is_err()); }
#[test]
#[cfg(feature = "ban-manager")]
fn test_validate_ban_target_ip() {
use crate::storage::BanTarget;
let ip_target = BanTarget::Ip("192.168.1.1".to_string());
assert!(validate_ban_target(&ip_target).is_ok());
let ipv6_target = BanTarget::Ip("::1".to_string());
assert!(validate_ban_target(&ipv6_target).is_ok());
let invalid_ip = BanTarget::Ip("invalid".to_string());
assert!(validate_ban_target(&invalid_ip).is_err());
}
#[test]
#[cfg(feature = "ban-manager")]
fn test_validate_ban_target_user_id() {
use crate::storage::BanTarget;
let user_target = BanTarget::UserId("user123".to_string());
assert!(validate_ban_target(&user_target).is_ok());
let invalid_user = BanTarget::UserId("".to_string());
assert!(validate_ban_target(&invalid_user).is_err());
}
#[test]
#[cfg(feature = "ban-manager")]
fn test_validate_ban_target_mac() {
use crate::storage::BanTarget;
let mac_target = BanTarget::Mac("00:1A:2B:3C:4D:5E".to_string());
assert!(validate_ban_target(&mac_target).is_ok());
let invalid_mac = BanTarget::Mac("invalid".to_string());
assert!(validate_ban_target(&invalid_mac).is_err());
}
#[test]
#[cfg(feature = "ban-manager")]
fn test_validate_ban_target_geo() {
use crate::storage::BanTarget;
assert!(
validate_ban_target(&BanTarget::Geo {
country_code: "CN".to_string()
})
.is_ok()
);
assert!(
validate_ban_target(&BanTarget::Geo {
country_code: "US".to_string()
})
.is_ok()
);
assert!(
validate_ban_target(&BanTarget::Geo {
country_code: "JP".to_string()
})
.is_ok()
);
assert!(
validate_ban_target(&BanTarget::Geo {
country_code: "cn".to_string()
})
.is_err()
);
assert!(
validate_ban_target(&BanTarget::Geo {
country_code: "CHN".to_string()
})
.is_err()
);
assert!(
validate_ban_target(&BanTarget::Geo {
country_code: "".to_string()
})
.is_err()
);
assert!(
validate_ban_target(&BanTarget::Geo {
country_code: "12".to_string()
})
.is_err()
);
assert!(
validate_ban_target(&BanTarget::Geo {
country_code: "C".to_string()
})
.is_err()
);
}
#[test]
#[cfg(feature = "ban-manager")]
fn test_validate_geo_country_code_direct() {
assert!(validate_geo_country_code("CN").is_ok());
assert!(validate_geo_country_code("US").is_ok());
assert!(validate_geo_country_code("JP").is_ok());
assert!(validate_geo_country_code("DE").is_ok());
assert!(validate_geo_country_code("").is_err());
assert!(validate_geo_country_code("C").is_err());
assert!(validate_geo_country_code("CHN").is_err());
assert!(validate_geo_country_code("cn").is_err());
assert!(validate_geo_country_code("Cn").is_err());
assert!(validate_geo_country_code("12").is_err());
assert!(validate_geo_country_code("C!").is_err());
assert!(validate_geo_country_code("C ").is_err());
}
#[test]
fn test_validate_ip_address_length_exceeded() {
let long_ip = format!(
"{}:{}:{}:{}:{}:{}:{}:{}",
"11111", "22222", "33333", "44444", "55555", "66666", "77777", "88888"
);
assert!(long_ip.len() > 45);
assert!(validate_ip_address(&long_ip).is_err());
}
#[test]
fn test_validate_ip_address_max_length_boundary() {
assert!(validate_ip_address("1234:5678:9abc:def0:1234:5678:9abc:def0").is_ok());
}
#[test]
fn test_validate_user_id_length_exceeded() {
let long_id = "x".repeat(257);
assert!(validate_user_id(&long_id).is_err());
}
#[test]
fn test_validate_user_id_max_length_boundary() {
let max_id = "x".repeat(256);
assert!(validate_user_id(&max_id).is_ok());
}
#[test]
fn test_validate_user_id_dot_allowed() {
assert!(validate_user_id("user.name").is_ok());
assert!(validate_user_id("first.last@domain.com").is_ok());
}
#[test]
fn test_validate_user_id_underscore_at_boundary() {
assert!(validate_user_id("_").is_ok());
assert!(validate_user_id("-").is_ok());
assert!(validate_user_id("@").is_ok());
assert!(validate_user_id(".").is_ok());
}
#[test]
fn test_validate_mac_address_hyphens() {
assert!(validate_mac_address("AA-BB-CC-DD-EE-FF").is_ok());
assert!(validate_mac_address("aa-bb-cc-dd-ee-ff").is_ok());
}
#[test]
fn test_validate_mac_address_periods() {
assert!(validate_mac_address("AABB.CCDD.EEFF").is_ok());
assert!(validate_mac_address("0011.2233.4455").is_ok());
}
#[test]
fn test_validate_mac_address_mixed_case() {
assert!(validate_mac_address("AA:bb:CC:dd:EE:ff").is_ok());
}
#[test]
fn test_validate_header_value_ok() {
assert!(validate_header_value("text/plain").is_ok());
assert!(validate_header_value("").is_ok());
assert!(validate_header_value("application/json; charset=utf-8").is_ok());
}
#[test]
fn test_validate_header_value_exceeds() {
let long_value = "x".repeat(8193);
assert!(validate_header_value(&long_value).is_err());
}
#[test]
fn test_validate_header_value_max_boundary() {
let max_value = "x".repeat(8192);
assert!(validate_header_value(&max_value).is_ok());
}
#[test]
fn test_validate_path_ok() {
assert!(validate_path("/api/v1/users").is_ok());
assert!(validate_path("").is_ok());
assert!(validate_path("/").is_ok());
}
#[test]
fn test_validate_path_exceeds() {
let long_path = "/".to_string() + &"x".repeat(2048);
assert!(long_path.len() > 2048);
assert!(validate_path(&long_path).is_err());
}
#[test]
fn test_validate_path_max_boundary() {
let max_path = "/".to_string() + &"x".repeat(2047);
assert_eq!(max_path.len(), 2048);
assert!(validate_path(&max_path).is_ok());
}
#[test]
fn test_validate_ban_reason_empty() {
assert!(validate_ban_reason("").is_ok());
}
#[test]
fn test_validate_ban_reason_exact_max() {
assert!(validate_ban_reason(&"x".repeat(500)).is_ok());
}
#[test]
fn test_validate_api_key_empty() {
assert!(validate_api_key("").is_ok());
}
#[test]
fn test_validate_api_key_exact_max() {
assert!(validate_api_key(&"k".repeat(512)).is_ok());
}
#[test]
fn test_validate_length_zero_value() {
assert!(validate_length("", 0, "field").is_ok());
}
#[test]
fn test_validate_length_zero_max() {
assert!(validate_length("x", 0, "field").is_err());
}
#[test]
fn test_validate_length_exact_boundary() {
assert!(validate_length("abc", 3, "field").is_ok());
assert!(validate_length("abcd", 3, "field").is_err());
}
#[test]
fn test_validate_ip_address_single_colon_no_dot() {
assert!(validate_ip_address("hostname:8080").is_err());
}
#[test]
fn test_validate_ip_address_trailing_dot_invalid() {
assert!(validate_ip_address("192.168.1.").is_err());
}
}