Adding upstream version 0.0.22.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
2f814b513a
commit
b06d3acde8
190 changed files with 61565 additions and 0 deletions
42
icann-rdap-client/Cargo.toml
Normal file
42
icann-rdap-client/Cargo.toml
Normal 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
105
icann-rdap-client/README.md
Normal 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 ICANN’s option, without any additional terms or conditions.
|
258
icann-rdap-client/src/gtld/domain.rs
Normal file
258
icann-rdap-client/src/gtld/domain.rs
Normal 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);
|
||||
}
|
||||
}
|
270
icann-rdap-client/src/gtld/entity.rs
Normal file
270
icann-rdap-client/src/gtld/entity.rs
Normal 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()
|
||||
}
|
83
icann-rdap-client/src/gtld/mod.rs
Normal file
83
icann-rdap-client/src/gtld/mod.rs
Normal 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 = ¶ms.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,
|
||||
}
|
22
icann-rdap-client/src/gtld/nameserver.rs
Normal file
22
icann-rdap-client/src/gtld/nameserver.rs
Normal 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
|
||||
}
|
||||
}
|
30
icann-rdap-client/src/gtld/network.rs
Normal file
30
icann-rdap-client/src/gtld/network.rs
Normal 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
|
||||
}
|
||||
}
|
10
icann-rdap-client/src/gtld/types.rs
Normal file
10
icann-rdap-client/src/gtld/types.rs
Normal 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()
|
||||
}
|
||||
}
|
9
icann-rdap-client/src/http/mod.rs
Normal file
9
icann-rdap-client/src/http/mod.rs
Normal 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;
|
226
icann-rdap-client/src/http/reqwest.rs
Normal file
226
icann-rdap-client/src/http/reqwest.rs
Normal 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
|
||||
}
|
297
icann-rdap-client/src/http/wrapped.rs
Normal file
297
icann-rdap-client/src/http/wrapped.rs
Normal 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 })
|
||||
}
|
623
icann-rdap-client/src/iana/bootstrap.rs
Normal file
623
icann-rdap-client/src/iana/bootstrap.rs
Normal 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/");
|
||||
}
|
||||
}
|
48
icann-rdap-client/src/iana/iana_request.rs
Normal file
48
icann-rdap-client/src/iana/iana_request.rs
Normal 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,
|
||||
})
|
||||
}
|
9
icann-rdap-client/src/iana/mod.rs
Normal file
9
icann-rdap-client/src/iana/mod.rs
Normal 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;
|
112
icann-rdap-client/src/lib.rs
Normal file
112
icann-rdap-client/src/lib.rs
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
111
icann-rdap-client/src/md/autnum.rs
Normal file
111
icann-rdap-client/src/md/autnum.rs
Normal 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()
|
||||
}
|
||||
}
|
267
icann-rdap-client/src/md/domain.rs
Normal file
267
icann-rdap-client/src/md/domain.rs
Normal 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,
|
||||
®istered_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()
|
||||
}
|
||||
}
|
359
icann-rdap-client/src/md/entity.rs
Normal file
359
icann-rdap-client/src/md/entity.rs
Normal 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,
|
||||
®istrant_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", ®istrant_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()
|
||||
}
|
||||
}
|
21
icann-rdap-client/src/md/error.rs
Normal file
21
icann-rdap-client/src/md/error.rs
Normal 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()
|
||||
}
|
||||
}
|
21
icann-rdap-client/src/md/help.rs
Normal file
21
icann-rdap-client/src/md/help.rs
Normal 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()
|
||||
}
|
||||
}
|
205
icann-rdap-client/src/md/mod.rs
Normal file
205
icann-rdap-client/src/md/mod.rs
Normal 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(¶ms.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,
|
||||
}
|
||||
}
|
||||
}
|
106
icann-rdap-client/src/md/nameserver.rs
Normal file
106
icann-rdap-client/src/md/nameserver.rs
Normal 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()
|
||||
}
|
||||
}
|
108
icann-rdap-client/src/md/network.rs
Normal file
108
icann-rdap-client/src/md/network.rs
Normal 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()
|
||||
}
|
||||
}
|
279
icann-rdap-client/src/md/redacted.rs
Normal file
279
icann-rdap-client/src/md/redacted.rs
Normal 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);
|
||||
}
|
||||
}
|
82
icann-rdap-client/src/md/search.rs
Normal file
82
icann-rdap-client/src/md/search.rs
Normal 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()
|
||||
}
|
||||
}
|
267
icann-rdap-client/src/md/string.rs
Normal file
267
icann-rdap-client/src/md/string.rs
Normal 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");
|
||||
}
|
||||
}
|
477
icann-rdap-client/src/md/table.rs
Normal file
477
icann-rdap-client/src/md/table.rs
Normal 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"
|
||||
)
|
||||
}
|
||||
}
|
468
icann-rdap-client/src/md/types.rs
Normal file
468
icann-rdap-client/src/md/types.rs
Normal 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(¬ice.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(¬ices.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
|
||||
}
|
15
icann-rdap-client/src/rdap/mod.rs
Normal file
15
icann-rdap-client/src/rdap/mod.rs
Normal 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;
|
645
icann-rdap-client/src/rdap/qtype.rs
Normal file
645
icann-rdap-client/src/rdap/qtype.rs
Normal 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")
|
||||
}
|
||||
}
|
636
icann-rdap-client/src/rdap/registered_redactions.rs
Normal file
636
icann-rdap-client/src/rdap/registered_redactions.rs
Normal 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()));
|
||||
}
|
||||
}
|
177
icann-rdap-client/src/rdap/request.rs
Normal file
177
icann-rdap-client/src/rdap/request.rs
Normal 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,
|
||||
}
|
60
icann-rdap-client/src/rdap/rr.rs
Normal file
60
icann-rdap-client/src/rdap/rr.rs
Normal 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>>;
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
240
icann-rdap-client/src/test_files/example-1_empty_value.json
Normal file
240
icann-rdap-client/src/test_files/example-1_empty_value.json
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
283
icann-rdap-client/src/test_files/example-2_partial_value.json
Normal file
283
icann-rdap-client/src/test_files/example-2_partial_value.json
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
24
icann-rdap-client/src/test_files/home.moscow-expected.gtld
Normal file
24
icann-rdap-client/src/test_files/home.moscow-expected.gtld
Normal 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 <<<
|
345
icann-rdap-client/src/test_files/home.moscow.json
Normal file
345
icann-rdap-client/src/test_files/home.moscow.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
52
icann-rdap-client/src/test_files/lemonde.fr-expected.gtld
Normal file
52
icann-rdap-client/src/test_files/lemonde.fr-expected.gtld
Normal 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/
|
1007
icann-rdap-client/src/test_files/lemonde.fr.json
Normal file
1007
icann-rdap-client/src/test_files/lemonde.fr.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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 <<<
|
455
icann-rdap-client/src/test_files/microsoft.click.json
Normal file
455
icann-rdap-client/src/test_files/microsoft.click.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue