mod denic;
mod educause;
mod eis;
mod eurid;
mod generic;
mod isoc_il;
mod jprs;
mod kisa;
mod nic_it;
mod nic_lv;
mod nominet;
mod sidn;
use once_cell::sync::Lazy;
use super::parser::WhoisResponse;
pub use denic::DenicParser;
pub use educause::EducauseParser;
pub use eis::EisParser;
pub use eurid::EuridParser;
pub use generic::GenericParser;
pub use isoc_il::IsocIlParser;
pub use jprs::JprsParser;
pub use kisa::KisaParser;
pub use nic_it::NicItParser;
pub use nic_lv::NicLvParser;
pub use nominet::NominetParser;
pub use sidn::SidnParser;
pub(crate) const MAX_NAMESERVERS: usize = 32;
pub(crate) const MAX_STATUSES: usize = 32;
pub(crate) fn push_bounded(vec: &mut Vec<String>, value: String, cap: usize) {
if !value.is_empty() && vec.len() < cap && !vec.contains(&value) {
vec.push(value);
}
}
pub trait RegistryParser: Send + Sync {
fn supported_tlds(&self) -> &[&str];
fn parse(&self, domain: &str, server: &str, raw: &str) -> WhoisResponse;
}
pub struct ParserRegistry {
parsers: Vec<Box<dyn RegistryParser>>,
fallback: GenericParser,
}
impl ParserRegistry {
pub fn new() -> Self {
Self {
parsers: vec![
Box::new(DenicParser::new()), Box::new(EducauseParser::new()), Box::new(EisParser::new()), Box::new(EuridParser::new()), Box::new(IsocIlParser::new()), Box::new(JprsParser::new()), Box::new(KisaParser::new()), Box::new(NicItParser::new()), Box::new(NicLvParser::new()), Box::new(NominetParser::new()), Box::new(SidnParser::new()), ],
fallback: GenericParser::new(),
}
}
pub fn parse(&self, domain: &str, server: &str, raw: &str) -> WhoisResponse {
let tld = extract_tld(domain);
let sld_tld = extract_second_level_tld(domain);
for parser in &self.parsers {
let supported = parser.supported_tlds();
if let Some(sld) = &sld_tld {
if supported.contains(&sld.as_str()) {
return parser.parse(domain, server, raw);
}
}
if let Some(tld) = &tld {
if supported.contains(&tld.as_str()) {
return parser.parse(domain, server, raw);
}
}
}
self.fallback.parse(domain, server, raw)
}
}
impl Default for ParserRegistry {
fn default() -> Self {
Self::new()
}
}
pub static PARSER_REGISTRY: Lazy<ParserRegistry> = Lazy::new(ParserRegistry::new);
fn extract_tld(domain: &str) -> Option<String> {
domain.rsplit('.').next().map(|s| s.to_lowercase())
}
fn extract_second_level_tld(domain: &str) -> Option<String> {
let parts: Vec<&str> = domain.rsplit('.').collect();
if parts.len() >= 3 {
Some(format!(
"{}.{}",
parts[1].to_lowercase(),
parts[0].to_lowercase()
))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_tld() {
assert_eq!(extract_tld("example.com"), Some("com".to_string()));
assert_eq!(extract_tld("example.co.uk"), Some("uk".to_string()));
assert_eq!(extract_tld("example.de"), Some("de".to_string()));
}
#[test]
fn test_extract_second_level_tld() {
assert_eq!(
extract_second_level_tld("example.co.uk"),
Some("co.uk".to_string())
);
assert_eq!(extract_second_level_tld("example.com"), None);
}
#[test]
fn test_parser_registry_selects_denic_for_de() {
let registry = ParserRegistry::new();
let result = registry.parse(
"example.de",
"whois.denic.de",
"Domain: example.de\nStatus: connect",
);
assert_eq!(result.status, vec!["active"]);
}
#[test]
fn test_parser_registry_selects_nominet_for_uk() {
let registry = ParserRegistry::new();
let result = registry.parse(
"example.co.uk",
"whois.nic.uk",
"Domain name:\n example.co.uk\n\nRegistrar:\n Example Registrar Ltd [Tag = EXAMPLE]\n",
);
assert_eq!(result.registrar.as_deref(), Some("Example Registrar Ltd"));
}
#[test]
fn test_parser_registry_uses_generic_for_unknown() {
let registry = ParserRegistry::new();
let result = registry.parse(
"example.com",
"whois.verisign-grs.com",
"Domain Name: example.com\nRegistrar: Example Registrar, Inc.\nStatus: connect\n",
);
assert_eq!(result.registrar.as_deref(), Some("Example Registrar, Inc."));
assert_eq!(result.status, vec!["connect"]);
}
#[test]
fn test_parser_registry_selects_kisa_for_korean_idn_tlds() {
let raw = "# ENGLISH\n\
\n\
Domain Name : example.xn--3e0b707e\n\
Authorized Agency : Whois Corp.(http://whois.co.kr)\n\
\n\
Primary Name Server\n\
\x20 Host Name : ns1.example.kr\n";
let registry = ParserRegistry::new();
for domain in ["example.xn--3e0b707e", "example.xn--cg4bki"] {
let result = registry.parse(domain, "whois.kr", raw);
assert_eq!(
result.registrar.as_deref(),
Some("Whois Corp."),
"{domain} must use the KISA parser"
);
assert_eq!(result.nameservers, vec!["ns1.example.kr"], "{domain}");
}
}
#[test]
fn test_parser_registry_selects_eurid_for_eu_idn_tlds() {
let raw = "Domain: example.xn--e1a4c\n\
\n\
Name servers:\n\
\x20 ns1.example.eu (192.0.2.1)\n\
\x20 ns2.example.eu\n\
\n\
Keys:\n\
\x20 flags:KSK protocol:3 algorithm:RSA_SHA256 pubKey:AwEAAtest\n";
let registry = ParserRegistry::new();
for domain in ["example.xn--e1a4c", "example.xn--qxa6a"] {
let result = registry.parse(domain, "whois.eu", raw);
assert_eq!(
result.nameservers,
vec!["ns1.example.eu", "ns2.example.eu"],
"{domain} must use the EURid parser"
);
assert_eq!(
result.dnssec.as_deref(),
Some("signedDelegation"),
"{domain}"
);
}
}
}