1use 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 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 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#[allow(clippy::too_many_arguments)] async 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 let mut test_run = new_run_fn(vec![], addr, port);
259 if !should_skip {
260 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 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 let mut test_run = new_run_fn(vec![RunFeature::ExtsList], addr, port);
301 if !should_skip {
302 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 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 tokio::spawn(bg);
355
356 let mut dns_data = DnsData::default();
357
358 let query = client.query(Name::from_str(host)?, DNSClass::IN, RecordType::A);
360
361 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 }
392 };
393 }
394
395 let query = client.query(Name::from_str(host)?, DNSClass::IN, RecordType::AAAA);
397
398 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 }
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 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 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 let given = vec![ExtensionGroup::Gtld];
512
513 let options = TestOptions {
515 expect_groups: given,
516 ..Default::default()
517 };
518 let actual = normalize_extension_ids(&options).unwrap();
519
520 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 let groups = vec![ExtensionGroup::Nro];
540 let exts = vec!["foo1".to_string()];
541
542 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 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 let groups = vec![ExtensionGroup::Nro];
562 let exts = vec!["foo1".to_string()];
563
564 let options = TestOptions {
566 expect_extensions: exts,
567 expect_groups: groups,
568 ..Default::default()
569 };
570 let actual = normalize_extension_ids(&options);
571
572 assert!(actual.is_err())
574 }
575
576 #[test]
577 fn GIVEN_unregistered_ext_WHEN_normalize_extensions_THEN_error() {
578 let given = vec!["foo".to_string()];
580
581 let options = TestOptions {
583 expect_extensions: given,
584 ..Default::default()
585 };
586 let actual = normalize_extension_ids(&options);
587
588 assert!(actual.is_err());
590 }
591
592 #[test]
593 fn GIVEN_unregistered_ext_WHEN_allowed_THEN_no_error() {
594 let given = vec!["foo".to_string()];
596
597 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 assert!(actual.is_ok());
607 }
608}