#[cfg(feature = "srv")]
use std::str::FromStr;
use std::{
borrow::Cow,
collections::HashMap,
fs::File,
io::{self, Read, Write},
net::{SocketAddr, TcpStream, ToSocketAddrs},
path::Path,
sync::LazyLock,
time::{Duration, Instant},
};
#[cfg(feature = "srv")]
use hickory_client::{
client::{Client, SyncClient},
op::DnsResponse,
rr::{DNSClass, Name, RData, Record, RecordType},
udp::UdpClientConnection,
};
use regex::bytes::Regex;
use serde_json::{Map, Value};
#[cfg(feature = "tokio")]
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use validators::models::Host;
use crate::{WhoIsError, WhoIsHost, WhoIsLookupOptions, WhoIsServerValue};
const DEFAULT_WHOIS_HOST_PORT: u16 = 43;
const DEFAULT_WHOIS_HOST_QUERY: &str = "$addr\r\n";
const READ_BUFFER_SIZE: usize = 8 * 1024;
static RE_SERVER: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?m-u)^[^\S\n]*(?:Registrar[^\S\n]+)?(?:ReferralServer|Registrar Whois|Whois Server|WHOIS Server|Registrar WHOIS Server):[^\S\n]*(?:r?whois://)?(\S*)").unwrap()
});
fn extract_referral_host(query_result: &[u8]) -> Option<&str> {
let host = RE_SERVER.captures(query_result)?.get(1)?.as_bytes();
let host = std::str::from_utf8(host).ok()?;
if host.is_empty() { None } else { Some(host) }
}
#[cfg(not(feature = "charset"))]
fn decode_response(bytes: Vec<u8>, _tld: Option<&str>) -> String {
match String::from_utf8(bytes) {
Ok(s) => s,
Err(error) => String::from_utf8_lossy(error.as_bytes()).into_owned(),
}
}
#[cfg(feature = "charset")]
fn decode_response(bytes: Vec<u8>, tld: Option<&str>) -> String {
use chardetng::{EncodingDetector, Iso2022JpDetection, Utf8Detection};
let bytes = match String::from_utf8(bytes) {
Ok(s) => return s,
Err(error) => error.into_bytes(),
};
let mut detector = EncodingDetector::new(Iso2022JpDetection::Allow);
detector.feed(&bytes, true);
detector.guess(tld.map(str::as_bytes), Utf8Detection::Allow).decode(&bytes).0.into_owned()
}
fn tld_hint(host: &Host) -> Option<Cow<'_, str>> {
let Host::Domain(domain) = host else {
return None;
};
let tld = match domain.rfind('.') {
Some(index) => &domain[index + 1..],
None => domain.as_str(),
};
if tld.is_empty() || !tld.is_ascii() {
return None;
}
if tld.bytes().any(|b| b.is_ascii_uppercase()) {
Some(Cow::Owned(tld.to_ascii_lowercase()))
} else {
Some(Cow::Borrowed(tld))
}
}
fn query_text<'a>(server: &WhoIsServerValue, host: &'a Host) -> Cow<'a, str> {
match host {
Host::Domain(domain) => server.encode_domain(domain),
Host::IPv4(ip) => Cow::Owned(ip.to_string()),
Host::IPv6(ip) => Cow::Owned(ip.to_string()),
}
}
fn build_request(server: &WhoIsServerValue, text: &str) -> String {
let query = server.query.as_deref().unwrap_or(DEFAULT_WHOIS_HOST_QUERY);
let mut request = query.replace("$addr", text);
if !request.ends_with("\r\n") {
if request.ends_with('\n') {
request.pop();
}
request.push_str("\r\n");
}
request
}
fn same_host(a: &Host, b: &Host) -> bool {
match (a, b) {
(Host::Domain(a), Host::Domain(b)) => a.eq_ignore_ascii_case(b),
_ => a == b,
}
}
#[derive(Debug)]
struct Deadline {
start: Instant,
timeout: Duration,
}
impl Deadline {
#[inline]
fn new(timeout: Duration) -> Self {
Deadline {
start: Instant::now(),
timeout,
}
}
fn remaining(&self) -> Result<Duration, io::Error> {
match self.timeout.checked_sub(self.start.elapsed()) {
Some(remaining) if !remaining.is_zero() => Ok(remaining),
_ => Err(io::Error::new(io::ErrorKind::TimedOut, "the WHOIS lookup timed out")),
}
}
}
#[inline]
fn no_address_error() -> io::Error {
io::Error::new(
io::ErrorKind::AddrNotAvailable,
"the host is not resolved to any socket address",
)
}
fn connect(socket_addrs: &[SocketAddr], deadline: &Deadline) -> io::Result<TcpStream> {
let mut last_error = None;
for socket_addr in socket_addrs {
match TcpStream::connect_timeout(socket_addr, deadline.remaining()?) {
Ok(client) => return Ok(client),
Err(error) => last_error = Some(error),
}
}
Err(last_error.unwrap_or_else(no_address_error))
}
fn read_response(
client: &mut TcpStream,
max_response_size: Option<usize>,
deadline: Option<&Deadline>,
) -> io::Result<Vec<u8>> {
let mut query_result = Vec::new();
let mut buffer = [0u8; READ_BUFFER_SIZE];
loop {
if let Some(deadline) = deadline {
client.set_read_timeout(Some(deadline.remaining()?))?;
}
let read = client.read(&mut buffer)?;
if read == 0 {
break;
}
if let Some(max) = max_response_size
&& query_result.len() + read > max
{
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"the WHOIS response is too big",
));
}
query_result.extend_from_slice(&buffer[..read]);
}
Ok(query_result)
}
#[cfg(feature = "tokio")]
async fn connect_async(
socket_addrs: &[SocketAddr],
deadline: &Deadline,
) -> Result<tokio::net::TcpStream, WhoIsError> {
let mut last_error = None;
for socket_addr in socket_addrs {
match tokio::time::timeout(
deadline.remaining()?,
tokio::net::TcpStream::connect(socket_addr),
)
.await
{
Ok(Ok(client)) => return Ok(client),
Ok(Err(error)) => last_error = Some(WhoIsError::IOError(error)),
Err(error) => last_error = Some(WhoIsError::Elapsed(error)),
}
}
Err(last_error.unwrap_or_else(|| WhoIsError::IOError(no_address_error())))
}
#[cfg(feature = "tokio")]
async fn read_response_async(
client: &mut tokio::net::TcpStream,
max_response_size: Option<usize>,
) -> io::Result<Vec<u8>> {
let mut query_result = Vec::new();
match max_response_size {
Some(max) => {
client.take(max.saturating_add(1) as u64).read_to_end(&mut query_result).await?;
if query_result.len() > max {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"the WHOIS response is too big",
));
}
},
None => {
client.read_to_end(&mut query_result).await?;
},
}
Ok(query_result)
}
#[derive(Debug, Clone)]
pub struct WhoIs {
map: HashMap<String, WhoIsServerValue>,
ip: WhoIsServerValue,
}
impl WhoIs {
pub fn from_host<T: AsRef<str>>(host: T) -> Result<WhoIs, WhoIsError> {
Ok(Self {
map: HashMap::new(), ip: WhoIsServerValue::from_string(host)?
})
}
#[inline]
pub fn from_path<P: AsRef<Path>>(path: P) -> Result<WhoIs, WhoIsError> {
let path = path.as_ref();
let file = File::open(path)?;
let map: Map<String, Value> = serde_json::from_reader(file)?;
Self::from_inner(map)
}
#[cfg(feature = "tokio")]
#[inline]
pub async fn from_path_async<P: AsRef<Path>>(path: P) -> Result<WhoIs, WhoIsError> {
let file = tokio::fs::read(path).await?;
let map: Map<String, Value> = serde_json::from_slice(file.as_slice())?;
Self::from_inner(map)
}
#[inline]
pub fn from_string<S: AsRef<str>>(string: S) -> Result<WhoIs, WhoIsError> {
let string = string.as_ref();
let map: Map<String, Value> = serde_json::from_str(string)?;
Self::from_inner(map)
}
fn from_inner(mut map: Map<String, Value>) -> Result<WhoIs, WhoIsError> {
let ip = match map.remove("_") {
Some(server) => {
if let Value::Object(server) = server {
match server.get("ip") {
Some(server) => {
if server.is_null() {
return Err(WhoIsError::MapError(
"`ip` in the `_` object in the server list is null.",
));
}
WhoIsServerValue::from_value(server)?
},
None => {
return Err(WhoIsError::MapError(
"Cannot find `ip` in the `_` object in the server list.",
));
},
}
} else {
return Err(WhoIsError::MapError("`_` in the server list is not an object."));
}
},
None => return Err(WhoIsError::MapError("Cannot find `_` in the server list.")),
};
let mut new_map: HashMap<String, WhoIsServerValue> = HashMap::with_capacity(map.len());
for (k, v) in map {
if !v.is_null() {
let server_value = WhoIsServerValue::from_value(&v)?;
new_map.insert(k, server_value);
}
}
Ok(WhoIs {
map: new_map,
ip,
})
}
}
#[cfg(feature = "srv")]
impl WhoIs {
pub fn can_find_server_for_tld<T: AsRef<str>, D: AsRef<str>>(
&mut self,
tld: T,
dns_server: D,
) -> Result<bool, WhoIsError> {
let mut tld = tld.as_ref();
let dns_server = dns_server.as_ref();
if self.map.contains_key(tld) {
return Ok(true);
}
let address = match dns_server.parse() {
Ok(address) => address,
Err(_error) => {
return Err(WhoIsError::MapError("The DNS server address is incorrect."));
},
};
let conn = UdpClientConnection::new(address).map_err(io::Error::other)?;
let client = SyncClient::new(conn);
loop {
match tld.find('.') {
Some(index) => {
tld = &tld[index + 1..];
},
None => {
return Ok(false);
},
}
if tld.is_empty() {
return Ok(false);
}
if self.map.contains_key(tld) {
return Ok(true);
}
let name =
Name::from_str(&format!("_nicname._tcp.{tld}.")).map_err(io::Error::other)?;
let response: DnsResponse =
client.query(&name, DNSClass::IN, RecordType::SRV).map_err(io::Error::other)?;
let answers: &[Record] = response.answers();
for record in answers {
if let Some(RData::SRV(record)) = record.data() {
let target = record.target().to_string();
let new_server =
match WhoIsServerValue::from_string(target.trim_end_matches('.')) {
Ok(new_server) => new_server,
Err(_error) => continue,
};
self.map.insert(tld.to_string(), new_server);
return Ok(true);
}
}
}
}
}
impl WhoIs {
pub fn get_server_by_tld(&self, mut tld: &str) -> Option<&WhoIsServerValue> {
let mut server;
loop {
server = self.map.get(tld);
if server.is_some() {
break;
}
if tld.is_empty() {
break;
}
match tld.find('.') {
Some(index) => {
tld = &tld[index + 1..];
},
None => {
tld = "";
},
}
}
server
}
fn get_server_by_host(&self, host: &WhoIsHost) -> Option<&WhoIsServerValue> {
self.map.values().find(|server| &server.host == host)
}
fn get_server<'a>(
&'a self,
options: &'a WhoIsLookupOptions,
) -> Result<&'a WhoIsServerValue, WhoIsError> {
if let Some(server) = &options.server {
return Ok(server);
}
match &options.target.0 {
Host::IPv4(_) | Host::IPv6(_) => Ok(&self.ip),
Host::Domain(domain) => self
.get_server_by_tld(domain.as_str())
.ok_or(WhoIsError::MapError("No whois server is known for this kind of object.")),
}
}
fn lookup_once(
server: &WhoIsServerValue,
text: &str,
options: &WhoIsLookupOptions,
) -> Result<Vec<u8>, WhoIsError> {
let addr = server.host.to_addr_string(DEFAULT_WHOIS_HOST_PORT);
let request = build_request(server, text);
match options.timeout {
Some(timeout) => {
let deadline = Deadline::new(timeout);
let socket_addrs: Vec<SocketAddr> = addr.to_socket_addrs()?.collect();
let mut client = connect(&socket_addrs, &deadline)?;
client.set_write_timeout(Some(deadline.remaining()?))?;
client.write_all(request.as_bytes())?;
client.flush()?;
Ok(read_response(&mut client, options.max_response_size, Some(&deadline))?)
},
None => {
let mut client = TcpStream::connect(&addr)?;
client.write_all(request.as_bytes())?;
client.flush()?;
Ok(read_response(&mut client, options.max_response_size, None)?)
},
}
}
fn lookup_inner(
&self,
server: &WhoIsServerValue,
options: &WhoIsLookupOptions,
) -> Result<Vec<u8>, WhoIsError> {
let host = &options.target.0;
let mut follow = options.follow;
let mut server = Cow::Borrowed(server);
let text = query_text(&server, host);
let mut query_result = Self::lookup_once(&server, text.as_ref(), options)?;
while follow > 0 {
let Some(referral) = extract_referral_host(&query_result)
.and_then(|h| WhoIsServerValue::from_string(h).ok())
else {
break;
};
if same_host(referral.host.host(), server.host.host()) {
break;
}
server = match self.get_server_by_host(&referral.host) {
Some(server) => Cow::Borrowed(server),
None => Cow::Owned(referral),
};
let text = query_text(&server, host);
query_result = Self::lookup_once(&server, text.as_ref(), options)?;
follow -= 1;
}
Ok(query_result)
}
#[inline]
pub fn lookup_raw(&self, options: WhoIsLookupOptions) -> Result<Vec<u8>, WhoIsError> {
let server = self.get_server(&options)?;
self.lookup_inner(server, &options)
}
pub fn lookup(&self, options: WhoIsLookupOptions) -> Result<String, WhoIsError> {
let server = self.get_server(&options)?;
let query_result = self.lookup_inner(server, &options)?;
Ok(decode_response(query_result, tld_hint(&options.target.0).as_deref()))
}
}
#[cfg(feature = "tokio")]
impl WhoIs {
async fn lookup_once_async(
server: &WhoIsServerValue,
text: &str,
options: &WhoIsLookupOptions,
) -> Result<Vec<u8>, WhoIsError> {
let addr = server.host.to_addr_string(DEFAULT_WHOIS_HOST_PORT);
let request = build_request(server, text);
match options.timeout {
Some(timeout) => {
let deadline = Deadline::new(timeout);
let socket_addrs: Vec<SocketAddr> =
tokio::time::timeout(deadline.remaining()?, tokio::net::lookup_host(&addr))
.await??
.collect();
let mut client = connect_async(&socket_addrs, &deadline).await?;
tokio::time::timeout(deadline.remaining()?, client.write_all(request.as_bytes()))
.await??;
tokio::time::timeout(deadline.remaining()?, client.flush()).await??;
Ok(tokio::time::timeout(
deadline.remaining()?,
read_response_async(&mut client, options.max_response_size),
)
.await??)
},
None => {
let mut client = tokio::net::TcpStream::connect(&addr).await?;
client.write_all(request.as_bytes()).await?;
client.flush().await?;
Ok(read_response_async(&mut client, options.max_response_size).await?)
},
}
}
async fn lookup_inner_async(
&self,
server: &WhoIsServerValue,
options: &WhoIsLookupOptions,
) -> Result<Vec<u8>, WhoIsError> {
let host = &options.target.0;
let mut follow = options.follow;
let mut server = Cow::Borrowed(server);
let text = query_text(&server, host);
let mut query_result = Self::lookup_once_async(&server, text.as_ref(), options).await?;
while follow > 0 {
let Some(referral) = extract_referral_host(&query_result)
.and_then(|h| WhoIsServerValue::from_string(h).ok())
else {
break;
};
if same_host(referral.host.host(), server.host.host()) {
break;
}
server = match self.get_server_by_host(&referral.host) {
Some(server) => Cow::Borrowed(server),
None => Cow::Owned(referral),
};
let text = query_text(&server, host);
query_result = Self::lookup_once_async(&server, text.as_ref(), options).await?;
follow -= 1;
}
Ok(query_result)
}
#[inline]
pub async fn lookup_raw_async(
&self,
options: WhoIsLookupOptions,
) -> Result<Vec<u8>, WhoIsError> {
let server = self.get_server(&options)?;
self.lookup_inner_async(server, &options).await
}
pub async fn lookup_async(&self, options: WhoIsLookupOptions) -> Result<String, WhoIsError> {
let server = self.get_server(&options)?;
let query_result = self.lookup_inner_async(server, &options).await?;
Ok(decode_response(query_result, tld_hint(&options.target.0).as_deref()))
}
}
#[cfg(test)]
mod tests {
use validators::models::Host;
use super::{build_request, decode_response, extract_referral_host, tld_hint};
use crate::WhoIsServerValue;
#[test]
fn extract_referral_host_trims_trailing_cr() {
let body = b"Domain: example.com\r\nReferralServer: whois://whois.arin.net\r\n";
assert_eq!(Some("whois.arin.net"), extract_referral_host(body));
}
#[test]
fn extract_referral_host_ignores_non_utf8_body() {
let body = b"\xB5n\xBFy: example.tw\r\nWhois Server: whois.twnic.net.tw\r\n";
assert_eq!(Some("whois.twnic.net.tw"), extract_referral_host(body));
}
#[test]
fn extract_referral_host_takes_the_first_token() {
let body = b" Registrar WHOIS Server: whois.example.com (see the note below)\r\n";
assert_eq!(Some("whois.example.com"), extract_referral_host(body));
}
#[test]
fn extract_referral_host_ignores_a_field_name_in_the_middle_of_a_line() {
let body = b"Do not trust any Whois Server: which is not listed above.\r\n";
assert_eq!(None, extract_referral_host(body));
}
#[test]
fn tld_hint_takes_the_last_label() {
assert_eq!(Some("tw"), tld_hint(&Host::Domain(String::from("magiclen.com.tw"))).as_deref());
assert_eq!(Some("com"), tld_hint(&Host::Domain(String::from("magiclen.COM"))).as_deref());
assert_eq!(None, tld_hint(&Host::IPv4("172.105.210.153".parse().unwrap())).as_deref());
}
#[test]
fn build_request_ends_with_crlf() {
let mut server = WhoIsServerValue::from_string("whois.example.com").unwrap();
assert_eq!("magiclen.org\r\n", build_request(&server, "magiclen.org"));
server.query = Some(String::from("domain $addr\r\n"));
assert_eq!("domain magiclen.org\r\n", build_request(&server, "magiclen.org"));
server.query = Some(String::from("domain $addr\n"));
assert_eq!("domain magiclen.org\r\n", build_request(&server, "magiclen.org"));
server.query = Some(String::from("domain $addr"));
assert_eq!("domain magiclen.org\r\n", build_request(&server, "magiclen.org"));
}
#[test]
fn decode_response_passes_through_utf8() {
assert_eq!("café", decode_response("café".as_bytes().to_vec(), None));
}
#[cfg(feature = "charset")]
#[test]
fn decode_response_detects_windows_1252() {
assert_eq!("café", decode_response(vec![b'c', b'a', b'f', 0xE9], None));
}
#[cfg(not(feature = "charset"))]
#[test]
fn decode_response_is_lossy_without_charset() {
assert_eq!("caf\u{FFFD}", decode_response(vec![b'c', b'a', b'f', 0xE9], None));
}
}