icann_rdap_cli/rt/
exec.rs

1//! Function to execute tests.
2
3use std::{
4    net::{Ipv4Addr, Ipv6Addr},
5    str::FromStr,
6};
7
8use {
9    hickory_client::{
10        client::{AsyncClient, ClientConnection, ClientHandle},
11        rr::{DNSClass, Name, RecordType},
12        udp::UdpClientConnection,
13    },
14    icann_rdap_client::{
15        http::{create_client, create_client_with_addr, ClientConfig},
16        iana::{qtype_to_bootstrap_url, BootstrapStore},
17        rdap::{rdap_url_request, QueryType},
18        RdapClientError,
19    },
20    icann_rdap_common::response::{get_related_links, ExtensionId},
21    reqwest::{header::HeaderValue, Url},
22    thiserror::Error,
23    tracing::{debug, info},
24    url::ParseError,
25};
26
27use crate::rt::results::{RunFeature, TestRun};
28
29use super::results::{DnsData, TestResults};
30
31#[derive(Default)]
32pub struct TestOptions {
33    pub skip_v4: bool,
34    pub skip_v6: bool,
35    pub skip_origin: bool,
36    pub origin_value: String,
37    pub chase_referral: bool,
38    pub expect_extensions: Vec<String>,
39    pub expect_groups: Vec<ExtensionGroup>,
40    pub allow_unregistered_extensions: bool,
41    pub one_addr: bool,
42    pub dns_resolver: Option<String>,
43}
44
45#[derive(Clone)]
46pub enum ExtensionGroup {
47    Gtld,
48    Nro,
49    NroAsn,
50}
51
52#[derive(Debug, Error)]
53pub enum TestExecutionError {
54    #[error(transparent)]
55    RdapClient(#[from] RdapClientError),
56    #[error(transparent)]
57    UrlParseError(#[from] ParseError),
58    #[error(transparent)]
59    AddrParseError(#[from] std::net::AddrParseError),
60    #[error("No host to resolve")]
61    NoHostToResolve,
62    #[error("No rdata")]
63    NoRdata,
64    #[error("Bad rdata")]
65    BadRdata,
66    #[error(transparent)]
67    Client(#[from] reqwest::Error),
68    #[error(transparent)]
69    InvalidHeader(#[from] reqwest::header::InvalidHeaderValue),
70    #[error("Unsupporte Query Type")]
71    UnsupportedQueryType,
72    #[error("No referral to chase")]
73    NoReferralToChase,
74    #[error("Unregistered extension")]
75    UnregisteredExtension,
76}
77
78pub async fn execute_tests<BS: BootstrapStore>(
79    bs: &BS,
80    value: &QueryType,
81    options: &TestOptions,
82    client_config: &ClientConfig,
83) -> Result<TestResults, TestExecutionError> {
84    let bs_client = create_client(client_config)?;
85
86    // normalize extensions
87    let extensions = normalize_extension_ids(options)?;
88    let options = &TestOptions {
89        expect_extensions: extensions,
90        expect_groups: options.expect_groups.clone(),
91        origin_value: options.origin_value.clone(),
92        dns_resolver: options.dns_resolver.clone(),
93        ..*options
94    };
95
96    // get the query url
97    let mut query_url = match value {
98        QueryType::Help => return Err(TestExecutionError::UnsupportedQueryType),
99        QueryType::Url(url) => url.to_owned(),
100        _ => {
101            let base_url = qtype_to_bootstrap_url(&bs_client, bs, value, |reg| {
102                info!("Fetching IANA registry {} for value {value}", reg.url())
103            })
104            .await?;
105            value.query_url(&base_url)?
106        }
107    };
108    // if the URL to test is a referral
109    if options.chase_referral {
110        let client = create_client(client_config)?;
111        info!("Fetching referral from {query_url}");
112        let response_data = rdap_url_request(&query_url, &client).await?;
113        query_url = get_related_links(&response_data.rdap)
114            .first()
115            .ok_or(TestExecutionError::NoReferralToChase)?
116            .to_string();
117        info!("Referral is {query_url}");
118    }
119
120    let parsed_url = Url::parse(&query_url)?;
121    let port = parsed_url.port().unwrap_or_else(|| {
122        if parsed_url.scheme().eq("https") {
123            443
124        } else {
125            80
126        }
127    });
128    let host = parsed_url
129        .host_str()
130        .ok_or(TestExecutionError::NoHostToResolve)?;
131
132    info!("Testing {query_url}");
133    let dns_data = get_dns_records(host, options).await?;
134    let mut test_results = TestResults::new(query_url.clone(), dns_data.clone());
135
136    let mut more_runs = true;
137    for v4 in dns_data.v4_addrs {
138        // test run without origin
139        let mut test_run = TestRun::new_v4(vec![], v4, port);
140        if !options.skip_v4 && more_runs {
141            let client = create_client_with_addr(client_config, host, test_run.socket_addr)?;
142            info!("Sending request to {}", test_run.socket_addr);
143            let rdap_response = rdap_url_request(&query_url, &client).await;
144            test_run = test_run.end(rdap_response, options);
145        }
146        test_results.add_test_run(test_run);
147
148        // test run with origin
149        let mut test_run = TestRun::new_v4(vec![RunFeature::OriginHeader], v4, port);
150        if !options.skip_v4 && !options.skip_origin && more_runs {
151            let client_config = ClientConfig::from_config(client_config)
152                .origin(HeaderValue::from_str(&options.origin_value)?)
153                .build();
154            let client = create_client_with_addr(&client_config, host, test_run.socket_addr)?;
155            info!("Sending request to {}", test_run.socket_addr);
156            let rdap_response = rdap_url_request(&query_url, &client).await;
157            test_run = test_run.end(rdap_response, options);
158        }
159        test_results.add_test_run(test_run);
160        if options.one_addr {
161            more_runs = false;
162        }
163    }
164
165    let mut more_runs = true;
166    for v6 in dns_data.v6_addrs {
167        // test run without origin
168        let mut test_run = TestRun::new_v6(vec![], v6, port);
169        if !options.skip_v6 && more_runs {
170            let client = create_client_with_addr(client_config, host, test_run.socket_addr)?;
171            info!("Sending request to {}", test_run.socket_addr);
172            let rdap_response = rdap_url_request(&query_url, &client).await;
173            test_run = test_run.end(rdap_response, options);
174        }
175        test_results.add_test_run(test_run);
176
177        // test run with origin
178        let mut test_run = TestRun::new_v6(vec![RunFeature::OriginHeader], v6, port);
179        if !options.skip_v6 && !options.skip_origin && more_runs {
180            let client_config = ClientConfig::from_config(client_config)
181                .origin(HeaderValue::from_str(&options.origin_value)?)
182                .build();
183            let client = create_client_with_addr(&client_config, host, test_run.socket_addr)?;
184            info!("Sending request to {}", test_run.socket_addr);
185            let rdap_response = rdap_url_request(&query_url, &client).await;
186            test_run = test_run.end(rdap_response, options);
187        }
188        test_results.add_test_run(test_run);
189        if options.one_addr {
190            more_runs = false;
191        }
192    }
193
194    test_results.end(options);
195    info!("Testing complete.");
196    Ok(test_results)
197}
198
199async fn get_dns_records(host: &str, options: &TestOptions) -> Result<DnsData, TestExecutionError> {
200    // short circuit dns if these are ip addresses
201    if let Ok(ip4) = Ipv4Addr::from_str(host) {
202        return Ok(DnsData {
203            v4_cname: None,
204            v6_cname: None,
205            v4_addrs: vec![ip4],
206            v6_addrs: vec![],
207        });
208    } else if let Ok(ip6) = Ipv6Addr::from_str(host.trim_start_matches('[').trim_end_matches(']')) {
209        return Ok(DnsData {
210            v4_cname: None,
211            v6_cname: None,
212            v4_addrs: vec![],
213            v6_addrs: vec![ip6],
214        });
215    }
216
217    let def_dns_resolver = "8.8.8.8:53".to_string();
218    let dns_resolver = options.dns_resolver.as_ref().unwrap_or(&def_dns_resolver);
219    let conn = UdpClientConnection::new(dns_resolver.parse()?)
220        .unwrap()
221        .new_stream(None);
222    let (mut client, bg) = AsyncClient::connect(conn).await.unwrap();
223
224    // make sure to run the background task
225    tokio::spawn(bg);
226
227    let mut dns_data = DnsData::default();
228
229    // Create a query future
230    let query = client.query(Name::from_str(host).unwrap(), DNSClass::IN, RecordType::A);
231
232    // wait for its response
233    let response = query.await.unwrap();
234
235    for answer in response.answers() {
236        match answer.record_type() {
237            RecordType::CNAME => {
238                let cname = answer
239                    .data()
240                    .ok_or(TestExecutionError::NoRdata)?
241                    .clone()
242                    .into_cname()
243                    .map_err(|_e| TestExecutionError::BadRdata)?
244                    .0
245                    .to_string();
246                debug!("Found cname {cname}");
247                dns_data.v4_cname = Some(cname);
248            }
249            RecordType::A => {
250                let addr = answer
251                    .data()
252                    .ok_or(TestExecutionError::NoRdata)?
253                    .clone()
254                    .into_a()
255                    .map_err(|_e| TestExecutionError::BadRdata)?
256                    .0;
257                debug!("Found IPv4 {addr}");
258                dns_data.v4_addrs.push(addr);
259            }
260            _ => {
261                // do nothing
262            }
263        };
264    }
265
266    // Create a query future
267    let query = client.query(
268        Name::from_str(host).unwrap(),
269        DNSClass::IN,
270        RecordType::AAAA,
271    );
272
273    // wait for its response
274    let response = query.await.unwrap();
275
276    for answer in response.answers() {
277        match answer.record_type() {
278            RecordType::CNAME => {
279                let cname = answer
280                    .data()
281                    .ok_or(TestExecutionError::NoRdata)?
282                    .clone()
283                    .into_cname()
284                    .map_err(|_e| TestExecutionError::BadRdata)?
285                    .0
286                    .to_string();
287                debug!("Found cname {cname}");
288                dns_data.v6_cname = Some(cname);
289            }
290            RecordType::AAAA => {
291                let addr = answer
292                    .data()
293                    .ok_or(TestExecutionError::NoRdata)?
294                    .clone()
295                    .into_aaaa()
296                    .map_err(|_e| TestExecutionError::BadRdata)?
297                    .0;
298                debug!("Found IPv6 {addr}");
299                dns_data.v6_addrs.push(addr);
300            }
301            _ => {
302                // do nothing
303            }
304        };
305    }
306
307    Ok(dns_data)
308}
309
310fn normalize_extension_ids(options: &TestOptions) -> Result<Vec<String>, TestExecutionError> {
311    let mut retval = options.expect_extensions.clone();
312
313    // check for unregistered extensions
314    if !options.allow_unregistered_extensions {
315        for ext in &retval {
316            if ExtensionId::from_str(ext).is_err() {
317                return Err(TestExecutionError::UnregisteredExtension);
318            }
319        }
320    }
321
322    // put the groups in
323    for group in &options.expect_groups {
324        match group {
325            ExtensionGroup::Gtld => {
326                retval.push(format!(
327                    "{}|{}",
328                    ExtensionId::IcannRdapResponseProfile0,
329                    ExtensionId::IcannRdapResponseProfile1
330                ));
331                retval.push(format!(
332                    "{}|{}",
333                    ExtensionId::IcannRdapTechnicalImplementationGuide0,
334                    ExtensionId::IcannRdapTechnicalImplementationGuide1
335                ));
336            }
337            ExtensionGroup::Nro => {
338                retval.push(ExtensionId::NroRdapProfile0.to_string());
339                retval.push(ExtensionId::Cidr0.to_string());
340            }
341            ExtensionGroup::NroAsn => {
342                retval.push(ExtensionId::NroRdapProfile0.to_string());
343                retval.push(format!(
344                    "{}|{}",
345                    ExtensionId::NroRdapProfileAsnFlat0,
346                    ExtensionId::NroRdapProfileAsnHierarchical0
347                ));
348            }
349        }
350    }
351    Ok(retval)
352}
353
354#[cfg(test)]
355#[allow(non_snake_case)]
356mod tests {
357    use icann_rdap_common::response::ExtensionId;
358
359    use crate::rt::exec::{ExtensionGroup, TestOptions};
360
361    use super::normalize_extension_ids;
362
363    #[test]
364    fn GIVEN_gtld_WHEN_normalize_extensions_THEN_list_contains_gtld_ids() {
365        // GIVEN
366        let given = vec![ExtensionGroup::Gtld];
367
368        // WHEN
369        let options = TestOptions {
370            expect_groups: given,
371            ..Default::default()
372        };
373        let actual = normalize_extension_ids(&options).unwrap();
374
375        // THEN
376        let expected1 = format!(
377            "{}|{}",
378            ExtensionId::IcannRdapResponseProfile0,
379            ExtensionId::IcannRdapResponseProfile1
380        );
381        assert!(actual.contains(&expected1));
382
383        let expected2 = format!(
384            "{}|{}",
385            ExtensionId::IcannRdapTechnicalImplementationGuide0,
386            ExtensionId::IcannRdapTechnicalImplementationGuide1
387        );
388        assert!(actual.contains(&expected2));
389    }
390
391    #[test]
392    fn GIVEN_nro_and_foo_WHEN_normalize_extensions_THEN_list_contains_nro_ids_and_foo() {
393        // GIVEN
394        let groups = vec![ExtensionGroup::Nro];
395        let exts = vec!["foo1".to_string()];
396
397        // WHEN
398        let options = TestOptions {
399            allow_unregistered_extensions: true,
400            expect_extensions: exts,
401            expect_groups: groups,
402            ..Default::default()
403        };
404        let actual = normalize_extension_ids(&options).unwrap();
405        dbg!(&actual);
406
407        // THEN
408        assert!(actual.contains(&ExtensionId::NroRdapProfile0.to_string()));
409        assert!(actual.contains(&ExtensionId::Cidr0.to_string()));
410        assert!(actual.contains(&"foo1".to_string()));
411    }
412
413    #[test]
414    fn GIVEN_nro_and_foo_WHEN_unreg_disallowed_THEN_err() {
415        // GIVEN
416        let groups = vec![ExtensionGroup::Nro];
417        let exts = vec!["foo1".to_string()];
418
419        // WHEN
420        let options = TestOptions {
421            expect_extensions: exts,
422            expect_groups: groups,
423            ..Default::default()
424        };
425        let actual = normalize_extension_ids(&options);
426
427        // THEN
428        assert!(actual.is_err())
429    }
430
431    #[test]
432    fn GIVEN_unregistered_ext_WHEN_normalize_extensions_THEN_error() {
433        // GIVEN
434        let given = vec!["foo".to_string()];
435
436        // WHEN
437        let options = TestOptions {
438            expect_extensions: given,
439            ..Default::default()
440        };
441        let actual = normalize_extension_ids(&options);
442
443        // THEN
444        assert!(actual.is_err());
445    }
446
447    #[test]
448    fn GIVEN_unregistered_ext_WHEN_allowed_THEN_no_error() {
449        // GIVEN
450        let given = vec!["foo".to_string()];
451
452        // WHEN
453        let options = TestOptions {
454            expect_extensions: given,
455            allow_unregistered_extensions: true,
456            ..Default::default()
457        };
458        let actual = normalize_extension_ids(&options);
459
460        // THEN
461        assert!(actual.is_ok());
462    }
463}