use crate::error::{AppError, AppResult}; use regex::Regex; use std::sync::LazyLock; static NAME_RE: LazyLock = LazyLock::new(|| Regex::new(r"^(@|\*|[a-zA-Z0-9_]([a-zA-Z0-9_-]*[a-zA-Z0-9_])?(\.[a-zA-Z0-9_]([a-zA-Z0-9_-]*[a-zA-Z0-9_])?)*)$").unwrap()); static IPV4_RE: LazyLock = LazyLock::new(|| Regex::new(r"^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$").unwrap()); static IPV6_RE: LazyLock = LazyLock::new(|| Regex::new(r"^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$").unwrap()); const ALLOWED_TYPES: &[&str] = &["A", "AAAA", "CNAME", "TXT", "MX", "NS", "SRV", "CAA"]; pub fn validate_dns_record( record_type: &str, name: &str, content: &str, ttl: i64, proxied: bool, ) -> AppResult<()> { let rt = record_type.to_uppercase(); if !ALLOWED_TYPES.contains(&rt.as_str()) { return Err(AppError::Validation(format!("unsupported record type: {record_type}"))); } if !NAME_RE.is_match(name) { return Err(AppError::Validation(format!("invalid record name: {name}"))); } if ttl != 1 && !(60..=86400).contains(&ttl) { return Err(AppError::Validation("ttl must be 1 (auto) or 60-86400".into())); } if proxied && !matches!(rt.as_str(), "A" | "AAAA" | "CNAME") { return Err(AppError::Validation("proxied only allowed for A, AAAA, CNAME".into())); } match rt.as_str() { "A" if !IPV4_RE.is_match(content) => { return Err(AppError::Validation("A record requires valid IPv4".into())); } "AAAA" if !IPV6_RE.is_match(content) => { return Err(AppError::Validation("AAAA record requires valid IPv6".into())); } "CNAME" | "NS" if content.is_empty() || content.contains(' ') => { return Err(AppError::Validation("CNAME/NS requires valid hostname".into())); } "TXT" if content.is_empty() || content.len() > 2048 => { return Err(AppError::Validation("TXT content length 1-2048".into())); } _ => {} } Ok(()) } pub fn cert_status_from_expiry(days_left: i64) -> &'static str { if days_left < 0 { crate::domain::CERT_EXPIRED } else if days_left <= 30 { crate::domain::CERT_WARNING } else { crate::domain::CERT_OK } } #[cfg(test)] mod tests { use super::*; #[test] fn validates_a_record() { assert!(validate_dns_record("A", "@", "192.168.1.1", 1, false).is_ok()); assert!(validate_dns_record("A", "@", "invalid", 1, false).is_err()); } #[test] fn cert_status_thresholds() { assert_eq!(cert_status_from_expiry(60), crate::domain::CERT_OK); assert_eq!(cert_status_from_expiry(10), crate::domain::CERT_WARNING); assert_eq!(cert_status_from_expiry(-1), crate::domain::CERT_EXPIRED); } }