Skip to main content

whois_rust/
who_is.rs

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, ClientHandle},
17    proto::{
18        rr::{DNSClass, Name, RData, RecordType},
19        runtime::TokioRuntimeProvider,
20        udp::UdpClientStream,
21    },
22};
23use regex::bytes::Regex;
24use serde_json::{Map, Value};
25#[cfg(feature = "tokio")]
26use tokio::io::{AsyncReadExt, AsyncWriteExt};
27use validators::models::Host;
28
29use crate::{WhoIsError, WhoIsHost, WhoIsLookupOptions, WhoIsServerValue};
30
31const DEFAULT_WHOIS_HOST_PORT: u16 = 43;
32const DEFAULT_WHOIS_HOST_QUERY: &str = "$addr\r\n";
33const READ_BUFFER_SIZE: usize = 8 * 1024;
34
35// The pattern is matched against the raw response, so Unicode mode is turned off to let `\S` mean "any non-whitespace byte". A field name has to start its own line, otherwise a mention of it in the legal notice of a response would be picked up.
36static RE_SERVER: LazyLock<Regex> = LazyLock::new(|| {
37    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()
38});
39
40/// Extract the referral WHOIS host from a query result. The response is not decoded first because a host is always ASCII, and only the first token of the value is taken because responses usually end lines with CRLF and some servers append a note to the host.
41fn extract_referral_host(query_result: &[u8]) -> Option<&str> {
42    let host = RE_SERVER.captures(query_result)?.get(1)?.as_bytes();
43    let host = std::str::from_utf8(host).ok()?;
44
45    if host.is_empty() { None } else { Some(host) }
46}
47
48/// Decode the raw bytes of a WHOIS response into a `String`. Valid UTF-8 always passes through without being copied. When the `charset` feature is disabled, invalid UTF-8 falls back to a lossy conversion (malformed bytes become `U+FFFD`), and the TLD hint is unused.
49#[cfg(not(feature = "charset"))]
50fn decode_response(bytes: Vec<u8>, _tld: Option<&str>) -> String {
51    match String::from_utf8(bytes) {
52        Ok(s) => s,
53        Err(error) => String::from_utf8_lossy(error.as_bytes()).into_owned(),
54    }
55}
56
57/// Decode the raw bytes of a WHOIS response into a `String`. Valid UTF-8 always passes through without being copied. Otherwise the encoding is detected with `chardetng` and decoded with `encoding_rs` (malformed bytes become `U+FFFD`). The TLD hint biases the detector towards the encodings that are common in that TLD, which matters for CJK ccTLDs.
58#[cfg(feature = "charset")]
59fn decode_response(bytes: Vec<u8>, tld: Option<&str>) -> String {
60    use chardetng::{EncodingDetector, Iso2022JpDetection, Utf8Detection};
61
62    let bytes = match String::from_utf8(bytes) {
63        Ok(s) => return s,
64        Err(error) => error.into_bytes(),
65    };
66
67    let mut detector = EncodingDetector::new(Iso2022JpDetection::Allow);
68
69    detector.feed(&bytes, true);
70
71    detector.guess(tld.map(str::as_bytes), Utf8Detection::Allow).decode(&bytes).0.into_owned()
72}
73
74/// Get the TLD of a target to hint the charset detector with. `chardetng` requires it to be ASCII without upper case letters, so an upper case TLD is lower cased and a non-ASCII one gives `None`.
75fn tld_hint(host: &Host) -> Option<Cow<'_, str>> {
76    let Host::Domain(domain) = host else {
77        return None;
78    };
79
80    let tld = match domain.rfind('.') {
81        Some(index) => &domain[index + 1..],
82        None => domain.as_str(),
83    };
84
85    if tld.is_empty() || !tld.is_ascii() {
86        return None;
87    }
88
89    if tld.bytes().any(|b| b.is_ascii_uppercase()) {
90        Some(Cow::Owned(tld.to_ascii_lowercase()))
91    } else {
92        Some(Cow::Borrowed(tld))
93    }
94}
95
96/// Build the text of a target to send to a server. Unlike a URI authority, IPv6 addresses are **not** wrapped in `[` and `]`, which is what WHOIS servers expect.
97fn query_text<'a>(server: &WhoIsServerValue, host: &'a Host) -> Cow<'a, str> {
98    match host {
99        // The domain is already ASCII (Punycode) encoded; decode it back to Unicode for servers that require it.
100        Host::Domain(domain) => server.encode_domain(domain),
101        Host::IPv4(ip) => Cow::Owned(ip.to_string()),
102        Host::IPv6(ip) => Cow::Owned(ip.to_string()),
103    }
104}
105
106/// Build the request to send to a server. RFC 3912 requires a request to be terminated with CRLF, so a query which is not is fixed up instead of being sent as it is.
107fn build_request(server: &WhoIsServerValue, text: &str) -> String {
108    let query = server.query.as_deref().unwrap_or(DEFAULT_WHOIS_HOST_QUERY);
109
110    let mut request = query.replace("$addr", text);
111
112    if !request.ends_with("\r\n") {
113        if request.ends_with('\n') {
114            request.pop();
115        }
116
117        request.push_str("\r\n");
118    }
119
120    request
121}
122
123/// Whether two hosts point at the same WHOIS server. Domains are compared case insensitively, because a referral does not have to spell its host the same way as the server list does.
124fn same_host(a: &Host, b: &Host) -> bool {
125    match (a, b) {
126        (Host::Domain(a), Host::Domain(b)) => a.eq_ignore_ascii_case(b),
127        _ => a == b,
128    }
129}
130
131/// A timeout budget shared by every step of one connection, so that a host with several addresses cannot make a lookup take the timeout once per address.
132#[derive(Debug)]
133struct Deadline {
134    start:   Instant,
135    timeout: Duration,
136}
137
138impl Deadline {
139    #[inline]
140    fn new(timeout: Duration) -> Self {
141        Deadline {
142            start: Instant::now(),
143            timeout,
144        }
145    }
146
147    /// Get the part of the budget which is still available, or an error if it has run out.
148    fn remaining(&self) -> Result<Duration, io::Error> {
149        match self.timeout.checked_sub(self.start.elapsed()) {
150            Some(remaining) if !remaining.is_zero() => Ok(remaining),
151            _ => Err(io::Error::new(io::ErrorKind::TimedOut, "the WHOIS lookup timed out")),
152        }
153    }
154}
155
156/// The error to report when a host has no address left to try.
157#[inline]
158fn no_address_error() -> io::Error {
159    io::Error::new(
160        io::ErrorKind::AddrNotAvailable,
161        "the host is not resolved to any socket address",
162    )
163}
164
165/// Connect to the first address which accepts the connection, spending one shared budget on all of the attempts.
166fn connect(socket_addrs: &[SocketAddr], deadline: &Deadline) -> io::Result<TcpStream> {
167    let mut last_error = None;
168
169    for socket_addr in socket_addrs {
170        match TcpStream::connect_timeout(socket_addr, deadline.remaining()?) {
171            Ok(client) => return Ok(client),
172            Err(error) => last_error = Some(error),
173        }
174    }
175
176    Err(last_error.unwrap_or_else(no_address_error))
177}
178
179/// Read a whole WHOIS response, failing instead of truncating when it is bigger than the given limit. The deadline, when there is one, bounds the whole response instead of each read, so that a server which trickles its response out cannot hold the connection for longer than the timeout.
180fn read_response(
181    client: &mut TcpStream,
182    max_response_size: Option<usize>,
183    deadline: Option<&Deadline>,
184) -> io::Result<Vec<u8>> {
185    let mut query_result = Vec::new();
186    let mut buffer = [0u8; READ_BUFFER_SIZE];
187
188    loop {
189        if let Some(deadline) = deadline {
190            client.set_read_timeout(Some(deadline.remaining()?))?;
191        }
192
193        // RFC 3912: the response is over when the server closes the connection.
194        let read = client.read(&mut buffer)?;
195
196        if read == 0 {
197            break;
198        }
199
200        if let Some(max) = max_response_size
201            && query_result.len() + read > max
202        {
203            return Err(io::Error::new(
204                io::ErrorKind::InvalidData,
205                "the WHOIS response is too big",
206            ));
207        }
208
209        query_result.extend_from_slice(&buffer[..read]);
210    }
211
212    Ok(query_result)
213}
214
215/// Connect to the first address which accepts the connection, spending one shared budget on all of the attempts.
216#[cfg(feature = "tokio")]
217async fn connect_async(
218    socket_addrs: &[SocketAddr],
219    deadline: &Deadline,
220) -> Result<tokio::net::TcpStream, WhoIsError> {
221    let mut last_error = None;
222
223    for socket_addr in socket_addrs {
224        match tokio::time::timeout(
225            deadline.remaining()?,
226            tokio::net::TcpStream::connect(socket_addr),
227        )
228        .await
229        {
230            Ok(Ok(client)) => return Ok(client),
231            Ok(Err(error)) => last_error = Some(WhoIsError::IOError(error)),
232            Err(error) => last_error = Some(WhoIsError::Elapsed(error)),
233        }
234    }
235
236    Err(last_error.unwrap_or_else(|| WhoIsError::IOError(no_address_error())))
237}
238
239/// Read the address of the DNS server to query the SRV records through.
240#[cfg(feature = "srv")]
241fn parse_dns_server(dns_server: &str) -> Result<SocketAddr, WhoIsError> {
242    dns_server
243        .parse()
244        .map_err(|_error| WhoIsError::MapError("The DNS server address is incorrect."))
245}
246
247/// Strip the leftmost label off a TLD/domain, giving `None` when there is nothing left to query.
248#[cfg(feature = "srv")]
249fn strip_label(tld: &str) -> Option<&str> {
250    let parent = &tld[tld.find('.')? + 1..];
251
252    if parent.is_empty() { None } else { Some(parent) }
253}
254
255/// Query the SRV records of a name through a DNS server, returning the target of every answer.
256#[cfg(feature = "srv")]
257async fn query_srv_targets_async(
258    dns_server: SocketAddr,
259    name: Name,
260) -> Result<Vec<String>, WhoIsError> {
261    let stream = UdpClientStream::builder(dns_server, TokioRuntimeProvider::new()).build();
262
263    let (mut client, background) = Client::connect(stream).await.map_err(io::Error::other)?;
264
265    // The connection is driven by this task, so the query cannot make any progress without it.
266    let background = tokio::spawn(background);
267
268    let response = client.query(name, DNSClass::IN, RecordType::SRV).await;
269
270    background.abort();
271
272    let response = response.map_err(io::Error::other)?;
273
274    Ok(response
275        .answers()
276        .iter()
277        .filter_map(|record| match record.data() {
278            RData::SRV(srv) => Some(srv.target().to_string()),
279            _ => None,
280        })
281        .collect())
282}
283
284/// Query the SRV records of a name through a DNS server, returning the target of every answer. `hickory-client` is async only, so the query is run on a thread of its own with a runtime of its own, which keeps this usable from inside an async runtime as well.
285#[cfg(feature = "srv")]
286fn query_srv_targets(dns_server: SocketAddr, name: Name) -> Result<Vec<String>, WhoIsError> {
287    let thread = std::thread::spawn(move || -> Result<Vec<String>, WhoIsError> {
288        let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
289
290        runtime.block_on(query_srv_targets_async(dns_server, name))
291    });
292
293    match thread.join() {
294        Ok(targets) => targets,
295        Err(error) => std::panic::resume_unwind(error),
296    }
297}
298
299/// Read a whole WHOIS response, failing instead of truncating when it is bigger than the given limit. The caller bounds the time this takes by awaiting it with a timeout.
300#[cfg(feature = "tokio")]
301async fn read_response_async(
302    client: &mut tokio::net::TcpStream,
303    max_response_size: Option<usize>,
304) -> io::Result<Vec<u8>> {
305    let mut query_result = Vec::new();
306
307    match max_response_size {
308        Some(max) => {
309            // One byte past the limit is read so that a response which is too big can be told apart from one which exactly fits.
310            client.take(max.saturating_add(1) as u64).read_to_end(&mut query_result).await?;
311
312            if query_result.len() > max {
313                return Err(io::Error::new(
314                    io::ErrorKind::InvalidData,
315                    "the WHOIS response is too big",
316                ));
317            }
318        },
319        None => {
320            client.read_to_end(&mut query_result).await?;
321        },
322    }
323
324    Ok(query_result)
325}
326
327/// The `WhoIs` structure stores the list of WHOIS servers in-memory.
328#[derive(Debug, Clone)]
329pub struct WhoIs {
330    map: HashMap<String, WhoIsServerValue>,
331    ip:  WhoIsServerValue,
332}
333
334impl WhoIs {
335    /// Create a `WhoIs` instance which doesn't have a WHOIS server list. You should provide the host that is used for query ip. You may want to use the host `"whois.arin.net"`.
336    pub fn from_host<T: AsRef<str>>(host: T) -> Result<WhoIs, WhoIsError> {
337        Ok(Self {
338            map: HashMap::new(), ip: WhoIsServerValue::from_string(host)?
339        })
340    }
341
342    /// Read the list of WHOIS servers (JSON data) from a file to create a `WhoIs` instance.
343    #[inline]
344    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<WhoIs, WhoIsError> {
345        let path = path.as_ref();
346
347        let file = File::open(path)?;
348
349        let map: Map<String, Value> = serde_json::from_reader(file)?;
350
351        Self::from_inner(map)
352    }
353
354    /// Read the list of WHOIS servers (JSON data) from a file to create a `WhoIs` instance. For `serde_json` doesn't support async functions, consider just using the `from_path` function.
355    #[cfg(feature = "tokio")]
356    #[inline]
357    pub async fn from_path_async<P: AsRef<Path>>(path: P) -> Result<WhoIs, WhoIsError> {
358        let file = tokio::fs::read(path).await?;
359
360        let map: Map<String, Value> = serde_json::from_slice(file.as_slice())?;
361
362        Self::from_inner(map)
363    }
364
365    /// Read the list of WHOIS servers (JSON data) from a string to create a `WhoIs` instance.
366    #[inline]
367    pub fn from_string<S: AsRef<str>>(string: S) -> Result<WhoIs, WhoIsError> {
368        let string = string.as_ref();
369
370        let map: Map<String, Value> = serde_json::from_str(string)?;
371
372        Self::from_inner(map)
373    }
374
375    fn from_inner(mut map: Map<String, Value>) -> Result<WhoIs, WhoIsError> {
376        let ip = match map.remove("_") {
377            Some(server) => {
378                if let Value::Object(server) = server {
379                    match server.get("ip") {
380                        Some(server) => {
381                            if server.is_null() {
382                                return Err(WhoIsError::MapError(
383                                    "`ip` in the `_` object in the server list is null.",
384                                ));
385                            }
386
387                            WhoIsServerValue::from_value(server)?
388                        },
389                        None => {
390                            return Err(WhoIsError::MapError(
391                                "Cannot find `ip` in the `_` object in the server list.",
392                            ));
393                        },
394                    }
395                } else {
396                    return Err(WhoIsError::MapError("`_` in the server list is not an object."));
397                }
398            },
399            None => return Err(WhoIsError::MapError("Cannot find `_` in the server list.")),
400        };
401
402        let mut new_map: HashMap<String, WhoIsServerValue> = HashMap::with_capacity(map.len());
403
404        for (k, v) in map {
405            if !v.is_null() {
406                let server_value = WhoIsServerValue::from_value(&v)?;
407                new_map.insert(k, server_value);
408            }
409        }
410
411        Ok(WhoIs {
412            map: new_map,
413            ip,
414        })
415    }
416}
417
418#[cfg(feature = "srv")]
419impl WhoIs {
420    /// Cache the first target which is a valid host as the server of a TLD. Returns whether one of them was.
421    fn cache_first_server(&mut self, tld: &str, targets: Vec<String>) -> bool {
422        for target in targets {
423            let Ok(new_server) = WhoIsServerValue::from_string(target.trim_end_matches('.')) else {
424                continue;
425            };
426
427            self.map.insert(tld.to_string(), new_server);
428
429            return true;
430        }
431
432        false
433    }
434
435    /// Try to find a WHOIS server for a TLD by querying the `_nicname._tcp.<tld>` SRV records through the given DNS server, caching any server that is found for later lookups. Returns whether a server is available for the TLD (already known or newly discovered).
436    ///
437    /// One label is stripped off before each query, so pass `".example.com"` to have `example.com` itself queried. A TLD which is already in the server list is never replaced by a discovered one.
438    ///
439    /// This method blocks. It may be called from inside an async runtime, but `can_find_server_for_tld_async` (which needs the `tokio` feature) is the one to use there.
440    pub fn can_find_server_for_tld<T: AsRef<str>, D: AsRef<str>>(
441        &mut self,
442        tld: T,
443        dns_server: D,
444    ) -> Result<bool, WhoIsError> {
445        let mut tld = tld.as_ref();
446
447        // The TLD as given may already be known, in which case nothing else has to be set up.
448        if self.map.contains_key(tld) {
449            return Ok(true);
450        }
451
452        let address = parse_dns_server(dns_server.as_ref())?;
453
454        loop {
455            let Some(parent) = strip_label(tld) else {
456                return Ok(false);
457            };
458
459            tld = parent;
460
461            // The parent is checked before it is queried, so a known server is never queried for nor overwritten.
462            if self.map.contains_key(tld) {
463                return Ok(true);
464            }
465
466            let name =
467                Name::from_str(&format!("_nicname._tcp.{tld}.")).map_err(io::Error::other)?;
468
469            if self.cache_first_server(tld, query_srv_targets(address, name)?) {
470                return Ok(true);
471            }
472        }
473    }
474}
475
476#[cfg(all(feature = "srv", feature = "tokio"))]
477impl WhoIs {
478    /// Try to find a WHOIS server for a TLD by querying the `_nicname._tcp.<tld>` SRV records through the given DNS server, caching any server that is found for later lookups. Returns whether a server is available for the TLD (already known or newly discovered).
479    ///
480    /// One label is stripped off before each query, so pass `".example.com"` to have `example.com` itself queried. A TLD which is already in the server list is never replaced by a discovered one.
481    pub async fn can_find_server_for_tld_async<T: AsRef<str>, D: AsRef<str>>(
482        &mut self,
483        tld: T,
484        dns_server: D,
485    ) -> Result<bool, WhoIsError> {
486        let mut tld = tld.as_ref();
487
488        // The TLD as given may already be known, in which case nothing else has to be set up.
489        if self.map.contains_key(tld) {
490            return Ok(true);
491        }
492
493        let address = parse_dns_server(dns_server.as_ref())?;
494
495        loop {
496            let Some(parent) = strip_label(tld) else {
497                return Ok(false);
498            };
499
500            tld = parent;
501
502            // The parent is checked before it is queried, so a known server is never queried for nor overwritten.
503            if self.map.contains_key(tld) {
504                return Ok(true);
505            }
506
507            let name =
508                Name::from_str(&format!("_nicname._tcp.{tld}.")).map_err(io::Error::other)?;
509
510            if self.cache_first_server(tld, query_srv_targets_async(address, name).await?) {
511                return Ok(true);
512            }
513        }
514    }
515}
516
517impl WhoIs {
518    /// Try to find a WHOIS server for a TLD from the in-memory server list, walking up the labels of the given TLD/domain (e.g. `a.b.example` -> `b.example` -> `example` -> `""`) until a match is found. Returns the matched server, if any.
519    pub fn get_server_by_tld(&self, mut tld: &str) -> Option<&WhoIsServerValue> {
520        let mut server;
521
522        loop {
523            server = self.map.get(tld);
524
525            if server.is_some() {
526                break;
527            }
528
529            if tld.is_empty() {
530                break;
531            }
532
533            match tld.find('.') {
534                Some(index) => {
535                    tld = &tld[index + 1..];
536                },
537                None => {
538                    tld = "";
539                },
540            }
541        }
542
543        server
544    }
545
546    /// Find the entry of the server list which is served by a host, so that following a referral to a known server keeps that server's `query` and `punycode` settings.
547    fn get_server_by_host(&self, host: &WhoIsHost) -> Option<&WhoIsServerValue> {
548        self.map.values().find(|server| &server.host == host)
549    }
550
551    /// Pick the server a lookup starts from: the one given in the options, or the best match from the server list.
552    fn get_server<'a>(
553        &'a self,
554        options: &'a WhoIsLookupOptions,
555    ) -> Result<&'a WhoIsServerValue, WhoIsError> {
556        if let Some(server) = &options.server {
557            return Ok(server);
558        }
559
560        match &options.target.0 {
561            Host::IPv4(_) | Host::IPv6(_) => Ok(&self.ip),
562            Host::Domain(domain) => self
563                .get_server_by_tld(domain.as_str())
564                .ok_or(WhoIsError::MapError("No whois server is known for this kind of object.")),
565        }
566    }
567
568    fn lookup_once(
569        server: &WhoIsServerValue,
570        text: &str,
571        options: &WhoIsLookupOptions,
572    ) -> Result<Vec<u8>, WhoIsError> {
573        let addr = server.host.to_addr_string(DEFAULT_WHOIS_HOST_PORT);
574        let request = build_request(server, text);
575
576        match options.timeout {
577            Some(timeout) => {
578                let deadline = Deadline::new(timeout);
579
580                let socket_addrs: Vec<SocketAddr> = addr.to_socket_addrs()?.collect();
581
582                let mut client = connect(&socket_addrs, &deadline)?;
583
584                client.set_write_timeout(Some(deadline.remaining()?))?;
585                client.write_all(request.as_bytes())?;
586                client.flush()?;
587
588                Ok(read_response(&mut client, options.max_response_size, Some(&deadline))?)
589            },
590            None => {
591                let mut client = TcpStream::connect(&addr)?;
592
593                client.write_all(request.as_bytes())?;
594                client.flush()?;
595
596                Ok(read_response(&mut client, options.max_response_size, None)?)
597            },
598        }
599    }
600
601    fn lookup_inner(
602        &self,
603        server: &WhoIsServerValue,
604        options: &WhoIsLookupOptions,
605    ) -> Result<Vec<u8>, WhoIsError> {
606        let host = &options.target.0;
607        let mut follow = options.follow;
608        let mut server = Cow::Borrowed(server);
609
610        let text = query_text(&server, host);
611        let mut query_result = Self::lookup_once(&server, text.as_ref(), options)?;
612
613        while follow > 0 {
614            let Some(referral) = extract_referral_host(&query_result)
615                .and_then(|h| WhoIsServerValue::from_string(h).ok())
616            else {
617                break;
618            };
619
620            // The referral may carry a port, so compare the host alone to stop a server which refers to itself.
621            if same_host(referral.host.host(), server.host.host()) {
622                break;
623            }
624
625            server = match self.get_server_by_host(&referral.host) {
626                Some(server) => Cow::Borrowed(server),
627                None => Cow::Owned(referral),
628            };
629
630            let text = query_text(&server, host);
631
632            query_result = Self::lookup_once(&server, text.as_ref(), options)?;
633
634            follow -= 1;
635        }
636
637        Ok(query_result)
638    }
639
640    /// Lookup a domain or an IP, returning the raw bytes of the final WHOIS response without decoding.
641    #[inline]
642    pub fn lookup_raw(&self, options: WhoIsLookupOptions) -> Result<Vec<u8>, WhoIsError> {
643        let server = self.get_server(&options)?;
644
645        self.lookup_inner(server, &options)
646    }
647
648    /// Lookup a domain or an IP.
649    pub fn lookup(&self, options: WhoIsLookupOptions) -> Result<String, WhoIsError> {
650        let server = self.get_server(&options)?;
651
652        let query_result = self.lookup_inner(server, &options)?;
653
654        Ok(decode_response(query_result, tld_hint(&options.target.0).as_deref()))
655    }
656}
657
658#[cfg(feature = "tokio")]
659impl WhoIs {
660    async fn lookup_once_async(
661        server: &WhoIsServerValue,
662        text: &str,
663        options: &WhoIsLookupOptions,
664    ) -> Result<Vec<u8>, WhoIsError> {
665        let addr = server.host.to_addr_string(DEFAULT_WHOIS_HOST_PORT);
666        let request = build_request(server, text);
667
668        match options.timeout {
669            Some(timeout) => {
670                let deadline = Deadline::new(timeout);
671
672                // Resolving has to go through tokio as well, because the blocking resolver would stall the whole runtime.
673                let socket_addrs: Vec<SocketAddr> =
674                    tokio::time::timeout(deadline.remaining()?, tokio::net::lookup_host(&addr))
675                        .await??
676                        .collect();
677
678                let mut client = connect_async(&socket_addrs, &deadline).await?;
679
680                tokio::time::timeout(deadline.remaining()?, client.write_all(request.as_bytes()))
681                    .await??;
682                tokio::time::timeout(deadline.remaining()?, client.flush()).await??;
683
684                Ok(tokio::time::timeout(
685                    deadline.remaining()?,
686                    read_response_async(&mut client, options.max_response_size),
687                )
688                .await??)
689            },
690            None => {
691                let mut client = tokio::net::TcpStream::connect(&addr).await?;
692
693                client.write_all(request.as_bytes()).await?;
694                client.flush().await?;
695
696                Ok(read_response_async(&mut client, options.max_response_size).await?)
697            },
698        }
699    }
700
701    async fn lookup_inner_async(
702        &self,
703        server: &WhoIsServerValue,
704        options: &WhoIsLookupOptions,
705    ) -> Result<Vec<u8>, WhoIsError> {
706        let host = &options.target.0;
707        let mut follow = options.follow;
708        let mut server = Cow::Borrowed(server);
709
710        let text = query_text(&server, host);
711        let mut query_result = Self::lookup_once_async(&server, text.as_ref(), options).await?;
712
713        while follow > 0 {
714            let Some(referral) = extract_referral_host(&query_result)
715                .and_then(|h| WhoIsServerValue::from_string(h).ok())
716            else {
717                break;
718            };
719
720            // The referral may carry a port, so compare the host alone to stop a server which refers to itself.
721            if same_host(referral.host.host(), server.host.host()) {
722                break;
723            }
724
725            server = match self.get_server_by_host(&referral.host) {
726                Some(server) => Cow::Borrowed(server),
727                None => Cow::Owned(referral),
728            };
729
730            let text = query_text(&server, host);
731
732            query_result = Self::lookup_once_async(&server, text.as_ref(), options).await?;
733
734            follow -= 1;
735        }
736
737        Ok(query_result)
738    }
739
740    /// Lookup a domain or an IP, returning the raw bytes of the final WHOIS response without decoding.
741    #[inline]
742    pub async fn lookup_raw_async(
743        &self,
744        options: WhoIsLookupOptions,
745    ) -> Result<Vec<u8>, WhoIsError> {
746        let server = self.get_server(&options)?;
747
748        self.lookup_inner_async(server, &options).await
749    }
750
751    /// Lookup a domain or an IP.
752    pub async fn lookup_async(&self, options: WhoIsLookupOptions) -> Result<String, WhoIsError> {
753        let server = self.get_server(&options)?;
754
755        let query_result = self.lookup_inner_async(server, &options).await?;
756
757        Ok(decode_response(query_result, tld_hint(&options.target.0).as_deref()))
758    }
759}
760
761#[cfg(test)]
762mod tests {
763    use validators::models::Host;
764
765    use super::{build_request, decode_response, extract_referral_host, tld_hint};
766    use crate::WhoIsServerValue;
767
768    #[test]
769    fn extract_referral_host_trims_trailing_cr() {
770        let body = b"Domain: example.com\r\nReferralServer: whois://whois.arin.net\r\n";
771
772        assert_eq!(Some("whois.arin.net"), extract_referral_host(body));
773    }
774
775    #[test]
776    fn extract_referral_host_ignores_non_utf8_body() {
777        // The referral line is ASCII even when the rest of the response is in some other encoding.
778        let body = b"\xB5n\xBFy: example.tw\r\nWhois Server: whois.twnic.net.tw\r\n";
779
780        assert_eq!(Some("whois.twnic.net.tw"), extract_referral_host(body));
781    }
782
783    #[test]
784    fn extract_referral_host_takes_the_first_token() {
785        let body = b"   Registrar WHOIS Server: whois.example.com (see the note below)\r\n";
786
787        assert_eq!(Some("whois.example.com"), extract_referral_host(body));
788    }
789
790    #[test]
791    fn extract_referral_host_ignores_a_field_name_in_the_middle_of_a_line() {
792        let body = b"Do not trust any Whois Server: which is not listed above.\r\n";
793
794        assert_eq!(None, extract_referral_host(body));
795    }
796
797    #[test]
798    fn tld_hint_takes_the_last_label() {
799        assert_eq!(Some("tw"), tld_hint(&Host::Domain(String::from("magiclen.com.tw"))).as_deref());
800        assert_eq!(Some("com"), tld_hint(&Host::Domain(String::from("magiclen.COM"))).as_deref());
801        assert_eq!(None, tld_hint(&Host::IPv4("172.105.210.153".parse().unwrap())).as_deref());
802    }
803
804    #[test]
805    fn build_request_ends_with_crlf() {
806        let mut server = WhoIsServerValue::from_string("whois.example.com").unwrap();
807
808        assert_eq!("magiclen.org\r\n", build_request(&server, "magiclen.org"));
809
810        server.query = Some(String::from("domain $addr\r\n"));
811        assert_eq!("domain magiclen.org\r\n", build_request(&server, "magiclen.org"));
812
813        // A query which is terminated with LF alone, or not terminated at all, is fixed up.
814        server.query = Some(String::from("domain $addr\n"));
815        assert_eq!("domain magiclen.org\r\n", build_request(&server, "magiclen.org"));
816
817        server.query = Some(String::from("domain $addr"));
818        assert_eq!("domain magiclen.org\r\n", build_request(&server, "magiclen.org"));
819    }
820
821    #[test]
822    fn decode_response_passes_through_utf8() {
823        assert_eq!("café", decode_response("café".as_bytes().to_vec(), None));
824    }
825
826    // "café" with `é` as the single byte 0xE9 (Windows-1252 / ISO-8859-1).
827    #[cfg(feature = "charset")]
828    #[test]
829    fn decode_response_detects_windows_1252() {
830        assert_eq!("café", decode_response(vec![b'c', b'a', b'f', 0xE9], None));
831    }
832
833    #[cfg(not(feature = "charset"))]
834    #[test]
835    fn decode_response_is_lossy_without_charset() {
836        assert_eq!("caf\u{FFFD}", decode_response(vec![b'c', b'a', b'f', 0xE9], None));
837    }
838}