1use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
2
3use chrono::{DateTime, Utc};
5use icann_rdap_common::check::{
6 process::{do_check_processing, get_summaries},
7 CheckSummary,
8};
9use {
10 icann_rdap_client::{
11 md::{string::StringUtil, table::MultiPartTable, MdOptions},
12 rdap::ResponseData,
13 RdapClientError,
14 },
15 icann_rdap_common::{
16 check::{Check, CheckClass, CheckItem, Checks},
17 response::ExtensionId,
18 },
19 reqwest::StatusCode,
20 serde::Serialize,
21 strum_macros::Display,
22};
23
24use super::exec::TestOptions;
25
26#[derive(Debug, Serialize, Clone)]
27pub enum TestResults {
28 Http(HttpResults),
29 String(Box<StringResult>),
30}
31
32impl TestResults {
33 pub fn to_md(&self, options: &MdOptions) -> String {
34 match self {
35 TestResults::Http(http_results) => http_results.to_md(options),
36 TestResults::String(string_result) => string_result.to_md(options),
37 }
38 }
39
40 pub fn execution_errors(&self) -> bool {
41 match self {
42 TestResults::Http(http_results) => http_results.execution_errors(),
43 TestResults::String(string_result) => string_result.execution_errors(),
44 }
45 }
46
47 pub fn are_there_checks(&self, classes: Vec<CheckClass>) -> bool {
48 match self {
49 TestResults::Http(http_results) => http_results.are_there_checks(classes),
50 TestResults::String(string_result) => string_result.are_there_checks(classes),
51 }
52 }
53
54 pub fn filter_test_results(&self, classes: Vec<CheckClass>) -> TestResults {
55 match self {
56 TestResults::Http(http_results) => {
57 TestResults::Http(http_results.clone().filter_test_results(&classes))
58 }
59 TestResults::String(string_result) => TestResults::String(Box::new(
60 string_result.clone().filter_test_results(&classes),
61 )),
62 }
63 }
64}
65
66#[derive(Debug, Serialize, Clone)]
67pub struct HttpResults {
68 pub query_url: String,
69 pub start_time: DateTime<Utc>,
70 pub end_time: Option<DateTime<Utc>>,
71 pub dns_data: DnsData,
72 pub service_checks: Vec<CheckItem>,
73 pub test_runs: Vec<TestRun>,
74}
75
76#[derive(Debug, Serialize, Clone)]
77pub struct StringResult {
78 pub test_run: Option<TestRun>,
79}
80
81impl HttpResults {
82 pub fn new(query_url: String, dns_data: DnsData) -> Self {
83 Self {
84 query_url,
85 dns_data,
86 service_checks: vec![],
87 test_runs: vec![],
88 start_time: Utc::now(),
89 end_time: None,
90 }
91 }
92
93 pub fn end(&mut self, options: &TestOptions) {
94 self.end_time = Some(Utc::now());
95
96 if self.dns_data.v4_cname.is_some() && self.dns_data.v4_addrs.is_empty() {
98 self.service_checks
99 .push(Check::CnameWithoutARecords.check_item());
100 }
101 if self.dns_data.v6_cname.is_some() && self.dns_data.v6_addrs.is_empty() {
102 self.service_checks
103 .push(Check::CnameWithoutAAAARecords.check_item());
104 }
105 if self.dns_data.v4_addrs.is_empty() {
106 self.service_checks.push(Check::NoARecords.check_item());
107 }
108 if self.dns_data.v6_addrs.is_empty() {
109 self.service_checks.push(Check::NoAAAARecords.check_item());
110
111 let tig0 = ExtensionId::IcannRdapTechnicalImplementationGuide0.to_string();
113 let tig1 = ExtensionId::IcannRdapTechnicalImplementationGuide1.to_string();
114 let both_tigs = format!("{tig0}|{tig1}");
115 if options.expect_extensions.contains(&tig0)
116 || options.expect_extensions.contains(&tig1)
117 || options.expect_extensions.contains(&both_tigs)
118 {
119 self.service_checks
120 .push(Check::Ipv6SupportRequiredByGtldProfile.check_item())
121 }
122 }
123 }
124
125 pub fn add_test_run(&mut self, test_run: TestRun) {
126 self.test_runs.push(test_run);
127 }
128
129 pub fn to_md(&self, options: &MdOptions) -> String {
130 let mut md = String::new();
131
132 md.push_str(&format!(
134 "\n{}\n",
135 self.query_url.to_owned().to_header(1, options)
136 ));
137
138 let mut table = MultiPartTable::new();
140
141 table = table.multi_raw(vec![
143 "Start Time".to_inline(options),
144 "End Time".to_inline(options),
145 "Duration".to_inline(options),
146 "Tested".to_inline(options),
147 ]);
148 let (end_time_s, duration_s) = if let Some(end_time) = self.end_time {
149 (
150 format_date_time(end_time),
151 format!("{} s", (end_time - self.start_time).num_seconds()),
152 )
153 } else {
154 ("FATAL".to_em(options), "N/A".to_string())
155 };
156 let tested = self
157 .test_runs
158 .iter()
159 .filter(|r| matches!(r.outcome, RunOutcome::Tested))
160 .count();
161 table = table.multi_raw(vec![
162 format_date_time(self.start_time),
163 end_time_s,
164 duration_s,
165 format!("{tested} of {}", self.test_runs.len()),
166 ]);
167
168 table = table.multi_raw(vec![
170 "DNS Query".to_inline(options),
171 "DNS Answer".to_inline(options),
172 ]);
173 let v4_cname = if let Some(ref cname) = self.dns_data.v4_cname {
174 cname.to_owned()
175 } else {
176 format!("{} A records", self.dns_data.v4_addrs.len())
177 };
178 table = table.multi_raw(vec!["A (v4)".to_string(), v4_cname]);
179 let v6_cname = if let Some(ref cname) = self.dns_data.v6_cname {
180 cname.to_owned()
181 } else {
182 format!("{} AAAA records", self.dns_data.v6_addrs.len())
183 };
184 table = table.multi_raw(vec!["AAAA (v6)".to_string(), v6_cname]);
185
186 table = table.multi_raw(vec![
188 "Address".to_inline(options),
189 "Attributes".to_inline(options),
190 "Duration".to_inline(options),
191 "Outcome".to_inline(options),
192 ]);
193 for test_run in &self.test_runs {
194 table = test_run.add_summary(table, options);
195 }
196 md.push_str(&table.to_md_table(options));
197
198 md.push('\n');
199
200 if !self.service_checks.is_empty() {
202 md.push_str(&"Service Checks".to_string().to_header(1, options));
203 let mut table = MultiPartTable::new();
204
205 table = table.multi_raw(vec!["Message".to_inline(options)]);
206 for c in &self.service_checks {
207 let message = check_item_md(c, options);
208 table = table.multi_raw(vec![message]);
209 }
210 md.push_str(&table.to_md_table(options));
211 md.push('\n');
212 }
213
214 for run in &self.test_runs {
216 md.push_str(&run.to_md(options));
217 }
218 md
219 }
220
221 pub fn execution_errors(&self) -> bool {
222 self.test_runs
223 .iter()
224 .filter(|r| !matches!(r.outcome, RunOutcome::Tested | RunOutcome::Skipped))
225 .count()
226 != 0
227 }
228
229 pub fn are_there_checks(&self, classes: Vec<CheckClass>) -> bool {
230 let run_count = self
232 .test_runs
233 .iter()
234 .filter(|r| {
235 r.summaries
236 .as_deref()
237 .unwrap_or_default()
238 .iter()
239 .any(|s| classes.contains(&s.item.check_class))
240 })
241 .count();
242 let service_count = self
244 .service_checks
245 .iter()
246 .filter(|c| classes.contains(&c.check_class))
247 .count();
248 run_count + service_count != 0
249 }
250
251 pub fn filter_test_results(self, classes: &[CheckClass]) -> Self {
252 let filtered_service_checks: Vec<CheckItem> = self
254 .service_checks
255 .into_iter()
256 .filter(|c| classes.contains(&c.check_class))
257 .collect();
258
259 let mut filtered_test_runs = vec![];
261 for mut test_run in self.test_runs {
262 let filtered_summary: Vec<CheckSummary> = test_run
263 .summaries
264 .unwrap_or_default()
265 .into_iter()
266 .filter(|s| classes.contains(&s.item.check_class))
267 .collect();
268 test_run.summaries = Some(filtered_summary);
269 filtered_test_runs.push(test_run);
270 }
271
272 Self {
274 service_checks: filtered_service_checks,
275 test_runs: filtered_test_runs,
276 ..self
277 }
278 }
279}
280
281impl StringResult {
282 pub fn new(test_run: TestRun) -> Self {
283 Self {
284 test_run: Some(test_run),
285 }
286 }
287
288 pub fn to_md(&self, options: &MdOptions) -> String {
289 let mut md = String::new();
290
291 if let Some(test_run) = &self.test_run {
292 md.push_str(&test_run.to_md(options));
293 }
294 md
295 }
296
297 pub fn execution_errors(&self) -> bool {
298 self.test_run
299 .iter()
300 .filter(|r| !matches!(r.outcome, RunOutcome::Tested | RunOutcome::Skipped))
301 .count()
302 != 0
303 }
304
305 pub fn are_there_checks(&self, classes: Vec<CheckClass>) -> bool {
306 let run_count = self
308 .test_run
309 .iter()
310 .filter(|r| {
311 r.summaries
312 .as_deref()
313 .unwrap_or_default()
314 .iter()
315 .any(|s| classes.contains(&s.item.check_class))
316 })
317 .count();
318 run_count != 0
319 }
320
321 pub fn filter_test_results(self, classes: &[CheckClass]) -> Self {
322 let mut filtered_test_run = None;
324 if let Some(mut test_run) = self.test_run {
325 let filtered_summary: Vec<CheckSummary> = test_run
326 .summaries
327 .unwrap_or_default()
328 .into_iter()
329 .filter(|s| classes.contains(&s.item.check_class))
330 .collect();
331 test_run.summaries = Some(filtered_summary);
332 filtered_test_run = Some(test_run);
333 }
334 Self {
336 test_run: filtered_test_run,
337 }
338 }
339}
340
341#[derive(Debug, Serialize, Clone, Default)]
342pub struct DnsData {
343 pub v4_cname: Option<String>,
344 pub v6_cname: Option<String>,
345 pub v4_addrs: Vec<Ipv4Addr>,
346 pub v6_addrs: Vec<Ipv6Addr>,
347}
348
349#[derive(Debug, Serialize, Display, Clone)]
350#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
351pub enum RunOutcome {
352 Tested,
353 NetworkError,
354 HttpProtocolError,
355 HttpConnectError,
356 HttpRedirectResponse,
357 HttpTimeoutError,
358 HttpNon200Error,
359 HttpTooManyRequestsError,
360 HttpNotFoundError,
361 HttpBadRequestError,
362 HttpUnauthorizedError,
363 HttpForbiddenError,
364 JsonError,
365 RdapDataError,
366 InternalError,
367 Skipped,
368}
369
370#[derive(Debug, Serialize, Display, Clone)]
371#[strum(serialize_all = "snake_case")]
372pub enum RunFeature {
373 OriginHeader,
374 ExtsList,
375}
376
377impl RunOutcome {
378 pub fn to_md(&self, options: &MdOptions) -> String {
379 match self {
380 Self::Tested => self.to_bold(options),
381 Self::Skipped => self.to_string(),
382 _ => self.to_em(options),
383 }
384 }
385}
386
387#[derive(Debug, Serialize, Clone)]
388pub struct TestRun {
389 pub features: Vec<RunFeature>,
390 pub socket_addr: Option<SocketAddr>,
391 pub start_time: DateTime<Utc>,
392 pub end_time: Option<DateTime<Utc>>,
393 pub response_data: Option<ResponseData>,
394 pub outcome: RunOutcome,
395 pub summaries: Option<Vec<CheckSummary>>,
396}
397
398impl TestRun {
399 pub fn new(features: Vec<RunFeature>) -> Self {
400 Self {
401 features,
402 start_time: Utc::now(),
403 socket_addr: None,
404 end_time: None,
405 response_data: None,
406 outcome: RunOutcome::Skipped,
407 summaries: None,
408 }
409 }
410
411 pub fn new_ip(features: Vec<RunFeature>, socket_addr: SocketAddr) -> Self {
412 Self {
413 features,
414 start_time: Utc::now(),
415 socket_addr: Some(socket_addr),
416 end_time: None,
417 response_data: None,
418 outcome: RunOutcome::Skipped,
419 summaries: None,
420 }
421 }
422
423 pub fn new_v4(features: Vec<RunFeature>, ipv4: Ipv4Addr, port: u16) -> Self {
424 Self::new_ip(features, SocketAddr::new(IpAddr::V4(ipv4), port))
425 }
426
427 pub fn new_v6(features: Vec<RunFeature>, ipv6: Ipv6Addr, port: u16) -> Self {
428 Self::new_ip(features, SocketAddr::new(IpAddr::V6(ipv6), port))
429 }
430
431 pub fn end(
432 mut self,
433 rdap_response: Result<ResponseData, RdapClientError>,
434 options: &TestOptions,
435 ) -> Self {
436 if let Ok(response_data) = rdap_response {
437 self.end_time = Some(Utc::now());
438 self.outcome = RunOutcome::Tested;
439 self.summaries = Some(get_summaries(&do_checks(&response_data, options), None));
440 self.response_data = Some(response_data);
441 } else {
442 self.outcome = match rdap_response.err().unwrap() {
443 RdapClientError::InvalidQueryValue
444 | RdapClientError::AmbiguousQueryType
445 | RdapClientError::Poison
446 | RdapClientError::DomainNameError(_)
447 | RdapClientError::BootstrapUnavailable
448 | RdapClientError::BootstrapError(_)
449 | RdapClientError::IanaResponse(_) => RunOutcome::InternalError,
450 RdapClientError::Response(_) => RunOutcome::RdapDataError,
451 RdapClientError::Json(_) => RunOutcome::JsonError,
452 RdapClientError::ParsingError(e) => {
453 let status_code = e.http_data.status_code();
454 if status_code > 299 && status_code < 400 {
455 RunOutcome::HttpRedirectResponse
456 } else {
457 RunOutcome::JsonError
458 }
459 }
460 RdapClientError::IoError(_) => RunOutcome::NetworkError,
461 RdapClientError::Client(e) => {
462 if e.is_redirect() {
463 RunOutcome::HttpRedirectResponse
464 } else if e.is_connect() {
465 RunOutcome::HttpConnectError
466 } else if e.is_timeout() {
467 RunOutcome::HttpTimeoutError
468 } else if e.is_status() {
469 match e.status().unwrap() {
470 StatusCode::TOO_MANY_REQUESTS => RunOutcome::HttpTooManyRequestsError,
471 StatusCode::NOT_FOUND => RunOutcome::HttpNotFoundError,
472 StatusCode::BAD_REQUEST => RunOutcome::HttpBadRequestError,
473 StatusCode::UNAUTHORIZED => RunOutcome::HttpUnauthorizedError,
474 StatusCode::FORBIDDEN => RunOutcome::HttpForbiddenError,
475 _ => RunOutcome::HttpNon200Error,
476 }
477 } else {
478 RunOutcome::HttpProtocolError
479 }
480 }
481 };
482 self.end_time = Some(Utc::now());
483 };
484 self
485 }
486
487 fn add_summary(&self, mut table: MultiPartTable, options: &MdOptions) -> MultiPartTable {
488 let duration_s = if let Some(end_time) = self.end_time {
489 format!("{} ms", (end_time - self.start_time).num_milliseconds())
490 } else {
491 "n/a".to_string()
492 };
493 table = table.multi_raw(vec![
494 socket_addr_string(self.socket_addr),
495 self.attribute_set(),
496 duration_s,
497 self.outcome.to_md(options),
498 ]);
499 table
500 }
501
502 fn to_md(&self, options: &MdOptions) -> String {
503 let mut md = String::new();
504
505 let header_value = format!(
507 "{} - {}",
508 socket_addr_string(self.socket_addr),
509 self.attribute_set()
510 );
511 md.push_str(&format!("\n{}\n", header_value.to_header(1, options)));
512
513 if matches!(self.outcome, RunOutcome::Tested) {
515 let mut check_v: Vec<(String, String)> = vec![];
517 for summary in self.summaries.as_deref().unwrap_or_default() {
518 let message = check_item_md(&summary.item, options);
519 check_v.push((summary.structure.to_string(), message));
520 }
521
522 let mut table = MultiPartTable::new();
524
525 if check_v.is_empty() {
526 table = table.header_ref(&"No issues or errors.");
527 } else {
528 table = table.multi_raw(vec![
529 "RDAP Structure".to_inline(options),
530 "Message".to_inline(options),
531 ]);
532 for c in check_v {
533 table = table.nv_raw(&c.0, c.1);
534 }
535 }
536 md.push_str(&table.to_md_table(options));
537 } else {
538 let mut table = MultiPartTable::new();
539 table = table.multi_raw(vec![self.outcome.to_md(options)]);
540 md.push_str(&table.to_md_table(options));
541 }
542
543 md
544 }
545
546 fn attribute_set(&self) -> String {
547 let socket_type = socket_type_string(self.socket_addr);
548 if !self.features.is_empty() {
549 format!(
550 "{socket_type}, {}",
551 self.features
552 .iter()
553 .map(|f| f.to_string())
554 .collect::<Vec<_>>()
555 .join(", ")
556 )
557 } else {
558 socket_type.to_string()
559 }
560 }
561}
562
563fn socket_type_string(sock: Option<SocketAddr>) -> String {
564 if let Some(sock) = sock {
565 if sock.is_ipv4() {
566 "v4".to_string()
567 } else {
568 "v6".to_string()
569 }
570 } else {
571 "file".to_string()
572 }
573}
574
575fn socket_addr_string(sock: Option<SocketAddr>) -> String {
576 if let Some(sock) = sock {
577 sock.to_string()
578 } else {
579 "localhost".to_string()
580 }
581}
582
583fn check_item_md(item: &CheckItem, options: &MdOptions) -> String {
584 if !matches!(item.check_class, CheckClass::Informational)
585 && !matches!(item.check_class, CheckClass::SpecificationNote)
586 {
587 item.to_string().to_em(options)
588 } else {
589 item.to_string()
590 }
591}
592
593fn format_date_time(date: DateTime<Utc>) -> String {
594 date.format("%a, %v %X %Z").to_string()
595}
596
597fn do_checks(response: &ResponseData, options: &TestOptions) -> Checks {
598 let http_data = if matches!(options.test_type, super::exec::TestType::Http(_)) {
599 Some(&response.http_data)
600 } else {
601 None
602 };
603 do_check_processing(
604 &response.rdap,
605 http_data,
606 Some(&options.expect_extensions),
607 options.allow_unregistered_extensions,
608 )
609}