Adding upstream version 0.0.22.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
2f814b513a
commit
b06d3acde8
190 changed files with 61565 additions and 0 deletions
573
icann-rdap-cli/src/bin/rdap-test/main.rs
Normal file
573
icann-rdap-cli/src/bin/rdap-test/main.rs
Normal file
|
@ -0,0 +1,573 @@
|
|||
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()
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue