Skip to main content

icann_rdap_client/rdap/
qtype.rs

1//! Defines the various types of RDAP queries.
2use std::{
3    net::{IpAddr, Ipv4Addr, Ipv6Addr},
4    str::FromStr,
5    sync::LazyLock,
6};
7
8use icann_rdap_common::rdns::{ip_to_reverse_dns, reverse_dns_to_ip};
9
10use {
11    cidr::{IpCidr, Ipv4Cidr, Ipv6Cidr},
12    icann_rdap_common::{check::StringCheck, dns_types::DomainName},
13    pct_str::{PctString, UriReserved},
14    regex::Regex,
15    strum_macros::Display,
16};
17
18use crate::RdapClientError;
19
20/// Defines the various types of RDAP lookups and searches.
21#[derive(Display, Debug, Clone)]
22pub enum QueryType {
23    #[strum(serialize = "IpV4 Address Lookup")]
24    IpV4Addr(Ipv4Addr),
25
26    #[strum(serialize = "IpV6 Address Lookup")]
27    IpV6Addr(Ipv6Addr),
28
29    #[strum(serialize = "IpV4 CIDR Lookup")]
30    IpV4Cidr(Ipv4Cidr),
31
32    #[strum(serialize = "IpV6 CIDR Lookup")]
33    IpV6Cidr(Ipv6Cidr),
34
35    #[strum(serialize = "Autonomous System Number Lookup")]
36    AsNumber(u32),
37
38    #[strum(serialize = "Domain Lookup")]
39    Domain(DomainName),
40
41    #[strum(serialize = "A-Label Domain Lookup")]
42    ALabel(DomainName),
43
44    #[strum(serialize = "Reverse DNS Domain Lookup")]
45    ReverseDNs(IpAddr),
46
47    #[strum(serialize = "Entity Lookup")]
48    Entity(String),
49
50    #[strum(serialize = "Nameserver Lookup")]
51    Nameserver(DomainName),
52
53    #[strum(serialize = "Entity Name Search")]
54    EntityNameSearch(String),
55
56    #[strum(serialize = "Entity Handle Search")]
57    EntityHandleSearch(String),
58
59    #[strum(serialize = "Domain Name Search")]
60    DomainNameSearch(String),
61
62    #[strum(serialize = "Domain Nameserver Name Search")]
63    DomainNsNameSearch(String),
64
65    #[strum(serialize = "Domain Nameserver IP Address Search")]
66    DomainNsIpSearch(IpAddr),
67
68    #[strum(serialize = "Nameserver Name Search")]
69    NameserverNameSearch(String),
70
71    #[strum(serialize = "Nameserver IP Address Search")]
72    NameserverIpSearch(IpAddr),
73
74    #[strum(serialize = "Server Help Lookup")]
75    Help,
76
77    #[strum(serialize = "Explicit URL")]
78    Url(String),
79}
80
81impl QueryType {
82    pub fn query_url(&self, base_url: &str) -> Result<String, RdapClientError> {
83        let base_url = base_url.trim_end_matches('/');
84        match self {
85            Self::IpV4Addr(value) => Ok(format!(
86                "{base_url}/ip/{}",
87                PctString::encode(value.to_string().chars(), UriReserved::Path)
88            )),
89            Self::IpV6Addr(value) => Ok(format!(
90                "{base_url}/ip/{}",
91                PctString::encode(value.to_string().chars(), UriReserved::Path)
92            )),
93            Self::IpV4Cidr(value) => Ok(format!(
94                "{base_url}/ip/{}/{}",
95                PctString::encode(value.first_address().to_string().chars(), UriReserved::Path),
96                PctString::encode(
97                    value.network_length().to_string().chars(),
98                    UriReserved::Path
99                )
100            )),
101            Self::IpV6Cidr(value) => Ok(format!(
102                "{base_url}/ip/{}/{}",
103                PctString::encode(value.first_address().to_string().chars(), UriReserved::Path),
104                PctString::encode(
105                    value.network_length().to_string().chars(),
106                    UriReserved::Path
107                )
108            )),
109            Self::AsNumber(value) => Ok(format!(
110                "{base_url}/autnum/{}",
111                PctString::encode(value.to_string().chars(), UriReserved::Path)
112            )),
113            Self::Domain(value) => Ok(format!(
114                "{base_url}/domain/{}",
115                PctString::encode(value.trim_leading_dot().chars(), UriReserved::Path)
116            )),
117            Self::ReverseDNs(value) => Ok(format!(
118                "{base_url}/domain/{}",
119                PctString::encode(ip_to_reverse_dns(value).chars(), UriReserved::Path)
120            )),
121            Self::ALabel(value) => Ok(format!(
122                "{base_url}/domain/{}",
123                PctString::encode(value.to_ascii().chars(), UriReserved::Path),
124            )),
125            Self::Entity(value) => Ok(format!(
126                "{base_url}/entity/{}",
127                PctString::encode(value.chars(), UriReserved::Path)
128            )),
129            Self::Nameserver(value) => Ok(format!(
130                "{base_url}/nameserver/{}",
131                PctString::encode(value.to_ascii().chars(), UriReserved::Path)
132            )),
133            Self::EntityNameSearch(value) => search_query(value, "entities?fn", base_url),
134            Self::EntityHandleSearch(value) => search_query(value, "entities?handle", base_url),
135            Self::DomainNameSearch(value) => search_query(value, "domains?name", base_url),
136            Self::DomainNsNameSearch(value) => search_query(value, "domains?nsLdhName", base_url),
137            Self::DomainNsIpSearch(value) => {
138                search_query(&value.to_string(), "domains?nsIp", base_url)
139            }
140            Self::NameserverNameSearch(value) => search_query(value, "nameservers?name", base_url),
141            Self::NameserverIpSearch(value) => {
142                search_query(&value.to_string(), "nameservers?ip", base_url)
143            }
144            Self::Help => Ok(format!("{base_url}/help")),
145            Self::Url(url) => Ok(url.to_owned()),
146        }
147    }
148
149    pub fn domain(domain_name: &str) -> Result<Self, RdapClientError> {
150        Ok(Self::Domain(DomainName::from_str(domain_name)?))
151    }
152
153    pub fn alabel(alabel: &str) -> Result<Self, RdapClientError> {
154        Ok(Self::ALabel(DomainName::from_str(alabel)?))
155    }
156
157    pub fn rdns(domain_name: &str) -> Result<Self, RdapClientError> {
158        let value = reverse_dns_to_ip(domain_name).ok_or(RdapClientError::InvalidQueryValue)?;
159        Ok(Self::ReverseDNs(value))
160    }
161
162    pub fn rdns_ipstr(ip_address: &str) -> Result<Self, RdapClientError> {
163        let value =
164            IpAddr::from_str(ip_address).map_err(|_e| RdapClientError::InvalidQueryValue)?;
165        Ok(Self::ReverseDNs(value))
166    }
167
168    pub fn ns(nameserver: &str) -> Result<Self, RdapClientError> {
169        Ok(Self::Nameserver(DomainName::from_str(nameserver)?))
170    }
171
172    pub fn autnum(autnum: &str) -> Result<Self, RdapClientError> {
173        let value = autnum
174            .trim_start_matches(|c| -> bool { matches!(c, 'a' | 'A' | 's' | 'S') })
175            .parse::<u32>()
176            .map_err(|_e| RdapClientError::InvalidQueryValue)?;
177        Ok(Self::AsNumber(value))
178    }
179
180    pub fn ipv4(ip: &str) -> Result<Self, RdapClientError> {
181        let value = Ipv4Addr::from_str(ip).map_err(|_e| RdapClientError::InvalidQueryValue)?;
182        Ok(Self::IpV4Addr(value))
183    }
184
185    pub fn ipv6(ip: &str) -> Result<Self, RdapClientError> {
186        let value = Ipv6Addr::from_str(ip).map_err(|_e| RdapClientError::InvalidQueryValue)?;
187        Ok(Self::IpV6Addr(value))
188    }
189
190    pub fn ipv4cidr(cidr: &str) -> Result<Self, RdapClientError> {
191        let value = cidr::parsers::parse_cidr_ignore_hostbits::<IpCidr, _>(
192            cidr,
193            cidr::parsers::parse_loose_ip,
194        )
195        .map_err(|_e| RdapClientError::InvalidQueryValue)?;
196        if let IpCidr::V4(v4) = value {
197            Ok(Self::IpV4Cidr(v4))
198        } else {
199            Err(RdapClientError::AmbiguousQueryType)
200        }
201    }
202
203    pub fn ipv6cidr(cidr: &str) -> Result<Self, RdapClientError> {
204        let value = cidr::parsers::parse_cidr_ignore_hostbits::<IpCidr, _>(
205            cidr,
206            cidr::parsers::parse_loose_ip,
207        )
208        .map_err(|_e| RdapClientError::InvalidQueryValue)?;
209        if let IpCidr::V6(v6) = value {
210            Ok(Self::IpV6Cidr(v6))
211        } else {
212            Err(RdapClientError::AmbiguousQueryType)
213        }
214    }
215
216    pub fn domain_ns_ip_search(ip: &str) -> Result<Self, RdapClientError> {
217        let value = IpAddr::from_str(ip).map_err(|_e| RdapClientError::InvalidQueryValue)?;
218        Ok(Self::DomainNsIpSearch(value))
219    }
220
221    pub fn ns_ip_search(ip: &str) -> Result<Self, RdapClientError> {
222        let value = IpAddr::from_str(ip).map_err(|_e| RdapClientError::InvalidQueryValue)?;
223        Ok(Self::NameserverIpSearch(value))
224    }
225}
226
227fn search_query(value: &str, path_query: &str, base_url: &str) -> Result<String, RdapClientError> {
228    Ok(format!(
229        "{base_url}/{path_query}={}",
230        PctString::encode(value.chars(), UriReserved::Any)
231    ))
232}
233
234impl FromStr for QueryType {
235    type Err = RdapClientError;
236
237    fn from_str(s: &str) -> Result<Self, Self::Err> {
238        // if it looks like a HTTP(S) url
239        if s.starts_with("http://") || s.starts_with("https://") {
240            return Ok(Self::Url(s.to_owned()));
241        }
242
243        // if looks like an autnum
244        let autnum = s.trim_start_matches(|c| -> bool { matches!(c, 'a' | 'A' | 's' | 'S') });
245        if u32::from_str(autnum).is_ok() {
246            return Self::autnum(s);
247        }
248
249        // If it's an IP address
250        if let Ok(ip_addr) = IpAddr::from_str(s) {
251            if ip_addr.is_ipv4() {
252                return Self::ipv4(s);
253            } else {
254                return Self::ipv6(s);
255            }
256        }
257
258        // if it is a cidr
259        if let Ok(ip_cidr) = parse_cidr(s) {
260            return Ok(match ip_cidr {
261                IpCidr::V4(cidr) => Self::IpV4Cidr(cidr),
262                IpCidr::V6(cidr) => Self::IpV6Cidr(cidr),
263            });
264        }
265
266        // if it looks like a domain name
267        if is_domain_name(s) {
268            return if is_nameserver(s) {
269                Self::ns(s)
270            } else if let Some(ip) = reverse_dns_to_ip(s) {
271                Ok(Self::ReverseDNs(ip))
272            } else {
273                Self::domain(s)
274            };
275        }
276
277        // if it is just one word
278        if !s.contains(|c: char| c.is_whitespace() || matches!(c, '.' | ',' | '"')) {
279            return Ok(Self::Entity(s.to_owned()));
280        }
281
282        // The query type cannot be determined.
283        Err(RdapClientError::AmbiguousQueryType)
284    }
285}
286
287fn parse_cidr(s: &str) -> Result<IpCidr, RdapClientError> {
288    let Some((prefix, suffix)) = s.split_once('/') else {
289        return Err(RdapClientError::InvalidQueryValue);
290    };
291    if prefix.chars().all(|c: char| c.is_ascii_alphanumeric()) {
292        let cidr = cidr::parsers::parse_short_ip_address_as_cidr(prefix)
293            .map_err(|_e| RdapClientError::InvalidQueryValue)?;
294        IpCidr::new(
295            cidr.first_address(),
296            suffix
297                .parse::<u8>()
298                .map_err(|_e| RdapClientError::InvalidQueryValue)?,
299        )
300        .map_err(|_e| RdapClientError::InvalidQueryValue)
301    } else {
302        cidr::parsers::parse_cidr_ignore_hostbits::<IpCidr, _>(s, cidr::parsers::parse_loose_ip)
303            .map_err(|_e| RdapClientError::InvalidQueryValue)
304    }
305}
306
307fn is_ldh_domain(text: &str) -> bool {
308    static LDH_DOMAIN_RE: LazyLock<Regex> =
309        LazyLock::new(|| Regex::new(r"^(?i)(\.?[a-zA-Z0-9-]+)*\.[a-zA-Z0-9-]+\.?$").unwrap());
310    LDH_DOMAIN_RE.is_match(text)
311}
312
313fn is_domain_name(text: &str) -> bool {
314    text.contains('.') && text.is_unicode_domain_name()
315}
316
317fn is_nameserver(text: &str) -> bool {
318    static NS_RE: LazyLock<Regex> = LazyLock::new(|| {
319        Regex::new(r"^(?i)(ns)[a-zA-Z0-9-]*\.[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+\.?$").unwrap()
320    });
321    NS_RE.is_match(text)
322}
323
324#[cfg(test)]
325mod tests {
326    use std::str::FromStr;
327
328    use rstest::rstest;
329
330    use super::*;
331
332    #[test]
333    fn test_ipv4_query_type_from_str() {
334        // GIVEN
335        let s = "129.129.1.1";
336
337        // WHEN
338        let q = QueryType::from_str(s);
339
340        // THEN
341        assert!(matches!(q.unwrap(), QueryType::IpV4Addr(_)))
342    }
343
344    #[test]
345    fn test_ipv6_query_type_from_str() {
346        // GIVEN
347        let s = "2001::1";
348
349        // WHEN
350        let q = QueryType::from_str(s);
351
352        // THEN
353        assert!(matches!(q.unwrap(), QueryType::IpV6Addr(_)))
354    }
355
356    #[test]
357    fn test_ipv4_cidr_query_type_from_str() {
358        // GIVEN
359        let s = "129.129.1.1/8";
360
361        // WHEN
362        let q = QueryType::from_str(s);
363
364        // THEN
365        assert!(matches!(q.unwrap(), QueryType::IpV4Cidr(_)))
366    }
367
368    #[test]
369    fn test_ipv6_cidr_query_type_from_str() {
370        // GIVEN
371        let s = "2001::1/20";
372
373        // WHEN
374        let q = QueryType::from_str(s);
375
376        // THEN
377        assert!(matches!(q.unwrap(), QueryType::IpV6Cidr(_)))
378    }
379
380    #[test]
381    fn test_number_query_type_from_str() {
382        // GIVEN
383        let s = "16509";
384
385        // WHEN
386        let q = QueryType::from_str(s);
387
388        // THEN
389        assert!(matches!(q.unwrap(), QueryType::AsNumber(_)))
390    }
391
392    #[test]
393    fn test_as_followed_by_number_query_type_from_str() {
394        // GIVEN
395        let s = "as16509";
396
397        // WHEN
398        let q = QueryType::from_str(s);
399
400        // THEN
401        assert!(matches!(q.unwrap(), QueryType::AsNumber(_)))
402    }
403
404    #[rstest]
405    #[case("example.com")]
406    #[case("foo.example.com")]
407    #[case("snark.fail")]
408    #[case("ns.fail")]
409    #[case(".com")]
410    fn test_domain_name_query_type_from_str(#[case] input: &str) {
411        // GIVEN case input
412
413        // WHEN
414        let q = QueryType::from_str(input);
415
416        // THEN
417        assert!(matches!(q.unwrap(), QueryType::Domain(_)))
418    }
419
420    #[rstest]
421    #[case("ns.example.com")]
422    #[case("ns1.example.com")]
423    #[case("NS1.example.com")]
424    fn test_name_server_query_type_from_str(#[case] input: &str) {
425        // GIVEN case input
426
427        // WHEN
428        let q = QueryType::from_str(input);
429
430        // THEN
431        assert!(matches!(q.unwrap(), QueryType::Nameserver(_)))
432    }
433
434    #[test]
435    fn test_single_word_query_type_from_str() {
436        // GIVEN
437        let s = "foo";
438
439        // WHEN
440        let q = QueryType::from_str(s);
441
442        // THEN
443        let q = q.unwrap();
444        assert!(matches!(q, QueryType::Entity(_)))
445    }
446
447    #[rstest]
448    #[case("https://example.com")]
449    #[case("http://foo.example.com")]
450    fn test_url_query_type_from_str(#[case] input: &str) {
451        // GIVEN case input
452
453        // WHEN
454        let q = QueryType::from_str(input);
455
456        // THEN
457        assert!(matches!(q.unwrap(), QueryType::Url(_)))
458    }
459
460    #[rstest]
461    #[case("ns.foo_bar.com")]
462    #[case("ns.foo bar.com")]
463    fn test_bad_input_query_type_from_str(#[case] input: &str) {
464        // GIVEN case input
465
466        // WHEN
467        let q = QueryType::from_str(input);
468
469        // THEN
470        assert!(q.is_err());
471    }
472
473    #[rstest]
474    #[case("10.0.0.0/8", "10.0.0.0/8")]
475    #[case("10.0.0/8", "10.0.0.0/8")]
476    #[case("10.0/8", "10.0.0.0/8")]
477    #[case("10/8", "10.0.0.0/8")]
478    #[case("10.0.0.0/24", "10.0.0.0/24")]
479    #[case("10.0.0/24", "10.0.0.0/24")]
480    #[case("10.0/24", "10.0.0.0/24")]
481    #[case("10/24", "10.0.0.0/24")]
482    #[case("129.129.1.1/8", "129.0.0.0/8")]
483    #[case("2001::1/32", "2001::/32")]
484    fn test_cidr_parse_cidr(#[case] actual: &str, #[case] expected: &str) {
485        // GIVEN case input
486
487        // WHEN
488        let q = parse_cidr(actual);
489
490        // THEN
491        assert_eq!(q.unwrap().to_string(), expected)
492    }
493
494    #[test]
495    fn test_ipv4addr_query_url() {
496        // GIVEN ipv4 addr query
497        let q = QueryType::from_str("199.1.1.1").expect("query type");
498
499        // WHEN
500        let actual = q.query_url("https://example.com").expect("query url");
501
502        // THEN
503        assert_eq!(actual, "https://example.com/ip/199.1.1.1")
504    }
505
506    #[test]
507    fn test_ipv6addr_query_url() {
508        // GIVEN
509        let q = QueryType::from_str("2000::1").expect("query type");
510
511        // WHEN
512        let actual = q.query_url("https://example.com").expect("query url");
513
514        // THEN
515        assert_eq!(actual, "https://example.com/ip/2000%3A%3A1")
516    }
517
518    #[test]
519    fn test_ipv4cidr_query_url() {
520        // GIVEN
521        let q = QueryType::from_str("199.1.1.1/16").expect("query type");
522
523        // WHEN
524        let actual = q.query_url("https://example.com").expect("query url");
525
526        // THEN
527        assert_eq!(actual, "https://example.com/ip/199.1.0.0/16")
528    }
529
530    #[test]
531    fn test_ipv6cidr_query_url() {
532        // GIVEN
533        let q = QueryType::from_str("2000::1/16").expect("query type");
534
535        // WHEN
536        let actual = q.query_url("https://example.com").expect("query url");
537
538        // THEN
539        assert_eq!(actual, "https://example.com/ip/2000%3A%3A/16")
540    }
541
542    #[test]
543    fn test_autnum_query_url() {
544        // GIVEN
545        let q = QueryType::from_str("as16509").expect("query type");
546
547        // WHEN
548        let actual = q.query_url("https://example.com").expect("query url");
549
550        // THEN
551        assert_eq!(actual, "https://example.com/autnum/16509")
552    }
553
554    #[test]
555    fn test_domain_query_url() {
556        // GIVEN
557        let q = QueryType::from_str("example.com").expect("query type");
558
559        // WHEN
560        let actual = q.query_url("https://example.com").expect("query url");
561
562        // THEN
563        assert_eq!(actual, "https://example.com/domain/example.com")
564    }
565
566    #[test]
567    fn test_ns_query_url() {
568        // GIVEN
569        let q = QueryType::from_str("ns.example.com").expect("query type");
570
571        // WHEN
572        let actual = q.query_url("https://example.com").expect("query url");
573
574        // THEN
575        assert_eq!(actual, "https://example.com/nameserver/ns.example.com")
576    }
577
578    #[test]
579    fn test_entity_query_url() {
580        // GIVEN
581        let q = QueryType::from_str("foo").expect("query type");
582
583        // WHEN
584        let actual = q.query_url("https://example.com").expect("query url");
585
586        // THEN
587        assert_eq!(actual, "https://example.com/entity/foo")
588    }
589
590    #[test]
591    fn test_entity_name_search_query_url() {
592        // GIVEN
593        let q = QueryType::EntityNameSearch("foo".to_string());
594
595        // WHEN
596        let actual = q.query_url("https://example.com").expect("query url");
597
598        // THEN
599        assert_eq!(actual, "https://example.com/entities?fn=foo")
600    }
601
602    #[test]
603    fn test_entity_handle_search_query_url() {
604        // GIVEN
605        let q = QueryType::EntityHandleSearch("foo".to_string());
606
607        // WHEN
608        let actual = q.query_url("https://example.com").expect("query url");
609
610        // THEN
611        assert_eq!(actual, "https://example.com/entities?handle=foo")
612    }
613
614    #[test]
615    fn test_domain_name_search_query_url() {
616        // GIVEN
617        let q = QueryType::DomainNameSearch("foo".to_string());
618
619        // WHEN
620        let actual = q.query_url("https://example.com").expect("query url");
621
622        // THEN
623        assert_eq!(actual, "https://example.com/domains?name=foo")
624    }
625
626    #[test]
627    fn test_domain_ns_name_search_query_url() {
628        // GIVEN
629        let q = QueryType::DomainNsNameSearch("foo".to_string());
630
631        // WHEN
632        let actual = q.query_url("https://example.com").expect("query url");
633
634        // THEN
635        assert_eq!(actual, "https://example.com/domains?nsLdhName=foo")
636    }
637
638    #[test]
639    fn test_domain_ns_ip_search_query_url() {
640        // GIVEN
641        let q = QueryType::DomainNsIpSearch(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)));
642
643        // WHEN
644        let actual = q.query_url("https://example.com").expect("query url");
645
646        // THEN
647        assert_eq!(actual, "https://example.com/domains?nsIp=1.1.1.1")
648    }
649
650    #[test]
651    fn test_ns_name_search_query_url() {
652        // GIVEN
653        let q = QueryType::NameserverNameSearch("foo".to_string());
654
655        // WHEN
656        let actual = q.query_url("https://example.com").expect("query url");
657
658        // THEN
659        assert_eq!(actual, "https://example.com/nameservers?name=foo")
660    }
661
662    #[test]
663    fn test_ns_ip_search_query_url() {
664        // GIVEN
665        let q = QueryType::NameserverIpSearch(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)));
666
667        // WHEN
668        let actual = q.query_url("https://example.com").expect("query url");
669
670        // THEN
671        assert_eq!(actual, "https://example.com/nameservers?ip=1.1.1.1")
672    }
673}