1
0
Fork 0

Adding upstream version 0.0.22.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-08 18:41:54 +02:00
parent 2f814b513a
commit b06d3acde8
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
190 changed files with 61565 additions and 0 deletions

View file

@ -0,0 +1,98 @@
use std::process::{ExitCode, Termination};
use {
icann_rdap_cli::rt::exec::TestExecutionError,
icann_rdap_client::{iana::IanaResponseError, RdapClientError},
thiserror::Error,
};
#[derive(Debug, Error)]
pub enum RdapTestError {
#[error("No errors encountered")]
Success,
#[error("Tests completed with execution errors.")]
TestsCompletedExecutionErrors,
#[error("Tests completed, warning checks found.")]
TestsCompletedWarningsFound,
#[error("Tests completed, error checks found.")]
TestsCompletedErrorsFound,
#[error(transparent)]
RdapClient(#[from] RdapClientError),
#[error(transparent)]
TestExecutionError(#[from] TestExecutionError),
#[error(transparent)]
Termimad(#[from] termimad::Error),
#[error(transparent)]
IoError(#[from] std::io::Error),
#[error("Unknown output type")]
UnknownOutputType,
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error(transparent)]
Iana(#[from] IanaResponseError),
#[error("Invalid IANA bootsrap file")]
InvalidBootstrap,
#[error("Bootstrap not found")]
BootstrapNotFound,
#[error("No registrar found")]
NoRegistrarFound,
#[error("No registry found")]
NoRegistryFound,
}
impl Termination for RdapTestError {
fn report(self) -> std::process::ExitCode {
let exit_code: u8 = match self {
// Success
Self::Success => 0,
Self::TestsCompletedExecutionErrors => 1,
Self::TestsCompletedWarningsFound => 2,
Self::TestsCompletedErrorsFound => 3,
// Internal Errors
Self::Termimad(_) => 10,
// I/O Errors
Self::IoError(_) => 40,
Self::TestExecutionError(_) => 40,
// RDAP Errors
Self::Json(_) => 100,
Self::Iana(_) => 101,
Self::InvalidBootstrap => 102,
Self::BootstrapNotFound => 103,
Self::NoRegistrarFound => 104,
Self::NoRegistryFound => 105,
// User Errors
Self::UnknownOutputType => 200,
// RDAP Client Errrors
Self::RdapClient(e) => match e {
// I/O Errors
RdapClientError::Client(_) => 42,
RdapClientError::IoError(_) => 43,
// RDAP Server Errors
RdapClientError::Response(_) => 60,
RdapClientError::ParsingError(_) => 62,
RdapClientError::Json(_) => 63,
// Bootstrap Errors
RdapClientError::BootstrapUnavailable => 70,
RdapClientError::BootstrapError(_) => 71,
RdapClientError::IanaResponse(_) => 72,
// User Errors
RdapClientError::InvalidQueryValue => 202,
RdapClientError::AmbiquousQueryType => 203,
RdapClientError::DomainNameError(_) => 204,
// Internal Errors
RdapClientError::Poison => 250,
// _ => 255,
},
};
ExitCode::from(exit_code)
}
}

View 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()
}
}

View file

@ -0,0 +1,27 @@
Configuration:
Configuration of this program may also be set using an environment variables configuration file in the configuration directory of this program. An example is automatically written to the configuration directory. This configuraiton file may be customized by uncommenting out the provided environment variable settings.
The location of the configuration file is platform dependent.
On Linux, this file is located at $XDG_CONFIG_HOME/rdap/rdap.env or
$HOME/.config/rdap/rdap.env.
On macOS, this file is located at
$HOME/Library/Application Support/org.ICANN.rdap/rdap.env.
On Windows, this file is located at
{FOLDERID_RoamingAppData}\rdap\config\rdap.env.
Caches:
Cache data used by this program is kept in a location dependent on the platform:
On Linux, these files are located at $XDG_CACHE_HOME/rdap/ or
$HOME/.cache/rdap/.
On macOS, these files are located at
$HOME/Library/Caches/org.ICANN.rdap/.
On Windows, this file is located at
{FOLDERID_LocalAppData}\rdap\.

View file

@ -0,0 +1,4 @@
Copyright (C) 2023 Internet Corporation for Assigned Names and Numbers
This software is dual licensed using Apache License 2.0 and MIT License.
Information on this software may be found at https://github.com/icann/icann-rdap
Information on ICANN's RDAP program may be found at https://www.icann.org/rdap

View file

@ -0,0 +1,105 @@
use {
crate::error::RdapCliError,
icann_rdap_cli::dirs::fcbs::FileCacheBootstrapStore,
icann_rdap_client::{
http::Client,
iana::{fetch_bootstrap, qtype_to_bootstrap_url, BootstrapStore, PreferredUrl},
rdap::QueryType,
},
icann_rdap_common::iana::IanaRegistryType,
tracing::debug,
};
/// Defines the type of bootstrapping to use.
pub(crate) enum BootstrapType {
/// Use RFC 9224 bootstrapping.
///
/// This is the typical bootstrapping for RDAP as defined by RFC 9224.
Rfc9224,
/// Use the supplied URL.
///
/// Essentially, this means no bootstrapping as the client is being given
/// a full URL.
Url(String),
/// Use a hint.
///
/// This will try to find an authoritative server by cycling through the various
/// bootstrap registries in the following order: object tags, TLDs, IP addresses,
/// ASNs.
Hint(String),
}
pub(crate) async fn get_base_url(
bootstrap_type: &BootstrapType,
client: &Client,
query_type: &QueryType,
) -> Result<String, RdapCliError> {
if let QueryType::Url(url) = query_type {
// this is ultimately ignored without this logic a bootstrap not found error is thrown
// which is wrong for URL queries.
return Ok(url.to_owned());
}
let store = FileCacheBootstrapStore;
match bootstrap_type {
BootstrapType::Rfc9224 => Ok(qtype_to_bootstrap_url(client, &store, query_type, |reg| {
debug!("Fetching IANA registry {}", reg.url())
})
.await?),
BootstrapType::Url(url) => Ok(url.to_owned()),
BootstrapType::Hint(hint) => {
fetch_bootstrap(&IanaRegistryType::RdapObjectTags, client, &store, |_reg| {
debug!("Fetching IANA RDAP Object Tag Registry")
})
.await?;
if let Ok(urls) = store.get_tag_urls(hint) {
Ok(urls.preferred_url()?)
} else {
fetch_bootstrap(
&IanaRegistryType::RdapBootstrapDns,
client,
&store,
|_reg| debug!("Fetching IANA RDAP DNS Registry"),
)
.await?;
if let Ok(urls) = store.get_dns_urls(hint) {
Ok(urls.preferred_url()?)
} else {
fetch_bootstrap(
&IanaRegistryType::RdapBootstrapIpv4,
client,
&store,
|_reg| debug!("Fetching IANA RDAP IPv4 Registry"),
)
.await?;
if let Ok(urls) = store.get_ipv4_urls(hint) {
Ok(urls.preferred_url()?)
} else {
fetch_bootstrap(
&IanaRegistryType::RdapBootstrapIpv6,
client,
&store,
|_reg| debug!("Fetching IANA RDAP IPv6 Registry"),
)
.await?;
if let Ok(urls) = store.get_ipv6_urls(hint) {
Ok(urls.preferred_url()?)
} else {
fetch_bootstrap(
&IanaRegistryType::RdapBootstrapAsn,
client,
&store,
|_reg| debug!("Fetching IANA RDAP ASN Registry"),
)
.await?;
Ok(store.get_asn_urls(hint)?.preferred_url()?)
}
}
}
}
}
}
}

View file

@ -0,0 +1,108 @@
use std::process::{ExitCode, Termination};
use {
icann_rdap_client::{iana::IanaResponseError, RdapClientError},
minus::MinusError,
thiserror::Error,
tracing::error,
};
#[derive(Debug, Error)]
pub enum RdapCliError {
#[error("No errors encountered")]
Success,
#[error(transparent)]
RdapClient(#[from] RdapClientError),
#[error(transparent)]
Termimad(#[from] termimad::Error),
#[error(transparent)]
IoError(#[from] std::io::Error),
#[error(transparent)]
Minus(#[from] MinusError),
#[error("Unknown output type")]
UnknownOutputType,
#[error("RDAP response failed checks.")]
ErrorOnChecks,
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error(transparent)]
Iana(#[from] IanaResponseError),
#[error("Invalid IANA bootsrap file")]
InvalidBootstrap,
#[error("Bootstrap not found")]
BootstrapNotFound,
#[error("No registrar found")]
NoRegistrarFound,
#[error("No registry found")]
NoRegistryFound,
}
impl RdapCliError {
pub(crate) fn exit_code(&self) -> u8 {
match self {
// Success
Self::Success => 0,
// Internal Errors
Self::Termimad(_) => 10,
Self::Minus(_) => 11,
// I/O Errors
Self::IoError(_) => 40,
// RDAP Errors
Self::Json(_) => 100,
Self::Iana(_) => 101,
Self::InvalidBootstrap => 102,
Self::BootstrapNotFound => 103,
Self::NoRegistrarFound => 104,
Self::NoRegistryFound => 105,
// User Errors
Self::UnknownOutputType => 200,
Self::ErrorOnChecks => 201,
// RDAP Client Errrors
Self::RdapClient(e) => match e {
// I/O Errors
RdapClientError::Client(ce) => {
if ce.is_builder() {
match ce.url() {
Some(url) if url.scheme() == "http" => 202,
_ => 42,
}
} else {
42
}
}
RdapClientError::IoError(_) => 43,
// RDAP Server Errors
RdapClientError::Response(_) => 60,
RdapClientError::ParsingError(_) => 62,
RdapClientError::Json(_) => 63,
// Bootstrap Errors
RdapClientError::BootstrapUnavailable => 70,
RdapClientError::BootstrapError(_) => 71,
RdapClientError::IanaResponse(_) => 72,
// User Errors
RdapClientError::InvalidQueryValue => 202,
RdapClientError::AmbiquousQueryType => 203,
RdapClientError::DomainNameError(_) => 204,
// Internal Errors
RdapClientError::Poison => 250,
// _ => 255,
},
}
}
}
impl Termination for RdapCliError {
fn report(self) -> std::process::ExitCode {
let exit_code = self.exit_code();
ExitCode::from(exit_code)
}
}

View file

@ -0,0 +1,732 @@
#[cfg(debug_assertions)]
use tracing::warn;
use {
bootstrap::BootstrapType,
clap::builder::{styling::AnsiColor, Styles},
error::RdapCliError,
icann_rdap_cli::dirs,
icann_rdap_client::http::{create_client, Client, ClientConfig},
icann_rdap_common::check::CheckClass,
query::{InrBackupBootstrap, ProcessType, ProcessingParams, TldLookup},
std::{io::IsTerminal, str::FromStr},
tracing::{error, info},
tracing_subscriber::filter::LevelFilter,
write::{FmtWrite, PagerWrite},
};
use {
clap::{ArgGroup, Parser, ValueEnum},
icann_rdap_client::rdap::QueryType,
icann_rdap_common::VERSION,
query::OutputType,
tokio::{join, task::spawn_blocking},
};
use crate::query::do_query;
pub mod bootstrap;
pub mod error;
pub mod query;
pub mod request;
pub mod write;
const BEFORE_LONG_HELP: &str = include_str!("before_long_help.txt");
const AFTER_LONG_HELP: &str = include_str!("after_long_help.txt");
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())]
#[command(group(
ArgGroup::new("input")
.required(true)
.args(["query_value", "server_help", "reset"]),
))]
#[command(group(
ArgGroup::new("base_specify")
.args(["base", "base_url"]),
))]
#[command(before_long_help(BEFORE_LONG_HELP))]
#[command(after_long_help(AFTER_LONG_HELP))]
/// This program queries network registry information from domain name registries and registrars
/// and Internet number registries (i.e. Regional Internet Registries) using the Registry Data
/// Access Protocol (RDAP).
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: Option<String>,
/// Type of the query when using a query value.
///
/// Without this option, the query type will be inferred based on the query value.
/// To supress the infererence and explicitly specifty the query type, use this
/// option.
#[arg(
short = 't',
long,
requires = "query_value",
required = false,
value_enum
)]
query_type: Option<QtypeArg>,
/// Get an RDAP server's help information.
///
/// Ask for a server's help information.
#[arg(short = 'S', long, conflicts_with = "query_type")]
server_help: bool,
/// An RDAP base signifier.
///
/// This option gets a base URL from the RDAP bootstrap registries maintained
/// by IANA. For example, using "com" will get the base URL for the .com
/// registry, and "arin" will get the base URL for the RDAP tags registry,
/// which points to the ARIN RIR. This option checks the bootstrap registries
/// in the following order: object tags, TLDs, IPv4, IPv6, ASN.
#[arg(short = 'b', long, required = false, env = "RDAP_BASE")]
base: Option<String>,
/// An RDAP base URL for a specific RDAP server.
///
/// Use this option to explicitly give an RDAP base URL when issuing queries.
/// If not specified, the base URL will come from the RDAP boostrap process
/// outlined in RFC 9224.
#[arg(short = 'B', long, required = false, env = "RDAP_BASE_URL")]
base_url: Option<String>,
/// Specify where to send TLD queries.
///
/// Defaults to IANA.
#[arg(
long,
required = false,
env = "RDAP_TLD_LOOKUP",
value_enum,
default_value_t = TldLookupArg::Iana,
)]
tld_lookup: TldLookupArg,
/// Specify a backup INR bootstrap.
///
/// This is used as a backup when the bootstrapping process cannot find an authoritative
/// server for IP addresses and Autonomous System Numbers. Defaults to ARIN.
#[arg(
long,
required = false,
env = "RDAP_INR_BACKUP_BOOTSTRAP",
value_enum,
default_value_t = InrBackupBootstrapArg::Arin,
)]
inr_backup_bootstrap: InrBackupBootstrapArg,
/// Output format.
///
/// This option determines the format of the result.
#[arg(
short = 'O',
long,
required = false,
env = "RDAP_OUTPUT",
value_enum,
default_value_t = OtypeArg::Auto,
)]
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>,
/// Error if RDAP checks found.
///
/// The program will log error messages for non-info
/// checks found in the RDAP response(s) and exit with a
/// non-zero status.
#[arg(long, env = "RDAP_ERROR_ON_CHECK")]
error_on_checks: bool,
/// Process Type
///
/// Specifies a process for handling the data.
#[arg(
short = 'p',
long,
required = false,
env = "RDAP_PROCESS_TYPE",
value_enum
)]
process_type: Option<ProcTypeArg>,
/// Pager Usage.
///
/// Determines how to handle paging output.
/// When using the embedded pager, all log messages will be sent to the
/// pager as well. Otherwise, log messages are sent to stderr.
#[arg(
short = 'P',
long,
required = false,
env = "RDAP_PAGING",
value_enum,
default_value_t = PagerType::None,
)]
page_output: PagerType,
/// Log level.
///
/// This option determines the level of logging.
#[arg(
short = 'L',
long,
required = false,
env = "RDAP_LOG",
value_enum,
default_value_t = LogLevel::Info
)]
log_level: LogLevel,
/// Do not use the cache.
///
/// When given, the cache will be neither read from nor written to.
#[arg(short = 'N', long, required = false, env = "RDAP_NO_CACHE")]
no_cache: bool,
/// Max cache age.
///
/// Specifies the maximum age in seconds of an item in the cache.
#[arg(
long,
required = false,
env = "RDAP_MAX_CACHE_AGE",
default_value = "86400"
)]
max_cache_age: u32,
/// 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_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_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_ALLOW_INVALID_CERTIFICATES"
)]
allow_invalid_certificates: bool,
/// 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_TIMEOUT_SECS",
default_value = "60"
)]
timeout_secs: u64,
/// 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_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_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_MAX_RETRIES", default_value = "1")]
max_retries: u16,
/// Reset.
///
/// Removes the cache files and resets the config file.
#[arg(long, required = false)]
reset: bool,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum QtypeArg {
/// Ipv4 Address Lookup
V4,
/// Ipv6 Address Lookup
V6,
/// Ipv4 CIDR Lookup
V4Cidr,
/// Ipv6 CIDR Lookup
V6Cidr,
/// Autonomous System Number Lookup
Autnum,
/// Domain Lookup
Domain,
/// A-Label Domain Lookup
ALabel,
/// Entity Lookup
Entity,
/// Nameserver Lookup
Ns,
/// Entity Name Search
EntityName,
/// Entity Handle Search
EntityHandle,
/// Domain Name Search
DomainName,
/// Domain Nameserver Name Search
DomainNsName,
/// Domain Nameserver IP Address Search
DomainNsIp,
/// Nameserver Name Search
NsName,
/// Nameserver IP Address Search
NsIp,
/// RDAP URL
Url,
}
/// 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,
/// RDAP JSON with extra information.
JsonExtra,
/// Global Top Level Domain Output
GtldWhois,
/// URL of RDAP servers.
Url,
/// Automatically determine the output type.
Auto,
}
#[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 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,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum ProcTypeArg {
/// Only display the data from the domain registrar.
Registrar,
/// Only display the data from the domain registry.
Registry,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum PagerType {
/// Use the embedded pager.
Embedded,
/// Use no pager.
None,
/// Automatically determine pager use.
Auto,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum TldLookupArg {
/// Use IANA for TLD lookups.
Iana,
/// No TLD specific lookups.
None,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum InrBackupBootstrapArg {
/// Use ARIN when no INR bootstrap can be found.
Arin,
/// No backup for INR bootstraps.
None,
}
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() -> RdapCliError {
if let Err(e) = wrapped_main().await {
let ec = e.exit_code();
match ec {
202 => error!("Use -T or --allow-http to allow insecure HTTP connections."),
_ => eprintln!("\n{e}\n"),
};
return e;
} else {
return RdapCliError::Success;
}
}
pub async fn wrapped_main() -> Result<(), RdapCliError> {
dirs::init()?;
dotenv::from_path(dirs::config_path()).ok();
let cli = Cli::parse();
if cli.reset {
dirs::reset()?;
return Ok(());
}
let level = LevelFilter::from(&cli.log_level);
let query_type = query_type_from_cli(&cli)?;
let use_pager = match cli.page_output {
PagerType::Embedded => true,
PagerType::None => false,
PagerType::Auto => std::io::stdout().is_terminal(),
};
let output_type = match cli.output_type {
OtypeArg::Auto => {
if std::io::stdout().is_terminal() {
OutputType::RenderedMarkdown
} else {
OutputType::Json
}
}
OtypeArg::RenderedMarkdown => OutputType::RenderedMarkdown,
OtypeArg::Markdown => OutputType::Markdown,
OtypeArg::Json => OutputType::Json,
OtypeArg::PrettyJson => OutputType::PrettyJson,
OtypeArg::JsonExtra => OutputType::JsonExtra,
OtypeArg::GtldWhois => OutputType::GtldWhois,
OtypeArg::Url => OutputType::Url,
};
let process_type = match cli.process_type {
Some(p) => match p {
ProcTypeArg::Registrar => ProcessType::Registrar,
ProcTypeArg::Registry => ProcessType::Registry,
},
None => ProcessType::Standard,
};
let check_types = if cli.check_type.is_empty() {
vec![
CheckClass::Informational,
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 bootstrap_type = if let Some(ref tag) = cli.base {
BootstrapType::Hint(tag.to_string())
} else if let Some(ref base_url) = cli.base_url {
BootstrapType::Url(base_url.to_string())
} else {
BootstrapType::Rfc9224
};
let tld_lookup = match cli.tld_lookup {
TldLookupArg::Iana => TldLookup::Iana,
TldLookupArg::None => TldLookup::None,
};
let inr_backup_bootstrap = match cli.inr_backup_bootstrap {
InrBackupBootstrapArg::Arin => InrBackupBootstrap::Arin,
InrBackupBootstrapArg::None => InrBackupBootstrap::None,
};
let processing_params = ProcessingParams {
bootstrap_type,
output_type,
check_types,
process_type,
tld_lookup,
inr_backup_bootstrap,
error_on_checks: cli.error_on_checks,
no_cache: cli.no_cache,
max_cache_age: cli.max_cache_age,
};
let client_config = ClientConfig::builder()
.user_agent_suffix("CLI")
.https_only(!cli.allow_http)
.accept_invalid_host_names(cli.allow_invalid_host_names)
.accept_invalid_certificates(cli.allow_invalid_certificates)
.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();
let rdap_client = create_client(&client_config);
if let Ok(client) = rdap_client {
if !use_pager {
tracing_subscriber::fmt()
.with_max_level(level)
.with_writer(std::io::stderr)
.init();
let output = &mut std::io::stdout();
let res1 = join!(exec(
cli.query_value,
&query_type,
&processing_params,
&client,
output,
));
res1.0?;
} else {
let pager = minus::Pager::new();
pager
.set_prompt(format!(
"{query_type} - Q to quit, j/k or pgup/pgdn to scroll"
))
.expect("unable to set prompt");
let output = FmtWrite(pager.clone());
let pager2 = pager.clone();
tracing_subscriber::fmt()
.with_max_level(level)
.with_writer(move || -> Box<dyn std::io::Write> {
Box::new(PagerWrite(pager2.clone()))
})
.init();
let pager = pager.clone();
let (res1, res2) = join!(
spawn_blocking(move || minus::dynamic_paging(pager)),
exec(
cli.query_value,
&query_type,
&processing_params,
&client,
output
)
);
res1.unwrap()?;
res2?;
}
} else {
error!("{}", rdap_client.err().unwrap())
};
Ok(())
}
async fn exec<W: std::io::Write>(
query_value: Option<String>,
query_type: &QueryType,
processing_params: &ProcessingParams,
client: &Client,
mut output: W,
) -> Result<(), RdapCliError> {
info!("ICANN RDAP {} Command Line Interface", VERSION);
#[cfg(debug_assertions)]
warn!("This is a development build of this software.");
if let Some(query_value) = query_value {
info!("query type is {query_type} for value '{}'", query_value);
} else {
info!("query is {query_type}");
}
let result = do_query(query_type, processing_params, client, &mut output).await;
match result {
Ok(_) => Ok(()),
Err(error) => {
error!("{}", error);
Err(error)
}
}
}
fn query_type_from_cli(cli: &Cli) -> Result<QueryType, RdapCliError> {
let Some(query_value) = cli.query_value.clone() else {
return Ok(QueryType::Help);
};
let Some(query_type) = cli.query_type else {
return Ok(QueryType::from_str(&query_value)?);
};
let q = match query_type {
QtypeArg::V4 => QueryType::ipv4(&query_value)?,
QtypeArg::V6 => QueryType::ipv6(&query_value)?,
QtypeArg::V4Cidr => QueryType::ipv4cidr(&query_value)?,
QtypeArg::V6Cidr => QueryType::ipv6cidr(&query_value)?,
QtypeArg::Autnum => QueryType::autnum(&query_value)?,
QtypeArg::Domain => QueryType::domain(&query_value)?,
QtypeArg::ALabel => QueryType::alabel(&query_value)?,
QtypeArg::Entity => QueryType::Entity(query_value),
QtypeArg::Ns => QueryType::ns(&query_value)?,
QtypeArg::EntityName => QueryType::EntityNameSearch(query_value),
QtypeArg::EntityHandle => QueryType::EntityHandleSearch(query_value),
QtypeArg::DomainName => QueryType::DomainNameSearch(query_value),
QtypeArg::DomainNsName => QueryType::DomainNsNameSearch(query_value),
QtypeArg::DomainNsIp => QueryType::domain_ns_ip_search(&query_value)?,
QtypeArg::NsName => QueryType::NameserverNameSearch(query_value),
QtypeArg::NsIp => QueryType::ns_ip_search(&query_value)?,
QtypeArg::Url => QueryType::Url(query_value),
};
Ok(q)
}
#[cfg(test)]
mod tests {
use crate::Cli;
#[test]
fn cli_debug_assert_test() {
use clap::CommandFactory;
Cli::command().debug_assert()
}
}

View file

@ -0,0 +1,479 @@
use {
icann_rdap_client::http::Client,
icann_rdap_common::{
check::{traverse_checks, CheckClass, CheckParams, Checks, GetChecks},
response::get_related_links,
},
tracing::{debug, error, info},
};
use {
icann_rdap_client::{
gtld::{GtldParams, ToGtldWhois},
md::{redacted::replace_redacted_items, MdOptions, MdParams, ToMd},
rdap::{
QueryType, RequestData, RequestResponse, RequestResponses, ResponseData, SourceType,
},
},
termimad::{crossterm::style::Color::*, Alignment, MadSkin},
};
use crate::{
bootstrap::{get_base_url, BootstrapType},
error::RdapCliError,
request::do_request,
};
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum OutputType {
/// 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,
/// Global Top Level Domain Output
GtldWhois,
/// RDAP JSON with extra information.
JsonExtra,
/// URL
Url,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum ProcessType {
/// Standard data processing.
Standard,
/// Process data specifically from a registrar.
Registrar,
/// Process data specifically from a registry.
Registry,
}
/// Used for doing TLD Lookups.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum TldLookup {
/// Use IANA for TLD lookups.
Iana,
/// No TLD specific lookups.
None,
}
/// Used for doing TLD Lookups.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum InrBackupBootstrap {
/// Use ARIN if no bootstraps can be found for INR queries.
Arin,
/// No INR bootstrap backup.
None,
}
pub(crate) struct ProcessingParams {
pub bootstrap_type: BootstrapType,
pub output_type: OutputType,
pub check_types: Vec<CheckClass>,
pub process_type: ProcessType,
pub tld_lookup: TldLookup,
pub inr_backup_bootstrap: InrBackupBootstrap,
pub error_on_checks: bool,
pub no_cache: bool,
pub max_cache_age: u32,
}
pub(crate) async fn do_query<'a, W: std::io::Write>(
query_type: &QueryType,
processing_params: &ProcessingParams,
client: &Client,
write: &mut W,
) -> Result<(), RdapCliError> {
match query_type {
QueryType::IpV4Addr(_)
| QueryType::IpV6Addr(_)
| QueryType::IpV4Cidr(_)
| QueryType::IpV6Cidr(_)
| QueryType::AsNumber(_) => {
do_inr_query(query_type, processing_params, client, write).await
}
QueryType::Domain(_) | QueryType::DomainNameSearch(_) => {
do_domain_query(query_type, processing_params, client, write).await
}
_ => do_basic_query(query_type, processing_params, None, client, write).await,
}
}
async fn do_domain_query<'a, W: std::io::Write>(
query_type: &QueryType,
processing_params: &ProcessingParams,
client: &Client,
write: &mut W,
) -> Result<(), RdapCliError> {
let mut transactions = RequestResponses::new();
// special processing for TLD Lookups
let base_url = if let QueryType::Domain(ref domain) = query_type {
if domain.is_tld() && matches!(processing_params.tld_lookup, TldLookup::Iana) {
"https://rdap.iana.org".to_string()
} else {
get_base_url(&processing_params.bootstrap_type, client, query_type).await?
}
} else {
get_base_url(&processing_params.bootstrap_type, client, query_type).await?
};
let response = do_request(&base_url, query_type, processing_params, client).await;
let registrar_response;
match response {
Ok(response) => {
let source_host = response.http_data.host.to_owned();
let req_data = RequestData {
req_number: 1,
source_host: &source_host,
source_type: SourceType::DomainRegistry,
};
let replaced_rdap = replace_redacted_items(response.rdap.clone());
let replaced_data = ResponseData {
rdap: replaced_rdap,
// copy other fields from `response`
..response.clone()
};
if let ProcessType::Registrar = processing_params.process_type {
transactions =
do_no_output(processing_params, &req_data, &replaced_data, transactions);
} else {
transactions = do_output(
processing_params,
&req_data,
&replaced_data,
write,
transactions,
)?;
}
let regr_source_host;
let regr_req_data: RequestData;
if !matches!(processing_params.process_type, ProcessType::Registry) {
if let Some(url) = get_related_links(&response.rdap).first() {
info!("Querying domain name from registrar.");
debug!("Registrar RDAP Url: {url}");
let query_type = QueryType::Url(url.to_string());
let registrar_response_result =
do_request(&base_url, &query_type, processing_params, client).await;
match registrar_response_result {
Ok(response_data) => {
registrar_response = response_data;
regr_source_host = registrar_response.http_data.host.to_owned();
regr_req_data = RequestData {
req_number: 2,
source_host: &regr_source_host,
source_type: SourceType::DomainRegistrar,
};
if let ProcessType::Registry = processing_params.process_type {
transactions = do_no_output(
processing_params,
&regr_req_data,
&registrar_response,
transactions,
);
} else {
transactions = do_output(
processing_params,
&regr_req_data,
&registrar_response,
write,
transactions,
)?;
}
}
Err(error) => return Err(error),
}
} else if matches!(processing_params.process_type, ProcessType::Registrar) {
return Err(RdapCliError::NoRegistrarFound);
}
}
do_final_output(processing_params, write, transactions)?;
}
Err(error) => {
if matches!(processing_params.process_type, ProcessType::Registry) {
return Err(RdapCliError::NoRegistryFound);
} else {
return Err(error);
}
}
};
Ok(())
}
async fn do_inr_query<'a, W: std::io::Write>(
query_type: &QueryType,
processing_params: &ProcessingParams,
client: &Client,
write: &mut W,
) -> Result<(), RdapCliError> {
let mut transactions = RequestResponses::new();
let mut base_url = get_base_url(&processing_params.bootstrap_type, client, query_type).await;
if base_url.is_err()
&& matches!(
processing_params.inr_backup_bootstrap,
InrBackupBootstrap::Arin
)
{
base_url = Ok("https://rdap.arin.net/registry".to_string());
};
let response = do_request(&base_url?, query_type, processing_params, client).await;
match response {
Ok(response) => {
let source_host = response.http_data.host.to_owned();
let req_data = RequestData {
req_number: 1,
source_host: &source_host,
source_type: SourceType::RegionalInternetRegistry,
};
let replaced_rdap = replace_redacted_items(response.rdap.clone());
let replaced_data = ResponseData {
rdap: replaced_rdap,
// copy other fields from `response`
..response.clone()
};
transactions = do_output(
processing_params,
&req_data,
&replaced_data,
write,
transactions,
)?;
do_final_output(processing_params, write, transactions)?;
}
Err(error) => return Err(error),
};
Ok(())
}
async fn do_basic_query<'a, W: std::io::Write>(
query_type: &QueryType,
processing_params: &ProcessingParams,
req_data: Option<&'a RequestData<'a>>,
client: &Client,
write: &mut W,
) -> Result<(), RdapCliError> {
let mut transactions = RequestResponses::new();
let base_url = get_base_url(&processing_params.bootstrap_type, client, query_type).await?;
let response = do_request(&base_url, query_type, processing_params, client).await;
match response {
Ok(response) => {
let source_host = response.http_data.host.to_owned();
let req_data = if let Some(meta) = req_data {
RequestData {
req_number: meta.req_number + 1,
source_host: meta.source_host,
source_type: SourceType::UncategorizedRegistry,
}
} else {
RequestData {
req_number: 1,
source_host: &source_host,
source_type: SourceType::UncategorizedRegistry,
}
};
let replaced_rdap = replace_redacted_items(response.rdap.clone());
let replaced_data = ResponseData {
rdap: replaced_rdap,
// copy other fields from `response`
..response.clone()
};
transactions = do_output(
processing_params,
&req_data,
&replaced_data,
write,
transactions,
)?;
do_final_output(processing_params, write, transactions)?;
}
Err(error) => return Err(error),
};
Ok(())
}
fn do_output<'a, W: std::io::Write>(
processing_params: &ProcessingParams,
req_data: &'a RequestData,
response: &'a ResponseData,
write: &mut W,
mut transactions: RequestResponses<'a>,
) -> Result<RequestResponses<'a>, RdapCliError> {
match processing_params.output_type {
OutputType::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(
write,
&response.rdap.to_md(MdParams {
heading_level: 1,
root: &response.rdap,
http_data: &response.http_data,
parent_type: response.rdap.get_type(),
check_types: &processing_params.check_types,
options: &MdOptions::default(),
req_data,
}),
)?;
}
OutputType::Markdown => {
writeln!(
write,
"{}",
response.rdap.to_md(MdParams {
heading_level: 1,
root: &response.rdap,
http_data: &response.http_data,
parent_type: response.rdap.get_type(),
check_types: &processing_params.check_types,
options: &MdOptions {
text_style_char: '_',
style_in_justify: true,
..MdOptions::default()
},
req_data,
})
)?;
}
OutputType::GtldWhois => {
let mut params = GtldParams {
root: &response.rdap,
parent_type: response.rdap.get_type(),
label: "".to_string(),
};
writeln!(write, "{}", response.rdap.to_gtld_whois(&mut params))?;
}
_ => {} // do nothing
};
let req_res = RequestResponse {
checks: do_output_checks(response),
req_data,
res_data: response,
};
transactions.push(req_res);
Ok(transactions)
}
fn do_no_output<'a>(
_processing_params: &ProcessingParams,
req_data: &'a RequestData,
response: &'a ResponseData,
mut transactions: RequestResponses<'a>,
) -> RequestResponses<'a> {
let req_res = RequestResponse {
checks: do_output_checks(response),
req_data,
res_data: response,
};
transactions.push(req_res);
transactions
}
fn do_output_checks(response: &ResponseData) -> Checks {
let check_params = CheckParams {
do_subchecks: true,
root: &response.rdap,
parent_type: response.rdap.get_type(),
allow_unreg_ext: false,
};
let mut checks = response.rdap.get_checks(check_params);
checks
.items
.append(&mut response.http_data.get_checks(check_params).items);
checks
}
fn do_final_output<W: std::io::Write>(
processing_params: &ProcessingParams,
write: &mut W,
transactions: RequestResponses<'_>,
) -> Result<(), RdapCliError> {
match processing_params.output_type {
OutputType::Json => {
for req_res in &transactions {
writeln!(
write,
"{}",
serde_json::to_string(&req_res.res_data.rdap).unwrap()
)?;
}
}
OutputType::PrettyJson => {
for req_res in &transactions {
writeln!(
write,
"{}",
serde_json::to_string_pretty(&req_res.res_data.rdap).unwrap()
)?;
}
}
OutputType::JsonExtra => {
writeln!(write, "{}", serde_json::to_string(&transactions).unwrap())?
}
OutputType::GtldWhois => {}
OutputType::Url => {
for rr in &transactions {
if let Some(url) = rr.res_data.http_data.request_uri() {
writeln!(write, "{url}")?;
}
}
}
_ => {} // do nothing
};
let mut checks_found = false;
// we don't want to error on informational
let error_check_types: Vec<CheckClass> = processing_params
.check_types
.iter()
.filter(|ct| *ct != &CheckClass::Informational)
.copied()
.collect();
for req_res in &transactions {
let found = traverse_checks(
&req_res.checks,
&error_check_types,
None,
&mut |struct_tree, check_item| {
if processing_params.error_on_checks {
error!("{struct_tree} -> {check_item}")
}
},
);
if found {
checks_found = true
}
}
if checks_found && processing_params.error_on_checks {
return Err(RdapCliError::ErrorOnChecks);
}
Ok(())
}

View file

@ -0,0 +1,88 @@
use std::{
fs::{self, File},
io::{BufRead, BufReader},
};
use {
icann_rdap_client::{
http::Client,
rdap::{rdap_url_request, QueryType, ResponseData},
},
icann_rdap_common::{httpdata::HttpData, response::GetSelfLink},
pct_str::{PctString, URIReserved},
tracing::{debug, info},
};
use crate::{dirs::rdap_cache_path, error::RdapCliError, query::ProcessingParams};
pub(crate) async fn do_request(
base_url: &str,
query_type: &QueryType,
processing_params: &ProcessingParams,
client: &Client,
) -> Result<ResponseData, RdapCliError> {
if processing_params.no_cache {
info!("Cache has been disabled.")
}
let query_url = query_type.query_url(base_url)?;
if !processing_params.no_cache {
let file_name = format!(
"{}.cache",
PctString::encode(query_url.chars(), URIReserved)
);
let path = rdap_cache_path().join(&file_name);
if path.exists() {
let input = File::open(path)?;
let buf = BufReader::new(input);
let mut lines = vec![];
for line in buf.lines() {
lines.push(line?)
}
let cache_data = HttpData::from_lines(&lines)?;
if !cache_data
.0
.is_expired(processing_params.max_cache_age as i64)
{
debug!("Returning response from cache file {file_name}");
let response: ResponseData = serde_json::from_str(&cache_data.1.join(""))?;
return Ok(response);
}
}
}
let response = rdap_url_request(&query_url, client).await?;
if !processing_params.no_cache {
if response.http_data.should_cache() {
let data = serde_json::to_string_pretty(&response)?;
let cache_contents = response.http_data.to_lines(&data)?;
let query_url = query_type.query_url(base_url)?;
let file_name = format!(
"{}.cache",
PctString::encode(query_url.chars(), URIReserved)
);
debug!("Saving query response to cache file {file_name}");
let path = rdap_cache_path().join(file_name);
fs::write(path, &cache_contents)?;
if let Some(self_link) = response.rdap.get_self_link() {
if let Some(self_link_href) = &self_link.href {
if query_url != *self_link_href {
let file_name = format!(
"{}.cache",
PctString::encode(self_link_href.chars(), URIReserved)
);
debug!("Saving object with self link to cache file {file_name}");
let path = rdap_cache_path().join(file_name);
fs::write(path, &cache_contents)?;
}
}
}
} else {
debug!("Not caching data according to server policy.");
debug!("Expires header: {:?}", &response.http_data.expires);
debug!(
"Cache-control header: {:?}",
&response.http_data.cache_control
);
}
}
Ok(response)
}

View file

@ -0,0 +1,35 @@
use std::io::ErrorKind;
use minus::Pager;
#[derive(Clone)]
pub(crate) struct FmtWrite<W: std::fmt::Write>(pub(crate) W);
impl<W: std::fmt::Write> std::io::Write for FmtWrite<W> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0
.write_str(&String::from_utf8_lossy(buf))
.map_err(|e| std::io::Error::new(ErrorKind::Other, e))?;
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
#[derive(Clone)]
pub(crate) struct PagerWrite(pub(crate) Pager);
impl std::io::Write for PagerWrite {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0
.push_str(String::from_utf8_lossy(buf))
.map_err(|e| std::io::Error::new(ErrorKind::Other, e))?;
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}

View file

@ -0,0 +1,389 @@
use std::{
fs::{self, File},
io::{BufRead, BufReader},
path::PathBuf,
};
use {
icann_rdap_client::iana::{BootstrapStore, RegistryHasNotExpired},
icann_rdap_common::{
httpdata::HttpData,
iana::{BootstrapRegistry, IanaRegistry, IanaRegistryType},
},
tracing::debug,
};
use super::bootstrap_cache_path;
pub struct FileCacheBootstrapStore;
impl BootstrapStore for FileCacheBootstrapStore {
fn has_bootstrap_registry(
&self,
reg_type: &IanaRegistryType,
) -> Result<bool, icann_rdap_client::RdapClientError> {
let path = bootstrap_cache_path().join(reg_type.file_name());
if path.exists() {
let fc_reg = fetch_file_cache_bootstrap(path, |s| debug!("Checking for {s}"))?;
return Ok(Some(fc_reg).registry_has_not_expired());
}
Ok(false)
}
fn put_bootstrap_registry(
&self,
reg_type: &IanaRegistryType,
registry: IanaRegistry,
http_data: HttpData,
) -> Result<(), icann_rdap_client::RdapClientError> {
let path = bootstrap_cache_path().join(reg_type.file_name());
let data = serde_json::to_string_pretty(&registry)?;
let cache_contents = http_data.to_lines(&data)?;
fs::write(path, cache_contents)?;
Ok(())
}
fn get_dns_urls(&self, ldh: &str) -> Result<Vec<String>, icann_rdap_client::RdapClientError> {
let path = bootstrap_cache_path().join(IanaRegistryType::RdapBootstrapDns.file_name());
let (iana, _http_data) = fetch_file_cache_bootstrap(path, |s| debug!("Reading {s}"))?;
Ok(iana.get_dns_bootstrap_urls(ldh)?)
}
fn get_asn_urls(&self, asn: &str) -> Result<Vec<String>, icann_rdap_client::RdapClientError> {
let path = bootstrap_cache_path().join(IanaRegistryType::RdapBootstrapAsn.file_name());
let (iana, _http_data) = fetch_file_cache_bootstrap(path, |s| debug!("Reading {s}"))?;
Ok(iana.get_asn_bootstrap_urls(asn)?)
}
fn get_ipv4_urls(&self, ipv4: &str) -> Result<Vec<String>, icann_rdap_client::RdapClientError> {
let path = bootstrap_cache_path().join(IanaRegistryType::RdapBootstrapIpv4.file_name());
let (iana, _http_data) = fetch_file_cache_bootstrap(path, |s| debug!("Reading {s}"))?;
Ok(iana.get_ipv4_bootstrap_urls(ipv4)?)
}
fn get_ipv6_urls(&self, ipv6: &str) -> Result<Vec<String>, icann_rdap_client::RdapClientError> {
let path = bootstrap_cache_path().join(IanaRegistryType::RdapBootstrapIpv6.file_name());
let (iana, _http_data) = fetch_file_cache_bootstrap(path, |s| debug!("Reading {s}"))?;
Ok(iana.get_ipv6_bootstrap_urls(ipv6)?)
}
fn get_tag_urls(&self, tag: &str) -> Result<Vec<String>, icann_rdap_client::RdapClientError> {
let path = bootstrap_cache_path().join(IanaRegistryType::RdapObjectTags.file_name());
let (iana, _http_data) = fetch_file_cache_bootstrap(path, |s| debug!("Reading {s}"))?;
Ok(iana.get_tag_bootstrap_urls(tag)?)
}
}
pub fn fetch_file_cache_bootstrap<F>(
path: PathBuf,
callback: F,
) -> Result<(IanaRegistry, HttpData), std::io::Error>
where
F: FnOnce(String),
{
let input = File::open(&path)?;
let buf = BufReader::new(input);
let mut lines = vec![];
for line in buf.lines() {
lines.push(line?);
}
let cache_data = HttpData::from_lines(&lines)?;
callback(path.display().to_string());
let iana: IanaRegistry = serde_json::from_str(&cache_data.1.join(""))?;
Ok((iana, cache_data.0))
}
#[cfg(test)]
#[allow(non_snake_case)]
mod test {
use {
icann_rdap_client::{
iana::{BootstrapStore, PreferredUrl},
rdap::QueryType,
},
icann_rdap_common::{
httpdata::HttpData,
iana::{IanaRegistry, IanaRegistryType},
},
serial_test::serial,
test_dir::{DirBuilder, FileType, TestDir},
};
use crate::dirs::{self, fcbs::FileCacheBootstrapStore};
fn test_dir() -> TestDir {
let test_dir = TestDir::temp()
.create("cache", FileType::Dir)
.create("config", FileType::Dir);
std::env::set_var("XDG_CACHE_HOME", test_dir.path("cache"));
std::env::set_var("XDG_CONFIG_HOME", test_dir.path("config"));
dirs::init().expect("unable to init directories");
test_dir
}
#[test]
#[serial]
fn GIVEN_fcbootstrap_with_dns_WHEN_get_domain_query_url_THEN_correct_url() {
// GIVEN
let _test_dir = test_dir();
let bs = FileCacheBootstrapStore;
let bootstrap = r#"
{
"version": "1.0",
"publication": "2024-01-07T10:11:12Z",
"description": "Some text",
"services": [
[
["net", "com"],
[
"https://registry.example.com/myrdap/"
]
],
[
["org", "mytld"],
[
"https://example.org/"
]
]
]
}
"#;
let iana =
serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse domain bootstrap");
bs.put_bootstrap_registry(
&IanaRegistryType::RdapBootstrapDns,
iana,
HttpData::example().build(),
)
.expect("put iana registry");
// WHEN
let actual = bs
.get_domain_query_urls(&QueryType::domain("example.org").expect("invalid domain name"))
.expect("get bootstrap url")
.preferred_url()
.expect("preferred url");
// THEN
assert_eq!(actual, "https://example.org/")
}
#[test]
#[serial]
fn GIVEN_fcbootstrap_with_autnum_WHEN_get_autnum_query_url_THEN_correct_url() {
// GIVEN
let _test_dir = test_dir();
let bs = FileCacheBootstrapStore;
let bootstrap = r#"
{
"version": "1.0",
"publication": "2024-01-07T10:11:12Z",
"description": "RDAP Bootstrap file for example registries.",
"services": [
[
["64496-64496"],
[
"https://rir3.example.com/myrdap/"
]
],
[
["64497-64510", "65536-65551"],
[
"https://example.org/"
]
],
[
["64512-65534"],
[
"http://example.net/rdaprir2/",
"https://example.net/rdaprir2/"
]
]
]
}
"#;
let iana =
serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse autnum bootstrap");
bs.put_bootstrap_registry(
&IanaRegistryType::RdapBootstrapAsn,
iana,
HttpData::example().build(),
)
.expect("put iana registry");
// WHEN
let actual = bs
.get_autnum_query_urls(&QueryType::autnum("as64512").expect("invalid autnum"))
.expect("get bootstrap url")
.preferred_url()
.expect("preferred url");
// THEN
assert_eq!(actual, "https://example.net/rdaprir2/");
}
#[test]
#[serial]
fn GIVEN_fcbootstrap_with_ipv4_THEN_get_ipv4_query_urls_THEN_correct_url() {
// GIVEN
let _test_dir = test_dir();
let bs = FileCacheBootstrapStore;
let bootstrap = r#"
{
"version": "1.0",
"publication": "2024-01-07T10:11:12Z",
"description": "RDAP Bootstrap file for example registries.",
"services": [
[
["198.51.100.0/24", "192.0.0.0/8"],
[
"https://rir1.example.com/myrdap/"
]
],
[
["203.0.113.0/24", "192.0.2.0/24"],
[
"https://example.org/"
]
],
[
["203.0.113.0/28"],
[
"https://example.net/rdaprir2/",
"http://example.net/rdaprir2/"
]
]
]
}
"#;
let iana =
serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse autnum bootstrap");
bs.put_bootstrap_registry(
&IanaRegistryType::RdapBootstrapIpv4,
iana,
HttpData::example().build(),
)
.expect("put iana registry");
// WHEN
let actual = bs
.get_ipv4_query_urls(&QueryType::ipv4("198.51.100.1").expect("invalid IP address"))
.expect("get bootstrap url")
.preferred_url()
.expect("preferred url");
// THEN
assert_eq!(actual, "https://rir1.example.com/myrdap/");
}
#[test]
#[serial]
fn GIVEN_fcbootstrap_with_ipv6_THEN_get_ipv6_query_urls_THEN_correct_url() {
// GIVEN
let _test_dir = test_dir();
let bs = FileCacheBootstrapStore;
let bootstrap = r#"
{
"version": "1.0",
"publication": "2024-01-07T10:11:12Z",
"description": "RDAP Bootstrap file for example registries.",
"services": [
[
["2001:db8::/34"],
[
"https://rir2.example.com/myrdap/"
]
],
[
["2001:db8:4000::/36", "2001:db8:ffff::/48"],
[
"https://example.org/"
]
],
[
["2001:db8:1000::/36"],
[
"https://example.net/rdaprir2/",
"http://example.net/rdaprir2/"
]
]
]
}
"#;
let iana =
serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse autnum bootstrap");
bs.put_bootstrap_registry(
&IanaRegistryType::RdapBootstrapIpv6,
iana,
HttpData::example().build(),
)
.expect("put iana registry");
// WHEN
let actual = bs
.get_ipv6_query_urls(&QueryType::ipv6("2001:db8::1").expect("invalid IP address"))
.expect("get bootstrap url")
.preferred_url()
.expect("preferred url");
// THEN
assert_eq!(actual, "https://rir2.example.com/myrdap/");
}
#[test]
#[serial]
fn GIVEN_fcbootstrap_with_tag_THEN_get_entity_handle_query_urls_THEN_correct_url() {
// GIVEN
let _test_dir = test_dir();
let bs = FileCacheBootstrapStore;
let bootstrap = r#"
{
"version": "1.0",
"publication": "YYYY-MM-DDTHH:MM:SSZ",
"description": "RDAP bootstrap file for service provider object tags",
"services": [
[
["contact@example.com"],
["YYYY"],
[
"https://example.com/rdap/"
]
],
[
["contact@example.org"],
["ZZ54"],
[
"http://rdap.example.org/"
]
],
[
["contact@example.net"],
["1754"],
[
"https://example.net/rdap/",
"http://example.net/rdap/"
]
]
]
}
"#;
let iana =
serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse autnum bootstrap");
bs.put_bootstrap_registry(
&IanaRegistryType::RdapObjectTags,
iana,
HttpData::example().build(),
)
.expect("put iana registry");
// WHEN
let actual = bs
.get_entity_handle_query_urls(&QueryType::Entity("foo-YYYY".to_string()))
.expect("get bootstrap url")
.preferred_url()
.expect("preferred url");
// THEN
assert_eq!(actual, "https://example.com/rdap/");
}
}

View file

@ -0,0 +1,4 @@
pub mod fcbs;
pub mod project;
pub use project::*;

View file

@ -0,0 +1,57 @@
use std::{
fs::{create_dir_all, remove_dir_all, write},
path::PathBuf,
sync::LazyLock,
};
use directories::ProjectDirs;
pub const QUALIFIER: &str = "org";
pub const ORGANIZATION: &str = "ICANN";
pub const APPLICATION: &str = "rdap";
pub const ENV_FILE_NAME: &str = "rdap.env";
pub const RDAP_CACHE_NAME: &str = "rdap_cache";
pub const BOOTSTRAP_CACHE_NAME: &str = "bootstrap_cache";
pub(crate) static PROJECT_DIRS: LazyLock<ProjectDirs> = LazyLock::new(|| {
ProjectDirs::from(QUALIFIER, ORGANIZATION, APPLICATION)
.expect("unable to formulate project directories")
});
/// Initializes the directories to be used.
pub fn init() -> Result<(), std::io::Error> {
create_dir_all(PROJECT_DIRS.config_dir())?;
create_dir_all(PROJECT_DIRS.cache_dir())?;
create_dir_all(rdap_cache_path())?;
create_dir_all(bootstrap_cache_path())?;
// create default config file
if !config_path().exists() {
let example_config = include_str!("rdap.env");
write(config_path(), example_config)?;
}
Ok(())
}
/// Reset the directories.
pub fn reset() -> Result<(), std::io::Error> {
remove_dir_all(PROJECT_DIRS.config_dir())?;
remove_dir_all(PROJECT_DIRS.cache_dir())?;
init()
}
/// Returns a [PathBuf] to the configuration file.
pub fn config_path() -> PathBuf {
PROJECT_DIRS.config_dir().join(ENV_FILE_NAME)
}
/// Returns a [PathBuf] to the cache directory for RDAP responses.
pub fn rdap_cache_path() -> PathBuf {
PROJECT_DIRS.cache_dir().join(RDAP_CACHE_NAME)
}
/// Returns a [PathBuf] to the cache directory for bootstrap files.
pub fn bootstrap_cache_path() -> PathBuf {
PROJECT_DIRS.cache_dir().join(BOOTSTRAP_CACHE_NAME)
}

View file

@ -0,0 +1,40 @@
# This file controls the environment variables for the RDAP CLI.
# The file format is that of a shell script setting variables.
# Use --help to determine the active values.
# Sets the logging level. Valid values are off, error, warn, info, debug, and trace.
#RDAP_LOG=info
# Determines if output is sent to a pager. Valid values are embedded, none, and auto.
#RDAP_PAGING=none
# Determines the output format of the output. Valid values are markdown, rendered-markdown, pretty-json, json, json-extra, and auto.
#RDAP_OUTPUT=auto
# Sets a base URL from a name in the RDAP bootstrap registry.
#RDAP_BASE=
# Sets a base URL explicitly.
#RDAP_BASE_URL
# Where to lookup TLDs
#RDAP_TLD_LOOKUP=iana
# Which base URL to use if no IP address or autnum bootstrap can be found.
#RDAP_INR_BACKUP_BOOTSTRAP=arin
# Do not use cache.
#RDAP_NO_CACHE=true
# The maximum age of an item in the cache.
#RDAP_MAX_CACHE_AGE=86400
# Allow HTTP connections
#RDAP_ALLOW_HTTP=true
# Allow invalid host names in HTTPS.
#RDAP_ALLOW_INVALID_HOST_NAMES=true
# Allow invalid certificates in HTTPS.
#RDAP_ALLOW_INVALID_CERTIFICATES=true

View file

@ -0,0 +1,2 @@
pub mod dirs;
pub mod rt;

View file

@ -0,0 +1,463 @@
//! Function to execute tests.
use std::{
net::{Ipv4Addr, Ipv6Addr},
str::FromStr,
};
use {
hickory_client::{
client::{AsyncClient, ClientConnection, ClientHandle},
rr::{DNSClass, Name, RecordType},
udp::UdpClientConnection,
},
icann_rdap_client::{
http::{create_client, create_client_with_addr, ClientConfig},
iana::{qtype_to_bootstrap_url, BootstrapStore},
rdap::{rdap_url_request, QueryType},
RdapClientError,
},
icann_rdap_common::response::{get_related_links, ExtensionId},
reqwest::{header::HeaderValue, Url},
thiserror::Error,
tracing::{debug, info},
url::ParseError,
};
use crate::rt::results::{RunFeature, TestRun};
use super::results::{DnsData, TestResults};
#[derive(Default)]
pub struct TestOptions {
pub skip_v4: bool,
pub skip_v6: bool,
pub skip_origin: bool,
pub origin_value: String,
pub chase_referral: bool,
pub expect_extensions: Vec<String>,
pub expect_groups: Vec<ExtensionGroup>,
pub allow_unregistered_extensions: bool,
pub one_addr: bool,
pub dns_resolver: Option<String>,
}
#[derive(Clone)]
pub enum ExtensionGroup {
Gtld,
Nro,
NroAsn,
}
#[derive(Debug, Error)]
pub enum TestExecutionError {
#[error(transparent)]
RdapClient(#[from] RdapClientError),
#[error(transparent)]
UrlParseError(#[from] ParseError),
#[error(transparent)]
AddrParseError(#[from] std::net::AddrParseError),
#[error("No host to resolve")]
NoHostToResolve,
#[error("No rdata")]
NoRdata,
#[error("Bad rdata")]
BadRdata,
#[error(transparent)]
Client(#[from] reqwest::Error),
#[error(transparent)]
InvalidHeader(#[from] reqwest::header::InvalidHeaderValue),
#[error("Unsupporte Query Type")]
UnsupportedQueryType,
#[error("No referral to chase")]
NoReferralToChase,
#[error("Unregistered extension")]
UnregisteredExtension,
}
pub async fn execute_tests<'a, BS: BootstrapStore>(
bs: &BS,
value: &QueryType,
options: &TestOptions,
client_config: &ClientConfig,
) -> Result<TestResults, TestExecutionError> {
let bs_client = create_client(client_config)?;
// normalize extensions
let extensions = normalize_extension_ids(options)?;
let options = &TestOptions {
expect_extensions: extensions,
expect_groups: options.expect_groups.clone(),
origin_value: options.origin_value.clone(),
dns_resolver: options.dns_resolver.clone(),
..*options
};
// get the query url
let mut query_url = match value {
QueryType::Help => return Err(TestExecutionError::UnsupportedQueryType),
QueryType::Url(url) => url.to_owned(),
_ => {
let base_url = qtype_to_bootstrap_url(&bs_client, bs, value, |reg| {
info!("Fetching IANA registry {} for value {value}", reg.url())
})
.await?;
value.query_url(&base_url)?
}
};
// if the URL to test is a referral
if options.chase_referral {
let client = create_client(client_config)?;
info!("Fetching referral from {query_url}");
let response_data = rdap_url_request(&query_url, &client).await?;
query_url = get_related_links(&response_data.rdap)
.first()
.ok_or(TestExecutionError::NoReferralToChase)?
.to_string();
info!("Referral is {query_url}");
}
let parsed_url = Url::parse(&query_url)?;
let port = parsed_url.port().unwrap_or_else(|| {
if parsed_url.scheme().eq("https") {
443
} else {
80
}
});
let host = parsed_url
.host_str()
.ok_or(TestExecutionError::NoHostToResolve)?;
info!("Testing {query_url}");
let dns_data = get_dns_records(host, options).await?;
let mut test_results = TestResults::new(query_url.clone(), dns_data.clone());
let mut more_runs = true;
for v4 in dns_data.v4_addrs {
// test run without origin
let mut test_run = TestRun::new_v4(vec![], v4, port);
if !options.skip_v4 && more_runs {
let client = create_client_with_addr(client_config, host, test_run.socket_addr)?;
info!("Sending request to {}", test_run.socket_addr);
let rdap_response = rdap_url_request(&query_url, &client).await;
test_run = test_run.end(rdap_response, options);
}
test_results.add_test_run(test_run);
// test run with origin
let mut test_run = TestRun::new_v4(vec![RunFeature::OriginHeader], v4, port);
if !options.skip_v4 && !options.skip_origin && more_runs {
let client_config = ClientConfig::from_config(client_config)
.origin(HeaderValue::from_str(&options.origin_value)?)
.build();
let client = create_client_with_addr(&client_config, host, test_run.socket_addr)?;
info!("Sending request to {}", test_run.socket_addr);
let rdap_response = rdap_url_request(&query_url, &client).await;
test_run = test_run.end(rdap_response, options);
}
test_results.add_test_run(test_run);
if options.one_addr {
more_runs = false;
}
}
let mut more_runs = true;
for v6 in dns_data.v6_addrs {
// test run without origin
let mut test_run = TestRun::new_v6(vec![], v6, port);
if !options.skip_v6 && more_runs {
let client = create_client_with_addr(client_config, host, test_run.socket_addr)?;
info!("Sending request to {}", test_run.socket_addr);
let rdap_response = rdap_url_request(&query_url, &client).await;
test_run = test_run.end(rdap_response, options);
}
test_results.add_test_run(test_run);
// test run with origin
let mut test_run = TestRun::new_v6(vec![RunFeature::OriginHeader], v6, port);
if !options.skip_v6 && !options.skip_origin && more_runs {
let client_config = ClientConfig::from_config(client_config)
.origin(HeaderValue::from_str(&options.origin_value)?)
.build();
let client = create_client_with_addr(&client_config, host, test_run.socket_addr)?;
info!("Sending request to {}", test_run.socket_addr);
let rdap_response = rdap_url_request(&query_url, &client).await;
test_run = test_run.end(rdap_response, options);
}
test_results.add_test_run(test_run);
if options.one_addr {
more_runs = false;
}
}
test_results.end(options);
info!("Testing complete.");
Ok(test_results)
}
async fn get_dns_records(host: &str, options: &TestOptions) -> Result<DnsData, TestExecutionError> {
// short circuit dns if these are ip addresses
if let Ok(ip4) = Ipv4Addr::from_str(host) {
return Ok(DnsData {
v4_cname: None,
v6_cname: None,
v4_addrs: vec![ip4],
v6_addrs: vec![],
});
} else if let Ok(ip6) = Ipv6Addr::from_str(host.trim_start_matches('[').trim_end_matches(']')) {
return Ok(DnsData {
v4_cname: None,
v6_cname: None,
v4_addrs: vec![],
v6_addrs: vec![ip6],
});
}
let def_dns_resolver = "8.8.8.8:53".to_string();
let dns_resolver = options.dns_resolver.as_ref().unwrap_or(&def_dns_resolver);
let conn = UdpClientConnection::new(dns_resolver.parse()?)
.unwrap()
.new_stream(None);
let (mut client, bg) = AsyncClient::connect(conn).await.unwrap();
// make sure to run the background task
tokio::spawn(bg);
let mut dns_data = DnsData::default();
// Create a query future
let query = client.query(Name::from_str(host).unwrap(), DNSClass::IN, RecordType::A);
// wait for its response
let response = query.await.unwrap();
for answer in response.answers() {
match answer.record_type() {
RecordType::CNAME => {
let cname = answer
.data()
.ok_or(TestExecutionError::NoRdata)?
.clone()
.into_cname()
.map_err(|_e| TestExecutionError::BadRdata)?
.0
.to_string();
debug!("Found cname {cname}");
dns_data.v4_cname = Some(cname);
}
RecordType::A => {
let addr = answer
.data()
.ok_or(TestExecutionError::NoRdata)?
.clone()
.into_a()
.map_err(|_e| TestExecutionError::BadRdata)?
.0;
debug!("Found IPv4 {addr}");
dns_data.v4_addrs.push(addr);
}
_ => {
// do nothing
}
};
}
// Create a query future
let query = client.query(
Name::from_str(host).unwrap(),
DNSClass::IN,
RecordType::AAAA,
);
// wait for its response
let response = query.await.unwrap();
for answer in response.answers() {
match answer.record_type() {
RecordType::CNAME => {
let cname = answer
.data()
.ok_or(TestExecutionError::NoRdata)?
.clone()
.into_cname()
.map_err(|_e| TestExecutionError::BadRdata)?
.0
.to_string();
debug!("Found cname {cname}");
dns_data.v6_cname = Some(cname);
}
RecordType::AAAA => {
let addr = answer
.data()
.ok_or(TestExecutionError::NoRdata)?
.clone()
.into_aaaa()
.map_err(|_e| TestExecutionError::BadRdata)?
.0;
debug!("Found IPv6 {addr}");
dns_data.v6_addrs.push(addr);
}
_ => {
// do nothing
}
};
}
Ok(dns_data)
}
fn normalize_extension_ids(options: &TestOptions) -> Result<Vec<String>, TestExecutionError> {
let mut retval = options.expect_extensions.clone();
// check for unregistered extensions
if !options.allow_unregistered_extensions {
for ext in &retval {
if ExtensionId::from_str(ext).is_err() {
return Err(TestExecutionError::UnregisteredExtension);
}
}
}
// put the groups in
for group in &options.expect_groups {
match group {
ExtensionGroup::Gtld => {
retval.push(format!(
"{}|{}",
ExtensionId::IcannRdapResponseProfile0,
ExtensionId::IcannRdapResponseProfile1
));
retval.push(format!(
"{}|{}",
ExtensionId::IcannRdapTechnicalImplementationGuide0,
ExtensionId::IcannRdapTechnicalImplementationGuide1
));
}
ExtensionGroup::Nro => {
retval.push(ExtensionId::NroRdapProfile0.to_string());
retval.push(ExtensionId::Cidr0.to_string());
}
ExtensionGroup::NroAsn => {
retval.push(ExtensionId::NroRdapProfile0.to_string());
retval.push(format!(
"{}|{}",
ExtensionId::NroRdapProfileAsnFlat0,
ExtensionId::NroRdapProfileAsnHierarchical0
));
}
}
}
Ok(retval)
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use icann_rdap_common::response::ExtensionId;
use crate::rt::exec::{ExtensionGroup, TestOptions};
use super::normalize_extension_ids;
#[test]
fn GIVEN_gtld_WHEN_normalize_extensions_THEN_list_contains_gtld_ids() {
// GIVEN
let given = vec![ExtensionGroup::Gtld];
// WHEN
let options = TestOptions {
expect_groups: given,
..Default::default()
};
let actual = normalize_extension_ids(&options).unwrap();
// THEN
let expected1 = format!(
"{}|{}",
ExtensionId::IcannRdapResponseProfile0,
ExtensionId::IcannRdapResponseProfile1
);
assert!(actual.contains(&expected1));
let expected2 = format!(
"{}|{}",
ExtensionId::IcannRdapTechnicalImplementationGuide0,
ExtensionId::IcannRdapTechnicalImplementationGuide1
);
assert!(actual.contains(&expected2));
}
#[test]
fn GIVEN_nro_and_foo_WHEN_normalize_extensions_THEN_list_contains_nro_ids_and_foo() {
// GIVEN
let groups = vec![ExtensionGroup::Nro];
let exts = vec!["foo1".to_string()];
// WHEN
let options = TestOptions {
allow_unregistered_extensions: true,
expect_extensions: exts,
expect_groups: groups,
..Default::default()
};
let actual = normalize_extension_ids(&options).unwrap();
dbg!(&actual);
// THEN
assert!(actual.contains(&ExtensionId::NroRdapProfile0.to_string()));
assert!(actual.contains(&ExtensionId::Cidr0.to_string()));
assert!(actual.contains(&"foo1".to_string()));
}
#[test]
fn GIVEN_nro_and_foo_WHEN_unreg_disallowed_THEN_err() {
// GIVEN
let groups = vec![ExtensionGroup::Nro];
let exts = vec!["foo1".to_string()];
// WHEN
let options = TestOptions {
expect_extensions: exts,
expect_groups: groups,
..Default::default()
};
let actual = normalize_extension_ids(&options);
// THEN
assert!(actual.is_err())
}
#[test]
fn GIVEN_unregistered_ext_WHEN_normalize_extensions_THEN_error() {
// GIVEN
let given = vec!["foo".to_string()];
// WHEN
let options = TestOptions {
expect_extensions: given,
..Default::default()
};
let actual = normalize_extension_ids(&options);
// THEN
assert!(actual.is_err());
}
#[test]
fn GIVEN_unregistered_ext_WHEN_allowed_THEN_no_error() {
// GIVEN
let given = vec!["foo".to_string()];
// WHEN
let options = TestOptions {
expect_extensions: given,
allow_unregistered_extensions: true,
..Default::default()
};
let actual = normalize_extension_ids(&options);
// THEN
assert!(actual.is_ok());
}
}

View file

@ -0,0 +1,2 @@
pub mod exec;
pub mod results;

View file

@ -0,0 +1,487 @@
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
/// Contains the results of test execution.
use chrono::{DateTime, Utc};
use {
icann_rdap_client::{
md::{string::StringUtil, table::MultiPartTable, MdOptions},
rdap::ResponseData,
RdapClientError,
},
icann_rdap_common::{
check::{traverse_checks, Check, CheckClass, CheckItem, CheckParams, Checks, GetChecks},
response::{ExtensionId, RdapResponse},
},
reqwest::StatusCode,
serde::Serialize,
strum_macros::Display,
};
use super::exec::TestOptions;
#[derive(Debug, Serialize)]
pub struct TestResults {
pub query_url: String,
pub dns_data: DnsData,
pub start_time: DateTime<Utc>,
pub end_time: Option<DateTime<Utc>>,
pub service_checks: Vec<CheckItem>,
pub test_runs: Vec<TestRun>,
}
impl TestResults {
pub fn new(query_url: String, dns_data: DnsData) -> Self {
Self {
query_url,
dns_data,
start_time: Utc::now(),
end_time: None,
service_checks: vec![],
test_runs: vec![],
}
}
pub fn end(&mut self, options: &TestOptions) {
self.end_time = Some(Utc::now());
//service checks
if self.dns_data.v4_cname.is_some() && self.dns_data.v4_addrs.is_empty() {
self.service_checks
.push(Check::CnameWithoutARecords.check_item());
}
if self.dns_data.v6_cname.is_some() && self.dns_data.v6_addrs.is_empty() {
self.service_checks
.push(Check::CnameWithoutAAAARecords.check_item());
}
if self.dns_data.v4_addrs.is_empty() {
self.service_checks.push(Check::NoARecords.check_item());
}
if self.dns_data.v6_addrs.is_empty() {
self.service_checks.push(Check::NoAAAARecords.check_item());
// see if required by ICANN
let tig0 = ExtensionId::IcannRdapTechnicalImplementationGuide0.to_string();
let tig1 = ExtensionId::IcannRdapTechnicalImplementationGuide1.to_string();
let both_tigs = format!("{tig0}|{tig1}");
if options.expect_extensions.contains(&tig0)
|| options.expect_extensions.contains(&tig1)
|| options.expect_extensions.contains(&both_tigs)
{
self.service_checks
.push(Check::Ipv6SupportRequiredByIcann.check_item())
}
}
}
pub fn add_test_run(&mut self, test_run: TestRun) {
self.test_runs.push(test_run);
}
pub fn to_md(&self, options: &MdOptions, check_classes: &[CheckClass]) -> String {
let mut md = String::new();
// h1
md.push_str(&format!(
"\n{}\n",
self.query_url.to_owned().to_header(1, options)
));
// table
let mut table = MultiPartTable::new();
// test results summary
table = table.multi_raw(vec![
"Start Time".to_inline(options),
"End Time".to_inline(options),
"Duration".to_inline(options),
"Tested".to_inline(options),
]);
let (end_time_s, duration_s) = if let Some(end_time) = self.end_time {
(
format_date_time(end_time),
format!("{} s", (end_time - self.start_time).num_seconds()),
)
} else {
("FATAL".to_em(options), "N/A".to_string())
};
let tested = self
.test_runs
.iter()
.filter(|r| matches!(r.outcome, RunOutcome::Tested))
.count();
table = table.multi_raw(vec![
format_date_time(self.start_time),
end_time_s,
duration_s,
format!("{tested} of {}", self.test_runs.len()),
]);
// dns data
table = table.multi_raw(vec![
"DNS Query".to_inline(options),
"DNS Answer".to_inline(options),
]);
let v4_cname = if let Some(ref cname) = self.dns_data.v4_cname {
cname.to_owned()
} else {
format!("{} A records", self.dns_data.v4_addrs.len())
};
table = table.multi_raw(vec!["A (v4)".to_string(), v4_cname]);
let v6_cname = if let Some(ref cname) = self.dns_data.v6_cname {
cname.to_owned()
} else {
format!("{} AAAA records", self.dns_data.v6_addrs.len())
};
table = table.multi_raw(vec!["AAAA (v6)".to_string(), v6_cname]);
// summary of each run
table = table.multi_raw(vec![
"Address".to_inline(options),
"Attributes".to_inline(options),
"Duration".to_inline(options),
"Outcome".to_inline(options),
]);
for test_run in &self.test_runs {
table = test_run.add_summary(table, options);
}
md.push_str(&table.to_md_table(options));
md.push('\n');
// checks that are about the service and not a particular test run
if !self.service_checks.is_empty() {
md.push_str(&"Service Checks".to_string().to_header(1, options));
let mut table = MultiPartTable::new();
table = table.multi_raw(vec!["Message".to_inline(options)]);
for c in &self.service_checks {
let message = check_item_md(c, options);
table = table.multi_raw(vec![message]);
}
md.push_str(&table.to_md_table(options));
md.push('\n');
}
// each run in detail
for run in &self.test_runs {
md.push_str(&run.to_md(options, check_classes));
}
md
}
}
#[derive(Debug, Serialize, Clone, Default)]
pub struct DnsData {
pub v4_cname: Option<String>,
pub v6_cname: Option<String>,
pub v4_addrs: Vec<Ipv4Addr>,
pub v6_addrs: Vec<Ipv6Addr>,
}
#[derive(Debug, Serialize, Display)]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
pub enum RunOutcome {
Tested,
NetworkError,
HttpProtocolError,
HttpConnectError,
HttpRedirectResponse,
HttpTimeoutError,
HttpNon200Error,
HttpTooManyRequestsError,
HttpNotFoundError,
HttpBadRequestError,
HttpUnauthorizedError,
HttpForbiddenError,
JsonError,
RdapDataError,
InternalError,
Skipped,
}
#[derive(Debug, Serialize, Display)]
#[strum(serialize_all = "snake_case")]
pub enum RunFeature {
OriginHeader,
}
impl RunOutcome {
pub fn to_md(&self, options: &MdOptions) -> String {
match self {
Self::Tested => self.to_bold(options),
Self::Skipped => self.to_string(),
_ => self.to_em(options),
}
}
}
#[derive(Debug, Serialize)]
pub struct TestRun {
pub features: Vec<RunFeature>,
pub socket_addr: SocketAddr,
pub start_time: DateTime<Utc>,
pub end_time: Option<DateTime<Utc>>,
pub response_data: Option<ResponseData>,
pub outcome: RunOutcome,
pub checks: Option<Checks>,
}
impl TestRun {
fn new(features: Vec<RunFeature>, socket_addr: SocketAddr) -> Self {
Self {
features,
start_time: Utc::now(),
socket_addr,
end_time: None,
response_data: None,
outcome: RunOutcome::Skipped,
checks: None,
}
}
pub fn new_v4(features: Vec<RunFeature>, ipv4: Ipv4Addr, port: u16) -> Self {
Self::new(features, SocketAddr::new(IpAddr::V4(ipv4), port))
}
pub fn new_v6(features: Vec<RunFeature>, ipv6: Ipv6Addr, port: u16) -> Self {
Self::new(features, SocketAddr::new(IpAddr::V6(ipv6), port))
}
pub fn end(
mut self,
rdap_response: Result<ResponseData, RdapClientError>,
options: &TestOptions,
) -> Self {
if let Ok(response_data) = rdap_response {
self.end_time = Some(Utc::now());
self.outcome = RunOutcome::Tested;
self.checks = Some(do_checks(&response_data, options));
self.response_data = Some(response_data);
} else {
self.outcome = match rdap_response.err().unwrap() {
RdapClientError::InvalidQueryValue
| RdapClientError::AmbiquousQueryType
| RdapClientError::Poison
| RdapClientError::DomainNameError(_)
| RdapClientError::BootstrapUnavailable
| RdapClientError::BootstrapError(_)
| RdapClientError::IanaResponse(_) => RunOutcome::InternalError,
RdapClientError::Response(_) => RunOutcome::RdapDataError,
RdapClientError::Json(_) => RunOutcome::JsonError,
RdapClientError::ParsingError(e) => {
let status_code = e.http_data.status_code();
if status_code > 299 && status_code < 400 {
RunOutcome::HttpRedirectResponse
} else {
RunOutcome::JsonError
}
}
RdapClientError::IoError(_) => RunOutcome::NetworkError,
RdapClientError::Client(e) => {
if e.is_redirect() {
RunOutcome::HttpRedirectResponse
} else if e.is_connect() {
RunOutcome::HttpConnectError
} else if e.is_timeout() {
RunOutcome::HttpTimeoutError
} else if e.is_status() {
match e.status().unwrap() {
StatusCode::TOO_MANY_REQUESTS => RunOutcome::HttpTooManyRequestsError,
StatusCode::NOT_FOUND => RunOutcome::HttpNotFoundError,
StatusCode::BAD_REQUEST => RunOutcome::HttpBadRequestError,
StatusCode::UNAUTHORIZED => RunOutcome::HttpUnauthorizedError,
StatusCode::FORBIDDEN => RunOutcome::HttpForbiddenError,
_ => RunOutcome::HttpNon200Error,
}
} else {
RunOutcome::HttpProtocolError
}
}
};
self.end_time = Some(Utc::now());
};
self
}
fn add_summary(&self, mut table: MultiPartTable, options: &MdOptions) -> MultiPartTable {
let duration_s = if let Some(end_time) = self.end_time {
format!("{} ms", (end_time - self.start_time).num_milliseconds())
} else {
"n/a".to_string()
};
table = table.multi_raw(vec![
self.socket_addr.to_string(),
self.attribute_set(),
duration_s,
self.outcome.to_md(options),
]);
table
}
fn to_md(&self, options: &MdOptions, check_classes: &[CheckClass]) -> String {
let mut md = String::new();
// h1
let header_value = format!("{} - {}", self.socket_addr, self.attribute_set());
md.push_str(&format!("\n{}\n", header_value.to_header(1, options)));
// if outcome is tested
if matches!(self.outcome, RunOutcome::Tested) {
// get check items according to class
let mut check_v: Vec<(String, String)> = vec![];
if let Some(ref checks) = self.checks {
traverse_checks(checks, check_classes, None, &mut |struct_name, item| {
let message = check_item_md(item, options);
check_v.push((struct_name.to_string(), message))
});
};
// table
let mut table = MultiPartTable::new();
if check_v.is_empty() {
table = table.header_ref(&"No issues or errors.");
} else {
table = table.multi_raw(vec![
"RDAP Structure".to_inline(options),
"Message".to_inline(options),
]);
for c in check_v {
table = table.nv_raw(&c.0, c.1);
}
}
md.push_str(&table.to_md_table(options));
} else {
let mut table = MultiPartTable::new();
table = table.multi_raw(vec![self.outcome.to_md(options)]);
md.push_str(&table.to_md_table(options));
}
md
}
fn attribute_set(&self) -> String {
let socket_type = if self.socket_addr.is_ipv4() {
"v4"
} else {
"v6"
};
if !self.features.is_empty() {
format!(
"{socket_type}, {}",
self.features
.iter()
.map(|f| f.to_string())
.collect::<Vec<_>>()
.join(", ")
)
} else {
socket_type.to_string()
}
}
}
fn check_item_md(item: &CheckItem, options: &MdOptions) -> String {
if !matches!(item.check_class, CheckClass::Informational)
&& !matches!(item.check_class, CheckClass::SpecificationNote)
{
item.to_string().to_em(options)
} else {
item.to_string()
}
}
fn format_date_time(date: DateTime<Utc>) -> String {
date.format("%a, %v %X %Z").to_string()
}
fn do_checks(response: &ResponseData, options: &TestOptions) -> Checks {
let check_params = CheckParams {
do_subchecks: true,
root: &response.rdap,
parent_type: response.rdap.get_type(),
allow_unreg_ext: options.allow_unregistered_extensions,
};
let mut checks = response.rdap.get_checks(check_params);
// httpdata checks
checks
.items
.append(&mut response.http_data.get_checks(check_params).items);
// add expected extension checks
for ext in &options.expect_extensions {
if !rdap_has_expected_extension(&response.rdap, ext) {
checks
.items
.push(Check::ExpectedExtensionNotFound.check_item());
}
}
//return
checks
}
fn rdap_has_expected_extension(rdap: &RdapResponse, ext: &str) -> bool {
let count = ext.split('|').filter(|s| rdap.has_extension(s)).count();
count > 0
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use icann_rdap_common::{
prelude::ToResponse,
response::{Domain, Extension},
};
use super::rdap_has_expected_extension;
#[test]
fn GIVEN_expected_extension_WHEN_rdap_has_THEN_true() {
// GIVEN
let domain = Domain::builder()
.extension(Extension::from("foo0"))
.ldh_name("foo.example.com")
.build();
let rdap = domain.to_response();
// WHEN
let actual = rdap_has_expected_extension(&rdap, "foo0");
// THEN
assert!(actual);
}
#[test]
fn GIVEN_expected_extension_WHEN_rdap_does_not_have_THEN_false() {
// GIVEN
let domain = Domain::builder()
.extension(Extension::from("foo0"))
.ldh_name("foo.example.com")
.build();
let rdap = domain.to_response();
// WHEN
let actual = rdap_has_expected_extension(&rdap, "foo1");
// THEN
assert!(!actual);
}
#[test]
fn GIVEN_compound_expected_extension_WHEN_rdap_has_THEN_true() {
// GIVEN
let domain = Domain::builder()
.extension(Extension::from("foo0"))
.ldh_name("foo.example.com")
.build();
let rdap = domain.to_response();
// WHEN
let actual = rdap_has_expected_extension(&rdap, "foo0|foo1");
// THEN
assert!(actual);
}
}