Skip to main content

icann_rdap_cli/rt/
exec.rs

1//! Function to execute tests.
2
3use std::{
4    collections::HashSet,
5    net::{Ipv4Addr, Ipv6Addr},
6    str::FromStr,
7};
8
9use icann_rdap_client::rdap::ResponseData;
10use icann_rdap_common::{
11    httpdata::HttpData,
12    prelude::{get_relationship_links, RdapResponse},
13};
14
15use {
16    hickory_client::{
17        client::{AsyncClient, ClientConnection, ClientHandle},
18        error::ClientError,
19        proto::error::ProtoError,
20        rr::{DNSClass, Name, RecordType},
21        udp::UdpClientConnection,
22    },
23    icann_rdap_client::{
24        http::{create_client, create_client_with_addr, ClientConfig},
25        iana::{qtype_to_bootstrap_url, BootstrapStore},
26        rdap::{rdap_url_request, QueryType},
27        RdapClientError,
28    },
29    icann_rdap_common::response::ExtensionId,
30    reqwest::{header::HeaderValue, Url},
31    thiserror::Error,
32    tracing::{debug, info},
33    url::ParseError,
34};
35
36use crate::{
37    args::target::LinkParams,
38    rt::results::{HttpResults, RunFeature, TestRun},
39};
40
41use super::results::{DnsData, StringResult, TestResults};
42
43pub struct TestOptions {
44    pub test_type: TestType,
45    pub expect_extensions: Vec<String>,
46    pub expect_groups: Vec<ExtensionGroup>,
47    pub allow_unregistered_extensions: bool,
48}
49
50impl Default for TestOptions {
51    fn default() -> Self {
52        Self {
53            test_type: TestType::String {
54                field1: StringTestOptions {
55                    json: String::default(),
56                },
57            },
58            expect_extensions: vec![],
59            expect_groups: vec![],
60            allow_unregistered_extensions: false,
61        }
62    }
63}
64
65#[derive(Clone)]
66pub enum TestType {
67    Http(Box<HttpTestOptions>),
68    String { field1: StringTestOptions },
69}
70
71#[derive(Clone)]
72pub struct HttpTestOptions {
73    pub value: QueryType,
74    pub client_config: ClientConfig,
75    pub skip_v4: bool,
76    pub skip_v6: bool,
77    pub skip_origin: bool,
78    pub origin_value: String,
79    pub one_addr: bool,
80    pub dns_resolver: Option<String>,
81    pub link_params: LinkParams,
82}
83
84#[derive(Clone)]
85pub struct StringTestOptions {
86    pub json: String,
87}
88
89#[derive(Clone)]
90pub enum ExtensionGroup {
91    Gtld,
92    Nro,
93    NroAsn,
94}
95
96#[derive(Debug, Error)]
97pub enum TestExecutionError {
98    #[error(transparent)]
99    RdapClient(#[from] RdapClientError),
100    #[error(transparent)]
101    UrlParseError(#[from] ParseError),
102    #[error(transparent)]
103    AddrParseError(#[from] std::net::AddrParseError),
104    #[error("No host to resolve")]
105    NoHostToResolve,
106    #[error("No rdata")]
107    NoRdata,
108    #[error("Bad rdata")]
109    BadRdata,
110    #[error(transparent)]
111    Client(#[from] reqwest::Error),
112    #[error(transparent)]
113    InvalidHeader(#[from] reqwest::header::InvalidHeaderValue),
114    #[error("Unsupported Query Type")]
115    UnsupportedQueryType,
116    #[error("No referral to chase")]
117    NoReferralToChase,
118    #[error("Unregistered extension")]
119    UnregisteredExtension,
120    #[error("Hickory Client Error: {0}")]
121    HickoryClient(#[from] ClientError),
122    #[error("Hickory Proto Error: {0}")]
123    HickoryProto(#[from] ProtoError),
124}
125
126pub async fn execute_tests<BS: BootstrapStore>(
127    bs: &BS,
128    options: &TestOptions,
129) -> Result<TestResults, TestExecutionError> {
130    match &options.test_type {
131        TestType::Http(http_options) => execute_http_tests(bs, http_options, options).await,
132        TestType::String {
133            field1: string_options,
134        } => execute_string_test(string_options, options),
135    }
136}
137
138pub async fn execute_http_tests<BS: BootstrapStore>(
139    bs: &BS,
140    http_options: &HttpTestOptions,
141    options: &TestOptions,
142) -> Result<TestResults, TestExecutionError> {
143    let bs_client = create_client(&http_options.client_config)?;
144
145    // normalize extensions
146    let extensions = normalize_extension_ids(options)?;
147    let options = &TestOptions {
148        test_type: options.test_type.clone(),
149        expect_extensions: extensions,
150        expect_groups: options.expect_groups.clone(),
151        ..*options
152    };
153
154    // get the query url
155    let mut query_url = match &http_options.value {
156        QueryType::Help => return Err(TestExecutionError::UnsupportedQueryType),
157        QueryType::Url(url) => url.to_owned(),
158        _ => {
159            let base_url = qtype_to_bootstrap_url(&bs_client, bs, &http_options.value, |reg| {
160                info!(
161                    "Fetching IANA registry {} for value {}",
162                    reg.url(),
163                    http_options.value
164                )
165            })
166            .await?;
167            http_options.value.query_url(&base_url)?
168        }
169    };
170
171    for req_number in 1..http_options.link_params.max_link_depth {
172        let client = create_client(&http_options.client_config)?;
173        info!("Fetching referral from {query_url}");
174        let response_data = rdap_url_request(&query_url, &client).await?;
175        if let Some(url) =
176            get_relationship_links(&http_options.link_params.link_targets, &response_data.rdap)
177                .first()
178        {
179            info!("Found referral to {url}");
180            query_url = url.to_string();
181        } else if req_number < http_options.link_params.min_link_depth {
182            return Err(TestExecutionError::NoReferralToChase);
183        } else {
184            break;
185        }
186    }
187
188    let parsed_url = Url::parse(&query_url)?;
189    let port = parsed_url.port().unwrap_or_else(|| {
190        if parsed_url.scheme().eq("https") {
191            443
192        } else {
193            80
194        }
195    });
196    let host = parsed_url
197        .host_str()
198        .ok_or(TestExecutionError::NoHostToResolve)?;
199
200    info!("Testing {query_url}");
201    let dns_data = get_dns_records(host, http_options).await?;
202    let mut http_results = HttpResults::new(query_url.clone(), dns_data.clone());
203
204    execute_http_tests_for_family(
205        dns_data.v4_addrs,
206        port,
207        TestRun::new_v4,
208        http_options.skip_v4,
209        &query_url,
210        host,
211        http_options,
212        options,
213        &mut http_results,
214    )
215    .await?;
216
217    execute_http_tests_for_family(
218        dns_data.v6_addrs,
219        port,
220        TestRun::new_v6,
221        http_options.skip_v6,
222        &query_url,
223        host,
224        http_options,
225        options,
226        &mut http_results,
227    )
228    .await?;
229
230    http_results.end(options);
231    info!("Testing complete.");
232    Ok(TestResults::Http(http_results))
233}
234
235// Helper function for a family of addresses (v4 or v6)
236#[allow(clippy::too_many_arguments)] //allowed here because all but one parameter are diff types
237async fn execute_http_tests_for_family<A>(
238    addrs: Vec<A>,
239    port: u16,
240    new_run_fn: impl Fn(Vec<RunFeature>, A, u16) -> TestRun,
241    should_skip: bool,
242    query_url: &str,
243    host: &str,
244    http_options: &HttpTestOptions,
245    options: &TestOptions,
246    http_results: &mut HttpResults,
247) -> Result<(), TestExecutionError>
248where
249    A: Copy,
250{
251    let mut more_runs = true;
252    for addr in addrs {
253        if !more_runs {
254            break;
255        }
256
257        // test run without origin
258        let mut test_run = new_run_fn(vec![], addr, port);
259        if !should_skip {
260            // the non-featured run needs to turn off the exts_list by passing in an empty set.
261            let client_config = ClientConfig::from_config(&http_options.client_config)
262                .exts_list(HashSet::default())
263                .build();
264            let client = create_client_with_addr(
265                &client_config,
266                host,
267                test_run.socket_addr.expect("socket"),
268            )?;
269            info!(
270                "Sending request to {}",
271                test_run.socket_addr.expect("socket")
272            );
273            let rdap_response = rdap_url_request(query_url, &client).await;
274            test_run = test_run.end(rdap_response, options);
275        }
276        http_results.add_test_run(test_run);
277
278        // test run with origin
279        let mut test_run = new_run_fn(vec![RunFeature::OriginHeader], addr, port);
280        if !should_skip && !http_options.skip_origin {
281            let client_config = ClientConfig::from_config(&http_options.client_config)
282                .origin(HeaderValue::from_str(&http_options.origin_value)?)
283                .exts_list(HashSet::default())
284                .build();
285            let client = create_client_with_addr(
286                &client_config,
287                host,
288                test_run.socket_addr.expect("socket"),
289            )?;
290            info!(
291                "Sending request to {}",
292                test_run.socket_addr.expect("socket")
293            );
294            let rdap_response = rdap_url_request(query_url, &client).await;
295            test_run = test_run.end(rdap_response, options);
296        }
297        http_results.add_test_run(test_run);
298
299        // test run with exts_list
300        let mut test_run = new_run_fn(vec![RunFeature::ExtsList], addr, port);
301        if !should_skip {
302            // exts_list is the default in the client config
303            let client = create_client_with_addr(
304                &http_options.client_config,
305                host,
306                test_run.socket_addr.expect("socket"),
307            )?;
308            info!(
309                "Sending request to {}",
310                test_run.socket_addr.expect("socket")
311            );
312            let rdap_response = rdap_url_request(query_url, &client).await;
313            test_run = test_run.end(rdap_response, options);
314        }
315        http_results.add_test_run(test_run);
316
317        if http_options.one_addr {
318            more_runs = false;
319        }
320    }
321    Ok(())
322}
323
324async fn get_dns_records(
325    host: &str,
326    http_options: &HttpTestOptions,
327) -> Result<DnsData, TestExecutionError> {
328    // short circuit dns if these are ip addresses
329    if let Ok(ip4) = Ipv4Addr::from_str(host) {
330        return Ok(DnsData {
331            v4_cname: None,
332            v6_cname: None,
333            v4_addrs: vec![ip4],
334            v6_addrs: vec![],
335        });
336    } else if let Ok(ip6) = Ipv6Addr::from_str(host.trim_start_matches('[').trim_end_matches(']')) {
337        return Ok(DnsData {
338            v4_cname: None,
339            v6_cname: None,
340            v4_addrs: vec![],
341            v6_addrs: vec![ip6],
342        });
343    }
344
345    let def_dns_resolver = "8.8.8.8:53".to_string();
346    let dns_resolver = http_options
347        .dns_resolver
348        .as_ref()
349        .unwrap_or(&def_dns_resolver);
350    let conn = UdpClientConnection::new(dns_resolver.parse()?)?.new_stream(None);
351    let (mut client, bg) = AsyncClient::connect(conn).await?;
352
353    // make sure to run the background task
354    tokio::spawn(bg);
355
356    let mut dns_data = DnsData::default();
357
358    // Create a query future
359    let query = client.query(Name::from_str(host)?, DNSClass::IN, RecordType::A);
360
361    // wait for its response
362    let response = query.await?;
363
364    for answer in response.answers() {
365        match answer.record_type() {
366            RecordType::CNAME => {
367                let cname = answer
368                    .data()
369                    .ok_or(TestExecutionError::NoRdata)?
370                    .clone()
371                    .into_cname()
372                    .map_err(|_e| TestExecutionError::BadRdata)?
373                    .0
374                    .to_string();
375                debug!("Found cname {cname}");
376                dns_data.v4_cname = Some(cname);
377            }
378            RecordType::A => {
379                let addr = answer
380                    .data()
381                    .ok_or(TestExecutionError::NoRdata)?
382                    .clone()
383                    .into_a()
384                    .map_err(|_e| TestExecutionError::BadRdata)?
385                    .0;
386                debug!("Found IPv4 {addr}");
387                dns_data.v4_addrs.push(addr);
388            }
389            _ => {
390                // do nothing
391            }
392        };
393    }
394
395    // Create a query future
396    let query = client.query(Name::from_str(host)?, DNSClass::IN, RecordType::AAAA);
397
398    // wait for its response
399    let response = query.await?;
400
401    for answer in response.answers() {
402        match answer.record_type() {
403            RecordType::CNAME => {
404                let cname = answer
405                    .data()
406                    .ok_or(TestExecutionError::NoRdata)?
407                    .clone()
408                    .into_cname()
409                    .map_err(|_e| TestExecutionError::BadRdata)?
410                    .0
411                    .to_string();
412                debug!("Found cname {cname}");
413                dns_data.v6_cname = Some(cname);
414            }
415            RecordType::AAAA => {
416                let addr = answer
417                    .data()
418                    .ok_or(TestExecutionError::NoRdata)?
419                    .clone()
420                    .into_aaaa()
421                    .map_err(|_e| TestExecutionError::BadRdata)?
422                    .0;
423                debug!("Found IPv6 {addr}");
424                dns_data.v6_addrs.push(addr);
425            }
426            _ => {
427                // do nothing
428            }
429        };
430    }
431
432    Ok(dns_data)
433}
434
435pub fn execute_string_test(
436    string_options: &StringTestOptions,
437    options: &TestOptions,
438) -> Result<TestResults, TestExecutionError> {
439    let mut test_run = TestRun::new(vec![]);
440    let rdap =
441        serde_json::from_str::<RdapResponse>(&string_options.json).map_err(RdapClientError::Json);
442    let res_data = match rdap {
443        Ok(rdap) => Ok(ResponseData {
444            rdap_type: "unknown".to_string(),
445            rdap,
446            http_data: HttpData::now().scheme("file").host("localhost").build(),
447        }),
448        Err(e) => Err(e),
449    };
450    test_run = test_run.end(res_data, options);
451    let string_result = StringResult::new(test_run);
452    Ok(TestResults::String(Box::new(string_result)))
453}
454
455fn normalize_extension_ids(options: &TestOptions) -> Result<Vec<String>, TestExecutionError> {
456    let mut retval = options.expect_extensions.clone();
457
458    // check for unregistered extensions
459    if !options.allow_unregistered_extensions {
460        for ext in &retval {
461            if ExtensionId::from_str(ext).is_err() {
462                return Err(TestExecutionError::UnregisteredExtension);
463            }
464        }
465    }
466
467    // put the groups in
468    for group in &options.expect_groups {
469        match group {
470            ExtensionGroup::Gtld => {
471                retval.push(format!(
472                    "{}|{}",
473                    ExtensionId::IcannRdapResponseProfile0,
474                    ExtensionId::IcannRdapResponseProfile1
475                ));
476                retval.push(format!(
477                    "{}|{}",
478                    ExtensionId::IcannRdapTechnicalImplementationGuide0,
479                    ExtensionId::IcannRdapTechnicalImplementationGuide1
480                ));
481            }
482            ExtensionGroup::Nro => {
483                retval.push(ExtensionId::NroRdapProfile0.to_string());
484                retval.push(ExtensionId::Cidr0.to_string());
485            }
486            ExtensionGroup::NroAsn => {
487                retval.push(ExtensionId::NroRdapProfile0.to_string());
488                retval.push(format!(
489                    "{}|{}",
490                    ExtensionId::NroRdapProfileAsnFlat0,
491                    ExtensionId::NroRdapProfileAsnHierarchical0
492                ));
493            }
494        }
495    }
496    Ok(retval)
497}
498
499#[cfg(test)]
500#[allow(non_snake_case)]
501mod tests {
502    use icann_rdap_common::response::ExtensionId;
503
504    use crate::rt::exec::{ExtensionGroup, TestOptions};
505
506    use super::normalize_extension_ids;
507
508    #[test]
509    fn GIVEN_gtld_WHEN_normalize_extensions_THEN_list_contains_gtld_ids() {
510        // GIVEN
511        let given = vec![ExtensionGroup::Gtld];
512
513        // WHEN
514        let options = TestOptions {
515            expect_groups: given,
516            ..Default::default()
517        };
518        let actual = normalize_extension_ids(&options).unwrap();
519
520        // THEN
521        let expected1 = format!(
522            "{}|{}",
523            ExtensionId::IcannRdapResponseProfile0,
524            ExtensionId::IcannRdapResponseProfile1
525        );
526        assert!(actual.contains(&expected1));
527
528        let expected2 = format!(
529            "{}|{}",
530            ExtensionId::IcannRdapTechnicalImplementationGuide0,
531            ExtensionId::IcannRdapTechnicalImplementationGuide1
532        );
533        assert!(actual.contains(&expected2));
534    }
535
536    #[test]
537    fn GIVEN_nro_and_foo_WHEN_normalize_extensions_THEN_list_contains_nro_ids_and_foo() {
538        // GIVEN
539        let groups = vec![ExtensionGroup::Nro];
540        let exts = vec!["foo1".to_string()];
541
542        // WHEN
543        let options = TestOptions {
544            allow_unregistered_extensions: true,
545            expect_extensions: exts,
546            expect_groups: groups,
547            ..Default::default()
548        };
549        let actual = normalize_extension_ids(&options).unwrap();
550        dbg!(&actual);
551
552        // THEN
553        assert!(actual.contains(&ExtensionId::NroRdapProfile0.to_string()));
554        assert!(actual.contains(&ExtensionId::Cidr0.to_string()));
555        assert!(actual.contains(&"foo1".to_string()));
556    }
557
558    #[test]
559    fn GIVEN_nro_and_foo_WHEN_unreg_disallowed_THEN_err() {
560        // GIVEN
561        let groups = vec![ExtensionGroup::Nro];
562        let exts = vec!["foo1".to_string()];
563
564        // WHEN
565        let options = TestOptions {
566            expect_extensions: exts,
567            expect_groups: groups,
568            ..Default::default()
569        };
570        let actual = normalize_extension_ids(&options);
571
572        // THEN
573        assert!(actual.is_err())
574    }
575
576    #[test]
577    fn GIVEN_unregistered_ext_WHEN_normalize_extensions_THEN_error() {
578        // GIVEN
579        let given = vec!["foo".to_string()];
580
581        // WHEN
582        let options = TestOptions {
583            expect_extensions: given,
584            ..Default::default()
585        };
586        let actual = normalize_extension_ids(&options);
587
588        // THEN
589        assert!(actual.is_err());
590    }
591
592    #[test]
593    fn GIVEN_unregistered_ext_WHEN_allowed_THEN_no_error() {
594        // GIVEN
595        let given = vec!["foo".to_string()];
596
597        // WHEN
598        let options = TestOptions {
599            expect_extensions: given,
600            allow_unregistered_extensions: true,
601            ..Default::default()
602        };
603        let actual = normalize_extension_ids(&options);
604
605        // THEN
606        assert!(actual.is_ok());
607    }
608}