use {
crate::SUFFIX_SERVER_LIST,
lazy_regex::{regex_captures, regex_is_match},
};
#[cfg(feature = "chrono")]
type DT = chrono::DateTime<chrono::Utc>;
#[cfg(all(feature = "time", not(feature = "chrono")))]
type DT = time::OffsetDateTime;
#[cfg(not(any(feature = "chrono", feature = "time")))]
type DT = String;
#[derive(Debug, Default)]
pub struct Domain {
pub name: String,
pub suffix: String,
pub created: Option<DT>,
pub expiry: Option<DT>,
pub name_servers: Vec<String>,
}
pub fn parse(raw: &str) -> Option<Domain> {
if not_found(raw) {
return None;
}
let raw = raw.replace("\r\n", "\n").replace(":\n", ":");
let name = parse_name(&raw)?;
let suffix = name.rsplit('.').next()?;
let puny_name = if suffix.chars().next()?.is_ascii_alphabetic() {
name.clone()
} else {
let puny = punycode::encode(suffix).ok()?;
name.replace(suffix, &format!("xn--{puny}"))
};
let suffix = SUFFIX_SERVER_LIST
.keys()
.find(|&suffix| puny_name.ends_with(suffix))?
.to_string();
if suffix == ".de" {
return if raw.contains(": connect") { Some(Domain { name, suffix, ..Default::default() }) } else { None };
}
let created = parse_created(&raw);
let expiry = parse_expiry(&raw);
let name_servers = parse_ns(&raw);
Some(Domain { name, suffix, created, expiry, name_servers })
}
fn not_found(raw: &str) -> bool {
regex_is_match!(r"([Nn]o \w+ [Ff]ound|returned 0 objects|NOT FOUND)", raw)
}
pub fn parse_name(raw: &str) -> Option<String> {
regex_captures!(
r"^.*(?:[Dd]omain:?\s*[Nn]ame\s*[:.\]]*|[Dd]omain\s*:?|DOMAIN\s+NAME:|[Dd]omain\.+:|Nom de domaine:|NOMBRE DE DOMINIO:|Informations about)\W*([\w.-]+\.(?:[Xx][Nn]--[\w-]+|\w+))"m,
raw
)
.map(|caps| caps.1)
.map(|s| s.to_ascii_lowercase())
}
pub fn parse_created(raw: &str) -> Option<DT> {
regex_captures!(r"^.*[(]?(?:(?:[Cc]reat(?:ion|ed)|[Rr]egist(?:ered|ration))\s*(?:[Dd]ate|[Oo]n|[Tt]ime)?|dateregistered|[Aa]ctivation|Fecha de activación|activated|création)\W*(\d[0-9TZ.:-]+|\w+\s+\w*\d+\w+)"m, raw)
.map(|caps| caps.1)
.and_then(_parse_datetime)
}
pub fn parse_expiry(raw: &str) -> Option<DT> {
regex_captures!(r"^.*(?:[Ee]xpir(?:y|ation|es?)\s*(?:[Dd]ate|[Tt]ime|[Oo]n|)|[Ee]xp [Dd]ate|[Rr]enewal [Dd]ate|datebilleduntil|paid-till|[Vv]alidity:|[Vv]alid [Uu]ntil|Fecha de corte)\W*([0-9A-Za-z:. -]+\d[0-9A-Za-z:. -]+)"m, raw)
.map(|caps| caps.1)
.and_then(_parse_datetime)
}
pub fn parse_ns(raw: &str) -> Vec<String> {
raw.lines()
.filter_map(|line| {
regex_captures!(r"(?:[Nn]servers?|[Nn]ame\s*[Ss]ervers?|DNS:)\W+([0-9a-zA-Z.-]+)", line)
.map(|caps| caps.1)
.map(|s| s.to_ascii_lowercase())
})
.collect()
}
#[cfg(feature = "chrono")]
fn _parse_datetime(raw: &str) -> Option<DT> {
if let Ok(rfc3339) = chrono::DateTime::parse_from_rfc3339(raw) {
return Some(rfc3339.to_utc());
}
if let Ok(dotted_dmy_padded) = chrono::DateTime::parse_from_str(raw, "%d.%m.%Y") {
return Some(dotted_dmy_padded.to_utc());
}
None
}
#[cfg(all(feature = "time", not(feature = "chrono")))]
fn _parse_datetime(raw: &str) -> Option<DT> {
use time::format_description::well_known::Rfc3339;
if let Ok(rfc3339) = time::OffsetDateTime::parse(raw, &Rfc3339) {
return Some(rfc3339.to_offset(time::UtcOffset::UTC));
}
None
}
#[cfg(not(any(feature = "chrono", feature = "time")))]
fn _parse_datetime(raw: &str) -> Option<DT> {
Some(raw.to_string())
}