1#[cfg(feature = "srv")]
2use std::str::FromStr;
3use std::{
4 borrow::Cow,
5 collections::HashMap,
6 fs::File,
7 io::{self, Read, Write},
8 net::{SocketAddr, TcpStream, ToSocketAddrs},
9 path::Path,
10 sync::LazyLock,
11 time::{Duration, Instant},
12};
13
14#[cfg(feature = "srv")]
15use hickory_client::{
16 client::{Client, SyncClient},
17 op::DnsResponse,
18 rr::{DNSClass, Name, RData, Record, RecordType},
19 udp::UdpClientConnection,
20};
21use regex::bytes::Regex;
22use serde_json::{Map, Value};
23#[cfg(feature = "tokio")]
24use tokio::io::{AsyncReadExt, AsyncWriteExt};
25use validators::models::Host;
26
27use crate::{WhoIsError, WhoIsHost, WhoIsLookupOptions, WhoIsServerValue};
28
29const DEFAULT_WHOIS_HOST_PORT: u16 = 43;
30const DEFAULT_WHOIS_HOST_QUERY: &str = "$addr\r\n";
31const READ_BUFFER_SIZE: usize = 8 * 1024;
32
33static RE_SERVER: LazyLock<Regex> = LazyLock::new(|| {
35 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()
36});
37
38fn extract_referral_host(query_result: &[u8]) -> Option<&str> {
40 let host = RE_SERVER.captures(query_result)?.get(1)?.as_bytes();
41 let host = std::str::from_utf8(host).ok()?;
42
43 if host.is_empty() { None } else { Some(host) }
44}
45
46#[cfg(not(feature = "charset"))]
48fn decode_response(bytes: Vec<u8>, _tld: Option<&str>) -> String {
49 match String::from_utf8(bytes) {
50 Ok(s) => s,
51 Err(error) => String::from_utf8_lossy(error.as_bytes()).into_owned(),
52 }
53}
54
55#[cfg(feature = "charset")]
57fn decode_response(bytes: Vec<u8>, tld: Option<&str>) -> String {
58 use chardetng::{EncodingDetector, Iso2022JpDetection, Utf8Detection};
59
60 let bytes = match String::from_utf8(bytes) {
61 Ok(s) => return s,
62 Err(error) => error.into_bytes(),
63 };
64
65 let mut detector = EncodingDetector::new(Iso2022JpDetection::Allow);
66
67 detector.feed(&bytes, true);
68
69 detector.guess(tld.map(str::as_bytes), Utf8Detection::Allow).decode(&bytes).0.into_owned()
70}
71
72fn tld_hint(host: &Host) -> Option<Cow<'_, str>> {
74 let Host::Domain(domain) = host else {
75 return None;
76 };
77
78 let tld = match domain.rfind('.') {
79 Some(index) => &domain[index + 1..],
80 None => domain.as_str(),
81 };
82
83 if tld.is_empty() || !tld.is_ascii() {
84 return None;
85 }
86
87 if tld.bytes().any(|b| b.is_ascii_uppercase()) {
88 Some(Cow::Owned(tld.to_ascii_lowercase()))
89 } else {
90 Some(Cow::Borrowed(tld))
91 }
92}
93
94fn query_text<'a>(server: &WhoIsServerValue, host: &'a Host) -> Cow<'a, str> {
96 match host {
97 Host::Domain(domain) => server.encode_domain(domain),
99 Host::IPv4(ip) => Cow::Owned(ip.to_string()),
100 Host::IPv6(ip) => Cow::Owned(ip.to_string()),
101 }
102}
103
104fn build_request(server: &WhoIsServerValue, text: &str) -> String {
106 let query = server.query.as_deref().unwrap_or(DEFAULT_WHOIS_HOST_QUERY);
107
108 let mut request = query.replace("$addr", text);
109
110 if !request.ends_with("\r\n") {
111 if request.ends_with('\n') {
112 request.pop();
113 }
114
115 request.push_str("\r\n");
116 }
117
118 request
119}
120
121fn same_host(a: &Host, b: &Host) -> bool {
123 match (a, b) {
124 (Host::Domain(a), Host::Domain(b)) => a.eq_ignore_ascii_case(b),
125 _ => a == b,
126 }
127}
128
129#[derive(Debug)]
131struct Deadline {
132 start: Instant,
133 timeout: Duration,
134}
135
136impl Deadline {
137 #[inline]
138 fn new(timeout: Duration) -> Self {
139 Deadline {
140 start: Instant::now(),
141 timeout,
142 }
143 }
144
145 fn remaining(&self) -> Result<Duration, io::Error> {
147 match self.timeout.checked_sub(self.start.elapsed()) {
148 Some(remaining) if !remaining.is_zero() => Ok(remaining),
149 _ => Err(io::Error::new(io::ErrorKind::TimedOut, "the WHOIS lookup timed out")),
150 }
151 }
152}
153
154#[inline]
156fn no_address_error() -> io::Error {
157 io::Error::new(
158 io::ErrorKind::AddrNotAvailable,
159 "the host is not resolved to any socket address",
160 )
161}
162
163fn connect(socket_addrs: &[SocketAddr], deadline: &Deadline) -> io::Result<TcpStream> {
165 let mut last_error = None;
166
167 for socket_addr in socket_addrs {
168 match TcpStream::connect_timeout(socket_addr, deadline.remaining()?) {
169 Ok(client) => return Ok(client),
170 Err(error) => last_error = Some(error),
171 }
172 }
173
174 Err(last_error.unwrap_or_else(no_address_error))
175}
176
177fn read_response(
179 client: &mut TcpStream,
180 max_response_size: Option<usize>,
181 deadline: Option<&Deadline>,
182) -> io::Result<Vec<u8>> {
183 let mut query_result = Vec::new();
184 let mut buffer = [0u8; READ_BUFFER_SIZE];
185
186 loop {
187 if let Some(deadline) = deadline {
188 client.set_read_timeout(Some(deadline.remaining()?))?;
189 }
190
191 let read = client.read(&mut buffer)?;
193
194 if read == 0 {
195 break;
196 }
197
198 if let Some(max) = max_response_size
199 && query_result.len() + read > max
200 {
201 return Err(io::Error::new(
202 io::ErrorKind::InvalidData,
203 "the WHOIS response is too big",
204 ));
205 }
206
207 query_result.extend_from_slice(&buffer[..read]);
208 }
209
210 Ok(query_result)
211}
212
213#[cfg(feature = "tokio")]
215async fn connect_async(
216 socket_addrs: &[SocketAddr],
217 deadline: &Deadline,
218) -> Result<tokio::net::TcpStream, WhoIsError> {
219 let mut last_error = None;
220
221 for socket_addr in socket_addrs {
222 match tokio::time::timeout(
223 deadline.remaining()?,
224 tokio::net::TcpStream::connect(socket_addr),
225 )
226 .await
227 {
228 Ok(Ok(client)) => return Ok(client),
229 Ok(Err(error)) => last_error = Some(WhoIsError::IOError(error)),
230 Err(error) => last_error = Some(WhoIsError::Elapsed(error)),
231 }
232 }
233
234 Err(last_error.unwrap_or_else(|| WhoIsError::IOError(no_address_error())))
235}
236
237#[cfg(feature = "tokio")]
239async fn read_response_async(
240 client: &mut tokio::net::TcpStream,
241 max_response_size: Option<usize>,
242) -> io::Result<Vec<u8>> {
243 let mut query_result = Vec::new();
244
245 match max_response_size {
246 Some(max) => {
247 client.take(max.saturating_add(1) as u64).read_to_end(&mut query_result).await?;
249
250 if query_result.len() > max {
251 return Err(io::Error::new(
252 io::ErrorKind::InvalidData,
253 "the WHOIS response is too big",
254 ));
255 }
256 },
257 None => {
258 client.read_to_end(&mut query_result).await?;
259 },
260 }
261
262 Ok(query_result)
263}
264
265#[derive(Debug, Clone)]
267pub struct WhoIs {
268 map: HashMap<String, WhoIsServerValue>,
269 ip: WhoIsServerValue,
270}
271
272impl WhoIs {
273 pub fn from_host<T: AsRef<str>>(host: T) -> Result<WhoIs, WhoIsError> {
275 Ok(Self {
276 map: HashMap::new(), ip: WhoIsServerValue::from_string(host)?
277 })
278 }
279
280 #[inline]
282 pub fn from_path<P: AsRef<Path>>(path: P) -> Result<WhoIs, WhoIsError> {
283 let path = path.as_ref();
284
285 let file = File::open(path)?;
286
287 let map: Map<String, Value> = serde_json::from_reader(file)?;
288
289 Self::from_inner(map)
290 }
291
292 #[cfg(feature = "tokio")]
294 #[inline]
295 pub async fn from_path_async<P: AsRef<Path>>(path: P) -> Result<WhoIs, WhoIsError> {
296 let file = tokio::fs::read(path).await?;
297
298 let map: Map<String, Value> = serde_json::from_slice(file.as_slice())?;
299
300 Self::from_inner(map)
301 }
302
303 #[inline]
305 pub fn from_string<S: AsRef<str>>(string: S) -> Result<WhoIs, WhoIsError> {
306 let string = string.as_ref();
307
308 let map: Map<String, Value> = serde_json::from_str(string)?;
309
310 Self::from_inner(map)
311 }
312
313 fn from_inner(mut map: Map<String, Value>) -> Result<WhoIs, WhoIsError> {
314 let ip = match map.remove("_") {
315 Some(server) => {
316 if let Value::Object(server) = server {
317 match server.get("ip") {
318 Some(server) => {
319 if server.is_null() {
320 return Err(WhoIsError::MapError(
321 "`ip` in the `_` object in the server list is null.",
322 ));
323 }
324
325 WhoIsServerValue::from_value(server)?
326 },
327 None => {
328 return Err(WhoIsError::MapError(
329 "Cannot find `ip` in the `_` object in the server list.",
330 ));
331 },
332 }
333 } else {
334 return Err(WhoIsError::MapError("`_` in the server list is not an object."));
335 }
336 },
337 None => return Err(WhoIsError::MapError("Cannot find `_` in the server list.")),
338 };
339
340 let mut new_map: HashMap<String, WhoIsServerValue> = HashMap::with_capacity(map.len());
341
342 for (k, v) in map {
343 if !v.is_null() {
344 let server_value = WhoIsServerValue::from_value(&v)?;
345 new_map.insert(k, server_value);
346 }
347 }
348
349 Ok(WhoIs {
350 map: new_map,
351 ip,
352 })
353 }
354}
355
356#[cfg(feature = "srv")]
357impl WhoIs {
358 pub fn can_find_server_for_tld<T: AsRef<str>, D: AsRef<str>>(
362 &mut self,
363 tld: T,
364 dns_server: D,
365 ) -> Result<bool, WhoIsError> {
366 let mut tld = tld.as_ref();
367 let dns_server = dns_server.as_ref();
368
369 if self.map.contains_key(tld) {
371 return Ok(true);
372 }
373
374 let address = match dns_server.parse() {
375 Ok(address) => address,
376 Err(_error) => {
377 return Err(WhoIsError::MapError("The DNS server address is incorrect."));
378 },
379 };
380 let conn = UdpClientConnection::new(address).map_err(io::Error::other)?;
381 let client = SyncClient::new(conn);
382
383 loop {
384 match tld.find('.') {
385 Some(index) => {
386 tld = &tld[index + 1..];
387 },
388 None => {
389 return Ok(false);
390 },
391 }
392
393 if tld.is_empty() {
394 return Ok(false);
395 }
396
397 if self.map.contains_key(tld) {
399 return Ok(true);
400 }
401
402 let name =
403 Name::from_str(&format!("_nicname._tcp.{tld}.")).map_err(io::Error::other)?;
404 let response: DnsResponse =
405 client.query(&name, DNSClass::IN, RecordType::SRV).map_err(io::Error::other)?;
406 let answers: &[Record] = response.answers();
407
408 for record in answers {
409 if let Some(RData::SRV(record)) = record.data() {
410 let target = record.target().to_string();
411 let new_server =
412 match WhoIsServerValue::from_string(target.trim_end_matches('.')) {
413 Ok(new_server) => new_server,
414 Err(_error) => continue,
415 };
416
417 self.map.insert(tld.to_string(), new_server);
418
419 return Ok(true);
420 }
421 }
422 }
423 }
424}
425
426impl WhoIs {
427 pub fn get_server_by_tld(&self, mut tld: &str) -> Option<&WhoIsServerValue> {
429 let mut server;
430
431 loop {
432 server = self.map.get(tld);
433
434 if server.is_some() {
435 break;
436 }
437
438 if tld.is_empty() {
439 break;
440 }
441
442 match tld.find('.') {
443 Some(index) => {
444 tld = &tld[index + 1..];
445 },
446 None => {
447 tld = "";
448 },
449 }
450 }
451
452 server
453 }
454
455 fn get_server_by_host(&self, host: &WhoIsHost) -> Option<&WhoIsServerValue> {
457 self.map.values().find(|server| &server.host == host)
458 }
459
460 fn get_server<'a>(
462 &'a self,
463 options: &'a WhoIsLookupOptions,
464 ) -> Result<&'a WhoIsServerValue, WhoIsError> {
465 if let Some(server) = &options.server {
466 return Ok(server);
467 }
468
469 match &options.target.0 {
470 Host::IPv4(_) | Host::IPv6(_) => Ok(&self.ip),
471 Host::Domain(domain) => self
472 .get_server_by_tld(domain.as_str())
473 .ok_or(WhoIsError::MapError("No whois server is known for this kind of object.")),
474 }
475 }
476
477 fn lookup_once(
478 server: &WhoIsServerValue,
479 text: &str,
480 options: &WhoIsLookupOptions,
481 ) -> Result<Vec<u8>, WhoIsError> {
482 let addr = server.host.to_addr_string(DEFAULT_WHOIS_HOST_PORT);
483 let request = build_request(server, text);
484
485 match options.timeout {
486 Some(timeout) => {
487 let deadline = Deadline::new(timeout);
488
489 let socket_addrs: Vec<SocketAddr> = addr.to_socket_addrs()?.collect();
490
491 let mut client = connect(&socket_addrs, &deadline)?;
492
493 client.set_write_timeout(Some(deadline.remaining()?))?;
494 client.write_all(request.as_bytes())?;
495 client.flush()?;
496
497 Ok(read_response(&mut client, options.max_response_size, Some(&deadline))?)
498 },
499 None => {
500 let mut client = TcpStream::connect(&addr)?;
501
502 client.write_all(request.as_bytes())?;
503 client.flush()?;
504
505 Ok(read_response(&mut client, options.max_response_size, None)?)
506 },
507 }
508 }
509
510 fn lookup_inner(
511 &self,
512 server: &WhoIsServerValue,
513 options: &WhoIsLookupOptions,
514 ) -> Result<Vec<u8>, WhoIsError> {
515 let host = &options.target.0;
516 let mut follow = options.follow;
517 let mut server = Cow::Borrowed(server);
518
519 let text = query_text(&server, host);
520 let mut query_result = Self::lookup_once(&server, text.as_ref(), options)?;
521
522 while follow > 0 {
523 let Some(referral) = extract_referral_host(&query_result)
524 .and_then(|h| WhoIsServerValue::from_string(h).ok())
525 else {
526 break;
527 };
528
529 if same_host(referral.host.host(), server.host.host()) {
531 break;
532 }
533
534 server = match self.get_server_by_host(&referral.host) {
535 Some(server) => Cow::Borrowed(server),
536 None => Cow::Owned(referral),
537 };
538
539 let text = query_text(&server, host);
540
541 query_result = Self::lookup_once(&server, text.as_ref(), options)?;
542
543 follow -= 1;
544 }
545
546 Ok(query_result)
547 }
548
549 #[inline]
551 pub fn lookup_raw(&self, options: WhoIsLookupOptions) -> Result<Vec<u8>, WhoIsError> {
552 let server = self.get_server(&options)?;
553
554 self.lookup_inner(server, &options)
555 }
556
557 pub fn lookup(&self, options: WhoIsLookupOptions) -> Result<String, WhoIsError> {
559 let server = self.get_server(&options)?;
560
561 let query_result = self.lookup_inner(server, &options)?;
562
563 Ok(decode_response(query_result, tld_hint(&options.target.0).as_deref()))
564 }
565}
566
567#[cfg(feature = "tokio")]
568impl WhoIs {
569 async fn lookup_once_async(
570 server: &WhoIsServerValue,
571 text: &str,
572 options: &WhoIsLookupOptions,
573 ) -> Result<Vec<u8>, WhoIsError> {
574 let addr = server.host.to_addr_string(DEFAULT_WHOIS_HOST_PORT);
575 let request = build_request(server, text);
576
577 match options.timeout {
578 Some(timeout) => {
579 let deadline = Deadline::new(timeout);
580
581 let socket_addrs: Vec<SocketAddr> =
583 tokio::time::timeout(deadline.remaining()?, tokio::net::lookup_host(&addr))
584 .await??
585 .collect();
586
587 let mut client = connect_async(&socket_addrs, &deadline).await?;
588
589 tokio::time::timeout(deadline.remaining()?, client.write_all(request.as_bytes()))
590 .await??;
591 tokio::time::timeout(deadline.remaining()?, client.flush()).await??;
592
593 Ok(tokio::time::timeout(
594 deadline.remaining()?,
595 read_response_async(&mut client, options.max_response_size),
596 )
597 .await??)
598 },
599 None => {
600 let mut client = tokio::net::TcpStream::connect(&addr).await?;
601
602 client.write_all(request.as_bytes()).await?;
603 client.flush().await?;
604
605 Ok(read_response_async(&mut client, options.max_response_size).await?)
606 },
607 }
608 }
609
610 async fn lookup_inner_async(
611 &self,
612 server: &WhoIsServerValue,
613 options: &WhoIsLookupOptions,
614 ) -> Result<Vec<u8>, WhoIsError> {
615 let host = &options.target.0;
616 let mut follow = options.follow;
617 let mut server = Cow::Borrowed(server);
618
619 let text = query_text(&server, host);
620 let mut query_result = Self::lookup_once_async(&server, text.as_ref(), options).await?;
621
622 while follow > 0 {
623 let Some(referral) = extract_referral_host(&query_result)
624 .and_then(|h| WhoIsServerValue::from_string(h).ok())
625 else {
626 break;
627 };
628
629 if same_host(referral.host.host(), server.host.host()) {
631 break;
632 }
633
634 server = match self.get_server_by_host(&referral.host) {
635 Some(server) => Cow::Borrowed(server),
636 None => Cow::Owned(referral),
637 };
638
639 let text = query_text(&server, host);
640
641 query_result = Self::lookup_once_async(&server, text.as_ref(), options).await?;
642
643 follow -= 1;
644 }
645
646 Ok(query_result)
647 }
648
649 #[inline]
651 pub async fn lookup_raw_async(
652 &self,
653 options: WhoIsLookupOptions,
654 ) -> Result<Vec<u8>, WhoIsError> {
655 let server = self.get_server(&options)?;
656
657 self.lookup_inner_async(server, &options).await
658 }
659
660 pub async fn lookup_async(&self, options: WhoIsLookupOptions) -> Result<String, WhoIsError> {
662 let server = self.get_server(&options)?;
663
664 let query_result = self.lookup_inner_async(server, &options).await?;
665
666 Ok(decode_response(query_result, tld_hint(&options.target.0).as_deref()))
667 }
668}
669
670#[cfg(test)]
671mod tests {
672 use validators::models::Host;
673
674 use super::{build_request, decode_response, extract_referral_host, tld_hint};
675 use crate::WhoIsServerValue;
676
677 #[test]
678 fn extract_referral_host_trims_trailing_cr() {
679 let body = b"Domain: example.com\r\nReferralServer: whois://whois.arin.net\r\n";
680
681 assert_eq!(Some("whois.arin.net"), extract_referral_host(body));
682 }
683
684 #[test]
685 fn extract_referral_host_ignores_non_utf8_body() {
686 let body = b"\xB5n\xBFy: example.tw\r\nWhois Server: whois.twnic.net.tw\r\n";
688
689 assert_eq!(Some("whois.twnic.net.tw"), extract_referral_host(body));
690 }
691
692 #[test]
693 fn extract_referral_host_takes_the_first_token() {
694 let body = b" Registrar WHOIS Server: whois.example.com (see the note below)\r\n";
695
696 assert_eq!(Some("whois.example.com"), extract_referral_host(body));
697 }
698
699 #[test]
700 fn extract_referral_host_ignores_a_field_name_in_the_middle_of_a_line() {
701 let body = b"Do not trust any Whois Server: which is not listed above.\r\n";
702
703 assert_eq!(None, extract_referral_host(body));
704 }
705
706 #[test]
707 fn tld_hint_takes_the_last_label() {
708 assert_eq!(Some("tw"), tld_hint(&Host::Domain(String::from("magiclen.com.tw"))).as_deref());
709 assert_eq!(Some("com"), tld_hint(&Host::Domain(String::from("magiclen.COM"))).as_deref());
710 assert_eq!(None, tld_hint(&Host::IPv4("172.105.210.153".parse().unwrap())).as_deref());
711 }
712
713 #[test]
714 fn build_request_ends_with_crlf() {
715 let mut server = WhoIsServerValue::from_string("whois.example.com").unwrap();
716
717 assert_eq!("magiclen.org\r\n", build_request(&server, "magiclen.org"));
718
719 server.query = Some(String::from("domain $addr\r\n"));
720 assert_eq!("domain magiclen.org\r\n", build_request(&server, "magiclen.org"));
721
722 server.query = Some(String::from("domain $addr\n"));
724 assert_eq!("domain magiclen.org\r\n", build_request(&server, "magiclen.org"));
725
726 server.query = Some(String::from("domain $addr"));
727 assert_eq!("domain magiclen.org\r\n", build_request(&server, "magiclen.org"));
728 }
729
730 #[test]
731 fn decode_response_passes_through_utf8() {
732 assert_eq!("café", decode_response("café".as_bytes().to_vec(), None));
733 }
734
735 #[cfg(feature = "charset")]
737 #[test]
738 fn decode_response_detects_windows_1252() {
739 assert_eq!("café", decode_response(vec![b'c', b'a', b'f', 0xE9], None));
740 }
741
742 #[cfg(not(feature = "charset"))]
743 #[test]
744 fn decode_response_is_lossy_without_charset() {
745 assert_eq!("caf\u{FFFD}", decode_response(vec![b'c', b'a', b'f', 0xE9], None));
746 }
747}