573 lines
16 KiB
Rust
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()
|
|
}
|
|
}
|