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, prelude::*};
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/// The `WhoIs` structure stores the list of WHOIS servers in-memory.
41#[derive(Debug, Clone)]
42pub struct WhoIs {
43    map: HashMap<String, WhoIsServerValue>,
44    ip:  WhoIsServerValue,
45}
46
47impl WhoIs {
48    /// 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"`.
49    pub fn from_host<T: AsRef<str>>(host: T) -> Result<WhoIs, WhoIsError> {
50        Ok(Self {
51            map: HashMap::new(), ip: WhoIsServerValue::from_string(host)?
52        })
53    }
54
55    /// Read the list of WHOIS servers (JSON data) from a file to create a `WhoIs` instance.
56    #[inline]
57    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<WhoIs, WhoIsError> {
58        let path = path.as_ref();
59
60        let file = File::open(path)?;
61
62        let map: Map<String, Value> = serde_json::from_reader(file)?;
63
64        Self::from_inner(map)
65    }
66
67    #[cfg(feature = "tokio")]
68    /// 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.
69    #[inline]
70    pub async fn from_path_async<P: AsRef<Path>>(path: P) -> Result<WhoIs, WhoIsError> {
71        let file = tokio::fs::read(path).await?;
72
73        let map: Map<String, Value> = serde_json::from_slice(file.as_slice())?;
74
75        Self::from_inner(map)
76    }
77
78    /// Read the list of WHOIS servers (JSON data) from a string to create a `WhoIs` instance.
79    #[inline]
80    pub fn from_string<S: AsRef<str>>(string: S) -> Result<WhoIs, WhoIsError> {
81        let string = string.as_ref();
82
83        let map: Map<String, Value> = serde_json::from_str(string)?;
84
85        Self::from_inner(map)
86    }
87
88    fn from_inner(mut map: Map<String, Value>) -> Result<WhoIs, WhoIsError> {
89        let ip = match map.remove("_") {
90            Some(server) => {
91                if let Value::Object(server) = server {
92                    match server.get("ip") {
93                        Some(server) => {
94                            if server.is_null() {
95                                return Err(WhoIsError::MapError(
96                                    "`ip` in the `_` object in the server list is null.",
97                                ));
98                            }
99
100                            WhoIsServerValue::from_value(server)?
101                        },
102                        None => {
103                            return Err(WhoIsError::MapError(
104                                "Cannot find `ip` in the `_` object in the server list.",
105                            ));
106                        },
107                    }
108                } else {
109                    return Err(WhoIsError::MapError("`_` in the server list is not an object."));
110                }
111            },
112            None => return Err(WhoIsError::MapError("Cannot find `_` in the server list.")),
113        };
114
115        let mut new_map: HashMap<String, WhoIsServerValue> = HashMap::with_capacity(map.len());
116
117        for (k, v) in map {
118            if !v.is_null() {
119                let server_value = WhoIsServerValue::from_value(&v)?;
120                new_map.insert(k, server_value);
121            }
122        }
123
124        Ok(WhoIs {
125            map: new_map,
126            ip,
127        })
128    }
129}
130
131impl WhoIs {
132    /// 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).
133    pub fn can_find_server_for_tld<T: AsRef<str>, D: AsRef<str>>(
134        &mut self,
135        tld: T,
136        dns_server: D,
137    ) -> Result<bool, WhoIsError> {
138        let mut tld = tld.as_ref();
139        let dns_server = dns_server.as_ref();
140
141        let address = match dns_server.parse() {
142            Ok(address) => address,
143            Err(_error) => {
144                return Err(WhoIsError::MapError("The DNS server address is incorrect."));
145            },
146        };
147        let conn = UdpClientConnection::new(address).map_err(io::Error::other)?;
148        let client = SyncClient::new(conn);
149
150        loop {
151            if self.map.contains_key(tld) {
152                return Ok(true);
153            }
154
155            match tld.find('.') {
156                Some(index) => {
157                    tld = &tld[index + 1..];
158                },
159                None => {
160                    tld = "";
161                },
162            }
163
164            if tld.is_empty() {
165                return Ok(false);
166            }
167
168            let name =
169                Name::from_str(&format!("_nicname._tcp.{tld}.")).map_err(io::Error::other)?;
170            let response: DnsResponse =
171                client.query(&name, DNSClass::IN, RecordType::SRV).map_err(io::Error::other)?;
172            let answers: &[Record] = response.answers();
173
174            for record in answers {
175                if let Some(RData::SRV(record)) = record.data() {
176                    let target = record.target().to_string();
177                    let new_server =
178                        match WhoIsServerValue::from_string(target.trim_end_matches('.')) {
179                            Ok(new_server) => new_server,
180                            Err(_error) => continue,
181                        };
182
183                    self.map.insert(tld.to_string(), new_server);
184
185                    return Ok(true);
186                }
187            }
188        }
189    }
190
191    fn get_server_by_tld(&self, mut tld: &str) -> Option<&WhoIsServerValue> {
192        let mut server;
193
194        loop {
195            server = self.map.get(tld);
196
197            if server.is_some() {
198                break;
199            }
200
201            if tld.is_empty() {
202                break;
203            }
204
205            match tld.find('.') {
206                Some(index) => {
207                    tld = &tld[index + 1..];
208                },
209                None => {
210                    tld = "";
211                },
212            }
213        }
214
215        server
216    }
217
218    fn lookup_once(
219        server: &WhoIsServerValue,
220        text: &str,
221        timeout: Option<Duration>,
222    ) -> Result<(String, String), WhoIsError> {
223        let addr = server.host.to_addr_string(DEFAULT_WHOIS_HOST_PORT);
224
225        let mut client = if let Some(timeout) = timeout {
226            let socket_addrs: Vec<SocketAddr> = addr.to_socket_addrs()?.collect();
227
228            if socket_addrs.is_empty() {
229                return Err(io::Error::new(
230                    io::ErrorKind::AddrNotAvailable,
231                    "the host is not resolved to any socket address",
232                )
233                .into());
234            }
235
236            let mut client = None;
237
238            for socket_addr in socket_addrs.iter().take(socket_addrs.len() - 1) {
239                if let Ok(c) = TcpStream::connect_timeout(socket_addr, timeout) {
240                    client = Some(c);
241                    break;
242                }
243            }
244
245            let client = if let Some(client) = client {
246                client
247            } else {
248                let socket_addr = &socket_addrs[socket_addrs.len() - 1];
249                TcpStream::connect_timeout(socket_addr, timeout)?
250            };
251
252            client.set_read_timeout(Some(timeout))?;
253            client.set_write_timeout(Some(timeout))?;
254            client
255        } else {
256            TcpStream::connect(&addr)?
257        };
258
259        if let Some(query) = &server.query {
260            client.write_all(query.replace("$addr", text).as_bytes())?;
261        } else {
262            client.write_all(DEFAULT_WHOIS_HOST_QUERY.replace("$addr", text).as_bytes())?;
263        }
264
265        client.flush()?;
266
267        let mut query_result = String::new();
268
269        client.read_to_string(&mut query_result)?;
270
271        // Return the host without the port so a referral host can be compared against it.
272        Ok((server.host.host.to_string(), query_result))
273    }
274
275    fn lookup_inner(
276        server: &WhoIsServerValue,
277        text: &str,
278        timeout: Option<Duration>,
279        mut follow: u16,
280    ) -> Result<String, WhoIsError> {
281        let mut query_result = Self::lookup_once(server, text, timeout)?;
282
283        while follow > 0 {
284            if let Some(h) = extract_referral_host(&query_result.1)
285                && !h.eq_ignore_ascii_case(&query_result.0)
286                && let Ok(server) = WhoIsServerValue::from_string(h)
287            {
288                query_result = Self::lookup_once(&server, text, timeout)?;
289
290                follow -= 1;
291
292                continue;
293            }
294
295            break;
296        }
297
298        Ok(query_result.1)
299    }
300
301    /// Lookup a domain or an IP.
302    pub fn lookup(&self, options: WhoIsLookupOptions) -> Result<String, WhoIsError> {
303        match &options.target.0 {
304            Host::IPv4(_) | Host::IPv6(_) => {
305                let server = match &options.server {
306                    Some(server) => server,
307                    None => &self.ip,
308                };
309
310                Self::lookup_inner(
311                    server,
312                    options.target.to_uri_authority_string().as_ref(),
313                    options.timeout,
314                    options.follow,
315                )
316            },
317            Host::Domain(domain) => {
318                let server = match &options.server {
319                    Some(server) => server,
320                    None => match self.get_server_by_tld(domain.as_str()) {
321                        Some(server) => server,
322                        None => {
323                            return Err(WhoIsError::MapError(
324                                "No whois server is known for this kind of object.",
325                            ));
326                        },
327                    },
328                };
329
330                // The domain is already ASCII (Punycode) encoded; decode it back to Unicode for servers that require it.
331                let text = server.encode_domain(domain);
332
333                Self::lookup_inner(server, text.as_ref(), options.timeout, options.follow)
334            },
335        }
336    }
337}
338
339#[cfg(feature = "tokio")]
340impl WhoIs {
341    async fn lookup_once_async(
342        server: &WhoIsServerValue,
343        text: &str,
344        timeout: Option<Duration>,
345    ) -> Result<(String, String), WhoIsError> {
346        let addr = server.host.to_addr_string(DEFAULT_WHOIS_HOST_PORT);
347
348        if let Some(timeout) = timeout {
349            let socket_addrs: Vec<SocketAddr> = addr.to_socket_addrs()?.collect();
350
351            if socket_addrs.is_empty() {
352                return Err(io::Error::new(
353                    io::ErrorKind::AddrNotAvailable,
354                    "the host is not resolved to any socket address",
355                )
356                .into());
357            }
358
359            let mut client = None;
360
361            for socket_addr in socket_addrs.iter().take(socket_addrs.len() - 1) {
362                if let Ok(c) =
363                    tokio::time::timeout(timeout, tokio::net::TcpStream::connect(&socket_addr))
364                        .await?
365                {
366                    client = Some(c);
367                    break;
368                }
369            }
370
371            let mut client = if let Some(client) = client {
372                client
373            } else {
374                let socket_addr = &socket_addrs[socket_addrs.len() - 1];
375                tokio::time::timeout(timeout, tokio::net::TcpStream::connect(socket_addr)).await??
376            };
377
378            if let Some(query) = &server.query {
379                tokio::time::timeout(
380                    timeout,
381                    client.write_all(query.replace("$addr", text).as_bytes()),
382                )
383                .await??;
384            } else {
385                tokio::time::timeout(
386                    timeout,
387                    client.write_all(DEFAULT_WHOIS_HOST_QUERY.replace("$addr", text).as_bytes()),
388                )
389                .await??;
390            }
391
392            tokio::time::timeout(timeout, client.flush()).await??;
393
394            let mut query_result = String::new();
395
396            tokio::time::timeout(timeout, client.read_to_string(&mut query_result)).await??;
397
398            // Return the host without the port so a referral host can be compared against it.
399            Ok((server.host.host.to_string(), query_result))
400        } else {
401            let mut client = tokio::net::TcpStream::connect(&addr).await?;
402
403            if let Some(query) = &server.query {
404                client.write_all(query.replace("$addr", text).as_bytes()).await?;
405            } else {
406                client
407                    .write_all(DEFAULT_WHOIS_HOST_QUERY.replace("$addr", text).as_bytes())
408                    .await?;
409            }
410
411            client.flush().await?;
412
413            let mut query_result = String::new();
414
415            client.read_to_string(&mut query_result).await?;
416
417            // Return the host without the port so a referral host can be compared against it.
418            Ok((server.host.host.to_string(), query_result))
419        }
420    }
421
422    async fn lookup_inner_async<'a>(
423        server: &'a WhoIsServerValue,
424        text: &'a str,
425        timeout: Option<Duration>,
426        mut follow: u16,
427    ) -> Result<String, WhoIsError> {
428        let mut query_result = Self::lookup_once_async(server, text, timeout).await?;
429
430        while follow > 0 {
431            if let Some(h) = extract_referral_host(&query_result.1)
432                && !h.eq_ignore_ascii_case(&query_result.0)
433                && let Ok(server) = WhoIsServerValue::from_string(h)
434            {
435                query_result = Self::lookup_once_async(&server, text, timeout).await?;
436
437                follow -= 1;
438
439                continue;
440            }
441
442            break;
443        }
444
445        Ok(query_result.1)
446    }
447
448    /// Lookup a domain or an IP.
449    pub async fn lookup_async(&self, options: WhoIsLookupOptions) -> Result<String, WhoIsError> {
450        match &options.target.0 {
451            Host::IPv4(_) | Host::IPv6(_) => {
452                let server = match &options.server {
453                    Some(server) => server,
454                    None => &self.ip,
455                };
456
457                Self::lookup_inner_async(
458                    server,
459                    options.target.to_uri_authority_string().as_ref(),
460                    options.timeout,
461                    options.follow,
462                )
463                .await
464            },
465            Host::Domain(domain) => {
466                let server = match &options.server {
467                    Some(server) => server,
468                    None => match self.get_server_by_tld(domain.as_str()) {
469                        Some(server) => server,
470                        None => {
471                            return Err(WhoIsError::MapError(
472                                "No whois server is known for this kind of object.",
473                            ));
474                        },
475                    },
476                };
477
478                // The domain is already ASCII (Punycode) encoded; decode it back to Unicode for servers that require it.
479                let text = server.encode_domain(domain);
480
481                Self::lookup_inner_async(server, text.as_ref(), options.timeout, options.follow)
482                    .await
483            },
484        }
485    }
486}
487
488#[cfg(test)]
489mod tests {
490    use super::extract_referral_host;
491
492    #[test]
493    fn extract_referral_host_trims_trailing_cr() {
494        let body = "Domain: example.com\r\nReferralServer: whois://whois.arin.net\r\n";
495
496        assert_eq!(Some("whois.arin.net"), extract_referral_host(body));
497    }
498}