Files
cloudflare-domain-manager/backend/src/domain/validators.rs
T
Denozordec b1467575c5
Build, Test, and Push CFDM Docker Image / test (push) Failing after 3h13m2s
Build, Test, and Push CFDM Docker Image / create-release (push) Has been cancelled
Build, Test, and Push CFDM Docker Image / build-and-push (push) Has been cancelled
Build, Test, and Push CFDM Docker Image / update-wiki (push) Has been cancelled
Refactor project structure to use pnpm monorepo; update Dockerfile and related configurations for frontend build process. Adjust .dockerignore and .gitignore to reflect new paths. Modify .env.example for cron job timing. Update CONTRIBUTING.md and README.md for new development instructions.
2026-06-15 15:37:36 +07:00

78 lines
2.8 KiB
Rust

use crate::error::{AppError, AppResult};
use regex::Regex;
use std::sync::LazyLock;
static NAME_RE: LazyLock<Regex> =
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<Regex> =
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<Regex> = 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);
}
}