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
78 lines
2.8 KiB
Rust
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);
|
|
}
|
|
}
|