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,42 @@
[package]
name = "icann-rdap-client"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
description = """
An RDAP client library.
"""
[dependencies]
icann-rdap-common = { version = "0.0.22", path = "../icann-rdap-common" }
buildstructor.workspace = true
cidr.workspace = true
chrono.workspace = true
const_format.workspace = true
idna.workspace = true
ipnet.workspace = true
jsonpath-rust.workspace = true
jsonpath_lib.workspace = true
pct-str.workspace = true
regex.workspace = true
reqwest.workspace = true
serde.workspace = true
serde_json.workspace = true
strum.workspace = true
strum_macros.workspace = true
thiserror.workspace = true
tracing.workspace = true
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio.workspace = true
[dev-dependencies]
# fixture testings
rstest = "0.17.0"
# tokio async runtime
tokio = { version = "1.21", features = [ "full" ] }

105
icann-rdap-client/README.md Normal file
View file

@ -0,0 +1,105 @@
ICANN RDAP Client Library
=========================
This is a client library for the Registration Data Access Protocol (RDAP) written and sponsored
by the Internet Corporation for Assigned Names and Numbers [(ICANN)](https://www.icann.org).
RDAP is standard of the [IETF](https://ietf.org/), and extensions
to RDAP are a current work activity of the IETF's [REGEXT working group](https://datatracker.ietf.org/wg/regext/documents/).
More information on ICANN's role in RDAP can be found [here](https://www.icann.org/rdap).
General information on RDAP can be found [here](https://rdap.rcode3.com/).
Installation
------------
Add the library to your Cargo.toml: `cargo add icann-rdap-client`
Also, add the commons library: `cargo add icann-rdap-common`.
Both [icann_rdap_common] and this crate can be compiled for WASM targets.
Usage
-----
In RDAP, [bootstrapping](https://rdap.rcode3.com/bootstrapping/iana.html)
is the process of finding the authoritative RDAP server to
query using the IANA RDAP bootstrap files. To make a query using bootstrapping:
```rust,no_run
use icann_rdap_client::prelude::*;
use std::str::FromStr;
use tokio::main;
#[tokio::main]
async fn main() -> Result<(), RdapClientError> {
// create a query
let query = QueryType::from_str("192.168.0.1")?;
// or
let query = QueryType::from_str("icann.org")?;
// create a client (from icann-rdap-common)
let config = ClientConfig::default();
// or let config = ClientConfig::builder().build();
let client = create_client(&config)?;
// ideally, keep store in same context as client
let store = MemoryBootstrapStore::new();
// issue the RDAP query
let response =
rdap_bootstrapped_request(
&query,
&client,
&store,
|reg| eprintln!("fetching {reg:?}")
).await?;
Ok(())
}
```
To specify a base URL:
```rust,no_run
use icann_rdap_client::prelude::*;
use std::str::FromStr;
use tokio::main;
#[tokio::main]
async fn main() -> Result<(), RdapClientError> {
// create a query
let query = QueryType::from_str("192.168.0.1")?;
// or
let query = QueryType::from_str("icann.org")?;
// create a client (from icann-rdap-common)
let config = ClientConfig::builder().build();
// or let config = ClientConfig::default();
let client = create_client(&config)?;
// issue the RDAP query
let base_url = "https://rdap-bootstrap.arin.net/bootstrap";
let response = rdap_request(base_url, &query, &client).await?;
Ok(())
}
```
License
-------
Licensed under either of
* Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
* MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT) at your option.
Contribution
------------
Unless you explicitly state otherwise, any contribution, as defined in the Apache-2.0 license,
intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license,
shall be dual licensed pursuant to the Apache License, Version 2.0 or the MIT License referenced
as above, at ICANNs option, without any additional terms or conditions.

View file

@ -0,0 +1,258 @@
use {
super::{GtldParams, ToGtldWhois},
icann_rdap_common::response::{Boolish, Domain, Event, Nameserver, Network, SecureDns},
};
impl ToGtldWhois for Domain {
fn to_gtld_whois(&self, params: &mut GtldParams) -> String {
let mut gtld = String::new();
gtld.push_str("\n\n");
// Domain Name
let domain_name = format_domain_name(self);
gtld.push_str(&domain_name);
// Domain ID
let domain_id = format_domain_id(self.object_common.handle.as_ref());
gtld.push_str(&domain_id);
// Date Time for Registry
let date_info = format_registry_dates(&self.object_common.events);
gtld.push_str(&date_info);
// Common Object Stuff
let domain_info = format_domain_info(
&self.object_common.status.as_ref().map(|v| v.vec().clone()),
&self.object_common.port_43,
);
gtld.push_str(&domain_info);
// Enitities: registrar and abuse/tech/admin/registrant info
let formatted_data = self.object_common.entities.to_gtld_whois(params);
gtld.push_str(&formatted_data);
// nameservers and network
let additional_info =
format_nameservers_and_network(&self.nameservers, &self.network, params);
gtld.push_str(&additional_info);
// secure dns
let dnssec_info = format_dnssec_info(&self.secure_dns);
gtld.push_str(&dnssec_info);
gtld.push_str(
"URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf/\n",
);
// last update info
format_last_update_info(&self.object_common.events, &mut gtld);
gtld
}
}
fn format_domain_name(domain: &Domain) -> String {
if let Some(unicode_name) = &domain.unicode_name {
format!("Domain Name: {unicode_name}\n")
} else if let Some(ldh_name) = &domain.ldh_name {
format!("Domain Name: {ldh_name}\n")
} else if let Some(handle) = &domain.object_common.handle {
format!("Domain Name: {handle}\n")
} else {
"Domain Name: \n".to_string()
}
}
fn format_domain_id(handle: Option<&String>) -> String {
if let Some(handle) = handle {
format!("Registry Domain ID: {handle}\n")
} else {
"Registry Domain ID: \n".to_string()
}
}
fn format_registry_dates(events: &Option<Vec<Event>>) -> String {
let mut formatted_dates = String::new();
if let Some(events) = events {
for event in events {
if let Some(event_action) = &event.event_action {
match event_action.as_str() {
"last changed" => {
if let Some(event_date) = &event.event_date {
formatted_dates.push_str(&format!("Updated Date: {}\n", event_date));
}
}
"registration" => {
if let Some(event_date) = &event.event_date {
formatted_dates.push_str(&format!("Creation Date: {}\n", event_date));
}
}
"expiration" => {
if let Some(event_date) = &event.event_date {
formatted_dates
.push_str(&format!("Registry Expiry Date: {}\n", event_date));
}
}
_ => {}
}
}
}
}
formatted_dates
}
fn format_domain_info(status: &Option<Vec<String>>, port_43: &Option<String>) -> String {
let mut info = String::new();
if let Some(status) = status {
for value in status {
info.push_str(&format!("Domain Status: {}\n", *value));
}
}
if let Some(port_43) = port_43 {
if !port_43.is_empty() {
info.push_str(&format!("Registrar Whois Server: {}\n", port_43));
}
}
info
}
fn format_nameservers_and_network(
nameservers: &Option<Vec<Nameserver>>,
network: &Option<Network>,
params: &mut GtldParams,
) -> String {
let mut gtld = String::new();
if let Some(nameservers) = nameservers {
nameservers
.iter()
.for_each(|ns| gtld.push_str(&ns.to_gtld_whois(params)));
}
if let Some(network) = network {
gtld.push_str(&network.to_gtld_whois(params));
}
gtld
}
fn format_dnssec_info(secure_dns: &Option<SecureDns>) -> String {
let mut dnssec_info = String::new();
if let Some(secure_dns) = secure_dns {
if secure_dns
.delegation_signed
.as_ref()
.unwrap_or(&Boolish::from(false))
.into_bool()
{
dnssec_info.push_str("DNSSEC: signedDelegation\n");
if let Some(ds_data) = &secure_dns.ds_data {
for ds in ds_data {
if let (Some(key_tag), Some(algorithm), Some(digest_type), Some(digest)) = (
ds.key_tag.as_ref(),
ds.algorithm.as_ref(),
ds.digest_type.as_ref(),
ds.digest.as_ref(),
) {
dnssec_info.push_str(&format!(
"DNSSEC DS Data: {} {} {} {}\n",
key_tag, algorithm, digest_type, digest
));
}
}
}
}
}
dnssec_info
}
fn format_last_update_info(events: &Option<Vec<Event>>, gtld: &mut String) {
if let Some(events) = events {
for event in events {
if let Some(event_action) = &event.event_action {
if event_action == "last update of RDAP database" {
if let Some(event_date) = &event.event_date {
gtld.push_str(&format!(
">>> Last update of RDAP database: {} <<<\n",
event_date
));
}
break;
}
}
}
}
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use crate::gtld::ToGtldWhois;
use {
super::GtldParams,
icann_rdap_common::{prelude::ToResponse, response::Domain},
};
use {
serde_json::Value,
std::{any::TypeId, error::Error, fs::File, io::Read},
};
fn process_gtld_file(file_path: &str) -> Result<String, Box<dyn Error>> {
let mut file = File::open(file_path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let toplevel_json_response: Value = serde_json::from_str(&contents)?;
let actual = serde_json::from_value::<Domain>(toplevel_json_response);
let gtld_version_of_the_domain = match actual {
Ok(domain) => {
let rdap_response = Domain::builder().ldh_name("").build().to_response();
let mut gtld_params = GtldParams {
root: &rdap_response,
parent_type: TypeId::of::<Domain>(),
label: "".to_string(),
};
domain.to_gtld_whois(&mut gtld_params)
}
Err(e) => {
return Err(Box::new(e));
}
};
Ok(gtld_version_of_the_domain)
}
#[test]
fn test_ms_click_response() {
let expected_output =
std::fs::read_to_string("src/test_files/microsoft.click-expected.gtld").unwrap();
let output = process_gtld_file("src/test_files/microsoft.click.json").unwrap();
assert_eq!(output, expected_output);
}
#[test]
fn test_lemonde_response() {
let expected_output =
std::fs::read_to_string("src/test_files/lemonde.fr-expected.gtld").unwrap();
let output = process_gtld_file("src/test_files/lemonde.fr.json").unwrap();
assert_eq!(output, expected_output);
}
#[test]
fn test_moscow_response() {
let expected_output =
std::fs::read_to_string("src/test_files/home.moscow-expected.gtld").unwrap();
let output = process_gtld_file("src/test_files/home.moscow.json").unwrap();
assert_eq!(output, expected_output);
}
}

View file

@ -0,0 +1,270 @@
use {
super::{GtldParams, RoleInfo, ToGtldWhois},
icann_rdap_common::{
contact::{Contact, PostalAddress},
response::Entity,
},
};
impl ToGtldWhois for Option<Vec<Entity>> {
fn to_gtld_whois(&self, params: &mut GtldParams) -> String {
let mut front_formatted_data = String::new();
let mut formatted_data = String::new();
if let Some(entities) = self {
for entity in entities {
for role in entity.roles() {
match role.as_str() {
"registrar" => {
if let Some(vcard_array) = &entity.vcard_array {
let role_info = extract_role_info(role, vcard_array, params);
// Now use role_info to append to formatted_data
if !role_info.name.is_empty() {
front_formatted_data +=
&format!("{}: {}\n", cfl(role), role_info.name);
}
if !role_info.org.is_empty() {
front_formatted_data +=
&format!("{} Organization: {}\n", cfl(role), role_info.org);
}
if !role_info.adr.is_empty() {
front_formatted_data += &role_info.adr;
}
}
// Special Sauce for Registrar IANA ID and Abuse Contact
if let Some(public_ids) = &entity.public_ids {
for public_id in public_ids {
if let Some(id_type) = &public_id.id_type {
if let Some(identifier) = &public_id.identifier {
if id_type.as_str() == "IANA Registrar ID"
&& !identifier.is_empty()
{
front_formatted_data += &format!(
"Registrar IANA ID: {}\n",
identifier.clone()
);
}
}
}
}
}
append_abuse_contact_info(entity, &mut front_formatted_data);
}
"technical" | "administrative" | "registrant" => {
if let Some(vcard_array) = &entity.vcard_array {
let role_info = extract_role_info(role, vcard_array, params);
// Now use role_info to append to formatted_data
if !role_info.name.is_empty() {
formatted_data +=
&format!("{} Name: {}\n", cfl(role), role_info.name);
}
if !role_info.org.is_empty() {
formatted_data +=
&format!("{} Organization: {}\n", cfl(role), role_info.org);
}
if !role_info.adr.is_empty() {
formatted_data += &role_info.adr;
}
if !role_info.email.is_empty() {
formatted_data +=
&format!("{} Email: {}\n", cfl(role), role_info.email);
}
if !role_info.phone.is_empty() {
formatted_data +=
&format!("{} Phone: {}\n", cfl(role), role_info.phone);
}
if !role_info.fax.is_empty() {
formatted_data +=
&format!("{} Fax: {}\n", cfl(role), role_info.fax);
}
}
}
_ => {} // Are there any roles we are missing?
}
}
}
}
front_formatted_data += &formatted_data;
front_formatted_data
}
}
fn format_address_with_label(
params: &mut GtldParams,
address_components: &[serde_json::Value],
) -> String {
// TODO once from_vcard is fixed to handle the way addressing is done, replace this with the normal builder.
let postal_address = PostalAddress::builder()
.street_parts(
address_components
.get(2)
.and_then(|v| v.as_str())
.map_or_else(Vec::new, |s| vec![s.to_string()]),
)
.locality(
address_components
.get(3)
.and_then(|v| v.as_str())
.map_or_else(String::new, String::from),
)
.region_name(
address_components
.get(4)
.and_then(|v| v.as_str())
.map_or_else(String::new, String::from),
)
.country_name(
address_components
.get(6)
.and_then(|v| v.as_str())
.map_or_else(String::new, String::from),
)
.country_code(
address_components
.get(6)
.and_then(|v| v.as_str())
.map_or_else(String::new, String::from),
)
.postal_code(
address_components
.get(5)
.and_then(|v| v.as_str())
.map_or_else(String::new, String::from),
)
.build();
postal_address.to_gtld_whois(params).to_string()
}
fn extract_role_info(
role: &str,
vcard_array: &[serde_json::Value],
params: &mut GtldParams,
) -> RoleInfo {
let contact = match Contact::from_vcard(vcard_array) {
Some(contact) => contact,
None => return RoleInfo::default(),
};
let mut adr = String::new();
let label = match role {
"registrar" => "Registrar",
"technical" => "Technical",
"administrative" => "Admin",
"registrant" => "Registrant",
_ => "",
};
params.label = label.to_string();
let name = contact.full_name.unwrap_or_default();
let org = contact
.organization_names
.and_then(|orgs| orgs.first().cloned())
.unwrap_or_default();
// TODO this is a workout to get the address out of the contact. Replace this when from_vcard is fixed
for vcard in vcard_array.iter() {
if let Some(properties) = vcard.as_array() {
for property in properties {
if let Some(property) = property.as_array() {
if let "adr" = property[0].as_str().unwrap_or("") {
if let Some(address_components) = property[3].as_array() {
adr = format_address_with_label(params, address_components);
}
}
}
}
}
}
let email = contact
.emails
.and_then(|emails| emails.first().map(|email| email.email.clone()))
.unwrap_or_default();
let phone = contact
.phones
.as_ref()
.and_then(|phones| {
phones
.iter()
.find(|phone| {
phone
.features
.as_ref()
.map_or(true, |features| !features.contains(&"fax".to_string()))
})
.map(|phone| phone.phone.clone())
})
.unwrap_or_default();
let fax = contact
.phones
.as_ref()
.and_then(|phones| {
phones
.iter()
.find(|phone| {
phone
.features
.as_ref()
.map_or(false, |features| features.contains(&"fax".to_string()))
})
.map(|phone| phone.phone.clone())
})
.unwrap_or_default();
RoleInfo {
name,
org,
adr,
email,
phone,
fax,
}
}
fn append_abuse_contact_info(entity: &Entity, front_formatted_data: &mut String) {
if let Some(entities) = &entity.object_common.entities {
for entity in entities {
for role in entity.roles() {
if role.as_str() == "abuse" {
if let Some(vcard_array) = &entity.vcard_array {
if let Some(contact) = Contact::from_vcard(vcard_array) {
// Emails
if let Some(emails) = &contact.emails {
for email in emails {
let abuse_contact_email = &email.email;
if !abuse_contact_email.is_empty() {
front_formatted_data.push_str(&format!(
"Registrar Abuse Contact Email: {}\n",
abuse_contact_email
));
}
}
}
// Phones
if let Some(phones) = &contact.phones {
for phone in phones {
let abuse_contact_phone = &phone.phone;
if !abuse_contact_phone.is_empty() {
front_formatted_data.push_str(&format!(
"Registrar Abuse Contact Phone: {}\n",
abuse_contact_phone
));
}
}
}
}
}
}
}
}
}
}
// capitalize first letter
fn cfl(s: &str) -> String {
s.char_indices()
.next()
.map(|(i, c)| c.to_uppercase().collect::<String>() + &s[i + 1..])
.unwrap_or_default()
}

View file

@ -0,0 +1,83 @@
//! Converts RDAP structures to gTLD Whois output.
use {
icann_rdap_common::{contact::PostalAddress, response::RdapResponse},
std::any::TypeId,
};
pub mod domain;
pub mod entity;
pub mod nameserver;
pub mod network;
pub mod types;
#[derive(Clone)]
pub struct GtldParams<'a> {
pub root: &'a RdapResponse,
pub parent_type: TypeId,
pub label: String,
}
impl GtldParams<'_> {
pub fn from_parent(&mut self, parent_type: TypeId) -> Self {
Self {
parent_type,
root: self.root,
label: self.label.clone(),
}
}
pub fn next_level(&self) -> Self {
Self {
label: self.label.clone(),
..*self
}
}
}
pub trait ToGtldWhois {
fn to_gtld_whois(&self, params: &mut GtldParams) -> String;
}
impl ToGtldWhois for RdapResponse {
fn to_gtld_whois(&self, params: &mut GtldParams) -> String {
let mut gtld = String::new();
let variant_gtld = match &self {
Self::Domain(domain) => domain.to_gtld_whois(params),
_ => String::new(),
};
gtld.push_str(&variant_gtld);
gtld
}
}
impl ToGtldWhois for PostalAddress {
fn to_gtld_whois(&self, params: &mut GtldParams) -> String {
let label = &params.label;
let street = self
.street_parts
.as_ref()
.map(|parts| parts.join(" "))
.unwrap_or_default();
let city = self.locality.as_deref().unwrap_or("");
let state = self.region_name.as_deref().unwrap_or("");
let postal_code = self.postal_code.as_deref().unwrap_or("");
let country = self.country_code.as_deref().unwrap_or("");
format!(
"{} Street: {}\n{} City: {}\n{} State/Province: {}\n{} Postal Code: {}\n{} Country: {}\n",
label, street, label, city, label, state, label, postal_code, label, country
)
}
}
#[derive(Default)]
pub struct RoleInfo {
name: String,
org: String,
adr: String,
email: String,
phone: String,
fax: String,
}

View file

@ -0,0 +1,22 @@
use {
super::{GtldParams, ToGtldWhois},
icann_rdap_common::response::Nameserver,
};
impl ToGtldWhois for Nameserver {
fn to_gtld_whois(&self, _params: &mut GtldParams) -> String {
let mut gtld = String::new();
// header
let header_text = if let Some(unicode_name) = &self.unicode_name {
format!("Name Server: {unicode_name}\n")
} else if let Some(ldh_name) = &self.ldh_name {
format!("Name Server: {ldh_name}\n")
} else if let Some(handle) = &self.object_common.handle {
format!("Name Server: {handle}\n")
} else {
"Name Server: \n".to_string()
};
gtld.push_str(&header_text);
gtld
}
}

View file

@ -0,0 +1,30 @@
use {
super::{GtldParams, ToGtldWhois},
icann_rdap_common::response::Network,
std::any::TypeId,
};
impl ToGtldWhois for Network {
fn to_gtld_whois(&self, params: &mut GtldParams) -> String {
let _typeid = TypeId::of::<Self>();
let mut gtld = String::new();
gtld.push_str(&self.common.to_gtld_whois(params));
let header_text = if self.start_address.is_some() && self.end_address.is_some() {
format!(
"IP Network: {}-{}\n",
&self.start_address.as_ref().unwrap(),
&self.end_address.as_ref().unwrap()
)
} else if let Some(start_address) = &self.start_address {
format!("IP Network: {start_address}\n")
} else if let Some(handle) = &self.object_common.handle {
format!("IP Network: {handle}\n")
} else if let Some(name) = &self.name {
format!("IP Network: {name}\n")
} else {
"IP Network:\n".to_string()
};
gtld.push_str(&header_text);
gtld
}
}

View file

@ -0,0 +1,10 @@
use {
super::{GtldParams, ToGtldWhois},
icann_rdap_common::response::Common,
};
impl ToGtldWhois for Common {
fn to_gtld_whois(&self, _params: &mut GtldParams) -> String {
String::new()
}
}

View file

@ -0,0 +1,9 @@
//! The HTTP layer of RDAP.
#[doc(inline)]
pub use reqwest::*;
#[doc(inline)]
pub use wrapped::*;
pub(crate) mod reqwest;
pub(crate) mod wrapped;

View file

@ -0,0 +1,226 @@
//! Creates a Reqwest client.
pub use reqwest::{
header::{self, HeaderValue},
Client as ReqwestClient, Error as ReqwestError,
};
use icann_rdap_common::media_types::{JSON_MEDIA_TYPE, RDAP_MEDIA_TYPE};
#[cfg(not(target_arch = "wasm32"))]
use {icann_rdap_common::VERSION, std::net::SocketAddr, std::time::Duration};
const ACCEPT_HEADER_VALUES: &str = const_format::formatcp!("{RDAP_MEDIA_TYPE}, {JSON_MEDIA_TYPE}");
/// Configures the HTTP client.
pub struct ReqwestClientConfig {
/// This string is appended to the user agent.
///
/// It is provided so
/// library users may identify their programs.
/// This is ignored on wasm32.
pub user_agent_suffix: String,
/// If set to true, connections will be required to use HTTPS.
///
/// This is ignored on wasm32.
pub https_only: bool,
/// If set to true, invalid host names will be accepted.
///
/// This is ignored on wasm32.
pub accept_invalid_host_names: bool,
/// If set to true, invalid certificates will be accepted.
///
/// This is ignored on wasm32.
pub accept_invalid_certificates: bool,
/// If true, HTTP redirects will be followed.
///
/// This is ignored on wasm32.
pub follow_redirects: bool,
/// Specify Host
pub host: Option<HeaderValue>,
/// Specify the value of the origin header.
///
/// Most browsers ignore this by default.
pub origin: Option<HeaderValue>,
/// Query timeout in seconds.
///
/// This corresponds to the total timeout of the request (connection plus reading all the data).
///
/// This is ignored on wasm32.
pub timeout_secs: u64,
}
impl Default for ReqwestClientConfig {
fn default() -> Self {
Self {
user_agent_suffix: "library".to_string(),
https_only: true,
accept_invalid_host_names: false,
accept_invalid_certificates: false,
follow_redirects: true,
host: None,
origin: None,
timeout_secs: 60,
}
}
}
#[buildstructor::buildstructor]
impl ReqwestClientConfig {
#[builder]
#[allow(clippy::too_many_arguments)]
pub fn new(
user_agent_suffix: Option<String>,
https_only: Option<bool>,
accept_invalid_host_names: Option<bool>,
accept_invalid_certificates: Option<bool>,
follow_redirects: Option<bool>,
host: Option<HeaderValue>,
origin: Option<HeaderValue>,
timeout_secs: Option<u64>,
) -> Self {
let default = Self::default();
Self {
user_agent_suffix: user_agent_suffix.unwrap_or(default.user_agent_suffix),
https_only: https_only.unwrap_or(default.https_only),
accept_invalid_host_names: accept_invalid_host_names
.unwrap_or(default.accept_invalid_host_names),
accept_invalid_certificates: accept_invalid_certificates
.unwrap_or(default.accept_invalid_certificates),
follow_redirects: follow_redirects.unwrap_or(default.follow_redirects),
host,
origin,
timeout_secs: timeout_secs.unwrap_or(default.timeout_secs),
}
}
#[builder(entry = "from_config", exit = "build")]
#[allow(clippy::too_many_arguments)]
pub fn new_from_config(
&self,
user_agent_suffix: Option<String>,
https_only: Option<bool>,
accept_invalid_host_names: Option<bool>,
accept_invalid_certificates: Option<bool>,
follow_redirects: Option<bool>,
host: Option<HeaderValue>,
origin: Option<HeaderValue>,
timeout_secs: Option<u64>,
) -> Self {
Self {
user_agent_suffix: user_agent_suffix.unwrap_or(self.user_agent_suffix.clone()),
https_only: https_only.unwrap_or(self.https_only),
accept_invalid_host_names: accept_invalid_host_names
.unwrap_or(self.accept_invalid_host_names),
accept_invalid_certificates: accept_invalid_certificates
.unwrap_or(self.accept_invalid_certificates),
follow_redirects: follow_redirects.unwrap_or(self.follow_redirects),
host: host.map_or(self.host.clone(), Some),
origin: origin.map_or(self.origin.clone(), Some),
timeout_secs: timeout_secs.unwrap_or(self.timeout_secs),
}
}
}
/// Creates an HTTP client using Reqwest. The Reqwest
/// client holds its own connection pools, so in many
/// uses cases creating only one client per process is
/// necessary.
#[cfg(not(target_arch = "wasm32"))]
pub fn create_reqwest_client(config: &ReqwestClientConfig) -> Result<ReqwestClient, ReqwestError> {
let default_headers = default_headers(config);
let mut client = reqwest::Client::builder();
let redirects = if config.follow_redirects {
reqwest::redirect::Policy::default()
} else {
reqwest::redirect::Policy::none()
};
client = client
.timeout(Duration::from_secs(config.timeout_secs))
.user_agent(format!(
"icann_rdap client {VERSION} {}",
config.user_agent_suffix
))
.redirect(redirects)
.https_only(config.https_only)
.danger_accept_invalid_hostnames(config.accept_invalid_host_names)
.danger_accept_invalid_certs(config.accept_invalid_certificates);
let client = client.default_headers(default_headers).build()?;
Ok(client)
}
/// Creates an HTTP client using Reqwest. The Reqwest
/// client holds its own connection pools, so in many
/// uses cases creating only one client per process is
/// necessary.
#[cfg(not(target_arch = "wasm32"))]
pub fn create_reqwest_client_with_addr(
config: &ReqwestClientConfig,
domain: &str,
addr: SocketAddr,
) -> Result<ReqwestClient, ReqwestError> {
let default_headers = default_headers(config);
let mut client = reqwest::Client::builder();
let redirects = if config.follow_redirects {
reqwest::redirect::Policy::default()
} else {
reqwest::redirect::Policy::none()
};
client = client
.timeout(Duration::from_secs(config.timeout_secs))
.user_agent(format!(
"icann_rdap client {VERSION} {}",
config.user_agent_suffix
))
.redirect(redirects)
.https_only(config.https_only)
.danger_accept_invalid_hostnames(config.accept_invalid_host_names)
.danger_accept_invalid_certs(config.accept_invalid_certificates)
.resolve(domain, addr);
let client = client.default_headers(default_headers).build()?;
Ok(client)
}
/// Creates an HTTP client using Reqwest. The Reqwest
/// client holds its own connection pools, so in many
/// uses cases creating only one client per process is
/// necessary.
/// Note that the WASM version does not set redirect policy,
/// https_only, or TLS settings.
#[cfg(target_arch = "wasm32")]
pub fn create_reqwest_client(config: &ReqwestClientConfig) -> Result<ReqwestClient, ReqwestError> {
let default_headers = default_headers(config);
let client = reqwest::Client::builder();
let client = client.default_headers(default_headers).build()?;
Ok(client)
}
fn default_headers(config: &ReqwestClientConfig) -> header::HeaderMap {
let mut default_headers = header::HeaderMap::new();
default_headers.insert(
header::ACCEPT,
HeaderValue::from_static(ACCEPT_HEADER_VALUES),
);
if let Some(host) = &config.host {
default_headers.insert(header::HOST, host.into());
};
if let Some(origin) = &config.origin {
default_headers.insert(header::ORIGIN, origin.into());
}
default_headers
}

View file

@ -0,0 +1,297 @@
//! Wrapped Client.
pub use reqwest::{header::HeaderValue, Client as ReqwestClient, Error as ReqwestError};
use {
icann_rdap_common::httpdata::HttpData,
reqwest::header::{
ACCESS_CONTROL_ALLOW_ORIGIN, CACHE_CONTROL, CONTENT_TYPE, EXPIRES, LOCATION, RETRY_AFTER,
STRICT_TRANSPORT_SECURITY,
},
};
use {
super::{create_reqwest_client, ReqwestClientConfig},
crate::RdapClientError,
};
#[cfg(not(target_arch = "wasm32"))]
use {
super::create_reqwest_client_with_addr, chrono::DateTime, chrono::Utc, reqwest::StatusCode,
std::net::SocketAddr, tracing::debug, tracing::info,
};
/// Used by the request functions.
#[derive(Clone, Copy)]
pub struct RequestOptions {
pub(crate) max_retry_secs: u32,
pub(crate) def_retry_secs: u32,
pub(crate) max_retries: u16,
}
impl Default for RequestOptions {
fn default() -> Self {
Self {
max_retry_secs: 120,
def_retry_secs: 60,
max_retries: 1,
}
}
}
/// Configures the HTTP client.
#[derive(Default)]
pub struct ClientConfig {
/// Config for the Reqwest client.
client_config: ReqwestClientConfig,
/// Request options.
request_options: RequestOptions,
}
#[buildstructor::buildstructor]
impl ClientConfig {
#[builder]
#[allow(clippy::too_many_arguments)]
pub fn new(
user_agent_suffix: Option<String>,
https_only: Option<bool>,
accept_invalid_host_names: Option<bool>,
accept_invalid_certificates: Option<bool>,
follow_redirects: Option<bool>,
host: Option<HeaderValue>,
origin: Option<HeaderValue>,
timeout_secs: Option<u64>,
max_retry_secs: Option<u32>,
def_retry_secs: Option<u32>,
max_retries: Option<u16>,
) -> Self {
let default_cc = ReqwestClientConfig::default();
let default_ro = RequestOptions::default();
Self {
client_config: ReqwestClientConfig {
user_agent_suffix: user_agent_suffix.unwrap_or(default_cc.user_agent_suffix),
https_only: https_only.unwrap_or(default_cc.https_only),
accept_invalid_host_names: accept_invalid_host_names
.unwrap_or(default_cc.accept_invalid_host_names),
accept_invalid_certificates: accept_invalid_certificates
.unwrap_or(default_cc.accept_invalid_certificates),
follow_redirects: follow_redirects.unwrap_or(default_cc.follow_redirects),
host,
origin,
timeout_secs: timeout_secs.unwrap_or(default_cc.timeout_secs),
},
request_options: RequestOptions {
max_retry_secs: max_retry_secs.unwrap_or(default_ro.max_retry_secs),
def_retry_secs: def_retry_secs.unwrap_or(default_ro.def_retry_secs),
max_retries: max_retries.unwrap_or(default_ro.max_retries),
},
}
}
#[builder(entry = "from_config", exit = "build")]
#[allow(clippy::too_many_arguments)]
pub fn new_from_config(
&self,
user_agent_suffix: Option<String>,
https_only: Option<bool>,
accept_invalid_host_names: Option<bool>,
accept_invalid_certificates: Option<bool>,
follow_redirects: Option<bool>,
host: Option<HeaderValue>,
origin: Option<HeaderValue>,
timeout_secs: Option<u64>,
max_retry_secs: Option<u32>,
def_retry_secs: Option<u32>,
max_retries: Option<u16>,
) -> Self {
Self {
client_config: ReqwestClientConfig {
user_agent_suffix: user_agent_suffix
.unwrap_or(self.client_config.user_agent_suffix.clone()),
https_only: https_only.unwrap_or(self.client_config.https_only),
accept_invalid_host_names: accept_invalid_host_names
.unwrap_or(self.client_config.accept_invalid_host_names),
accept_invalid_certificates: accept_invalid_certificates
.unwrap_or(self.client_config.accept_invalid_certificates),
follow_redirects: follow_redirects.unwrap_or(self.client_config.follow_redirects),
host: host.map_or(self.client_config.host.clone(), Some),
origin: origin.map_or(self.client_config.origin.clone(), Some),
timeout_secs: timeout_secs.unwrap_or(self.client_config.timeout_secs),
},
request_options: RequestOptions {
max_retry_secs: max_retry_secs.unwrap_or(self.request_options.max_retry_secs),
def_retry_secs: def_retry_secs.unwrap_or(self.request_options.def_retry_secs),
max_retries: max_retries.unwrap_or(self.request_options.max_retries),
},
}
}
}
/// A wrapper around Reqwest client to give additional features when used with the request functions.
pub struct Client {
/// The reqwest client.
pub(crate) reqwest_client: ReqwestClient,
/// Request options.
pub(crate) request_options: RequestOptions,
}
impl Client {
pub fn new(reqwest_client: ReqwestClient, request_options: RequestOptions) -> Self {
Self {
reqwest_client,
request_options,
}
}
}
/// Creates a wrapped HTTP client. The wrapped
/// client holds its own connection pools, so in many
/// uses cases creating only one client per process is
/// necessary.
pub fn create_client(config: &ClientConfig) -> Result<Client, RdapClientError> {
let client = create_reqwest_client(&config.client_config)?;
Ok(Client::new(client, config.request_options))
}
/// Creates a wrapped HTTP client.
/// This will direct the underlying client to connect to a specific socket.
#[cfg(not(target_arch = "wasm32"))]
pub fn create_client_with_addr(
config: &ClientConfig,
domain: &str,
addr: SocketAddr,
) -> Result<Client, RdapClientError> {
let client = create_reqwest_client_with_addr(&config.client_config, domain, addr)?;
Ok(Client::new(client, config.request_options))
}
pub(crate) struct WrappedResponse {
pub(crate) http_data: HttpData,
pub(crate) text: String,
}
pub(crate) async fn wrapped_request(
request_uri: &str,
client: &Client,
) -> Result<WrappedResponse, ReqwestError> {
// send request and loop for possible retries
#[allow(unused_mut)] //because of wasm32 exclusion below
let mut response = client.reqwest_client.get(request_uri).send().await?;
// this doesn't work on wasm32 because tokio doesn't work on wasm
#[cfg(not(target_arch = "wasm32"))]
{
let mut tries: u16 = 0;
loop {
debug!("HTTP version: {:?}", response.version());
// don't repeat the request
if !matches!(response.status(), StatusCode::TOO_MANY_REQUESTS) {
break;
}
// loop if HTTP 429
let retry_after_header = response
.headers()
.get(RETRY_AFTER)
.map(|value| value.to_str().unwrap().to_string());
let retry_after = if let Some(rt) = retry_after_header {
info!("Server says too many requests and to retry-after '{rt}'.");
rt
} else {
info!("Server says too many requests but does not offer 'retry-after' value.");
client.request_options.def_retry_secs.to_string()
};
let mut wait_time_seconds = if let Ok(date) = DateTime::parse_from_rfc2822(&retry_after)
{
(date.with_timezone(&Utc) - Utc::now()).num_seconds() as u64
} else if let Ok(seconds) = retry_after.parse::<u64>() {
seconds
} else {
info!(
"Unable to parse retry-after header value. Using {}",
client.request_options.def_retry_secs
);
client.request_options.def_retry_secs.into()
};
if wait_time_seconds == 0 {
info!("Given {wait_time_seconds} for retry-after. Does not make sense.");
wait_time_seconds = client.request_options.def_retry_secs as u64;
}
if wait_time_seconds > client.request_options.max_retry_secs as u64 {
info!(
"Server is asking to wait longer than configured max of {}.",
client.request_options.max_retry_secs
);
wait_time_seconds = client.request_options.max_retry_secs as u64;
}
info!("Waiting {wait_time_seconds} seconds to retry.");
tokio::time::sleep(tokio::time::Duration::from_secs(wait_time_seconds + 1)).await;
tries += 1;
if tries > client.request_options.max_retries {
info!("Max query retries reached.");
break;
} else {
// send the query again
response = client.reqwest_client.get(request_uri).send().await?;
}
}
}
// throw an error if not 200 OK
let response = response.error_for_status()?;
// get the response
let content_type = response
.headers()
.get(CONTENT_TYPE)
.map(|value| value.to_str().unwrap().to_string());
let expires = response
.headers()
.get(EXPIRES)
.map(|value| value.to_str().unwrap().to_string());
let cache_control = response
.headers()
.get(CACHE_CONTROL)
.map(|value| value.to_str().unwrap().to_string());
let location = response
.headers()
.get(LOCATION)
.map(|value| value.to_str().unwrap().to_string());
let access_control_allow_origin = response
.headers()
.get(ACCESS_CONTROL_ALLOW_ORIGIN)
.map(|value| value.to_str().unwrap().to_string());
let strict_transport_security = response
.headers()
.get(STRICT_TRANSPORT_SECURITY)
.map(|value| value.to_str().unwrap().to_string());
let retry_after = response
.headers()
.get(RETRY_AFTER)
.map(|value| value.to_str().unwrap().to_string());
let content_length = response.content_length();
let status_code = response.status().as_u16();
let url = response.url().to_owned();
let text = response.text().await?;
let http_data = HttpData::now()
.status_code(status_code)
.and_location(location)
.and_content_length(content_length)
.and_content_type(content_type)
.scheme(url.scheme())
.host(
url.host_str()
.expect("URL has no host. This shouldn't happen.")
.to_owned(),
)
.and_expires(expires)
.and_cache_control(cache_control)
.and_access_control_allow_origin(access_control_allow_origin)
.and_strict_transport_security(strict_transport_security)
.and_retry_after(retry_after)
.request_uri(request_uri)
.build();
Ok(WrappedResponse { http_data, text })
}

View file

@ -0,0 +1,623 @@
//! Does RDAP query bootstrapping.
use std::sync::{Arc, RwLock};
use icann_rdap_common::{
httpdata::HttpData,
iana::{
get_preferred_url, BootstrapRegistry, BootstrapRegistryError, IanaRegistry,
IanaRegistryType,
},
};
use crate::{http::Client, iana::iana_request::iana_request, rdap::QueryType, RdapClientError};
const SECONDS_IN_WEEK: i64 = 604800;
/// Defines a trait for things that store bootstrap registries.
pub trait BootstrapStore: Send + Sync {
/// Called when store is checked to see if it has a valid bootstrap registry.
///
/// This method should return false (i.e. `Ok(false)``) if the registry doesn't
/// exist in the store or if the registry in the store is out-of-date (such as
/// the cache control data indicates it is old).
fn has_bootstrap_registry(&self, reg_type: &IanaRegistryType) -> Result<bool, RdapClientError>;
/// Puts a registry into the bootstrap registry store.
fn put_bootstrap_registry(
&self,
reg_type: &IanaRegistryType,
registry: IanaRegistry,
http_data: HttpData,
) -> Result<(), RdapClientError>;
/// Get the urls for a domain or nameserver (which are domain names) query type.
///
/// The default method should be good enough for most trait implementations.
fn get_domain_query_urls(
&self,
query_type: &QueryType,
) -> Result<Vec<String>, RdapClientError> {
let domain_name = match query_type {
QueryType::Domain(domain) => domain.to_ascii(),
QueryType::Nameserver(ns) => ns.to_ascii(),
_ => panic!("invalid domain query type"),
};
self.get_dns_urls(domain_name)
}
/// Get the urls for an autnum query type.
///
/// The default method should be good enough for most trait implementations.
fn get_autnum_query_urls(
&self,
query_type: &QueryType,
) -> Result<Vec<String>, RdapClientError> {
let QueryType::AsNumber(asn) = query_type else {
panic!("invalid query type")
};
self.get_asn_urls(asn.to_string().as_str())
}
/// Get the urls for an IPv4 query type.
///
/// The default method should be good enough for most trait implementations.
fn get_ipv4_query_urls(&self, query_type: &QueryType) -> Result<Vec<String>, RdapClientError> {
let ip = match query_type {
QueryType::IpV4Addr(addr) => format!("{addr}/32"),
QueryType::IpV4Cidr(cidr) => cidr.to_string(),
_ => panic!("non ip query for ip bootstrap"),
};
self.get_ipv4_urls(&ip)
}
/// Get the urls for an IPv6 query type.
///
/// The default method should be good enough for most trait implementations.
fn get_ipv6_query_urls(&self, query_type: &QueryType) -> Result<Vec<String>, RdapClientError> {
let ip = match query_type {
QueryType::IpV6Addr(addr) => format!("{addr}/128"),
QueryType::IpV6Cidr(cidr) => cidr.to_string(),
_ => panic!("non ip query for ip bootstrap"),
};
self.get_ipv6_urls(&ip)
}
/// Get the urls for an entity handle query type.
///
/// The default method should be good enough for most trait implementations.
fn get_entity_handle_query_urls(
&self,
query_type: &QueryType,
) -> Result<Vec<String>, RdapClientError> {
let QueryType::Entity(handle) = query_type else {
panic!("non entity handle for bootstrap")
};
let handle_split = handle
.rsplit_once('-')
.ok_or(BootstrapRegistryError::InvalidBootstrapInput)?;
self.get_tag_query_urls(handle_split.1)
}
/// Get the urls for an object tag query type.
///
/// The default method should be good enough for most trait implementations.
fn get_tag_query_urls(&self, tag: &str) -> Result<Vec<String>, RdapClientError> {
self.get_tag_urls(tag)
}
/// Get the URLs associated with the IANA RDAP DNS bootstrap.
///
/// Implementations should implement the logic to pull the [icann_rdap_common::iana::IanaRegistry]
/// and ultimately call its [icann_rdap_common::iana::IanaRegistry::get_dns_bootstrap_urls] method.
fn get_dns_urls(&self, ldh: &str) -> Result<Vec<String>, RdapClientError>;
/// Get the URLs associated with the IANA RDAP ASN bootstrap.
///
/// Implementations should implement the logic to pull the [icann_rdap_common::iana::IanaRegistry]
/// and ultimately call its [icann_rdap_common::iana::IanaRegistry::get_asn_bootstrap_urls] method.
fn get_asn_urls(&self, asn: &str) -> Result<Vec<String>, RdapClientError>;
/// Get the URLs associated with the IANA RDAP IPv4 bootstrap.
///
/// Implementations should implement the logic to pull the [icann_rdap_common::iana::IanaRegistry]
/// and ultimately call its [icann_rdap_common::iana::IanaRegistry::get_ipv4_bootstrap_urls] method.
fn get_ipv4_urls(&self, ipv4: &str) -> Result<Vec<String>, RdapClientError>;
/// Get the URLs associated with the IANA RDAP IPv6 bootstrap.
///
/// Implementations should implement the logic to pull the [icann_rdap_common::iana::IanaRegistry]
/// and ultimately call its [icann_rdap_common::iana::IanaRegistry::get_ipv6_bootstrap_urls] method.
fn get_ipv6_urls(&self, ipv6: &str) -> Result<Vec<String>, RdapClientError>;
/// Get the URLs associated with the IANA RDAP Object Tags bootstrap.
///
/// Implementations should implement the logic to pull the [icann_rdap_common::iana::IanaRegistry]
/// and ultimately call its [icann_rdap_common::iana::IanaRegistry::get_tag_bootstrap_urls] method.
fn get_tag_urls(&self, tag: &str) -> Result<Vec<String>, RdapClientError>;
}
/// A trait to find the preferred URL from a bootstrap service.
pub trait PreferredUrl {
fn preferred_url(self) -> Result<String, RdapClientError>;
}
impl PreferredUrl for Vec<String> {
fn preferred_url(self) -> Result<String, RdapClientError> {
Ok(get_preferred_url(self)?)
}
}
/// A bootstrap registry store backed by memory.
///
/// This implementation of [BootstrapStore] keeps registries in memory. Every new instance starts with
/// no registries in memory. They are added and maintained over time by calls to [MemoryBootstrapStore::put_bootstrap_registry()] by the
/// machinery of [crate::rdap::request::rdap_bootstrapped_request()] and [crate::iana::bootstrap::qtype_to_bootstrap_url()].
///
/// Ideally, this should be kept in the same scope as [reqwest::Client].
pub struct MemoryBootstrapStore {
ipv4: Arc<RwLock<Option<(IanaRegistry, HttpData)>>>,
ipv6: Arc<RwLock<Option<(IanaRegistry, HttpData)>>>,
autnum: Arc<RwLock<Option<(IanaRegistry, HttpData)>>>,
dns: Arc<RwLock<Option<(IanaRegistry, HttpData)>>>,
tag: Arc<RwLock<Option<(IanaRegistry, HttpData)>>>,
}
unsafe impl Send for MemoryBootstrapStore {}
unsafe impl Sync for MemoryBootstrapStore {}
impl Default for MemoryBootstrapStore {
fn default() -> Self {
Self::new()
}
}
impl MemoryBootstrapStore {
pub fn new() -> Self {
Self {
ipv4: <_>::default(),
ipv6: <_>::default(),
autnum: <_>::default(),
dns: <_>::default(),
tag: <_>::default(),
}
}
}
impl BootstrapStore for MemoryBootstrapStore {
fn has_bootstrap_registry(&self, reg_type: &IanaRegistryType) -> Result<bool, RdapClientError> {
Ok(match reg_type {
IanaRegistryType::RdapBootstrapDns => self.dns.read()?.registry_has_not_expired(),
IanaRegistryType::RdapBootstrapAsn => self.autnum.read()?.registry_has_not_expired(),
IanaRegistryType::RdapBootstrapIpv4 => self.ipv4.read()?.registry_has_not_expired(),
IanaRegistryType::RdapBootstrapIpv6 => self.ipv6.read()?.registry_has_not_expired(),
IanaRegistryType::RdapObjectTags => self.tag.read()?.registry_has_not_expired(),
})
}
fn put_bootstrap_registry(
&self,
reg_type: &IanaRegistryType,
registry: IanaRegistry,
http_data: HttpData,
) -> Result<(), RdapClientError> {
match reg_type {
IanaRegistryType::RdapBootstrapDns => {
let mut g = self.dns.write()?;
*g = Some((registry, http_data));
}
IanaRegistryType::RdapBootstrapAsn => {
let mut g = self.autnum.write()?;
*g = Some((registry, http_data));
}
IanaRegistryType::RdapBootstrapIpv4 => {
let mut g = self.ipv4.write()?;
*g = Some((registry, http_data));
}
IanaRegistryType::RdapBootstrapIpv6 => {
let mut g = self.ipv6.write()?;
*g = Some((registry, http_data));
}
IanaRegistryType::RdapObjectTags => {
let mut g = self.tag.write()?;
*g = Some((registry, http_data));
}
};
Ok(())
}
fn get_dns_urls(&self, ldh: &str) -> Result<Vec<String>, RdapClientError> {
if let Some((iana, _http_data)) = self.dns.read()?.as_ref() {
Ok(iana.get_dns_bootstrap_urls(ldh)?)
} else {
Err(RdapClientError::BootstrapUnavailable)
}
}
fn get_asn_urls(&self, asn: &str) -> Result<Vec<String>, RdapClientError> {
if let Some((iana, _http_data)) = self.autnum.read()?.as_ref() {
Ok(iana.get_asn_bootstrap_urls(asn)?)
} else {
Err(RdapClientError::BootstrapUnavailable)
}
}
fn get_ipv4_urls(&self, ipv4: &str) -> Result<Vec<String>, RdapClientError> {
if let Some((iana, _http_data)) = self.ipv4.read()?.as_ref() {
Ok(iana.get_ipv4_bootstrap_urls(ipv4)?)
} else {
Err(RdapClientError::BootstrapUnavailable)
}
}
fn get_ipv6_urls(&self, ipv6: &str) -> Result<Vec<String>, RdapClientError> {
if let Some((iana, _http_data)) = self.ipv6.read()?.as_ref() {
Ok(iana.get_ipv6_bootstrap_urls(ipv6)?)
} else {
Err(RdapClientError::BootstrapUnavailable)
}
}
fn get_tag_urls(&self, tag: &str) -> Result<Vec<String>, RdapClientError> {
if let Some((iana, _http_data)) = self.tag.read()?.as_ref() {
Ok(iana.get_tag_bootstrap_urls(tag)?)
} else {
Err(RdapClientError::BootstrapUnavailable)
}
}
}
/// Trait to determine if a bootstrap registry is past its expiration (i.e. needs to be rechecked).
pub trait RegistryHasNotExpired {
fn registry_has_not_expired(&self) -> bool;
}
impl RegistryHasNotExpired for Option<(IanaRegistry, HttpData)> {
fn registry_has_not_expired(&self) -> bool {
if let Some((_iana, http_data)) = self {
!http_data.is_expired(SECONDS_IN_WEEK)
} else {
false
}
}
}
/// Given a [QueryType], it will get the bootstrap URL.
pub async fn qtype_to_bootstrap_url<F>(
client: &Client,
store: &dyn BootstrapStore,
query_type: &QueryType,
callback: F,
) -> Result<String, RdapClientError>
where
F: FnOnce(&IanaRegistryType),
{
match query_type {
QueryType::IpV4Addr(_) | QueryType::IpV4Cidr(_) => {
fetch_bootstrap(
&IanaRegistryType::RdapBootstrapIpv4,
client,
store,
callback,
)
.await?;
Ok(store.get_ipv4_query_urls(query_type)?.preferred_url()?)
}
QueryType::IpV6Addr(_) | QueryType::IpV6Cidr(_) => {
fetch_bootstrap(
&IanaRegistryType::RdapBootstrapIpv6,
client,
store,
callback,
)
.await?;
Ok(store.get_ipv6_query_urls(query_type)?.preferred_url()?)
}
QueryType::AsNumber(_) => {
fetch_bootstrap(&IanaRegistryType::RdapBootstrapAsn, client, store, callback).await?;
Ok(store.get_autnum_query_urls(query_type)?.preferred_url()?)
}
QueryType::Domain(_) => {
fetch_bootstrap(&IanaRegistryType::RdapBootstrapDns, client, store, callback).await?;
Ok(store.get_domain_query_urls(query_type)?.preferred_url()?)
}
QueryType::Entity(_) => {
fetch_bootstrap(&IanaRegistryType::RdapObjectTags, client, store, callback).await?;
Ok(store
.get_entity_handle_query_urls(query_type)?
.preferred_url()?)
}
QueryType::Nameserver(_) => {
fetch_bootstrap(&IanaRegistryType::RdapBootstrapDns, client, store, callback).await?;
Ok(store.get_domain_query_urls(query_type)?.preferred_url()?)
}
_ => Err(RdapClientError::BootstrapUnavailable),
}
}
/// Fetches a bootstrap registry for a [BootstrapStore].
pub async fn fetch_bootstrap<F>(
reg_type: &IanaRegistryType,
client: &Client,
store: &dyn BootstrapStore,
callback: F,
) -> Result<(), RdapClientError>
where
F: FnOnce(&IanaRegistryType),
{
if !store.has_bootstrap_registry(reg_type)? {
callback(reg_type);
let iana_resp = iana_request(reg_type.clone(), client).await?;
store.put_bootstrap_registry(reg_type, iana_resp.registry, iana_resp.http_data)?;
}
Ok(())
}
#[cfg(test)]
#[allow(non_snake_case)]
mod test {
use icann_rdap_common::{
httpdata::HttpData,
iana::{IanaRegistry, IanaRegistryType},
};
use crate::{iana::bootstrap::PreferredUrl, rdap::QueryType};
use super::{BootstrapStore, MemoryBootstrapStore};
#[test]
fn GIVEN_membootstrap_with_dns_WHEN_get_domain_query_url_THEN_correct_url() {
// GIVEN
let mem = MemoryBootstrapStore::new();
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");
mem.put_bootstrap_registry(
&IanaRegistryType::RdapBootstrapDns,
iana,
HttpData::example().build(),
)
.expect("put iana registry");
// WHEN
let actual = mem
.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]
fn GIVEN_membootstrap_with_autnum_WHEN_get_autnum_query_url_THEN_correct_url() {
// GIVEN
let mem = MemoryBootstrapStore::new();
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");
mem.put_bootstrap_registry(
&IanaRegistryType::RdapBootstrapAsn,
iana,
HttpData::example().build(),
)
.expect("put iana registry");
// WHEN
let actual = mem
.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]
fn GIVEN_membootstrap_with_ipv4_THEN_get_ipv4_query_urls_THEN_correct_url() {
// GIVEN
let mem = MemoryBootstrapStore::new();
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");
mem.put_bootstrap_registry(
&IanaRegistryType::RdapBootstrapIpv4,
iana,
HttpData::example().build(),
)
.expect("put iana registry");
// WHEN
let actual = mem
.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]
fn GIVEN_membootstrap_with_ipv6_THEN_get_ipv6_query_urls_THEN_correct_url() {
// GIVEN
let mem = MemoryBootstrapStore::new();
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");
mem.put_bootstrap_registry(
&IanaRegistryType::RdapBootstrapIpv6,
iana,
HttpData::example().build(),
)
.expect("put iana registry");
// WHEN
let actual = mem
.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]
fn GIVEN_membootstrap_with_tag_THEN_get_tag_query_urls_THEN_correct_url() {
// GIVEN
let mem = MemoryBootstrapStore::new();
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");
mem.put_bootstrap_registry(
&IanaRegistryType::RdapObjectTags,
iana,
HttpData::example().build(),
)
.expect("put iana registry");
// WHEN
let actual = mem
.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,48 @@
//! The IANA RDAP Bootstrap Registries.
use {
icann_rdap_common::{
httpdata::HttpData,
iana::{IanaRegistry, IanaRegistryType, RdapBootstrapRegistry},
},
serde::{Deserialize, Serialize},
thiserror::Error,
};
use crate::http::{wrapped_request, Client};
/// Response from getting an IANA registry.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct IanaResponse {
pub registry: IanaRegistry,
pub registry_type: IanaRegistryType,
pub http_data: HttpData,
}
/// Errors from issuing a request to get an IANA registry.
#[derive(Debug, Error)]
pub enum IanaResponseError {
#[error(transparent)]
Reqwest(#[from] reqwest::Error),
#[error(transparent)]
SerdeJson(#[from] serde_json::Error),
}
/// Issues the HTTP request to get an IANA registry.
pub async fn iana_request(
registry_type: IanaRegistryType,
client: &Client,
) -> Result<IanaResponse, IanaResponseError> {
let url = registry_type.url();
let wrapped_response = wrapped_request(url, client).await?;
let text = wrapped_response.text;
let http_data = wrapped_response.http_data;
let json: RdapBootstrapRegistry = serde_json::from_str(&text)?;
Ok(IanaResponse {
registry: IanaRegistry::RdapBootstrapRegistry(json),
registry_type,
http_data,
})
}

View file

@ -0,0 +1,9 @@
//! IANA and RDAP Bootstrapping
#[doc(inline)]
pub use bootstrap::*;
#[doc(inline)]
pub use iana_request::*;
pub(crate) mod bootstrap;
pub(crate) mod iana_request;

View file

@ -0,0 +1,112 @@
#![allow(dead_code)] // TODO remove this at some point
#![allow(rustdoc::bare_urls)]
#![doc = include_str!("../README.md")]
use std::{fmt::Display, sync::PoisonError};
use {
iana::iana_request::IanaResponseError,
icann_rdap_common::{
dns_types::DomainNameError, httpdata::HttpData, iana::BootstrapRegistryError,
response::RdapResponseError,
},
thiserror::Error,
};
pub mod gtld;
pub mod http;
pub mod iana;
pub mod md;
pub mod rdap;
/// Basics necesasry for a simple clients.
pub mod prelude {
#[doc(inline)]
pub use crate::http::create_client;
#[doc(inline)]
pub use crate::http::ClientConfig;
#[doc(inline)]
pub use crate::iana::MemoryBootstrapStore;
#[doc(inline)]
pub use crate::rdap::rdap_bootstrapped_request;
#[doc(inline)]
pub use crate::rdap::rdap_request;
#[doc(inline)]
pub use crate::rdap::rdap_url_request;
#[doc(inline)]
pub use crate::rdap::QueryType;
#[doc(inline)]
pub use crate::RdapClientError;
}
/// Error returned by RDAP client functions and methods.
#[derive(Error, Debug)]
pub enum RdapClientError {
#[error("Query value is not valid.")]
InvalidQueryValue,
#[error("Ambiquous query type.")]
AmbiquousQueryType,
#[error(transparent)]
Response(#[from] RdapResponseError),
#[error(transparent)]
Client(#[from] reqwest::Error),
#[error("Error parsing response")]
ParsingError(Box<ParsingErrorInfo>),
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error("RwLock Poison Error")]
Poison,
#[error("Bootstrap unavailable")]
BootstrapUnavailable,
#[error(transparent)]
BootstrapError(#[from] BootstrapRegistryError),
#[error(transparent)]
IanaResponse(#[from] IanaResponseError),
#[error(transparent)]
IoError(#[from] std::io::Error),
#[error(transparent)]
DomainNameError(#[from] DomainNameError),
}
impl<T> From<PoisonError<T>> for RdapClientError {
fn from(_err: PoisonError<T>) -> Self {
Self::Poison
}
}
/// Describes the error that occurs when parsing RDAP responses.
#[derive(Debug)]
pub struct ParsingErrorInfo {
pub text: String,
pub http_data: HttpData,
pub error: serde_json::Error,
}
impl Display for ParsingErrorInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Error: {}\n,Content Length: {}\nContent Type: {}\nUrl: {}\nText:\n{}\n",
self.error,
self.http_data
.content_length
.map_or("No content length given".to_string(), |n| n.to_string()),
self.http_data
.content_type
.clone()
.unwrap_or("No content type given".to_string()),
self.http_data.host,
self.text
)
}
}

View file

@ -0,0 +1,111 @@
use std::any::TypeId;
use icann_rdap_common::{
check::{CheckParams, GetChecks, GetSubChecks},
response::Autnum,
};
use super::{
string::StringUtil,
table::{MultiPartTable, ToMpTable},
types::checks_to_table,
FromMd, MdHeaderText, MdParams, MdUtil, ToMd, HR,
};
impl ToMd for Autnum {
fn to_md(&self, params: MdParams) -> String {
let typeid = TypeId::of::<Self>();
let mut md = String::new();
md.push_str(&self.common.to_md(params.from_parent(typeid)));
let header_text = self.get_header_text();
md.push_str(
&header_text
.to_string()
.to_header(params.heading_level, params.options),
);
// multipart data
let mut table = MultiPartTable::new();
// summary
table = table.summary(header_text);
// identifiers
table = table
.header_ref(&"Identifiers")
.and_nv_ref(
&"Start AS Number",
&self.start_autnum.as_ref().map(|n| n.to_string()),
)
.and_nv_ref(
&"End AS Number",
&self.end_autnum.as_ref().map(|n| n.to_string()),
)
.and_nv_ref(&"Handle", &self.object_common.handle)
.and_nv_ref(&"Autnum Type", &self.autnum_type)
.and_nv_ref(&"Autnum Name", &self.name)
.and_nv_ref(&"Country", &self.country);
// common object stuff
table = self.object_common.add_to_mptable(table, params);
// checks
let check_params = CheckParams::from_md(params, typeid);
let mut checks = self.object_common.get_sub_checks(check_params);
checks.push(self.get_checks(check_params));
table = checks_to_table(checks, table, params);
// render table
md.push_str(&table.to_md(params));
// remarks
md.push_str(&self.object_common.remarks.to_md(params.from_parent(typeid)));
// only other object classes from here
md.push_str(HR);
// entities
md.push_str(
&self
.object_common
.entities
.to_md(params.from_parent(typeid)),
);
// redacted
if let Some(redacted) = &self.object_common.redacted {
md.push_str(&redacted.as_slice().to_md(params.from_parent(typeid)));
}
md.push('\n');
md
}
}
impl MdUtil for Autnum {
fn get_header_text(&self) -> MdHeaderText {
let header_text = if self.start_autnum.is_some() && self.end_autnum.is_some() {
format!(
"Autonomous Systems {} - {}",
&self.start_autnum.as_ref().unwrap().replace_md_chars(),
&self.end_autnum.as_ref().unwrap().replace_md_chars()
)
} else if let Some(start_autnum) = &self.start_autnum {
format!("Autonomous System {}", start_autnum.replace_md_chars())
} else if let Some(handle) = &self.object_common.handle {
format!("Autonomous System {}", handle.replace_md_chars())
} else if let Some(name) = &self.name {
format!("Autonomous System {}", name.replace_md_chars())
} else {
"Autonomous System".to_string()
};
let mut header_text = MdHeaderText::builder().header_text(header_text);
if let Some(entities) = &self.object_common.entities {
for entity in entities {
header_text = header_text.children_entry(entity.get_header_text());
}
};
header_text.build()
}
}

View file

@ -0,0 +1,267 @@
use std::any::TypeId;
use icann_rdap_common::{
dns_types::{DnsAlgorithmType, DnsDigestType},
response::{Domain, SecureDns, Variant},
};
use icann_rdap_common::check::{CheckParams, GetChecks, GetSubChecks};
use crate::rdap::registered_redactions::{self, text_or_registered_redaction};
use super::{
redacted::REDACTED_TEXT,
string::{StringListUtil, StringUtil},
table::{MultiPartTable, ToMpTable},
types::{checks_to_table, events_to_table, links_to_table, public_ids_to_table},
FromMd, MdHeaderText, MdParams, MdUtil, ToMd, HR,
};
impl ToMd for Domain {
fn to_md(&self, params: MdParams) -> String {
let typeid = TypeId::of::<Self>();
let mut md = String::new();
md.push_str(&self.common.to_md(params.from_parent(typeid)));
// header
let header_text = self.get_header_text();
md.push_str(
&header_text
.to_string()
.to_header(params.heading_level, params.options),
);
// multipart data
let mut table = MultiPartTable::new();
let domain_handle = text_or_registered_redaction(
params.root,
&registered_redactions::RedactedName::RegistryDomainId,
&self.object_common.handle,
REDACTED_TEXT,
);
// summary
table = table.summary(header_text);
// identifiers
table = table
.header_ref(&"Identifiers")
.and_nv_ref(&"LDH Name", &self.ldh_name)
.and_nv_ref(&"Unicode Name", &self.unicode_name)
.and_nv_ref(&"Handle", &domain_handle);
if let Some(public_ids) = &self.public_ids {
table = public_ids_to_table(public_ids, table);
}
// common object stuff
table = self.object_common.add_to_mptable(table, params);
// checks
let check_params = CheckParams::from_md(params, typeid);
let mut checks = self.object_common.get_sub_checks(check_params);
checks.push(self.get_checks(check_params));
table = checks_to_table(checks, table, params);
// render table
md.push_str(&table.to_md(params));
// variants require a custom table
if let Some(variants) = &self.variants {
md.push_str(&do_variants(variants, params))
}
// secure dns
if let Some(secure_dns) = &self.secure_dns {
md.push_str(&do_secure_dns(secure_dns, params))
}
// remarks
md.push_str(&self.object_common.remarks.to_md(params.from_parent(typeid)));
// only other object classes from here
md.push_str(HR);
// entities
md.push_str(
&self
.object_common
.entities
.to_md(params.from_parent(typeid)),
);
// nameservers
if let Some(nameservers) = &self.nameservers {
nameservers
.iter()
.for_each(|ns| md.push_str(&ns.to_md(params.next_level())));
}
// network
if let Some(network) = &self.network {
md.push_str(&network.to_md(params.next_level()));
}
// redacted
if let Some(redacted) = &self.object_common.redacted {
md.push_str(&redacted.as_slice().to_md(params.from_parent(typeid)));
}
md.push('\n');
md
}
}
fn do_variants(variants: &[Variant], params: MdParams) -> String {
let mut md = String::new();
md.push_str(&format!(
"|:-:|\n|{}|\n",
"Domain Variants".to_right_bold(8, params.options)
));
md.push_str("|:-:|:-:|:-:|\n|Relations|IDN Table|Variant Names|\n");
variants.iter().for_each(|v| {
md.push_str(&format!(
"|{}|{}|{}|",
v.relations().make_title_case_list(),
v.idn_table.as_deref().unwrap_or_default(),
v.variant_names
.as_deref()
.unwrap_or_default()
.iter()
.map(|dv| format!(
"ldh: '{}' utf:'{}'",
dv.ldh_name.as_deref().unwrap_or_default(),
dv.unicode_name.as_deref().unwrap_or_default()
))
.collect::<Vec<String>>()
.join(", "),
))
});
md.push('\n');
md
}
fn do_secure_dns(secure_dns: &SecureDns, params: MdParams) -> String {
let mut md = String::new();
// multipart data
let mut table = MultiPartTable::new();
table = table
.header_ref(&"DNSSEC Information")
.and_nv_ref(
&"Zone Signed",
&secure_dns.zone_signed.as_ref().map(|b| b.to_string()),
)
.and_nv_ref(
&"Delegation Signed",
&secure_dns.delegation_signed.as_ref().map(|b| b.to_string()),
)
.and_nv_ref(
&"Max Sig Life",
&secure_dns.max_sig_life.as_ref().map(|u| u.to_string()),
);
if let Some(ds_data) = &secure_dns.ds_data {
for (i, ds) in ds_data.iter().enumerate() {
let header = format!("DS Data ({i})").replace_md_chars();
table = table
.header_ref(&header)
.and_nv_ref(&"Key Tag", &ds.key_tag.as_ref().map(|k| k.to_string()))
.and_nv_ref(
&"Algorithm",
&dns_algorithm(&ds.algorithm.as_ref().and_then(|a| a.as_u8())),
)
.and_nv_ref(&"Digest", &ds.digest)
.and_nv_ref(
&"Digest Type",
&dns_digest_type(&ds.digest_type.as_ref().and_then(|d| d.as_u8())),
);
if let Some(events) = &ds.events {
let ds_header = format!("DS ({i}) Events");
table = events_to_table(events, table, &ds_header, params);
}
if let Some(links) = &ds.links {
let ds_header = format!("DS ({i}) Links");
table = links_to_table(links, table, &ds_header);
}
}
}
if let Some(key_data) = &secure_dns.key_data {
for (i, key) in key_data.iter().enumerate() {
let header = format!("Key Data ({i})").replace_md_chars();
table = table
.header_ref(&header)
.and_nv_ref(&"Flags", &key.flags.as_ref().map(|k| k.to_string()))
.and_nv_ref(&"Protocol", &key.protocol.as_ref().map(|a| a.to_string()))
.and_nv_ref(&"Public Key", &key.public_key)
.and_nv_ref(
&"Algorithm",
&dns_algorithm(&key.algorithm.as_ref().and_then(|a| a.as_u8())),
);
if let Some(events) = &key.events {
let key_header = format!("Key ({i}) Events");
table = events_to_table(events, table, &key_header, params);
}
if let Some(links) = &key.links {
let key_header = format!("Key ({i}) Links");
table = links_to_table(links, table, &key_header);
}
}
}
// checks
let typeid = TypeId::of::<Domain>();
let check_params = CheckParams::from_md(params, typeid);
let checks = secure_dns.get_sub_checks(check_params);
table = checks_to_table(checks, table, params);
// render table
md.push_str(&table.to_md(params));
md
}
fn dns_algorithm(alg: &Option<u8>) -> Option<String> {
alg.map(|alg| {
DnsAlgorithmType::mnemonic(alg).map_or(format!("{alg} - Unassigned or Reserved"), |a| {
format!("{alg} - {a}")
})
})
}
fn dns_digest_type(dt: &Option<u8>) -> Option<String> {
dt.map(|dt| {
DnsDigestType::mnemonic(dt).map_or(format!("{dt} - Unassigned or Reserved"), |a| {
format!("{dt} - {a}")
})
})
}
impl MdUtil for Domain {
fn get_header_text(&self) -> MdHeaderText {
let header_text = if let Some(unicode_name) = &self.unicode_name {
format!("Domain {}", unicode_name.replace_md_chars())
} else if let Some(ldh_name) = &self.ldh_name {
format!("Domain {}", ldh_name.replace_md_chars())
} else if let Some(handle) = &self.object_common.handle {
format!("Domain {}", handle.replace_md_chars())
} else {
"Domain".to_string()
};
let mut header_text = MdHeaderText::builder().header_text(header_text);
if let Some(entities) = &self.object_common.entities {
for entity in entities {
header_text = header_text.children_entry(entity.get_header_text());
}
};
if let Some(nameservers) = &self.nameservers {
for ns in nameservers {
header_text = header_text.children_entry(ns.get_header_text());
}
};
if let Some(network) = &self.network {
header_text = header_text.children_entry(network.get_header_text());
}
header_text.build()
}
}

View file

@ -0,0 +1,359 @@
use std::any::TypeId;
use icann_rdap_common::{
contact::{NameParts, PostalAddress},
response::{Entity, EntityRole},
};
use icann_rdap_common::check::{CheckParams, GetChecks, GetSubChecks};
use crate::rdap::registered_redactions::{
are_redactions_registered_for_roles, is_redaction_registered_for_role,
text_or_registered_redaction_for_role, RedactedName,
};
use super::{
redacted::REDACTED_TEXT,
string::StringUtil,
table::{MultiPartTable, ToMpTable},
types::{checks_to_table, public_ids_to_table},
FromMd, MdHeaderText, MdParams, MdUtil, ToMd, HR,
};
impl ToMd for Entity {
fn to_md(&self, params: MdParams) -> String {
let typeid = TypeId::of::<Self>();
let mut md = String::new();
md.push_str(&self.common.to_md(params.from_parent(typeid)));
// header
let header_text = self.get_header_text();
md.push_str(
&header_text
.to_string()
.to_header(params.heading_level, params.options),
);
// A note about the RFC 9537 redactions. A lot of this code is to do RFC 9537 redactions
// that are registered with the IANA. As RFC 9537 is horribly broken, it is likely only
// gTLD registries will use registered redactions, and when they do they will use all
// of them. Therefore, as horribly complicated as this logic is, it attempts to simplify
// things by assuming all the registrations will be used at once, which will be the case
// in the gTLD space.
// check if registrant or tech ids are RFC 9537 redacted
let mut entity_handle = text_or_registered_redaction_for_role(
params.root,
&RedactedName::RegistryRegistrantId,
self,
&EntityRole::Registrant,
&self.object_common.handle,
REDACTED_TEXT,
);
entity_handle = text_or_registered_redaction_for_role(
params.root,
&RedactedName::RegistryTechId,
self,
&EntityRole::Technical,
&entity_handle,
REDACTED_TEXT,
);
// multipart data
let mut table = MultiPartTable::new();
// summary
table = table.summary(header_text);
// identifiers
table = table
.header_ref(&"Identifiers")
.and_nv_ref(&"Handle", &entity_handle)
.and_nv_ul(&"Roles", Some(self.roles().to_vec()));
if let Some(public_ids) = &self.public_ids {
table = public_ids_to_table(public_ids, table);
}
if let Some(contact) = self.contact() {
// nutty RFC 9537 redaction stuff
// check if registrant or tech name are redacted
let mut registrant_name = text_or_registered_redaction_for_role(
params.root,
&RedactedName::RegistrantName,
self,
&EntityRole::Registrant,
&contact.full_name,
REDACTED_TEXT,
);
registrant_name = text_or_registered_redaction_for_role(
params.root,
&RedactedName::TechName,
self,
&EntityRole::Technical,
&registrant_name,
REDACTED_TEXT,
);
// check to see if registrant postal address parts are redacted
let postal_addresses = if are_redactions_registered_for_roles(
params.root,
&[
&RedactedName::RegistrantStreet,
&RedactedName::RegistrantCity,
&RedactedName::RegistrantPostalCode,
],
self,
&[&EntityRole::Registrant],
) {
let mut new_pas = contact.postal_addresses.clone();
if let Some(ref mut new_pas) = new_pas {
new_pas.iter_mut().for_each(|pa| {
pa.street_parts = Some(vec![REDACTED_TEXT.to_string()]);
pa.locality = Some(REDACTED_TEXT.to_string());
pa.postal_code = Some(REDACTED_TEXT.to_string());
})
}
new_pas
} else {
contact.postal_addresses
};
table = table
.header_ref(&"Contact")
.and_nv_ref_maybe(&"Kind", &contact.kind)
.and_nv_ref_maybe(&"Full Name", &registrant_name)
.and_nv_ul(&"Titles", contact.titles)
.and_nv_ul(&"Org Roles", contact.roles)
.and_nv_ul(&"Nicknames", contact.nick_names);
if is_redaction_registered_for_role(
params.root,
&RedactedName::RegistrantOrganization,
self,
&EntityRole::Registrant,
) {
table = table.nv_ref(&"Organization Name", &REDACTED_TEXT.to_string());
} else {
table = table.and_nv_ul(&"Organization Names", contact.organization_names);
}
table = table.and_nv_ul(&"Languages", contact.langs);
if are_redactions_registered_for_roles(
params.root,
&[
&RedactedName::RegistrantPhone,
&RedactedName::RegistrantPhoneExt,
&RedactedName::RegistrantFax,
&RedactedName::RegistrantFaxExt,
&RedactedName::TechPhone,
&RedactedName::TechPhoneExt,
],
self,
&[&EntityRole::Registrant, &EntityRole::Technical],
) {
table = table.nv_ref(&"Phones", &REDACTED_TEXT.to_string());
} else {
table = table.and_nv_ul(&"Phones", contact.phones);
}
if are_redactions_registered_for_roles(
params.root,
&[&RedactedName::TechEmail, &RedactedName::RegistrantEmail],
self,
&[&EntityRole::Registrant, &EntityRole::Technical],
) {
table = table.nv_ref(&"Emails", &REDACTED_TEXT.to_string());
} else {
table = table.and_nv_ul(&"Emails", contact.emails);
}
table = table
.and_nv_ul(&"Web Contact", contact.contact_uris)
.and_nv_ul(&"URLs", contact.urls);
table = postal_addresses.add_to_mptable(table, params);
table = contact.name_parts.add_to_mptable(table, params)
}
// common object stuff
table = self.object_common.add_to_mptable(table, params);
// checks
let check_params = CheckParams::from_md(params, typeid);
let mut checks = self.object_common.get_sub_checks(check_params);
checks.push(self.get_checks(check_params));
table = checks_to_table(checks, table, params);
// render table
md.push_str(&table.to_md(params));
// remarks
md.push_str(&self.object_common.remarks.to_md(params.from_parent(typeid)));
// only other object classes from here
md.push_str(HR);
// entities
md.push_str(
&self
.object_common
.entities
.to_md(params.from_parent(typeid)),
);
// redacted
if let Some(redacted) = &self.object_common.redacted {
md.push_str(&redacted.as_slice().to_md(params.from_parent(typeid)));
}
md.push('\n');
md
}
}
impl ToMd for Option<Vec<Entity>> {
fn to_md(&self, params: MdParams) -> String {
let mut md = String::new();
if let Some(entities) = &self {
entities
.iter()
.for_each(|entity| md.push_str(&entity.to_md(params.next_level())));
}
md
}
}
impl ToMpTable for Option<Vec<PostalAddress>> {
fn add_to_mptable(&self, mut table: MultiPartTable, params: MdParams) -> MultiPartTable {
if let Some(addrs) = self {
for addr in addrs {
table = addr.add_to_mptable(table, params);
}
}
table
}
}
impl ToMpTable for PostalAddress {
fn add_to_mptable(&self, mut table: MultiPartTable, _params: MdParams) -> MultiPartTable {
if self.contexts.is_some() && self.preference.is_some() {
table = table.nv(
&"Address",
format!(
"{} (pref: {})",
self.contexts.as_ref().unwrap().join(" "),
self.preference.unwrap()
),
);
} else if self.contexts.is_some() {
table = table.nv(&"Address", self.contexts.as_ref().unwrap().join(" "));
} else if self.preference.is_some() {
table = table.nv(
&"Address",
format!("preference: {}", self.preference.unwrap()),
);
} else {
table = table.nv(&"Address", "");
}
if let Some(street_parts) = &self.street_parts {
table = table.nv_ul_ref(&"Street", street_parts.iter().collect());
}
if let Some(locality) = &self.locality {
table = table.nv_ref(&"Locality", locality);
}
if self.region_name.is_some() && self.region_code.is_some() {
table = table.nv(
&"Region",
format!(
"{} ({})",
self.region_name.as_ref().unwrap(),
self.region_code.as_ref().unwrap()
),
);
} else if let Some(region_name) = &self.region_name {
table = table.nv_ref(&"Region", region_name);
} else if let Some(region_code) = &self.region_code {
table = table.nv_ref(&"Region", region_code);
}
if self.country_name.is_some() && self.country_code.is_some() {
table = table.nv(
&"Country",
format!(
"{} ({})",
self.country_name.as_ref().unwrap(),
self.country_code.as_ref().unwrap()
),
);
} else if let Some(country_name) = &self.country_name {
table = table.nv_ref(&"Country", country_name);
} else if let Some(country_code) = &self.country_code {
table = table.nv_ref(&"Country", country_code);
}
if let Some(postal_code) = &self.postal_code {
table = table.nv_ref(&"Postal Code", postal_code);
}
if let Some(full_address) = &self.full_address {
let parts = full_address.split('\n').collect::<Vec<&str>>();
for (i, p) in parts.iter().enumerate() {
table = table.nv_ref(&i.to_string(), p);
}
}
table
}
}
impl ToMpTable for Option<NameParts> {
fn add_to_mptable(&self, mut table: MultiPartTable, _params: MdParams) -> MultiPartTable {
if let Some(parts) = self {
if let Some(prefixes) = &parts.prefixes {
table = table.nv(&"Honorifics", prefixes.join(", "));
}
if let Some(given_names) = &parts.given_names {
table = table.nv_ul(&"Given Names", given_names.to_vec());
}
if let Some(middle_names) = &parts.middle_names {
table = table.nv_ul(&"Middle Names", middle_names.to_vec());
}
if let Some(surnames) = &parts.surnames {
table = table.nv_ul(&"Surnames", surnames.to_vec());
}
if let Some(suffixes) = &parts.suffixes {
table = table.nv(&"Suffixes", suffixes.join(", "));
}
}
table
}
}
impl MdUtil for Entity {
fn get_header_text(&self) -> MdHeaderText {
let role = self
.roles()
.first()
.map(|s| s.replace_md_chars().to_title_case());
let header_text = if let Some(handle) = &self.object_common.handle {
if let Some(role) = role {
format!("{} ({})", handle.replace_md_chars(), role)
} else {
format!("Entity {}", handle)
}
} else if let Some(role) = role {
role.to_string()
} else {
"Entity".to_string()
};
let mut header_text = MdHeaderText::builder().header_text(header_text);
if let Some(entities) = &self.object_common.entities {
for entity in entities {
header_text = header_text.children_entry(entity.get_header_text());
}
};
if let Some(networks) = &self.networks {
for network in networks {
header_text = header_text.children_entry(network.get_header_text());
}
};
if let Some(autnums) = &self.autnums {
for autnum in autnums {
header_text = header_text.children_entry(autnum.get_header_text());
}
};
header_text.build()
}
}

View file

@ -0,0 +1,21 @@
use std::any::TypeId;
use icann_rdap_common::response::Rfc9083Error;
use super::{MdHeaderText, MdParams, MdUtil, ToMd, HR};
impl ToMd for Rfc9083Error {
fn to_md(&self, params: MdParams) -> String {
let mut md = String::new();
md.push_str(&self.common.to_md(params.from_parent(TypeId::of::<Self>())));
md.push_str(HR);
md.push('\n');
md
}
}
impl MdUtil for Rfc9083Error {
fn get_header_text(&self) -> MdHeaderText {
MdHeaderText::builder().header_text("RDAP Error").build()
}
}

View file

@ -0,0 +1,21 @@
use std::any::TypeId;
use icann_rdap_common::response::Help;
use super::{MdHeaderText, MdParams, MdUtil, ToMd, HR};
impl ToMd for Help {
fn to_md(&self, params: MdParams) -> String {
let mut md = String::new();
md.push_str(&self.common.to_md(params.from_parent(TypeId::of::<Self>())));
md.push_str(HR);
md.push('\n');
md
}
}
impl MdUtil for Help {
fn get_header_text(&self) -> MdHeaderText {
MdHeaderText::builder().header_text("Server Help").build()
}
}

View file

@ -0,0 +1,205 @@
//! Converts RDAP to Markdown.
use {
crate::rdap::rr::RequestData,
buildstructor::Builder,
icann_rdap_common::{check::CheckParams, httpdata::HttpData, response::RdapResponse},
std::{any::TypeId, char},
strum::EnumMessage,
};
use icann_rdap_common::check::{CheckClass, Checks, CHECK_CLASS_LEN};
use self::string::StringUtil;
pub mod autnum;
pub mod domain;
pub mod entity;
pub mod error;
pub mod help;
pub mod nameserver;
pub mod network;
pub mod redacted;
pub mod search;
pub mod string;
pub mod table;
pub mod types;
pub(crate) const _CODE_INDENT: &str = " ";
pub(crate) const HR: &str = "----------------------------------------\n";
/// Specifies options for generating markdown.
pub struct MdOptions {
/// If true, do not use Unicode characters.
pub no_unicode_chars: bool,
/// The character used for text styling of bold and italics.
pub text_style_char: char,
/// If true, headers use the hash marks or under lines.
pub hash_headers: bool,
/// If true, the text_style_char will appear in a justified text.
pub style_in_justify: bool,
}
impl Default for MdOptions {
fn default() -> Self {
Self {
no_unicode_chars: false,
text_style_char: '*',
hash_headers: true,
style_in_justify: false,
}
}
}
impl MdOptions {
/// Defaults for markdown that looks more like plain text.
pub fn plain_text() -> Self {
Self {
no_unicode_chars: true,
text_style_char: '_',
hash_headers: false,
style_in_justify: true,
}
}
}
#[derive(Clone, Copy)]
pub struct MdParams<'a> {
pub heading_level: usize,
pub root: &'a RdapResponse,
pub http_data: &'a HttpData,
pub parent_type: TypeId,
pub check_types: &'a [CheckClass],
pub options: &'a MdOptions,
pub req_data: &'a RequestData<'a>,
}
impl MdParams<'_> {
pub fn from_parent(&self, parent_type: TypeId) -> Self {
Self {
parent_type,
heading_level: self.heading_level,
root: self.root,
http_data: self.http_data,
check_types: self.check_types,
options: self.options,
req_data: self.req_data,
}
}
pub fn next_level(&self) -> Self {
Self {
heading_level: self.heading_level + 1,
..*self
}
}
}
pub trait ToMd {
fn to_md(&self, params: MdParams) -> String;
}
impl ToMd for RdapResponse {
fn to_md(&self, params: MdParams) -> String {
let mut md = String::new();
md.push_str(&params.http_data.to_md(params));
let variant_md = match &self {
Self::Entity(entity) => entity.to_md(params),
Self::Domain(domain) => domain.to_md(params),
Self::Nameserver(nameserver) => nameserver.to_md(params),
Self::Autnum(autnum) => autnum.to_md(params),
Self::Network(network) => network.to_md(params),
Self::DomainSearchResults(results) => results.to_md(params),
Self::EntitySearchResults(results) => results.to_md(params),
Self::NameserverSearchResults(results) => results.to_md(params),
Self::ErrorResponse(error) => error.to_md(params),
Self::Help(help) => help.to_md(params),
};
md.push_str(&variant_md);
md
}
}
pub trait MdUtil {
fn get_header_text(&self) -> MdHeaderText;
}
#[derive(Builder)]
pub struct MdHeaderText {
header_text: String,
children: Vec<MdHeaderText>,
}
#[allow(clippy::to_string_trait_impl)]
impl ToString for MdHeaderText {
fn to_string(&self) -> String {
self.header_text.clone()
}
}
impl MdUtil for RdapResponse {
fn get_header_text(&self) -> MdHeaderText {
match &self {
Self::Entity(entity) => entity.get_header_text(),
Self::Domain(domain) => domain.get_header_text(),
Self::Nameserver(nameserver) => nameserver.get_header_text(),
Self::Autnum(autnum) => autnum.get_header_text(),
Self::Network(network) => network.get_header_text(),
Self::DomainSearchResults(results) => results.get_header_text(),
Self::EntitySearchResults(results) => results.get_header_text(),
Self::NameserverSearchResults(results) => results.get_header_text(),
Self::ErrorResponse(error) => error.get_header_text(),
Self::Help(help) => help.get_header_text(),
}
}
}
pub(crate) fn checks_ul(checks: &Checks, params: MdParams) -> String {
let mut md = String::new();
checks
.items
.iter()
.filter(|item| params.check_types.contains(&item.check_class))
.for_each(|item| {
md.push_str(&format!(
"* {}: {}\n",
&item
.check_class
.to_string()
.to_right_em(*CHECK_CLASS_LEN, params.options),
item.check
.get_message()
.expect("Check has no message. Coding error.")
))
});
md
}
pub(crate) trait FromMd<'a> {
fn from_md(md_params: MdParams<'a>, parent_type: TypeId) -> Self;
fn from_md_no_parent(md_params: MdParams<'a>) -> Self;
}
impl<'a> FromMd<'a> for CheckParams<'a> {
fn from_md(md_params: MdParams<'a>, parent_type: TypeId) -> Self {
Self {
do_subchecks: false,
root: md_params.root,
parent_type,
allow_unreg_ext: false,
}
}
fn from_md_no_parent(md_params: MdParams<'a>) -> Self {
Self {
do_subchecks: false,
root: md_params.root,
parent_type: md_params.parent_type,
allow_unreg_ext: false,
}
}
}

View file

@ -0,0 +1,106 @@
use std::any::TypeId;
use icann_rdap_common::response::Nameserver;
use icann_rdap_common::check::{CheckParams, GetChecks, GetSubChecks};
use super::{
string::StringUtil,
table::{MultiPartTable, ToMpTable},
types::checks_to_table,
FromMd, MdHeaderText, MdParams, MdUtil, ToMd, HR,
};
impl ToMd for Nameserver {
fn to_md(&self, params: MdParams) -> String {
let typeid = TypeId::of::<Self>();
let mut md = String::new();
// other common stuff
md.push_str(&self.common.to_md(params.from_parent(typeid)));
// header
let header_text = self.get_header_text();
md.push_str(
&header_text
.to_string()
.to_header(params.heading_level, params.options),
);
// multipart data
let mut table = MultiPartTable::new();
// summary
table = table.summary(header_text);
// identifiers
table = table
.header_ref(&"Identifiers")
.and_nv_ref(&"LDH Name", &self.ldh_name)
.and_nv_ref(&"Unicode Name", &self.unicode_name)
.and_nv_ref(&"Handle", &self.object_common.handle);
if let Some(addresses) = &self.ip_addresses {
if let Some(v4) = &addresses.v4 {
table = table.nv_ul_ref(&"Ipv4", v4.vec().iter().collect());
}
if let Some(v6) = &addresses.v6 {
table = table.nv_ul_ref(&"Ipv6", v6.vec().iter().collect());
}
}
// common object stuff
table = self.object_common.add_to_mptable(table, params);
// checks
let check_params = CheckParams::from_md(params, typeid);
let mut checks = self.object_common.get_sub_checks(check_params);
checks.push(self.get_checks(check_params));
table = checks_to_table(checks, table, params);
// render table
md.push_str(&table.to_md(params));
// remarks
md.push_str(&self.object_common.remarks.to_md(params.from_parent(typeid)));
// only other object classes from here
md.push_str(HR);
// entities
md.push_str(
&self
.object_common
.entities
.to_md(params.from_parent(typeid)),
);
// redacted
if let Some(redacted) = &self.object_common.redacted {
md.push_str(&redacted.as_slice().to_md(params.from_parent(typeid)));
}
md.push('\n');
md
}
}
impl MdUtil for Nameserver {
fn get_header_text(&self) -> MdHeaderText {
let header_text = if let Some(unicode_name) = &self.unicode_name {
format!("Nameserver {}", unicode_name.replace_md_chars())
} else if let Some(ldh_name) = &self.ldh_name {
format!("Nameserver {}", ldh_name.replace_md_chars())
} else if let Some(handle) = &self.object_common.handle {
format!("Nameserver {}", handle.replace_md_chars())
} else {
"Domain".to_string()
};
let mut header_text = MdHeaderText::builder().header_text(header_text);
if let Some(entities) = &self.object_common.entities {
for entity in entities {
header_text = header_text.children_entry(entity.get_header_text());
}
};
header_text.build()
}
}

View file

@ -0,0 +1,108 @@
use std::any::TypeId;
use icann_rdap_common::{
check::{CheckParams, GetChecks, GetSubChecks},
response::Network,
};
use super::{
string::StringUtil,
table::{MultiPartTable, ToMpTable},
types::checks_to_table,
FromMd, MdHeaderText, MdParams, MdUtil, ToMd, HR,
};
impl ToMd for Network {
fn to_md(&self, params: MdParams) -> String {
let typeid = TypeId::of::<Self>();
let mut md = String::new();
md.push_str(&self.common.to_md(params));
let header_text = self.get_header_text();
md.push_str(
&header_text
.to_string()
.to_header(params.heading_level, params.options),
);
// multipart data
let mut table = MultiPartTable::new();
// summary
table = table.summary(header_text);
// identifiers
table = table
.header_ref(&"Identifiers")
.and_nv_ref(&"Start Address", &self.start_address)
.and_nv_ref(&"End Address", &self.end_address)
.and_nv_ref(&"IP Version", &self.ip_version)
.and_nv_ul(&"CIDR", self.cidr0_cidrs.clone())
.and_nv_ref(&"Handle", &self.object_common.handle)
.and_nv_ref(&"Parent Handle", &self.parent_handle)
.and_nv_ref(&"Network Type", &self.network_type)
.and_nv_ref(&"Network Name", &self.name)
.and_nv_ref(&"Country", &self.country);
// common object stuff
table = self.object_common.add_to_mptable(table, params);
// checks
let check_params = CheckParams::from_md(params, typeid);
let mut checks = self.object_common.get_sub_checks(check_params);
checks.push(self.get_checks(check_params));
table = checks_to_table(checks, table, params);
// render table
md.push_str(&table.to_md(params));
// remarks
md.push_str(&self.object_common.remarks.to_md(params.from_parent(typeid)));
// only other object classes from here
md.push_str(HR);
// entities
md.push_str(
&self
.object_common
.entities
.to_md(params.from_parent(typeid)),
);
// redacted
if let Some(redacted) = &self.object_common.redacted {
md.push_str(&redacted.as_slice().to_md(params.from_parent(typeid)));
}
md.push('\n');
md
}
}
impl MdUtil for Network {
fn get_header_text(&self) -> MdHeaderText {
let header_text = if self.start_address.is_some() && self.end_address.is_some() {
format!(
"IP Network {} - {}",
&self.start_address.as_ref().unwrap().replace_md_chars(),
&self.end_address.as_ref().unwrap().replace_md_chars()
)
} else if let Some(start_address) = &self.start_address {
format!("IP Network {}", start_address.replace_md_chars())
} else if let Some(handle) = &self.object_common.handle {
format!("IP Network {}", handle.replace_md_chars())
} else if let Some(name) = &self.name {
format!("IP Network {}", name.replace_md_chars())
} else {
"IP Network".to_string()
};
let mut header_text = MdHeaderText::builder().header_text(header_text);
if let Some(entities) = &self.object_common.entities {
for entity in entities {
header_text = header_text.children_entry(entity.get_header_text());
}
};
header_text.build()
}
}

View file

@ -0,0 +1,279 @@
use std::str::FromStr;
use {
icann_rdap_common::response::redacted::Redacted,
jsonpath::replace_with,
jsonpath_lib as jsonpath,
jsonpath_rust::{JsonPathFinder, JsonPathInst},
serde_json::{json, Value},
};
use {
super::{string::StringUtil, table::MultiPartTable, MdOptions, MdParams, ToMd},
icann_rdap_common::response::RdapResponse,
};
/// The text to appear if something is redacted.
///
/// This should be REDACTED in bold.
pub const REDACTED_TEXT: &str = "*REDACTED*";
impl ToMd for &[Redacted] {
fn to_md(&self, params: MdParams) -> String {
let mut md = String::new();
// header
let header_text = "Redacted".to_string();
md.push_str(&header_text.to_header(params.heading_level, params.options));
// multipart data
let mut table = MultiPartTable::new();
table = table.header_ref(&"Fields");
for (index, redacted) in self.iter().enumerate() {
let options = MdOptions {
text_style_char: '*',
..Default::default()
};
// make the name bold
let name = "Redaction";
let b_name = name.to_bold(&options);
// build the table
table = table.and_nv_ref(&b_name, &Some((index + 1).to_string()));
// Get the data itself
let name_data = redacted
.name
.description
.clone()
.or(redacted.name.type_field.clone());
let method_data = redacted.method.as_ref().map(|m| m.to_string());
let reason_data = redacted.reason.as_ref().map(|m| m.to_string());
// Special case the 'column' fields
table = table
.and_nv_ref(&"name".to_title_case(), &name_data)
.and_nv_ref(&"prePath".to_title_case(), &redacted.pre_path)
.and_nv_ref(&"postPath".to_title_case(), &redacted.post_path)
.and_nv_ref(
&"replacementPath".to_title_case(),
&redacted.replacement_path,
)
.and_nv_ref(&"pathLang".to_title_case(), &redacted.path_lang)
.and_nv_ref(&"method".to_title_case(), &method_data)
.and_nv_ref(&"reason".to_title_case(), &reason_data);
// we don't have these right now but if we put them in later we will need them
// let check_params = CheckParams::from_md(params, typeid);
// let mut checks = redacted.object_common.get_sub_checks(check_params);
// checks.push(redacted.get_checks(check_params));
// table = checks_to_table(checks, table, params);
}
// render table
md.push_str(&table.to_md(params));
md.push('\n');
md
}
}
// this is our public entry point
pub fn replace_redacted_items(orignal_response: RdapResponse) -> RdapResponse {
// convert the RdapResponse to a string
let rdap_json = serde_json::to_string(&orignal_response).unwrap();
// Redaction is not a top-level entity so we have to check the JSON
// to see if anything exists in the way of "redacted", this should find it in the rdapConformance
if !rdap_json.contains("\"redacted\"") {
// If there are no redactions, return the original response
return orignal_response;
}
// convert the string to a JSON Value
let mut rdap_json_response: Value = serde_json::from_str(&rdap_json).unwrap();
// this double checks to see if "redacted" is an array
if rdap_json_response["redacted"].as_array().is_none() {
// If "redacted" is not an array, return the original response
return orignal_response;
}
// Initialize the final response with the original response
let mut response = orignal_response;
// pull the redacted array out of the JSON
let redacted_array_option = rdap_json_response["redacted"].as_array().cloned();
// if there are any redactions we need to do some modifications
if let Some(ref redacted_array) = redacted_array_option {
let new_json_response = convert_redactions(&mut rdap_json_response, redacted_array).clone();
// convert the Value back to a RdapResponse
response = serde_json::from_value(new_json_response).unwrap();
}
// send the response back so we can display it to the client
response
}
fn convert_redactions<'a>(
rdap_json_response: &'a mut Value,
redacted_array: &'a [Value],
) -> &'a mut Value {
for item in redacted_array {
let item_map = item.as_object().unwrap();
let post_path = get_string_from_map(item_map, "postPath");
let method = get_string_from_map(item_map, "method");
if let Some(path_lang) = item_map.get("pathLang") {
if let Some(path_lang) = path_lang.as_str() {
if !path_lang.eq_ignore_ascii_case("jsonpath") {
continue;
}
}
}
// if method doesn't equal emptyValue or partialValue, we don't need to do anything, we can skip to the next item
if method != "emptyValue" && method != "partialValue" && !post_path.is_empty() {
continue;
}
match JsonPathInst::from_str(&post_path) {
Ok(json_path) => {
let finder =
JsonPathFinder::new(Box::new(rdap_json_response.clone()), Box::new(json_path));
let matches = finder.find_as_path();
if let Value::Array(paths) = matches {
if paths.is_empty() {
continue; // we don't need to do anything, we can skip to the next item
} else {
for path_value in paths {
if let Value::String(found_path) = path_value {
let no_value = Value::String("NO_VALUE".to_string());
let json_pointer = convert_to_json_pointer_path(&found_path);
let value_at_path = rdap_json_response
.pointer(&json_pointer)
.unwrap_or(&no_value);
if value_at_path.is_string() {
// grab the value at the end point of the JSON path
let end_of_path_value =
match rdap_json_response.pointer(&json_pointer) {
Some(value) => value.clone(),
None => {
continue;
}
};
let replaced_json = replace_with(
rdap_json_response.clone(),
&found_path,
&mut |x| {
// STRING ONLY! This is the only spot where we are ACTUALLY replacing or updating something
if x.is_string() {
match x.as_str() {
Some("") => Some(json!("*REDACTED*")),
Some(s) => Some(json!(format!("*{}*", s))),
_ => Some(json!("*REDACTED*")),
}
} else {
Some(end_of_path_value.clone()) // it isn't a string, put it back in there
}
},
);
match replaced_json {
Ok(new_json) => *rdap_json_response = new_json,
_ => {
// why did we fail to modify the JSON?
}
};
}
}
}
}
}
}
_ => {
// do nothing
}
}
}
rdap_json_response
}
// utility functions
fn convert_to_json_pointer_path(path: &str) -> String {
let pointer_path = path
.trim_start_matches('$')
.replace('.', "/")
.replace("['", "/")
.replace("']", "")
.replace('[', "/")
.replace(']', "")
.replace("//", "/");
pointer_path
}
fn get_string_from_map(map: &serde_json::Map<String, Value>, key: &str) -> String {
map.get(key)
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.unwrap_or_default()
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use {
serde_json::Value,
std::{error::Error, fs::File, io::Read},
};
fn process_redacted_file(file_path: &str) -> Result<String, Box<dyn Error>> {
let mut file = File::open(file_path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
// this has to be setup very specifically, just like replace_redacted_items is setup.
let mut rdap_json_response: Value = serde_json::from_str(&contents)?;
let redacted_array_option = rdap_json_response["redacted"].as_array().cloned();
// we are testing parse_redacted_json here -- just the JSON transforms
if let Some(redacted_array) = redacted_array_option {
crate::md::redacted::convert_redactions(&mut rdap_json_response, &redacted_array);
} else {
panic!("No redacted array found in the JSON");
}
let pretty_json = serde_json::to_string_pretty(&rdap_json_response)?;
println!("{}", pretty_json);
Ok(pretty_json)
}
#[test]
fn test_process_empty_value() {
let expected_output =
std::fs::read_to_string("src/test_files/example-1_empty_value-expected.json").unwrap();
let output = process_redacted_file("src/test_files/example-1_empty_value.json").unwrap();
assert_eq!(output, expected_output);
}
#[test]
fn test_process_partial_value() {
let expected_output =
std::fs::read_to_string("src/test_files/example-2_partial_value-expected.json")
.unwrap();
let output = process_redacted_file("src/test_files/example-2_partial_value.json").unwrap();
assert_eq!(output, expected_output);
}
#[test]
fn test_process_dont_replace_number() {
let expected_output = std::fs::read_to_string(
"src/test_files/example-3-dont_replace_redaction_of_a_number.json",
)
.unwrap();
// we don't need an expected for this one, it should remain unchanged
let output = process_redacted_file(
"src/test_files/example-3-dont_replace_redaction_of_a_number.json",
)
.unwrap();
assert_eq!(output, expected_output);
}
}

View file

@ -0,0 +1,82 @@
use std::any::TypeId;
use icann_rdap_common::response::{
DomainSearchResults, EntitySearchResults, NameserverSearchResults,
};
use super::{MdHeaderText, MdParams, MdUtil, ToMd};
impl ToMd for DomainSearchResults {
fn to_md(&self, params: MdParams) -> String {
let typeid = TypeId::of::<Self>();
let mut md = String::new();
md.push_str(&self.common.to_md(params.from_parent(typeid)));
self.results.iter().for_each(|result| {
md.push_str(&result.to_md(MdParams {
heading_level: params.heading_level + 1,
parent_type: typeid,
..params
}))
});
md.push('\n');
md
}
}
impl ToMd for NameserverSearchResults {
fn to_md(&self, params: MdParams) -> String {
let typeid = TypeId::of::<Self>();
let mut md = String::new();
md.push_str(&self.common.to_md(params.from_parent(typeid)));
self.results.iter().for_each(|result| {
md.push_str(&result.to_md(MdParams {
heading_level: params.heading_level + 1,
parent_type: typeid,
..params
}))
});
md.push('\n');
md
}
}
impl ToMd for EntitySearchResults {
fn to_md(&self, params: MdParams) -> String {
let typeid = TypeId::of::<Self>();
let mut md = String::new();
md.push_str(&self.common.to_md(params.from_parent(typeid)));
self.results.iter().for_each(|result| {
md.push_str(&result.to_md(MdParams {
heading_level: params.heading_level + 1,
parent_type: typeid,
..params
}))
});
md.push('\n');
md
}
}
impl MdUtil for DomainSearchResults {
fn get_header_text(&self) -> MdHeaderText {
MdHeaderText::builder()
.header_text("Domain Search Results")
.build()
}
}
impl MdUtil for EntitySearchResults {
fn get_header_text(&self) -> MdHeaderText {
MdHeaderText::builder()
.header_text("Entity Search Results")
.build()
}
}
impl MdUtil for NameserverSearchResults {
fn get_header_text(&self) -> MdHeaderText {
MdHeaderText::builder()
.header_text("Nameserver Search Results")
.build()
}
}

View file

@ -0,0 +1,267 @@
use chrono::DateTime;
use super::{MdOptions, MdParams};
pub trait StringUtil {
/// Replaces and filters markdown characters.
fn replace_md_chars(self) -> String;
fn to_em(self, options: &MdOptions) -> String;
fn to_bold(self, options: &MdOptions) -> String;
fn to_inline(self, options: &MdOptions) -> String;
fn to_header(self, level: usize, options: &MdOptions) -> String;
fn to_right(self, width: usize, options: &MdOptions) -> String;
fn to_right_em(self, width: usize, options: &MdOptions) -> String;
fn to_right_bold(self, width: usize, options: &MdOptions) -> String;
fn to_left(self, width: usize, options: &MdOptions) -> String;
fn to_left_em(self, width: usize, options: &MdOptions) -> String;
fn to_left_bold(self, width: usize, options: &MdOptions) -> String;
fn to_center(self, width: usize, options: &MdOptions) -> String;
fn to_center_em(self, width: usize, options: &MdOptions) -> String;
fn to_center_bold(self, width: usize, options: &MdOptions) -> String;
fn to_title_case(self) -> String;
fn to_words_title_case(self) -> String;
fn to_cap_acronyms(self) -> String;
fn format_date_time(self, params: MdParams) -> Option<String>;
}
impl<T: ToString> StringUtil for T {
fn replace_md_chars(self) -> String {
self.to_string()
.replace(|c: char| c.is_whitespace(), " ")
.chars()
.map(|c| match c {
'*' | '_' | '|' | '#' => format!("\\{c}"),
_ => c.to_string(),
})
.collect()
}
fn to_em(self, options: &MdOptions) -> String {
format!(
"{}{}{}",
options.text_style_char,
self.to_string(),
options.text_style_char
)
}
fn to_bold(self, options: &MdOptions) -> String {
format!(
"{}{}{}{}{}",
options.text_style_char,
options.text_style_char,
self.to_string(),
options.text_style_char,
options.text_style_char
)
}
fn to_inline(self, _options: &MdOptions) -> String {
format!("`{}`", self.to_string(),)
}
fn to_header(self, level: usize, options: &MdOptions) -> String {
let s = self.to_string();
if options.hash_headers {
format!("{} {s}\n\n", "#".repeat(level))
} else {
let line = if level == 1 {
"=".repeat(s.len())
} else {
"-".repeat(s.len())
};
format!("{s}\n{line}\n\n")
}
}
fn to_right(self, width: usize, options: &MdOptions) -> String {
let str = self.to_string();
if options.no_unicode_chars {
format!("{str:>width$}")
} else {
format!("{str:\u{2003}>width$}")
}
}
fn to_right_em(self, width: usize, options: &MdOptions) -> String {
if options.style_in_justify {
self.to_em(options).to_right(width, options)
} else {
self.to_right(width, options).to_em(options)
}
}
fn to_right_bold(self, width: usize, options: &MdOptions) -> String {
if options.style_in_justify {
self.to_bold(options).to_right(width, options)
} else {
self.to_right(width, options).to_bold(options)
}
}
fn to_left(self, width: usize, options: &MdOptions) -> String {
let str = self.to_string();
if options.no_unicode_chars {
format!("{str:<width$}")
} else {
format!("{str:\u{2003}<width$}")
}
}
fn to_left_em(self, width: usize, options: &MdOptions) -> String {
if options.style_in_justify {
self.to_em(options).to_left(width, options)
} else {
self.to_left(width, options).to_em(options)
}
}
fn to_left_bold(self, width: usize, options: &MdOptions) -> String {
if options.style_in_justify {
self.to_bold(options).to_left(width, options)
} else {
self.to_left(width, options).to_bold(options)
}
}
fn to_center(self, width: usize, options: &MdOptions) -> String {
let str = self.to_string();
if options.no_unicode_chars {
format!("{str:^width$}")
} else {
format!("{str:\u{2003}^width$}")
}
}
fn to_center_em(self, width: usize, options: &MdOptions) -> String {
if options.style_in_justify {
self.to_em(options).to_center(width, options)
} else {
self.to_center(width, options).to_bold(options)
}
}
fn to_center_bold(self, width: usize, options: &MdOptions) -> String {
if options.style_in_justify {
self.to_bold(options).to_center(width, options)
} else {
self.to_center(width, options).to_bold(options)
}
}
fn to_title_case(self) -> String {
self.to_string()
.char_indices()
.map(|(i, mut c)| {
if i == 0 {
c.make_ascii_uppercase();
c
} else {
c
}
})
.collect::<String>()
}
fn to_words_title_case(self) -> String {
self.to_string()
.split_whitespace()
.map(|s| s.to_title_case())
.collect::<Vec<String>>()
.join(" ")
}
fn format_date_time(self, _params: MdParams) -> Option<String> {
let date = DateTime::parse_from_rfc3339(&self.to_string()).ok()?;
Some(date.format("%a, %v %X %Z").to_string())
}
fn to_cap_acronyms(self) -> String {
self.to_string()
.replace_md_chars()
.replace("rdap", "RDAP")
.replace("icann", "ICANN")
.replace("arin", "ARIN")
.replace("ripe", "RIPE")
.replace("apnic", "APNIC")
.replace("lacnic", "LACNIC")
.replace("afrinic", "AFRINIC")
.replace("nro", "NRO")
.replace("ietf", "IETF")
}
}
pub(crate) trait StringListUtil {
fn make_list_all_title_case(self) -> Vec<String>;
fn make_title_case_list(self) -> String;
}
impl<T: ToString> StringListUtil for &[T] {
fn make_list_all_title_case(self) -> Vec<String> {
self.iter()
.map(|s| s.to_string().to_words_title_case())
.collect::<Vec<String>>()
}
fn make_title_case_list(self) -> String {
self.make_list_all_title_case().join(", ")
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::{StringListUtil, StringUtil};
#[rstest]
#[case("foo", "Foo")]
#[case("FOO", "FOO")]
fn test_words(#[case] word: &str, #[case] expected: &str) {
// GIVEN in arguments
// WHEN
let actual = word.to_title_case();
// THEN
assert_eq!(actual, expected);
}
#[rstest]
#[case("foo bar", "Foo Bar")]
#[case("foo bar", "Foo Bar")]
#[case("foO baR", "FoO BaR")]
fn test_sentences(#[case] sentence: &str, #[case] expected: &str) {
// GIVEN in arguments
// WHEN
let actual = sentence.to_words_title_case();
// THEN
assert_eq!(actual, expected);
}
#[test]
fn test_list_of_sentences() {
// GIVEN
let v = ["foo bar", "foO baR"];
// WHEN
let actual = v.make_list_all_title_case();
// THEN
assert_eq!(actual, vec!["Foo Bar".to_string(), "FoO BaR".to_string()])
}
#[test]
fn test_list() {
// GIVEN
let list = ["foo bar", "bizz buzz"];
// WHEN
let actual = list.make_title_case_list();
// THEN
assert_eq!(actual, "Foo Bar, Bizz Buzz");
}
}

View file

@ -0,0 +1,477 @@
use std::cmp::max;
use super::{string::StringUtil, MdHeaderText, MdOptions, MdParams, ToMd};
pub(crate) trait ToMpTable {
fn add_to_mptable(&self, table: MultiPartTable, params: MdParams) -> MultiPartTable;
}
/// A datastructue to hold various row types for a markdown table.
///
/// This datastructure has the following types of rows:
/// * header - just the left most column which is centered and bolded text
/// * name/value - first column is the name and the second column is data.
///
/// For name/value rows, the name is right justified. Name/value rows may also
/// have unordered (bulleted) lists. In markdown, there is no such thing as a
/// multiline row, so this creates multiple rows where the name is left blank.
pub struct MultiPartTable {
rows: Vec<Row>,
}
enum Row {
Header(String),
NameValue((String, String)),
MultiValue(Vec<String>),
}
impl Default for MultiPartTable {
fn default() -> Self {
Self::new()
}
}
impl MultiPartTable {
pub fn new() -> Self {
Self { rows: vec![] }
}
/// Add a header row.
pub fn header_ref(mut self, name: &impl ToString) -> Self {
self.rows.push(Row::Header(name.to_string()));
self
}
/// Add a name/value row.
pub fn nv_ref(mut self, name: &impl ToString, value: &impl ToString) -> Self {
self.rows.push(Row::NameValue((
name.to_string(),
value.to_string().replace_md_chars(),
)));
self
}
/// Add a name/value row.
pub fn nv(mut self, name: &impl ToString, value: impl ToString) -> Self {
self.rows.push(Row::NameValue((
name.to_string(),
value.to_string().replace_md_chars(),
)));
self
}
/// Add a name/value row without processing whitespace or markdown charaters.
pub fn nv_raw(mut self, name: &impl ToString, value: impl ToString) -> Self {
self.rows
.push(Row::NameValue((name.to_string(), value.to_string())));
self
}
/// Add a name/value row with unordered list.
pub fn nv_ul_ref(mut self, name: &impl ToString, value: Vec<&impl ToString>) -> Self {
value.iter().enumerate().for_each(|(i, v)| {
if i == 0 {
self.rows.push(Row::NameValue((
name.to_string(),
format!("* {}", v.to_string().replace_md_chars()),
)))
} else {
self.rows.push(Row::NameValue((
String::default(),
format!("* {}", v.to_string().replace_md_chars()),
)))
}
});
self
}
/// Add a name/value row with unordered list.
pub fn nv_ul(mut self, name: &impl ToString, value: Vec<impl ToString>) -> Self {
value.iter().enumerate().for_each(|(i, v)| {
if i == 0 {
self.rows.push(Row::NameValue((
name.to_string(),
format!("* {}", v.to_string().replace_md_chars()),
)))
} else {
self.rows.push(Row::NameValue((
String::default(),
format!("* {}", v.to_string().replace_md_chars()),
)))
}
});
self
}
/// Add a name/value row.
pub fn and_nv_ref(mut self, name: &impl ToString, value: &Option<String>) -> Self {
self.rows.push(Row::NameValue((
name.to_string(),
value
.as_deref()
.unwrap_or_default()
.to_string()
.replace_md_chars(),
)));
self
}
/// Add a name/value row.
pub fn and_nv_ref_maybe(self, name: &impl ToString, value: &Option<String>) -> Self {
if let Some(value) = value {
self.nv_ref(name, value)
} else {
self
}
}
/// Add a name/value row with unordered list.
pub fn and_nv_ul_ref(self, name: &impl ToString, value: Option<Vec<&impl ToString>>) -> Self {
if let Some(value) = value {
self.nv_ul_ref(name, value)
} else {
self
}
}
/// Add a name/value row with unordered list.
pub fn and_nv_ul(self, name: &impl ToString, value: Option<Vec<impl ToString>>) -> Self {
if let Some(value) = value {
self.nv_ul(name, value)
} else {
self
}
}
/// A summary row is a special type of name/value row that has an unordered (bulleted) list
/// that is output in a tree structure (max 3 levels).
pub fn summary(mut self, header_text: MdHeaderText) -> Self {
self.rows.push(Row::NameValue((
"Summary".to_string(),
header_text.to_string().replace_md_chars().to_string(),
)));
// note that termimad has limits on list depth, so we can't go too crazy.
// however, this seems perfectly reasonable for must RDAP use cases.
for level1 in header_text.children {
self.rows.push(Row::NameValue((
"".to_string(),
format!("* {}", level1.to_string().replace_md_chars()),
)));
for level2 in level1.children {
self.rows.push(Row::NameValue((
"".to_string(),
format!(" * {}", level2.to_string().replace_md_chars()),
)));
}
}
self
}
/// Adds a multivalue row.
pub fn multi(mut self, values: Vec<String>) -> Self {
self.rows.push(Row::MultiValue(
values.iter().map(|s| s.replace_md_chars()).collect(),
));
self
}
/// Adds a multivalue row.
pub fn multi_ref(mut self, values: &[&str]) -> Self {
self.rows.push(Row::MultiValue(
values.iter().map(|s| s.replace_md_chars()).collect(),
));
self
}
/// Adds a multivalue row without processing whitespace or markdown characters.
pub fn multi_raw(mut self, values: Vec<String>) -> Self {
self.rows.push(Row::MultiValue(
values.iter().map(|s| s.to_owned()).collect(),
));
self
}
/// Adds a multivalue row without processing whitespace or markdown characters.
pub fn multi_raw_ref(mut self, values: &[&str]) -> Self {
self.rows.push(Row::MultiValue(
values.iter().map(|s| s.to_string()).collect(),
));
self
}
pub fn to_md_table(&self, options: &MdOptions) -> String {
let mut md = String::new();
let col_type_width = max(
self.rows
.iter()
.map(|row| match row {
Row::Header(header) => header.len(),
Row::NameValue((name, _value)) => name.len(),
Row::MultiValue(_) => 1,
})
.max()
.unwrap_or(1),
1,
);
self.rows
.iter()
.scan(true, |state, x| {
let new_state = match x {
Row::Header(name) => {
md.push_str(&format!(
"|:-:|\n|{}|\n",
name.to_center_bold(col_type_width, options)
));
true
}
Row::NameValue((name, value)) => {
if *state {
md.push_str("|-:|:-|\n");
};
md.push_str(&format!(
"|{}|{}|\n",
name.to_right(col_type_width, options),
value
));
false
}
Row::MultiValue(values) => {
// column formatting
md.push('|');
for _col in values {
md.push_str(":--:|");
}
md.push('\n');
// the actual data
md.push('|');
for col in values {
md.push_str(&format!("{col}|"));
}
md.push('\n');
true
}
};
*state = new_state;
Some(new_state)
})
.last();
md.push_str("|\n\n");
md
}
}
impl ToMd for MultiPartTable {
fn to_md(&self, params: super::MdParams) -> String {
self.to_md_table(params.options)
}
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use icann_rdap_common::{httpdata::HttpData, prelude::ToResponse, response::Rfc9083Error};
use crate::{
md::ToMd,
rdap::rr::{RequestData, SourceType},
};
use super::MultiPartTable;
#[test]
fn GIVEN_header_WHEN_to_md_THEN_header_format_and_header() {
// GIVEN
let table = MultiPartTable::new().header_ref(&"foo");
// WHEN
let req_data = RequestData {
req_number: 0,
source_host: "",
source_type: SourceType::UncategorizedRegistry,
};
let rdap_response = Rfc9083Error::builder()
.error_code(500)
.build()
.to_response();
let actual = table.to_md(crate::md::MdParams {
heading_level: 0,
root: &rdap_response,
http_data: &HttpData::example().build(),
parent_type: std::any::TypeId::of::<crate::md::MdParams>(),
check_types: &[],
options: &crate::md::MdOptions::plain_text(),
req_data: &req_data,
});
assert_eq!(actual, "|:-:|\n|__foo__|\n|\n\n")
}
#[test]
fn GIVEN_header_and_data_ref_WHEN_to_md_THEN_header_format_and_header() {
// GIVEN
let table = MultiPartTable::new()
.header_ref(&"foo")
.nv_ref(&"bizz", &"buzz");
// WHEN
let req_data = RequestData {
req_number: 0,
source_host: "",
source_type: SourceType::UncategorizedRegistry,
};
let rdap_response = Rfc9083Error::builder()
.error_code(500)
.build()
.to_response();
let actual = table.to_md(crate::md::MdParams {
heading_level: 0,
root: &rdap_response,
http_data: &HttpData::example().build(),
parent_type: std::any::TypeId::of::<crate::md::MdParams>(),
check_types: &[],
options: &crate::md::MdOptions::plain_text(),
req_data: &req_data,
});
assert_eq!(actual, "|:-:|\n|__foo__|\n|-:|:-|\n|bizz|buzz|\n|\n\n")
}
#[test]
fn GIVEN_header_and_2_data_ref_WHEN_to_md_THEN_header_format_and_header() {
// GIVEN
let table = MultiPartTable::new()
.header_ref(&"foo")
.nv_ref(&"bizz", &"buzz")
.nv_ref(&"bar", &"baz");
// WHEN
let req_data = RequestData {
req_number: 0,
source_host: "",
source_type: SourceType::UncategorizedRegistry,
};
let rdap_response = Rfc9083Error::builder()
.error_code(500)
.build()
.to_response();
let actual = table.to_md(crate::md::MdParams {
heading_level: 0,
root: &rdap_response,
http_data: &HttpData::example().build(),
parent_type: std::any::TypeId::of::<crate::md::MdParams>(),
check_types: &[],
options: &crate::md::MdOptions::plain_text(),
req_data: &req_data,
});
assert_eq!(
actual,
"|:-:|\n|__foo__|\n|-:|:-|\n|bizz|buzz|\n| bar|baz|\n|\n\n"
)
}
#[test]
fn GIVEN_header_and_data_WHEN_to_md_THEN_header_format_and_header() {
// GIVEN
let table = MultiPartTable::new()
.header_ref(&"foo")
.nv(&"bizz", "buzz".to_string());
// WHEN
let req_data = RequestData {
req_number: 0,
source_host: "",
source_type: SourceType::UncategorizedRegistry,
};
let rdap_response = Rfc9083Error::builder()
.error_code(500)
.build()
.to_response();
let actual = table.to_md(crate::md::MdParams {
heading_level: 0,
root: &rdap_response,
http_data: &HttpData::example().build(),
parent_type: std::any::TypeId::of::<crate::md::MdParams>(),
check_types: &[],
options: &crate::md::MdOptions::plain_text(),
req_data: &req_data,
});
assert_eq!(actual, "|:-:|\n|__foo__|\n|-:|:-|\n|bizz|buzz|\n|\n\n")
}
#[test]
fn GIVEN_header_and_2_data_WHEN_to_md_THEN_header_format_and_header() {
// GIVEN
let table = MultiPartTable::new()
.header_ref(&"foo")
.nv(&"bizz", "buzz")
.nv(&"bar", "baz");
// WHEN
let req_data = RequestData {
req_number: 0,
source_host: "",
source_type: SourceType::UncategorizedRegistry,
};
let rdap_response = Rfc9083Error::builder()
.error_code(500)
.build()
.to_response();
let actual = table.to_md(crate::md::MdParams {
heading_level: 0,
root: &rdap_response,
http_data: &HttpData::example().build(),
parent_type: std::any::TypeId::of::<crate::md::MdParams>(),
check_types: &[],
options: &crate::md::MdOptions::plain_text(),
req_data: &req_data,
});
assert_eq!(
actual,
"|:-:|\n|__foo__|\n|-:|:-|\n|bizz|buzz|\n| bar|baz|\n|\n\n"
)
}
#[test]
fn GIVEN_header_and_2_data_ref_twice_WHEN_to_md_THEN_header_format_and_header() {
// GIVEN
let table = MultiPartTable::new()
.header_ref(&"foo")
.nv_ref(&"bizz", &"buzz")
.nv_ref(&"bar", &"baz")
.header_ref(&"foo")
.nv_ref(&"bizz", &"buzz")
.nv_ref(&"bar", &"baz");
// WHEN
let req_data = RequestData {
req_number: 0,
source_host: "",
source_type: SourceType::UncategorizedRegistry,
};
let rdap_response = Rfc9083Error::builder()
.error_code(500)
.build()
.to_response();
let actual = table.to_md(crate::md::MdParams {
heading_level: 0,
root: &rdap_response,
http_data: &HttpData::example().build(),
parent_type: std::any::TypeId::of::<crate::md::MdParams>(),
check_types: &[],
options: &crate::md::MdOptions::plain_text(),
req_data: &req_data,
});
assert_eq!(
actual,
"|:-:|\n|__foo__|\n|-:|:-|\n|bizz|buzz|\n| bar|baz|\n|:-:|\n|__foo__|\n|-:|:-|\n|bizz|buzz|\n| bar|baz|\n|\n\n"
)
}
}

View file

@ -0,0 +1,468 @@
use {
icann_rdap_common::prelude::ObjectCommon,
std::{any::TypeId, sync::LazyLock},
};
use {
icann_rdap_common::{
check::StringCheck,
httpdata::HttpData,
response::{
Common, Event, Link, Links, NoticeOrRemark, Notices, PublicId, RdapConformance, Remarks,
},
},
reqwest::header::{
ACCESS_CONTROL_ALLOW_ORIGIN, CACHE_CONTROL, CONTENT_LENGTH, EXPIRES, HOST,
STRICT_TRANSPORT_SECURITY,
},
strum::EnumMessage,
};
use icann_rdap_common::check::{
CheckClass, CheckItem, CheckParams, Checks, GetChecks, CHECK_CLASS_LEN,
};
use super::{
checks_ul,
string::{StringListUtil, StringUtil},
table::{MultiPartTable, ToMpTable},
FromMd, MdParams, ToMd, HR,
};
impl ToMd for RdapConformance {
fn to_md(&self, params: MdParams) -> String {
let mut md = String::new();
md.push_str(
&format!(
"{} Conformance Claims",
params.req_data.source_host.to_title_case()
)
.to_header(5, params.options),
);
self.iter().for_each(|s| {
md.push_str(&format!(
"* {}\n",
s.0.replace('_', " ")
.to_cap_acronyms()
.to_words_title_case()
))
});
self.get_checks(CheckParams::from_md_no_parent(params))
.items
.iter()
.filter(|item| params.check_types.contains(&item.check_class))
.for_each(|item| {
md.push_str(&format!(
"* {}: {}\n",
item.check_class.to_string().to_em(params.options),
item.check
.get_message()
.expect("Check has no message. Coding error.")
))
});
md.push('\n');
md
}
}
impl ToMd for Links {
fn to_md(&self, mdparams: MdParams) -> String {
let mut md = String::new();
self.iter()
.for_each(|link| md.push_str(&link.to_md(mdparams)));
md
}
}
impl ToMd for Link {
fn to_md(&self, params: MdParams) -> String {
let mut md = String::new();
if let Some(title) = &self.title {
md.push_str(&format!("* {}:\n", title.replace_md_chars()));
} else {
md.push_str("* Link:\n")
};
if let Some(href) = &self.href {
md.push_str(&format!(
"* {}\n",
href.to_owned().to_inline(params.options)
));
};
if let Some(rel) = &self.rel {
md.push_str(&format!("* Relation: {}\n", rel.replace_md_chars()));
};
if let Some(media_type) = &self.media_type {
md.push_str(&format!("* Type: {}\n", media_type.replace_md_chars()));
};
if let Some(media) = &self.media {
md.push_str(&format!("* Media: {}\n", media.replace_md_chars()));
};
if let Some(value) = &self.value {
md.push_str(&format!("* Value: {}\n", value.replace_md_chars()));
};
if let Some(hreflang) = &self.hreflang {
match hreflang {
icann_rdap_common::response::HrefLang::Lang(lang) => {
md.push_str(&format!("* Language: {}\n", lang.replace_md_chars()));
}
icann_rdap_common::response::HrefLang::Langs(langs) => {
md.push_str(&format!(
"* Languages: {}",
langs.join(", ").replace_md_chars()
));
}
}
};
let checks = self.get_checks(CheckParams::from_md(params, TypeId::of::<Link>()));
md.push_str(&checks_ul(&checks, params));
md.push('\n');
md
}
}
impl ToMd for Notices {
fn to_md(&self, params: MdParams) -> String {
let mut md = String::new();
self.iter()
.for_each(|notice| md.push_str(&notice.0.to_md(params)));
md
}
}
impl ToMd for Remarks {
fn to_md(&self, params: MdParams) -> String {
let mut md = String::new();
self.iter()
.for_each(|remark| md.push_str(&remark.0.to_md(params)));
md
}
}
impl ToMd for Option<Remarks> {
fn to_md(&self, params: MdParams) -> String {
if let Some(remarks) = &self {
remarks.to_md(params)
} else {
String::new()
}
}
}
impl ToMd for NoticeOrRemark {
fn to_md(&self, params: MdParams) -> String {
let mut md = String::new();
if let Some(title) = &self.title {
md.push_str(&format!("{}\n", title.to_bold(params.options)));
};
if let Some(nr_type) = &self.nr_type {
md.push_str(&format!("Type: {}\n", nr_type.to_words_title_case()));
};
if let Some(description) = &self.description {
description.vec().iter().for_each(|s| {
if !s.is_whitespace_or_empty() {
md.push_str(&format!("> {}\n\n", s.trim().replace_md_chars()))
}
});
}
self.get_checks(CheckParams::from_md(params, TypeId::of::<Self>()))
.items
.iter()
.filter(|item| params.check_types.contains(&item.check_class))
.for_each(|item| {
md.push_str(&format!(
"* {}: {}\n",
&item.check_class.to_string().to_em(params.options),
item.check
.get_message()
.expect("Check has no message. Coding error.")
))
});
if let Some(links) = &self.links {
links
.iter()
.for_each(|link| md.push_str(&link.to_md(params)));
}
md.push('\n');
md
}
}
impl ToMd for Common {
fn to_md(&self, params: MdParams) -> String {
let mut md = String::new();
let not_empty = self.rdap_conformance.is_some() || self.notices.is_some();
if not_empty {
md.push('\n');
md.push_str(HR);
let header_text = format!(
"Response from {} at {}",
params.req_data.source_type,
params.req_data.source_host.to_title_case()
);
md.push_str(&header_text.to_header(params.heading_level, params.options));
};
if let Some(rdap_conformance) = &self.rdap_conformance {
md.push_str(&rdap_conformance.to_md(params));
};
if let Some(notices) = &self.notices {
md.push_str(&"Server Notices".to_header(5, params.options));
md.push_str(&notices.to_md(params));
}
if not_empty {
md.push_str(HR);
};
md
}
}
const RECEIVED: &str = "Received";
const REQUEST_URI: &str = "Request URI";
pub static NAMES: LazyLock<[String; 7]> = LazyLock::new(|| {
[
HOST.to_string(),
reqwest::header::EXPIRES.to_string(),
reqwest::header::CACHE_CONTROL.to_string(),
reqwest::header::STRICT_TRANSPORT_SECURITY.to_string(),
reqwest::header::ACCESS_CONTROL_ALLOW_ORIGIN.to_string(),
RECEIVED.to_string(),
REQUEST_URI.to_string(),
]
});
pub static NAME_LEN: LazyLock<usize> = LazyLock::new(|| {
NAMES
.iter()
.max_by_key(|x| x.to_string().len())
.map_or(8, |x| x.to_string().len())
});
impl ToMd for HttpData {
fn to_md(&self, params: MdParams) -> String {
let mut md = HR.to_string();
md.push_str(&format!(" * {:<NAME_LEN$}: {}\n", HOST, &self.host));
if let Some(request_uri) = &self.request_uri {
md.push_str(&format!(" * {:<NAME_LEN$}: {}\n", REQUEST_URI, request_uri));
}
if let Some(content_length) = &self.content_length {
md.push_str(&format!(
" * {:<NAME_LEN$}: {}\n",
CONTENT_LENGTH, content_length
));
}
if let Some(expires) = &self.expires {
md.push_str(&format!(" * {:<NAME_LEN$}: {}\n", EXPIRES, expires));
}
if let Some(cache_control) = &self.cache_control {
md.push_str(&format!(
" * {:<NAME_LEN$}: {}\n",
CACHE_CONTROL, cache_control
));
}
if let Some(strict_transport_security) = &self.strict_transport_security {
md.push_str(&format!(
" * {:<NAME_LEN$}: {}\n",
STRICT_TRANSPORT_SECURITY, strict_transport_security
));
}
if let Some(access_control_allow_origin) = &self.access_control_allow_origin {
md.push_str(&format!(
" * {:<NAME_LEN$}: {}\n",
ACCESS_CONTROL_ALLOW_ORIGIN, access_control_allow_origin
));
}
md.push_str(&format!(" * {RECEIVED:<NAME_LEN$}: {}\n", &self.received));
self.get_checks(CheckParams::from_md(params, TypeId::of::<NoticeOrRemark>()))
.items
.iter()
.filter(|item| params.check_types.contains(&item.check_class))
.for_each(|item| {
md.push_str(&format!(
"* {}: {}\n",
&item.check_class.to_string().to_em(params.options),
item.check
.get_message()
.expect("Check has no message. Coding error.")
))
});
md
}
}
impl ToMpTable for ObjectCommon {
fn add_to_mptable(&self, mut table: MultiPartTable, params: MdParams) -> MultiPartTable {
if self.status.is_some() || self.port_43.is_some() {
table = table.header_ref(&"Information");
// Status
if let Some(status) = &self.status {
let values = status.vec();
table = table.nv_ul(&"Status", values.make_list_all_title_case());
}
// Port 43
table = table.and_nv_ref(&"Whois", &self.port_43);
}
// Events
if let Some(events) = &self.events {
table = events_to_table(events, table, "Events", params);
}
// Links
if let Some(links) = &self.links {
table = links_to_table(links, table, "Links");
}
// TODO Checks
table
}
}
pub(crate) fn public_ids_to_table(
publid_ids: &[PublicId],
mut table: MultiPartTable,
) -> MultiPartTable {
for pid in publid_ids {
table = table.nv_ref(
pid.id_type.as_ref().unwrap_or(&"(not given)".to_string()),
pid.identifier
.as_ref()
.unwrap_or(&"(not given)".to_string()),
);
}
table
}
pub(crate) fn events_to_table(
events: &[Event],
mut table: MultiPartTable,
header_name: &str,
params: MdParams,
) -> MultiPartTable {
table = table.header_ref(&header_name.replace_md_chars());
for event in events {
let event_date = &event
.event_date
.to_owned()
.unwrap_or("????".to_string())
.format_date_time(params)
.unwrap_or_default();
let mut ul: Vec<&String> = vec![event_date];
if let Some(event_actor) = &event.event_actor {
ul.push(event_actor);
}
table = table.nv_ul_ref(
&event
.event_action
.as_ref()
.unwrap_or(&"action not given".to_string())
.to_owned()
.to_words_title_case(),
ul,
);
}
table
}
pub(crate) fn links_to_table(
links: &[Link],
mut table: MultiPartTable,
header_name: &str,
) -> MultiPartTable {
table = table.header_ref(&header_name.replace_md_chars());
for link in links {
if let Some(title) = &link.title {
table = table.nv_ref(&"Title", &title.trim());
};
let rel = link
.rel
.as_ref()
.unwrap_or(&"Link".to_string())
.to_title_case();
let mut ul: Vec<&String> = vec![];
if let Some(href) = &link.href {
ul.push(href)
}
if let Some(media_type) = &link.media_type {
ul.push(media_type)
};
if let Some(media) = &link.media {
ul.push(media)
};
if let Some(value) = &link.value {
ul.push(value)
};
let hreflang_s;
if let Some(hreflang) = &link.hreflang {
hreflang_s = match hreflang {
icann_rdap_common::response::HrefLang::Lang(lang) => lang.to_owned(),
icann_rdap_common::response::HrefLang::Langs(langs) => langs.join(", "),
};
ul.push(&hreflang_s)
};
table = table.nv_ul_ref(&rel, ul);
}
table
}
pub(crate) fn checks_to_table(
checks: Vec<Checks>,
mut table: MultiPartTable,
params: MdParams,
) -> MultiPartTable {
let mut filtered_checks: Vec<CheckItem> = checks
.into_iter()
.flat_map(|checks| checks.items)
.filter(|item| params.check_types.contains(&item.check_class))
.collect();
if !filtered_checks.is_empty() {
filtered_checks.sort();
filtered_checks.dedup();
table = table.header_ref(&"Checks");
// Informational
let class = CheckClass::Informational;
let ul: Vec<String> = filtered_checks
.iter()
.filter(|item| item.check_class == class)
.map(|item| item.check.get_message().unwrap_or_default().to_owned())
.collect();
table = table.nv_ul_ref(
&&class
.to_string()
.to_right_em(*CHECK_CLASS_LEN, params.options),
ul.iter().collect(),
);
// Specification Warning
let class = CheckClass::StdWarning;
let ul: Vec<String> = filtered_checks
.iter()
.filter(|item| item.check_class == class)
.map(|item| item.check.get_message().unwrap_or_default().to_owned())
.collect();
table = table.nv_ul_ref(
&class
.to_string()
.to_right_em(*CHECK_CLASS_LEN, params.options),
ul.iter().collect(),
);
// Specification Error
let class = CheckClass::StdError;
let ul: Vec<String> = filtered_checks
.iter()
.filter(|item| item.check_class == class)
.map(|item| item.check.get_message().unwrap_or_default().to_owned())
.collect();
table = table.nv_ul_ref(
&&class
.to_string()
.to_right_em(*CHECK_CLASS_LEN, params.options),
ul.iter().collect(),
);
}
table
}

View file

@ -0,0 +1,15 @@
//! Code for managing RDAP queries.
#[doc(inline)]
pub use qtype::*;
#[doc(inline)]
pub use registered_redactions::*;
#[doc(inline)]
pub use request::*;
#[doc(inline)]
pub use rr::*;
pub(crate) mod qtype;
pub(crate) mod registered_redactions;
pub(crate) mod request;
pub(crate) mod rr;

View file

@ -0,0 +1,645 @@
//! Defines the various types of RDAP queries.
use std::{
net::{IpAddr, Ipv4Addr, Ipv6Addr},
str::FromStr,
sync::LazyLock,
};
use {
cidr::{IpCidr, Ipv4Cidr, Ipv6Cidr},
icann_rdap_common::{check::StringCheck, dns_types::DomainName},
pct_str::{PctString, URIReserved},
regex::Regex,
strum_macros::Display,
};
use crate::RdapClientError;
/// Defines the various types of RDAP lookups and searches.
#[derive(Display, Debug)]
pub enum QueryType {
#[strum(serialize = "IpV4 Address Lookup")]
IpV4Addr(Ipv4Addr),
#[strum(serialize = "IpV6 Address Lookup")]
IpV6Addr(Ipv6Addr),
#[strum(serialize = "IpV4 CIDR Lookup")]
IpV4Cidr(Ipv4Cidr),
#[strum(serialize = "IpV6 CIDR Lookup")]
IpV6Cidr(Ipv6Cidr),
#[strum(serialize = "Autonomous System Number Lookup")]
AsNumber(u32),
#[strum(serialize = "Domain Lookup")]
Domain(DomainName),
#[strum(serialize = "A-Label Domain Lookup")]
ALabel(DomainName),
#[strum(serialize = "Entity Lookup")]
Entity(String),
#[strum(serialize = "Nameserver Lookup")]
Nameserver(DomainName),
#[strum(serialize = "Entity Name Search")]
EntityNameSearch(String),
#[strum(serialize = "Entity Handle Search")]
EntityHandleSearch(String),
#[strum(serialize = "Domain Name Search")]
DomainNameSearch(String),
#[strum(serialize = "Domain Nameserver Name Search")]
DomainNsNameSearch(String),
#[strum(serialize = "Domain Nameserver IP Address Search")]
DomainNsIpSearch(IpAddr),
#[strum(serialize = "Nameserver Name Search")]
NameserverNameSearch(String),
#[strum(serialize = "Nameserver IP Address Search")]
NameserverIpSearch(IpAddr),
#[strum(serialize = "Server Help Lookup")]
Help,
#[strum(serialize = "Explicit URL")]
Url(String),
}
impl QueryType {
pub fn query_url(&self, base_url: &str) -> Result<String, RdapClientError> {
let base_url = base_url.trim_end_matches('/');
match self {
Self::IpV4Addr(value) => Ok(format!(
"{base_url}/ip/{}",
PctString::encode(value.to_string().chars(), URIReserved)
)),
Self::IpV6Addr(value) => Ok(format!(
"{base_url}/ip/{}",
PctString::encode(value.to_string().chars(), URIReserved)
)),
Self::IpV4Cidr(value) => Ok(format!(
"{base_url}/ip/{}/{}",
PctString::encode(value.first_address().to_string().chars(), URIReserved),
PctString::encode(value.network_length().to_string().chars(), URIReserved)
)),
Self::IpV6Cidr(value) => Ok(format!(
"{base_url}/ip/{}/{}",
PctString::encode(value.first_address().to_string().chars(), URIReserved),
PctString::encode(value.network_length().to_string().chars(), URIReserved)
)),
Self::AsNumber(value) => Ok(format!(
"{base_url}/autnum/{}",
PctString::encode(value.to_string().chars(), URIReserved)
)),
Self::Domain(value) => Ok(format!(
"{base_url}/domain/{}",
PctString::encode(value.trim_leading_dot().chars(), URIReserved)
)),
Self::ALabel(value) => Ok(format!(
"{base_url}/domain/{}",
PctString::encode(value.to_ascii().chars(), URIReserved),
)),
Self::Entity(value) => Ok(format!(
"{base_url}/entity/{}",
PctString::encode(value.chars(), URIReserved)
)),
Self::Nameserver(value) => Ok(format!(
"{base_url}/nameserver/{}",
PctString::encode(value.to_ascii().chars(), URIReserved)
)),
Self::EntityNameSearch(value) => search_query(value, "entities?fn", base_url),
Self::EntityHandleSearch(value) => search_query(value, "entities?handle", base_url),
Self::DomainNameSearch(value) => search_query(value, "domains?name", base_url),
Self::DomainNsNameSearch(value) => search_query(value, "domains?nsLdhName", base_url),
Self::DomainNsIpSearch(value) => {
search_query(&value.to_string(), "domains?nsIp", base_url)
}
Self::NameserverNameSearch(value) => search_query(value, "nameservers?name", base_url),
Self::NameserverIpSearch(value) => {
search_query(&value.to_string(), "nameservers?ip", base_url)
}
Self::Help => Ok(format!("{base_url}/help")),
Self::Url(url) => Ok(url.to_owned()),
}
}
pub fn domain(domain_name: &str) -> Result<QueryType, RdapClientError> {
Ok(Self::Domain(DomainName::from_str(domain_name)?))
}
pub fn alabel(alabel: &str) -> Result<QueryType, RdapClientError> {
Ok(Self::ALabel(DomainName::from_str(alabel)?))
}
pub fn ns(nameserver: &str) -> Result<QueryType, RdapClientError> {
Ok(Self::Nameserver(DomainName::from_str(nameserver)?))
}
pub fn autnum(autnum: &str) -> Result<QueryType, RdapClientError> {
let value = autnum
.trim_start_matches(|c| -> bool { matches!(c, 'a' | 'A' | 's' | 'S') })
.parse::<u32>()
.map_err(|_e| RdapClientError::InvalidQueryValue)?;
Ok(Self::AsNumber(value))
}
pub fn ipv4(ip: &str) -> Result<QueryType, RdapClientError> {
let value = Ipv4Addr::from_str(ip).map_err(|_e| RdapClientError::InvalidQueryValue)?;
Ok(Self::IpV4Addr(value))
}
pub fn ipv6(ip: &str) -> Result<QueryType, RdapClientError> {
let value = Ipv6Addr::from_str(ip).map_err(|_e| RdapClientError::InvalidQueryValue)?;
Ok(Self::IpV6Addr(value))
}
pub fn ipv4cidr(cidr: &str) -> Result<QueryType, RdapClientError> {
let value = cidr::parsers::parse_cidr_ignore_hostbits::<IpCidr, _>(
cidr,
cidr::parsers::parse_loose_ip,
)
.map_err(|_e| RdapClientError::InvalidQueryValue)?;
if let IpCidr::V4(v4) = value {
Ok(Self::IpV4Cidr(v4))
} else {
Err(RdapClientError::AmbiquousQueryType)
}
}
pub fn ipv6cidr(cidr: &str) -> Result<QueryType, RdapClientError> {
let value = cidr::parsers::parse_cidr_ignore_hostbits::<IpCidr, _>(
cidr,
cidr::parsers::parse_loose_ip,
)
.map_err(|_e| RdapClientError::InvalidQueryValue)?;
if let IpCidr::V6(v6) = value {
Ok(Self::IpV6Cidr(v6))
} else {
Err(RdapClientError::AmbiquousQueryType)
}
}
pub fn domain_ns_ip_search(ip: &str) -> Result<QueryType, RdapClientError> {
let value = IpAddr::from_str(ip).map_err(|_e| RdapClientError::InvalidQueryValue)?;
Ok(Self::DomainNsIpSearch(value))
}
pub fn ns_ip_search(ip: &str) -> Result<QueryType, RdapClientError> {
let value = IpAddr::from_str(ip).map_err(|_e| RdapClientError::InvalidQueryValue)?;
Ok(Self::NameserverIpSearch(value))
}
}
fn search_query(value: &str, path_query: &str, base_url: &str) -> Result<String, RdapClientError> {
Ok(format!(
"{base_url}/{path_query}={}",
PctString::encode(value.chars(), URIReserved)
))
}
impl FromStr for QueryType {
type Err = RdapClientError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
// if it looks like a HTTP(S) url
if s.starts_with("http://") || s.starts_with("https://") {
return Ok(Self::Url(s.to_owned()));
}
// if looks like an autnum
let autnum = s.trim_start_matches(|c| -> bool { matches!(c, 'a' | 'A' | 's' | 'S') });
if u32::from_str(autnum).is_ok() {
return Self::autnum(s);
}
// If it's an IP address
if let Ok(ip_addr) = IpAddr::from_str(s) {
if ip_addr.is_ipv4() {
return Self::ipv4(s);
} else {
return Self::ipv6(s);
}
}
// if it is a cidr
if let Ok(ip_cidr) = parse_cidr(s) {
return Ok(match ip_cidr {
IpCidr::V4(cidr) => Self::IpV4Cidr(cidr),
IpCidr::V6(cidr) => Self::IpV6Cidr(cidr),
});
}
// if it looks like a domain name
if is_domain_name(s) {
return if is_nameserver(s) {
Self::ns(s)
} else {
Self::domain(s)
};
}
// if it is just one word
if !s.contains(|c: char| c.is_whitespace() || matches!(c, '.' | ',' | '"')) {
return Ok(Self::Entity(s.to_owned()));
}
// The query type cannot be deteremined.
Err(RdapClientError::AmbiquousQueryType)
}
}
fn parse_cidr(s: &str) -> Result<IpCidr, RdapClientError> {
let Some((prefix, suffix)) = s.split_once('/') else {
return Err(RdapClientError::InvalidQueryValue);
};
if prefix.chars().all(|c: char| c.is_ascii_alphanumeric()) {
let cidr = cidr::parsers::parse_short_ip_address_as_cidr(prefix)
.map_err(|_e| RdapClientError::InvalidQueryValue)?;
IpCidr::new(
cidr.first_address(),
suffix
.parse::<u8>()
.map_err(|_e| RdapClientError::InvalidQueryValue)?,
)
.map_err(|_e| RdapClientError::InvalidQueryValue)
} else {
cidr::parsers::parse_cidr_ignore_hostbits::<IpCidr, _>(s, cidr::parsers::parse_loose_ip)
.map_err(|_e| RdapClientError::InvalidQueryValue)
}
}
fn is_ldh_domain(text: &str) -> bool {
static LDH_DOMAIN_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^(?i)(\.?[a-zA-Z0-9-]+)*\.[a-zA-Z0-9-]+\.?$").unwrap());
LDH_DOMAIN_RE.is_match(text)
}
fn is_domain_name(text: &str) -> bool {
text.contains('.') && text.is_unicode_domain_name()
}
fn is_nameserver(text: &str) -> bool {
static NS_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^(?i)(ns)[a-zA-Z0-9-]*\.[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+\.?$").unwrap()
});
NS_RE.is_match(text)
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use rstest::rstest;
use super::*;
#[test]
fn test_ipv4_query_type_from_str() {
// GIVEN
let s = "129.129.1.1";
// WHEN
let q = QueryType::from_str(s);
// THEN
assert!(matches!(q.unwrap(), QueryType::IpV4Addr(_)))
}
#[test]
fn test_ipv6_query_type_from_str() {
// GIVEN
let s = "2001::1";
// WHEN
let q = QueryType::from_str(s);
// THEN
assert!(matches!(q.unwrap(), QueryType::IpV6Addr(_)))
}
#[test]
fn test_ipv4_cidr_query_type_from_str() {
// GIVEN
let s = "129.129.1.1/8";
// WHEN
let q = QueryType::from_str(s);
// THEN
assert!(matches!(q.unwrap(), QueryType::IpV4Cidr(_)))
}
#[test]
fn test_ipv6_cidr_query_type_from_str() {
// GIVEN
let s = "2001::1/20";
// WHEN
let q = QueryType::from_str(s);
// THEN
assert!(matches!(q.unwrap(), QueryType::IpV6Cidr(_)))
}
#[test]
fn test_number_query_type_from_str() {
// GIVEN
let s = "16509";
// WHEN
let q = QueryType::from_str(s);
// THEN
assert!(matches!(q.unwrap(), QueryType::AsNumber(_)))
}
#[test]
fn test_as_followed_by_number_query_type_from_str() {
// GIVEN
let s = "as16509";
// WHEN
let q = QueryType::from_str(s);
// THEN
assert!(matches!(q.unwrap(), QueryType::AsNumber(_)))
}
#[rstest]
#[case("example.com")]
#[case("foo.example.com")]
#[case("snark.fail")]
#[case("ns.fail")]
#[case(".com")]
fn test_domain_name_query_type_from_str(#[case] input: &str) {
// GIVEN case input
// WHEN
let q = QueryType::from_str(input);
// THEN
assert!(matches!(q.unwrap(), QueryType::Domain(_)))
}
#[rstest]
#[case("ns.example.com")]
#[case("ns1.example.com")]
#[case("NS1.example.com")]
fn test_name_server_query_type_from_str(#[case] input: &str) {
// GIVEN case input
// WHEN
let q = QueryType::from_str(input);
// THEN
assert!(matches!(q.unwrap(), QueryType::Nameserver(_)))
}
#[test]
fn test_single_word_query_type_from_str() {
// GIVEN
let s = "foo";
// WHEN
let q = QueryType::from_str(s);
// THEN
let q = q.unwrap();
assert!(matches!(q, QueryType::Entity(_)))
}
#[rstest]
#[case("https://example.com")]
#[case("http://foo.example.com")]
fn test_url_query_type_from_str(#[case] input: &str) {
// GIVEN case input
// WHEN
let q = QueryType::from_str(input);
// THEN
assert!(matches!(q.unwrap(), QueryType::Url(_)))
}
#[rstest]
#[case("ns.foo_bar.com")]
#[case("ns.foo bar.com")]
fn test_bad_input_query_type_from_str(#[case] input: &str) {
// GIVEN case input
// WHEN
let q = QueryType::from_str(input);
// THEN
assert!(q.is_err());
}
#[rstest]
#[case("10.0.0.0/8", "10.0.0.0/8")]
#[case("10.0.0/8", "10.0.0.0/8")]
#[case("10.0/8", "10.0.0.0/8")]
#[case("10/8", "10.0.0.0/8")]
#[case("10.0.0.0/24", "10.0.0.0/24")]
#[case("10.0.0/24", "10.0.0.0/24")]
#[case("10.0/24", "10.0.0.0/24")]
#[case("10/24", "10.0.0.0/24")]
#[case("129.129.1.1/8", "129.0.0.0/8")]
#[case("2001::1/32", "2001::/32")]
fn test_cidr_parse_cidr(#[case] actual: &str, #[case] expected: &str) {
// GIVEN case input
// WHEN
let q = parse_cidr(actual);
// THEN
assert_eq!(q.unwrap().to_string(), expected)
}
#[test]
fn test_ipv4addr_query_url() {
// GIVEN ipv4 addr query
let q = QueryType::from_str("199.1.1.1").expect("query type");
// WHEN
let actual = q.query_url("https://example.com").expect("query url");
// THEN
assert_eq!(actual, "https://example.com/ip/199.1.1.1")
}
#[test]
fn test_ipv6addr_query_url() {
// GIVEN
let q = QueryType::from_str("2000::1").expect("query type");
// WHEN
let actual = q.query_url("https://example.com").expect("query url");
// THEN
assert_eq!(actual, "https://example.com/ip/2000%3A%3A1")
}
#[test]
fn test_ipv4cidr_query_url() {
// GIVEN
let q = QueryType::from_str("199.1.1.1/16").expect("query type");
// WHEN
let actual = q.query_url("https://example.com").expect("query url");
// THEN
assert_eq!(actual, "https://example.com/ip/199.1.0.0/16")
}
#[test]
fn test_ipv6cidr_query_url() {
// GIVEN
let q = QueryType::from_str("2000::1/16").expect("query type");
// WHEN
let actual = q.query_url("https://example.com").expect("query url");
// THEN
assert_eq!(actual, "https://example.com/ip/2000%3A%3A/16")
}
#[test]
fn test_autnum_query_url() {
// GIVEN
let q = QueryType::from_str("as16509").expect("query type");
// WHEN
let actual = q.query_url("https://example.com").expect("query url");
// THEN
assert_eq!(actual, "https://example.com/autnum/16509")
}
#[test]
fn test_domain_query_url() {
// GIVEN
let q = QueryType::from_str("example.com").expect("query type");
// WHEN
let actual = q.query_url("https://example.com").expect("query url");
// THEN
assert_eq!(actual, "https://example.com/domain/example.com")
}
#[test]
fn test_ns_query_url() {
// GIVEN
let q = QueryType::from_str("ns.example.com").expect("query type");
// WHEN
let actual = q.query_url("https://example.com").expect("query url");
// THEN
assert_eq!(actual, "https://example.com/nameserver/ns.example.com")
}
#[test]
fn test_entity_query_url() {
// GIVEN
let q = QueryType::from_str("foo").expect("query type");
// WHEN
let actual = q.query_url("https://example.com").expect("query url");
// THEN
assert_eq!(actual, "https://example.com/entity/foo")
}
#[test]
fn test_entity_name_search_query_url() {
// GIVEN
let q = QueryType::EntityNameSearch("foo".to_string());
// WHEN
let actual = q.query_url("https://example.com").expect("query url");
// THEN
assert_eq!(actual, "https://example.com/entities?fn=foo")
}
#[test]
fn test_entity_handle_search_query_url() {
// GIVEN
let q = QueryType::EntityHandleSearch("foo".to_string());
// WHEN
let actual = q.query_url("https://example.com").expect("query url");
// THEN
assert_eq!(actual, "https://example.com/entities?handle=foo")
}
#[test]
fn test_domain_name_search_query_url() {
// GIVEN
let q = QueryType::DomainNameSearch("foo".to_string());
// WHEN
let actual = q.query_url("https://example.com").expect("query url");
// THEN
assert_eq!(actual, "https://example.com/domains?name=foo")
}
#[test]
fn test_domain_ns_name_search_query_url() {
// GIVEN
let q = QueryType::DomainNsNameSearch("foo".to_string());
// WHEN
let actual = q.query_url("https://example.com").expect("query url");
// THEN
assert_eq!(actual, "https://example.com/domains?nsLdhName=foo")
}
#[test]
fn test_domain_ns_ip_search_query_url() {
// GIVEN
let q = QueryType::DomainNsIpSearch(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)));
// WHEN
let actual = q.query_url("https://example.com").expect("query url");
// THEN
assert_eq!(actual, "https://example.com/domains?nsIp=1.1.1.1")
}
#[test]
fn test_ns_name_search_query_url() {
// GIVEN
let q = QueryType::NameserverNameSearch("foo".to_string());
// WHEN
let actual = q.query_url("https://example.com").expect("query url");
// THEN
assert_eq!(actual, "https://example.com/nameservers?name=foo")
}
#[test]
fn test_ns_ip_search_query_url() {
// GIVEN
let q = QueryType::NameserverIpSearch(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)));
// WHEN
let actual = q.query_url("https://example.com").expect("query url");
// THEN
assert_eq!(actual, "https://example.com/nameservers?ip=1.1.1.1")
}
}

View file

@ -0,0 +1,636 @@
//! Determines of an RFC 9537 registered redaction is present.
use {
icann_rdap_common::response::{Entity, EntityRole, RdapResponse},
strum_macros::{Display, EnumString},
};
/// Redacted types in the IANA registry
#[derive(Debug, PartialEq, Eq, EnumString, Display)]
pub enum RedactedName {
#[strum(serialize = "Registry Domain ID")]
RegistryDomainId,
#[strum(serialize = "Registry Registrant ID")]
RegistryRegistrantId,
#[strum(serialize = "Registrant Name")]
RegistrantName,
#[strum(serialize = "Registrant Organization")]
RegistrantOrganization,
#[strum(serialize = "Registrant Street")]
RegistrantStreet,
#[strum(serialize = "Registrant City")]
RegistrantCity,
#[strum(serialize = "Registrant Postal Code")]
RegistrantPostalCode,
#[strum(serialize = "Registrant Phone")]
RegistrantPhone,
#[strum(serialize = "Registrant Phone Ext")]
RegistrantPhoneExt,
#[strum(serialize = "Registrant Fax")]
RegistrantFax,
#[strum(serialize = "Registrant Fax Ext")]
RegistrantFaxExt,
#[strum(serialize = "Registrant Email")]
RegistrantEmail,
#[strum(serialize = "Registry Tech ID")]
RegistryTechId,
#[strum(serialize = "Tech Name")]
TechName,
#[strum(serialize = "Tech Phone")]
TechPhone,
#[strum(serialize = "Tech Phone Ext")]
TechPhoneExt,
#[strum(serialize = "Tech Email")]
TechEmail,
}
/// This function looks at the RDAP response to see if a
/// redaction is present where the type of redaction is registered
/// with the IANA.
///
/// * rdap_response - a reference to the RDAP response.
/// * redaction_type - a reference to the string registered in the IANA.
pub fn is_redaction_registered(
rdap_response: &RdapResponse,
redaction_type: &RedactedName,
) -> bool {
let object_common = match rdap_response {
RdapResponse::Entity(e) => Some(&e.object_common.redacted),
RdapResponse::Domain(d) => Some(&d.object_common.redacted),
RdapResponse::Nameserver(s) => Some(&s.object_common.redacted),
RdapResponse::Autnum(a) => Some(&a.object_common.redacted),
RdapResponse::Network(n) => Some(&n.object_common.redacted),
_ => None,
};
if let Some(Some(redacted_vec)) = object_common {
redacted_vec.iter().any(|r| {
if let Some(r_type) = &r.name.type_field {
r_type.eq_ignore_ascii_case(&redaction_type.to_string())
} else {
false
}
})
} else {
false
}
}
/// This function takes a set of [RedactedName]s instead of just one,
/// and runs them through [is_redaction_registered].
pub fn are_redactions_registered(
rdap_response: &RdapResponse,
redaction_types: &[&RedactedName],
) -> bool {
redaction_types
.iter()
.any(|rn| is_redaction_registered(rdap_response, rn))
}
/// This function substitutes redaction_text if [is_redaction_registered] returns true.
pub fn text_or_registered_redaction(
rdap_response: &RdapResponse,
redaction_type: &RedactedName,
text: &Option<String>,
redaction_text: &str,
) -> Option<String> {
if is_redaction_registered(rdap_response, redaction_type) {
Some(redaction_text.to_string())
} else {
text.clone()
}
}
/// This function checks that an entity has a certain role, and if so then
/// checks of the redaction is registered for IANA.
///
/// * rdap_response - a reference to the RDAP response.
/// * redaction_type - a reference to the string registered in the IANA.
/// * entity - a reference to the entity to check
/// * role - the role of the entity
pub fn is_redaction_registered_for_role(
rdap_response: &RdapResponse,
redaction_type: &RedactedName,
entity: &Entity,
entity_role: &EntityRole,
) -> bool {
let roles = entity.roles();
if roles
.iter()
.any(|r| r.eq_ignore_ascii_case(&entity_role.to_string()))
{
return is_redaction_registered(rdap_response, redaction_type);
}
false
}
/// Same as [is_redaction_registered_for_role] but takes an array of [EntityRole] references.
pub fn are_redactions_registered_for_roles(
rdap_response: &RdapResponse,
redaction_type: &[&RedactedName],
entity: &Entity,
entity_roles: &[&EntityRole],
) -> bool {
let roles = entity.roles();
if roles.iter().any(|r| {
entity_roles
.iter()
.any(|er| r.eq_ignore_ascii_case(&er.to_string()))
}) {
return are_redactions_registered(rdap_response, redaction_type);
}
false
}
/// This function substitutes redaction_text if [is_redaction_registered_for_role] return true.
pub fn text_or_registered_redaction_for_role(
rdap_response: &RdapResponse,
redaction_type: &RedactedName,
entity: &Entity,
entity_role: &EntityRole,
text: &Option<String>,
redaction_text: &str,
) -> Option<String> {
if is_redaction_registered_for_role(rdap_response, redaction_type, entity, entity_role) {
Some(redaction_text.to_string())
} else {
text.clone()
}
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use icann_rdap_common::{
prelude::ToResponse,
response::{
redacted::{Name, Redacted},
Domain,
},
};
use super::*;
#[test]
fn GIVEN_redaction_type_WHEN_search_for_type_THEN_true() {
// GIVEN
let domain = Domain::builder()
.ldh_name("example.com")
.redacted(vec![Redacted {
name: Name {
description: None,
type_field: Some(RedactedName::TechEmail.to_string()),
},
reason: None,
pre_path: None,
post_path: None,
path_lang: None,
replacement_path: None,
method: None,
}])
.build();
let rdap = domain.to_response();
// WHEN
let actual = is_redaction_registered(&rdap, &RedactedName::TechEmail);
// THEN
assert!(actual);
}
#[test]
fn GIVEN_redaction_type_WHEN_get_text_for_type_THEN_redacted_text_returned() {
// GIVEN
let domain = Domain::builder()
.ldh_name("example.com")
.redacted(vec![Redacted {
name: Name {
description: None,
type_field: Some(RedactedName::TechEmail.to_string()),
},
reason: None,
pre_path: None,
post_path: None,
path_lang: None,
replacement_path: None,
method: None,
}])
.build();
let rdap = domain.to_response();
// WHEN
let actual = text_or_registered_redaction(
&rdap,
&RedactedName::TechEmail,
&Some("not_redacted".to_string()),
"redacted",
);
// THEN
assert_eq!(actual, Some("redacted".to_string()));
}
#[test]
fn GIVEN_multiple_redaction_type_WHEN_search_for_one_of_the_types_THEN_true() {
// GIVEN
let domain = Domain::builder()
.ldh_name("example.com")
.redacted(vec![
Redacted {
name: Name {
description: None,
type_field: Some(RedactedName::TechEmail.to_string()),
},
reason: None,
pre_path: None,
post_path: None,
path_lang: None,
replacement_path: None,
method: None,
},
Redacted {
name: Name {
description: None,
type_field: Some(RedactedName::RegistryRegistrantId.to_string()),
},
reason: None,
pre_path: None,
post_path: None,
path_lang: None,
replacement_path: None,
method: None,
},
])
.build();
let rdap = domain.to_response();
// WHEN
let actual = is_redaction_registered(&rdap, &RedactedName::TechEmail);
// THEN
assert!(actual);
}
#[test]
fn GIVEN_multiple_redaction_type_WHEN_search_for_multiple_that_some_exist_THEN_true() {
// GIVEN
let domain = Domain::builder()
.ldh_name("example.com")
.redacted(vec![
Redacted {
name: Name {
description: None,
type_field: Some(RedactedName::TechEmail.to_string()),
},
reason: None,
pre_path: None,
post_path: None,
path_lang: None,
replacement_path: None,
method: None,
},
Redacted {
name: Name {
description: None,
type_field: Some(RedactedName::RegistryRegistrantId.to_string()),
},
reason: None,
pre_path: None,
post_path: None,
path_lang: None,
replacement_path: None,
method: None,
},
])
.build();
let rdap = domain.to_response();
// WHEN
let actual = are_redactions_registered(
&rdap,
&[&RedactedName::TechEmail, &RedactedName::RegistrantName],
);
// THEN
assert!(actual);
}
#[test]
fn GIVEN_multiple_redaction_type_WHEN_search_for_multiple_that_not_exist_THEN_false() {
// GIVEN
let domain = Domain::builder()
.ldh_name("example.com")
.redacted(vec![
Redacted {
name: Name {
description: None,
type_field: Some(RedactedName::TechEmail.to_string()),
},
reason: None,
pre_path: None,
post_path: None,
path_lang: None,
replacement_path: None,
method: None,
},
Redacted {
name: Name {
description: None,
type_field: Some(RedactedName::RegistryRegistrantId.to_string()),
},
reason: None,
pre_path: None,
post_path: None,
path_lang: None,
replacement_path: None,
method: None,
},
])
.build();
let rdap = domain.to_response();
// WHEN
let actual = are_redactions_registered(
&rdap,
&[
&RedactedName::RegistrantPhone,
&RedactedName::RegistrantName,
],
);
// THEN
assert!(!actual);
}
#[test]
fn GIVEN_no_redactions_WHEN_search_for_type_THEN_false() {
// GIVEN
let domain = Domain::builder().ldh_name("example.com").build();
let rdap = domain.to_response();
// WHEN
let actual = is_redaction_registered(&rdap, &RedactedName::TechEmail);
// THEN
assert!(!actual);
}
#[test]
fn GIVEN_redaction_type_WHEN_search_for_wrong_type_THEN_false() {
// GIVEN
let domain = Domain::builder()
.ldh_name("example.com")
.redacted(vec![Redacted {
name: Name {
description: None,
type_field: Some(RedactedName::RegistryRegistrantId.to_string()),
},
reason: None,
pre_path: None,
post_path: None,
path_lang: None,
replacement_path: None,
method: None,
}])
.build();
let rdap = domain.to_response();
// WHEN
let actual = is_redaction_registered(&rdap, &RedactedName::TechEmail);
// THEN
assert!(!actual);
}
#[test]
fn GIVEN_entity_and_redaction_type_WHEN_search_for_type_on_entity_with_role_THEN_true() {
// GIVEN
let domain = Domain::builder()
.ldh_name("example.com")
.redacted(vec![Redacted {
name: Name {
description: None,
type_field: Some(RedactedName::TechEmail.to_string()),
},
reason: None,
pre_path: None,
post_path: None,
path_lang: None,
replacement_path: None,
method: None,
}])
.build();
let rdap = domain.to_response();
let role = EntityRole::Technical.to_string();
let entity = Entity::builder()
.handle("foo_bar")
.role(role.clone())
.build();
// WHEN
let actual = is_redaction_registered_for_role(
&rdap,
&RedactedName::TechEmail,
&entity,
&EntityRole::Technical,
);
// THEN
assert!(actual);
}
#[test]
fn GIVEN_entity_and_multiple_redaction_WHEN_search_for_multipe_type_on_entity_with_roles_THEN_true(
) {
// GIVEN
let domain = Domain::builder()
.ldh_name("example.com")
.redacted(vec![
Redacted {
name: Name {
description: None,
type_field: Some(RedactedName::TechEmail.to_string()),
},
reason: None,
pre_path: None,
post_path: None,
path_lang: None,
replacement_path: None,
method: None,
},
Redacted {
name: Name {
description: None,
type_field: Some(RedactedName::RegistryRegistrantId.to_string()),
},
reason: None,
pre_path: None,
post_path: None,
path_lang: None,
replacement_path: None,
method: None,
},
])
.build();
let rdap = domain.to_response();
let role = EntityRole::Technical.to_string();
let entity = Entity::builder()
.handle("foo_bar")
.role(role.clone())
.build();
// WHEN
let actual = are_redactions_registered_for_roles(
&rdap,
&[&RedactedName::TechEmail, &RedactedName::TechPhoneExt],
&entity,
&[&EntityRole::Technical, &EntityRole::Abuse],
);
// THEN
assert!(actual);
}
#[test]
fn GIVEN_entity_and_multiple_redaction_WHEN_search_for_not_exist_type_on_entity_with_roles_THEN_false(
) {
// GIVEN
let domain = Domain::builder()
.ldh_name("example.com")
.redacted(vec![
Redacted {
name: Name {
description: None,
type_field: Some(RedactedName::TechEmail.to_string()),
},
reason: None,
pre_path: None,
post_path: None,
path_lang: None,
replacement_path: None,
method: None,
},
Redacted {
name: Name {
description: None,
type_field: Some(RedactedName::RegistryRegistrantId.to_string()),
},
reason: None,
pre_path: None,
post_path: None,
path_lang: None,
replacement_path: None,
method: None,
},
])
.build();
let rdap = domain.to_response();
let role = EntityRole::Technical.to_string();
let entity = Entity::builder()
.handle("foo_bar")
.role(role.clone())
.build();
// WHEN
let actual = are_redactions_registered_for_roles(
&rdap,
&[&RedactedName::TechPhone, &RedactedName::TechPhoneExt],
&entity,
&[&EntityRole::Technical, &EntityRole::Abuse],
);
// THEN
assert!(!actual);
}
#[test]
fn GIVEN_entity_and_multiple_redaction_WHEN_search_for_type_on_entity_with_other_rolesroles_THEN_false(
) {
// GIVEN
let domain = Domain::builder()
.ldh_name("example.com")
.redacted(vec![
Redacted {
name: Name {
description: None,
type_field: Some(RedactedName::TechEmail.to_string()),
},
reason: None,
pre_path: None,
post_path: None,
path_lang: None,
replacement_path: None,
method: None,
},
Redacted {
name: Name {
description: None,
type_field: Some(RedactedName::RegistryRegistrantId.to_string()),
},
reason: None,
pre_path: None,
post_path: None,
path_lang: None,
replacement_path: None,
method: None,
},
])
.build();
let rdap = domain.to_response();
let role = EntityRole::Technical.to_string();
let entity = Entity::builder()
.handle("foo_bar")
.role(role.clone())
.build();
// WHEN
let actual = are_redactions_registered_for_roles(
&rdap,
&[&RedactedName::TechEmail, &RedactedName::TechPhoneExt],
&entity,
&[&EntityRole::Billing, &EntityRole::Abuse],
);
// THEN
assert!(!actual);
}
#[test]
fn GIVEN_entity_and_redaction_type_WHEN_get_text_for_type_on_entity_with_role_THEN_redaction_text_returned(
) {
// GIVEN
let domain = Domain::builder()
.ldh_name("example.com")
.redacted(vec![Redacted {
name: Name {
description: None,
type_field: Some(RedactedName::TechEmail.to_string()),
},
reason: None,
pre_path: None,
post_path: None,
path_lang: None,
replacement_path: None,
method: None,
}])
.build();
let rdap = domain.to_response();
let role = EntityRole::Technical.to_string();
let entity = Entity::builder()
.handle("foo_bar")
.role(role.clone())
.build();
// WHEN
let actual = text_or_registered_redaction_for_role(
&rdap,
&RedactedName::TechEmail,
&entity,
&EntityRole::Technical,
&Some("not_redacted".to_string()),
"redacted",
);
// THEN
assert_eq!(actual, Some("redacted".to_string()));
}
}

View file

@ -0,0 +1,177 @@
//! Functions to make RDAP requests.
use {
icann_rdap_common::{httpdata::HttpData, iana::IanaRegistryType, response::RdapResponse},
serde::{Deserialize, Serialize},
serde_json::Value,
};
use crate::{
http::{wrapped_request, Client},
iana::bootstrap::{qtype_to_bootstrap_url, BootstrapStore},
RdapClientError,
};
use super::qtype::QueryType;
/// Makes an RDAP request with a full RDAP URL.
///
/// This function takes the following parameters:
/// * url - a string reference of the URL
/// * client - a reference to a [reqwest::Client].
///
/// ```no_run
/// use icann_rdap_client::prelude::*;
/// use std::str::FromStr;
/// use tokio::main;
///
/// #[tokio::main]
/// async fn main() -> Result<(), RdapClientError> {
///
/// // create a client (from icann-rdap-common)
/// let config = ClientConfig::default();
/// let client = create_client(&config)?;
///
/// // issue the RDAP query
/// let response =
/// rdap_url_request(
/// "https://rdap-bootstrap.arin.net/bootstrap/ip/192.168.0.1",
/// &client,
/// ).await?;
///
/// Ok(())
/// }
/// ```
pub async fn rdap_url_request(url: &str, client: &Client) -> Result<ResponseData, RdapClientError> {
let wrapped_response = wrapped_request(url, client).await?;
// for convenience purposes
let text = wrapped_response.text;
let http_data = wrapped_response.http_data;
let json: Result<Value, serde_json::Error> = serde_json::from_str(&text);
if let Ok(rdap_json) = json {
let rdap = RdapResponse::try_from(rdap_json)?;
Ok(ResponseData {
http_data,
rdap_type: rdap.to_string(),
rdap,
})
} else {
Err(RdapClientError::ParsingError(Box::new(
crate::ParsingErrorInfo {
text,
http_data,
error: json.err().unwrap(),
},
)))
}
}
/// Makes an RDAP request with a base URL.
///
/// This function takes the following parameters:
/// * base_url - a string reference of the base URL
/// * query_type - a reference to the RDAP query.
/// * client - a reference to a [reqwest::Client].
///
/// ```no_run
/// use icann_rdap_client::prelude::*;
/// use std::str::FromStr;
/// use tokio::main;
///
/// #[tokio::main]
/// async fn main() -> Result<(), RdapClientError> {
///
/// // create a query
/// let query = QueryType::from_str("192.168.0.1")?;
/// // or
/// let query = QueryType::from_str("icann.org")?;
///
/// // create a client (from icann-rdap-common)
/// let config = ClientConfig::default();
/// let client = create_client(&config)?;
///
/// // issue the RDAP query
/// let response =
/// rdap_request(
/// "https://rdap-bootstrap.arin.net/bootstrap",
/// &query,
/// &client,
/// ).await?;
///
/// Ok(())
/// }
/// ```
pub async fn rdap_request(
base_url: &str,
query_type: &QueryType,
client: &Client,
) -> Result<ResponseData, RdapClientError> {
let url = query_type.query_url(base_url)?;
rdap_url_request(&url, client).await
}
/// Makes an RDAP request using bootstrapping.
///
/// This function takes the following parameters:
/// * query_type - a reference to the RDAP query.
/// * client - a reference to a [reqwest::Client].
/// * store - a reference to a [BootstrapStore].
/// * callback - a closure that is called when an IANA registry is fetched.
///
/// The [BootstrapStore] is responsible for holding IANA RDAP bootstrap registries.
/// It will be populated with IANA registries as needed. Ideally, the calling code
/// would be kept it in the same scope as `client`. When using the [crate::iana::bootstrap::MemoryBootstrapStore],
/// creating a new store for each request will result it fetching the appropriate IANA
/// registry with each request which is most likely not the desired behavior.
///
/// ```no_run
/// use icann_rdap_client::prelude::*;
/// use std::str::FromStr;
/// use tokio::main;
///
/// #[tokio::main]
/// async fn main() -> Result<(), RdapClientError> {
///
/// // create a query
/// let query = QueryType::from_str("192.168.0.1")?;
/// // or
/// let query = QueryType::from_str("icann.org")?;
///
/// // create a client (from icann-rdap-common)
/// let config = ClientConfig::default();
/// let client = create_client(&config)?;
/// let store = MemoryBootstrapStore::new();
///
/// // issue the RDAP query
/// let response =
/// rdap_bootstrapped_request(
/// &query,
/// &client,
/// &store,
/// |reg| eprintln!("fetching {reg:?}")
/// ).await?;
///
/// Ok(())
/// }
/// ```
pub async fn rdap_bootstrapped_request<F>(
query_type: &QueryType,
client: &Client,
store: &dyn BootstrapStore,
callback: F,
) -> Result<ResponseData, RdapClientError>
where
F: FnOnce(&IanaRegistryType),
{
let base_url = qtype_to_bootstrap_url(client, store, query_type, callback).await?;
rdap_request(&base_url, query_type, client).await
}
/// The data returned from an rdap request.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ResponseData {
pub rdap: RdapResponse,
pub rdap_type: String,
pub http_data: HttpData,
}

View file

@ -0,0 +1,60 @@
//! Structures that describe a request/response.
use {
icann_rdap_common::check::Checks,
serde::{Deserialize, Serialize},
strum_macros::Display,
};
use crate::rdap::request::ResponseData;
/// Types of RDAP servers.
#[derive(Serialize, Deserialize, Display, Clone, Copy)]
pub enum SourceType {
#[strum(serialize = "Domain Registry")]
DomainRegistry,
#[strum(serialize = "Domain Registrar")]
DomainRegistrar,
#[strum(serialize = "Regional Internet Registry")]
RegionalInternetRegistry,
#[strum(serialize = "Local Internet Registry")]
LocalInternetRegistry,
#[strum(serialize = "Uncategorized Registry")]
UncategorizedRegistry,
}
/// Represents meta data about the request.
#[derive(Serialize, Deserialize, Clone, Copy)]
pub struct RequestData<'a> {
/// The request number. That is, request 1, request 2, etc...
pub req_number: usize,
/// A human-friendly name to identify the source of the information.
/// Examples might be "registry", "registrar", etc...
pub source_host: &'a str,
/// Represents the type of source.
pub source_type: SourceType,
}
/// Structure for serializing request and response data.
#[derive(Clone, Serialize)]
pub struct RequestResponse<'a> {
pub req_data: &'a RequestData<'a>,
pub res_data: &'a ResponseData,
pub checks: Checks,
}
/// The primary purpose for this struct is to allow deserialization for testing.
/// If somebody can help get #[serde(borrow)] to work for the non-owned version,
/// that would be awesome.
#[derive(Clone, Deserialize)]
pub struct RequestResponseOwned<'a> {
#[serde(borrow)]
pub req_data: RequestData<'a>,
pub res_data: ResponseData,
pub checks: Checks,
}
/// A [Vec] of [RequestResponse].
pub type RequestResponses<'a> = Vec<RequestResponse<'a>>;

View file

@ -0,0 +1,259 @@
{
"rdapConformance": [
"rdap_level_0",
"redacted"
],
"objectClassName": "domain",
"ldhName": "example-1.net",
"secureDNS": {
"delegationSigned": false
},
"notices": [
{
"title": "Terms of Use",
"description": [
"Service subject to Terms of Use."
],
"links": [
{
"rel": "self",
"href": "https://www.example.com/terms-of-use",
"type": "text/html",
"value": "https://www.example.com/terms-of-use"
}
]
}
],
"nameservers": [
{
"objectClassName": "nameserver",
"ldhName": "ns1.example.com"
},
{
"objectClassName": "nameserver",
"ldhName": "ns2.example.com"
}
],
"entities": [
{
"objectClassName": "entity",
"handle": "123",
"roles": [
"registrar"
],
"publicIds": [
{
"type": "IANA Registrar ID",
"identifier": "1"
}
],
"vcardArray": [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"fn",
{},
"text",
"Example Registrar Inc."
],
[
"adr",
{},
"text",
[
"",
"Suite 100",
"123 Example Dr.",
"Dulles",
"VA",
"20166-6503",
"US"
]
],
[
"email",
{},
"text",
"contact@organization.example"
],
[
"tel",
{
"type": "voice"
},
"uri",
"tel:+1.7035555555"
],
[
"tel",
{
"type": "fax"
},
"uri",
"tel:+1.7035555556"
]
]
],
"entities": [
{
"objectClassName": "entity",
"roles": [
"abuse"
],
"vcardArray": [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"fn",
{},
"text",
"Abuse Contact"
],
[
"email",
{},
"text",
"abuse@organization.example"
],
[
"tel",
{
"type": "voice"
},
"uri",
"tel:+1.7035555555"
]
]
]
}
]
},
{
"objectClassName": "entity",
"handle": "XXXX",
"roles": [
"registrant"
],
"vcardArray": [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"fn",
{},
"text",
"*REDACTED*"
],
[
"adr",
{},
"text",
[
"",
"",
"",
"",
"QC",
"",
"Canada"
]
]
]
]
},
{
"objectClassName": "entity",
"handle": "YYYY",
"roles": [
"technical"
],
"vcardArray": [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"fn",
{},
"text",
""
],
[
"org",
{},
"text",
"Example Inc."
],
[
"adr",
{},
"text",
[
"",
"Suite 1234",
"4321 Rue Somewhere",
"Quebec",
"QC",
"G1V 2M2",
"Canada"
]
]
]
]
}
],
"events": [
{
"eventAction": "registration",
"eventDate": "1997-06-03T00:00:00Z"
},
{
"eventAction": "last changed",
"eventDate": "2020-05-28T01:35:00Z"
},
{
"eventAction": "expiration",
"eventDate": "2021-06-03T04:00:00Z"
}
],
"status": [
"server delete prohibited",
"server update prohibited",
"server transfer prohibited",
"client transfer prohibited"
],
"redacted": [
{
"name": {
"description": "Registrant Name"
},
"postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='fn')][3]",
"pathLang": "jsonpath",
"method": "emptyValue",
"reason": {
"description": "Server policy"
}
}
]
}

View file

@ -0,0 +1,240 @@
{
"rdapConformance": [
"rdap_level_0",
"redacted"
],
"objectClassName": "domain",
"ldhName": "example-1.net",
"secureDNS": { "delegationSigned": false },
"notices": [
{
"title": "Terms of Use",
"description": [ "Service subject to Terms of Use." ],
"links": [
{
"rel": "self",
"href": "https://www.example.com/terms-of-use",
"type": "text/html",
"value": "https://www.example.com/terms-of-use"
}
]
}
],
"nameservers": [
{
"objectClassName": "nameserver", "ldhName": "ns1.example.com" },
{
"objectClassName": "nameserver", "ldhName": "ns2.example.com" }
],
"entities": [
{
"objectClassName": "entity",
"handle": "123",
"roles": [ "registrar" ],
"publicIds": [
{ "type": "IANA Registrar ID", "identifier": "1" }
],
"vcardArray": [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"fn",
{},
"text",
"Example Registrar Inc."
],
[
"adr",
{},
"text",
[
"",
"Suite 100",
"123 Example Dr.",
"Dulles",
"VA",
"20166-6503",
"US"
]
],
[
"email",
{},
"text",
"contact@organization.example"
],
[
"tel",
{
"type": "voice"
},
"uri",
"tel:+1.7035555555"
],
[
"tel",
{
"type": "fax"
},
"uri",
"tel:+1.7035555556"
]
]
],
"entities": [
{
"objectClassName": "entity",
"roles": [
"abuse"
],
"vcardArray": [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"fn",
{},
"text",
"Abuse Contact"
],
[
"email",
{},
"text",
"abuse@organization.example"
],
[
"tel",
{
"type": "voice"
},
"uri",
"tel:+1.7035555555"
]
]
]
}
]
},
{
"objectClassName": "entity",
"handle": "XXXX",
"roles": [
"registrant"
],
"vcardArray": [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"fn",
{},
"text",
""
],
[
"adr",
{},
"text",
[
"",
"",
"",
"",
"QC",
"",
"Canada"
]
]
]
]
},
{
"objectClassName": "entity",
"handle": "YYYY",
"roles": [
"technical"
],
"vcardArray": [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"fn",
{},
"text",
""
],
[
"org",
{},
"text",
"Example Inc."
],
[
"adr",
{},
"text",
[
"",
"Suite 1234",
"4321 Rue Somewhere",
"Quebec",
"QC",
"G1V 2M2",
"Canada"
]
]
]
]
}
],
"events": [
{
"eventAction": "registration", "eventDate": "1997-06-03T00:00:00Z"
},
{
"eventAction": "last changed", "eventDate": "2020-05-28T01:35:00Z"
},
{
"eventAction": "expiration", "eventDate": "2021-06-03T04:00:00Z"
}
],
"status": [
"server delete prohibited", "server update prohibited", "server transfer prohibited", "client transfer prohibited"
],
"redacted": [
{
"name": {
"description": "Registrant Name"
},
"postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='fn')][3]",
"pathLang": "jsonpath",
"method": "emptyValue",
"reason": {
"description": "Server policy"
}
}
]
}

View file

@ -0,0 +1,302 @@
{
"rdapConformance": [
"rdap_level_0",
"redacted"
],
"objectClassName": "domain",
"ldhName": "example-3.net",
"secureDNS": {
"delegationSigned": false
},
"notices": [
{
"title": "Terms of Use",
"description": [
"Service subject to Terms of Use."
],
"links": [
{
"rel": "self",
"href": "https://www.example.com/terms-of-use",
"type": "text/html",
"value": "https://www.example.com/terms-of-use"
}
]
}
],
"nameservers": [
{
"objectClassName": "nameserver",
"ldhName": "ns1.example.com"
},
{
"objectClassName": "nameserver",
"ldhName": "ns2.example.com"
}
],
"entities": [
{
"objectClassName": "entity",
"handle": "123",
"roles": [
"registrar"
],
"publicIds": [
{
"type": "IANA Registrar ID",
"identifier": "1"
}
],
"vcardArray": [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"fn",
{},
"text",
"Example Registrar Inc."
],
[
"adr",
{},
"text",
[
"",
"Suite 100",
"123 Example Dr.",
"Dulles",
"VA",
"20166-6503",
"US"
]
],
[
"email",
{},
"text",
"contact@organization.example"
],
[
"tel",
{
"type": "voice"
},
"uri",
"tel:+1.7035555555"
],
[
"tel",
{
"type": "fax"
},
"uri",
"tel:+1.7035555556"
]
]
],
"entities": [
{
"objectClassName": "entity",
"roles": [
"abuse"
],
"vcardArray": [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"fn",
{},
"text",
"Abuse Contact"
],
[
"email",
{},
"text",
"abuse@organization.example"
],
[
"tel",
{
"type": "voice"
},
"uri",
"tel:+1.7035555555"
]
]
]
}
]
},
{
"objectClassName": "entity",
"handle": "XXXX",
"roles": [
"registrant"
],
"vcardArray": [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"fn",
{},
"text",
"*REDACTED*"
],
[
"adr",
{},
"text",
[
"*REDACTED*",
"*REDACTED*",
"*REDACTED*",
"*REDACTED*",
"QC",
"*REDACTED*",
"Canada"
]
]
]
]
},
{
"objectClassName": "entity",
"handle": "YYYY",
"roles": [
"technical"
],
"vcardArray": [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"fn",
{},
"text",
"*REDACTED*"
],
[
"org",
{},
"text",
"Example Inc."
],
[
"adr",
{},
"text",
[
"",
"Suite 1234",
"4321 Rue Somewhere",
"Quebec",
"QC",
"G1V 2M2",
"Canada"
]
]
]
]
}
],
"events": [
{
"eventAction": "registration",
"eventDate": "1997-06-03T00:00:00Z"
},
{
"eventAction": "last changed",
"eventDate": "2020-05-28T01:35:00Z"
},
{
"eventAction": "expiration",
"eventDate": "2021-06-03T04:00:00Z"
}
],
"status": [
"server delete prohibited",
"server update prohibited",
"server transfer prohibited",
"client transfer prohibited"
],
"redacted": [
{
"name": {
"description": "Registrant Name"
},
"postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='fn')][3]",
"pathLang": "jsonpath",
"method": "partialValue",
"reason": {
"description": "Server policy"
}
},
{
"name": {
"description": "Registrant Street"
},
"postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='adr')][3][:3]",
"pathLang": "jsonpath",
"method": "partialValue",
"reason": {
"description": "Server policy"
}
},
{
"name": {
"description": "Registrant City"
},
"postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='adr')][3][3]",
"pathLang": "jsonpath",
"method": "partialValue",
"reason": {
"description": "Server policy"
}
},
{
"name": {
"description": "Registrant Postal Code"
},
"postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='adr')][3][5]",
"pathLang": "jsonpath",
"method": "partialValue",
"reason": {
"description": "Server policy"
}
},
{
"name": {
"description": "Technical Name"
},
"postPath": "$.entities[?(@.roles[0]=='technical')].vcardArray[1][?(@[0]=='fn')][3]",
"method": "partialValue",
"reason": {
"description": "Server policy"
}
}
]
}

View file

@ -0,0 +1,283 @@
{
"rdapConformance": [
"rdap_level_0",
"redacted"
],
"objectClassName": "domain",
"ldhName": "example-3.net",
"secureDNS": { "delegationSigned": false },
"notices": [
{
"title": "Terms of Use",
"description": [ "Service subject to Terms of Use." ],
"links": [
{
"rel": "self",
"href": "https://www.example.com/terms-of-use",
"type": "text/html",
"value": "https://www.example.com/terms-of-use"
}
]
}
],
"nameservers": [
{
"objectClassName": "nameserver", "ldhName": "ns1.example.com" },
{
"objectClassName": "nameserver", "ldhName": "ns2.example.com" }
],
"entities": [
{
"objectClassName": "entity",
"handle": "123",
"roles": [ "registrar" ],
"publicIds": [
{ "type": "IANA Registrar ID", "identifier": "1" }
],
"vcardArray": [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"fn",
{},
"text",
"Example Registrar Inc."
],
[
"adr",
{},
"text",
[
"",
"Suite 100",
"123 Example Dr.",
"Dulles",
"VA",
"20166-6503",
"US"
]
],
[
"email",
{},
"text",
"contact@organization.example"
],
[
"tel",
{
"type": "voice"
},
"uri",
"tel:+1.7035555555"
],
[
"tel",
{
"type": "fax"
},
"uri",
"tel:+1.7035555556"
]
]
],
"entities": [
{
"objectClassName": "entity",
"roles": [
"abuse"
],
"vcardArray": [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"fn",
{},
"text",
"Abuse Contact"
],
[
"email",
{},
"text",
"abuse@organization.example"
],
[
"tel",
{
"type": "voice"
},
"uri",
"tel:+1.7035555555"
]
]
]
}
]
},
{
"objectClassName": "entity",
"handle": "XXXX",
"roles": [
"registrant"
],
"vcardArray": [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"fn",
{},
"text",
""
],
[
"adr",
{},
"text",
[
"",
"",
"",
"",
"QC",
"",
"Canada"
]
]
]
]
},
{
"objectClassName": "entity",
"handle": "YYYY",
"roles": [
"technical"
],
"vcardArray": [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"fn",
{},
"text",
""
],
[
"org",
{},
"text",
"Example Inc."
],
[
"adr",
{},
"text",
[
"",
"Suite 1234",
"4321 Rue Somewhere",
"Quebec",
"QC",
"G1V 2M2",
"Canada"
]
]
]
]
}
],
"events": [
{
"eventAction": "registration", "eventDate": "1997-06-03T00:00:00Z"
},
{
"eventAction": "last changed", "eventDate": "2020-05-28T01:35:00Z"
},
{
"eventAction": "expiration", "eventDate": "2021-06-03T04:00:00Z"
}
],
"status": [
"server delete prohibited", "server update prohibited", "server transfer prohibited", "client transfer prohibited"
],
"redacted": [
{
"name": {
"description": "Registrant Name"
},
"postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='fn')][3]",
"pathLang": "jsonpath",
"method": "partialValue",
"reason": {
"description": "Server policy"
}
},
{
"name": {
"description": "Registrant Street"
},
"postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='adr')][3][:3]",
"pathLang": "jsonpath",
"method": "partialValue",
"reason": {
"description": "Server policy"
}
},
{
"name": {
"description": "Registrant City"
},
"postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='adr')][3][3]",
"pathLang": "jsonpath",
"method": "partialValue",
"reason": {
"description": "Server policy"
}
},
{
"name": {
"description": "Registrant Postal Code"
},
"postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='adr')][3][5]",
"pathLang": "jsonpath",
"method": "partialValue",
"reason": {
"description": "Server policy"
}
},
{
"name": {
"description": "Technical Name"
},
"postPath": "$.entities[?(@.roles[0]=='technical')].vcardArray[1][?(@[0]=='fn')][3]",
"method": "partialValue",
"reason": {
"description": "Server policy"
}
}
]
}

View file

@ -0,0 +1,212 @@
{
"rdapConformance": [
"rdap_level_0",
"redacted"
],
"objectClassName": "domain",
"handle": "PPP",
"ldhName": "0.2.192.in-addr.arpa",
"nameservers": [
{
"objectClassName": "nameserver",
"ldhName": "ns1.rir.example"
},
{
"objectClassName": "nameserver",
"ldhName": "ns2.rir.example"
}
],
"secureDNS": {
"delegationSigned": true,
"dsData": [
{
"keyTag": 25345,
"algorithm": 8,
"digestType": 2,
"digest": "2788970E18EA14...C890C85B8205B94"
}
]
},
"remarks": [
{
"description": [
"She sells sea shells down by the sea shore.",
"Originally written by Terry Sullivan."
]
}
],
"links": [
{
"value": "https://example.net/domain/0.2.192.in-addr.arpa",
"rel": "self",
"href": "https://example.net/domain/0.2.192.in-addr.arpa",
"type": "application/rdap+json"
}
],
"events": [
{
"eventAction": "registration",
"eventDate": "1990-12-31T23:59:59Z"
},
{
"eventAction": "last changed",
"eventDate": "1991-12-31T23:59:59Z",
"eventActor": "joe@example.com"
}
],
"entities": [
{
"objectClassName": "entity",
"handle": "XXXX",
"vcardArray": [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"fn",
{},
"text",
"Joe User"
],
[
"kind",
{},
"text",
"individual"
],
[
"lang",
{
"pref": "1"
},
"language-tag",
"fr"
],
[
"lang",
{
"pref": "2"
},
"language-tag",
"en"
],
[
"org",
{
"type": "work"
},
"text",
"Example"
],
[
"title",
{},
"text",
"Research Scientist"
],
[
"role",
{},
"text",
"Project Lead"
],
[
"adr",
{
"type": "work"
},
"text",
[
"",
"Suite 1234",
"4321 Rue Somewhere",
"Quebec",
"QC",
"G1V 2M2",
"Canada"
]
],
[
"tel",
{
"type": [
"work",
"voice"
],
"pref": "1"
},
"uri",
"tel:+1-555-555-1234;ext=102"
],
[
"email",
{
"type": "work"
},
"text",
"joe.user@example.com"
]
]
],
"roles": [
"registrant"
],
"remarks": [
{
"description": [
"She sells sea shells down by the sea shore.",
"Originally written by Terry Sullivan."
]
}
],
"links": [
{
"value": "https://example.net/entity/XXXX",
"rel": "self",
"href": "https://example.net/entity/XXXX",
"type": "application/rdap+json"
}
],
"events": [
{
"eventAction": "registration",
"eventDate": "1990-12-31T23:59:59Z"
},
{
"eventAction": "last changed",
"eventDate": "1991-12-31T23:59:59Z",
"eventActor": "joe@example.com"
}
]
}
],
"network": {
"objectClassName": "ip network",
"handle": "XXXX-RIR",
"startAddress": "192.0.2.0",
"endAddress": "192.0.2.255",
"ipVersion": "v4",
"name": "NET-RTR-1",
"type": "DIRECT ALLOCATION",
"country": "AU",
"parentHandle": "YYYY-RIR",
"status": [
"active"
]
},
"redacted": [
{
"name": {
"description": "Registrant keyTag"
},
"postPath": "$['secureDNS']['dsData'][0]['keyTag']",
"pathLang": "jsonpath",
"method": "partialValue"
}
]
}

View file

@ -0,0 +1,24 @@
Domain Name: home.moscow
Registry Domain ID: 20211019192813345912_c936bef81d9614db04ffc278b29daf5a_domain-FIR
Creation Date: 2021-10-19T19:28:12Z
Updated Date: 2023-11-20T18:36:22Z
Registry Expiry Date: 2024-10-19T19:28:12Z
Domain Status: client transfer prohibited
Registrar Whois Server: whois.flexireg.net
Registrar: Limited Liability Company "Registrar of domain names REG.RU"
Registrar Street:
Registrar City:
Registrar State/Province:
Registrar Postal Code:
Registrar Country:
Registrar IANA ID: 1606
Registrar Abuse Contact Email: abuse@reg.ru
Registrar Abuse Contact Phone: tel:+7
Registrar Abuse Contact Phone: tel:495
Registrar Abuse Contact Phone: tel:5801111
Name Server: ns1.reg.ru
Name Server: ns2.reg.ru
URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf/
>>> Last update of RDAP database: 2024-06-18T22:01:39+03:00 <<<

View file

@ -0,0 +1,345 @@
{
"rdapConformance": [
"rdap_level_0",
"icann_rdap_response_profile_0",
"icann_rdap_technical_implementation_guide_0"
],
"notices": [
{
"title": "Terms of Use",
"description": [
"Terms of Use page."
],
"links": [
{
"value": "https://flexireg.net/ru/whois-terms_of_use.en.html",
"rel": "related",
"href": "https://flexireg.net/ru/whois-terms_of_use.en.html",
"type": "text/html"
}
]
},
{
"title": "Status Codes",
"description": [
"For more information on domain status codes, please visit https://icann.org/epp."
],
"links": [
{
"value": "https://icann.org/epp",
"rel": "related",
"href": "https://icann.org/epp",
"type": "text/html"
}
]
},
{
"title": "RDDS Inaccuracy Complaint Form",
"description": [
"URL of the ICANN RDDS Inaccuracy Complaint Form:https://icann.org/wicf"
],
"links": [
{
"value": "https://www.icann.org/wicf",
"rel": "related",
"href": "https://www.icann.org/wicf",
"type": "text/html"
}
]
}
],
"objectClassName": "domain",
"handle": "20211019192813345912_c936bef81d9614db04ffc278b29daf5a_domain-FIR",
"links": [
{
"value": "https://flexireg.net/moscow/rdap/domain/home.moscow",
"rel": "self",
"href": "https://flexireg.net/moscow/rdap/domain/home.moscow",
"type": "application/rdap+json"
}
],
"events": [
{
"eventAction": "registration",
"eventDate": "2021-10-19T19:28:12Z"
},
{
"eventAction": "last changed",
"eventDate": "2023-11-20T18:36:22Z"
},
{
"eventAction": "expiration",
"eventDate": "2024-10-19T19:28:12Z"
},
{
"eventAction": "last update of RDAP database",
"eventDate": "2024-06-18T22:01:39+03:00"
}
],
"status": [
"client transfer prohibited"
],
"port43": "whois.flexireg.net",
"entities": [
{
"objectClassName": "entity",
"handle": "regru-msk-fir",
"links": [
{
"value": "https://flexireg.net/moscow/rdap/entity/regru-msk-fir",
"rel": "self",
"href": "https://flexireg.net/moscow/rdap/entity/regru-msk-fir",
"type": "application/rdap+json"
}
],
"entities": [
{
"objectClassName": "entity",
"vcardArray": [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"fn",
{},
"text",
"Abuse contact"
],
[
"tel",
{
"pref": "1",
"type": [
"work",
"voice"
]
},
"uri",
"tel:+7"
],
[
"tel",
{
"pref": "1",
"type": [
"work",
"voice"
]
},
"uri",
"tel:495"
],
[
"tel",
{
"pref": "1",
"type": [
"work",
"voice"
]
},
"uri",
"tel:5801111"
],
[
"email",
{
"type": "work"
},
"text",
"abuse@reg.ru"
]
]
],
"roles": [
"abuse"
]
}
],
"vcardArray": [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"fn",
{},
"text",
"Limited Liability Company \"Registrar of domain names REG.RU\""
],
[
"adr",
{
"type": "work"
},
"text",
[
"",
"",
"",
"",
"",
"",
""
]
],
[
"email",
{
"type": "work"
},
"text",
"info@reg.ru"
]
]
],
"roles": [
"registrar"
],
"publicIds": [
{
"type": "IANA Registrar ID",
"identifier": "1606"
}
]
},
{
"objectClassName": "entity",
"handle": "20231120183619454758_28c280fc7dcaa0dff7cd2b0e9b87e263_contact-FIR",
"remarks": [
{
"title": "REDACTED FOR PRIVACY",
"description": [
"Some of the data in this object has been removed"
]
},
{
"title": "EMAIL REDACTED FOR PRIVACY",
"description": [
"Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant of the queried domain name"
]
}
],
"links": [
{
"value": "https://flexireg.net/moscow/rdap/entity/20231120183619454758_28c280fc7dcaa0dff7cd2b0e9b87e263_contact-FIR",
"rel": "self",
"href": "https://flexireg.net/moscow/rdap/entity/20231120183619454758_28c280fc7dcaa0dff7cd2b0e9b87e263_contact-FIR",
"type": "application/rdap+json"
}
],
"roles": [
"registrant"
]
},
{
"objectClassName": "entity",
"handle": "20231120183619841977_163830e2aa5d03e5d5f7d2a1b936acec_contact-FIR",
"remarks": [
{
"title": "REDACTED FOR PRIVACY",
"description": [
"Some of the data in this object has been removed"
]
},
{
"title": "EMAIL REDACTED FOR PRIVACY",
"description": [
"Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant of the queried domain name"
]
}
],
"links": [
{
"value": "https://flexireg.net/moscow/rdap/entity/20231120183619841977_163830e2aa5d03e5d5f7d2a1b936acec_contact-FIR",
"rel": "self",
"href": "https://flexireg.net/moscow/rdap/entity/20231120183619841977_163830e2aa5d03e5d5f7d2a1b936acec_contact-FIR",
"type": "application/rdap+json"
}
],
"roles": [
"administrative"
]
},
{
"objectClassName": "entity",
"handle": "20231120183621248371_e50abb5e0ee9095975084ceb57105843_contact-FIR",
"remarks": [
{
"title": "REDACTED FOR PRIVACY",
"description": [
"Some of the data in this object has been removed"
]
},
{
"title": "EMAIL REDACTED FOR PRIVACY",
"description": [
"Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant of the queried domain name"
]
}
],
"links": [
{
"value": "https://flexireg.net/moscow/rdap/entity/20231120183621248371_e50abb5e0ee9095975084ceb57105843_contact-FIR",
"rel": "self",
"href": "https://flexireg.net/moscow/rdap/entity/20231120183621248371_e50abb5e0ee9095975084ceb57105843_contact-FIR",
"type": "application/rdap+json"
}
],
"roles": [
"technical"
]
}
],
"ldhName": "home.moscow",
"secureDNS": {
"delegationSigned": false
},
"nameservers": [
{
"objectClassName": "nameserver",
"links": [
{
"value": "https://flexireg.net/moscow/rdap/nameserver/ns1.reg.ru",
"rel": "self",
"href": "https://flexireg.net/moscow/rdap/nameserver/ns1.reg.ru",
"type": "application/rdap+json"
}
],
"events": [
{
"eventAction": "registration",
"eventDate": "2015-02-05T12:41:57Z"
}
],
"ldhName": "ns1.reg.ru"
},
{
"objectClassName": "nameserver",
"links": [
{
"value": "https://flexireg.net/moscow/rdap/nameserver/ns2.reg.ru",
"rel": "self",
"href": "https://flexireg.net/moscow/rdap/nameserver/ns2.reg.ru",
"type": "application/rdap+json"
}
],
"events": [
{
"eventAction": "registration",
"eventDate": "2015-02-05T12:41:57Z"
}
],
"ldhName": "ns2.reg.ru"
}
]
}

View file

@ -0,0 +1,52 @@
Domain Name: lemonde.fr
Registry Domain ID: DOM000000024309-FRNIC
Creation Date: 2005-08-02T14:16:36Z
Registry Expiry Date: 2025-06-09T22:08:09Z
Updated Date: 2024-06-12T22:09:39.881923Z
Domain Status: server update prohibited
Domain Status: server transfer prohibited
Domain Status: server delete prohibited
Domain Status: server recover prohibited
Registrar Whois Server: whois.nameshield.net
Registrar: NAMESHIELD
Registrar Street: 39 boulevard des Capucines
Registrar City: PARIS
Registrar State/Province:
Registrar Postal Code: 75002
Registrar Country: FR
Registrar IANA ID: 1251
Technical Name: NAMESHIELD
Technical Organization: TECHNICAL department
Technical Street: 79 rue Desjardins
Technical City: ANGERS
Technical State/Province:
Technical Postal Code: 49100
Technical Country: FR
Technical Email: technical@nameshield.net
Technical Phone: +33.241182828
Technical Fax: +33.241182829
Registrant Name: SOCIETE EDITRICE du monde
Registrant Organization: SOCIETE EDITRICE DU MONDE
Registrant Street: 67-69 avenue Pierre Mendes-France
Registrant City: PARIS
Registrant State/Province:
Registrant Postal Code: 75013
Registrant Country: FR
Registrant Email: domain_names@lemonde.fr
Registrant Phone: +33.157282224
Administrative Name: SOCIETE EDITRICE du monde
Administrative Organization: SOCIETE EDITRICE DU MONDE
Admin Street: 67-69 avenue Pierre Mendes-France
Admin City: PARIS
Admin State/Province:
Admin Postal Code: 75013
Admin Country: FR
Administrative Email: domain_names@lemonde.fr
Administrative Phone: +33.157282224
Name Server: ns-cloud-b4.googledomains.com
Name Server: ns-cloud-b2.googledomains.com
Name Server: ns-cloud-b3.googledomains.com
Name Server: ns-cloud-b1.googledomains.com
URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf/

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,41 @@
Domain Name: microsoft.click
Registry Domain ID: DO_a7aec7e93f5797ee898b23cefe340fe3-UR
Creation Date: 2014-11-12T19:15:55.283Z
Registry Expiry Date: 2024-11-12T19:15:55.283Z
Updated Date: 2023-10-17T10:47:21.733Z
Domain Status: client update prohibited
Domain Status: client transfer prohibited
Domain Status: client delete prohibited
Registrar: MarkMonitor Inc.
Registrar Street: 3540 East Longwing Lane, Suite 300
Registrar City: Meridian
Registrar State/Province: ID
Registrar Postal Code: 83646
Registrar Country: US
Registrar Abuse Contact Email: abusecomplaints@markmonitor.com
Registrar Abuse Contact Phone: tel:+1.2083895740
Registrar Abuse Contact Phone: tel:+1.2083895771
Registrant Organization: Microsoft Corporation
Registrant Street:
Registrant City:
Registrant State/Province: WA
Registrant Postal Code:
Registrant Country: US
Admin Street:
Admin City:
Admin State/Province:
Admin Postal Code:
Admin Country:
Technical Street:
Technical City:
Technical State/Province:
Technical Postal Code:
Technical Country:
Name Server: ns4-08.azure-dns.info
Name Server: ns2-08.azure-dns.net
Name Server: ns3-08.azure-dns.org
Name Server: ns1-08.azure-dns.com
URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf/
>>> Last update of RDAP database: 2024-06-18T14:25:27.257Z <<<

View file

@ -0,0 +1,455 @@
{
"rdapConformance": [
"icann_rdap_technical_implementation_guide_0",
"ur_domain_check_0"
],
"notices": [
{
"title": "Status Codes",
"description": [
"For more information on domain status codes, please visit https://icann.org/epp"
],
"links": [
{
"href": "https://icann.org/epp"
}
]
},
{
"title": "RDDS Inaccuracy Complaint Form",
"description": [
"URL of the ICANN RDDS Inaccuracy Complaint Form: https://www.icann.org/wicf/"
],
"links": [
{
"href": "https://www.icann.org/wicf/"
}
]
},
{
"title": "Terms of service",
"description": [
"The WHOIS information provided in this page has been redacted",
"in compliance with ICANN's Temporary Specification for gTLD",
"Registration Data.",
"",
"The data in this record is provided by Uniregistry for informational",
"purposes only, and it does not guarantee its accuracy. Uniregistry is",
"authoritative for whois information in top-level domains it operates",
"under contract with the Internet Corporation for Assigned Names and",
"Numbers. Whois information from other top-level domains is provided by",
"a third-party under license to Uniregistry.",
"",
"This service is intended only for query-based access. By using this",
"service, you agree that you will use any data presented only for lawful",
"purposes and that, under no circumstances will you use (a) data",
"acquired for the purpose of allowing, enabling, or otherwise supporting",
"the transmission by e-mail, telephone, facsimile or other",
"communications mechanism of mass unsolicited, commercial advertising",
"or solicitations to entities other than your existing customers; or",
"(b) this service to enable high volume, automated, electronic processes",
"that send queries or data to the systems of any Registrar or any",
"Registry except as reasonably necessary to register domain names or",
"modify existing domain name registrations.",
"",
"Uniregistry reserves the right to modify these terms at any time. By",
"submitting this query, you agree to abide by this policy. All rights",
"reserved.",
""
],
"links": [
{
"href": "https://whois.uniregistry.net/"
}
]
}
],
"objectClassName": "domain",
"handle": "DO_a7aec7e93f5797ee898b23cefe340fe3-UR",
"events": [
{
"eventAction": "registration",
"eventActor": "markmonitor",
"eventDate": "2014-11-12T19:15:55.283Z"
},
{
"eventAction": "expiration",
"eventDate": "2024-11-12T19:15:55.283Z"
},
{
"eventAction": "last changed",
"eventDate": "2023-10-17T10:47:21.733Z"
},
{
"eventAction": "last update of RDAP database",
"eventDate": "2024-06-18T14:25:27.257Z"
}
],
"status": [
"client update prohibited",
"client transfer prohibited",
"client delete prohibited"
],
"entities": [
{
"objectClassName": "entity",
"remarks": [
{
"title": "REDACTED FOR PRIVACY",
"description": [
"Some of the data in this object has been removed"
]
},
{
"title": "EMAIL REDACTED FOR PRIVACY",
"description": [
"Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name."
]
}
],
"events": [
{
"eventAction": "last update of RDAP database",
"eventDate": "2024-06-18T14:30:59.585Z"
}
],
"vcardArray": [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"org",
{},
"text",
"Microsoft Corporation"
],
[
"adr",
{},
"text",
[
"",
"",
"",
"",
"WA",
"",
"US"
]
]
]
],
"roles": [
"registrant"
]
},
{
"objectClassName": "entity",
"remarks": [
{
"title": "REDACTED FOR PRIVACY",
"description": [
"Some of the data in this object has been removed"
]
},
{
"title": "EMAIL REDACTED FOR PRIVACY",
"description": [
"Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name."
]
}
],
"events": [
{
"eventAction": "last update of RDAP database",
"eventDate": "2024-06-18T14:30:59.585Z"
}
],
"vcardArray": [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"adr",
{},
"text",
[
"",
"",
"",
"",
""
]
]
]
],
"roles": [
"administrative"
]
},
{
"objectClassName": "entity",
"remarks": [
{
"title": "REDACTED FOR PRIVACY",
"description": [
"Some of the data in this object has been removed"
]
},
{
"title": "EMAIL REDACTED FOR PRIVACY",
"description": [
"Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name."
]
}
],
"events": [
{
"eventAction": "last update of RDAP database",
"eventDate": "2024-06-18T14:30:59.585Z"
}
],
"vcardArray": [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"adr",
{},
"text",
[
"",
"",
"",
"",
""
]
]
]
],
"roles": [
"technical"
]
},
{
"objectClassName": "entity",
"handle": "292",
"events": [
{
"eventAction": "last update of RDAP database",
"eventDate": "2024-06-18T14:30:59.585Z"
}
],
"entities": [
{
"objectClassName": "entity",
"handle": "CO_03a1e5a41de9801039c48cce9ea7414f-UR",
"events": [
{
"eventAction": "last update of RDAP database",
"eventDate": "2024-06-18T14:30:59.585Z"
}
],
"vcardArray": [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"fn",
{},
"text",
"Markmonitor"
],
[
"adr",
{},
"text",
[
"",
"",
[
"2150 S Bonito Way, Suite 150",
"",
""
],
"Meridian",
"ID",
"83642",
"US"
]
],
[
"tel",
{
"type": "voice"
},
"uri",
"tel:+1.2083895740"
],
[
"tel",
{
"type": "fax"
},
"uri",
"tel:+1.2083895771"
],
[
"email",
{},
"text",
"abusecomplaints@markmonitor.com"
]
]
],
"roles": [
"abuse"
]
}
],
"vcardArray": [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"fn",
{},
"text",
"MarkMonitor Inc."
],
[
"adr",
{},
"text",
[
"",
"",
"3540 East Longwing Lane, Suite 300",
"Meridian",
"ID",
"83646",
"US"
]
],
[
"tel",
{
"type": "voice"
},
"uri",
"tel:+1.208389574"
],
[
"tel",
{
"type": "fax"
},
"uri",
"tel:+1.2083895771"
],
[
"email",
{},
"text",
"ccops@markmonitor.com"
]
]
],
"roles": [
"registrar"
]
}
],
"ldhName": "microsoft.click",
"unicodeName": "microsoft.click",
"secureDNS": {
"delegationSigned": false
},
"nameservers": [
{
"objectClassName": "nameserver",
"handle": "HO_6d2f0b70100a174318954d7e2af08b36-UR",
"events": [
{
"eventAction": "last update of RDAP database",
"eventDate": "2024-06-18T14:30:59.585Z"
}
],
"status": [
"associated"
],
"ldhName": "ns4-08.azure-dns.info",
"unicodeName": "ns4-08.azure-dns.info"
},
{
"objectClassName": "nameserver",
"handle": "HO_a639e2ec1bd022f8dcf45d00dfc1cf7d-UR",
"events": [
{
"eventAction": "last update of RDAP database",
"eventDate": "2024-06-18T14:30:59.585Z"
}
],
"status": [
"associated"
],
"ldhName": "ns2-08.azure-dns.net",
"unicodeName": "ns2-08.azure-dns.net"
},
{
"objectClassName": "nameserver",
"handle": "HO_bcdf18efa72577e5ac61514bce694770-UR",
"events": [
{
"eventAction": "last update of RDAP database",
"eventDate": "2024-06-18T14:30:59.585Z"
}
],
"status": [
"associated"
],
"ldhName": "ns3-08.azure-dns.org",
"unicodeName": "ns3-08.azure-dns.org"
},
{
"objectClassName": "nameserver",
"handle": "HO_8fa40a46321cfbe1b88b1590e5bd9cea-UR",
"events": [
{
"eventAction": "last update of RDAP database",
"eventDate": "2024-06-18T14:30:59.585Z"
}
],
"status": [
"associated"
],
"ldhName": "ns1-08.azure-dns.com",
"unicodeName": "ns1-08.azure-dns.com"
}
]
}