Skip to main content

whois_rust/
who_is.rs

1use std::{
2    collections::HashMap,
3    fs::File,
4    io::{self, Read, Write},
5    net::{SocketAddr, TcpStream, ToSocketAddrs},
6    path::Path,
7    str::FromStr,
8    sync::LazyLock,
9    time::Duration,
10};
11
12use hickory_client::{
13    client::{Client, SyncClient},
14    op::DnsResponse,
15    rr::{DNSClass, Name, RData, Record, RecordType},
16    udp::UdpClientConnection,
17};
18use regex::Regex;
19use serde_json::{Map, Value};
20#[cfg(feature = "tokio")]
21use tokio::io::{AsyncReadExt, AsyncWriteExt};
22use validators::models::Host;
23
24use crate::{WhoIsError, WhoIsLookupOptions, WhoIsServerValue};
25
26const DEFAULT_WHOIS_HOST_PORT: u16 = 43;
27const DEFAULT_WHOIS_HOST_QUERY: &str = "$addr\r\n";
28
29static RE_SERVER: LazyLock<Regex> = LazyLock::new(|| {
30    Regex::new(r"(ReferralServer|Registrar Whois|Whois Server|WHOIS Server|Registrar WHOIS Server):[^\S\n]*(r?whois://)?(.*)").unwrap()
31});
32
33/// Extract the referral WHOIS host from a query result. The captured value is trimmed because responses usually end lines with CRLF, and a trailing `\r` would make the host unparsable.
34fn extract_referral_host(query_result: &str) -> Option<&str> {
35    let host = RE_SERVER.captures(query_result)?.get(3)?.as_str().trim();
36
37    if host.is_empty() { None } else { Some(host) }
38}
39
40/// Decode the raw bytes of a WHOIS response into a `String`. Valid UTF-8 always passes through unchanged. When the `charset` feature is disabled, invalid UTF-8 falls back to a lossy conversion (malformed bytes become `U+FFFD`).
41#[cfg(not(feature = "charset"))]
42fn decode_response(bytes: &[u8]) -> String {
43    match std::str::from_utf8(bytes) {
44        Ok(s) => s.to_string(),
45        Err(_) => String::from_utf8_lossy(bytes).into_owned(),
46    }
47}
48
49/// Decode the raw bytes of a WHOIS response into a `String`. Valid UTF-8 always passes through unchanged. Otherwise the encoding is detected with `chardetng` and decoded with `encoding_rs` (malformed bytes become `U+FFFD`).
50#[cfg(feature = "charset")]
51fn decode_response(bytes: &[u8]) -> String {
52    if let Ok(s) = std::str::from_utf8(bytes) {
53        return s.to_string();
54    }
55
56    let mut detector = chardetng::EncodingDetector::new();
57
58    detector.feed(bytes, true);
59
60    detector.guess(None, true).decode(bytes).0.into_owned()
61}
62
63/// Format an IP target as WHOIS query text. Unlike a URI authority, IPv6 addresses are **not** wrapped in `[` and `]`, which is what WHOIS servers expect.
64fn format_ip_query(host: &Host) -> String {
65    match host {
66        Host::IPv4(ip) => ip.to_string(),
67        Host::IPv6(ip) => ip.to_string(),
68        Host::Domain(domain) => domain.to_string(),
69    }
70}
71
72/// The `WhoIs` structure stores the list of WHOIS servers in-memory.
73#[derive(Debug, Clone)]
74pub struct WhoIs {
75    map: HashMap<String, WhoIsServerValue>,
76    ip:  WhoIsServerValue,
77}
78
79impl WhoIs {
80    /// 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"`.
81    pub fn from_host<T: AsRef<str>>(host: T) -> Result<WhoIs, WhoIsError> {
82        Ok(Self {
83            map: HashMap::new(), ip: WhoIsServerValue::from_string(host)?
84        })
85    }
86
87    /// Read the list of WHOIS servers (JSON data) from a file to create a `WhoIs` instance.
88    #[inline]
89    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<WhoIs, WhoIsError> {
90        let path = path.as_ref();
91
92        let file = File::open(path)?;
93
94        let map: Map<String, Value> = serde_json::from_reader(file)?;
95
96        Self::from_inner(map)
97    }
98
99    #[cfg(feature = "tokio")]
100    /// 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.
101    #[inline]
102    pub async fn from_path_async<P: AsRef<Path>>(path: P) -> Result<WhoIs, WhoIsError> {
103        let file = tokio::fs::read(path).await?;
104
105        let map: Map<String, Value> = serde_json::from_slice(file.as_slice())?;
106
107        Self::from_inner(map)
108    }
109
110    /// Read the list of WHOIS servers (JSON data) from a string to create a `WhoIs` instance.
111    #[inline]
112    pub fn from_string<S: AsRef<str>>(string: S) -> Result<WhoIs, WhoIsError> {
113        let string = string.as_ref();
114
115        let map: Map<String, Value> = serde_json::from_str(string)?;
116
117        Self::from_inner(map)
118    }
119
120    fn from_inner(mut map: Map<String, Value>) -> Result<WhoIs, WhoIsError> {
121        let ip = match map.remove("_") {
122            Some(server) => {
123                if let Value::Object(server) = server {
124                    match server.get("ip") {
125                        Some(server) => {
126                            if server.is_null() {
127                                return Err(WhoIsError::MapError(
128                                    "`ip` in the `_` object in the server list is null.",
129                                ));
130                            }
131
132                            WhoIsServerValue::from_value(server)?
133                        },
134                        None => {
135                            return Err(WhoIsError::MapError(
136                                "Cannot find `ip` in the `_` object in the server list.",
137                            ));
138                        },
139                    }
140                } else {
141                    return Err(WhoIsError::MapError("`_` in the server list is not an object."));
142                }
143            },
144            None => return Err(WhoIsError::MapError("Cannot find `_` in the server list.")),
145        };
146
147        let mut new_map: HashMap<String, WhoIsServerValue> = HashMap::with_capacity(map.len());
148
149        for (k, v) in map {
150            if !v.is_null() {
151                let server_value = WhoIsServerValue::from_value(&v)?;
152                new_map.insert(k, server_value);
153            }
154        }
155
156        Ok(WhoIs {
157            map: new_map,
158            ip,
159        })
160    }
161}
162
163impl WhoIs {
164    /// 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).
165    pub fn can_find_server_for_tld<T: AsRef<str>, D: AsRef<str>>(
166        &mut self,
167        tld: T,
168        dns_server: D,
169    ) -> Result<bool, WhoIsError> {
170        let mut tld = tld.as_ref();
171        let dns_server = dns_server.as_ref();
172
173        let address = match dns_server.parse() {
174            Ok(address) => address,
175            Err(_error) => {
176                return Err(WhoIsError::MapError("The DNS server address is incorrect."));
177            },
178        };
179        let conn = UdpClientConnection::new(address).map_err(io::Error::other)?;
180        let client = SyncClient::new(conn);
181
182        loop {
183            if self.map.contains_key(tld) {
184                return Ok(true);
185            }
186
187            match tld.find('.') {
188                Some(index) => {
189                    tld = &tld[index + 1..];
190                },
191                None => {
192                    tld = "";
193                },
194            }
195
196            if tld.is_empty() {
197                return Ok(false);
198            }
199
200            let name =
201                Name::from_str(&format!("_nicname._tcp.{tld}.")).map_err(io::Error::other)?;
202            let response: DnsResponse =
203                client.query(&name, DNSClass::IN, RecordType::SRV).map_err(io::Error::other)?;
204            let answers: &[Record] = response.answers();
205
206            for record in answers {
207                if let Some(RData::SRV(record)) = record.data() {
208                    let target = record.target().to_string();
209                    let new_server =
210                        match WhoIsServerValue::from_string(target.trim_end_matches('.')) {
211                            Ok(new_server) => new_server,
212                            Err(_error) => continue,
213                        };
214
215                    self.map.insert(tld.to_string(), new_server);
216
217                    return Ok(true);
218                }
219            }
220        }
221    }
222
223    /// 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.
224    pub fn get_server_by_tld(&self, mut tld: &str) -> Option<&WhoIsServerValue> {
225        let mut server;
226
227        loop {
228            server = self.map.get(tld);
229
230            if server.is_some() {
231                break;
232            }
233
234            if tld.is_empty() {
235                break;
236            }
237
238            match tld.find('.') {
239                Some(index) => {
240                    tld = &tld[index + 1..];
241                },
242                None => {
243                    tld = "";
244                },
245            }
246        }
247
248        server
249    }
250
251    fn lookup_once(
252        server: &WhoIsServerValue,
253        text: &str,
254        timeout: Option<Duration>,
255    ) -> Result<(String, Vec<u8>), WhoIsError> {
256        let addr = server.host.to_addr_string(DEFAULT_WHOIS_HOST_PORT);
257
258        let mut client = if let Some(timeout) = timeout {
259            let socket_addrs: Vec<SocketAddr> = addr.to_socket_addrs()?.collect();
260
261            if socket_addrs.is_empty() {
262                return Err(io::Error::new(
263                    io::ErrorKind::AddrNotAvailable,
264                    "the host is not resolved to any socket address",
265                )
266                .into());
267            }
268
269            let mut client = None;
270
271            for socket_addr in socket_addrs.iter().take(socket_addrs.len() - 1) {
272                if let Ok(c) = TcpStream::connect_timeout(socket_addr, timeout) {
273                    client = Some(c);
274                    break;
275                }
276            }
277
278            let client = if let Some(client) = client {
279                client
280            } else {
281                let socket_addr = &socket_addrs[socket_addrs.len() - 1];
282                TcpStream::connect_timeout(socket_addr, timeout)?
283            };
284
285            client.set_read_timeout(Some(timeout))?;
286            client.set_write_timeout(Some(timeout))?;
287            client
288        } else {
289            TcpStream::connect(&addr)?
290        };
291
292        if let Some(query) = &server.query {
293            client.write_all(query.replace("$addr", text).as_bytes())?;
294        } else {
295            client.write_all(DEFAULT_WHOIS_HOST_QUERY.replace("$addr", text).as_bytes())?;
296        }
297
298        client.flush()?;
299
300        let mut query_result = Vec::new();
301
302        client.read_to_end(&mut query_result)?;
303
304        // Return the host without the port so a referral host can be compared against it.
305        Ok((server.host.host.to_string(), query_result))
306    }
307
308    fn lookup_inner(
309        server: &WhoIsServerValue,
310        text: &str,
311        timeout: Option<Duration>,
312        mut follow: u16,
313    ) -> Result<Vec<u8>, WhoIsError> {
314        let mut query_result = Self::lookup_once(server, text, timeout)?;
315
316        while follow > 0 {
317            let body = decode_response(&query_result.1);
318
319            if let Some(h) = extract_referral_host(&body)
320                && !h.eq_ignore_ascii_case(&query_result.0)
321                && let Ok(server) = WhoIsServerValue::from_string(h)
322            {
323                query_result = Self::lookup_once(&server, text, timeout)?;
324
325                follow -= 1;
326
327                continue;
328            }
329
330            break;
331        }
332
333        Ok(query_result.1)
334    }
335
336    /// Lookup a domain or an IP, returning the raw bytes of the final WHOIS response without decoding.
337    pub fn lookup_raw(&self, options: WhoIsLookupOptions) -> Result<Vec<u8>, WhoIsError> {
338        match &options.target.0 {
339            Host::IPv4(_) | Host::IPv6(_) => {
340                let server = match &options.server {
341                    Some(server) => server,
342                    None => &self.ip,
343                };
344
345                let text = format_ip_query(&options.target.0);
346
347                Self::lookup_inner(server, &text, options.timeout, options.follow)
348            },
349            Host::Domain(domain) => {
350                let server = match &options.server {
351                    Some(server) => server,
352                    None => match self.get_server_by_tld(domain.as_str()) {
353                        Some(server) => server,
354                        None => {
355                            return Err(WhoIsError::MapError(
356                                "No whois server is known for this kind of object.",
357                            ));
358                        },
359                    },
360                };
361
362                // The domain is already ASCII (Punycode) encoded; decode it back to Unicode for servers that require it.
363                let text = server.encode_domain(domain);
364
365                Self::lookup_inner(server, text.as_ref(), options.timeout, options.follow)
366            },
367        }
368    }
369
370    /// Lookup a domain or an IP.
371    #[inline]
372    pub fn lookup(&self, options: WhoIsLookupOptions) -> Result<String, WhoIsError> {
373        Ok(decode_response(&self.lookup_raw(options)?))
374    }
375}
376
377#[cfg(feature = "tokio")]
378impl WhoIs {
379    async fn lookup_once_async(
380        server: &WhoIsServerValue,
381        text: &str,
382        timeout: Option<Duration>,
383    ) -> Result<(String, Vec<u8>), WhoIsError> {
384        let addr = server.host.to_addr_string(DEFAULT_WHOIS_HOST_PORT);
385
386        if let Some(timeout) = timeout {
387            let socket_addrs: Vec<SocketAddr> = addr.to_socket_addrs()?.collect();
388
389            if socket_addrs.is_empty() {
390                return Err(io::Error::new(
391                    io::ErrorKind::AddrNotAvailable,
392                    "the host is not resolved to any socket address",
393                )
394                .into());
395            }
396
397            let mut client = None;
398
399            for socket_addr in socket_addrs.iter().take(socket_addrs.len() - 1) {
400                if let Ok(c) =
401                    tokio::time::timeout(timeout, tokio::net::TcpStream::connect(&socket_addr))
402                        .await?
403                {
404                    client = Some(c);
405                    break;
406                }
407            }
408
409            let mut client = if let Some(client) = client {
410                client
411            } else {
412                let socket_addr = &socket_addrs[socket_addrs.len() - 1];
413                tokio::time::timeout(timeout, tokio::net::TcpStream::connect(socket_addr)).await??
414            };
415
416            if let Some(query) = &server.query {
417                tokio::time::timeout(
418                    timeout,
419                    client.write_all(query.replace("$addr", text).as_bytes()),
420                )
421                .await??;
422            } else {
423                tokio::time::timeout(
424                    timeout,
425                    client.write_all(DEFAULT_WHOIS_HOST_QUERY.replace("$addr", text).as_bytes()),
426                )
427                .await??;
428            }
429
430            tokio::time::timeout(timeout, client.flush()).await??;
431
432            let mut query_result = Vec::new();
433
434            tokio::time::timeout(timeout, client.read_to_end(&mut query_result)).await??;
435
436            // Return the host without the port so a referral host can be compared against it.
437            Ok((server.host.host.to_string(), query_result))
438        } else {
439            let mut client = tokio::net::TcpStream::connect(&addr).await?;
440
441            if let Some(query) = &server.query {
442                client.write_all(query.replace("$addr", text).as_bytes()).await?;
443            } else {
444                client
445                    .write_all(DEFAULT_WHOIS_HOST_QUERY.replace("$addr", text).as_bytes())
446                    .await?;
447            }
448
449            client.flush().await?;
450
451            let mut query_result = Vec::new();
452
453            client.read_to_end(&mut query_result).await?;
454
455            // Return the host without the port so a referral host can be compared against it.
456            Ok((server.host.host.to_string(), query_result))
457        }
458    }
459
460    async fn lookup_inner_async<'a>(
461        server: &'a WhoIsServerValue,
462        text: &'a str,
463        timeout: Option<Duration>,
464        mut follow: u16,
465    ) -> Result<Vec<u8>, WhoIsError> {
466        let mut query_result = Self::lookup_once_async(server, text, timeout).await?;
467
468        while follow > 0 {
469            let body = decode_response(&query_result.1);
470
471            if let Some(h) = extract_referral_host(&body)
472                && !h.eq_ignore_ascii_case(&query_result.0)
473                && let Ok(server) = WhoIsServerValue::from_string(h)
474            {
475                query_result = Self::lookup_once_async(&server, text, timeout).await?;
476
477                follow -= 1;
478
479                continue;
480            }
481
482            break;
483        }
484
485        Ok(query_result.1)
486    }
487
488    /// Lookup a domain or an IP, returning the raw bytes of the final WHOIS response without decoding.
489    pub async fn lookup_raw_async(
490        &self,
491        options: WhoIsLookupOptions,
492    ) -> Result<Vec<u8>, WhoIsError> {
493        match &options.target.0 {
494            Host::IPv4(_) | Host::IPv6(_) => {
495                let server = match &options.server {
496                    Some(server) => server,
497                    None => &self.ip,
498                };
499
500                let text = format_ip_query(&options.target.0);
501
502                Self::lookup_inner_async(server, &text, options.timeout, options.follow).await
503            },
504            Host::Domain(domain) => {
505                let server = match &options.server {
506                    Some(server) => server,
507                    None => match self.get_server_by_tld(domain.as_str()) {
508                        Some(server) => server,
509                        None => {
510                            return Err(WhoIsError::MapError(
511                                "No whois server is known for this kind of object.",
512                            ));
513                        },
514                    },
515                };
516
517                // The domain is already ASCII (Punycode) encoded; decode it back to Unicode for servers that require it.
518                let text = server.encode_domain(domain);
519
520                Self::lookup_inner_async(server, text.as_ref(), options.timeout, options.follow)
521                    .await
522            },
523        }
524    }
525
526    /// Lookup a domain or an IP.
527    #[inline]
528    pub async fn lookup_async(&self, options: WhoIsLookupOptions) -> Result<String, WhoIsError> {
529        Ok(decode_response(&self.lookup_raw_async(options).await?))
530    }
531}
532
533#[cfg(test)]
534mod tests {
535    use super::{decode_response, extract_referral_host};
536
537    #[test]
538    fn extract_referral_host_trims_trailing_cr() {
539        let body = "Domain: example.com\r\nReferralServer: whois://whois.arin.net\r\n";
540
541        assert_eq!(Some("whois.arin.net"), extract_referral_host(body));
542    }
543
544    #[test]
545    fn decode_response_passes_through_utf8() {
546        assert_eq!("café", decode_response("café".as_bytes()));
547    }
548
549    // "café" with `é` as the single byte 0xE9 (Windows-1252 / ISO-8859-1).
550    #[cfg(feature = "charset")]
551    #[test]
552    fn decode_response_detects_windows_1252() {
553        assert_eq!("café", decode_response(&[b'c', b'a', b'f', 0xE9]));
554    }
555
556    #[cfg(not(feature = "charset"))]
557    #[test]
558    fn decode_response_is_lossy_without_charset() {
559        assert_eq!("caf\u{FFFD}", decode_response(&[b'c', b'a', b'f', 0xE9]));
560    }
561}