mod bootstrap;
mod client;
mod types;
pub use client::RdapClient;
pub use types::{ContactInfo, RdapResponse};
use std::net::IpAddr;
use crate::error::{Result, SeerError};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RdapRoute {
Ip(IpAddr),
Asn(u32),
Domain(String),
}
pub fn classify(query: &str) -> Result<RdapRoute> {
let trimmed = query.trim();
if trimmed.is_empty() {
return Err(SeerError::InvalidInput("empty RDAP query".to_string()));
}
if let Ok(ip) = trimmed.parse::<IpAddr>() {
return Ok(RdapRoute::Ip(ip));
}
let upper = trimmed.to_uppercase();
if let Some(rest) = upper.strip_prefix("AS") {
if !rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit()) && !trimmed.contains('.') {
let asn: u32 = rest
.parse()
.map_err(|_| SeerError::InvalidInput(format!("invalid ASN: {query}")))?;
return Ok(RdapRoute::Asn(asn));
}
}
Ok(RdapRoute::Domain(trimmed.to_string()))
}
pub async fn auto_lookup(client: &RdapClient, query: &str) -> Result<RdapResponse> {
match classify(query)? {
RdapRoute::Ip(_ip) => client.lookup_ip(query.trim()).await,
RdapRoute::Asn(asn) => client.lookup_asn(asn).await,
RdapRoute::Domain(domain) => client.lookup_domain(&domain).await,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classifies_ipv4() {
assert!(matches!(classify("8.8.8.8").unwrap(), RdapRoute::Ip(_)));
assert!(matches!(classify("1.1.1.1").unwrap(), RdapRoute::Ip(_)));
}
#[test]
fn classifies_ipv6() {
assert!(matches!(
classify("2606:4700:4700::1111").unwrap(),
RdapRoute::Ip(_)
));
}
#[test]
fn classifies_asn_upper() {
assert_eq!(classify("AS1234").unwrap(), RdapRoute::Asn(1234));
}
#[test]
fn classifies_asn_lower() {
assert_eq!(classify("as1234").unwrap(), RdapRoute::Asn(1234));
}
#[test]
fn classifies_asn_mixed_case() {
assert_eq!(classify("As15169").unwrap(), RdapRoute::Asn(15169));
}
#[test]
fn as_prefix_domain_routes_to_domain() {
assert_eq!(
classify("as1234.io").unwrap(),
RdapRoute::Domain("as1234.io".to_string())
);
assert_eq!(
classify("AS1234.IO").unwrap(),
RdapRoute::Domain("AS1234.IO".to_string())
);
}
#[test]
fn asn_with_trailing_junk_routes_to_domain() {
assert_eq!(
classify("AS1234x").unwrap(),
RdapRoute::Domain("AS1234x".to_string())
);
}
#[test]
fn bare_as_routes_to_domain() {
assert_eq!(classify("AS").unwrap(), RdapRoute::Domain("AS".to_string()));
}
#[test]
fn normal_domain_routes_to_domain() {
assert_eq!(
classify("example.com").unwrap(),
RdapRoute::Domain("example.com".to_string())
);
}
#[test]
fn trims_whitespace() {
assert_eq!(classify(" AS64500 ").unwrap(), RdapRoute::Asn(64500));
assert_eq!(
classify(" example.com ").unwrap(),
RdapRoute::Domain("example.com".to_string())
);
}
#[test]
fn empty_query_errors() {
assert!(matches!(
classify("").unwrap_err(),
SeerError::InvalidInput(_)
));
assert!(matches!(
classify(" ").unwrap_err(),
SeerError::InvalidInput(_)
));
}
#[test]
fn asn_overflow_errors() {
assert!(matches!(
classify("AS99999999999999").unwrap_err(),
SeerError::InvalidInput(_)
));
}
}