1
0
Fork 0
icann-rdap/icann-rdap-cli/src/bin/rdap-test/main.rs
Daniel Baumann b06d3acde8
Adding upstream version 0.0.22.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-05-08 18:41:54 +02:00

573 lines
16 KiB
Rust

use std::{io::stdout, str::FromStr};
#[cfg(debug_assertions)]
use tracing::warn;
use {
clap::builder::{styling::AnsiColor, Styles},
error::RdapTestError,
icann_rdap_cli::{
dirs,
dirs::fcbs::FileCacheBootstrapStore,
rt::{
exec::{execute_tests, ExtensionGroup, TestOptions},
results::{RunOutcome, TestResults},
},
},
icann_rdap_client::{http::ClientConfig, md::MdOptions, rdap::QueryType},
icann_rdap_common::check::{traverse_checks, CheckClass},
termimad::{crossterm::style::Color::*, Alignment, MadSkin},
tracing::info,
tracing_subscriber::filter::LevelFilter,
};
use {
clap::{Parser, ValueEnum},
icann_rdap_common::VERSION,
};
pub mod error;
struct CliStyles;
impl CliStyles {
fn cli_styles() -> Styles {
Styles::styled()
.header(AnsiColor::Yellow.on_default())
.usage(AnsiColor::Green.on_default())
.literal(AnsiColor::Green.on_default())
.placeholder(AnsiColor::Green.on_default())
}
}
#[derive(Parser, Debug)]
#[command(author, version = VERSION, about, long_about, styles = CliStyles::cli_styles())]
/// This program aids in the troubleshooting of issues with RDAP servers.
struct Cli {
/// Value to be queried in RDAP.
///
/// This is the value to query. For example, a domain name or IP address.
#[arg()]
query_value: String,
/// Output format.
///
/// This option determines the format of the result.
#[arg(
short = 'O',
long,
required = false,
env = "RDAP_TEST_OUTPUT",
value_enum,
default_value_t = OtypeArg::RenderedMarkdown,
)]
output_type: OtypeArg,
/// Check type.
///
/// Specifies the type of checks to conduct on the RDAP
/// responses. These are RDAP specific checks and not
/// JSON validation which is done automatically. This
/// argument may be specified multiple times to include
/// multiple check types.
#[arg(short = 'C', long, required = false, value_enum)]
check_type: Vec<CheckTypeArg>,
/// Log level.
///
/// This option determines the level of logging.
#[arg(
short = 'L',
long,
required = false,
env = "RDAP_TEST_LOG",
value_enum,
default_value_t = LogLevel::Info
)]
log_level: LogLevel,
/// DNS Resolver
///
/// Specifies the address and port of the DNS resolver to query.
#[arg(
long,
required = false,
env = "RDAP_TEST_DNS_RESOLVER",
default_value = "8.8.8.8:53"
)]
dns_resolver: String,
/// Allow HTTP connections.
///
/// When given, allows connections to RDAP servers using HTTP.
/// Otherwise, only HTTPS is allowed.
#[arg(short = 'T', long, required = false, env = "RDAP_TEST_ALLOW_HTTP")]
allow_http: bool,
/// Allow invalid host names.
///
/// When given, allows HTTPS connections to servers where the host name does
/// not match the certificate's host name.
#[arg(
short = 'K',
long,
required = false,
env = "RDAP_TEST_ALLOW_INVALID_HOST_NAMES"
)]
allow_invalid_host_names: bool,
/// Allow invalid certificates.
///
/// When given, allows HTTPS connections to servers where the TLS certificates
/// are invalid.
#[arg(
short = 'I',
long,
required = false,
env = "RDAP_TEST_ALLOW_INVALID_CERTIFICATES"
)]
allow_invalid_certificates: bool,
/// Maximum retry wait time.
///
/// Sets the maximum number of seconds to wait before retrying a query when
/// a server has sent an HTTP 429 status code with a retry-after value.
/// That is, the value to used is no greater than this setting.
#[arg(
long,
required = false,
env = "RDAP_TEST_MAX_RETRY_SECS",
default_value = "120"
)]
max_retry_secs: u32,
/// Default retry wait time.
///
/// Sets the number of seconds to wait before retrying a query when
/// a server has sent an HTTP 429 status code without a retry-after value
/// or when the retry-after value does not make sense.
#[arg(
long,
required = false,
env = "RDAP_TEST_DEF_RETRY_SECS",
default_value = "60"
)]
def_retry_secs: u32,
/// Maximum number of retries.
///
/// This sets the maximum number of retries when a server signals too many
/// requests have been sent using an HTTP 429 status code.
#[arg(
long,
required = false,
env = "RDAP_TEST_MAX_RETRIES",
default_value = "1"
)]
max_retries: u16,
/// Set the query timeout.
///
/// This values specifies, in seconds, the total time to connect and read all
/// the data from a connection.
#[arg(
long,
required = false,
env = "RDAP_TEST_TIMEOUT_SECS",
default_value = "60"
)]
timeout_secs: u64,
/// Skip v4.
///
/// Skip testing of IPv4 connections.
#[arg(long, required = false, env = "RDAP_TEST_SKIP_v4")]
skip_v4: bool,
/// Skip v6.
///
/// Skip testing of IPv6 connections.
#[arg(long, required = false, env = "RDAP_TEST_SKIP_V6")]
skip_v6: bool,
/// Skip origin tests.
///
/// Skip testing with the HTTP origin header.
#[arg(long, required = false, env = "RDAP_TEST_SKIP_ORIGIN")]
skip_origin: bool,
/// Only test one address.
///
/// Only test one address per address family.
#[arg(long, required = false, env = "RDAP_TEST_ONE_ADDR")]
one_addr: bool,
/// Origin header value.
///
/// Specifies the origin header value.
/// This value is not used if the 'skip-origin' option is used.
#[arg(
long,
required = false,
env = "RDAP_TEST_ORIGIN_VALUE",
default_value = "https://example.com"
)]
origin_value: String,
/// Follow redirects.
///
/// When set, follows HTTP redirects.
#[arg(
short = 'R',
long,
required = false,
env = "RDAP_TEST_FOLLOW_REDIRECTS"
)]
follow_redirects: bool,
/// Chase a referral.
///
/// Get a referral in the first response and use that for testing. This is useful
/// for testing registrars by using the normal bootstrapping process to get the
/// referral to the registrar from the registry.
#[arg(short = 'r', long, required = false)]
referral: bool,
/// Expect extension.
///
/// Expect the RDAP response to contain a specific extension ID.
/// If a response does not contain the expected RDAP extension ID,
/// it will be added as an failed check. This parameter may also
/// take the form of "foo1|foo2" to be mean either expect "foo1" or
/// "foo2".
///
/// This value may be repeated more than once.
#[arg(
short = 'e',
long,
required = false,
env = "RDAP_TEST_EXPECT_EXTENSIONS"
)]
expect_extensions: Vec<String>,
/// Expect extension group.
///
/// Extension groups are known sets of extensions.
///
/// This value may be repeated more than once.
#[arg(
short = 'g',
long,
required = false,
value_enum,
env = "RDAP_TEST_EXPECT_EXTENSION_GROUP"
)]
expect_group: Vec<ExtensionGroupArg>,
/// Allow unregistered extensions.
///
/// Do not flag unregistered extensions.
#[arg(
short = 'E',
long,
required = false,
env = "RDAP_TEST_ALLOW_UNREGISTERED_EXTENSIONS"
)]
allow_unregistered_extensions: bool,
}
/// Represents the output type possibilities.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum OtypeArg {
/// Results are rendered as Markdown in the terminal using ANSI terminal capabilities.
RenderedMarkdown,
/// Results are rendered as Markdown in plain text.
Markdown,
/// Results are output as RDAP JSON.
Json,
/// Results are output as Pretty RDAP JSON.
PrettyJson,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum CheckTypeArg {
/// All checks.
All,
/// Informational items.
Info,
/// Specification Notes
SpecNote,
/// Checks for STD 95 warnings.
StdWarn,
/// Checks for STD 95 errors.
StdError,
/// Cidr0 errors.
Cidr0Error,
/// ICANN Profile errors.
IcannError,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum ExtensionGroupArg {
/// The gTLD RDAP profiles.
Gtld,
/// The base NRO profiles.
Nro,
/// The NRO ASN profiles including the base profile.
NroAsn,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum LogLevel {
/// No logging.
Off,
/// Log errors.
Error,
/// Log errors and warnings.
Warn,
/// Log informational messages, errors, and warnings.
Info,
/// Log debug messages, informational messages, errors and warnings.
Debug,
/// Log messages appropriate for software development.
Trace,
}
impl From<&LogLevel> for LevelFilter {
fn from(log_level: &LogLevel) -> Self {
match log_level {
LogLevel::Off => Self::OFF,
LogLevel::Error => Self::ERROR,
LogLevel::Warn => Self::WARN,
LogLevel::Info => Self::INFO,
LogLevel::Debug => Self::DEBUG,
LogLevel::Trace => Self::TRACE,
}
}
}
#[tokio::main]
pub async fn main() -> RdapTestError {
if let Err(e) = wrapped_main().await {
eprintln!("\n{e}\n");
return e;
} else {
return RdapTestError::Success;
}
}
pub async fn wrapped_main() -> Result<(), RdapTestError> {
dirs::init()?;
dotenv::from_path(dirs::config_path()).ok();
let cli = Cli::parse();
let level = LevelFilter::from(&cli.log_level);
tracing_subscriber::fmt()
.with_max_level(level)
.with_writer(std::io::stderr)
.init();
info!("ICANN RDAP {} Testing Tool", VERSION);
#[cfg(debug_assertions)]
warn!("This is a development build of this software.");
let query_type = QueryType::from_str(&cli.query_value)?;
let check_classes = if cli.check_type.is_empty() {
vec![
CheckClass::StdWarning,
CheckClass::StdError,
CheckClass::Cidr0Error,
CheckClass::IcannError,
]
} else if cli.check_type.contains(&CheckTypeArg::All) {
vec![
CheckClass::Informational,
CheckClass::SpecificationNote,
CheckClass::StdWarning,
CheckClass::StdError,
CheckClass::Cidr0Error,
CheckClass::IcannError,
]
} else {
cli.check_type
.iter()
.map(|c| match c {
CheckTypeArg::Info => CheckClass::Informational,
CheckTypeArg::SpecNote => CheckClass::SpecificationNote,
CheckTypeArg::StdWarn => CheckClass::StdWarning,
CheckTypeArg::StdError => CheckClass::StdError,
CheckTypeArg::Cidr0Error => CheckClass::Cidr0Error,
CheckTypeArg::IcannError => CheckClass::IcannError,
CheckTypeArg::All => panic!("check type for all should have been handled."),
})
.collect::<Vec<CheckClass>>()
};
let mut expect_groups = vec![];
for g in cli.expect_group {
match g {
ExtensionGroupArg::Gtld => expect_groups.push(ExtensionGroup::Gtld),
ExtensionGroupArg::Nro => expect_groups.push(ExtensionGroup::Nro),
ExtensionGroupArg::NroAsn => expect_groups.push(ExtensionGroup::NroAsn),
}
}
let bs = FileCacheBootstrapStore;
let options = TestOptions {
skip_v4: cli.skip_v4,
skip_v6: cli.skip_v6,
skip_origin: cli.skip_origin,
origin_value: cli.origin_value,
chase_referral: cli.referral,
expect_extensions: cli.expect_extensions,
expect_groups,
allow_unregistered_extensions: cli.allow_unregistered_extensions,
one_addr: cli.one_addr,
dns_resolver: Some(cli.dns_resolver),
};
let client_config = ClientConfig::builder()
.user_agent_suffix("RT")
.https_only(!cli.allow_http)
.accept_invalid_host_names(cli.allow_invalid_host_names)
.accept_invalid_certificates(cli.allow_invalid_certificates)
.follow_redirects(cli.follow_redirects)
.timeout_secs(cli.timeout_secs)
.max_retry_secs(cli.max_retry_secs)
.def_retry_secs(cli.def_retry_secs)
.max_retries(cli.max_retries)
.build();
// execute tests
let test_results = execute_tests(&bs, &query_type, &options, &client_config).await?;
// output results
let md_options = MdOptions::default();
match cli.output_type {
OtypeArg::RenderedMarkdown => {
let mut skin = MadSkin::default_dark();
skin.set_headers_fg(Yellow);
skin.headers[1].align = Alignment::Center;
skin.headers[2].align = Alignment::Center;
skin.headers[3].align = Alignment::Center;
skin.headers[4].compound_style.set_fg(DarkGreen);
skin.headers[5].compound_style.set_fg(Magenta);
skin.headers[6].compound_style.set_fg(Cyan);
skin.headers[7].compound_style.set_fg(Red);
skin.bold.set_fg(DarkBlue);
skin.italic.set_fg(Red);
skin.quote_mark.set_fg(DarkBlue);
skin.table.set_fg(DarkGreen);
skin.table.align = Alignment::Center;
skin.inline_code.set_fgbg(Cyan, Reset);
skin.write_text_on(
&mut stdout(),
&test_results.to_md(&md_options, &check_classes),
)?;
}
OtypeArg::Markdown => {
println!("{}", test_results.to_md(&md_options, &check_classes));
}
OtypeArg::Json => {
println!("{}", serde_json::to_string(&test_results).unwrap());
}
OtypeArg::PrettyJson => {
println!("{}", serde_json::to_string_pretty(&test_results).unwrap());
}
}
// if some tests could not execute
//
let execution_errors = test_results
.test_runs
.iter()
.filter(|r| !matches!(r.outcome, RunOutcome::Tested | RunOutcome::Skipped))
.count();
if execution_errors != 0 {
return Err(RdapTestError::TestsCompletedExecutionErrors);
}
// if tests had check errors
//
// get the error classes but only if they were specified.
let error_classes = check_classes
.iter()
.filter(|c| {
matches!(
c,
CheckClass::StdError | CheckClass::Cidr0Error | CheckClass::IcannError
)
})
.copied()
.collect::<Vec<CheckClass>>();
// return proper exit code if errors found
if are_there_checks(error_classes, &test_results) {
return Err(RdapTestError::TestsCompletedErrorsFound);
}
// if tests had check warnings
//
// get the warning classes but only if they were specified.
let warning_classes = check_classes
.iter()
.filter(|c| matches!(c, CheckClass::StdWarning))
.copied()
.collect::<Vec<CheckClass>>();
// return proper exit code if errors found
if are_there_checks(warning_classes, &test_results) {
return Err(RdapTestError::TestsCompletedWarningsFound);
}
Ok(())
}
fn are_there_checks(classes: Vec<CheckClass>, test_results: &TestResults) -> bool {
// see if there are any checks in the test runs
let run_count = test_results
.test_runs
.iter()
.filter(|r| {
if let Some(checks) = &r.checks {
traverse_checks(checks, &classes, None, &mut |_, _| {})
} else {
false
}
})
.count();
// see if there are any classes in the service checks
let service_count = test_results
.service_checks
.iter()
.filter(|c| classes.contains(&c.check_class))
.count();
run_count + service_count != 0
}
#[cfg(test)]
mod tests {
use crate::Cli;
#[test]
fn cli_debug_assert_test() {
use clap::CommandFactory;
Cli::command().debug_assert()
}
}