1
0
Fork 0

Adding upstream version 0.0.22.

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

View file

@ -0,0 +1,192 @@
use std::any::TypeId;
use crate::response::autnum::Autnum;
use super::{
string::StringCheck, Check, CheckParams, Checks, GetChecks, GetSubChecks, RdapStructure,
};
impl GetChecks for Autnum {
fn get_checks(&self, params: CheckParams) -> super::Checks {
let sub_checks = if params.do_subchecks {
let mut sub_checks: Vec<Checks> = self
.common
.get_sub_checks(params.from_parent(TypeId::of::<Self>()));
sub_checks.append(
&mut self
.object_common
.get_sub_checks(params.from_parent(TypeId::of::<Self>())),
);
sub_checks
} else {
vec![]
};
let mut items = vec![];
if self.start_autnum.is_none() || self.end_autnum.is_none() {
items.push(Check::AutnumMissing.check_item())
}
if let Some(start_num) = &self.start_autnum.as_ref().and_then(|n| n.as_u32()) {
if let Some(end_num) = &self.end_autnum.as_ref().and_then(|n| n.as_u32()) {
if start_num > end_num {
items.push(Check::AutnumEndBeforeStart.check_item())
}
if is_autnum_reserved(*start_num) || is_autnum_reserved(*end_num) {
items.push(Check::AutnumReserved.check_item())
}
if is_autnum_documentation(*start_num) || is_autnum_documentation(*end_num) {
items.push(Check::AutnumDocumentation.check_item())
}
if is_autnum_private_use(*start_num) || is_autnum_private_use(*end_num) {
items.push(Check::AutnumPrivateUse.check_item())
}
}
}
if let Some(name) = &self.name {
if name.is_whitespace_or_empty() {
items.push(Check::NetworkOrAutnumNameIsEmpty.check_item())
}
}
if let Some(autnum_type) = &self.autnum_type {
if autnum_type.is_whitespace_or_empty() {
items.push(Check::NetworkOrAutnumTypeIsEmpty.check_item())
}
}
Checks {
rdap_struct: RdapStructure::Autnum,
items,
sub_checks,
}
}
}
/// Returns true if the autnum is reserved.
pub fn is_autnum_reserved(autnum: u32) -> bool {
autnum == 0 || autnum == 65535 || autnum == 4294967295 || (65552..=131071).contains(&autnum)
}
/// Returns true if the autnum is for documentation.
pub fn is_autnum_documentation(autnum: u32) -> bool {
(64496..=64511).contains(&autnum) || (65536..=65551).contains(&autnum)
}
/// Returns true if the autnum is private use.
pub fn is_autnum_private_use(autnum: u32) -> bool {
(64512..=65534).contains(&autnum) || (4200000000..=4294967294).contains(&autnum)
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use crate::prelude::ToResponse;
use crate::{
check::{Check, CheckParams, GetChecks},
response::autnum::Autnum,
};
use super::*;
#[test]
fn check_autnum_with_empty_name() {
// GIVEN
let mut autnum = Autnum::builder().autnum_range(700..700).build();
autnum.name = Some("".to_string());
let rdap = autnum.to_response();
// WHEN
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
// THEN
dbg!(&checks);
assert!(checks
.items
.iter()
.any(|c| c.check == Check::NetworkOrAutnumNameIsEmpty));
}
#[test]
fn check_autnum_with_empty_type() {
// GIVEN
let mut autnum = Autnum::builder().autnum_range(700..700).build();
autnum.autnum_type = Some("".to_string());
let rdap = autnum.to_response();
// WHEN
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
// THEN
dbg!(&checks);
assert!(checks
.items
.iter()
.any(|c| c.check == Check::NetworkOrAutnumTypeIsEmpty));
}
#[rstest]
#[case(0, true)]
#[case(65535, true)]
#[case(65552, true)]
#[case(131071, true)]
#[case(4294967295, true)]
#[case(1, false)]
#[case(65534, false)]
#[case(65551, false)]
#[case(131072, false)]
#[case(4294967294, false)]
fn check_autnum_is_reserved(#[case] autnum: u32, #[case] expected: bool) {
// GIVEN in parameters
// WHEN
let actual = is_autnum_reserved(autnum);
// THEN
assert_eq!(actual, expected);
}
#[rstest]
#[case(64496, true)]
#[case(64511, true)]
#[case(65536, true)]
#[case(65551, true)]
#[case(64495, false)]
#[case(64512, false)]
#[case(65535, false)]
#[case(65552, false)]
fn check_autnum_is_documentation(#[case] autnum: u32, #[case] expected: bool) {
// GIVEN in parameters
// WHEN
let actual = is_autnum_documentation(autnum);
// THEN
assert_eq!(actual, expected);
}
#[rstest]
#[case(64512, true)]
#[case(65534, true)]
#[case(4200000000, true)]
#[case(4294967294, true)]
#[case(65534, true)]
#[case(64511, false)]
#[case(65535, false)]
#[case(4199999999, false)]
#[case(4294967295, false)]
fn check_autnum_is_private_use(#[case] autnum: u32, #[case] expected: bool) {
// GIVEN in parameters
// WHEN
let actual = is_autnum_private_use(autnum);
// THEN
assert_eq!(actual, expected);
}
}

View file

@ -0,0 +1,648 @@
use std::any::TypeId;
use crate::response::domain::{Domain, SecureDns};
use super::{string::StringCheck, Check, CheckParams, Checks, GetChecks, GetSubChecks};
impl GetChecks for Domain {
fn get_checks(&self, params: CheckParams) -> super::Checks {
let sub_checks = if params.do_subchecks {
let mut sub_checks: Vec<Checks> = self
.common
.get_sub_checks(params.from_parent(TypeId::of::<Self>()));
sub_checks.append(
&mut self
.object_common
.get_sub_checks(params.from_parent(TypeId::of::<Self>())),
);
if let Some(public_ids) = &self.public_ids {
sub_checks.append(&mut public_ids.get_sub_checks(params));
}
if let Some(secure_dns) = &self.secure_dns {
sub_checks.append(&mut secure_dns.get_sub_checks(params));
}
sub_checks
} else {
vec![]
};
let mut items = vec![];
// check variants
if let Some(variants) = &self.variants {
let empty_count = variants
.iter()
.filter(|v| {
v.relations.is_none() && v.idn_table.is_none() && v.variant_names.is_none()
})
.count();
if empty_count != 0 {
items.push(Check::VariantEmptyDomain.check_item());
};
};
// check ldh
if let Some(ldh) = &self.ldh_name {
if !ldh.is_ldh_domain_name() {
items.push(Check::LdhNameInvalid.check_item());
}
let name = ldh.trim_end_matches('.');
if name.eq("example")
|| name.ends_with(".example")
|| name.eq("example.com")
|| name.ends_with(".example.com")
|| name.eq("example.net")
|| name.ends_with(".example.net")
|| name.eq("example.org")
|| name.ends_with(".example.org")
{
items.push(Check::LdhNameDocumentation.check_item())
}
// if there is also a unicodeName
if let Some(unicode_name) = &self.unicode_name {
let expected = idna::domain_to_ascii(unicode_name);
if let Ok(expected) = expected {
if !expected.eq_ignore_ascii_case(ldh) {
items.push(Check::LdhNameDoesNotMatchUnicode.check_item())
}
}
}
}
// check unicode_name
if let Some(unicode_name) = &self.unicode_name {
if !unicode_name.is_unicode_domain_name() {
items.push(Check::UnicodeNameInvalidDomain.check_item());
}
let expected = idna::domain_to_ascii(unicode_name);
if expected.is_err() {
items.push(Check::UnicodeNameInvalidUnicode.check_item());
}
}
Checks {
rdap_struct: super::RdapStructure::Domain,
items,
sub_checks,
}
}
}
impl GetSubChecks for SecureDns {
fn get_sub_checks(&self, _params: CheckParams) -> Vec<Checks> {
let mut sub_checks = Vec::new();
if let Some(delegation_signed) = &self.delegation_signed {
if delegation_signed.is_string() {
sub_checks.push(Checks {
rdap_struct: super::RdapStructure::SecureDns,
items: vec![Check::DelegationSignedIsString.check_item()],
sub_checks: vec![],
});
}
}
if let Some(zone_signed) = &self.zone_signed {
if zone_signed.is_string() {
sub_checks.push(Checks {
rdap_struct: super::RdapStructure::SecureDns,
items: vec![Check::ZoneSignedIsString.check_item()],
sub_checks: vec![],
});
}
}
if let Some(max_sig_life) = &self.max_sig_life {
if max_sig_life.is_string() {
sub_checks.push(Checks {
rdap_struct: super::RdapStructure::SecureDns,
items: vec![Check::MaxSigLifeIsString.check_item()],
sub_checks: vec![],
});
}
}
if let Some(key_data) = &self.key_data {
for key_datum in key_data {
if let Some(alg) = &key_datum.algorithm {
if alg.is_string() {
sub_checks.push(Checks {
rdap_struct: super::RdapStructure::SecureDns,
items: vec![Check::KeyDatumAlgorithmIsString.check_item()],
sub_checks: vec![],
});
}
if alg.as_u8().is_none() {
sub_checks.push(Checks {
rdap_struct: super::RdapStructure::SecureDns,
items: vec![Check::KeyDatumAlgorithmIsOutOfRange.check_item()],
sub_checks: vec![],
});
}
}
if let Some(flags) = &key_datum.flags {
if flags.is_string() {
sub_checks.push(Checks {
rdap_struct: super::RdapStructure::SecureDns,
items: vec![Check::KeyDatumFlagsIsString.check_item()],
sub_checks: vec![],
});
}
if flags.as_u16().is_none() {
sub_checks.push(Checks {
rdap_struct: super::RdapStructure::SecureDns,
items: vec![Check::KeyDatumFlagsIsOutOfRange.check_item()],
sub_checks: vec![],
});
}
}
if let Some(protocol) = &key_datum.protocol {
if protocol.is_string() {
sub_checks.push(Checks {
rdap_struct: super::RdapStructure::SecureDns,
items: vec![Check::KeyDatumProtocolIsString.check_item()],
sub_checks: vec![],
});
}
if protocol.as_u8().is_none() {
sub_checks.push(Checks {
rdap_struct: super::RdapStructure::SecureDns,
items: vec![Check::KeyDatumProtocolIsOutOfRange.check_item()],
sub_checks: vec![],
});
}
}
}
}
if let Some(ds_data) = &self.ds_data {
for ds_datum in ds_data {
if let Some(alg) = &ds_datum.algorithm {
if alg.is_string() {
sub_checks.push(Checks {
rdap_struct: super::RdapStructure::SecureDns,
items: vec![Check::DsDatumAlgorithmIsString.check_item()],
sub_checks: vec![],
});
}
if alg.as_u8().is_none() {
sub_checks.push(Checks {
rdap_struct: super::RdapStructure::SecureDns,
items: vec![Check::DsDatumAlgorithmIsOutOfRange.check_item()],
sub_checks: vec![],
});
}
}
if let Some(key_tag) = &ds_datum.key_tag {
if key_tag.is_string() {
sub_checks.push(Checks {
rdap_struct: super::RdapStructure::SecureDns,
items: vec![Check::DsDatumKeyTagIsString.check_item()],
sub_checks: vec![],
});
}
if key_tag.as_u32().is_none() {
sub_checks.push(Checks {
rdap_struct: super::RdapStructure::SecureDns,
items: vec![Check::DsDatumKeyTagIsOutOfRange.check_item()],
sub_checks: vec![],
});
}
}
if let Some(digest_type) = &ds_datum.digest_type {
if digest_type.is_string() {
sub_checks.push(Checks {
rdap_struct: super::RdapStructure::SecureDns,
items: vec![Check::DsDatumDigestTypeIsString.check_item()],
sub_checks: vec![],
});
}
if digest_type.as_u8().is_none() {
sub_checks.push(Checks {
rdap_struct: super::RdapStructure::SecureDns,
items: vec![Check::DsDatumDigestTypeIsOutOfRange.check_item()],
sub_checks: vec![],
});
}
}
}
}
sub_checks
}
}
#[cfg(test)]
mod tests {
use std::any::TypeId;
use {
crate::{
check::{is_checked, is_checked_item, GetSubChecks},
prelude::ToResponse,
response::domain::{Domain, SecureDns},
},
rstest::rstest,
};
use crate::check::{Check, CheckParams, GetChecks};
#[rstest]
#[case("")]
#[case(" ")]
#[case("_.")]
fn test_check_for_bad_ldh(#[case] ldh: &str) {
// GIVEN
let domain = Domain::builder().ldh_name(ldh).build();
let rdap = domain.to_response();
// WHEN
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
// THEN
dbg!(&checks);
assert!(is_checked_item(Check::LdhNameInvalid, &checks));
}
#[rstest]
#[case("")]
#[case(" ")]
fn test_check_for_bad_unicode(#[case] unicode: &str) {
// GIVEN
let domain = Domain::idn().unicode_name(unicode).build();
let rdap = domain.to_response();
// WHEN
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
// THEN
dbg!(&checks);
assert!(is_checked_item(Check::UnicodeNameInvalidDomain, &checks));
}
#[test]
fn test_check_for_ldh_unicode_mismatch() {
// GIVEN
let domain = Domain::idn()
.unicode_name("foo.com")
.ldh_name("xn--foo.com")
.build();
let rdap = domain.to_response();
// WHEN
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
// THEN
dbg!(&checks);
assert!(is_checked_item(Check::LdhNameDoesNotMatchUnicode, &checks));
}
#[test]
fn test_delegation_signed_as_string() {
// GIVEN
let secure_dns = serde_json::from_str::<SecureDns>(
r#"{
"delegationSigned": "true"
}"#,
)
.unwrap();
// WHEN
let checks = secure_dns.get_sub_checks(CheckParams {
do_subchecks: false,
root: &Domain::builder()
.ldh_name("example.com")
.build()
.to_response(),
parent_type: TypeId::of::<SecureDns>(),
allow_unreg_ext: false,
});
// THEN
assert_eq!(checks.len(), 1);
assert!(is_checked(Check::DelegationSignedIsString, &checks));
}
#[test]
fn test_delegation_signed_as_bool() {
// GIVEN
let secure_dns = serde_json::from_str::<SecureDns>(
r#"{
"delegationSigned": true
}"#,
)
.unwrap();
// WHEN
let checks = secure_dns.get_sub_checks(CheckParams {
do_subchecks: false,
root: &Domain::builder()
.ldh_name("example.com")
.build()
.to_response(),
parent_type: TypeId::of::<SecureDns>(),
allow_unreg_ext: false,
});
// THEN
assert!(checks.is_empty());
}
#[test]
fn test_zone_signed_as_string() {
// GIVEN
let secure_dns = serde_json::from_str::<SecureDns>(
r#"{
"zoneSigned": "false"
}"#,
)
.unwrap();
// WHEN
let checks = secure_dns.get_sub_checks(CheckParams {
do_subchecks: false,
root: &Domain::builder()
.ldh_name("example.com")
.build()
.to_response(),
parent_type: TypeId::of::<SecureDns>(),
allow_unreg_ext: false,
});
// THEN
assert_eq!(checks.len(), 1);
assert!(is_checked(Check::ZoneSignedIsString, &checks));
}
#[test]
fn test_zone_signed_as_bool() {
// GIVEN
let secure_dns = serde_json::from_str::<SecureDns>(
r#"{
"zoneSigned": true
}"#,
)
.unwrap();
// WHEN
let checks = secure_dns.get_sub_checks(CheckParams {
do_subchecks: false,
root: &Domain::builder()
.ldh_name("example.com")
.build()
.to_response(),
parent_type: TypeId::of::<SecureDns>(),
allow_unreg_ext: false,
});
// THEN
assert!(checks.is_empty());
}
#[test]
fn test_max_sig_life_as_string() {
// GIVEN
let secure_dns = serde_json::from_str::<SecureDns>(
r#"{
"maxSigLife": "123"
}"#,
)
.unwrap();
// WHEN
let checks = secure_dns.get_sub_checks(CheckParams {
do_subchecks: false,
root: &Domain::builder()
.ldh_name("example.com")
.build()
.to_response(),
parent_type: TypeId::of::<SecureDns>(),
allow_unreg_ext: false,
});
// THEN
assert_eq!(checks.len(), 1);
assert!(is_checked(Check::MaxSigLifeIsString, &checks));
}
#[test]
fn test_max_sig_life_as_number() {
// GIVEN
let secure_dns = serde_json::from_str::<SecureDns>(
r#"{
"maxSigLife": 123
}"#,
)
.unwrap();
// WHEN
let checks = secure_dns.get_sub_checks(CheckParams {
do_subchecks: false,
root: &Domain::builder()
.ldh_name("example.com")
.build()
.to_response(),
parent_type: TypeId::of::<SecureDns>(),
allow_unreg_ext: false,
});
// THEN
assert!(checks.is_empty());
}
#[test]
fn test_key_data_attributes_as_string() {
// GIVEN
let secure_dns = serde_json::from_str::<SecureDns>(
r#"{
"keyData": [
{
"algorithm": "13",
"flags": "13",
"protocol": "13"
}
]
}"#,
)
.unwrap();
// WHEN
let checks = secure_dns.get_sub_checks(CheckParams {
do_subchecks: false,
root: &Domain::builder()
.ldh_name("example.com")
.build()
.to_response(),
parent_type: TypeId::of::<SecureDns>(),
allow_unreg_ext: false,
});
// THEN
assert_eq!(checks.len(), 3);
assert!(is_checked(Check::KeyDatumAlgorithmIsString, &checks));
assert!(is_checked(Check::KeyDatumFlagsIsString, &checks));
assert!(is_checked(Check::KeyDatumProtocolIsString, &checks));
}
#[test]
fn test_key_data_attributes_as_number() {
// GIVEN
let secure_dns = serde_json::from_str::<SecureDns>(
r#"{
"keyData": [
{
"algorithm": 13,
"flags": 13,
"protocol": 13
}
]
}"#,
)
.unwrap();
// WHEN
let checks = secure_dns.get_sub_checks(CheckParams {
do_subchecks: false,
root: &Domain::builder()
.ldh_name("example.com")
.build()
.to_response(),
parent_type: TypeId::of::<SecureDns>(),
allow_unreg_ext: false,
});
// THEN
assert!(checks.is_empty());
}
#[test]
fn test_key_data_attributes_out_of_range() {
// GIVEN
let secure_dns = serde_json::from_str::<SecureDns>(
r#"{
"keyData": [
{
"algorithm": 1300,
"flags": 130000,
"protocol": 1300
}
]
}"#,
)
.unwrap();
// WHEN
let checks = secure_dns.get_sub_checks(CheckParams {
do_subchecks: false,
root: &Domain::builder()
.ldh_name("example.com")
.build()
.to_response(),
parent_type: TypeId::of::<SecureDns>(),
allow_unreg_ext: false,
});
// THEN
assert_eq!(checks.len(), 3);
assert!(is_checked(Check::KeyDatumAlgorithmIsOutOfRange, &checks));
assert!(is_checked(Check::KeyDatumFlagsIsOutOfRange, &checks));
assert!(is_checked(Check::KeyDatumProtocolIsOutOfRange, &checks));
}
#[test]
fn test_ds_data_attributes_as_string() {
// GIVEN
let secure_dns = serde_json::from_str::<SecureDns>(
r#"{
"dsData": [
{
"algorithm": "13",
"keyTag": "13",
"digestType": "13"
}
]
}"#,
)
.unwrap();
// WHEN
let checks = secure_dns.get_sub_checks(CheckParams {
do_subchecks: false,
root: &Domain::builder()
.ldh_name("example.com")
.build()
.to_response(),
parent_type: TypeId::of::<SecureDns>(),
allow_unreg_ext: false,
});
// THEN
assert_eq!(checks.len(), 3);
assert!(is_checked(Check::DsDatumAlgorithmIsString, &checks));
assert!(is_checked(Check::DsDatumKeyTagIsString, &checks));
assert!(is_checked(Check::DsDatumDigestTypeIsString, &checks));
}
#[test]
fn test_ds_data_attributes_as_number() {
// GIVEN
let secure_dns = serde_json::from_str::<SecureDns>(
r#"{
"dsData": [
{
"algorithm": 13,
"keyTag": 13,
"digestType": 13
}
]
}"#,
)
.unwrap();
// WHEN
let checks = secure_dns.get_sub_checks(CheckParams {
do_subchecks: false,
root: &Domain::builder()
.ldh_name("example.com")
.build()
.to_response(),
parent_type: TypeId::of::<SecureDns>(),
allow_unreg_ext: false,
});
// THEN
assert!(checks.is_empty());
}
#[test]
fn test_ds_data_attributes_out_of_range() {
// GIVEN
let secure_dns = serde_json::from_str::<SecureDns>(
r#"{
"dsData": [
{
"algorithm": 1300,
"keyTag": 13000000000,
"digestType": 1300
}
]
}"#,
)
.unwrap();
// WHEN
let checks = secure_dns.get_sub_checks(CheckParams {
do_subchecks: false,
root: &Domain::builder()
.ldh_name("example.com")
.build()
.to_response(),
parent_type: TypeId::of::<SecureDns>(),
allow_unreg_ext: false,
});
// THEN
assert_eq!(checks.len(), 3);
assert!(is_checked(Check::DsDatumAlgorithmIsOutOfRange, &checks));
assert!(is_checked(Check::DsDatumKeyTagIsOutOfRange, &checks));
assert!(is_checked(Check::DsDatumDigestTypeIsOutOfRange, &checks));
}
}

View file

@ -0,0 +1,71 @@
use std::{any::TypeId, str::FromStr};
use crate::{
contact::Contact,
response::entity::{Entity, EntityRole},
};
use super::{
string::{StringCheck, StringListCheck},
Check, CheckParams, Checks, GetChecks, GetSubChecks, RdapStructure,
};
impl GetChecks for Entity {
fn get_checks(&self, params: CheckParams) -> super::Checks {
let sub_checks = if params.do_subchecks {
let mut sub_checks: Vec<Checks> = self
.common
.get_sub_checks(params.from_parent(TypeId::of::<Self>()));
sub_checks.append(
&mut self
.object_common
.get_sub_checks(params.from_parent(TypeId::of::<Self>())),
);
if let Some(public_ids) = &self.public_ids {
sub_checks.append(&mut public_ids.get_sub_checks(params));
}
sub_checks
} else {
vec![]
};
let mut items = vec![];
if let Some(roles) = &self.roles {
if roles.is_string() {
items.push(Check::RoleIsString.check_item());
}
let roles = roles.vec();
if roles.is_empty_or_any_empty_or_whitespace() {
items.push(Check::RoleIsEmpty.check_item());
} else {
for role in roles {
let r = EntityRole::from_str(role);
if r.is_err() {
items.push(Check::UnknownRole.check_item());
}
}
}
}
if let Some(vcard) = &self.vcard_array {
if let Some(contact) = Contact::from_vcard(vcard) {
if let Some(full_name) = contact.full_name {
if full_name.is_whitespace_or_empty() {
items.push(Check::VcardFnIsEmpty.check_item())
}
} else {
items.push(Check::VcardHasNoFn.check_item())
}
} else {
items.push(Check::VcardArrayIsEmpty.check_item())
}
}
Checks {
rdap_struct: RdapStructure::Entity,
items,
sub_checks,
}
}
}

View file

@ -0,0 +1,23 @@
use std::any::TypeId;
use crate::response::error::Rfc9083Error;
use super::{CheckParams, Checks, GetChecks, GetSubChecks};
impl GetChecks for Rfc9083Error {
fn get_checks(&self, params: CheckParams) -> super::Checks {
let sub_checks = if params.do_subchecks {
let sub_checks: Vec<Checks> = self
.common
.get_sub_checks(params.from_parent(TypeId::of::<Self>()));
sub_checks
} else {
vec![]
};
Checks {
rdap_struct: super::RdapStructure::Error,
items: vec![],
sub_checks,
}
}
}

View file

@ -0,0 +1,23 @@
use std::any::TypeId;
use crate::response::help::Help;
use super::{CheckParams, Checks, GetChecks, GetSubChecks};
impl GetChecks for Help {
fn get_checks(&self, params: CheckParams) -> super::Checks {
let sub_checks = if params.do_subchecks {
let sub_checks: Vec<Checks> = self
.common
.get_sub_checks(params.from_parent(TypeId::of::<Self>()));
sub_checks
} else {
vec![]
};
Checks {
rdap_struct: super::RdapStructure::Help,
items: vec![],
sub_checks,
}
}
}

View file

@ -0,0 +1,346 @@
use crate::{httpdata::HttpData, media_types::RDAP_MEDIA_TYPE, response::types::ExtensionId};
use super::{Check, Checks, GetChecks};
impl GetChecks for HttpData {
fn get_checks(&self, params: crate::check::CheckParams) -> crate::check::Checks {
let mut items = vec![];
// RFC checks
if let Some(allow_origin) = &self.access_control_allow_origin {
if !allow_origin.eq("*") {
items.push(Check::CorsAllowOriginStarRecommended.check_item())
}
} else {
items.push(Check::CorsAllowOriginRecommended.check_item())
}
if self.access_control_allow_credentials.is_some() {
items.push(Check::CorsAllowCredentialsNotRecommended.check_item())
}
if let Some(content_type) = &self.content_type {
if !content_type.starts_with(RDAP_MEDIA_TYPE) {
items.push(Check::ContentTypeIsNotRdap.check_item());
}
} else {
items.push(Check::ContentTypeIsAbsent.check_item());
}
// checks for ICANN profile
if params
.root
.has_extension_id(ExtensionId::IcannRdapTechnicalImplementationGuide0)
|| params
.root
.has_extension_id(ExtensionId::IcannRdapTechnicalImplementationGuide1)
{
if let Some(scheme) = &self.scheme {
if !scheme.eq_ignore_ascii_case("HTTPS") {
items.push(Check::MustUseHttps.check_item());
}
} else {
items.push(Check::MustUseHttps.check_item());
}
if let Some(allow_origin) = &self.access_control_allow_origin {
if !allow_origin.eq("*") {
items.push(Check::AllowOriginNotStar.check_item())
}
} else {
items.push(Check::AllowOriginNotStar.check_item())
}
}
Checks {
rdap_struct: super::RdapStructure::HttpData,
items,
sub_checks: vec![],
}
}
}
#[cfg(test)]
mod tests {
use crate::{
check::{Check, CheckParams, GetChecks},
httpdata::HttpData,
media_types::{JSON_MEDIA_TYPE, RDAP_MEDIA_TYPE},
prelude::{Common, ObjectCommon, ToResponse},
response::{domain::Domain, types::ExtensionId},
};
#[test]
fn check_not_rdap_media() {
// GIVEN an rdap response
let domain = Domain {
common: Common::level0()
.extension(ExtensionId::IcannRdapTechnicalImplementationGuide0.to_extension())
.build(),
object_common: ObjectCommon::domain().build(),
ldh_name: Some("foo.example".to_string()),
unicode_name: None,
variants: None,
secure_dns: None,
nameservers: None,
public_ids: None,
network: None,
};
let rdap = domain.to_response();
// and GIVEN httpdata with content type that is not RDAP media type
let http_data = HttpData::example().content_type(JSON_MEDIA_TYPE).build();
// WHEN checks are run
let checks = http_data.get_checks(CheckParams::for_rdap(&rdap));
// THEN incorrect media type check is found
assert!(checks
.items
.iter()
.any(|c| c.check == Check::ContentTypeIsNotRdap));
}
#[test]
fn check_exactly_rdap_media() {
// GIVEN an rdap response
let domain = Domain {
common: Common::level0()
.extension(ExtensionId::IcannRdapTechnicalImplementationGuide0.to_extension())
.build(),
object_common: ObjectCommon::domain().build(),
ldh_name: Some("foo.example".to_string()),
unicode_name: None,
variants: None,
secure_dns: None,
nameservers: None,
public_ids: None,
network: None,
};
let rdap = domain.to_response();
// and GIVEN httpdata with content type that is not RDAP media type
let http_data = HttpData::example().content_type(RDAP_MEDIA_TYPE).build();
// WHEN checks are run
let checks = http_data.get_checks(CheckParams::for_rdap(&rdap));
// THEN incorrect media type check is not found
assert!(!checks
.items
.iter()
.any(|c| c.check == Check::ContentTypeIsNotRdap));
}
#[test]
fn check_rdap_media_with_charset_parameter() {
// GIVEN an rdap response
let domain = Domain {
common: Common::level0()
.extension(ExtensionId::IcannRdapTechnicalImplementationGuide0.to_extension())
.build(),
object_common: ObjectCommon::domain().build(),
ldh_name: Some("foo.example".to_string()),
unicode_name: None,
variants: None,
secure_dns: None,
nameservers: None,
public_ids: None,
network: None,
};
let rdap = domain.to_response();
// and GIVEN httpdata with content type that is not RDAP media type with charset parameter
let mt = format!("{RDAP_MEDIA_TYPE};charset=UTF-8");
let http_data = HttpData::example().content_type(mt).build();
// WHEN checks are run
let checks = http_data.get_checks(CheckParams::for_rdap(&rdap));
// THEN incorrect media type check is not found
assert!(!checks
.items
.iter()
.any(|c| c.check == Check::ContentTypeIsNotRdap));
}
#[test]
fn check_media_type_absent() {
// GIVEN an rdap response
let domain = Domain {
common: Common::level0()
.extension(ExtensionId::IcannRdapTechnicalImplementationGuide0.to_extension())
.build(),
object_common: ObjectCommon::domain().build(),
ldh_name: Some("foo.example".to_string()),
unicode_name: None,
variants: None,
secure_dns: None,
nameservers: None,
public_ids: None,
network: None,
};
let rdap = domain.to_response();
// and GIVEN httpdata no content type
let http_data = HttpData::example().build();
// WHEN checks are run
let checks = http_data.get_checks(CheckParams::for_rdap(&rdap));
// THEN no media type check is found
assert!(checks
.items
.iter()
.any(|c| c.check == Check::ContentTypeIsAbsent));
}
#[test]
fn check_cors_header_with_tig() {
// GIVEN a response with gtld tig
let domain = Domain {
common: Common::level0()
.extension(ExtensionId::IcannRdapTechnicalImplementationGuide0.to_extension())
.build(),
object_common: ObjectCommon::domain().build(),
ldh_name: Some("foo.example".to_string()),
unicode_name: None,
variants: None,
secure_dns: None,
nameservers: None,
public_ids: None,
network: None,
};
let rdap = domain.to_response();
// and GIVEN a cors header with *
let http_data = HttpData::example().access_control_allow_origin("*").build();
// WHEN running checks
let checks = http_data.get_checks(CheckParams::for_rdap(&rdap));
// THEN no check is given
assert!(!checks
.items
.iter()
.any(|c| c.check == Check::AllowOriginNotStar));
}
#[test]
fn check_cors_header_with_foo_and_tig() {
// GIVEN a response with gtld tig extension
let domain = Domain {
common: Common::level0()
.extension(ExtensionId::IcannRdapTechnicalImplementationGuide0.to_extension())
.build(),
object_common: ObjectCommon::domain().build(),
ldh_name: Some("foo.example".to_string()),
unicode_name: None,
variants: None,
secure_dns: None,
nameservers: None,
public_ids: None,
network: None,
};
let rdap = domain.to_response();
// and GIVEN response with cors header of "foo" (not "*")
let http_data = HttpData::example()
.access_control_allow_origin("foo")
.build();
// WHEN running checks
let checks = http_data.get_checks(CheckParams::for_rdap(&rdap));
// THEN the check is found
assert!(checks
.items
.iter()
.any(|c| c.check == Check::AllowOriginNotStar));
}
#[test]
fn check_no_cors_header_and_tig() {
// GIVEN domain response with gtld tig extension id
let domain = Domain {
common: Common::level0()
.extension(ExtensionId::IcannRdapTechnicalImplementationGuide0.to_extension())
.build(),
object_common: ObjectCommon::domain().build(),
ldh_name: Some("foo.example".to_string()),
unicode_name: None,
variants: None,
secure_dns: None,
nameservers: None,
public_ids: None,
network: None,
};
let rdap = domain.to_response();
// and GIVEN a response with no cors header
let http_data = HttpData::example().build();
// WHEN running checks
let checks = http_data.get_checks(CheckParams::for_rdap(&rdap));
// THEN check for missing cors is found
dbg!(&checks);
assert!(checks
.items
.iter()
.any(|c| c.check == Check::AllowOriginNotStar));
}
#[test]
fn given_response_is_over_https_and_tig() {
// GIVEN response with gtld tig extension
let domain = Domain {
common: Common::level0()
.extension(ExtensionId::IcannRdapTechnicalImplementationGuide0.to_extension())
.build(),
object_common: ObjectCommon::domain().build(),
ldh_name: Some("foo.example".to_string()),
unicode_name: None,
variants: None,
secure_dns: None,
nameservers: None,
public_ids: None,
network: None,
};
let rdap = domain.to_response();
// and GIVEN response is over https
let http_data = HttpData::now().scheme("https").host("example.com").build();
// WHEN running checks
let checks = http_data.get_checks(CheckParams::for_rdap(&rdap));
// THEN then check for must use https is not present
assert!(!checks.items.iter().any(|c| c.check == Check::MustUseHttps));
}
#[test]
fn response_over_htttp_and_tig() {
// GIVEN domain response with gtld tig extension
let domain = Domain {
common: Common::level0()
.extension(ExtensionId::IcannRdapTechnicalImplementationGuide0.to_extension())
.build(),
object_common: ObjectCommon::domain().build(),
ldh_name: Some("foo.example".to_string()),
unicode_name: None,
variants: None,
secure_dns: None,
nameservers: None,
public_ids: None,
network: None,
};
let rdap = domain.to_response();
// and GIVEN response is with http (not https)
let http_data = HttpData::now().scheme("http").host("example.com").build();
// WHEN running checks
let checks = http_data.get_checks(CheckParams::for_rdap(&rdap));
// THEN check for must use https is found
assert!(checks.items.iter().any(|c| c.check == Check::MustUseHttps));
}
}

View file

@ -0,0 +1,754 @@
//! Conformance checks of RDAP structures.
use std::{any::TypeId, sync::LazyLock};
use {
crate::response::RdapResponse,
serde::{Deserialize, Serialize},
strum::{EnumMessage, IntoEnumIterator},
strum_macros::{Display, EnumIter, EnumMessage, EnumString, FromRepr},
};
#[doc(inline)]
pub use string::*;
mod autnum;
mod domain;
mod entity;
mod error;
mod help;
mod httpdata;
mod nameserver;
mod network;
mod search;
mod string;
mod types;
/// The max length of the check class string representations.
pub static CHECK_CLASS_LEN: LazyLock<usize> = LazyLock::new(|| {
CheckClass::iter()
.max_by_key(|x| x.to_string().len())
.map_or(8, |x| x.to_string().len())
});
/// Describes the classes of checks.
#[derive(
EnumIter,
EnumString,
Debug,
Display,
PartialEq,
Eq,
PartialOrd,
Ord,
Serialize,
Deserialize,
Clone,
Copy,
)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum CheckClass {
/// Informational
///
/// This class represents informational items.
#[strum(serialize = "Info")]
Informational,
/// Specification Note
///
/// This class represents notes about the RDAP response with respect to
/// the various RDAP and RDAP related specifications.
#[strum(serialize = "SpecNote")]
SpecificationNote,
/// STD 95 Warnings
///
/// This class represents warnings that may cause some clients to be unable
/// to conduct some operations.
#[strum(serialize = "StdWarn")]
StdWarning,
/// STD 95 Errors
///
/// This class represetns errors in the RDAP with respect to STD 95.
#[strum(serialize = "StdErr")]
StdError,
/// Cidr0 Errors
///
/// This class represents errors with respect to CIDR0.
#[strum(serialize = "Cidr0Err")]
Cidr0Error,
/// ICANN Profile Errors
///
/// This class represents errors with respect to the gTLD RDAP profile.
#[strum(serialize = "IcannErr")]
IcannError,
}
/// Represents the name of an RDAP structure for which a check appears.
///
/// An RDAP data structure is not the same as a Rust struct in that RDAP
/// data structures may consist of arrays and sometimes structured data
/// within a string.
#[derive(
Debug, Serialize, Deserialize, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Display, EnumString,
)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum RdapStructure {
Autnum,
Cidr0,
Domain,
DomainSearchResults,
Entity,
EntitySearchResults,
Events,
Error,
Help,
Handle,
HttpData,
IpNetwork,
Link,
Links,
Nameserver,
NameserverSearchResults,
NoticeOrRemark,
Notices,
PublidIds,
Port43,
RdapConformance,
Redacted,
Remarks,
SecureDns,
Status,
}
/// Contains many [CheckItem] structures and sub checks.
///
/// Checks are found on object classes and structures defined in [RdapStructure].
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, PartialOrd, Eq, Ord)]
pub struct Checks {
pub rdap_struct: RdapStructure,
pub items: Vec<CheckItem>,
pub sub_checks: Vec<Checks>,
}
impl Checks {
pub fn sub(&self, rdap_struct: RdapStructure) -> Option<&Self> {
self.sub_checks
.iter()
.find(|check| check.rdap_struct == rdap_struct)
}
}
/// A specific check item.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct CheckItem {
pub check_class: CheckClass,
pub check: Check,
}
impl std::fmt::Display for CheckItem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!(
"{}:({:0>4}) {}",
self.check_class,
self.check as usize,
self.check
.get_message()
.unwrap_or("[Check has no description]"),
))
}
}
/// Trait for an item that can get checks.
pub trait GetChecks {
fn get_checks(&self, params: CheckParams) -> Checks;
}
/// Parameters for finding checks.
#[derive(Clone, Copy)]
pub struct CheckParams<'a> {
pub do_subchecks: bool,
pub root: &'a RdapResponse,
pub parent_type: TypeId,
pub allow_unreg_ext: bool,
}
impl CheckParams<'_> {
pub fn from_parent(&self, parent_type: TypeId) -> Self {
Self {
do_subchecks: self.do_subchecks,
root: self.root,
parent_type,
allow_unreg_ext: self.allow_unreg_ext,
}
}
pub fn for_rdap(rdap: &RdapResponse) -> CheckParams<'_> {
CheckParams {
do_subchecks: true,
root: rdap,
parent_type: rdap.get_type(),
allow_unreg_ext: false,
}
}
}
impl GetChecks for RdapResponse {
fn get_checks(&self, params: CheckParams) -> Checks {
match &self {
Self::Entity(e) => e.get_checks(params),
Self::Domain(d) => d.get_checks(params),
Self::Nameserver(n) => n.get_checks(params),
Self::Autnum(a) => a.get_checks(params),
Self::Network(n) => n.get_checks(params),
Self::DomainSearchResults(r) => r.get_checks(params),
Self::EntitySearchResults(r) => r.get_checks(params),
Self::NameserverSearchResults(r) => r.get_checks(params),
Self::ErrorResponse(e) => e.get_checks(params),
Self::Help(h) => h.get_checks(params),
}
}
}
/// Trait to get checks for structures below that of the object class.
pub trait GetSubChecks {
fn get_sub_checks(&self, params: CheckParams) -> Vec<Checks>;
}
/// Traverse the checks, and return true if one is found.
pub fn traverse_checks<F>(
checks: &Checks,
classes: &[CheckClass],
parent_tree: Option<String>,
f: &mut F,
) -> bool
where
F: FnMut(&str, &CheckItem),
{
let mut found = false;
let struct_tree = format!(
"{}/{}",
parent_tree.unwrap_or_else(|| "[ROOT]".to_string()),
checks.rdap_struct
);
for item in &checks.items {
if classes.contains(&item.check_class) {
f(&struct_tree, item);
found = true;
}
}
for sub_checks in &checks.sub_checks {
if traverse_checks(sub_checks, classes, Some(struct_tree.clone()), f) {
found = true
}
}
found
}
/// Returns true if the check is in a check list
pub fn is_checked(check: Check, checks: &[Checks]) -> bool {
checks.iter().any(|c| is_checked_item(check, c))
}
/// Returns true if the check is in a list of check items.
pub fn is_checked_item(check: Check, checks: &Checks) -> bool {
checks.items.iter().any(|c| c.check == check)
}
/// The variant check types.
#[derive(
Debug,
EnumMessage,
EnumString,
Display,
Serialize,
Deserialize,
PartialEq,
PartialOrd,
Eq,
Ord,
Clone,
Copy,
FromRepr,
)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum Check {
// RDAP Conformance 100 - 199
#[strum(message = "RFC 9083 requires 'rdapConformance' on the root object.")]
RdapConformanceMissing = 100,
#[strum(message = "'rdapConformance' can only appear at the top of response.")]
RdapConformanceInvalidParent = 101,
#[strum(message = "declared extension may not be registered.")]
UnknownExtention = 102,
// Link 200 - 299
#[strum(message = "'value' property not found in Link structure as required by RFC 9083")]
LinkMissingValueProperty = 200,
#[strum(message = "'rel' property not found in Link structure as required by RFC 9083")]
LinkMissingRelProperty = 201,
#[strum(message = "ambiguous follow because related link has no 'type' property")]
LinkRelatedHasNoType = 202,
#[strum(message = "ambiguous follow because related link does not have RDAP media type")]
LinkRelatedIsNotRdap = 203,
#[strum(message = "self link has no 'type' property")]
LinkSelfHasNoType = 204,
#[strum(message = "self link does not have RDAP media type")]
LinkSelfIsNotRdap = 205,
#[strum(message = "RFC 9083 recommends self links for all object classes")]
LinkObjectClassHasNoSelf = 206,
#[strum(message = "'href' property not found in Link structure as required by RFC 9083")]
LinkMissingHrefProperty = 207,
// Domain Variant 300 - 399
#[strum(message = "empty domain variant is ambiguous")]
VariantEmptyDomain = 300,
// Event 400 - 499
#[strum(message = "event date is absent")]
EventDateIsAbsent = 400,
#[strum(message = "event date is not RFC 3339 compliant")]
EventDateIsNotRfc3339 = 401,
#[strum(message = "event action is absent")]
EventActionIsAbsent = 402,
// Notice Or Remark 500 - 599
#[strum(message = "RFC 9083 requires a description in a notice or remark")]
NoticeOrRemarkDescriptionIsAbsent = 500,
#[strum(message = "RFC 9083 requires a description to be an array of strings")]
NoticeOrRemarkDescriptionIsString = 501,
// Handle 600 - 699
#[strum(message = "handle appears to be empty or only whitespace")]
HandleIsEmpty = 600,
// Status 700 - 799
#[strum(message = "status appears to be empty or only whitespace")]
StatusIsEmpty = 700,
// Role 800 - 899
#[strum(message = "role appears to be empty or only whitespace")]
RoleIsEmpty = 800,
#[strum(message = "entity role may not be registered")]
UnknownRole = 801,
#[strum(message = "role is a string, not array of strings")]
RoleIsString = 802,
// LDH Name 900 - 999
#[strum(message = "ldhName does not appear to be an LDH name")]
LdhNameInvalid = 900,
#[strum(message = "Documentation domain name. See RFC 6761")]
LdhNameDocumentation = 901,
#[strum(message = "Unicode name does not match LDH")]
LdhNameDoesNotMatchUnicode = 902,
// Unicode Nmae 1000 - 1099
#[strum(message = "unicodeName does not appear to be a domain name")]
UnicodeNameInvalidDomain = 1000,
#[strum(message = "unicodeName does not appear to be valid Unicode")]
UnicodeNameInvalidUnicode = 1001,
// Network Or Autnum Name 1100 - 1199
#[strum(message = "name appears to be empty or only whitespace")]
NetworkOrAutnumNameIsEmpty = 1100,
// Network or Autnum Type 1200 - 1299
#[strum(message = "type appears to be empty or only whitespace")]
NetworkOrAutnumTypeIsEmpty = 1200,
// IP Address 1300 - 1399
#[strum(message = "start or end IP address is missing")]
IpAddressMissing = 1300,
#[strum(message = "IP address is malformed")]
IpAddressMalformed = 1301,
#[strum(message = "end IP address comes before start IP address")]
IpAddressEndBeforeStart = 1302,
#[strum(message = "IP version does not match IP address")]
IpAddressVersionMismatch = 1303,
#[strum(message = "IP version is malformed")]
IpAddressMalformedVersion = 1304,
#[strum(message = "IP address list is empty")]
IpAddressListIsEmpty = 1305,
#[strum(message = "\"This network.\" See RFC 791")]
IpAddressThisNetwork = 1306,
#[strum(message = "Private use. See RFC 1918")]
IpAddressPrivateUse = 1307,
#[strum(message = "Shared NAT network. See RFC 6598")]
IpAddressSharedNat = 1308,
#[strum(message = "Loopback network. See RFC 1122")]
IpAddressLoopback = 1309,
#[strum(message = "Link local network. See RFC 3927")]
IpAddressLinkLocal = 1310,
#[strum(message = "Unique local network. See RFC 8190")]
IpAddressUniqueLocal = 1311,
#[strum(message = "Documentation network. See RFC 5737")]
IpAddressDocumentationNet = 1312,
#[strum(message = "Reserved network. See RFC 1112")]
IpAddressReservedNet = 1313,
#[strum(message = "IP address array is a string.")]
IpAddressArrayIsString = 1314,
// Autnum 1400 - 1499
#[strum(message = "start or end autnum is missing")]
AutnumMissing = 1400,
#[strum(message = "end AS number comes before start AS number")]
AutnumEndBeforeStart = 1401,
#[strum(message = "Private use. See RFC 6996")]
AutnumPrivateUse = 1402,
#[strum(message = "Documentation AS number. See RFC 5398")]
AutnumDocumentation = 1403,
#[strum(message = "Reserved AS number. See RFC 6996")]
AutnumReserved = 1404,
// Vcard 1500 - 1599
#[strum(message = "vCard array does not contain a vCard")]
VcardArrayIsEmpty = 1500,
#[strum(message = "vCard has no fn property")]
VcardHasNoFn = 1501,
#[strum(message = "vCard fn property is empty")]
VcardFnIsEmpty = 1502,
// Port 43 1600 - 1699
#[strum(message = "port43 appears to be empty or only whitespace")]
Port43IsEmpty = 1600,
// Public Id 1700 - 1799
#[strum(message = "publicId type is absent")]
PublicIdTypeIsAbsent = 1700,
#[strum(message = "publicId identifier is absent")]
PublicIdIdentifierIsAbsent = 1701,
// HTTP 1800 - 1899
#[strum(message = "Use of access-control-allow-origin is recommended.")]
CorsAllowOriginRecommended = 1800,
#[strum(message = "Use of access-control-allow-origin with asterisk is recommended.")]
CorsAllowOriginStarRecommended = 1801,
#[strum(message = "Use of access-control-allow-credentials is not recommended.")]
CorsAllowCredentialsNotRecommended = 1802,
#[strum(message = "No content-type header received.")]
ContentTypeIsAbsent = 1803,
#[strum(message = "Content-type is not application/rdap+json.")]
ContentTypeIsNotRdap = 1804,
// Cidr0 1900 - 1999
#[strum(message = "Cidr0 v4 prefix is absent")]
Cidr0V4PrefixIsAbsent = 1900,
#[strum(message = "Cidr0 v4 length is absent")]
Cidr0V4LengthIsAbsent = 1901,
#[strum(message = "Cidr0 v6 prefix is absent")]
Cidr0V6PrefixIsAbsent = 1902,
#[strum(message = "Cidr0 v6 length is absent")]
Cidr0V6LengthIsAbsent = 1903,
// ICANN Profile 2000 - 2099
#[strum(message = "RDAP Service Must use HTTPS.")]
MustUseHttps = 2000,
#[strum(message = "access-control-allow-origin is not asterisk")]
AllowOriginNotStar = 2001,
// Explicit Testing Errors 2100 - 2199
#[strum(message = "CNAME without A records.")]
CnameWithoutARecords = 2100,
#[strum(message = "CNAME without AAAA records.")]
CnameWithoutAAAARecords = 2101,
#[strum(message = "No A records.")]
NoARecords = 2102,
#[strum(message = "No AAAA records.")]
NoAAAARecords = 2103,
#[strum(message = "Expected extension not found.")]
ExpectedExtensionNotFound = 2104,
#[strum(message = "IPv6 Support Required.")]
Ipv6SupportRequiredByIcann = 2105,
// Secure DNS 2200 - 2299
#[strum(message = "delegationSigned is a string not a bool.")]
DelegationSignedIsString = 2200,
#[strum(message = "zoneSigned is a string not a bool.")]
ZoneSignedIsString = 2201,
#[strum(message = "maxSigLife is a string not a number.")]
MaxSigLifeIsString = 2202,
// key data
#[strum(message = "keyData algorithm is a string not a number.")]
KeyDatumAlgorithmIsString = 2203,
#[strum(message = "keyData algorithm is out of range.")]
KeyDatumAlgorithmIsOutOfRange = 2204,
#[strum(message = "keyData flags is a string not a number.")]
KeyDatumFlagsIsString = 2205,
#[strum(message = "keyData flags is out of range.")]
KeyDatumFlagsIsOutOfRange = 2206,
#[strum(message = "keyData protocol is a string not a number.")]
KeyDatumProtocolIsString = 2207,
#[strum(message = "keyData protocol is out of range.")]
KeyDatumProtocolIsOutOfRange = 2208,
// ds data
#[strum(message = "dsData algorithm is a string not a number.")]
DsDatumAlgorithmIsString = 2213,
#[strum(message = "dsData algorithm is out of range.")]
DsDatumAlgorithmIsOutOfRange = 2214,
#[strum(message = "dsData keyTag is a string not a number.")]
DsDatumKeyTagIsString = 2215,
#[strum(message = "dsData keyTag is out of range.")]
DsDatumKeyTagIsOutOfRange = 2216,
#[strum(message = "dsData digestType is a string not a number.")]
DsDatumDigestTypeIsString = 2217,
#[strum(message = "dsData digestType is out of range.")]
DsDatumDigestTypeIsOutOfRange = 2218,
}
impl Check {
pub fn check_item(self) -> CheckItem {
let check_class = match self {
Self::RdapConformanceMissing | Self::RdapConformanceInvalidParent => {
CheckClass::StdError
}
Self::UnknownExtention => CheckClass::StdWarning,
Self::LinkMissingValueProperty | Self::LinkMissingRelProperty => CheckClass::StdError,
Self::LinkRelatedHasNoType
| Self::LinkRelatedIsNotRdap
| Self::LinkSelfHasNoType
| Self::LinkSelfIsNotRdap => CheckClass::StdWarning,
Self::LinkObjectClassHasNoSelf => CheckClass::SpecificationNote,
Self::LinkMissingHrefProperty => CheckClass::StdError,
Self::VariantEmptyDomain => CheckClass::StdWarning,
Self::EventDateIsAbsent
| Self::EventDateIsNotRfc3339
| Self::EventActionIsAbsent
| Self::NoticeOrRemarkDescriptionIsAbsent
| Self::NoticeOrRemarkDescriptionIsString => CheckClass::StdError,
Self::HandleIsEmpty => CheckClass::StdWarning,
Self::StatusIsEmpty | Self::RoleIsEmpty => CheckClass::StdError,
Self::UnknownRole => CheckClass::StdWarning,
Self::RoleIsString | Self::LdhNameInvalid => CheckClass::StdError,
Self::LdhNameDocumentation => CheckClass::Informational,
Self::LdhNameDoesNotMatchUnicode => CheckClass::StdWarning,
Self::UnicodeNameInvalidDomain | Self::UnicodeNameInvalidUnicode => {
CheckClass::StdError
}
Self::NetworkOrAutnumNameIsEmpty
| Self::NetworkOrAutnumTypeIsEmpty
| Self::IpAddressMissing => CheckClass::StdWarning,
Self::IpAddressMalformed => CheckClass::StdError,
Self::IpAddressEndBeforeStart | Self::IpAddressVersionMismatch => {
CheckClass::StdWarning
}
Self::IpAddressMalformedVersion | Self::IpAddressListIsEmpty => CheckClass::StdError,
Self::IpAddressThisNetwork
| Self::IpAddressPrivateUse
| Self::IpAddressSharedNat
| Self::IpAddressLoopback
| Self::IpAddressLinkLocal
| Self::IpAddressUniqueLocal
| Self::IpAddressDocumentationNet
| Self::IpAddressReservedNet => CheckClass::Informational,
Self::IpAddressArrayIsString => CheckClass::StdError,
Self::AutnumMissing | Self::AutnumEndBeforeStart => CheckClass::StdWarning,
Self::AutnumPrivateUse | Self::AutnumDocumentation | Self::AutnumReserved => {
CheckClass::Informational
}
Self::VcardArrayIsEmpty | Self::VcardHasNoFn => CheckClass::StdError,
Self::VcardFnIsEmpty => CheckClass::SpecificationNote,
Self::Port43IsEmpty | Self::PublicIdTypeIsAbsent | Self::PublicIdIdentifierIsAbsent => {
CheckClass::StdError
}
Self::CorsAllowOriginRecommended
| Self::CorsAllowOriginStarRecommended
| Self::CorsAllowCredentialsNotRecommended => CheckClass::StdWarning,
Self::ContentTypeIsAbsent | Self::ContentTypeIsNotRdap => CheckClass::StdError,
Self::Cidr0V4PrefixIsAbsent
| Self::Cidr0V4LengthIsAbsent
| Self::Cidr0V6PrefixIsAbsent
| Self::Cidr0V6LengthIsAbsent => CheckClass::Cidr0Error,
Self::MustUseHttps | Self::AllowOriginNotStar => CheckClass::IcannError,
Self::CnameWithoutARecords | Self::CnameWithoutAAAARecords => CheckClass::StdError,
Self::NoARecords | Self::NoAAAARecords => CheckClass::SpecificationNote,
Self::ExpectedExtensionNotFound => CheckClass::StdError,
Self::Ipv6SupportRequiredByIcann => CheckClass::IcannError,
Self::DelegationSignedIsString
| Self::ZoneSignedIsString
| Self::MaxSigLifeIsString
| Self::KeyDatumAlgorithmIsString
| Self::KeyDatumAlgorithmIsOutOfRange
| Self::KeyDatumFlagsIsString
| Self::KeyDatumFlagsIsOutOfRange
| Self::KeyDatumProtocolIsString
| Self::KeyDatumProtocolIsOutOfRange
| Self::DsDatumAlgorithmIsString
| Self::DsDatumAlgorithmIsOutOfRange
| Self::DsDatumKeyTagIsString
| Self::DsDatumKeyTagIsOutOfRange
| Self::DsDatumDigestTypeIsString
| Self::DsDatumDigestTypeIsOutOfRange => CheckClass::StdError,
};
CheckItem {
check_class,
check: self,
}
}
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use crate::check::RdapStructure;
use super::{traverse_checks, Check, CheckClass, CheckItem, Checks};
#[test]
fn GIVEN_info_checks_WHEN_traversed_for_info_THEN_found() {
// GIVEN
let checks = Checks {
rdap_struct: RdapStructure::Entity,
items: vec![CheckItem {
check_class: CheckClass::Informational,
check: Check::VariantEmptyDomain,
}],
sub_checks: vec![],
};
// WHEN
let found = traverse_checks(
&checks,
&[CheckClass::Informational],
None,
&mut |struct_tree, check_item| println!("{struct_tree} -> {check_item}"),
);
// THEN
assert!(found);
}
#[test]
fn GIVEN_specwarn_checks_WHEN_traversed_for_info_THEN_not_found() {
// GIVEN
let checks = Checks {
rdap_struct: RdapStructure::Entity,
items: vec![CheckItem {
check_class: CheckClass::StdWarning,
check: Check::VariantEmptyDomain,
}],
sub_checks: vec![],
};
// WHEN
let found = traverse_checks(
&checks,
&[CheckClass::Informational],
None,
&mut |struct_tree, check_item| println!("{struct_tree} -> {check_item}"),
);
// THEN
assert!(!found);
}
#[test]
fn GIVEN_info_subchecks_WHEN_traversed_for_info_THEN_found() {
// GIVEN
let checks = Checks {
rdap_struct: RdapStructure::Entity,
items: vec![],
sub_checks: vec![Checks {
rdap_struct: RdapStructure::Autnum,
items: vec![CheckItem {
check_class: CheckClass::Informational,
check: Check::VariantEmptyDomain,
}],
sub_checks: vec![],
}],
};
// WHEN
let found = traverse_checks(
&checks,
&[CheckClass::Informational],
None,
&mut |struct_tree, check_item| println!("{struct_tree} -> {check_item}"),
);
// THEN
assert!(found);
}
#[test]
fn GIVEN_specwarn_subchecks_WHEN_traversed_for_info_THEN_not_found() {
// GIVEN
let checks = Checks {
rdap_struct: RdapStructure::Entity,
items: vec![],
sub_checks: vec![Checks {
rdap_struct: RdapStructure::Autnum,
items: vec![CheckItem {
check_class: CheckClass::StdWarning,
check: Check::VariantEmptyDomain,
}],
sub_checks: vec![],
}],
};
// WHEN
let found = traverse_checks(
&checks,
&[CheckClass::Informational],
None,
&mut |struct_tree, check_item| println!("{struct_tree} -> {check_item}"),
);
// THEN
assert!(!found);
}
#[test]
fn GIVEN_checks_and_subchecks_WHEN_traversed_THEN_tree_structure_shows_tree() {
// GIVEN
let checks = Checks {
rdap_struct: RdapStructure::Entity,
items: vec![CheckItem {
check_class: CheckClass::Informational,
check: Check::RdapConformanceInvalidParent,
}],
sub_checks: vec![Checks {
rdap_struct: RdapStructure::Autnum,
items: vec![CheckItem {
check_class: CheckClass::Informational,
check: Check::VariantEmptyDomain,
}],
sub_checks: vec![],
}],
};
// WHEN
let mut structs: Vec<String> = vec![];
let found = traverse_checks(
&checks,
&[CheckClass::Informational],
None,
&mut |struct_tree, _check_item| structs.push(struct_tree.to_string()),
);
// THEN
assert!(found);
dbg!(&structs);
assert!(structs.contains(&"[ROOT]/entity".to_string()));
assert!(structs.contains(&"[ROOT]/entity/autnum".to_string()));
}
}

View file

@ -0,0 +1,184 @@
use std::{any::TypeId, net::IpAddr, str::FromStr};
use crate::response::nameserver::Nameserver;
use super::{
string::{StringCheck, StringListCheck},
Check, CheckParams, Checks, GetChecks, GetSubChecks,
};
impl GetChecks for Nameserver {
fn get_checks(&self, params: CheckParams) -> super::Checks {
let sub_checks = if params.do_subchecks {
let mut sub_checks: Vec<Checks> = self
.common
.get_sub_checks(params.from_parent(TypeId::of::<Self>()));
sub_checks.append(
&mut self
.object_common
.get_sub_checks(params.from_parent(TypeId::of::<Self>())),
);
sub_checks
} else {
vec![]
};
let mut items = vec![];
// check ldh
if let Some(ldh) = &self.ldh_name {
if !ldh.is_ldh_domain_name() {
items.push(Check::LdhNameInvalid.check_item());
}
}
if let Some(ip_addresses) = &self.ip_addresses {
if let Some(v6_addrs) = &ip_addresses.v6 {
if v6_addrs.is_string() {
items.push(Check::IpAddressArrayIsString.check_item())
}
if v6_addrs.is_empty_or_any_empty_or_whitespace() {
items.push(Check::IpAddressListIsEmpty.check_item())
}
if v6_addrs
.vec()
.iter()
.any(|ip| IpAddr::from_str(ip).is_err())
{
items.push(Check::IpAddressMalformed.check_item())
}
}
if let Some(v4_addrs) = &ip_addresses.v4 {
if v4_addrs.is_string() {
items.push(Check::IpAddressArrayIsString.check_item())
}
if v4_addrs.is_empty_or_any_empty_or_whitespace() {
items.push(Check::IpAddressListIsEmpty.check_item())
}
if v4_addrs
.vec()
.iter()
.any(|ip| IpAddr::from_str(ip).is_err())
{
items.push(Check::IpAddressMalformed.check_item())
}
}
}
Checks {
rdap_struct: super::RdapStructure::Nameserver,
items,
sub_checks,
}
}
}
#[cfg(test)]
mod tests {
use {crate::prelude::*, rstest::rstest};
use crate::check::{Check, CheckParams, GetChecks};
#[rstest]
#[case("")]
#[case(" ")]
#[case("_.")]
fn check_nameserver_with_bad_ldh(#[case] ldh: &str) {
// GIVEN
let rdap = Nameserver::builder()
.ldh_name(ldh)
.build()
.unwrap()
.to_response();
// WHEN
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
// THEN
dbg!(&checks);
assert!(checks
.items
.iter()
.any(|c| c.check == Check::LdhNameInvalid));
}
#[test]
fn check_nameserver_with_empty_v6s() {
// GIVEN
let ns = Nameserver::illegal()
.ldh_name("ns1.example.com")
.ip_addresses(IpAddresses::illegal().v6(vec![]).build())
.build()
.to_response();
// WHEN
let checks = ns.get_checks(CheckParams::for_rdap(&ns));
// THEN
dbg!(&checks);
assert!(checks
.items
.iter()
.any(|c| c.check == Check::IpAddressListIsEmpty));
}
#[test]
fn check_nameserver_with_empty_v4s() {
// GIVEN
let ns = Nameserver::illegal()
.ldh_name("ns1.example.com")
.ip_addresses(IpAddresses::illegal().v4(vec![]).build())
.build()
.to_response();
// WHEN
let checks = ns.get_checks(CheckParams::for_rdap(&ns));
// THEN
dbg!(&checks);
assert!(checks
.items
.iter()
.any(|c| c.check == Check::IpAddressListIsEmpty));
}
#[test]
fn check_nameserver_with_bad_v6s() {
// GIVEN
let ns = Nameserver::illegal()
.ldh_name("ns1.example.com")
.ip_addresses(IpAddresses::illegal().v6(vec!["__".to_string()]).build())
.build()
.to_response();
// WHEN
let checks = ns.get_checks(CheckParams::for_rdap(&ns));
// THEN
dbg!(&checks);
assert!(checks
.items
.iter()
.any(|c| c.check == Check::IpAddressMalformed));
}
#[test]
fn check_nameserver_with_bad_v4s() {
// GIVEN
let ns = Nameserver::illegal()
.ldh_name("ns1.example.com")
.ip_addresses(IpAddresses::illegal().v4(vec!["___".to_string()]).build())
.build()
.to_response();
// WHEN
let checks = ns.get_checks(CheckParams::for_rdap(&ns));
// THEN
dbg!(&checks);
assert!(checks
.items
.iter()
.any(|c| c.check == Check::IpAddressMalformed));
}
}

View file

@ -0,0 +1,482 @@
use std::{any::TypeId, net::IpAddr, str::FromStr};
use cidr::IpCidr;
use crate::response::network::{Cidr0Cidr, Network};
use super::{string::StringCheck, Check, CheckParams, Checks, GetChecks, GetSubChecks};
impl GetChecks for Network {
fn get_checks(&self, params: CheckParams) -> super::Checks {
let sub_checks = if params.do_subchecks {
let mut sub_checks: Vec<Checks> = self
.common
.get_sub_checks(params.from_parent(TypeId::of::<Self>()));
sub_checks.append(
&mut self
.object_common
.get_sub_checks(params.from_parent(TypeId::of::<Self>())),
);
if let Some(cidr0) = &self.cidr0_cidrs {
cidr0.iter().for_each(|cidr| match cidr {
Cidr0Cidr::V4Cidr(v4) => {
if v4.v4prefix.is_none() {
sub_checks.push(Checks {
rdap_struct: super::RdapStructure::Cidr0,
items: vec![Check::Cidr0V4PrefixIsAbsent.check_item()],
sub_checks: vec![],
})
}
if v4.length.is_none() {
sub_checks.push(Checks {
rdap_struct: super::RdapStructure::Cidr0,
items: vec![Check::Cidr0V4LengthIsAbsent.check_item()],
sub_checks: vec![],
})
}
}
Cidr0Cidr::V6Cidr(v6) => {
if v6.v6prefix.is_none() {
sub_checks.push(Checks {
rdap_struct: super::RdapStructure::Cidr0,
items: vec![Check::Cidr0V6PrefixIsAbsent.check_item()],
sub_checks: vec![],
})
}
if v6.length.is_none() {
sub_checks.push(Checks {
rdap_struct: super::RdapStructure::Cidr0,
items: vec![Check::Cidr0V6LengthIsAbsent.check_item()],
sub_checks: vec![],
})
}
}
})
}
sub_checks
} else {
vec![]
};
let mut items = vec![];
if let Some(name) = &self.name {
if name.is_whitespace_or_empty() {
items.push(Check::NetworkOrAutnumNameIsEmpty.check_item())
}
}
if let Some(network_type) = &self.network_type {
if network_type.is_whitespace_or_empty() {
items.push(Check::NetworkOrAutnumTypeIsEmpty.check_item())
}
}
if self.start_address.is_none() || self.end_address.is_none() {
items.push(Check::IpAddressMissing.check_item())
}
if let Some(start_ip) = &self.start_address {
let start_addr = IpAddr::from_str(start_ip);
if start_addr.is_err() {
items.push(Check::IpAddressMalformed.check_item())
} else if self.end_address.is_some() {
let Ok(start_addr) = start_addr else {
panic!("ip result did not work")
};
let Some(end_ip) = &self.end_address else {
panic!("end address unwrap failed")
};
if let Ok(end_addr) = IpAddr::from_str(end_ip) {
if start_addr > end_addr {
items.push(Check::IpAddressEndBeforeStart.check_item())
}
if let Some(ip_version) = &self.ip_version {
if (ip_version == "v4" && (start_addr.is_ipv6() || end_addr.is_ipv6()))
|| (ip_version == "v6" && (start_addr.is_ipv4() || end_addr.is_ipv4()))
{
items.push(Check::IpAddressVersionMismatch.check_item())
} else if ip_version != "v4" && ip_version != "v6" {
items.push(Check::IpAddressMalformedVersion.check_item())
}
}
let this_network =
IpCidr::from_str("0.0.0.0/8").expect("incorrect this netowrk cidr");
if this_network.contains(&start_addr) && this_network.contains(&end_addr) {
items.push(Check::IpAddressThisNetwork.check_item())
}
let private_10 = IpCidr::from_str("10.0.0.0/8").expect("incorrect net 10 cidr");
let private_172 =
IpCidr::from_str("172.16.0.0/12").expect("incorrect net 172.16 cidr");
let private_192 =
IpCidr::from_str("192.168.0.0/16").expect("incorrect net 192.168 cidr");
if (private_10.contains(&start_addr) && private_10.contains(&end_addr))
|| (private_172.contains(&start_addr) && private_172.contains(&end_addr))
|| (private_192.contains(&start_addr) && private_192.contains(&end_addr))
{
items.push(Check::IpAddressPrivateUse.check_item())
}
let shared_nat =
IpCidr::from_str("100.64.0.0/10").expect("incorrect net 100 cidr");
if shared_nat.contains(&start_addr) && shared_nat.contains(&end_addr) {
items.push(Check::IpAddressSharedNat.check_item())
}
let loopback =
IpCidr::from_str("127.0.0.0/8").expect("incorrect loopback cidr");
if loopback.contains(&start_addr) && loopback.contains(&end_addr) {
items.push(Check::IpAddressLoopback.check_item())
}
let linklocal1 =
IpCidr::from_str("169.254.0.0/16").expect("incorrect linklocal1 cidr");
let linklocal2 =
IpCidr::from_str("fe80::/10").expect("incorrect linklocal2 cidr");
if (linklocal1.contains(&start_addr) && linklocal1.contains(&end_addr))
|| (linklocal2.contains(&start_addr) && linklocal2.contains(&end_addr))
{
items.push(Check::IpAddressLinkLocal.check_item())
}
let uniquelocal =
IpCidr::from_str("fe80::/10").expect("incorrect unique local cidr");
if uniquelocal.contains(&start_addr) && uniquelocal.contains(&end_addr) {
items.push(Check::IpAddressUniqueLocal.check_item())
}
let doc1 = IpCidr::from_str("192.0.2.0/24").expect("incorrect doc1 cidr");
let doc2 = IpCidr::from_str("198.51.100.0/24").expect("incorrect doc2 cidr");
let doc3 = IpCidr::from_str("203.0.113.0/24").expect("incorrect doc3 cidr");
let doc4 = IpCidr::from_str("2001:db8::/32").expect("incorrect doc4 cidr");
if (doc1.contains(&start_addr) && doc1.contains(&end_addr))
|| (doc2.contains(&start_addr) && doc2.contains(&end_addr))
|| (doc3.contains(&start_addr) && doc3.contains(&end_addr))
|| (doc4.contains(&start_addr) && doc4.contains(&end_addr))
{
items.push(Check::IpAddressDocumentationNet.check_item())
}
let reserved =
IpCidr::from_str("240.0.0.0/4").expect("incorrect reserved cidr");
if reserved.contains(&start_addr) && reserved.contains(&end_addr) {
items.push(Check::IpAddressLinkLocal.check_item())
}
}
}
}
if let Some(end_ip) = &self.end_address {
let addr = IpAddr::from_str(end_ip);
if addr.is_err() {
items.push(Check::IpAddressMalformed.check_item())
}
}
Checks {
rdap_struct: super::RdapStructure::IpNetwork,
items,
sub_checks,
}
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use crate::{
prelude::{Numberish, ToResponse},
response::network::{Cidr0Cidr, Network, V4Cidr, V6Cidr},
};
use crate::check::{Check, CheckParams, GetChecks};
#[test]
fn check_network_with_empty_name() {
// GIVEN
let mut network = Network::builder()
.cidr("10.0.0.0/8")
.build()
.expect("invalid ip cidr");
network.name = Some("".to_string());
let rdap = network.to_response();
// WHEN
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
// THEN
dbg!(&checks);
assert!(checks
.items
.iter()
.any(|c| c.check == Check::NetworkOrAutnumNameIsEmpty));
}
#[test]
fn check_network_with_empty_type() {
// GIVEN
let mut network = Network::builder()
.cidr("10.0.0.0/8")
.build()
.expect("invalid ip cidr");
network.network_type = Some("".to_string());
let rdap = network.to_response();
// WHEN
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
// THEN
dbg!(&checks);
assert!(checks
.items
.iter()
.any(|c| c.check == Check::NetworkOrAutnumTypeIsEmpty));
}
#[test]
fn check_network_with_no_start() {
// GIVEN
let mut network = Network::builder()
.cidr("10.0.0.0/8")
.build()
.expect("invalid ip cidr");
network.start_address = None;
let rdap = network.to_response();
// WHEN
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
// THEN
dbg!(&checks);
assert!(checks
.items
.iter()
.any(|c| c.check == Check::IpAddressMissing));
}
#[test]
fn check_network_with_no_end() {
// GIVEN
let mut network = Network::builder()
.cidr("10.0.0.0/8")
.build()
.expect("invalid ip cidr");
network.end_address = None;
let rdap = network.to_response();
// WHEN
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
// THEN
dbg!(&checks);
assert!(checks
.items
.iter()
.any(|c| c.check == Check::IpAddressMissing));
}
#[test]
fn check_network_with_bad_start() {
// GIVEN
let mut network = Network::builder()
.cidr("10.0.0.0/8")
.build()
.expect("invalid ip cidr");
network.start_address = Some("____".to_string());
let rdap = network.to_response();
// WHEN
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
// THEN
dbg!(&checks);
assert!(checks
.items
.iter()
.any(|c| c.check == Check::IpAddressMalformed));
}
#[test]
fn check_network_with_bad_end() {
// GIVEN
let mut network = Network::builder()
.cidr("10.0.0.0/8")
.build()
.expect("invalid ip cidr");
network.end_address = Some("___".to_string());
let rdap = network.to_response();
// WHEN
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
// THEN
dbg!(&checks);
assert!(checks
.items
.iter()
.any(|c| c.check == Check::IpAddressMalformed));
}
#[test]
fn check_network_with_end_before_start() {
// GIVEN
let mut network = Network::builder()
.cidr("10.0.0.0/8")
.build()
.expect("invalid ip cidr");
let swap = network.end_address.clone();
network.end_address = network.start_address.clone();
network.start_address = swap;
let rdap = network.to_response();
// WHEN
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
// THEN
dbg!(&checks);
assert!(checks
.items
.iter()
.any(|c| c.check == Check::IpAddressEndBeforeStart));
}
#[rstest]
#[case("10.0.0.0/8", "v6")]
#[case("2000::/64", "v4")]
fn check_network_with_ip_version(#[case] cidr: &str, #[case] version: &str) {
// GIVEN
let mut network = Network::builder()
.cidr(cidr)
.build()
.expect("invalid ip cidr");
network.ip_version = Some(version.to_string());
let rdap = network.to_response();
// WHEN
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
// THEN
dbg!(&checks);
assert!(checks
.items
.iter()
.any(|c| c.check == Check::IpAddressVersionMismatch));
}
#[rstest]
#[case("10.0.0.0/8", "__")]
#[case("2000::/64", "__")]
#[case("10.0.0.0/8", "")]
#[case("2000::/64", "")]
fn check_network_with_bad_ip_version(#[case] cidr: &str, #[case] version: &str) {
// GIVEN
let mut network = Network::builder()
.cidr(cidr)
.build()
.expect("invalid ip cidr");
network.ip_version = Some(version.to_string());
let rdap = network.to_response();
// WHEN
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
// THEN
dbg!(&checks);
assert!(checks
.items
.iter()
.any(|c| c.check == Check::IpAddressMalformedVersion));
}
#[test]
fn check_cidr0_with_v4_prefixex() {
// GIVEN
let network = Network::illegal()
.cidr0_cidrs(vec![Cidr0Cidr::V4Cidr(V4Cidr {
v4prefix: None,
length: Some(Numberish::<u8>::from(0)),
})])
.build();
let rdap = network.to_response();
// WHEN
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
// THEN
dbg!(&checks);
assert!(checks
.sub(crate::check::RdapStructure::Cidr0)
.expect("Cidr0")
.items
.iter()
.any(|c| c.check == Check::Cidr0V4PrefixIsAbsent));
}
#[test]
fn check_cidr0_with_v6_prefixex() {
// GIVEN
let network = Network::illegal()
.cidr0_cidrs(vec![Cidr0Cidr::V6Cidr(V6Cidr {
v6prefix: None,
length: Some(Numberish::<u8>::from(0)),
})])
.build();
let rdap = network.to_response();
// WHEN
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
// THEN
dbg!(&checks);
assert!(checks
.sub(crate::check::RdapStructure::Cidr0)
.expect("Cidr0")
.items
.iter()
.any(|c| c.check == Check::Cidr0V6PrefixIsAbsent));
}
#[test]
fn check_cidr0_with_v4_length() {
// GIVEN
let network = Network::illegal()
.cidr0_cidrs(vec![Cidr0Cidr::V4Cidr(V4Cidr {
v4prefix: Some("0.0.0.0".to_string()),
length: None,
})])
.build();
let rdap = network.to_response();
// WHEN
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
// THEN
dbg!(&checks);
assert!(checks
.sub(crate::check::RdapStructure::Cidr0)
.expect("Cidr0")
.items
.iter()
.any(|c| c.check == Check::Cidr0V4LengthIsAbsent));
}
#[test]
fn check_cidr0_with_v6_length() {
// GIVEN
let network = Network::illegal()
.cidr0_cidrs(vec![Cidr0Cidr::V6Cidr(V6Cidr {
v6prefix: Some("0.0.0.0".to_string()),
length: None,
})])
.build();
let rdap = network.to_response();
// WHEN
let checks = rdap.get_checks(CheckParams::for_rdap(&rdap));
// THEN
dbg!(&checks);
assert!(checks
.sub(crate::check::RdapStructure::Cidr0)
.expect("Cidr0")
.items
.iter()
.any(|c| c.check == Check::Cidr0V6LengthIsAbsent));
}
}

View file

@ -0,0 +1,68 @@
use std::any::TypeId;
use crate::response::search::{DomainSearchResults, EntitySearchResults, NameserverSearchResults};
use super::{CheckParams, Checks, GetChecks, GetSubChecks};
impl GetChecks for DomainSearchResults {
fn get_checks(&self, params: CheckParams) -> super::Checks {
let sub_checks: Vec<Checks> = if params.do_subchecks {
let mut sub_checks = self
.common
.get_sub_checks(params.from_parent(TypeId::of::<Self>()));
self.results.iter().for_each(|result| {
sub_checks.push(result.get_checks(params.from_parent(TypeId::of::<Self>())))
});
sub_checks
} else {
vec![]
};
Checks {
rdap_struct: super::RdapStructure::DomainSearchResults,
items: vec![],
sub_checks,
}
}
}
impl GetChecks for NameserverSearchResults {
fn get_checks(&self, params: CheckParams) -> super::Checks {
let sub_checks: Vec<Checks> = if params.do_subchecks {
let mut sub_checks: Vec<Checks> = self
.common
.get_sub_checks(params.from_parent(TypeId::of::<Self>()));
self.results.iter().for_each(|result| {
sub_checks.push(result.get_checks(params.from_parent(TypeId::of::<Self>())))
});
sub_checks
} else {
vec![]
};
Checks {
rdap_struct: super::RdapStructure::NameserverSearchResults,
items: vec![],
sub_checks,
}
}
}
impl GetChecks for EntitySearchResults {
fn get_checks(&self, params: CheckParams) -> super::Checks {
let sub_checks: Vec<Checks> = if params.do_subchecks {
let mut sub_checks: Vec<Checks> = self
.common
.get_sub_checks(params.from_parent(TypeId::of::<Self>()));
self.results.iter().for_each(|result| {
sub_checks.push(result.get_checks(params.from_parent(TypeId::of::<Self>())))
});
sub_checks
} else {
vec![]
};
Checks {
rdap_struct: super::RdapStructure::EntitySearchResults,
items: vec![],
sub_checks,
}
}
}

View file

@ -0,0 +1,295 @@
/// Functions for types that can be turned into strings.
///
/// Example:
/// ```rust
/// use icann_rdap_common::check::*;
///
/// let s = " ";
/// assert!(s.is_whitespace_or_empty());
/// ```
pub trait StringCheck {
/// Tests if the string is empty, including for if the string only has whitespace.
fn is_whitespace_or_empty(&self) -> bool;
/// Tests if the string contains only letters, digits, or hyphens and is not empty.
fn is_ldh_string(&self) -> bool;
/// Tests if a string is an LDH doamin name. This is not to be confused with [StringCheck::is_ldh_string],
/// which checks individual domain labels.
fn is_ldh_domain_name(&self) -> bool;
/// Tests if a string is a Unicode domain name.
fn is_unicode_domain_name(&self) -> bool;
/// Tests if a string is begins with a period and only has one label.
fn is_tld(&self) -> bool;
}
impl<T: ToString> StringCheck for T {
fn is_whitespace_or_empty(&self) -> bool {
let s = self.to_string();
s.is_empty() || s.chars().all(char::is_whitespace)
}
fn is_ldh_string(&self) -> bool {
let s = self.to_string();
!s.is_empty() && s.chars().all(char::is_ldh)
}
fn is_ldh_domain_name(&self) -> bool {
let s = self.to_string();
s == "." || (!s.is_empty() && s.split_terminator('.').all(|s| s.is_ldh_string()))
}
fn is_unicode_domain_name(&self) -> bool {
let s = self.to_string();
s == "."
|| (!s.is_empty()
&& s.split_terminator('.').all(|s| {
s.chars()
.all(|c| c == '-' || (!c.is_ascii_punctuation() && !c.is_whitespace()))
}))
}
fn is_tld(&self) -> bool {
let s = self.to_string();
s.starts_with('.')
&& s.len() > 2
&& s.matches('.').count() == 1
&& s.split_terminator('.').all(|s| {
s.chars()
.all(|c| !c.is_ascii_punctuation() && !c.is_whitespace())
})
}
}
/// Functions for types that can be turned into arrays of strings.
///
/// Example:
/// ```rust
/// use icann_rdap_common::check::*;
///
/// let a: &[&str] = &["foo",""];
/// assert!(a.is_empty_or_any_empty_or_whitespace());
/// ```
pub trait StringListCheck {
/// Tests if a list of strings is empty, or if any of the
/// elemeents of the list are empty or whitespace.
fn is_empty_or_any_empty_or_whitespace(&self) -> bool;
/// Tests if a list of strings ard LDH strings. See [CharCheck::is_ldh].
fn is_ldh_string_list(&self) -> bool;
}
impl<T: ToString> StringListCheck for &[T] {
fn is_empty_or_any_empty_or_whitespace(&self) -> bool {
self.is_empty() || self.iter().any(|s| s.to_string().is_whitespace_or_empty())
}
fn is_ldh_string_list(&self) -> bool {
!self.is_empty() && self.iter().all(|s| s.to_string().is_ldh_string())
}
}
impl<T: ToString> StringListCheck for Vec<T> {
fn is_empty_or_any_empty_or_whitespace(&self) -> bool {
self.is_empty() || self.iter().any(|s| s.to_string().is_whitespace_or_empty())
}
fn is_ldh_string_list(&self) -> bool {
!self.is_empty() && self.iter().all(|s| s.to_string().is_ldh_string())
}
}
/// Functions for chars.
///
/// Example:
/// ```rust
/// use icann_rdap_common::check::*;
///
/// let c = 'a';
/// assert!(c.is_ldh());
/// ```
pub trait CharCheck {
/// Checks if the character is a letter, digit or a hyphen
#[allow(clippy::wrong_self_convention)]
fn is_ldh(self) -> bool;
}
impl CharCheck for char {
fn is_ldh(self) -> bool {
matches!(self, 'A'..='Z' | 'a'..='z' | '0'..='9' | '-')
}
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use rstest::rstest;
use crate::check::string::{CharCheck, StringListCheck};
use super::StringCheck;
#[rstest]
#[case("foo", false)]
#[case("", true)]
#[case(" ", true)]
#[case("foo bar", false)]
fn GIVEN_string_WHEN_is_whitespace_or_empty_THEN_correct_result(
#[case] test_string: &str,
#[case] expected: bool,
) {
// GIVEN in parameters
// WHEN
let actual = test_string.is_whitespace_or_empty();
// THEN
assert_eq!(actual, expected);
}
#[rstest]
#[case(&[], true)]
#[case(&["foo"], false)]
#[case(&["foo",""], true)]
#[case(&["foo","bar"], false)]
#[case(&["foo","bar baz"], false)]
#[case(&[""], true)]
#[case(&[" "], true)]
fn GIVEN_string_list_WHEN_is_whitespace_or_empty_THEN_correct_result(
#[case] test_list: &[&str],
#[case] expected: bool,
) {
// GIVEN in parameters
// WHEN
let actual = test_list.is_empty_or_any_empty_or_whitespace();
// THEN
assert_eq!(actual, expected);
}
#[rstest]
#[case('a', true)]
#[case('l', true)]
#[case('z', true)]
#[case('A', true)]
#[case('L', true)]
#[case('Z', true)]
#[case('0', true)]
#[case('3', true)]
#[case('9', true)]
#[case('-', true)]
#[case('_', false)]
#[case('.', false)]
fn GIVEN_char_WHEN_is_ldh_THEN_correct_result(#[case] test_char: char, #[case] expected: bool) {
// GIVEN in parameters
// WHEN
let actual = test_char.is_ldh();
// THEN
assert_eq!(actual, expected);
}
#[rstest]
#[case("foo", true)]
#[case("", false)]
#[case("foo-bar", true)]
#[case("foo bar", false)]
fn GIVEN_string_WHEN_is_ldh_string_THEN_correct_result(
#[case] test_string: &str,
#[case] expected: bool,
) {
// GIVEN in parameters
// WHEN
let actual = test_string.is_ldh_string();
// THEN
assert_eq!(actual, expected);
}
#[rstest]
#[case("foo", false)]
#[case("", false)]
#[case("foo-bar", false)]
#[case("foo bar", false)]
#[case(".", false)]
#[case(".foo.bar", false)]
#[case(".foo", true)]
fn GIVEN_string_WHEN_is_tld_THEN_correct_result(
#[case] test_string: &str,
#[case] expected: bool,
) {
// GIVEN in parameters
// WHEN
let actual = test_string.is_tld();
// THEN
assert_eq!(actual, expected);
}
#[rstest]
#[case(&[], false)]
#[case(&["foo"], true)]
#[case(&["foo",""], false)]
#[case(&["foo","bar"], true)]
#[case(&["foo","bar baz"], false)]
#[case(&[""], false)]
#[case(&[" "], false)]
fn GIVEN_string_list_WHEN_is_ldh_string_list_THEN_correct_result(
#[case] test_list: &[&str],
#[case] expected: bool,
) {
// GIVEN in parameters
// WHEN
let actual = test_list.is_ldh_string_list();
// THEN
assert_eq!(actual, expected);
}
#[rstest]
#[case("foo", true)]
#[case("", false)]
#[case(".", true)]
#[case("foo.bar", true)]
#[case("foo.bar.", true)]
fn GIVEN_string_WHEN_is_ldh_domain_name_THEN_correct_result(
#[case] test_string: &str,
#[case] expected: bool,
) {
// GIVEN in parameters
// WHEN
let actual = test_string.is_ldh_domain_name();
// THEN
assert_eq!(actual, expected);
}
#[rstest]
#[case("foo", true)]
#[case("", false)]
#[case(".", true)]
#[case("foo.bar", true)]
#[case("foo.bar.", true)]
#[case("fo_o.bar.", false)]
#[case("fo o.bar.", false)]
fn GIVEN_string_WHEN_is_unicode_domain_name_THEN_correct_result(
#[case] test_string: &str,
#[case] expected: bool,
) {
// GIVEN in parameters
// WHEN
let actual = test_string.is_unicode_domain_name();
// THEN
assert_eq!(actual, expected);
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,846 @@
//! Convert jCard/vCard to Contact.
use serde_json::Value;
use super::{Contact, Email, Lang, NameParts, Phone, PostalAddress};
impl Contact {
/// Creates a Contact from an array of [`Value`]s.
///
/// ```rust
/// use icann_rdap_common::contact::Contact;
/// use serde::Deserialize;
/// use serde_json::Value;
///
/// let json = r#"
/// [
/// "vcard",
/// [
/// ["version", {}, "text", "4.0"],
/// ["fn", {}, "text", "Joe User"],
/// ["kind", {}, "text", "individual"],
/// ["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"
/// ]
/// ]
/// ]"#;
///
/// let data: Vec<Value> = serde_json::from_str(json).unwrap();
/// let contact = Contact::from_vcard(&data);
/// ```
pub fn from_vcard(vcard_array: &[Value]) -> Option<Contact> {
// value should be "vcard" followed by array
let value = vcard_array.first()?;
let vcard_literal = value.as_str()?;
if !vcard_literal.eq_ignore_ascii_case("vcard") {
return None;
};
let vcard = vcard_array.get(1)?;
let vcard = vcard.as_array()?;
let contact = Contact::builder()
.and_full_name(vcard.find_property("fn").get_text())
.and_kind(vcard.find_property("kind").get_text())
.titles(vcard.find_properties("title").get_texts().unwrap_or(vec![]))
.roles(vcard.find_properties("role").get_texts().unwrap_or(vec![]))
.nick_names(
vcard
.find_properties("nickname")
.get_texts()
.unwrap_or(vec![]),
)
.organization_names(vcard.find_properties("org").get_texts().unwrap_or(vec![]))
.langs(vcard.find_properties("lang").get_langs().unwrap_or(vec![]))
.emails(
vcard
.find_properties("email")
.get_emails()
.unwrap_or(vec![]),
)
.phones(vcard.find_properties("tel").get_phones().unwrap_or(vec![]))
.postal_addresses(
vcard
.find_properties("adr")
.get_postal_addresses()
.unwrap_or(vec![]),
)
.and_name_parts(vcard.find_property("n").get_name_parts())
.contact_uris(
vcard
.find_properties("contact-uri")
.get_texts()
.unwrap_or(vec![]),
)
.urls(vcard.find_properties("url").get_texts().unwrap_or(vec![]))
.build();
contact.is_non_empty().then_some(contact)
}
}
trait FindProperty<'a> {
fn find_property(self, name: &'a str) -> Option<&'a Vec<Value>>;
}
impl<'a> FindProperty<'a> for &'a [Value] {
fn find_property(self, name: &'a str) -> Option<&'a Vec<Value>> {
self.iter()
.filter_map(|prop_array| prop_array.as_array())
.find(|prop_array| {
if let Some(prop_name) = prop_array.first() {
if let Some(prop_name) = prop_name.as_str() {
prop_name.eq_ignore_ascii_case(name)
} else {
false
}
} else {
false
}
})
}
}
trait FindProperties<'a> {
fn find_properties(self, name: &'a str) -> Vec<&'a Vec<Value>>;
}
impl<'a> FindProperties<'a> for &'a [Value] {
fn find_properties(self, name: &'a str) -> Vec<&'a Vec<Value>> {
self.iter()
.filter_map(|prop_array| prop_array.as_array())
.filter(|prop_array| {
if let Some(prop_name) = prop_array.first() {
if let Some(prop_name) = prop_name.as_str() {
prop_name.eq_ignore_ascii_case(name)
} else {
false
}
} else {
false
}
})
.collect()
}
}
trait GetText<'a> {
fn get_text(self) -> Option<String>;
}
impl<'a> GetText<'a> for Option<&'a Vec<Value>> {
fn get_text(self) -> Option<String> {
let values = self?;
let fourth = values.get(3)?;
fourth.as_str().map(|s| s.to_owned())
}
}
impl<'a> GetText<'a> for &'a Vec<Value> {
fn get_text(self) -> Option<String> {
let fourth = self.get(3)?;
fourth.as_str().map(|s| s.to_owned())
}
}
trait GetTexts<'a> {
fn get_texts(self) -> Option<Vec<String>>;
}
impl<'a> GetTexts<'a> for &'a [&'a Vec<Value>] {
fn get_texts(self) -> Option<Vec<String>> {
let texts = self
.iter()
.filter_map(|prop| (*prop).get_text())
.collect::<Vec<String>>();
(!texts.is_empty()).then_some(texts)
}
}
trait GetPreference<'a> {
fn get_preference(self) -> Option<u64>;
}
impl<'a> GetPreference<'a> for &'a Vec<Value> {
fn get_preference(self) -> Option<u64> {
let second = self.get(1)?;
let second = second.as_object()?;
let preference = second.get("pref")?;
preference.as_str().and_then(|s| s.parse().ok())
}
}
trait GetLabel<'a> {
fn get_label(self) -> Option<String>;
}
impl<'a> GetLabel<'a> for &'a Vec<Value> {
fn get_label(self) -> Option<String> {
let second = self.get(1)?;
let second = second.as_object()?;
let label = second.get("label")?;
label.as_str().map(|s| s.to_owned())
}
}
const CONTEXTS: [&str; 6] = ["home", "work", "office", "private", "mobile", "cell"];
trait GetContexts<'a> {
fn get_contexts(self) -> Option<Vec<String>>;
}
impl<'a> GetContexts<'a> for &'a Vec<Value> {
fn get_contexts(self) -> Option<Vec<String>> {
let second = self.get(1)?;
let second = second.as_object()?;
let contexts = second.get("type")?;
if let Some(context) = contexts.as_str() {
let context = context.to_lowercase();
if CONTEXTS.contains(&context.as_str()) {
return Some(vec![context]);
} else {
return None;
}
};
let contexts = contexts.as_array()?;
let contexts = contexts
.iter()
.filter_map(|v| v.as_str())
.map(|s| s.to_lowercase())
.filter(|s| CONTEXTS.contains(&s.as_str()))
.collect::<Vec<String>>();
(!contexts.is_empty()).then_some(contexts)
}
}
trait GetFeatures<'a> {
fn get_features(self) -> Option<Vec<String>>;
}
impl<'a> GetFeatures<'a> for &'a Vec<Value> {
fn get_features(self) -> Option<Vec<String>> {
let second = self.get(1)?;
let second = second.as_object()?;
let features = second.get("type")?;
if let Some(feature) = features.as_str() {
let feature = feature.to_lowercase();
if !CONTEXTS.contains(&feature.as_str()) {
return Some(vec![feature]);
} else {
return None;
}
};
let features = features.as_array()?;
let features = features
.iter()
.filter_map(|v| v.as_str())
.map(|s| s.to_lowercase())
.filter(|s| !CONTEXTS.contains(&s.as_str()))
.collect::<Vec<String>>();
(!features.is_empty()).then_some(features)
}
}
trait GetLangs<'a> {
fn get_langs(self) -> Option<Vec<Lang>>;
}
impl<'a> GetLangs<'a> for &'a [&'a Vec<Value>] {
fn get_langs(self) -> Option<Vec<Lang>> {
let langs = self
.iter()
.filter_map(|prop| {
let tag = (*prop).get_text()?;
let lang = Lang::builder()
.tag(tag)
.and_preference((*prop).get_preference())
.build();
Some(lang)
})
.collect::<Vec<Lang>>();
(!langs.is_empty()).then_some(langs)
}
}
trait GetEmails<'a> {
fn get_emails(self) -> Option<Vec<Email>>;
}
impl<'a> GetEmails<'a> for &'a [&'a Vec<Value>] {
fn get_emails(self) -> Option<Vec<Email>> {
let emails = self
.iter()
.filter_map(|prop| {
let addr = (*prop).get_text()?;
let email = Email::builder()
.email(addr)
.contexts((*prop).get_contexts().unwrap_or_default())
.and_preference((*prop).get_preference())
.build();
Some(email)
})
.collect::<Vec<Email>>();
(!emails.is_empty()).then_some(emails)
}
}
trait GetPhones<'a> {
fn get_phones(self) -> Option<Vec<Phone>>;
}
impl<'a> GetPhones<'a> for &'a [&'a Vec<Value>] {
fn get_phones(self) -> Option<Vec<Phone>> {
let phones = self
.iter()
.filter_map(|prop| {
let number = (*prop).get_text()?;
let phone = Phone::builder()
.phone(number)
.features((*prop).get_features().unwrap_or_default())
.contexts((*prop).get_contexts().unwrap_or_default())
.and_preference((*prop).get_preference())
.build();
Some(phone)
})
.collect::<Vec<Phone>>();
(!phones.is_empty()).then_some(phones)
}
}
trait GetPostalAddresses<'a> {
fn get_postal_addresses(self) -> Option<Vec<PostalAddress>>;
}
impl<'a> GetPostalAddresses<'a> for &'a [&'a Vec<Value>] {
fn get_postal_addresses(self) -> Option<Vec<PostalAddress>> {
let addrs = self
.iter()
.map(|prop| {
let mut postal_code: Option<String> = None;
let mut country_code: Option<String> = None;
let mut country_name: Option<String> = None;
let mut region_code: Option<String> = None;
let mut region_name: Option<String> = None;
let mut locality: Option<String> = None;
let mut street_parts: Vec<String> = vec![];
if let Some(fourth) = prop.get(3) {
if let Some(addr) = fourth.as_array() {
// the jcard address fields are in a different index of the array.
//
// [
// "adr",
// {},
// "text",
// [
// "Mail Stop 3", // post office box (not recommended for use)
// "Suite 3000", // apartment or suite (not recommended for use)
// "123 Maple Ave", // street address
// "Quebec", // locality or city name
// "QC", // region (can be either a code or full name)
// "G1V 2M2", // postal code
// "Canada" // full country name
// ]
// ],
if let Some(pobox) = addr.first() {
if let Some(s) = pobox.as_str() {
if !s.is_empty() {
street_parts.push(s.to_string())
}
}
}
if let Some(appt) = addr.get(1) {
if let Some(s) = appt.as_str() {
if !s.is_empty() {
street_parts.push(s.to_string())
}
}
}
if let Some(street) = addr.get(2) {
if let Some(s) = street.as_str() {
if !s.is_empty() {
street_parts.push(s.to_string())
}
} else if let Some(arry_s) = street.as_array() {
arry_s
.iter()
.filter_map(|v| v.as_str())
.filter(|s| !s.is_empty())
.for_each(|s| street_parts.push(s.to_string()))
}
}
if let Some(city) = addr.get(3) {
if let Some(s) = city.as_str() {
if !s.is_empty() {
locality = Some(s.to_string());
}
}
}
if let Some(region) = addr.get(4) {
if let Some(s) = region.as_str() {
if !s.is_empty() {
if s.len() == 2 && s.to_uppercase() == s {
region_code = Some(s.to_string())
} else {
region_name = Some(s.to_string())
}
}
}
}
if let Some(pc) = addr.get(5) {
if let Some(s) = pc.as_str() {
if !s.is_empty() {
postal_code = Some(s.to_string());
}
}
}
if let Some(country) = addr.get(6) {
if let Some(s) = country.as_str() {
if !s.is_empty() {
if s.len() == 2 && s.to_uppercase() == s {
country_code = Some(s.to_string())
} else {
country_name = Some(s.to_string())
}
}
}
}
}
};
let street_parts = (!street_parts.is_empty()).then_some(street_parts);
PostalAddress::builder()
.and_full_address((*prop).get_label())
.contexts((*prop).get_contexts().unwrap_or_default())
.and_preference((*prop).get_preference())
.and_country_code(country_code)
.and_country_name(country_name)
.and_postal_code(postal_code)
.and_region_name(region_name)
.and_region_code(region_code)
.and_locality(locality)
.street_parts(street_parts.unwrap_or_default())
.build()
})
.collect::<Vec<PostalAddress>>();
(!addrs.is_empty()).then_some(addrs)
}
}
trait GetNameParts<'a> {
fn get_name_parts(self) -> Option<NameParts>;
}
impl<'a> GetNameParts<'a> for Option<&'a Vec<Value>> {
fn get_name_parts(self) -> Option<NameParts> {
let values = self?;
let fourth = values.get(3)?;
let parts = fourth.as_array()?;
let mut iter = parts.iter().filter(|p| p.is_string() || p.is_array());
let mut prefixes: Option<Vec<String>> = None;
let mut surnames: Option<Vec<String>> = None;
let mut given_names: Option<Vec<String>> = None;
let mut middle_names: Option<Vec<String>> = None;
let mut suffixes: Option<Vec<String>> = None;
if let Some(e) = iter.next() {
surnames = get_string_or_vec(e);
};
if let Some(e) = iter.next() {
given_names = get_string_or_vec(e);
};
if let Some(e) = iter.next() {
middle_names = get_string_or_vec(e);
};
if let Some(e) = iter.next() {
prefixes = get_string_or_vec(e);
};
if let Some(e) = iter.next() {
suffixes = get_string_or_vec(e);
};
let name_parts = NameParts::builder()
.surnames(surnames.unwrap_or_default())
.prefixes(prefixes.unwrap_or_default())
.given_names(given_names.unwrap_or_default())
.middle_names(middle_names.unwrap_or_default())
.suffixes(suffixes.unwrap_or_default())
.build();
if name_parts.surnames.is_none()
&& name_parts.given_names.is_none()
&& name_parts.middle_names.is_none()
&& name_parts.suffixes.is_none()
&& name_parts.prefixes.is_none()
{
None
} else {
Some(name_parts)
}
}
}
fn get_string_or_vec(value: &Value) -> Option<Vec<String>> {
if let Some(e) = value.as_str() {
if e.is_empty() {
return None;
} else {
return Some(vec![e.to_string()]);
};
};
if let Some(e) = value.as_array() {
let strings = e
.iter()
.filter_map(|e| e.as_str())
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect::<Vec<String>>();
return (!strings.is_empty()).then_some(strings);
};
None
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use serde_json::Value;
use crate::contact::{Contact, NameParts};
#[test]
fn GIVEN_vcard_WHEN_from_vcard_THEN_properties_are_correct() {
// GIVEN
let vcard = r#"
[
"vcard",
[
["version", {}, "text", "4.0"],
["fn", {}, "text", "Joe User"],
["n", {}, "text",
["User", "Joe", "", "", ["ing. jr", "M.Sc."]]
],
["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"
]
],
["adr",
{
"type":"home",
"label":"123 Maple Ave\nSuite 90001\nVancouver\nBC\n1239\n"
},
"text",
[
"", "", "", "", "", "", ""
]
],
["tel",
{
"type":["work", "voice"],
"pref":"1"
},
"uri",
"tel:+1-555-555-1234;ext=102"
],
["tel",
{ "type":["work", "cell", "voice", "video", "text"] },
"uri",
"tel:+1-555-555-4321"
],
["email",
{ "type":"work" },
"text",
"joe.user@example.com"
],
["geo", {
"type":"work"
}, "uri", "geo:46.772673,-71.282945"],
["key",
{ "type":"work" },
"uri",
"https://www.example.com/joe.user/joe.asc"
],
["tz", {},
"utc-offset", "-05:00"],
["contact-uri", {},
"uri",
"https://example.com/contact-form"
],
["url", {},
"uri",
"https://example.com/some-url"
]
]
]
"#;
// WHEN
let actual = serde_json::from_str::<Vec<Value>>(vcard);
// THEN
let actual = actual.expect("parsing vcard");
let actual = Contact::from_vcard(&actual).expect("vcard not found");
// full name
assert_eq!(actual.full_name.expect("full_name not found"), "Joe User");
// kind
assert_eq!(actual.kind.expect("kind not found"), "individual");
// titles
assert_eq!(
actual
.titles
.expect("no titles")
.first()
.expect("titles empty"),
"Research Scientist"
);
// roles
assert_eq!(
actual
.roles
.expect("no roles")
.first()
.expect("roles empty"),
"Project Lead"
);
// organization names
assert_eq!(
actual
.organization_names
.expect("no organization_names")
.first()
.expect("organization_names empty"),
"Example"
);
// nick names
assert!(actual.nick_names.is_none());
// langs
let Some(langs) = actual.langs else {
panic!("langs not found")
};
assert_eq!(langs.len(), 2);
assert_eq!(langs.first().expect("first lang").tag, "fr");
assert_eq!(langs.first().expect("first lang").preference, Some(1));
assert_eq!(langs.get(1).expect("second lang").tag, "en");
assert_eq!(langs.get(1).expect("second lang").preference, Some(2));
// emails
let Some(emails) = actual.emails else {
panic!("emails not found")
};
let Some(email) = emails.first() else {
panic!("no email found")
};
assert_eq!(email.email, "joe.user@example.com");
assert!(email
.contexts
.as_ref()
.expect("contexts not found")
.contains(&"work".to_string()));
// phones
let Some(phones) = actual.phones else {
panic!("no phones found")
};
let Some(phone) = phones.first() else {
panic!("no first phone")
};
assert_eq!(phone.phone, "tel:+1-555-555-1234;ext=102");
assert!(phone
.contexts
.as_ref()
.expect("no contexts")
.contains(&"work".to_string()));
assert!(phone
.features
.as_ref()
.expect("no features")
.contains(&"voice".to_string()));
let Some(phone) = phones.last() else {
panic!("no last phone")
};
assert_eq!(phone.phone, "tel:+1-555-555-4321");
assert!(phone
.contexts
.as_ref()
.expect("no contexts")
.contains(&"cell".to_string()));
assert!(phone
.features
.as_ref()
.expect("no features")
.contains(&"video".to_string()));
// postal addresses
let Some(addresses) = actual.postal_addresses else {
panic!("no postal addresses")
};
let Some(addr) = addresses.first() else {
panic!("first address not found")
};
assert!(addr
.contexts
.as_ref()
.expect("no contexts")
.contains(&"work".to_string()));
let Some(street_parts) = &addr.street_parts else {
panic!("no street parts")
};
assert_eq!(street_parts.first().expect("street part 0"), "Suite 1234");
assert_eq!(
street_parts.get(1).expect("street part 1"),
"4321 Rue Somewhere"
);
assert_eq!(addr.country_name.as_ref().expect("country name"), "Canada");
assert!(addr.country_code.is_none());
assert_eq!(addr.region_code.as_ref().expect("region code"), "QC");
assert!(addr.region_name.is_none());
assert_eq!(addr.postal_code.as_ref().expect("postal code"), "G1V 2M2");
let Some(addr) = addresses.last() else {
panic!("last address not found")
};
assert!(addr
.contexts
.as_ref()
.expect("no contexts")
.contains(&"home".to_string()));
assert_eq!(
addr.full_address.as_ref().expect("full address not foudn"),
"123 Maple Ave\nSuite 90001\nVancouver\nBC\n1239\n"
);
// name parts
let Some(name_parts) = actual.name_parts else {
panic!("no name parts")
};
let expected = NameParts::builder()
.surnames(vec!["User".to_string()])
.given_names(vec!["Joe".to_string()])
.suffixes(vec!["ing. jr".to_string(), "M.Sc.".to_string()])
.build();
assert_eq!(name_parts, expected);
// contact-uris
assert_eq!(
actual
.contact_uris
.expect("no contact-uris")
.first()
.expect("contact-uris empty"),
"https://example.com/contact-form"
);
// urls
assert_eq!(
actual
.urls
.expect("no urls")
.first()
.expect("urls are empty"),
"https://example.com/some-url"
);
}
#[test]
fn GIVEN_vcard_with_addr_street_array_WHEN_from_vcard_THEN_properties_are_correct() {
// GIVEN
let vcard = r#"
[
"vcard",
[
["version", {}, "text", "4.0"],
["fn", {}, "text", "Joe User"],
["adr",
{ "type":"work" },
"text",
[
"",
"Suite 1234",
["4321 Rue Blue", "1, Gawwn"],
"Quebec",
"QC",
"G1V 2M2",
"Canada"
]
]
]
]
"#;
// WHEN
let actual = serde_json::from_str::<Vec<Value>>(vcard);
// THEN
let actual = actual.expect("parsing vcard");
let actual = Contact::from_vcard(&actual).expect("vcard not found");
// full name
assert_eq!(actual.full_name.expect("full_name not found"), "Joe User");
// postal addresses
let Some(addresses) = actual.postal_addresses else {
panic!("no postal addresses")
};
let Some(addr) = addresses.first() else {
panic!("first address not found")
};
assert!(addr
.contexts
.as_ref()
.expect("no contexts")
.contains(&"work".to_string()));
let Some(street_parts) = &addr.street_parts else {
panic!("no street parts")
};
assert_eq!(street_parts.first().expect("street part 0"), "Suite 1234");
assert_eq!(street_parts.get(1).expect("street part 1"), "4321 Rue Blue");
assert_eq!(street_parts.get(2).expect("street part 2"), "1, Gawwn");
assert_eq!(addr.country_name.as_ref().expect("country name"), "Canada");
assert!(addr.country_code.is_none());
assert_eq!(addr.region_code.as_ref().expect("region code"), "QC");
assert!(addr.region_name.is_none());
assert_eq!(addr.postal_code.as_ref().expect("postal code"), "G1V 2M2");
}
}

View file

@ -0,0 +1,486 @@
//! Easy representation of contact information found in an Entity.
//!
//! This module converts contact information to and from vCard/jCard, which is hard to
//! work with directly. It is also intended as a way of bridging the between vCard/jCard
//! and any new contact model.
//!
//! This struct can be built using the builder.
//!
//! ```rust
//! use icann_rdap_common::contact::Contact;
//!
//! let contact = Contact::builder()
//! .kind("individual")
//! .full_name("Bob Smurd")
//! .build();
//! ```
//!
//! Once built, a Contact struct can be converted to an array of [serde_json::Value]'s,
//! which can be used with serde to serialize to JSON.
//!
//! ```rust
//! use icann_rdap_common::contact::Contact;
//! use serde::Serialize;
//! use serde_json::Value;
//!
//! let contact = Contact::builder()
//! .kind("individual")
//! .full_name("Bob Smurd")
//! .build();
//!
//! let v = contact.to_vcard();
//! let json = serde_json::to_string(&v);
//! ```
//!
//! To deserialize, use the `from_vcard` function.
//!
//! ```rust
//! use icann_rdap_common::contact::Contact;
//! use serde::Deserialize;
//! use serde_json::Value;
//!
//! let json = r#"
//! [
//! "vcard",
//! [
//! ["version", {}, "text", "4.0"],
//! ["fn", {}, "text", "Joe User"],
//! ["kind", {}, "text", "individual"],
//! ["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"
//! ]
//! ]
//! ]"#;
//!
//! let data: Vec<Value> = serde_json::from_str(json).unwrap();
//! let contact = Contact::from_vcard(&data);
//! ```
mod from_vcard;
mod to_vcard;
use std::fmt::Display;
use buildstructor::Builder;
use crate::prelude::to_opt_vec;
/// Represents a contact. This more closely represents an EPP Contact with some
/// things taken from JSContact.
///
/// Using the builder to create the Contact:
/// ```rust
/// use icann_rdap_common::contact::Contact;
///
/// let contact = Contact::builder()
/// .kind("individual")
/// .full_name("Bob Smurd")
/// .build();
/// ```
///
///
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Contact {
/// Preferred languages.
pub langs: Option<Vec<Lang>>,
/// The kind such as individual, company, etc...
pub kind: Option<String>,
/// Full name of the contact.
pub full_name: Option<String>,
/// Structured parts of the name.
pub name_parts: Option<NameParts>,
/// Nick names.
pub nick_names: Option<Vec<String>>,
/// Titles.
pub titles: Option<Vec<String>>,
/// Organizational Roles
pub roles: Option<Vec<String>>,
/// Organization names.
pub organization_names: Option<Vec<String>>,
/// Postal addresses.
pub postal_addresses: Option<Vec<PostalAddress>>,
/// Email addresses.
pub emails: Option<Vec<Email>>,
/// Phone numbers.
pub phones: Option<Vec<Phone>>,
/// Contact URIs.
pub contact_uris: Option<Vec<String>>,
/// URLs
pub urls: Option<Vec<String>>,
}
#[buildstructor::buildstructor]
impl Contact {
#[builder(visibility = "pub")]
#[allow(clippy::too_many_arguments)]
fn new(
langs: Vec<Lang>,
kind: Option<String>,
full_name: Option<String>,
name_parts: Option<NameParts>,
nick_names: Vec<String>,
titles: Vec<String>,
roles: Vec<String>,
organization_names: Vec<String>,
postal_addresses: Vec<PostalAddress>,
emails: Vec<Email>,
phones: Vec<Phone>,
contact_uris: Vec<String>,
urls: Vec<String>,
) -> Self {
Self {
langs: to_opt_vec(langs),
kind,
full_name,
name_parts,
nick_names: to_opt_vec(nick_names),
titles: to_opt_vec(titles),
roles: to_opt_vec(roles),
organization_names: to_opt_vec(organization_names),
postal_addresses: to_opt_vec(postal_addresses),
emails: to_opt_vec(emails),
phones: to_opt_vec(phones),
contact_uris: to_opt_vec(contact_uris),
urls: to_opt_vec(urls),
}
}
/// Returns false if there is data in the Contact.
pub fn is_non_empty(&self) -> bool {
self.langs.is_some()
|| self.kind.is_some()
|| self.full_name.is_some()
|| self.name_parts.is_some()
|| self.nick_names.is_some()
|| self.titles.is_some()
|| self.roles.is_some()
|| self.organization_names.is_some()
|| self.postal_addresses.is_some()
|| self.emails.is_some()
|| self.phones.is_some()
|| self.contact_uris.is_some()
|| self.urls.is_some()
}
/// Set the set of emails.
pub fn set_emails(mut self, emails: &[impl ToString]) -> Self {
let emails: Vec<Email> = emails
.iter()
.map(|e| Email::builder().email(e.to_string()).build())
.collect();
self.emails = (!emails.is_empty()).then_some(emails);
self
}
/// Add a voice phone to the set of phones.
pub fn add_voice_phones(mut self, phones: &[impl ToString]) -> Self {
let mut phones: Vec<Phone> = phones
.iter()
.map(|p| {
Phone::builder()
.contexts(vec!["voice".to_string()])
.phone(p.to_string())
.build()
})
.collect();
if let Some(mut self_phones) = self.phones.clone() {
phones.append(&mut self_phones);
} else {
self.phones = (!phones.is_empty()).then_some(phones);
}
self
}
/// Add a facsimile phone to the set of phones.
pub fn add_fax_phones(mut self, phones: &[impl ToString]) -> Self {
let mut phones: Vec<Phone> = phones
.iter()
.map(|p| {
Phone::builder()
.contexts(vec!["fax".to_string()])
.phone(p.to_string())
.build()
})
.collect();
if let Some(mut self_phones) = self.phones.clone() {
phones.append(&mut self_phones);
} else {
self.phones = (!phones.is_empty()).then_some(phones);
}
self
}
/// Set the set of postal addresses to only be the passed in postal address.
pub fn set_postal_address(mut self, postal_address: PostalAddress) -> Self {
self.postal_addresses = Some(vec![postal_address]);
self
}
}
/// The language preference of the contact.
#[derive(Debug, Builder, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Lang {
/// The ordinal of the preference for this language.
pub preference: Option<u64>,
/// RFC 5646 language tag.
pub tag: String,
}
impl Display for Lang {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(pref) = self.preference {
write!(f, "{} (pref: {})", self.tag, pref)
} else {
f.write_str(&self.tag)
}
}
}
/// Name parts of a name.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct NameParts {
/// Name prefixes.
pub prefixes: Option<Vec<String>>,
/// Surnames or last names.
pub surnames: Option<Vec<String>>,
/// Middle names.
pub middle_names: Option<Vec<String>>,
/// Given or first names.
pub given_names: Option<Vec<String>>,
/// Name suffixes.
pub suffixes: Option<Vec<String>>,
}
#[buildstructor::buildstructor]
impl NameParts {
#[builder(visibility = "pub")]
#[allow(clippy::too_many_arguments)]
fn new(
prefixes: Vec<String>,
surnames: Vec<String>,
middle_names: Vec<String>,
given_names: Vec<String>,
suffixes: Vec<String>,
) -> Self {
Self {
prefixes: to_opt_vec(prefixes),
surnames: to_opt_vec(surnames),
middle_names: to_opt_vec(middle_names),
given_names: to_opt_vec(given_names),
suffixes: to_opt_vec(suffixes),
}
}
}
/// A postal address.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct PostalAddress {
/// Preference of this address in relation to others.
pub preference: Option<u64>,
/// Work, home, etc.... Known as "type" in JCard.
pub contexts: Option<Vec<String>>,
/// An unstructured address. An unstructured postal address is
/// usually the complete postal address. That is, this string
/// would contain the street address, country, region, postal code, etc...
///
/// Depending on how the postal address is given, it can either
/// be structured or unstructured. If it is given as unstructured,
/// then this value is populated.
///
/// It is possible that a single postal address is given as both,
/// in which case this value is populated along with the other
/// values of the postal address.
pub full_address: Option<String>,
/// Invidual street lines.
pub street_parts: Option<Vec<String>>,
/// City name, county name, etc...
pub locality: Option<String>,
/// Name of region (i.e. state, province, etc...).
pub region_name: Option<String>,
/// Code for region.
pub region_code: Option<String>,
/// Name of the country.
pub country_name: Option<String>,
/// Code of the country.
pub country_code: Option<String>,
/// Postal code.
pub postal_code: Option<String>,
}
#[buildstructor::buildstructor]
impl PostalAddress {
#[builder(visibility = "pub")]
#[allow(clippy::too_many_arguments)]
fn new(
preference: Option<u64>,
contexts: Vec<String>,
full_address: Option<String>,
street_parts: Vec<String>,
locality: Option<String>,
region_name: Option<String>,
region_code: Option<String>,
country_name: Option<String>,
country_code: Option<String>,
postal_code: Option<String>,
) -> Self {
Self {
preference,
contexts: to_opt_vec(contexts),
full_address,
street_parts: to_opt_vec(street_parts),
locality,
region_name,
region_code,
country_name,
country_code,
postal_code,
}
}
}
/// Represents an email address.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Email {
/// Preference of this email in relation to others.
pub preference: Option<u64>,
/// Work, home, etc.... Known as "type" in JCard.
pub contexts: Option<Vec<String>>,
/// The email address.
pub email: String,
}
#[buildstructor::buildstructor]
impl Email {
#[builder(visibility = "pub")]
fn new(preference: Option<u64>, contexts: Vec<String>, email: String) -> Self {
Self {
preference,
contexts: to_opt_vec(contexts),
email,
}
}
}
impl Display for Email {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut qualifiers = vec![];
if let Some(pref) = self.preference {
qualifiers.push(format!("(pref: {pref})"));
}
if let Some(contexts) = &self.contexts {
qualifiers.push(format!("({})", contexts.join(",")));
}
let qualifiers = qualifiers.join(" ");
if qualifiers.is_empty() {
f.write_str(&self.email)
} else {
write!(f, "{} {}", &self.email, qualifiers)
}
}
}
/// Represents phone number.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Phone {
/// Preference of this phone in relation to others.
pub preference: Option<u64>,
/// Work, home, etc.... Known as "type" in JCard.
pub contexts: Option<Vec<String>>,
/// The phone number.
pub phone: String,
/// Features (voice, fax, etc...)
pub features: Option<Vec<String>>,
}
#[buildstructor::buildstructor]
impl Phone {
#[builder(visibility = "pub")]
fn new(
preference: Option<u64>,
contexts: Vec<String>,
phone: String,
features: Vec<String>,
) -> Self {
Self {
preference,
contexts: to_opt_vec(contexts),
phone,
features: to_opt_vec(features),
}
}
}
impl Display for Phone {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut qualifiers = vec![];
if let Some(pref) = self.preference {
qualifiers.push(format!("(pref: {pref})"));
}
if let Some(contexts) = &self.contexts {
qualifiers.push(format!("({})", contexts.join(",")));
}
if let Some(features) = &self.features {
qualifiers.push(format!("({})", features.join(",")));
}
let qualifiers = qualifiers.join(" ");
if qualifiers.is_empty() {
f.write_str(&self.phone)
} else {
write!(f, "{} {}", &self.phone, qualifiers)
}
}
}

View file

@ -0,0 +1,303 @@
//! Convert a Contact to jCard/vCard.
use std::str::FromStr;
use serde_json::{json, Map, Value};
use super::Contact;
impl Contact {
/// Output the Contact data as vCard in JSON values ([`Vec<Value>`]).
///
/// ```rust
/// use icann_rdap_common::contact::Contact;
/// use serde::Serialize;
/// use serde_json::Value;
///
/// let contact = Contact::builder()
/// .kind("individual")
/// .full_name("Bob Smurd")
/// .build();
///
/// let v = contact.to_vcard();
/// let json = serde_json::to_string(&v);
/// ```
pub fn to_vcard(&self) -> Vec<Value> {
// start the vcard with the version.
let mut vcard: Vec<Value> = vec![json!(["version", {}, "text", "4.0"])];
if let Some(full_name) = &self.full_name {
vcard.push(json!(["fn", {}, "text", full_name]));
}
if let Some(name_parts) = &self.name_parts {
let surnames = vec_string_to_value(&name_parts.surnames);
let given_names = vec_string_to_value(&name_parts.given_names);
let middle_names = vec_string_to_value(&name_parts.middle_names);
let prefixes = vec_string_to_value(&name_parts.prefixes);
let suffixes = vec_string_to_value(&name_parts.suffixes);
vcard.push(json!([
"n",
{},
"text",
[surnames, given_names, middle_names, prefixes, suffixes]
]));
}
if let Some(kind) = &self.kind {
vcard.push(json!(["kind", {}, "text", kind]));
}
if let Some(langs) = &self.langs {
for lang in langs {
let mut params: Map<String, Value> = Map::new();
if let Some(pref) = lang.preference {
params.insert("pref".to_string(), Value::String(pref.to_string()));
}
vcard.push(json!([
"lang",
Value::from(params),
"language-tag",
lang.tag
]))
}
}
if let Some(org_names) = &self.organization_names {
for org_name in org_names {
vcard.push(json!(["org", {}, "text", org_name]));
}
}
if let Some(titles) = &self.titles {
for title in titles {
vcard.push(json!(["title", {}, "text", title]));
}
}
if let Some(roles) = &self.roles {
for role in roles {
vcard.push(json!(["role", {}, "text", role]));
}
}
if let Some(nick_names) = &self.nick_names {
for nick_name in nick_names {
vcard.push(json!(["nickname", {}, "text", nick_name]));
}
}
if let Some(emails) = &self.emails {
for email in emails {
let mut params: Map<String, Value> = Map::new();
if let Some(pref) = email.preference {
params.insert("pref".to_string(), Value::String(pref.to_string()));
}
if let Some(contexts) = email.contexts.as_ref() {
params.insert("type".to_string(), vec_string_to_param(contexts));
}
vcard.push(json!(["email", Value::from(params), "text", email.email]))
}
}
if let Some(phones) = &self.phones {
for phone in phones {
let mut params: Map<String, Value> = Map::new();
if let Some(pref) = phone.preference {
params.insert("pref".to_string(), Value::String(pref.to_string()));
}
let mut types: Vec<String> = vec![];
if let Some(contexts) = &phone.contexts {
types.append(&mut contexts.clone());
}
if let Some(features) = &phone.features {
types.append(&mut features.clone());
}
params.insert("type".to_string(), vec_string_to_param(&types));
vcard.push(json!(["tel", Value::from(params), "text", phone.phone]))
}
}
if let Some(addrs) = &self.postal_addresses {
for addr in addrs {
let mut params: Map<String, Value> = Map::new();
if let Some(pref) = addr.preference {
params.insert("pref".to_string(), Value::String(pref.to_string()));
}
if let Some(contexts) = addr.contexts.as_ref() {
params.insert("type".to_string(), vec_string_to_param(contexts));
}
if let Some(full_address) = &addr.full_address {
params.insert(
"label".to_string(),
Value::from_str(full_address).expect("serializing full address"),
);
}
let mut lines: Vec<String> = vec![];
if let Some(street_parts) = &addr.street_parts {
lines.push(street_parts.first().cloned().unwrap_or("".to_string()));
lines.push(street_parts.get(1).cloned().unwrap_or("".to_string()));
lines.push(street_parts.get(2).cloned().unwrap_or("".to_string()));
} else {
lines.push("".to_string());
lines.push("".to_string());
lines.push("".to_string());
}
if let Some(locality) = &addr.locality {
lines.push(locality.to_owned());
} else {
lines.push("".to_string());
}
if let Some(region_name) = &addr.region_name {
lines.push(region_name.to_owned());
} else if let Some(region_code) = &addr.region_code {
lines.push(region_code.to_owned());
} else {
lines.push("".to_string());
}
if let Some(postal_code) = &addr.postal_code {
lines.push(postal_code.to_owned());
} else {
lines.push("".to_string());
}
if let Some(country_name) = &addr.country_name {
lines.push(country_name.to_owned());
} else if let Some(country_code) = &addr.country_code {
lines.push(country_code.to_owned());
} else {
lines.push("".to_string());
}
vcard.push(json!(["adr", Value::from(params), "text", lines]))
}
}
if let Some(contact_uris) = &self.contact_uris {
for uri in contact_uris {
vcard.push(json!(["contact-uri", {}, "uri", uri]));
}
}
if let Some(urls) = &self.urls {
for url in urls {
vcard.push(json!(["url", {}, "uri", url]));
}
}
// return the vcard array
vec![Value::String("vcard".to_string()), Value::from(vcard)]
}
}
fn vec_string_to_value(strings: &Option<Vec<String>>) -> Value {
let Some(strings) = strings else {
return Value::String("".to_string());
};
if strings.is_empty() {
return Value::String("".to_string());
};
if strings.len() == 1 {
let Some(one) = strings.first() else {
panic!("couldn't get first element on length of 1")
};
return Value::String(one.to_owned());
};
// else
Value::from(strings.clone())
}
fn vec_string_to_param(strings: &[String]) -> Value {
if strings.is_empty() {
return Value::String("".to_string());
};
if strings.len() == 1 {
let Some(one) = strings.first() else {
panic!("couldn't get first element on length of 1")
};
return Value::String(one.to_owned());
};
// else
Value::from(strings)
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use crate::contact::{Contact, Email, Lang, NameParts, Phone, PostalAddress};
#[test]
fn GIVEN_contact_WHEN_to_vcard_THEN_from_vcard_is_same() {
// GIVEN
let contact = Contact::builder()
.full_name("Joe User")
.name_parts(
NameParts::builder()
.surnames(vec!["User".to_string()])
.given_names(vec!["Joe".to_string()])
.suffixes(vec!["ing. jr".to_string(), "M.Sc.".to_string()])
.build(),
)
.kind("individual")
.langs(vec![
Lang::builder().preference(1).tag("fr").build(),
Lang::builder().preference(2).tag("en").build(),
])
.organization_names(vec!["Example".to_string()])
.titles(vec!["Research Scientist".to_string()])
.roles(vec!["Project Lead".to_string()])
.contact_uris(vec!["https://example.com/contact-form".to_string()])
.postal_addresses(vec![PostalAddress::builder()
.country_name("Canada")
.postal_code("G1V 2M2")
.region_code("QC")
.locality("Quebec")
.street_parts(vec![
"Suite 1234".to_string(),
"4321 Rue Somewhere".to_string(),
])
.build()])
.phones(vec![
Phone::builder()
.preference(1)
.contexts(vec!["work".to_string()])
.features(vec!["voice".to_string()])
.phone("tel:+1-555-555-1234;ext=102")
.build(),
Phone::builder()
.contexts(vec!["work".to_string(), "cell".to_string()])
.features(vec![
"voice".to_string(),
"video".to_string(),
"text".to_string(),
])
.phone("tel:+1-555-555-4321")
.build(),
])
.emails(vec![Email::builder()
.contexts(vec!["work".to_string()])
.email("joe.user@example.com")
.build()])
.urls(vec!["https://example.com/some-url".to_string()])
.build();
// WHEN
let actual = Contact::from_vcard(&contact.to_vcard()).expect("from vcard");
// THEN
assert_eq!(contact.full_name, actual.full_name);
assert_eq!(contact.name_parts, actual.name_parts);
assert_eq!(contact.kind, actual.kind);
assert_eq!(contact.langs, actual.langs);
assert_eq!(contact.organization_names, actual.organization_names);
assert_eq!(contact.titles, actual.titles);
assert_eq!(contact.roles, actual.roles);
assert_eq!(contact.postal_addresses, actual.postal_addresses);
assert_eq!(contact.phones, actual.phones);
assert_eq!(contact.emails, actual.emails);
assert_eq!(contact.contact_uris, actual.contact_uris);
assert_eq!(contact.urls, actual.urls);
}
}

View file

@ -0,0 +1,320 @@
//! DNS and DNSSEC types.
use std::str::{Chars, FromStr};
use {idna::domain_to_ascii, thiserror::Error};
use crate::check::StringCheck;
/// Errors when determining DNS information.
#[derive(Debug, Error)]
pub enum DnsTypeError {
#[error("Invalid DNS Algorithm")]
InvalidAlgorithm,
#[error("Invalid DNS Digest")]
InvalidDigest,
}
/// Information about DNSSEC Algorithm.
pub struct DnsAlgorithm {
pub number: u8,
pub mnemonic: &'static str,
pub zone_signing: bool,
pub transaction_signing: bool,
}
/// DNS Algorithm Variants.
pub enum DnsAlgorithmType {
DeleteDs(DnsAlgorithm),
RsaMd5(DnsAlgorithm),
DiffieHellman(DnsAlgorithm),
Dsa(DnsAlgorithm),
RsaSha1(DnsAlgorithm),
DsaNsec3Sha1(DnsAlgorithm),
RsaSha1Nsec3Sha1(DnsAlgorithm),
RsaSha256(DnsAlgorithm),
RsaSha512(DnsAlgorithm),
EccGost(DnsAlgorithm),
EcdsaP256Sha256(DnsAlgorithm),
EcdsaP384Sha384(DnsAlgorithm),
Ed25519(DnsAlgorithm),
Ed448(DnsAlgorithm),
PrivateDns(DnsAlgorithm),
PrivateOid(DnsAlgorithm),
}
impl DnsAlgorithmType {
/// Convert an algorithm number to a [DnsAlgorithmType].
pub fn from_number(number: u8) -> Result<Self, DnsTypeError> {
Ok(match number {
0 => Self::DeleteDs(DnsAlgorithm {
number: 0,
mnemonic: "DELETE",
zone_signing: false,
transaction_signing: false,
}),
1 => Self::RsaMd5(DnsAlgorithm {
number: 1,
mnemonic: "RSAMD5",
zone_signing: false,
transaction_signing: true,
}),
2 => Self::DiffieHellman(DnsAlgorithm {
number: 2,
mnemonic: "DH",
zone_signing: false,
transaction_signing: true,
}),
3 => Self::Dsa(DnsAlgorithm {
number: 3,
mnemonic: "DSA",
zone_signing: true,
transaction_signing: true,
}),
5 => Self::RsaSha1(DnsAlgorithm {
number: 5,
mnemonic: "RSASHA1",
zone_signing: true,
transaction_signing: true,
}),
6 => Self::DsaNsec3Sha1(DnsAlgorithm {
number: 6,
mnemonic: "DSA-NSEC3-SHA1",
zone_signing: true,
transaction_signing: true,
}),
7 => Self::RsaSha1Nsec3Sha1(DnsAlgorithm {
number: 7,
mnemonic: "RSA-NSEC3-SHA1",
zone_signing: true,
transaction_signing: true,
}),
8 => Self::RsaSha256(DnsAlgorithm {
number: 8,
mnemonic: "RSASHA256",
zone_signing: true,
transaction_signing: false,
}),
10 => Self::RsaSha512(DnsAlgorithm {
number: 10,
mnemonic: "RSASHA512",
zone_signing: true,
transaction_signing: false,
}),
12 => Self::EccGost(DnsAlgorithm {
number: 12,
mnemonic: "ECC-GOST",
zone_signing: true,
transaction_signing: false,
}),
13 => Self::EcdsaP256Sha256(DnsAlgorithm {
number: 13,
mnemonic: "ECDSAP256SHA256",
zone_signing: true,
transaction_signing: false,
}),
14 => Self::EcdsaP384Sha384(DnsAlgorithm {
number: 14,
mnemonic: "ECDSAP384SHA384",
zone_signing: true,
transaction_signing: false,
}),
15 => Self::Ed25519(DnsAlgorithm {
number: 15,
mnemonic: "ED25519",
zone_signing: true,
transaction_signing: false,
}),
16 => Self::Ed448(DnsAlgorithm {
number: 16,
mnemonic: "ED448",
zone_signing: true,
transaction_signing: false,
}),
253 => Self::PrivateDns(DnsAlgorithm {
number: 253,
mnemonic: "PRIVATEDNS",
zone_signing: true,
transaction_signing: true,
}),
254 => Self::PrivateOid(DnsAlgorithm {
number: 254,
mnemonic: "PRIVATEOID",
zone_signing: true,
transaction_signing: true,
}),
_ => return Err(DnsTypeError::InvalidAlgorithm),
})
}
fn algo(self) -> DnsAlgorithm {
match self {
Self::DeleteDs(a)
| Self::RsaMd5(a)
| Self::DiffieHellman(a)
| Self::Dsa(a)
| Self::RsaSha1(a)
| Self::DsaNsec3Sha1(a)
| Self::RsaSha1Nsec3Sha1(a)
| Self::RsaSha256(a)
| Self::RsaSha512(a)
| Self::EccGost(a)
| Self::EcdsaP256Sha256(a)
| Self::EcdsaP384Sha384(a)
| Self::Ed25519(a)
| Self::Ed448(a)
| Self::PrivateDns(a)
| Self::PrivateOid(a) => a,
}
}
/// Get the mnemonic for the algorithm number.
pub fn mnemonic(number: u8) -> Result<&'static str, DnsTypeError> {
let alg = Self::from_number(number)?;
Ok(alg.algo().mnemonic)
}
/// True if the DNS Algorithm can sign zones.
pub fn zone_signing(number: u8) -> Result<bool, DnsTypeError> {
let alg = Self::from_number(number)?;
Ok(alg.algo().zone_signing)
}
}
/// DNS Digest.
pub struct DnsDigest {
pub number: u8,
pub mnemonic: &'static str,
pub mandatory: bool,
}
/*
0 Reserved - [RFC3658]
1 SHA-1 MANDATORY [RFC3658]
2 SHA-256 MANDATORY [RFC4509]
3 GOST R 34.11-94 OPTIONAL [RFC5933]
4 SHA-384 OPTIONAL
*/
/// DNS Digest Variants.
pub enum DnsDigestType {
Sha1(DnsDigest),
Sha256(DnsDigest),
Gost(DnsDigest),
Sha384(DnsDigest),
}
impl DnsDigestType {
/// Get the [DnsDigestType] from the protocol number.
pub fn from_number(number: u8) -> Result<Self, DnsTypeError> {
Ok(match number {
1 => Self::Sha1(DnsDigest {
number: 1,
mnemonic: "SHA1",
mandatory: true,
}),
2 => Self::Sha256(DnsDigest {
number: 2,
mnemonic: "SHA256",
mandatory: true,
}),
3 => Self::Gost(DnsDigest {
number: 3,
mnemonic: "GOST",
mandatory: false,
}),
4 => Self::Sha384(DnsDigest {
number: 4,
mnemonic: "SHA384",
mandatory: false,
}),
_ => return Err(DnsTypeError::InvalidDigest),
})
}
/// Get the mnemonic from the protocol number.
pub fn mnemonic(number: u8) -> Result<&'static str, DnsTypeError> {
let digest = Self::from_number(number)?;
Ok(match digest {
Self::Sha1(d) | Self::Sha256(d) | Self::Gost(d) | Self::Sha384(d) => d.mnemonic,
})
}
}
/// Error specific to processing of domain names.
#[derive(Debug, Error)]
pub enum DomainNameError {
#[error("Invalid Domain Name")]
InvalidDomainName,
#[error(transparent)]
IdnaError(#[from] idna::Errors),
}
/// Represents a Domain name.
#[derive(Debug)]
pub struct DomainName {
domain_name: String,
ascii: String,
}
impl DomainName {
/// Iterate over the characters of the domain name.
pub fn chars(&self) -> Chars<'_> {
self.domain_name.chars()
}
/// Is this domain name a TLD.
pub fn is_tld(&self) -> bool {
self.domain_name.is_tld()
}
/// Gets the ASCII version of the domain, which is different if this is an IDN.
pub fn to_ascii(&self) -> &str {
&self.ascii
}
/// Is this domain name an IDN.
pub fn is_idn(&self) -> bool {
!self.ascii.eq(&self.domain_name)
}
/// Is this the DNS root.
pub fn is_root(&self) -> bool {
self.domain_name.eq(".")
}
/// Get this domain name with a leading dot.
pub fn with_leading_dot(&self) -> String {
if !self.is_root() {
format!(".{}", self.domain_name)
} else {
self.domain_name.to_string()
}
}
/// Trim leading dot.
pub fn trim_leading_dot(&self) -> &str {
if !self.is_root() {
self.domain_name.trim_start_matches('.')
} else {
&self.domain_name
}
}
}
impl FromStr for DomainName {
type Err = DomainNameError;
/// Create a new DomainName from a string.
fn from_str(s: &str) -> Result<Self, Self::Err> {
if !s.is_unicode_domain_name() {
return Err(DomainNameError::InvalidDomainName);
}
let ascii = domain_to_ascii(s)?;
Ok(Self {
domain_name: s.to_string(),
ascii,
})
}
}

View file

@ -0,0 +1,327 @@
//! Code for handling HTTP caching.
use {
chrono::{DateTime, Duration, Utc},
serde::{Deserialize, Serialize},
};
/// Represents the data from HTTP responses.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct HttpData {
pub content_length: Option<u64>,
pub content_type: Option<String>,
pub scheme: Option<String>,
pub host: String,
pub expires: Option<String>,
pub cache_control: Option<String>,
pub received: DateTime<Utc>,
pub status_code: u16,
pub location: Option<String>,
pub access_control_allow_origin: Option<String>,
pub access_control_allow_credentials: Option<String>,
pub strict_transport_security: Option<String>,
pub retry_after: Option<String>,
pub request_uri: Option<String>,
}
#[buildstructor::buildstructor]
impl HttpData {
#[builder(visibility = "pub")]
#[allow(clippy::too_many_arguments)]
fn new(
content_length: Option<u64>,
content_type: Option<String>,
scheme: Option<String>,
host: String,
expires: Option<String>,
cache_control: Option<String>,
status_code: u16,
location: Option<String>,
access_control_allow_origin: Option<String>,
access_control_allow_credentials: Option<String>,
strict_transport_security: Option<String>,
retry_after: Option<String>,
received: DateTime<Utc>,
request_uri: Option<String>,
) -> Self {
Self {
content_length,
content_type,
scheme,
host,
expires,
cache_control,
received,
status_code,
location,
access_control_allow_origin,
access_control_allow_credentials,
strict_transport_security,
retry_after,
request_uri,
}
}
#[builder(entry = "now", visibility = "pub")]
#[allow(clippy::too_many_arguments)]
fn new_now(
content_length: Option<u64>,
content_type: Option<String>,
scheme: String,
host: String,
expires: Option<String>,
cache_control: Option<String>,
status_code: Option<u16>,
location: Option<String>,
access_control_allow_origin: Option<String>,
access_control_allow_credentials: Option<String>,
strict_transport_security: Option<String>,
retry_after: Option<String>,
request_uri: Option<String>,
) -> Self {
Self {
content_length,
content_type,
scheme: Some(scheme),
host,
expires,
cache_control,
received: Utc::now(),
status_code: status_code.unwrap_or(200),
location,
access_control_allow_origin,
access_control_allow_credentials,
strict_transport_security,
retry_after,
request_uri,
}
}
#[builder(entry = "example", visibility = "pub")]
#[allow(clippy::too_many_arguments)]
fn new_example(
content_length: Option<u64>,
content_type: Option<String>,
expires: Option<String>,
cache_control: Option<String>,
status_code: Option<u16>,
location: Option<String>,
access_control_allow_origin: Option<String>,
access_control_allow_credentials: Option<String>,
strict_transport_security: Option<String>,
retry_after: Option<String>,
request_uri: Option<String>,
) -> Self {
Self {
content_length,
content_type,
scheme: Some("http".to_string()),
host: "example.com".to_string(),
expires,
cache_control,
received: Utc::now(),
status_code: status_code.unwrap_or(200),
location,
access_control_allow_origin,
access_control_allow_credentials,
strict_transport_security,
retry_after,
request_uri,
}
}
pub fn is_expired(&self, max_age: i64) -> bool {
let now = Utc::now();
if now >= self.received + Duration::seconds(max_age) {
return true;
}
if let Some(cache_control) = &self.cache_control {
let cc_max_age = cache_control
.split(',')
.map(|s| s.trim())
.find(|s| s.starts_with("max-age="));
if let Some(cc_max_age) = cc_max_age {
let cc_max_age = cc_max_age.trim_start_matches("max-age=").parse::<i64>();
if let Ok(cc_max_age) = cc_max_age {
return now >= self.received + Duration::seconds(cc_max_age);
}
}
}
if let Some(expires) = &self.expires {
let expire_time = DateTime::parse_from_rfc2822(expires);
return if let Ok(expire_time) = expire_time {
now >= expire_time
} else {
false
};
}
false
}
pub fn should_cache(&self) -> bool {
if let Some(cache_control) = &self.cache_control {
return !cache_control
.split(',')
.map(|s| s.trim())
.any(|s| s.eq("no-store") || s.eq("no-cache"));
}
true
}
pub fn from_lines(lines: &[String]) -> Result<(Self, &[String]), serde_json::Error> {
let count = lines.iter().take_while(|s| !s.starts_with("---")).count();
let cache_data = lines
.iter()
.take(count)
.cloned()
.collect::<Vec<String>>()
.join("");
let cache_data = serde_json::from_str(&cache_data)?;
Ok((cache_data, &lines[count + 1..]))
}
pub fn to_lines(&self, data: &str) -> Result<String, serde_json::Error> {
let mut lines = serde_json::to_string(self)?;
lines.push_str("\n---\n");
lines.push_str(data);
Ok(lines)
}
pub fn content_length(&self) -> Option<u64> {
self.content_length
}
pub fn content_type(&self) -> Option<&str> {
self.content_type.as_deref()
}
pub fn scheme(&self) -> Option<&str> {
self.scheme.as_deref()
}
pub fn host(&self) -> &str {
&self.host
}
pub fn expires(&self) -> Option<&str> {
self.expires.as_deref()
}
pub fn cache_control(&self) -> Option<&str> {
self.cache_control.as_deref()
}
pub fn received(&self) -> &DateTime<Utc> {
&self.received
}
pub fn status_code(&self) -> u16 {
self.status_code
}
pub fn location(&self) -> Option<&str> {
self.location.as_deref()
}
pub fn access_control_allow_origin(&self) -> Option<&str> {
self.access_control_allow_origin.as_deref()
}
pub fn access_control_allow_credentials(&self) -> Option<&str> {
self.access_control_allow_credentials.as_deref()
}
pub fn strict_transport_security(&self) -> Option<&str> {
self.strict_transport_security.as_deref()
}
pub fn retry_after(&self) -> Option<&str> {
self.retry_after.as_deref()
}
pub fn request_uri(&self) -> Option<&str> {
self.request_uri.as_deref()
}
}
#[cfg(test)]
mod tests {
use {
super::HttpData,
chrono::{Duration, Utc},
rstest::rstest,
};
#[rstest]
#[case(HttpData::example().cache_control("max-age=0").build(), 100, true)]
#[case(HttpData::example().cache_control("max-age=100").build(), 0, true)]
#[case(HttpData::example().cache_control("max-age=100").build(), 50, false)]
#[case(HttpData::example().build(), 0, true)]
#[case(HttpData::example().build(), 100, false)]
#[case(HttpData::example().expires(Utc::now().to_rfc2822()).build(), 100, true)]
#[case(HttpData::example().expires((Utc::now() + Duration::seconds(50)).to_rfc2822()).build(), 100, false)]
#[case(HttpData::example().expires((Utc::now() + Duration::seconds(100)).to_rfc2822()).build(), 50, false)]
#[case(HttpData::example().cache_control("max-age=100").expires(Utc::now().to_rfc2822()).build(), 100, false)]
#[case(HttpData::example().cache_control("max-age=0").expires((Utc::now() + Duration::seconds(50)).to_rfc2822()).build(), 100, true)]
fn test_cache_data_and_max_age_is_expired(
#[case] cache_data: HttpData,
#[case] max_age: i64,
#[case] expected: bool,
) {
// GIVEN in parameters
// WHEN
let actual = cache_data.is_expired(max_age);
// THEN
assert_eq!(actual, expected);
}
#[rstest]
#[case(HttpData::example().cache_control("no-cache").build(), false)]
#[case(HttpData::example().cache_control("no-store").build(), false)]
#[case(HttpData::example().cache_control("max-age=40").build(), true)]
fn test_cache_control(#[case] cache_data: HttpData, #[case] expected: bool) {
// GIVEN in parameters
// WHEN
let actual = cache_data.should_cache();
// THEN
assert_eq!(actual, expected);
}
#[test]
fn test_data_and_data_cache_to_lines() {
// GIVEN
let data = "foo";
let cache_data = HttpData::example().content_length(14).build();
// WHEN
let actual = cache_data.to_lines(data).unwrap();
// THEN
let expected = format!("{}\n---\nfoo", serde_json::to_string(&cache_data).unwrap());
assert_eq!(actual, expected);
}
#[test]
fn test_from_lines() {
// GIVEN
let data = "foo";
let cache_data = HttpData::example().content_length(14).build();
let lines = cache_data
.to_lines(data)
.unwrap()
.split('\n')
.map(|s| s.to_string())
.collect::<Vec<String>>();
// WHEN
let actual = HttpData::from_lines(&lines).expect("parsing lines");
// THEN
assert_eq!(cache_data, actual.0);
assert_eq!(vec![data], actual.1);
}
}

View file

@ -0,0 +1,746 @@
//! The IANA RDAP Bootstrap Registries.
use {
ipnet::{Ipv4Net, Ipv6Net},
prefix_trie::PrefixMap,
serde::{Deserialize, Serialize},
thiserror::Error,
};
/// IANA registry variants for RDAP.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum IanaRegistryType {
RdapBootstrapDns,
RdapBootstrapAsn,
RdapBootstrapIpv4,
RdapBootstrapIpv6,
RdapObjectTags,
}
impl IanaRegistryType {
/// Get the URL for an IANA RDAP registry.
pub fn url(&self) -> &str {
match self {
Self::RdapBootstrapDns => "https://data.iana.org/rdap/dns.json",
Self::RdapBootstrapAsn => "https://data.iana.org/rdap/asn.json",
Self::RdapBootstrapIpv4 => "https://data.iana.org/rdap/ipv4.json",
Self::RdapBootstrapIpv6 => "https://data.iana.org/rdap/ipv6.json",
Self::RdapObjectTags => "https://data.iana.org/rdap/object-tags.json",
}
}
/// Get the filename in the URL for the IANA RDAP registry.
pub fn file_name(&self) -> &str {
let url = self.url();
url.rsplit('/')
.next()
.expect("unexpected errror: cannot get filename from url")
}
}
/// Classes of IANA registries.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(untagged)]
pub enum IanaRegistry {
RdapBootstrapRegistry(RdapBootstrapRegistry),
// might add IANA registrar IDs later
}
/// Represents an IANA RDAP bootstrap registry.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct RdapBootstrapRegistry {
pub version: String,
pub publication: String,
pub description: Option<String>,
pub services: Vec<Vec<Vec<String>>>,
}
pub trait BootstrapRegistry {
fn get_dns_bootstrap_urls(&self, ldh: &str) -> Result<Vec<String>, BootstrapRegistryError>;
fn get_asn_bootstrap_urls(&self, asn: &str) -> Result<Vec<String>, BootstrapRegistryError>;
fn get_ipv4_bootstrap_urls(&self, ipv4: &str) -> Result<Vec<String>, BootstrapRegistryError>;
fn get_ipv6_bootstrap_urls(&self, ipv6: &str) -> Result<Vec<String>, BootstrapRegistryError>;
fn get_tag_bootstrap_urls(&self, tag: &str) -> Result<Vec<String>, BootstrapRegistryError>;
}
/// Errors from processing IANA RDAP bootstrap registries.
#[derive(Debug, Error)]
pub enum BootstrapRegistryError {
#[error("Empty Service")]
EmptyService,
#[error("Empty URL Set")]
EmptyUrlSet,
#[error("Invalid Bootstrap Input")]
InvalidBootstrapInput,
#[error("No Bootstrap URLs Found")]
NoBootstrapUrls,
#[error("Invalid Bootstrap Service")]
InvalidBootstrapService,
}
impl BootstrapRegistry for IanaRegistry {
/// Get the URLs from the IANA domain bootstrap registry.
fn get_dns_bootstrap_urls(&self, ldh: &str) -> Result<Vec<String>, BootstrapRegistryError> {
let mut longest_match: Option<(usize, Vec<String>)> = None;
let Self::RdapBootstrapRegistry(bootstrap) = self;
for service in &bootstrap.services {
let tlds = service
.first()
.ok_or(BootstrapRegistryError::EmptyService)?;
for tld in tlds {
// if the ldh domain ends with the tld or the tld is the empty string which means the root
if ldh.ends_with(tld) || tld.is_empty() {
let urls = service.last().ok_or(BootstrapRegistryError::EmptyUrlSet)?;
let longest = longest_match.get_or_insert_with(|| (tld.len(), urls.to_owned()));
if longest.0 < tld.len() {
*longest = (tld.len(), urls.to_owned());
}
}
}
}
let longest = longest_match.ok_or(BootstrapRegistryError::NoBootstrapUrls)?;
Ok(longest.1)
}
/// Get the URLS from the IANA autnum bootstrap registry.
fn get_asn_bootstrap_urls(&self, asn: &str) -> Result<Vec<String>, BootstrapRegistryError> {
let autnum = asn
.trim_start_matches(|c| -> bool { matches!(c, 'a' | 'A' | 's' | 'S') })
.parse::<u32>()
.map_err(|_| BootstrapRegistryError::InvalidBootstrapInput)?;
let Self::RdapBootstrapRegistry(bootstrap) = self;
for service in &bootstrap.services {
let as_ranges = service
.first()
.ok_or(BootstrapRegistryError::EmptyService)?;
for range in as_ranges {
let as_split = range.split('-').collect::<Vec<&str>>();
let start_as = as_split
.first()
.ok_or(BootstrapRegistryError::InvalidBootstrapService)?
.parse::<u32>()
.map_err(|_| BootstrapRegistryError::InvalidBootstrapInput)?;
let end_as = as_split
.last()
.ok_or(BootstrapRegistryError::InvalidBootstrapService)?
.parse::<u32>()
.map_err(|_| BootstrapRegistryError::InvalidBootstrapService)?;
if start_as <= autnum && end_as >= autnum {
let urls = service.last().ok_or(BootstrapRegistryError::EmptyUrlSet)?;
return Ok(urls.to_owned());
}
}
}
Err(BootstrapRegistryError::NoBootstrapUrls)
}
/// Get the URLs from the IANA IPv4 bootstrap registry.
fn get_ipv4_bootstrap_urls(&self, ipv4: &str) -> Result<Vec<String>, BootstrapRegistryError> {
let mut pm: PrefixMap<Ipv4Net, Vec<String>> = PrefixMap::new();
let Self::RdapBootstrapRegistry(bootstrap) = self;
for service in &bootstrap.services {
let urls = service.last().ok_or(BootstrapRegistryError::EmptyService)?;
for cidr in service
.first()
.ok_or(BootstrapRegistryError::InvalidBootstrapService)?
{
pm.insert(
cidr.parse()
.map_err(|_| BootstrapRegistryError::InvalidBootstrapService)?,
urls.clone(),
);
}
}
let net = pm
.get_lpm(
&ipv4
.parse::<Ipv4Net>()
.map_err(|_| BootstrapRegistryError::InvalidBootstrapInput)?,
)
.ok_or(BootstrapRegistryError::NoBootstrapUrls)?;
Ok(net.1.to_owned())
}
/// Get the URLs from the IANA IPv6 bootstrap registry.
fn get_ipv6_bootstrap_urls(&self, ipv6: &str) -> Result<Vec<String>, BootstrapRegistryError> {
let mut pm: PrefixMap<Ipv6Net, Vec<String>> = PrefixMap::new();
let Self::RdapBootstrapRegistry(bootstrap) = self;
for service in &bootstrap.services {
let urls = service.last().ok_or(BootstrapRegistryError::EmptyService)?;
for cidr in service
.first()
.ok_or(BootstrapRegistryError::InvalidBootstrapService)?
{
pm.insert(
cidr.parse()
.map_err(|_| BootstrapRegistryError::InvalidBootstrapService)?,
urls.clone(),
);
}
}
let net = pm
.get_lpm(
&ipv6
.parse::<Ipv6Net>()
.map_err(|_| BootstrapRegistryError::InvalidBootstrapInput)?,
)
.ok_or(BootstrapRegistryError::NoBootstrapUrls)?;
Ok(net.1.to_owned())
}
/// Get the URLs from the IANA object tag bootstrap registry.
fn get_tag_bootstrap_urls(&self, tag: &str) -> Result<Vec<String>, BootstrapRegistryError> {
let Self::RdapBootstrapRegistry(bootstrap) = self;
for service in &bootstrap.services {
let object_tag = service
.get(1)
.ok_or(BootstrapRegistryError::InvalidBootstrapService)?
.first()
.ok_or(BootstrapRegistryError::EmptyService)?;
if object_tag.to_ascii_uppercase() == tag.to_ascii_uppercase() {
let urls = service.last().ok_or(BootstrapRegistryError::EmptyUrlSet)?;
return Ok(urls.to_owned());
}
}
Err(BootstrapRegistryError::NoBootstrapUrls)
}
}
/// Prefer HTTPS urls.
pub fn get_preferred_url(urls: Vec<String>) -> Result<String, BootstrapRegistryError> {
if urls.is_empty() {
Err(BootstrapRegistryError::EmptyUrlSet)
} else {
let url = urls
.iter()
.find(|s| s.starts_with("https://"))
.unwrap_or_else(|| urls.first().unwrap());
Ok(url.to_owned())
}
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use rstest::rstest;
use crate::iana::{get_preferred_url, BootstrapRegistry};
use super::{IanaRegistry, IanaRegistryType};
#[rstest]
#[case(IanaRegistryType::RdapBootstrapDns, "dns.json")]
#[case(IanaRegistryType::RdapBootstrapAsn, "asn.json")]
#[case(IanaRegistryType::RdapBootstrapIpv4, "ipv4.json")]
#[case(IanaRegistryType::RdapBootstrapIpv6, "ipv6.json")]
#[case(IanaRegistryType::RdapObjectTags, "object-tags.json")]
fn GIVEN_registry_WHEN_get_file_name_THEN_correct_result(
#[case] registry: IanaRegistryType,
#[case] expected: &str,
) {
// GIVEN in parameters
// WHEN
let actual = registry.file_name();
// THEN
assert_eq!(actual, expected);
}
#[test]
fn GIVEN_domain_bootstrap_WHEN_deserialize_THEN_success() {
// GIVEN
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/"
]
],
[
["xn--zckzah"],
[
"https://example.net/rdap/xn--zckzah/",
"http://example.net/rdap/xn--zckzah/"
]
]
]
}
"#;
// WHEN
let actual = serde_json::from_str::<IanaRegistry>(bootstrap);
// THEN
actual.unwrap();
}
#[test]
fn GIVEN_one_url_WHEN_preferred_urls_THEN_that_is_the_one() {
// GIVEN
let urls = vec!["http://foo.example".to_string()];
// WHEN
let actual = get_preferred_url(urls).expect("cannot get preferred url");
// THEN
assert_eq!(actual, "http://foo.example");
}
#[test]
fn GIVEN_one_http_and_https_url_WHEN_preferred_urls_THEN_return_https() {
// GIVEN
let urls = vec![
"http://foo.example".to_string(),
"https://foo.example".to_string(),
];
// WHEN
let actual = get_preferred_url(urls).expect("cannot get preferred url");
// THEN
assert_eq!(actual, "https://foo.example");
}
#[test]
fn GIVEN_domain_bootstrap_with_matching_WHEN_find_THEN_url_matches() {
// GIVEN
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");
// WHEN
let actual = iana.get_dns_bootstrap_urls("foo.org");
// THEN
assert_eq!(
actual.expect("no vec").first().expect("vec is empty"),
"https://example.org/"
);
}
#[test]
fn GIVEN_domain_bootstrap_with_two_matching_WHEN_find_THEN_return_longest_match() {
// GIVEN
let bootstrap = r#"
{
"version": "1.0",
"publication": "2024-01-07T10:11:12Z",
"description": "Some text",
"services": [
[
["co.uk"],
[
"https://registry.co.uk/"
]
],
[
["uk"],
[
"https://registry.uk/"
]
]
]
}
"#;
let iana =
serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse domain bootstrap");
// WHEN
let actual = iana.get_dns_bootstrap_urls("foo.co.uk");
// THEN
assert_eq!(
actual.expect("no vec").first().expect("vec is empty"),
"https://registry.co.uk/"
);
}
#[test]
fn GIVEN_domain_bootstrap_with_root_WHEN_find_THEN_url_matches() {
// GIVEN
let bootstrap = r#"
{
"version": "1.0",
"publication": "2024-01-07T10:11:12Z",
"description": "Some text",
"services": [
[
["net", "com"],
[
"https://registry.example.com/myrdap/"
]
],
[
[""],
[
"https://example.org/"
]
]
]
}
"#;
let iana =
serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse domain bootstrap");
// WHEN
let actual = iana.get_dns_bootstrap_urls("foo.org");
// THEN
assert_eq!(
actual.expect("no vec").first().expect("vec is empty"),
"https://example.org/"
);
}
#[test]
fn GIVEN_autnum_bootstrap_with_match_WHEN_find_with_string_THEN_return_match() {
// GIVEN
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");
// WHEN
let actual = iana.get_asn_bootstrap_urls("as64498");
// THEN
assert_eq!(
actual.expect("no vec").first().expect("vec is empty"),
"https://example.org/"
);
}
#[rstest]
#[case(64497u32, "https://example.org/")]
#[case(64498u32, "https://example.org/")]
#[case(64510u32, "https://example.org/")]
#[case(65536u32, "https://example.org/")]
#[case(65537u32, "https://example.org/")]
#[case(64513u32, "http://example.net/rdaprir2/")]
fn GIVEN_autnum_bootstrap_with_match_WHEN_find_with_number_THEN_return_match(
#[case] asn: u32,
#[case] bootstrap_url: &str,
) {
// GIVEN
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");
// WHEN
let actual = iana.get_asn_bootstrap_urls(&asn.to_string());
// THEN
assert_eq!(
actual.expect("no vec").first().expect("vec is empty"),
bootstrap_url
);
}
#[test]
fn GIVEN_ipv4_bootstrap_with_match_WHEN_find_with_ip_address_THEN_return_match() {
// GIVEN
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 ipv4 bootstrap");
// WHEN
let actual = iana.get_ipv4_bootstrap_urls("198.51.100.1/32");
// THEN
assert_eq!(
actual.expect("no vec").first().expect("vec is empty"),
"https://rir1.example.com/myrdap/"
);
}
#[test]
fn GIVEN_ipv4_bootstrap_with_match_WHEN_find_with_cidr_THEN_return_match() {
// GIVEN
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 ipv4 bootstrap");
// WHEN
let actual = iana.get_ipv4_bootstrap_urls("203.0.113.0/24");
// THEN
assert_eq!(
actual.expect("no vec").first().expect("vec is empty"),
"https://example.org/"
);
}
#[test]
fn GIVEN_ipv6_bootstrap_with_match_WHEN_find_with_ip_address_THEN_return_match() {
// GIVEN
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 ipv6 bootstrap");
// WHEN
let actual = iana.get_ipv6_bootstrap_urls("2001:db8::1/128");
// THEN
assert_eq!(
actual.expect("no vec").first().expect("vec is empty"),
"https://rir2.example.com/myrdap/"
);
}
#[test]
fn GIVEN_ipv6_bootstrap_with_match_WHEN_find_with_ip_cidr_THEN_return_match() {
// GIVEN
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 ipv6 bootstrap");
// WHEN
let actual = iana.get_ipv6_bootstrap_urls("2001:db8:4000::/36");
// THEN
assert_eq!(
actual.expect("no vec").first().expect("vec is empty"),
"https://example.org/"
);
}
#[test]
fn GIVEN_tag_bootstrap_with_match_WHEN_find_with_tag_THEN_return_match() {
// GIVEN
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 tag bootstrap");
// WHEN
let actual = iana.get_tag_bootstrap_urls("YYYY");
// THEN
assert_eq!(
actual.expect("no vec").first().expect("vec is empty"),
"https://example.com/rdap/"
);
}
}

View file

@ -0,0 +1,28 @@
#![allow(rustdoc::bare_urls)]
#![doc = include_str!("../README.md")]
pub mod check;
pub mod contact;
pub mod dns_types;
pub mod httpdata;
pub mod iana;
pub mod media_types;
pub mod response;
/// Basics RDAP structures.
pub mod prelude {
#[doc(inline)]
pub use crate::contact::*;
#[doc(inline)]
pub use crate::response::*;
}
#[cfg(debug_assertions)]
use const_format::formatcp;
/// Version of this software.
#[cfg(not(any(target_arch = "wasm32", debug_assertions)))]
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
/// Version of this software.
#[cfg(debug_assertions)]
pub const VERSION: &str = formatcp!("{}_DEV_BUILD", env!("CARGO_PKG_VERSION"));

View file

@ -0,0 +1,7 @@
//! RDAP media types (formerly known as mime types).
/// The "application/json" media type value.
pub const JSON_MEDIA_TYPE: &str = "application/json";
/// The "application/rdap+json" media type value.
pub const RDAP_MEDIA_TYPE: &str = "application/rdap+json";

View file

@ -0,0 +1,341 @@
//! RDAP Autonomous System Number.
use {
crate::prelude::{Common, Extension, ObjectCommon},
serde::{Deserialize, Serialize},
};
use super::{
to_opt_vec, types::Link, CommonFields, Entity, Event, GetSelfLink, Notice, Numberish,
ObjectCommonFields, Port43, Remark, SelfLink, ToChild, ToResponse,
};
/// Represents an RDAP [autnum](https://rdap.rcode3.com/protocol/object_classes.html#autnum) object response.
///
/// Using the builder to construct this structure is recommended
/// as it will fill-in many of the mandatory fields.
/// The following is an example.
///
/// ```rust
/// use icann_rdap_common::prelude::*;
///
/// let autnum = Autnum::builder()
/// .autnum_range(700..710) // the range of autnums
/// .handle("AS700-1")
/// .status("active")
/// .build();
/// let c = serde_json::to_string_pretty(&autnum).unwrap();
/// eprintln!("{c}");
/// ```
/// This will produce the following.
///
/// ```norust
/// {
/// "rdapConformance": [
/// "rdap_level_0"
/// ],
/// "objectClassName": "autnum",
/// "handle": "AS700-1",
/// "status": [
/// "active"
/// ],
/// "startAutnum": 700,
/// "endAutnum": 710
/// }
/// ```
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Autnum {
#[serde(flatten)]
pub common: Common,
#[serde(flatten)]
pub object_common: ObjectCommon,
#[serde(rename = "startAutnum")]
#[serde(skip_serializing_if = "Option::is_none")]
pub start_autnum: Option<Numberish<u32>>,
#[serde(rename = "endAutnum")]
#[serde(skip_serializing_if = "Option::is_none")]
pub end_autnum: Option<Numberish<u32>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(rename = "type")]
#[serde(skip_serializing_if = "Option::is_none")]
pub autnum_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub country: Option<String>,
}
#[buildstructor::buildstructor]
impl Autnum {
/// Builds a basic autnum object.
///
/// ```rust
/// use icann_rdap_common::prelude::*;
///
/// let autnum = Autnum::builder()
/// .autnum_range(700..710)
/// .handle("AS700-1")
/// .status("active")
/// .build();
/// ```
#[builder(visibility = "pub")]
#[allow(clippy::too_many_arguments)]
fn new(
autnum_range: std::ops::Range<u32>,
handle: Option<String>,
remarks: Vec<Remark>,
links: Vec<Link>,
events: Vec<Event>,
statuses: Vec<String>,
port_43: Option<Port43>,
entities: Vec<Entity>,
notices: Vec<Notice>,
country: Option<String>,
autnum_type: Option<String>,
name: Option<String>,
extensions: Vec<Extension>,
redacted: Option<Vec<crate::response::redacted::Redacted>>,
) -> Self {
Self {
common: Common::level0()
.extensions(extensions)
.and_notices(to_opt_vec(notices))
.build(),
object_common: ObjectCommon::autnum()
.and_handle(handle)
.and_remarks(to_opt_vec(remarks))
.and_links(to_opt_vec(links))
.and_events(to_opt_vec(events))
.status(statuses)
.and_port_43(port_43)
.and_entities(to_opt_vec(entities))
.and_redacted(redacted)
.build(),
start_autnum: Some(Numberish::<u32>::from(autnum_range.start)),
end_autnum: Some(Numberish::<u32>::from(autnum_range.end)),
name,
autnum_type,
country,
}
}
/// Returns the starting ASN of the range.
pub fn start_autnum(&self) -> Option<u32> {
self.start_autnum.as_ref().and_then(|n| n.as_u32())
}
/// Returns the ending ASN of the range.
pub fn end_autnum(&self) -> Option<u32> {
self.end_autnum.as_ref().and_then(|n| n.as_u32())
}
/// Returns the name of the ASN.
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
/// Returns the type of the ASN.
pub fn autnum_type(&self) -> Option<&str> {
self.autnum_type.as_deref()
}
/// Returns the country of the ASN.
pub fn country(&self) -> Option<&str> {
self.country.as_deref()
}
}
impl ToResponse for Autnum {
fn to_response(self) -> super::RdapResponse {
super::RdapResponse::Autnum(Box::new(self))
}
}
impl GetSelfLink for Autnum {
fn get_self_link(&self) -> Option<&Link> {
self.object_common.get_self_link()
}
}
impl SelfLink for Autnum {
fn set_self_link(mut self, link: Link) -> Self {
self.object_common = self.object_common.set_self_link(link);
self
}
}
impl ToChild for Autnum {
fn to_child(mut self) -> Self {
self.common = Common {
rdap_conformance: None,
notices: None,
};
self
}
}
impl CommonFields for Autnum {
fn common(&self) -> &Common {
&self.common
}
}
impl ObjectCommonFields for Autnum {
fn object_common(&self) -> &ObjectCommon {
&self.object_common
}
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use super::Autnum;
#[test]
fn GIVEN_autnum_WHEN_deserialize_THEN_success() {
// GIVEN
let expected = r#"
{
"objectClassName" : "autnum",
"handle" : "XXXX-RIR",
"startAutnum" : 65536,
"endAutnum" : 65541,
"name": "AS-RTR-1",
"type" : "DIRECT ALLOCATION",
"status" : [ "active" ],
"country": "AU",
"remarks" :
[
{
"description" :
[
"She sells sea shells down by the sea shore.",
"Originally written by Terry Sullivan."
]
}
],
"links" :
[
{
"value" : "https://example.net/autnum/65537",
"rel" : "self",
"href" : "https://example.net/autnum/65537",
"type" : "application/rdap+json"
}
],
"events" :
[
{
"eventAction" : "registration",
"eventDate" : "1990-12-31T23:59:59Z"
},
{
"eventAction" : "last changed",
"eventDate" : "1991-12-31T23:59:59Z"
}
],
"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"
}
]
}
]
}
"#;
// WHEN
let actual = serde_json::from_str::<Autnum>(expected);
// THEN
let actual = actual.unwrap();
assert_eq!(actual.object_common.object_class_name, "autnum");
assert!(actual.object_common.handle.is_some());
assert!(actual.start_autnum.is_some());
assert!(actual.end_autnum.is_some());
assert!(actual.name.is_some());
assert!(actual.autnum_type.is_some());
assert!(actual.object_common.status.is_some());
assert!(actual.country.is_some());
assert!(actual.object_common.remarks.is_some());
assert!(actual.object_common.links.is_some());
assert!(actual.object_common.events.is_some());
assert!(actual.object_common.entities.is_some());
}
}

View file

@ -0,0 +1,51 @@
use serde::{Deserialize, Serialize};
use super::{Extension, ExtensionId, Notice, Notices, RdapConformance};
/// Holds those types that are common in all responses.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Common {
#[serde(rename = "rdapConformance")]
#[serde(skip_serializing_if = "Option::is_none")]
pub rdap_conformance: Option<RdapConformance>,
#[serde(skip_serializing_if = "Option::is_none")]
pub notices: Option<Notices>,
}
#[buildstructor::buildstructor]
impl Common {
#[builder(entry = "level0", visibility = "pub(crate)")]
fn new_level0(mut extensions: Vec<Extension>, notices: Option<Vec<Notice>>) -> Self {
let mut standard_extensions = vec![ExtensionId::RdapLevel0.to_extension()];
extensions.append(&mut standard_extensions);
Self {
rdap_conformance: Some(extensions),
notices,
}
}
}
/// Empty Extensions.
static EMPTY_EXTENSIONS: Vec<Extension> = vec![];
/// Empty Notices.
static EMPTY_NOTICES: Vec<Notice> = vec![];
/// Convience methods for fields in [Common].
pub trait CommonFields {
/// Getter for [Common].
fn common(&self) -> &Common;
/// Getter for Vec of RDAP extensions.
fn extensions(&self) -> &Vec<Extension> {
self.common()
.rdap_conformance
.as_ref()
.unwrap_or(&EMPTY_EXTENSIONS)
}
/// Getter for Vec of Notices.
fn notices(&self) -> &Vec<Notice> {
self.common().notices.as_ref().unwrap_or(&EMPTY_NOTICES)
}
}

View file

@ -0,0 +1,986 @@
//! RDAP Domain Object Class
use {
crate::prelude::{Common, Extension, ObjectCommon},
buildstructor::Builder,
serde::{Deserialize, Serialize},
};
use super::{
lenient::{Boolish, Numberish},
nameserver::Nameserver,
network::Network,
to_opt_vec, to_opt_vectorstringish,
types::{Events, Link, Links, PublicIds},
CommonFields, Entity, Event, GetSelfLink, Notice, ObjectCommonFields, Port43, PublicId, Remark,
SelfLink, ToChild, ToResponse, VectorStringish, EMPTY_VEC_STRING,
};
/// Represents an RDAP variant name.
#[derive(Serialize, Deserialize, Builder, Clone, Debug, PartialEq, Eq)]
pub struct VariantName {
#[serde(rename = "ldhName")]
#[serde(skip_serializing_if = "Option::is_none")]
pub ldh_name: Option<String>,
#[serde(rename = "unicodeName")]
#[serde(skip_serializing_if = "Option::is_none")]
pub unicode_name: Option<String>,
}
impl VariantName {
/// Convenience method.
pub fn ldh_name(&self) -> Option<&str> {
self.ldh_name.as_deref()
}
/// Convenience method.
pub fn unicode_name(&self) -> Option<&str> {
self.unicode_name.as_deref()
}
}
/// Represents an RDAP IDN variant.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Variant {
#[serde(rename = "relation")]
#[serde(skip_serializing_if = "Option::is_none")]
pub relations: Option<VectorStringish>,
#[serde(rename = "idnTable")]
#[serde(skip_serializing_if = "Option::is_none")]
pub idn_table: Option<String>,
#[serde(rename = "variantNames")]
#[serde(skip_serializing_if = "Option::is_none")]
pub variant_names: Option<Vec<VariantName>>,
}
static EMPTY_VARIANT_NAMES: Vec<VariantName> = vec![];
#[buildstructor::buildstructor]
impl Variant {
#[builder(visibility = "pub")]
fn new(
relations: Vec<String>,
idn_table: Option<String>,
variant_names: Vec<VariantName>,
) -> Self {
Self {
relations: to_opt_vectorstringish(relations),
idn_table,
variant_names: to_opt_vec(variant_names),
}
}
/// Convenience method to get relations.
pub fn relations(&self) -> &Vec<String> {
self.relations
.as_ref()
.map(|v| v.vec())
.unwrap_or(&EMPTY_VEC_STRING)
}
/// Convenience method to get variant names.
pub fn variant_names(&self) -> &Vec<VariantName> {
self.variant_names.as_ref().unwrap_or(&EMPTY_VARIANT_NAMES)
}
/// Convenience method.
pub fn idn_table(&self) -> Option<&str> {
self.idn_table.as_deref()
}
}
static EMPTY_LINKS: Vec<Link> = vec![];
static EMPTY_EVENTS: Vec<Event> = vec![];
/// Represents `dsData`.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct DsDatum {
#[serde(rename = "keyTag")]
#[serde(skip_serializing_if = "Option::is_none")]
pub key_tag: Option<Numberish<u32>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub algorithm: Option<Numberish<u8>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub digest: Option<String>,
#[serde(rename = "digestType")]
#[serde(skip_serializing_if = "Option::is_none")]
pub digest_type: Option<Numberish<u8>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub links: Option<Links>,
#[serde(skip_serializing_if = "Option::is_none")]
pub events: Option<Events>,
}
#[buildstructor::buildstructor]
impl DsDatum {
/// Builder for `dsData`
#[builder(visibility = "pub")]
fn new(
key_tag: Option<u32>,
algorithm: Option<u8>,
digest: Option<String>,
digest_type: Option<u8>,
links: Vec<Link>,
events: Vec<Event>,
) -> Self {
Self {
key_tag: key_tag.map(Numberish::<u32>::from),
algorithm: algorithm.map(Numberish::<u8>::from),
digest,
digest_type: digest_type.map(Numberish::<u8>::from),
links: to_opt_vec(links),
events: to_opt_vec(events),
}
}
/// Convenience method to get links.
pub fn links(&self) -> &Vec<Link> {
self.links.as_ref().unwrap_or(&EMPTY_LINKS)
}
/// Convenience method to get events.
pub fn events(&self) -> &Vec<Event> {
self.events.as_ref().unwrap_or(&EMPTY_EVENTS)
}
/// Returns a u32 if it was given, otherwise None.
pub fn key_tag(&self) -> Option<u32> {
self.key_tag.as_ref().and_then(|n| n.as_u32())
}
/// Returns a u8 if it was given, otherwise None.
pub fn digest_type(&self) -> Option<u8> {
self.digest_type.as_ref().and_then(|n| n.as_u8())
}
/// Convenience method.
pub fn digest(&self) -> Option<&str> {
self.digest.as_deref()
}
}
/// Represents `keyData`.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct KeyDatum {
#[serde(skip_serializing_if = "Option::is_none")]
pub flags: Option<Numberish<u16>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub protocol: Option<Numberish<u8>>,
#[serde(rename = "publicKey")]
#[serde(skip_serializing_if = "Option::is_none")]
pub public_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub algorithm: Option<Numberish<u8>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub links: Option<Links>,
#[serde(skip_serializing_if = "Option::is_none")]
pub events: Option<Events>,
}
#[buildstructor::buildstructor]
impl KeyDatum {
/// Builder for `keyData`
#[builder(visibility = "pub")]
fn new(
flags: Option<u16>,
protocol: Option<u8>,
public_key: Option<String>,
algorithm: Option<u8>,
links: Vec<Link>,
events: Vec<Event>,
) -> Self {
Self {
flags: flags.map(Numberish::<u16>::from),
protocol: protocol.map(Numberish::<u8>::from),
public_key,
algorithm: algorithm.map(Numberish::<u8>::from),
links: to_opt_vec(links),
events: to_opt_vec(events),
}
}
/// Convenience method to get links.
pub fn links(&self) -> &Vec<Link> {
self.links.as_ref().unwrap_or(&EMPTY_LINKS)
}
/// Convenience method to get events.
pub fn events(&self) -> &Vec<Event> {
self.events.as_ref().unwrap_or(&EMPTY_EVENTS)
}
/// Returns a u16 if it was given, otherwise None.
pub fn flags(&self) -> Option<u16> {
self.flags.as_ref().and_then(|n| n.as_u16())
}
/// Returns a u8 if it was given, otherwise None.
pub fn protocol(&self) -> Option<u8> {
self.protocol.as_ref().and_then(|n| n.as_u8())
}
/// Returns a u8 if it was given, otherwise None.
pub fn algorithm(&self) -> Option<u8> {
self.algorithm.as_ref().and_then(|n| n.as_u8())
}
/// Convenience method.
pub fn public_key(&self) -> Option<&str> {
self.public_key.as_deref()
}
}
static EMPTY_DS_DATA: Vec<DsDatum> = vec![];
static EMPTY_KEY_DATA: Vec<KeyDatum> = vec![];
/// Represents the DNSSEC information of a domain.
///
/// The following shows how to use the builders to
/// create a domain with secure DNS informaiton.
///
/// ```rust
/// use icann_rdap_common::prelude::*;
///
/// // Builds DNS security `keyData`.
/// let key_datum = KeyDatum::builder()
/// .flags(257)
/// .protocol(3)
/// .algorithm(8)
/// .public_key("AwEAAa6eDzronzjEDbT...Jg1M5N rBSPkuXpdFE=")
/// .build();
///
/// // Builds DNS security `dsData`.
/// let ds_datum = DsDatum::builder()
/// .algorithm(13)
/// .key_tag(20149)
/// .digest_type(2)
/// .digest("cf066bceadb799a27b62e3e82dc2e4da314c1807db98f13d82f0043b1418cf4e")
/// .build();
///
/// // Builds DNS security.
/// let secure_dns = SecureDns::builder()
/// .ds_data(ds_datum)
/// .key_data(key_datum)
/// .zone_signed(true)
/// .delegation_signed(false)
/// .max_sig_life(604800)
/// .build();
///
/// // Builds `domain` with DNS security.
/// let domain = Domain::builder()
/// .ldh_name("example.com")
/// .handle("EXAMPLE-DOMAIN")
/// .status("active")
/// .secure_dns(secure_dns)
/// .build();
/// ```
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct SecureDns {
#[serde(rename = "zoneSigned")]
#[serde(skip_serializing_if = "Option::is_none")]
pub zone_signed: Option<Boolish>,
#[serde(rename = "delegationSigned")]
#[serde(skip_serializing_if = "Option::is_none")]
pub delegation_signed: Option<Boolish>,
#[serde(rename = "maxSigLife")]
#[serde(skip_serializing_if = "Option::is_none")]
pub max_sig_life: Option<Numberish<u64>>,
#[serde(rename = "dsData")]
#[serde(skip_serializing_if = "Option::is_none")]
pub ds_data: Option<Vec<DsDatum>>,
#[serde(rename = "keyData")]
#[serde(skip_serializing_if = "Option::is_none")]
pub key_data: Option<Vec<KeyDatum>>,
}
#[buildstructor::buildstructor]
impl SecureDns {
/// Builder for `secureDNS`.
#[builder(visibility = "pub")]
fn new(
zone_signed: Option<bool>,
delegation_signed: Option<bool>,
max_sig_life: Option<u64>,
ds_datas: Vec<DsDatum>,
key_datas: Vec<KeyDatum>,
) -> Self {
Self {
zone_signed: zone_signed.map(Boolish::from),
delegation_signed: delegation_signed.map(Boolish::from),
max_sig_life: max_sig_life.map(Numberish::<u64>::from),
ds_data: to_opt_vec(ds_datas),
key_data: to_opt_vec(key_datas),
}
}
/// Convenience method to get ds data.
pub fn ds_data(&self) -> &Vec<DsDatum> {
self.ds_data.as_ref().unwrap_or(&EMPTY_DS_DATA)
}
/// Convenience method to get key data.
pub fn key_data(&self) -> &Vec<KeyDatum> {
self.key_data.as_ref().unwrap_or(&EMPTY_KEY_DATA)
}
/// Returns true if a truish value was given, otherwise false.
pub fn zone_signed(&self) -> bool {
self.zone_signed.as_ref().map_or(false, |b| b.into_bool())
}
/// Returns true if a truish value was given, otherwise false.
pub fn delegation_signed(&self) -> bool {
self.delegation_signed
.as_ref()
.map_or(false, |b| b.into_bool())
}
/// Returns max_sig_life as a u64 if it was given, otherwise None.
pub fn max_sig_life(&self) -> Option<u64> {
self.max_sig_life.as_ref().and_then(|n| n.as_u64())
}
}
static EMPTY_PUBLIC_IDS: Vec<PublicId> = vec![];
static EMPTY_NAMESERVERS: Vec<Nameserver> = vec![];
/// Represents an RDAP [domain](https://rdap.rcode3.com/protocol/object_classes.html#domain) response.
///
/// Using the builder is recommended to construct this structure as it
/// will fill-in many of the mandatory fields.
/// The following is an example.
///
/// ```rust
/// use icann_rdap_common::prelude::*;
///
/// let domain = Domain::builder()
/// .ldh_name("foo.example.com")
/// .handle("foo_example_com-1")
/// .status("active")
/// .build();
/// let c = serde_json::to_string_pretty(&domain).unwrap();
/// eprintln!("{c}");
/// ```
///
/// This will produce the following.
///
/// ```norust
/// {
/// "rdapConformance": [
/// "rdap_level_0"
/// ],
/// "objectClassName": "domain",
/// "handle": "foo_example_com-1",
/// "status": [
/// "active"
/// ],
/// "ldhName": "foo.example.com"
/// }
/// ```
///
/// Domains have many sub-structures that are also constructed
/// using builders, which may then be passed into a Domain
/// builder.
///
/// ```rust
/// use icann_rdap_common::prelude::*;
///
/// let nameservers = vec![
/// Nameserver::builder()
/// .ldh_name("ns1.example.com")
/// .address("127.0.0.1")
/// .build()
/// .unwrap(),
/// Nameserver::builder()
/// .ldh_name("ns2.example.com")
/// .build()
/// .unwrap(),
/// ];
///
/// let ds_datum = DsDatum::builder()
/// .algorithm(13)
/// .key_tag(20149)
/// .digest_type(2)
/// .digest("cf066bceadb799a27b62e3e82dc2e4da314c1807db98f13d82f0043b1418cf4e")
/// .build();
///
/// let secure_dns = SecureDns::builder()
/// .ds_data(ds_datum)
/// .zone_signed(true)
/// .delegation_signed(false)
/// .build();
///
/// let domain = Domain::builder()
/// .ldh_name("foo.example.com")
/// .handle("foo_example_com-3")
/// .status("active")
/// .nameservers(nameservers)
/// .secure_dns(secure_dns)
/// .build();
/// ```
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Domain {
#[serde(flatten)]
pub common: Common,
#[serde(flatten)]
pub object_common: ObjectCommon,
#[serde(rename = "ldhName")]
#[serde(skip_serializing_if = "Option::is_none")]
pub ldh_name: Option<String>,
#[serde(rename = "unicodeName")]
#[serde(skip_serializing_if = "Option::is_none")]
pub unicode_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub variants: Option<Vec<Variant>>,
#[serde(rename = "secureDNS")]
#[serde(skip_serializing_if = "Option::is_none")]
pub secure_dns: Option<SecureDns>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nameservers: Option<Vec<Nameserver>>,
#[serde(rename = "publicIds")]
#[serde(skip_serializing_if = "Option::is_none")]
pub public_ids: Option<PublicIds>,
#[serde(skip_serializing_if = "Option::is_none")]
pub network: Option<Network>,
}
#[buildstructor::buildstructor]
impl Domain {
/// Builds a basic domain object.
///
/// ```rust
/// use icann_rdap_common::prelude::*;
///
/// let domain = Domain::builder()
/// .ldh_name("foo.example.com")
/// .handle("foo_example_com-1")
/// .status("active")
/// .build();
/// ```
#[builder(visibility = "pub")]
#[allow(clippy::too_many_arguments)]
fn new<T: Into<String>>(
ldh_name: T,
unicode_name: Option<String>,
nameservers: Vec<Nameserver>,
handle: Option<String>,
remarks: Vec<Remark>,
links: Vec<Link>,
events: Vec<Event>,
statuses: Vec<String>,
port_43: Option<Port43>,
entities: Vec<Entity>,
notices: Vec<Notice>,
public_ids: Vec<PublicId>,
secure_dns: Option<SecureDns>,
variants: Vec<Variant>,
network: Option<Network>,
extensions: Vec<Extension>,
redacted: Option<Vec<crate::response::redacted::Redacted>>,
) -> Self {
Self {
common: Common::level0()
.extensions(extensions)
.and_notices(to_opt_vec(notices))
.build(),
object_common: ObjectCommon::domain()
.and_handle(handle)
.and_remarks(to_opt_vec(remarks))
.and_links(to_opt_vec(links))
.and_events(to_opt_vec(events))
.status(statuses)
.and_port_43(port_43)
.and_entities(to_opt_vec(entities))
.and_redacted(redacted)
.build(),
ldh_name: Some(ldh_name.into()),
unicode_name,
variants: to_opt_vec(variants),
secure_dns,
nameservers: to_opt_vec(nameservers),
public_ids: to_opt_vec(public_ids),
network,
}
}
/// Builds an IDN object.
///
/// ```rust
/// use icann_rdap_common::prelude::*;
///
/// let domain = Domain::idn()
/// .unicode_name("foo.example.com")
/// .handle("foo_example_com-1")
/// .status("active")
/// .build();
/// ```
#[builder(entry = "idn", visibility = "pub")]
#[allow(clippy::too_many_arguments)]
fn new_idn<T: Into<String>>(
ldh_name: Option<String>,
unicode_name: T,
nameservers: Vec<Nameserver>,
handle: Option<String>,
remarks: Vec<Remark>,
links: Vec<Link>,
events: Vec<Event>,
statuses: Vec<String>,
port_43: Option<Port43>,
entities: Vec<Entity>,
notices: Vec<Notice>,
public_ids: Vec<PublicId>,
secure_dns: Option<SecureDns>,
variants: Vec<Variant>,
network: Option<Network>,
extensions: Vec<Extension>,
) -> Self {
Self {
common: Common::level0()
.extensions(extensions)
.and_notices(to_opt_vec(notices))
.build(),
object_common: ObjectCommon::domain()
.and_handle(handle)
.and_remarks(to_opt_vec(remarks))
.and_links(to_opt_vec(links))
.and_events(to_opt_vec(events))
.status(statuses)
.and_port_43(port_43)
.and_entities(to_opt_vec(entities))
.build(),
ldh_name,
unicode_name: Some(unicode_name.into()),
variants: to_opt_vec(variants),
secure_dns,
nameservers: to_opt_vec(nameservers),
public_ids: to_opt_vec(public_ids),
network,
}
}
/// Convenience method to get the public IDs.
pub fn public_ids(&self) -> &Vec<PublicId> {
self.public_ids.as_ref().unwrap_or(&EMPTY_PUBLIC_IDS)
}
/// Convenience method to get the nameservers.
pub fn nameservers(&self) -> &Vec<Nameserver> {
self.nameservers.as_ref().unwrap_or(&EMPTY_NAMESERVERS)
}
/// Convenience method.
pub fn ldh_name(&self) -> Option<&str> {
self.ldh_name.as_deref()
}
/// Convenience method.
pub fn unicode_name(&self) -> Option<&str> {
self.unicode_name.as_deref()
}
}
impl ToResponse for Domain {
fn to_response(self) -> super::RdapResponse {
super::RdapResponse::Domain(Box::new(self))
}
}
impl GetSelfLink for Domain {
fn get_self_link(&self) -> Option<&Link> {
self.object_common.get_self_link()
}
}
impl SelfLink for Domain {
fn set_self_link(mut self, link: Link) -> Self {
self.object_common = self.object_common.set_self_link(link);
self
}
}
impl ToChild for Domain {
fn to_child(mut self) -> Self {
self.common = Common {
rdap_conformance: None,
notices: None,
};
self
}
}
impl CommonFields for Domain {
fn common(&self) -> &Common {
&self.common
}
}
impl ObjectCommonFields for Domain {
fn object_common(&self) -> &ObjectCommon {
&self.object_common
}
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use crate::response::{types::Link, SelfLink};
use super::Domain;
#[test]
fn GIVEN_domain_WHEN_deserialize_THEN_success() {
// GIVEN
let expected = r#"
{
"objectClassName" : "domain",
"handle" : "XXXX",
"ldhName" : "xn--fo-5ja.example",
"unicodeName" : "fóo.example",
"variants" :
[
{
"relation" : [ "registered", "conjoined" ],
"variantNames" :
[
{
"ldhName" : "xn--fo-cka.example",
"unicodeName" : "fõo.example"
},
{
"ldhName" : "xn--fo-fka.example",
"unicodeName" : "föo.example"
}
]
},
{
"relation" : [ "unregistered", "registration restricted" ],
"idnTable": ".EXAMPLE Swedish",
"variantNames" :
[
{
"ldhName": "xn--fo-8ja.example",
"unicodeName" : "fôo.example"
}
]
}
],
"status" : [ "locked", "transfer prohibited" ],
"publicIds":[
{
"type":"ENS_Auth ID",
"identifier":"1234567890"
}
],
"nameservers" :
[
{
"objectClassName" : "nameserver",
"handle" : "XXXX",
"ldhName" : "ns1.example.com",
"status" : [ "active" ],
"ipAddresses" :
{
"v6": [ "2001:db8::123", "2001:db8::124" ],
"v4": [ "192.0.2.1", "192.0.2.2" ]
},
"remarks" :
[
{
"description" :
[
"She sells sea shells down by the sea shore.",
"Originally written by Terry Sullivan."
]
}
],
"links" :
[
{
"value" : "https://example.net/nameserver/ns1.example.com",
"rel" : "self",
"href" : "https://example.net/nameserver/ns1.example.com",
"type" : "application/rdap+json"
}
],
"events" :
[
{
"eventAction" : "registration",
"eventDate" : "1990-12-31T23:59:59Z"
},
{
"eventAction" : "last changed",
"eventDate" : "1991-12-31T23:59:59Z"
}
]
},
{
"objectClassName" : "nameserver",
"handle" : "XXXX",
"ldhName" : "ns2.example.com",
"status" : [ "active" ],
"ipAddresses" :
{
"v6" : [ "2001:db8::125", "2001:db8::126" ],
"v4" : [ "192.0.2.3", "192.0.2.4" ]
},
"remarks" :
[
{
"description" :
[
"She sells sea shells down by the sea shore.",
"Originally written by Terry Sullivan."
]
}
],
"links" :
[
{
"value" : "https://example.net/nameserver/ns2.example.com",
"rel" : "self",
"href" : "https://example.net/nameserver/ns2.example.com",
"type" : "application/rdap+json"
}
],
"events" :
[
{
"eventAction" : "registration",
"eventDate" : "1990-12-31T23:59:59Z"
},
{
"eventAction" : "last changed",
"eventDate" : "1991-12-31T23:59:59Z"
}
]
}
],
"secureDNS":
{
"zoneSigned": true,
"delegationSigned": true,
"maxSigLife": 604800,
"keyData":
[
{
"flags": 257,
"protocol": 3,
"algorithm": 8,
"publicKey": "AwEAAa6eDzronzjEDbT...Jg1M5N rBSPkuXpdFE=",
"events":
[
{
"eventAction": "last changed",
"eventDate": "2012-07-23T05:15:47Z"
}
]
}
]
},
"remarks" :
[
{
"description" :
[
"She sells sea shells down by the sea shore.",
"Originally written by Terry Sullivan."
]
}
],
"links" :
[
{
"value": "https://example.net/domain/xn--fo-5ja.example",
"rel" : "self",
"href" : "https://example.net/domain/xn--fo-5ja.example",
"type" : "application/rdap+json"
}
],
"port43" : "whois.example.net",
"events" :
[
{
"eventAction" : "registration",
"eventDate" : "1990-12-31T23:59:59Z"
},
{
"eventAction" : "last changed",
"eventDate" : "1991-12-31T23:59:59Z",
"eventActor" : "joe@example.com"
},
{
"eventAction" : "transfer",
"eventDate" : "1991-12-31T23:59:59Z",
"eventActor" : "joe@example.com"
},
{
"eventAction" : "expiration",
"eventDate" : "2016-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"
]
]
],
"status" : [ "validated", "locked" ],
"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"
}
]
}
]
}
"#;
// WHEN
let actual = serde_json::from_str::<Domain>(expected);
// THEN
let actual = actual.unwrap();
assert_eq!(actual.object_common.object_class_name, "domain");
assert!(actual.object_common.handle.is_some());
assert!(actual.ldh_name.is_some());
assert!(actual.unicode_name.is_some());
assert!(actual.variants.is_some());
assert!(actual.public_ids.is_some());
assert!(actual.object_common.remarks.is_some());
assert!(actual.object_common.links.is_some());
assert!(actual.object_common.events.is_some());
assert!(actual.object_common.port_43.is_some());
assert!(actual.object_common.entities.is_some());
assert!(actual.secure_dns.is_some());
}
#[test]
fn GIVEN_no_self_links_WHEN_set_self_link_THEN_link_is_only_one() {
// GIVEN
let mut domain = Domain::builder()
.ldh_name("foo.example")
.link(
Link::builder()
.href("http://bar.example")
.value("http://bar.example")
.rel("unknown")
.build(),
)
.build();
// WHEN
domain = domain.set_self_link(
Link::builder()
.href("http://foo.example")
.value("http://foo.example")
.rel("unknown")
.build(),
);
// THEN
assert_eq!(
domain
.object_common
.links
.expect("links are empty")
.iter()
.filter(|link| link.is_relation("self"))
.count(),
1
);
}
}

View file

@ -0,0 +1,420 @@
//! Entity object class.
use {
crate::{
contact::Contact,
prelude::{Common, Extension, ObjectCommon},
},
serde::{Deserialize, Serialize},
serde_json::Value,
strum_macros::{Display, EnumString},
};
use super::{
autnum::Autnum,
network::Network,
to_opt_vec, to_opt_vectorstringish,
types::{Events, Link, PublicIds},
CommonFields, Event, GetSelfLink, Notice, ObjectCommonFields, Port43, PublicId, Remark,
SelfLink, ToChild, ToResponse, VectorStringish, EMPTY_VEC_STRING,
};
/// Represents an RDAP [entity](https://rdap.rcode3.com/protocol/object_classes.html#entity) response.
///
/// Use of the builder is recommended when constructing this structure as it
/// will fill-in the mandatory fields.
/// The following is an example.
///
/// ```rust
/// use icann_rdap_common::prelude::*;
///
/// let contact = Contact::builder()
/// .kind("individual")
/// .full_name("Bob Smurd")
/// .build();
///
/// let entity = Entity::builder()
/// .handle("foo_example_com-1")
/// .status("active")
/// .role("registrant")
/// .contact(contact)
/// .build();
/// let c = serde_json::to_string_pretty(&entity).unwrap();
/// eprintln!("{c}");
/// ```
///
/// This will produce the following.
///
/// ```norust
/// {
/// "rdapConformance": [
/// "rdap_level_0"
/// ],
/// "objectClassName": "entity",
/// "handle": "foo_example_com-1",
/// "status": [
/// "active"
/// ],
/// "vcardArray": [
/// "vcard",
/// [
/// [
/// "version",
/// {},
/// "text",
/// "4.0"
/// ],
/// [
/// "fn",
/// {},
/// "text",
/// "Bob Smurd"
/// ],
/// [
/// "kind",
/// {},
/// "text",
/// "individual"
/// ]
/// ]
/// ],
/// "roles": [
/// "registrant"
/// ]
/// }
/// ```
///
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Entity {
#[serde(flatten)]
pub common: Common,
#[serde(flatten)]
pub object_common: ObjectCommon,
#[serde(rename = "vcardArray")]
#[serde(skip_serializing_if = "Option::is_none")]
pub vcard_array: Option<Vec<Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub roles: Option<VectorStringish>,
#[serde(rename = "publicIds")]
#[serde(skip_serializing_if = "Option::is_none")]
pub public_ids: Option<PublicIds>,
#[serde(rename = "asEventActor")]
#[serde(skip_serializing_if = "Option::is_none")]
pub as_event_actor: Option<Events>,
#[serde(skip_serializing_if = "Option::is_none")]
pub autnums: Option<Vec<Autnum>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub networks: Option<Vec<Network>>,
}
static EMPTY_PUBLIC_IDS: Vec<PublicId> = vec![];
static EMPTY_AS_EVENT_ACTORS: Vec<Event> = vec![];
static EMPTY_AUTNUMS: Vec<Autnum> = vec![];
static EMPTY_NETWORKS: Vec<Network> = vec![];
#[buildstructor::buildstructor]
impl Entity {
/// Builds a basic autnum object.
///
/// ```rust
/// use icann_rdap_common::prelude::*;
///
/// let contact = Contact::builder()
/// .kind("individual")
/// .full_name("Bob Smurd")
/// .build();
///
/// let entity = Entity::builder()
/// .handle("foo_example_com-1")
/// .status("active")
/// .role("registrant")
/// .contact(contact)
/// .build();
/// ```
#[builder(visibility = "pub")]
#[allow(clippy::too_many_arguments)]
fn new<T: Into<String>>(
handle: T,
remarks: Vec<Remark>,
links: Vec<Link>,
events: Vec<Event>,
statuses: Vec<String>,
port_43: Option<Port43>,
entities: Vec<Entity>,
as_event_actors: Vec<Event>,
contact: Option<Contact>,
roles: Vec<String>,
public_ids: Vec<PublicId>,
notices: Vec<Notice>,
networks: Vec<Network>,
autnums: Vec<Autnum>,
extensions: Vec<Extension>,
redacted: Option<Vec<crate::response::redacted::Redacted>>,
) -> Self {
Self {
common: Common::level0()
.extensions(extensions)
.and_notices(to_opt_vec(notices))
.build(),
object_common: ObjectCommon::entity()
.handle(handle.into())
.and_remarks(to_opt_vec(remarks))
.and_links(to_opt_vec(links))
.and_events(to_opt_vec(events))
.status(statuses)
.and_port_43(port_43)
.and_entities(to_opt_vec(entities))
.and_redacted(redacted)
.build(),
vcard_array: contact.map(|c| c.to_vcard()),
roles: to_opt_vectorstringish(roles),
public_ids: to_opt_vec(public_ids),
as_event_actor: to_opt_vec(as_event_actors),
autnums: to_opt_vec(autnums),
networks: to_opt_vec(networks),
}
}
/// Convenience method to get a [Contact] from the impentrable vCard.
pub fn contact(&self) -> Option<Contact> {
let vcard = self.vcard_array.as_ref()?;
Contact::from_vcard(vcard)
}
/// Convenience method to get the roles.
pub fn roles(&self) -> &Vec<String> {
self.roles
.as_ref()
.map(|v| v.vec())
.unwrap_or(&EMPTY_VEC_STRING)
}
/// Convenience method to get the public IDs.
pub fn public_ids(&self) -> &Vec<PublicId> {
self.public_ids.as_ref().unwrap_or(&EMPTY_PUBLIC_IDS)
}
/// Convenience method to get the events this entity acted on.
pub fn as_event_actors(&self) -> &Vec<Event> {
self.as_event_actor
.as_ref()
.unwrap_or(&EMPTY_AS_EVENT_ACTORS)
}
/// Convenience method to get the autnums.
pub fn autnums(&self) -> &Vec<Autnum> {
self.autnums.as_ref().unwrap_or(&EMPTY_AUTNUMS)
}
/// Convenience method to get the networks.
pub fn networks(&self) -> &Vec<Network> {
self.networks.as_ref().unwrap_or(&EMPTY_NETWORKS)
}
}
impl ToResponse for Entity {
fn to_response(self) -> super::RdapResponse {
super::RdapResponse::Entity(Box::new(self))
}
}
impl GetSelfLink for Entity {
fn get_self_link(&self) -> Option<&Link> {
self.object_common.get_self_link()
}
}
impl SelfLink for Entity {
fn set_self_link(mut self, link: Link) -> Self {
self.object_common = self.object_common.set_self_link(link);
self
}
}
impl ToChild for Entity {
fn to_child(mut self) -> Self {
self.common = Common {
rdap_conformance: None,
notices: None,
};
self
}
}
impl CommonFields for Entity {
fn common(&self) -> &Common {
&self.common
}
}
impl ObjectCommonFields for Entity {
fn object_common(&self) -> &ObjectCommon {
&self.object_common
}
}
/// IANA registered roles for entities.
#[derive(PartialEq, Eq, Debug, EnumString, Display)]
#[strum(serialize_all = "lowercase")]
pub enum EntityRole {
Registrant,
Technical,
Administrative,
Abuse,
Billing,
Registrar,
Reseller,
Sponsor,
Proxy,
Notifications,
Noc,
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use super::Entity;
#[test]
fn GIVEN_entity_WHEN_deserialize_THEN_success() {
// GIVEN
let expected = r#"
{
"objectClassName" : "entity",
"handle":"XXXX",
"vcardArray":[
"vcard",
[
["version", {}, "text", "4.0"],
["fn", {}, "text", "Joe User"],
["n", {}, "text",
["User", "Joe", "", "", ["ing. jr", "M.Sc."]]
],
["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"
]
],
["adr",
{
"type":"home",
"label":"123 Maple Ave\nSuite 90001\nVancouver\nBC\n1239\n"
},
"text",
[
"", "", "", "", "", "", ""
]
],
["tel",
{
"type":["work", "voice"],
"pref":"1"
},
"uri",
"tel:+1-555-555-1234;ext=102"
],
["tel",
{ "type":["work", "cell", "voice", "video", "text"] },
"uri",
"tel:+1-555-555-4321"
],
["email",
{ "type":"work" },
"text",
"joe.user@example.com"
],
["geo", {
"type":"work"
}, "uri", "geo:46.772673,-71.282945"],
["key",
{ "type":"work" },
"uri",
"https://www.example.com/joe.user/joe.asc"
],
["tz", {},
"utc-offset", "-05:00"],
["url", { "type":"home" },
"uri", "https://example.org"]
]
],
"roles":[ "registrar" ],
"publicIds":[
{
"type":"IANA Registrar ID",
"identifier":"1"
}
],
"remarks":[
{
"description":[
"She sells sea shells down by the sea shore.",
"Originally written by Terry Sullivan."
]
}
],
"links":[
{
"value":"https://example.com/entity/XXXX",
"rel":"self",
"href":"https://example.com/entity/XXXX",
"type" : "application/rdap+json"
}
],
"events":[
{
"eventAction":"registration",
"eventDate":"1990-12-31T23:59:59Z"
}
],
"asEventActor":[
{
"eventAction":"last changed",
"eventDate":"1991-12-31T23:59:59Z"
}
]
}
"#;
// WHEN
let actual = serde_json::from_str::<Entity>(expected);
// THEN
let actual = actual.unwrap();
assert_eq!(actual.object_common.object_class_name, "entity");
assert!(actual.object_common.handle.is_some());
assert!(actual.vcard_array.is_some());
assert!(actual.roles.is_some());
assert!(actual.public_ids.is_some());
assert!(actual.object_common.remarks.is_some());
assert!(actual.object_common.links.is_some());
assert!(actual.object_common.events.is_some());
assert!(actual.as_event_actor.is_some());
}
}

View file

@ -0,0 +1,119 @@
//! RFC 9083 Error
use {
crate::prelude::Extension,
serde::{Deserialize, Serialize},
};
use crate::media_types::RDAP_MEDIA_TYPE;
use super::{
types::{Link, Notice, NoticeOrRemark},
Common, CommonFields, ToResponse,
};
/// Represents an error response from an RDAP server.
///
/// This structure represents the JSON returned by an RDAP server
/// describing an error.
/// See [RFC 9083, Section 6](https://datatracker.ietf.org/doc/html/rfc9083#name-error-response-body).
///
/// Do not confuse this with [crate::response::RdapResponseError].
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Rfc9083Error {
#[serde(flatten)]
pub common: Common,
#[serde(rename = "errorCode")]
pub error_code: u16,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<Vec<String>>,
}
#[buildstructor::buildstructor]
impl Rfc9083Error {
/// Creates a new RFC 9083 Error for a specific HTTP error code.
#[builder(visibility = "pub")]
fn new(error_code: u16, notices: Vec<Notice>, extensions: Vec<Extension>) -> Self {
let notices = (!notices.is_empty()).then_some(notices);
Self {
common: Common::level0()
.extensions(extensions)
.and_notices(notices)
.build(),
error_code,
title: None,
description: None,
}
}
/// Creates an RFC 9083 error for an HTTP redirect.
#[builder(entry = "redirect", visibility = "pub")]
fn new_redirect(url: String, extensions: Vec<Extension>) -> Self {
let links = vec![Link::builder()
.href(&url)
.value(&url)
.media_type(RDAP_MEDIA_TYPE)
.rel("related")
.build()];
let notices = vec![Notice(NoticeOrRemark::builder().links(links).build())];
Self {
common: Common::level0()
.extensions(extensions)
.notices(notices)
.build(),
error_code: 307,
title: None,
description: None,
}
}
pub fn is_redirect(&self) -> bool {
self.error_code > 299 && self.error_code < 400
}
}
impl CommonFields for Rfc9083Error {
fn common(&self) -> &Common {
&self.common
}
}
impl ToResponse for Rfc9083Error {
fn to_response(self) -> super::RdapResponse {
super::RdapResponse::ErrorResponse(Box::new(self))
}
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use super::Rfc9083Error;
#[test]
fn GIVEN_error_code_301_WHEN_is_redirect_THEN_true() {
// GIVEN
let e = Rfc9083Error::redirect().url("https://foo.example").build();
// WHEN
let actual = e.is_redirect();
// THEN
assert!(actual);
}
#[test]
fn GIVEN_error_code_404_WHEN_is_redirect_THEN_false() {
// GIVEN
let e = Rfc9083Error::builder().error_code(404).build();
// WHEN
let actual = e.is_redirect();
// THEN
assert!(!actual);
}
}

View file

@ -0,0 +1,40 @@
//! Server Help Response.
use {
crate::prelude::{Extension, Notice},
serde::{Deserialize, Serialize},
};
use super::{to_opt_vec, Common, CommonFields, ToResponse};
/// Represents an RDAP help response.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Help {
#[serde(flatten)]
pub common: Common,
}
#[buildstructor::buildstructor]
impl Help {
/// Builds a basic help response.
#[builder(visibility = "pub")]
fn new(notices: Vec<Notice>, extensions: Vec<Extension>) -> Self {
Self {
common: Common::level0()
.extensions(extensions)
.and_notices(to_opt_vec(notices))
.build(),
}
}
}
impl CommonFields for Help {
fn common(&self) -> &Common {
&self.common
}
}
impl ToResponse for Help {
fn to_response(self) -> super::RdapResponse {
super::RdapResponse::Help(Box::new(self))
}
}

View file

@ -0,0 +1,626 @@
//! Types for more lenient processing of invalid RDAP
use std::{fmt::Display, marker::PhantomData, str::FromStr};
use {
serde::{de::Visitor, Deserialize, Deserializer, Serialize},
serde_json::Number,
};
use crate::check::StringListCheck;
/// A type that is suppose to be a vector of strings.
///
/// Provides a choice between a string or a vector of strings for deserialization.
///
/// This type is provided to be lenient with misbehaving RDAP servers that
/// serve a string when they are suppose to be serving an array of
/// strings.
///
/// Use one of the From methods for construction.
/// ```rust
/// use icann_rdap_common::prelude::*;
///
/// let v = VectorStringish::from(vec!["one".to_string(), "two".to_string()]);
///
/// // or
///
/// let v = VectorStringish::from("one".to_string());
/// ````
#[derive(Serialize, Clone, Debug, PartialEq, Eq)]
#[serde(transparent)]
pub struct VectorStringish {
vec: Vec<String>,
#[serde(skip)]
is_string: bool,
}
impl<'de> Deserialize<'de> for VectorStringish {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(VectorStringishVisitor)
}
}
struct VectorStringishVisitor;
impl<'de> Visitor<'de> for VectorStringishVisitor {
type Value = VectorStringish;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("expected an array of strings")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(VectorStringish {
vec: vec![v.to_owned()],
is_string: true,
})
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let mut v = vec![];
loop {
let n = seq.next_element()?;
if let Some(s) = n {
v.push(s);
} else {
break;
}
}
Ok(VectorStringish {
vec: v,
is_string: false,
})
}
}
impl From<String> for VectorStringish {
fn from(value: String) -> Self {
VectorStringish {
vec: vec![value],
is_string: false,
}
}
}
impl From<&str> for VectorStringish {
fn from(value: &str) -> Self {
VectorStringish {
vec: vec![value.to_owned()],
is_string: false,
}
}
}
impl From<Vec<String>> for VectorStringish {
fn from(value: Vec<String>) -> Self {
VectorStringish {
vec: value,
is_string: false,
}
}
}
impl From<VectorStringish> for Vec<String> {
fn from(value: VectorStringish) -> Self {
value.vec
}
}
impl From<&VectorStringish> for Vec<String> {
fn from(value: &VectorStringish) -> Self {
value.vec.to_owned()
}
}
impl VectorStringish {
/// Consumes and converts it to a `Vec<String>`.
pub fn into_vec(self) -> Vec<String> {
self.vec
}
/// Gets a reference to the underlying `Vec<String>`.
pub fn vec(&self) -> &Vec<String> {
&self.vec
}
/// Returns true if the deserialization was as a string.
pub fn is_string(&self) -> bool {
self.is_string
}
}
impl StringListCheck for VectorStringish {
fn is_empty_or_any_empty_or_whitespace(&self) -> bool {
self.vec().is_empty_or_any_empty_or_whitespace()
}
fn is_ldh_string_list(&self) -> bool {
self.vec().is_ldh_string_list()
}
}
/// Returns `Some(VectorStringish)` if the vector is not empty, otherwise `None`.
pub fn to_opt_vectorstringish(vec: Vec<String>) -> Option<VectorStringish> {
(!vec.is_empty()).then_some(VectorStringish::from(vec))
}
pub(crate) static EMPTY_VEC_STRING: Vec<String> = vec![];
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(untagged)]
enum BoolishInner {
/// Valid RDAP.
Bool(bool),
/// Invalide RDAP.
String(String),
}
/// A type that is suppose to be a boolean.
///
/// Provides a choice between a boolean or a string representation of a boolean for deserialization.
///
/// This type is provided to be lenient with misbehaving RDAP servers that
/// serve a string representation of a boolean when they are suppose to be serving a boolean
///
/// Use one of the From methods for construction.
/// ```rust
/// use icann_rdap_common::prelude::*;
///
/// let v = Boolish::from(true);
/// ````
///
/// When converting from a string (as would happen with deserialization),
/// the values "true", "t", "yes", and "y" (case-insensitive with whitespace trimmed)
/// will be true, all other values will be false.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(transparent)]
pub struct Boolish {
inner: BoolishInner,
}
impl From<bool> for Boolish {
fn from(value: bool) -> Self {
Boolish {
inner: BoolishInner::Bool(value),
}
}
}
impl Display for Boolish {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.into_bool())
}
}
impl Boolish {
/// Converts to a bool.
pub fn into_bool(&self) -> bool {
match &self.inner {
BoolishInner::Bool(value) => *value,
BoolishInner::String(value) => Boolish::is_true(value),
}
}
/// Returns true if the deserialization was as a string.
pub fn is_string(&self) -> bool {
match &self.inner {
BoolishInner::Bool(_) => false,
BoolishInner::String(_) => true,
}
}
fn is_true(value: &str) -> bool {
let s = value.trim().to_lowercase();
s == "true" || s == "t" || s == "yes" || s == "y"
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(untagged)]
enum NumberishInner {
/// Valid RDAP.
Number(Number),
/// Invalide RDAP.
String(String),
}
/// A type that is suppose to be a number.
///
/// Provides a choice between a number or a string representation of a number for deserialization.
///
/// This type is provided to be lenient with misbehaving RDAP servers that
/// serve a string representation of a number when they are suppose to be serving a number.
///
/// Use the From methods for construction.
/// ```rust
/// use icann_rdap_common::prelude::*;
///
/// let v = Numberish::from(123);
/// ````
///
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(transparent)]
pub struct Numberish<T> {
inner: NumberishInner,
phatom: PhantomData<T>,
}
impl<T> From<T> for Numberish<T>
where
Number: From<T>,
{
fn from(value: T) -> Self {
Numberish {
inner: NumberishInner::Number(Number::from(value)),
phatom: PhantomData,
}
}
}
impl<T> Display for Numberish<T>
where
Number: From<T>,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
self.as_u64()
.map_or("RANGE_ERRROR".to_string(), |u| u.to_string())
)
}
}
impl<T> Numberish<T>
where
Number: From<T>,
{
/// Returns true if the deserialization was as a string.
pub fn is_string(&self) -> bool {
match &self.inner {
NumberishInner::Number(_) => false,
NumberishInner::String(_) => true,
}
}
/// If the `Number` is an integer, represent it as u64 if possible. Returns None otherwise.
pub fn as_u64(&self) -> Option<u64> {
match &self.inner {
NumberishInner::Number(n) => n.as_u64(),
NumberishInner::String(s) => Number::from_str(s).ok()?.as_u64(),
}
}
/// If the `Number` is an integer, represent it as u32 if possible. Returns None otherwise.
pub fn as_u32(&self) -> Option<u32> {
match &self.inner {
NumberishInner::Number(n) => n.as_u64()?.try_into().ok(),
NumberishInner::String(s) => Number::from_str(s).ok()?.as_u64()?.try_into().ok(),
}
}
/// If the `Number` is an integer, represent it as u16 if possible. Returns None otherwise.
pub fn as_u16(&self) -> Option<u16> {
match &self.inner {
NumberishInner::Number(n) => n.as_u64()?.try_into().ok(),
NumberishInner::String(s) => Number::from_str(s).ok()?.as_u64()?.try_into().ok(),
}
}
/// If the `Number` is an integer, represent it as u8 if possible. Returns None otherwise.
pub fn as_u8(&self) -> Option<u8> {
match &self.inner {
NumberishInner::Number(n) => n.as_u64()?.try_into().ok(),
NumberishInner::String(s) => Number::from_str(s).ok()?.as_u64()?.try_into().ok(),
}
}
}
#[cfg(test)]
mod tests {
use {
super::*,
serde_json::{from_str, to_string},
};
//
// VectorStringish tests
//
#[test]
fn test_vectorstringish_serialize_many() {
// GIVEN
let many = VectorStringish::from(vec!["one".to_string(), "two".to_string()]);
// WHEN
let serialized = to_string(&many).unwrap();
// THEN
assert_eq!(serialized, r#"["one","two"]"#);
}
#[test]
fn test_vectorstringish_serialize_one() {
// GIVEN
let one = VectorStringish::from("one".to_string());
// WHEN
let serialized = to_string(&one).unwrap();
// THEN
assert_eq!(serialized, r#"["one"]"#);
}
#[test]
fn test_vectorstringish_deserialize_many() {
// GIVEN
let json_str = r#"["one","two"]"#;
// WHEN
let deserialized: VectorStringish = from_str(json_str).unwrap();
// THEN
assert_eq!(
deserialized.vec(),
&vec!["one".to_string(), "two".to_string()]
);
// and THEN is not string
assert!(!deserialized.is_string())
}
#[test]
fn test_vectorstringish_deserialize_one() {
// GIVEN
let json_str = r#""one""#;
// WHEN
let deserialized: VectorStringish = from_str(json_str).unwrap();
// THEN
assert_eq!(deserialized.vec(), &vec!["one".to_string()]);
// and THEN is string
assert!(deserialized.is_string())
}
//
// Boolish tests
//
#[test]
fn test_boolish_serialize_bool() {
// GIVEN
let b = Boolish::from(true);
// WHEN
let serialized = to_string(&b).unwrap();
// THEN
assert_eq!(serialized, "true");
}
#[test]
fn test_boolish_deserialize_bool_true() {
// GIVEN
let json_str = "true";
// WHEN
let deserialized: Boolish = from_str(json_str).unwrap();
// THEN
assert!(deserialized.into_bool());
assert!(!deserialized.is_string());
}
#[test]
fn test_boolish_deserialize_bool_false() {
// GIVEN
let json_str = "false";
// WHEN
let deserialized: Boolish = from_str(json_str).unwrap();
// THEN
assert!(!deserialized.into_bool());
assert!(!deserialized.is_string());
}
#[test]
fn test_boolish_deserialize_string_true() {
// GIVEN
let json_str = r#""true""#;
// WHEN
let deserialized: Boolish = from_str(json_str).unwrap();
// THEN
assert!(deserialized.into_bool());
assert!(deserialized.is_string());
}
#[test]
fn test_boolish_deserialize_string_false() {
// GIVEN
let json_str = r#""false""#;
// WHEN
let deserialized: Boolish = from_str(json_str).unwrap();
// THEN
assert!(!deserialized.into_bool());
assert!(deserialized.is_string());
}
#[test]
fn test_boolish_is_true() {
// GIVEN various true values
let true_values = ["true", "t", "yes", "y", " True ", " T ", " Yes ", " Y "];
// THEN all are true
for value in true_values {
assert!(Boolish::is_true(value));
}
}
#[test]
fn test_boolish_is_false() {
// GIVEN various false values
let false_values = ["false", "f", "no", "n", "False", "blah", "1", "0", ""];
// THEN all are false
for value in false_values {
assert!(!Boolish::is_true(value));
}
}
#[test]
fn test_boolish_from_bool() {
assert!(Boolish::from(true).into_bool());
assert!(!Boolish::from(false).into_bool());
}
//
// Numberish Tests
//
#[test]
fn test_numberish_serialize_number() {
// GIVEN a Numberish from a number
let n = Numberish::<u32>::from(123);
// WHEN serialized
let serialized = to_string(&n).unwrap();
// THEN it is the correct string
assert_eq!(serialized, "123");
}
#[test]
fn test_numberish_deserialize_number() {
// GIVEN a JSON string representing a number
let json_str = "123";
// WHEN deserialized
let deserialized: Numberish<u32> = from_str(json_str).unwrap();
// THEN the value is correct and it's not a string
assert_eq!(deserialized.as_u32(), Some(123));
assert!(!deserialized.is_string());
}
#[test]
fn test_numberish_deserialize_string() {
// GIVEN a JSON string representing a number as a string
let json_str = r#""123""#;
// WHEN deserialized
let deserialized: Numberish<u32> = from_str(json_str).unwrap();
// THEN the value is correct and it's a string
assert_eq!(deserialized.as_u32(), Some(123));
assert!(deserialized.is_string());
}
#[test]
fn test_numberish_as_u64_number() {
// GIVEN a Numberish from a u64
let n = Numberish::from(123u64);
// WHEN as_u64 is called
let result = n.as_u64();
// THEN the result is Some(123)
assert_eq!(result, Some(123));
}
#[test]
fn test_numberish_as_u64_string_invalid() {
// GIVEN a Numberish from a string that does not represent a u64
let n = Numberish {
inner: NumberishInner::String("abc".to_string()),
phatom: PhantomData::<u64>,
};
// WHEN as_u64 is called
let result = n.as_u64();
// THEN the result is None
assert_eq!(result, None);
}
#[test]
fn test_numberish_as_smaller_types() {
// GIVEN a valid number
let n = Numberish::from(123u64);
// THEN smaller type conversions work
assert_eq!(n.as_u32(), Some(123));
assert_eq!(n.as_u16(), Some(123));
assert_eq!(n.as_u8(), Some(123));
// GIVEN a number too large
let n = Numberish::from(u32::MAX as u64 + 1);
// THEN smaller type conversions fail
assert_eq!(n.as_u32(), None);
assert_eq!(n.as_u16(), None);
assert_eq!(n.as_u8(), None);
// GIVEN a valid number string
let n = Numberish {
inner: NumberishInner::String("123".to_string()),
phatom: PhantomData::<u64>,
};
// THEN smaller type conversions work
assert_eq!(n.as_u32(), Some(123));
assert_eq!(n.as_u16(), Some(123));
assert_eq!(n.as_u8(), Some(123));
// GIVEN a number string too large
let n = Numberish {
inner: NumberishInner::String((u32::MAX as u64 + 1).to_string()),
phatom: PhantomData::<u64>,
};
// THEN smaller type conversions fail
assert_eq!(n.as_u32(), None);
}
#[test]
fn test_numberish_display_number() {
let n = Numberish::<u32>::from(123);
assert_eq!(format!("{}", n), "123");
}
#[test]
fn test_numberish_display_string_valid() {
let n = Numberish {
inner: NumberishInner::String("123".to_string()),
phatom: PhantomData::<u32>,
};
assert_eq!(format!("{}", n), "123");
}
#[test]
fn test_numberish_display_string_invalid() {
let n = Numberish {
inner: NumberishInner::String("abc".to_string()),
phatom: PhantomData::<u32>,
};
assert_eq!(format!("{}", n), "RANGE_ERRROR");
}
}

View file

@ -0,0 +1,652 @@
//! RDAP structures for parsing and creating RDAP responses.
use std::any::TypeId;
use {
cidr,
serde::{Deserialize, Serialize},
serde_json::Value,
strum_macros::Display,
thiserror::Error,
};
use crate::media_types::RDAP_MEDIA_TYPE;
#[doc(inline)]
pub use autnum::*;
#[doc(inline)]
pub use common::*;
#[doc(inline)]
pub use domain::*;
#[doc(inline)]
pub use entity::*;
#[doc(inline)]
pub use error::*;
#[doc(inline)]
pub use help::*;
#[doc(inline)]
pub use lenient::*;
#[doc(inline)]
pub use nameserver::*;
#[doc(inline)]
pub use network::*;
#[doc(inline)]
pub use obj_common::*;
#[doc(inline)]
pub use search::*;
#[doc(inline)]
pub use types::*;
pub(crate) mod autnum;
pub(crate) mod common;
pub(crate) mod domain;
pub(crate) mod entity;
pub(crate) mod error;
pub(crate) mod help;
pub(crate) mod lenient;
pub(crate) mod nameserver;
pub(crate) mod network;
pub(crate) mod obj_common;
pub mod redacted; // RFC 9537 is not a mainstream extension.
pub(crate) mod search;
pub(crate) mod types;
/// An error caused be processing an RDAP response.
///
/// This is caused because the JSON constituting the
/// RDAP response has a problem that cannot be overcome.
///
/// Do not confuse this with [Rfc9083Error].
#[derive(Debug, Error)]
pub enum RdapResponseError {
/// The JSON type is incorrect.
#[error("Wrong JSON type: {0}")]
WrongJsonType(String),
/// The type of RDAP response is unknown.
#[error("Unknown RDAP response.")]
UnknownRdapResponse,
/// An error has occurred parsing the JSON.
#[error(transparent)]
SerdeJson(#[from] serde_json::Error),
/// An error with parsing an IP address.
#[error(transparent)]
AddrParse(#[from] std::net::AddrParseError),
/// An error caused with parsing a CIDR address.
#[error(transparent)]
CidrParse(#[from] cidr::errors::NetworkParseError),
}
/// The various types of RDAP response.
///
/// It can be parsed from JSON using serde:
///
/// ```rust
/// use icann_rdap_common::response::RdapResponse;
///
/// let json = r#"
/// {
/// "objectClassName": "ip network",
/// "links": [
/// {
/// "value": "http://localhost:3000/rdap/ip/10.0.0.0/16",
/// "rel": "self",
/// "href": "http://localhost:3000/rdap/ip/10.0.0.0/16",
/// "type": "application/rdap+json"
/// }
/// ],
/// "events": [
/// {
/// "eventAction": "registration",
/// "eventDate": "2023-06-16T22:56:49.594173356+00:00"
/// },
/// {
/// "eventAction": "last changed",
/// "eventDate": "2023-06-16T22:56:49.594189140+00:00"
/// }
/// ],
/// "startAddress": "10.0.0.0",
/// "endAddress": "10.0.255.255",
/// "ipVersion": "v4"
/// }
/// "#;
///
/// let rdap: RdapResponse = serde_json::from_str(json).unwrap();
/// assert!(matches!(rdap, RdapResponse::Network(_)));
/// ```
#[derive(Serialize, Deserialize, Clone, Display, PartialEq, Debug)]
#[serde(untagged, try_from = "Value")]
pub enum RdapResponse {
// Object Classes
Entity(Box<Entity>),
Domain(Box<Domain>),
Nameserver(Box<Nameserver>),
Autnum(Box<Autnum>),
Network(Box<Network>),
// Search Results
DomainSearchResults(Box<DomainSearchResults>),
EntitySearchResults(Box<EntitySearchResults>),
NameserverSearchResults(Box<NameserverSearchResults>),
// Error
ErrorResponse(Box<Rfc9083Error>),
// Help
Help(Box<Help>),
// These are all boxed to keep the variant size alligned.
// While not completely necessary for all these variants today,
// this will prevent an API change in the future when new items
// are added to each variant when supporting future RDAP extensions.
}
impl TryFrom<Value> for RdapResponse {
type Error = RdapResponseError;
fn try_from(value: Value) -> Result<Self, Self::Error> {
let response = if let Some(object) = value.as_object() {
object
} else {
return Err(RdapResponseError::WrongJsonType(
"response is not an object".to_string(),
));
};
// if it has an objectClassName
if let Some(class_name) = response.get("objectClassName") {
if let Some(name_str) = class_name.as_str() {
return match name_str {
"domain" => Ok(serde_json::from_value::<Domain>(value)?.to_response()),
"entity" => Ok(serde_json::from_value::<Entity>(value)?.to_response()),
"nameserver" => Ok(serde_json::from_value::<Nameserver>(value)?.to_response()),
"autnum" => Ok(serde_json::from_value::<Autnum>(value)?.to_response()),
"ip network" => Ok(serde_json::from_value::<Network>(value)?.to_response()),
_ => Err(RdapResponseError::UnknownRdapResponse),
};
} else {
return Err(RdapResponseError::WrongJsonType(
"'objectClassName' is not a string".to_string(),
));
}
};
// else if it is a domain search result
if let Some(result) = response.get("domainSearchResults") {
if result.is_array() {
return Ok(serde_json::from_value::<DomainSearchResults>(value)?.to_response());
} else {
return Err(RdapResponseError::WrongJsonType(
"'domainSearchResults' is not an array".to_string(),
));
}
}
// else if it is a entity search result
if let Some(result) = response.get("entitySearchResults") {
if result.is_array() {
return Ok(serde_json::from_value::<EntitySearchResults>(value)?.to_response());
} else {
return Err(RdapResponseError::WrongJsonType(
"'entitySearchResults' is not an array".to_string(),
));
}
}
// else if it is a nameserver search result
if let Some(result) = response.get("nameserverSearchResults") {
if result.is_array() {
return Ok(serde_json::from_value::<NameserverSearchResults>(value)?.to_response());
} else {
return Err(RdapResponseError::WrongJsonType(
"'nameserverSearchResults' is not an array".to_string(),
));
}
}
// else if it has an errorCode
if let Some(result) = response.get("errorCode") {
if result.is_u64() {
return Ok(serde_json::from_value::<Rfc9083Error>(value)?.to_response());
} else {
return Err(RdapResponseError::WrongJsonType(
"'errorCode' is not an unsigned integer".to_string(),
));
}
}
// else if it has a notices then it is help response at this point
if let Some(result) = response.get("notices") {
if result.is_array() {
return Ok(serde_json::from_value::<Help>(value)?.to_response());
} else {
return Err(RdapResponseError::WrongJsonType(
"'notices' is not an array".to_string(),
));
}
}
Err(RdapResponseError::UnknownRdapResponse)
}
}
impl RdapResponse {
pub fn get_type(&self) -> TypeId {
match self {
Self::Entity(_) => TypeId::of::<Entity>(),
Self::Domain(_) => TypeId::of::<Domain>(),
Self::Nameserver(_) => TypeId::of::<Nameserver>(),
Self::Autnum(_) => TypeId::of::<Autnum>(),
Self::Network(_) => TypeId::of::<Network>(),
Self::DomainSearchResults(_) => TypeId::of::<DomainSearchResults>(),
Self::EntitySearchResults(_) => TypeId::of::<EntitySearchResults>(),
Self::NameserverSearchResults(_) => TypeId::of::<NameserverSearchResults>(),
Self::ErrorResponse(_) => TypeId::of::<crate::response::Rfc9083Error>(),
Self::Help(_) => TypeId::of::<Help>(),
}
}
pub fn get_links(&self) -> Option<&Links> {
match self {
Self::Entity(e) => e.object_common.links.as_ref(),
Self::Domain(d) => d.object_common.links.as_ref(),
Self::Nameserver(n) => n.object_common.links.as_ref(),
Self::Autnum(a) => a.object_common.links.as_ref(),
Self::Network(n) => n.object_common.links.as_ref(),
Self::DomainSearchResults(_)
| Self::EntitySearchResults(_)
| Self::NameserverSearchResults(_)
| Self::ErrorResponse(_)
| Self::Help(_) => None,
}
}
pub fn get_conformance(&self) -> Option<&RdapConformance> {
match self {
Self::Entity(e) => e.common.rdap_conformance.as_ref(),
Self::Domain(d) => d.common.rdap_conformance.as_ref(),
Self::Nameserver(n) => n.common.rdap_conformance.as_ref(),
Self::Autnum(a) => a.common.rdap_conformance.as_ref(),
Self::Network(n) => n.common.rdap_conformance.as_ref(),
Self::DomainSearchResults(s) => s.common.rdap_conformance.as_ref(),
Self::EntitySearchResults(s) => s.common.rdap_conformance.as_ref(),
Self::NameserverSearchResults(s) => s.common.rdap_conformance.as_ref(),
Self::ErrorResponse(e) => e.common.rdap_conformance.as_ref(),
Self::Help(h) => h.common.rdap_conformance.as_ref(),
}
}
pub fn has_extension_id(&self, extension_id: ExtensionId) -> bool {
self.get_conformance().map_or(false, |conformance| {
conformance.contains(&extension_id.to_extension())
})
}
pub fn has_extension(&self, extension: &str) -> bool {
self.get_conformance().map_or(false, |conformance| {
conformance.contains(&Extension::from(extension))
})
}
pub fn is_redirect(&self) -> bool {
match self {
Self::ErrorResponse(e) => e.is_redirect(),
_ => false,
}
}
}
impl GetSelfLink for RdapResponse {
fn get_self_link(&self) -> Option<&Link> {
self.get_links()
.and_then(|links| links.iter().find(|link| link.is_relation("self")))
}
}
/// A trait for converting structs into an appropriate [RdapResponse] variant.
pub trait ToResponse {
/// Consumes the object and returns an [RdapResponse].
fn to_response(self) -> RdapResponse;
}
/// Trait for getting a link with a `rel` of "self".
pub trait GetSelfLink {
/// Get's the first self link.
/// See [crate::response::ObjectCommon::get_self_link()].
fn get_self_link(&self) -> Option<&Link>;
}
/// Train for setting a link with a `rel` of "self".
pub trait SelfLink: GetSelfLink {
/// See [crate::response::ObjectCommon::get_self_link()].
fn set_self_link(self, link: Link) -> Self;
}
/// Gets the `href` of a link with `rel` of "related" and `type` with the RDAP media type.
pub fn get_related_links(rdap_response: &RdapResponse) -> Vec<&str> {
let Some(links) = rdap_response.get_links() else {
return vec![];
};
let mut urls: Vec<_> = links
.iter()
.filter_map(|l| match (&l.href, &l.rel, &l.media_type) {
(Some(href), Some(rel), Some(media_type))
if rel.eq_ignore_ascii_case("related")
&& media_type.eq_ignore_ascii_case(RDAP_MEDIA_TYPE) =>
{
Some(href.as_str())
}
_ => None,
})
.collect();
// if none are found with correct media type, look for something that looks like an RDAP link
if urls.is_empty() {
urls = links
.iter()
.filter(|l| {
if let Some(href) = l.href() {
if let Some(rel) = l.rel() {
rel.eq_ignore_ascii_case("related")
&& (href.contains("/domain/")
|| href.contains("/ip/")
|| href.contains("/autnum/")
|| href.contains("/nameserver/")
|| href.contains("/entity/"))
} else {
false
}
} else {
false
}
})
.map(|l| l.href.as_ref().unwrap().as_str())
.collect::<Vec<&str>>();
}
urls
}
/// Makes a root object class suitable for being embedded in another object class.
pub trait ToChild {
/// Removes notices and rdapConformance so this object can be a child
/// of another object.
fn to_child(self) -> Self;
}
/// Returns `Some(Vec<T>)` if the vector is not empty, otherwise `None`.
pub fn to_opt_vec<T>(vec: Vec<T>) -> Option<Vec<T>> {
(!vec.is_empty()).then_some(vec)
}
/// Returns `Vec<T>` if `is_some()` else an empty vector.
pub fn opt_to_vec<T>(opt: Option<Vec<T>>) -> Vec<T> {
opt.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use serde_json::Value;
use crate::media_types::RDAP_MEDIA_TYPE;
use super::{get_related_links, Domain, Link, RdapResponse, ToResponse};
#[test]
fn test_redaction_response_gets_object() {
// GIVEN
let expected: Value =
serde_json::from_str(include_str!("test_files/lookup_with_redaction.json")).unwrap();
// WHEN
let actual = RdapResponse::try_from(expected).unwrap();
// THEN
assert!(matches!(actual, RdapResponse::Domain(_)));
}
#[test]
fn test_redaction_response_has_extension() {
// GIVEN
let expected: Value =
serde_json::from_str(include_str!("test_files/lookup_with_redaction.json")).unwrap();
// WHEN
let actual = RdapResponse::try_from(expected).unwrap();
// THEN
assert!(actual.has_extension_id(crate::response::types::ExtensionId::Redacted));
}
#[test]
fn test_redaction_response_domain_search() {
// GIVEN
let expected: Value =
serde_json::from_str(include_str!("test_files/domain_search_with_redaction.json"))
.unwrap();
// WHEN
let actual = RdapResponse::try_from(expected).unwrap();
// THEN
assert!(matches!(actual, RdapResponse::DomainSearchResults(_)));
}
#[test]
fn test_resopnse_is_domain() {
// GIVEN
let expected: Value =
serde_json::from_str(include_str!("test_files/domain_afnic_fr.json")).unwrap();
// WHEN
let actual = RdapResponse::try_from(expected).unwrap();
// THEN
assert!(matches!(actual, RdapResponse::Domain(_)));
}
#[test]
fn test_response_is_entity() {
// GIVEN
let expected: Value =
serde_json::from_str(include_str!("test_files/entity_arin_hostmaster.json")).unwrap();
// WHEN
let actual = RdapResponse::try_from(expected).unwrap();
// THEN
assert!(matches!(actual, RdapResponse::Entity(_)));
}
#[test]
fn test_response_is_nameserver() {
// GIVEN
let expected: Value =
serde_json::from_str(include_str!("test_files/nameserver_ns1_nic_fr.json")).unwrap();
// WHEN
let actual = RdapResponse::try_from(expected).unwrap();
// THEN
assert!(matches!(actual, RdapResponse::Nameserver(_)));
}
#[test]
fn test_response_is_autnum() {
// GIVEN
let expected: Value =
serde_json::from_str(include_str!("test_files/autnum_16509.json")).unwrap();
// WHEN
let actual = RdapResponse::try_from(expected).unwrap();
// THEN
assert!(matches!(actual, RdapResponse::Autnum(_)));
}
#[test]
fn test_response_is_network() {
// GIVEN
let expected: Value =
serde_json::from_str(include_str!("test_files/network_192_198_0_0.json")).unwrap();
// WHEN
let actual = RdapResponse::try_from(expected).unwrap();
// THEN
assert!(matches!(actual, RdapResponse::Network(_)));
}
#[test]
fn test_response_is_domain_search_results() {
// GIVEN
let expected: Value =
serde_json::from_str(include_str!("test_files/domains_ldhname_ns1_arin_net.json"))
.unwrap();
// WHEN
let actual = RdapResponse::try_from(expected).unwrap();
// THEN
assert!(matches!(actual, RdapResponse::DomainSearchResults(_)));
}
#[test]
fn test_response_is_entity_search_results() {
// GIVEN
let expected: Value =
serde_json::from_str(include_str!("test_files/entities_fn_arin.json")).unwrap();
// WHEN
let actual = RdapResponse::try_from(expected).unwrap();
// THEN
assert!(matches!(actual, RdapResponse::EntitySearchResults(_)));
}
#[test]
fn test_response_is_help() {
// GIVEN
let expected: Value =
serde_json::from_str(include_str!("test_files/help_nic_fr.json")).unwrap();
// WHEN
let actual = RdapResponse::try_from(expected).unwrap();
// THEN
assert!(matches!(actual, RdapResponse::Help(_)));
}
#[test]
fn test_response_is_error() {
// GIVEN
let expected: Value =
serde_json::from_str(include_str!("test_files/error_ripe_net.json")).unwrap();
// WHEN
let actual = RdapResponse::try_from(expected).unwrap();
// THEN
assert!(matches!(actual, RdapResponse::ErrorResponse(_)));
}
#[test]
fn test_string_is_entity_search_results() {
// GIVEN
let entity: Value =
serde_json::from_str(include_str!("test_files/entities_fn_arin.json")).unwrap();
let value = RdapResponse::try_from(entity).unwrap();
// WHEN
let actual = value.to_string();
// THEN
assert_eq!(actual, "EntitySearchResults");
}
#[test]
fn test_get_related_for_non_rel_link() {
// GIVEN
let rdap = Domain::builder()
.ldh_name("example.com")
.link(
Link::builder()
.rel("not-related")
.href("http://example.com")
.value("http://example.com")
.build(),
)
.build()
.to_response();
// WHEN
let links = get_related_links(&rdap);
// THEN
assert!(links.is_empty());
}
#[test]
fn test_get_related_for_rel_with_rdap_type_link() {
// GIVEN
let link = Link::builder()
.rel("related")
.href("http://example.com")
.value("http://example.com")
.media_type(RDAP_MEDIA_TYPE)
.build();
let rdap = Domain::builder()
.ldh_name("example.com")
.link(link.clone())
.build()
.to_response();
// WHEN
let links = get_related_links(&rdap);
// THEN
assert!(!links.is_empty());
assert_eq!(links.first().expect("empty links"), &link.href().unwrap());
}
#[test]
fn test_get_related_for_rel_link() {
// GIVEN
let link = Link::builder()
.rel("related")
.href("http://example.com")
.value("http://example.com")
.build();
let rdap = Domain::builder()
.ldh_name("example.com")
.link(link.clone())
.build()
.to_response();
// WHEN
let links = get_related_links(&rdap);
// THEN
assert!(links.is_empty());
}
#[test]
fn test_get_related_for_rel_link_that_look_like_rdap() {
// GIVEN
let link = Link::builder()
.rel("related")
.href("http://example.com/domain/foo")
.value("http://example.com")
.build();
let rdap = Domain::builder()
.ldh_name("example.com")
.link(link.clone())
.build()
.to_response();
// WHEN
let links = get_related_links(&rdap);
// THEN
assert!(!links.is_empty());
assert_eq!(links.first().expect("empty links"), &link.href().unwrap());
}
}

View file

@ -0,0 +1,341 @@
//! RDAP Nameserver object class.
use {
crate::prelude::{Common, Extension, ObjectCommon},
std::{net::IpAddr, str::FromStr},
};
use serde::{Deserialize, Serialize};
use super::{
to_opt_vec, to_opt_vectorstringish, types::Link, CommonFields, Entity, Event, GetSelfLink,
Notice, ObjectCommonFields, Port43, RdapResponseError, Remark, SelfLink, ToChild, ToResponse,
VectorStringish, EMPTY_VEC_STRING,
};
/// Represents an IP address set for nameservers.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub struct IpAddresses {
#[serde(skip_serializing_if = "Option::is_none")]
pub v6: Option<VectorStringish>,
#[serde(skip_serializing_if = "Option::is_none")]
pub v4: Option<VectorStringish>,
}
#[buildstructor::buildstructor]
impl IpAddresses {
/// Builds nameserver IP address.
#[builder(visibility = "pub")]
fn new(addresses: Vec<String>) -> Result<Self, RdapResponseError> {
let mut v4: Vec<String> = Vec::new();
let mut v6: Vec<String> = Vec::new();
for addr in addresses {
let ip = IpAddr::from_str(&addr)?;
match ip {
IpAddr::V4(_) => v4.push(addr),
IpAddr::V6(_) => v6.push(addr),
}
}
Ok(Self {
v4: to_opt_vectorstringish(v4),
v6: to_opt_vectorstringish(v6),
})
}
#[allow(dead_code)]
#[builder(entry = "illegal", visibility = "pub(crate)")]
fn new_illegal(v6: Option<Vec<String>>, v4: Option<Vec<String>>) -> Self {
Self {
v4: v4.map(VectorStringish::from),
v6: v6.map(VectorStringish::from),
}
}
/// Get the IPv6 addresses.
pub fn v6s(&self) -> &Vec<String> {
self.v6
.as_ref()
.map(|v| v.vec())
.unwrap_or(&EMPTY_VEC_STRING)
}
/// Get the IPv4 addresses.
pub fn v4s(&self) -> &Vec<String> {
self.v4
.as_ref()
.map(|v| v.vec())
.unwrap_or(&EMPTY_VEC_STRING)
}
}
/// Represents an RDAP [nameserver](https://rdap.rcode3.com/protocol/object_classes.html#nameserver) response.
///
/// Using the builder is recommended to construct this structure as it
/// will fill-in many of the mandatory fields.
/// The following is an example.
///
/// ```rust
/// use icann_rdap_common::prelude::*;
///
/// let ns = Nameserver::builder()
/// .ldh_name("ns1.example.com")
/// .handle("ns1_example_com-1")
/// .status("active")
/// .address("10.0.0.1")
/// .address("10.0.0.2")
/// .entity(Entity::builder().handle("FOO").build())
/// .build().unwrap();
/// let c = serde_json::to_string_pretty(&ns).unwrap();
/// eprintln!("{c}");
/// ```
///
/// This will produce the following.
///
/// ```norust
/// {
/// "rdapConformance": [
/// "rdap_level_0"
/// ],
/// "objectClassName": "nameserver",
/// "handle": "ns1_example_com-1",
/// "status": [
/// "active"
/// ],
/// "entities": [
/// {
/// "rdapConformance": [
/// "rdap_level_0"
/// ],
/// "objectClassName": "entity",
/// "handle": "FOO"
/// }
/// ],
/// "ldhName": "ns1.example.com",
/// "ipAddresses": {
/// "v4": [
/// "10.0.0.1",
/// "10.0.0.2"
/// ]
/// }
/// }
/// ```
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Nameserver {
#[serde(flatten)]
pub common: Common,
#[serde(flatten)]
pub object_common: ObjectCommon,
#[serde(rename = "ldhName")]
#[serde(skip_serializing_if = "Option::is_none")]
pub ldh_name: Option<String>,
#[serde(rename = "unicodeName")]
#[serde(skip_serializing_if = "Option::is_none")]
pub unicode_name: Option<String>,
#[serde(rename = "ipAddresses")]
#[serde(skip_serializing_if = "Option::is_none")]
pub ip_addresses: Option<IpAddresses>,
}
#[buildstructor::buildstructor]
impl Nameserver {
/// Builds a basic nameserver object.
///
/// ```rust
/// use icann_rdap_common::prelude::*;
///
/// let ns = Nameserver::builder()
/// .ldh_name("ns1.example.com")
/// .handle("ns1_example_com-1")
/// .status("active")
/// .address("10.0.0.1")
/// .address("10.0.0.2")
/// .entity(Entity::builder().handle("FOO").build())
/// .build().unwrap();
/// ```
#[builder(visibility = "pub")]
#[allow(clippy::too_many_arguments)]
fn new<T: Into<String>>(
ldh_name: T,
addresses: Vec<String>,
handle: Option<String>,
remarks: Vec<Remark>,
links: Vec<Link>,
events: Vec<Event>,
statuses: Vec<String>,
port_43: Option<Port43>,
entities: Vec<Entity>,
notices: Vec<Notice>,
extensions: Vec<Extension>,
redacted: Option<Vec<crate::response::redacted::Redacted>>,
) -> Result<Self, RdapResponseError> {
let ip_addresses = if !addresses.is_empty() {
Some(IpAddresses::builder().addresses(addresses).build()?)
} else {
None
};
Ok(Self {
common: Common::level0()
.extensions(extensions)
.and_notices(to_opt_vec(notices))
.build(),
object_common: ObjectCommon::nameserver()
.and_handle(handle)
.and_remarks(to_opt_vec(remarks))
.and_links(to_opt_vec(links))
.and_events(to_opt_vec(events))
.status(statuses)
.and_port_43(port_43)
.and_entities(to_opt_vec(entities))
.and_redacted(redacted)
.build(),
ldh_name: Some(ldh_name.into()),
unicode_name: None,
ip_addresses,
})
}
#[builder(entry = "illegal", visibility = "pub(crate)")]
#[allow(clippy::too_many_arguments)]
#[allow(dead_code)]
fn new_illegal(ldh_name: Option<String>, ip_addresses: Option<IpAddresses>) -> Self {
Self {
common: Common::level0().build(),
object_common: ObjectCommon::nameserver().build(),
ldh_name,
unicode_name: None,
ip_addresses,
}
}
/// Get the LDH name.
pub fn ldh_name(&self) -> Option<&str> {
self.ldh_name.as_deref()
}
/// Get the Unicode name.
pub fn unicode_name(&self) -> Option<&str> {
self.unicode_name.as_deref()
}
/// Get the IP addresses.
pub fn ip_addresses(&self) -> Option<&IpAddresses> {
self.ip_addresses.as_ref()
}
}
impl ToResponse for Nameserver {
fn to_response(self) -> super::RdapResponse {
super::RdapResponse::Nameserver(Box::new(self))
}
}
impl GetSelfLink for Nameserver {
fn get_self_link(&self) -> Option<&Link> {
self.object_common.get_self_link()
}
}
impl SelfLink for Nameserver {
fn set_self_link(mut self, link: Link) -> Self {
self.object_common = self.object_common.set_self_link(link);
self
}
}
impl ToChild for Nameserver {
fn to_child(mut self) -> Self {
self.common = Common {
rdap_conformance: None,
notices: None,
};
self
}
}
impl CommonFields for Nameserver {
fn common(&self) -> &Common {
&self.common
}
}
impl ObjectCommonFields for Nameserver {
fn object_common(&self) -> &ObjectCommon {
&self.object_common
}
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use super::Nameserver;
#[test]
fn GIVEN_nameserver_WHEN_deserialize_THEN_success() {
// GIVEN
let expected = r#"
{
"objectClassName" : "nameserver",
"handle" : "XXXX",
"ldhName" : "ns1.xn--fo-5ja.example",
"unicodeName" : "ns.fóo.example",
"status" : [ "active" ],
"ipAddresses" :
{
"v4": [ "192.0.2.1", "192.0.2.2" ],
"v6": [ "2001:db8::123" ]
},
"remarks" :
[
{
"description" :
[
"She sells sea shells down by the sea shore.",
"Originally written by Terry Sullivan."
]
}
],
"links" :
[
{
"value" : "https://example.net/nameserver/ns1.xn--fo-5ja.example",
"rel" : "self",
"href" : "https://example.net/nameserver/ns1.xn--fo-5ja.example",
"type" : "application/rdap+json"
}
],
"port43" : "whois.example.net",
"events" :
[
{
"eventAction" : "registration",
"eventDate" : "1990-12-31T23:59:59Z"
},
{
"eventAction" : "last changed",
"eventDate" : "1991-12-31T23:59:59Z",
"eventActor" : "joe@example.com"
}
]
}
"#;
// WHEN
let actual = serde_json::from_str::<Nameserver>(expected);
// THEN
let actual = actual.unwrap();
assert_eq!(actual.object_common.object_class_name, "nameserver");
assert!(actual.object_common.handle.is_some());
assert!(actual.ldh_name.is_some());
assert!(actual.unicode_name.is_some());
assert!(actual.ip_addresses.is_some());
assert!(actual.object_common.remarks.is_some());
assert!(actual.object_common.status.is_some());
assert!(actual.object_common.links.is_some());
assert!(actual.object_common.events.is_some());
}
}

View file

@ -0,0 +1,548 @@
//! RDAP IP Network.
use {
crate::prelude::{Common, Extension, ObjectCommon},
std::str::FromStr,
};
use {
cidr::IpInet,
serde::{Deserialize, Serialize},
};
use super::{
to_opt_vec,
types::{ExtensionId, Link},
CommonFields, Entity, Event, GetSelfLink, Notice, Numberish, ObjectCommonFields, Port43,
RdapResponseError, Remark, SelfLink, ToChild, ToResponse,
};
/// Cidr0 structure from the Cidr0 extension.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(untagged)]
pub enum Cidr0Cidr {
V4Cidr(V4Cidr),
V6Cidr(V6Cidr),
}
impl std::fmt::Display for Cidr0Cidr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::V4Cidr(cidr) => cidr.fmt(f),
Self::V6Cidr(cidr) => cidr.fmt(f),
}
}
}
/// Represents a CIDR0 V4 CIDR.
///
/// This structure allow both the prefix
/// and length to be optional to handle misbehaving servers, however
/// both are required according to the CIDR0 RDAP extension. To create
/// a valid stucture, use the builder.
///
/// However, it is recommended to use the builder on `Network` which will
/// create the appropriate CIDR0 structure.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct V4Cidr {
pub v4prefix: Option<String>,
pub length: Option<Numberish<u8>>,
}
#[buildstructor::buildstructor]
impl V4Cidr {
/// Builds an Ipv4 CIDR0.
#[builder(visibility = "pub")]
fn new(v4prefix: String, length: u8) -> Self {
V4Cidr {
v4prefix: Some(v4prefix),
length: Some(Numberish::<u8>::from(length)),
}
}
}
impl std::fmt::Display for V4Cidr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let length_s = if let Some(length) = &self.length {
length.to_string()
} else {
"not_given".to_string()
};
write!(
f,
"{}/{}",
self.v4prefix.as_ref().unwrap_or(&"not_given".to_string()),
length_s
)
}
}
/// Represents a CIDR0 V6 CIDR.
///
/// This structure allow both the prefix
/// and length to be optional to handle misbehaving servers, however
/// both are required according to the CIDR0 RDAP extension. To create
/// a valid stucture, use the builder.
///
/// However, it is recommended to use the builder on `Network` which will
/// create the appropriate CIDR0 structure.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct V6Cidr {
pub v6prefix: Option<String>,
pub length: Option<Numberish<u8>>,
}
#[buildstructor::buildstructor]
impl V6Cidr {
/// Builds an IPv6 CIDR0.
#[builder(visibility = "pub")]
fn new(v6prefix: String, length: u8) -> Self {
V6Cidr {
v6prefix: Some(v6prefix),
length: Some(Numberish::<u8>::from(length)),
}
}
}
impl std::fmt::Display for V6Cidr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let length_s = if let Some(length) = &self.length {
length.to_string()
} else {
"not_given".to_string()
};
write!(
f,
"{}/{}",
self.v6prefix.as_ref().unwrap_or(&"not_given".to_string()),
length_s
)
}
}
/// Represents an RDAP [IP network](https://rdap.rcode3.com/protocol/object_classes.html#ip-network) response.
///
/// Use of the builder is recommended to create this structure.
/// The builder will create the appropriate CIDR0 structures and
/// is easier than specifying start and end IP addresses.
///
/// ```rust
/// use icann_rdap_common::prelude::*;
///
/// let net = Network::builder()
/// .cidr("10.0.0.0/24")
/// .handle("NET-10-0-0-0")
/// .status("active")
/// .build().unwrap();
/// ```
///
/// This will create the following RDAP structure.
///
/// ```norust
/// {
/// "rdapConformance": [
/// "cidr0",
/// "rdap_level_0"
/// ],
/// "objectClassName": "ip network",
/// "handle": "NET-10-0-0-0",
/// "status": [
/// "active"
/// ],
/// "startAddress": "10.0.0.0",
/// "endAddress": "10.0.0.255",
/// "ipVersion": "v4",
/// "cidr0_cidrs": [
/// {
/// "v4prefix": "10.0.0.0",
/// "length": 24
/// }
/// ]
/// }
/// ```
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Network {
#[serde(flatten)]
pub common: Common,
#[serde(flatten)]
pub object_common: ObjectCommon,
#[serde(rename = "startAddress")]
#[serde(skip_serializing_if = "Option::is_none")]
pub start_address: Option<String>,
#[serde(rename = "endAddress")]
#[serde(skip_serializing_if = "Option::is_none")]
pub end_address: Option<String>,
#[serde(rename = "ipVersion")]
#[serde(skip_serializing_if = "Option::is_none")]
pub ip_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(rename = "type")]
#[serde(skip_serializing_if = "Option::is_none")]
pub network_type: Option<String>,
#[serde(rename = "parentHandle")]
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_handle: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub country: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cidr0_cidrs: Option<Vec<Cidr0Cidr>>,
}
static EMPTY_CIDR0CIDRS: Vec<Cidr0Cidr> = vec![];
#[buildstructor::buildstructor]
impl Network {
/// Builds a basic IP network object.
///
/// ```rust
/// use icann_rdap_common::prelude::*;
///
/// let net = Network::builder()
/// .cidr("10.0.0.0/24")
/// .handle("NET-10-0-0-0")
/// .status("active")
/// .build().unwrap();
/// ```
#[builder(visibility = "pub")]
#[allow(clippy::too_many_arguments)]
fn new(
cidr: String,
handle: Option<String>,
country: Option<String>,
name: Option<String>,
network_type: Option<String>,
parent_handle: Option<String>,
remarks: Vec<Remark>,
links: Vec<Link>,
events: Vec<Event>,
statuses: Vec<String>,
port_43: Option<Port43>,
entities: Vec<Entity>,
notices: Vec<Notice>,
mut extensions: Vec<Extension>,
redacted: Option<Vec<crate::response::redacted::Redacted>>,
) -> Result<Self, RdapResponseError> {
let mut net_exts = vec![ExtensionId::Cidr0.to_extension()];
net_exts.append(&mut extensions);
let cidr = IpInet::from_str(&cidr)?;
Ok(Self {
common: Common::level0()
.extensions(net_exts)
.and_notices(to_opt_vec(notices))
.build(),
object_common: ObjectCommon::ip_network()
.and_handle(handle)
.and_remarks(to_opt_vec(remarks))
.and_links(to_opt_vec(links))
.and_events(to_opt_vec(events))
.status(statuses)
.and_port_43(port_43)
.and_entities(to_opt_vec(entities))
.and_redacted(redacted)
.build(),
start_address: Some(cidr.first_address().to_string()),
end_address: Some(cidr.last_address().to_string()),
ip_version: Some(
match cidr {
IpInet::V4(_) => "v4",
IpInet::V6(_) => "v6",
}
.to_string(),
),
name,
network_type,
parent_handle,
country,
cidr0_cidrs: match cidr {
IpInet::V4(cidr) => Some(vec![Cidr0Cidr::V4Cidr(V4Cidr {
v4prefix: Some(cidr.first_address().to_string()),
length: Some(Numberish::<u8>::from(cidr.network_length())),
})]),
IpInet::V6(cidr) => Some(vec![Cidr0Cidr::V6Cidr(V6Cidr {
v6prefix: Some(cidr.first_address().to_string()),
length: Some(Numberish::<u8>::from(cidr.network_length())),
})]),
},
})
}
#[builder(entry = "illegal", visibility = "pub(crate)")]
#[allow(clippy::too_many_arguments)]
#[allow(dead_code)]
fn new_illegal(
start_address: Option<String>,
end_address: Option<String>,
ip_version: Option<String>,
cidr0_cidrs: Option<Vec<Cidr0Cidr>>,
country: Option<String>,
name: Option<String>,
network_type: Option<String>,
parent_handle: Option<String>,
notices: Vec<Notice>,
) -> Self {
Self {
common: Common::level0()
.extension(ExtensionId::Cidr0.to_extension())
.and_notices(to_opt_vec(notices))
.build(),
object_common: ObjectCommon::ip_network().build(),
start_address,
end_address,
ip_version,
name,
network_type,
parent_handle,
country,
cidr0_cidrs,
}
}
/// Returns the start address of the network.
pub fn start_address(&self) -> Option<&str> {
self.start_address.as_deref()
}
/// Returns the end address of the network.
pub fn end_address(&self) -> Option<&str> {
self.end_address.as_deref()
}
/// Returns the IP version of the network.
pub fn ip_version(&self) -> Option<&str> {
self.ip_version.as_deref()
}
/// Returns the name of the network.
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
/// Returns the type of the network.
pub fn network_type(&self) -> Option<&str> {
self.network_type.as_deref()
}
/// Returns the parent handle of the network.
pub fn parent_handle(&self) -> Option<&str> {
self.parent_handle.as_deref()
}
/// Returns the country of the network.
pub fn country(&self) -> Option<&str> {
self.country.as_deref()
}
/// Returns the CIDR0 CIDRs of the network.
pub fn cidr0_cidrs(&self) -> &Vec<Cidr0Cidr> {
self.cidr0_cidrs.as_ref().unwrap_or(&EMPTY_CIDR0CIDRS)
}
}
impl ToResponse for Network {
fn to_response(self) -> super::RdapResponse {
super::RdapResponse::Network(Box::new(self))
}
}
impl GetSelfLink for Network {
fn get_self_link(&self) -> Option<&Link> {
self.object_common.get_self_link()
}
}
impl SelfLink for Network {
fn set_self_link(mut self, link: Link) -> Self {
self.object_common = self.object_common.set_self_link(link);
self
}
}
impl ToChild for Network {
fn to_child(mut self) -> Self {
self.common = Common {
rdap_conformance: None,
notices: None,
};
self
}
}
impl CommonFields for Network {
fn common(&self) -> &Common {
&self.common
}
}
impl ObjectCommonFields for Network {
fn object_common(&self) -> &ObjectCommon {
&self.object_common
}
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use crate::response::network::Network;
#[test]
fn GIVEN_network_WHEN_deserialize_THEN_success() {
let expected = r#"
{
"objectClassName" : "ip network",
"handle" : "XXXX-RIR",
"startAddress" : "2001:db8::",
"endAddress" : "2001:db8:0:ffff:ffff:ffff:ffff:ffff",
"ipVersion" : "v6",
"name": "NET-RTR-1",
"type" : "DIRECT ALLOCATION",
"country" : "AU",
"parentHandle" : "YYYY-RIR",
"status" : [ "active" ],
"remarks" :
[
{
"description" :
[
"She sells sea shells down by the sea shore.",
"Originally written by Terry Sullivan."
]
}
],
"links" :
[
{
"value" : "https://example.net/ip/2001:db8::/48",
"rel" : "self",
"href" : "https://example.net/ip/2001:db8::/48",
"type" : "application/rdap+json"
},
{
"value" : "https://example.net/ip/2001:db8::/48",
"rel" : "up",
"href" : "https://example.net/ip/2001:db8::/32",
"type" : "application/rdap+json"
}
],
"events" :
[
{
"eventAction" : "registration",
"eventDate" : "1990-12-31T23:59:59Z"
},
{
"eventAction" : "last changed",
"eventDate" : "1991-12-31T23:59:59Z"
}
],
"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"
}
]
}
]
}
"#;
// WHEN
let actual = serde_json::from_str::<Network>(expected);
// THEN
let actual = actual.unwrap();
assert_eq!(actual.object_common.object_class_name, "ip network");
assert!(actual.object_common.handle.is_some());
assert!(actual.start_address.is_some());
assert!(actual.end_address.is_some());
assert!(actual.ip_version.is_some());
assert!(actual.name.is_some());
assert!(actual.network_type.is_some());
assert!(actual.parent_handle.is_some());
assert!(actual.object_common.status.is_some());
assert!(actual.country.is_some());
assert!(actual.object_common.remarks.is_some());
assert!(actual.object_common.links.is_some());
assert!(actual.object_common.events.is_some());
assert!(actual.object_common.entities.is_some());
}
}

View file

@ -0,0 +1,266 @@
use serde::{Deserialize, Serialize};
use super::{
redacted::Redacted, to_opt_vectorstringish, Entity, Events, Link, Links, Port43, Remarks,
VectorStringish, EMPTY_VEC_STRING,
};
/// Holds those types that are common in all object classes.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct ObjectCommon {
#[serde(rename = "objectClassName")]
pub object_class_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub handle: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub remarks: Option<Remarks>,
#[serde(skip_serializing_if = "Option::is_none")]
pub links: Option<Links>,
#[serde(skip_serializing_if = "Option::is_none")]
pub events: Option<Events>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<VectorStringish>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "port43")]
pub port_43: Option<Port43>,
#[serde(skip_serializing_if = "Option::is_none")]
pub entities: Option<Vec<Entity>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub redacted: Option<Vec<Redacted>>,
}
#[buildstructor::buildstructor]
impl ObjectCommon {
/// Builds [ObjectCommon] for a [crate::response::domain::Domain].
#[builder(entry = "domain", visibility = "pub(crate)")]
#[allow(clippy::too_many_arguments)]
fn new_domain(
handle: Option<String>,
remarks: Option<Remarks>,
links: Option<Links>,
events: Option<Events>,
status: Option<Vec<String>>,
port_43: Option<Port43>,
entities: Option<Vec<Entity>>,
redacted: Option<Vec<Redacted>>,
) -> Self {
Self {
object_class_name: "domain".to_string(),
handle,
remarks,
links,
events,
status: to_opt_vectorstringish(status.unwrap_or_default()),
port_43,
entities,
redacted,
}
}
/// Builds [ObjectCommon] for a [crate::response::network::Network].
#[builder(entry = "ip_network", visibility = "pub(crate)")]
#[allow(clippy::too_many_arguments)]
fn new_ip_network(
handle: Option<String>,
remarks: Option<Remarks>,
links: Option<Links>,
events: Option<Events>,
status: Option<Vec<String>>,
port_43: Option<Port43>,
entities: Option<Vec<Entity>>,
redacted: Option<Vec<Redacted>>,
) -> Self {
Self {
object_class_name: "ip network".to_string(),
handle,
remarks,
links,
events,
status: to_opt_vectorstringish(status.unwrap_or_default()),
port_43,
entities,
redacted,
}
}
/// Builds an [ObjectCommon] for an [crate::response::autnum::Autnum].
#[builder(entry = "autnum", visibility = "pub(crate)")]
#[allow(clippy::too_many_arguments)]
fn new_autnum(
handle: Option<String>,
remarks: Option<Remarks>,
links: Option<Links>,
events: Option<Events>,
status: Option<Vec<String>>,
port_43: Option<Port43>,
entities: Option<Vec<Entity>>,
redacted: Option<Vec<Redacted>>,
) -> Self {
Self {
object_class_name: "autnum".to_string(),
handle,
remarks,
links,
events,
status: to_opt_vectorstringish(status.unwrap_or_default()),
port_43,
entities,
redacted,
}
}
/// Builds an [ObjectCommon] for a [crate::response::nameserver::Nameserver].
#[builder(entry = "nameserver", visibility = "pub(crate)")]
#[allow(clippy::too_many_arguments)]
fn new_nameserver(
handle: Option<String>,
remarks: Option<Remarks>,
links: Option<Links>,
events: Option<Events>,
status: Option<Vec<String>>,
port_43: Option<Port43>,
entities: Option<Vec<Entity>>,
redacted: Option<Vec<Redacted>>,
) -> Self {
Self {
object_class_name: "nameserver".to_string(),
handle,
remarks,
links,
events,
status: to_opt_vectorstringish(status.unwrap_or_default()),
port_43,
entities,
redacted,
}
}
/// Builds an [ObjectCommon] for an [crate::response::entity::Entity].
#[builder(entry = "entity", visibility = "pub(crate)")]
#[allow(clippy::too_many_arguments)]
fn new_entity(
handle: Option<String>,
remarks: Option<Remarks>,
links: Option<Links>,
events: Option<Events>,
status: Option<Vec<String>>,
port_43: Option<Port43>,
entities: Option<Vec<Entity>>,
redacted: Option<Vec<Redacted>>,
) -> Self {
Self {
object_class_name: "entity".to_string(),
handle,
remarks,
links,
events,
status: to_opt_vectorstringish(status.unwrap_or_default()),
port_43,
entities,
redacted,
}
}
/// This will remove all other self links and place the provided link
/// into the Links. This method will also set the "rel" attribute
/// to "self" on the provided link.
pub fn set_self_link(mut self, mut link: Link) -> Self {
link.rel = Some("self".to_string());
if let Some(links) = self.links {
let mut new_links = links
.into_iter()
.filter(|link| !link.is_relation("self"))
.collect::<Vec<Link>>();
new_links.push(link);
self.links = Some(new_links);
} else {
self.links = Some(vec![link]);
}
self
}
/// Get the link with a `rel` of "self".
pub fn get_self_link(&self) -> Option<&Link> {
if let Some(links) = &self.links {
links.iter().find(|link| link.is_relation("self"))
} else {
None
}
}
}
/// Empty Remarks.
static EMPTY_REMARKS: Remarks = vec![];
/// Empty Links.
static EMPTY_LINKS: Links = vec![];
/// Empty Events.
static EMPTY_EVENTS: Events = vec![];
/// Empty Entities.
static EMPTY_ENTITIES: Vec<Entity> = vec![];
/// Convenience methods for fields in [ObjectCommon].
pub trait ObjectCommonFields {
/// Getter for [ObjectCommon].
fn object_common(&self) -> &ObjectCommon;
/// Returns the object class name.
fn object_class_name(&self) -> &str {
&self.object_common().object_class_name
}
/// Returns the handle, if present.
fn handle(&self) -> Option<&str> {
self.object_common().handle.as_deref()
}
/// Returns the port 43 information, if present.
fn port_43(&self) -> Option<&Port43> {
self.object_common().port_43.as_ref()
}
/// Getter for [Remarks].
fn remarks(&self) -> &Remarks {
self.object_common()
.remarks
.as_ref()
.unwrap_or(&EMPTY_REMARKS)
}
/// Getter for [Links].
fn links(&self) -> &Links {
self.object_common().links.as_ref().unwrap_or(&EMPTY_LINKS)
}
/// Getter for [Events].
fn events(&self) -> &Events {
self.object_common()
.events
.as_ref()
.unwrap_or(&EMPTY_EVENTS)
}
/// Getter for status.
fn status(&self) -> &Vec<String> {
self.object_common()
.status
.as_ref()
.map(|v| v.vec())
.unwrap_or(&EMPTY_VEC_STRING)
}
/// Getter for Vec of [Entity].
fn entities(&self) -> &Vec<Entity> {
self.object_common()
.entities
.as_ref()
.unwrap_or(&EMPTY_ENTITIES)
}
}

View file

@ -0,0 +1,251 @@
//! RFC 9537.
use {
buildstructor::Builder,
serde::{Deserialize, Serialize},
std::{any::TypeId, fmt},
};
use crate::check::Checks;
/// Redacted registered name.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Name {
#[serde(rename = "description")]
pub description: Option<String>,
#[serde(rename = "type")]
pub type_field: Option<String>,
}
impl Name {
/// Get the description.
pub fn description(&self) -> Option<&String> {
self.description.as_ref()
}
/// Get the redaction type.
pub fn type_field(&self) -> Option<&String> {
self.type_field.as_ref()
}
}
/// Redaction reason.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub struct Reason {
#[serde(rename = "description")]
pub description: Option<String>,
#[serde(rename = "type")]
pub type_field: Option<String>,
}
impl std::fmt::Display for Reason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let output = self.description.clone().unwrap_or_default();
write!(f, "{}", output)
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum Method {
Removal,
EmptyValue,
PartialValue,
ReplacementValue,
}
/// RFC 9537 redaction structure.
#[derive(Builder, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Redacted {
#[serde[rename = "name"]]
pub name: Name,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "reason")]
pub reason: Option<Reason>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "prePath")]
pub pre_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "postPath")]
pub post_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "pathLang")]
pub path_lang: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "replacementPath")]
pub replacement_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "method")]
pub method: Option<Method>,
}
impl Default for Name {
fn default() -> Self {
Self {
description: Some(String::default()),
type_field: None,
}
}
}
impl Default for Method {
fn default() -> Self {
Self::Removal // according to IETF draft this is the default
}
}
impl fmt::Display for Method {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Removal => write!(f, "Removal"),
Self::EmptyValue => write!(f, "EmptyValue"),
Self::PartialValue => write!(f, "PartialValue"),
Self::ReplacementValue => write!(f, "ReplacementValue"),
}
}
}
impl Redacted {
/// Get the checks from Redactions.
pub fn get_checks(&self, _check_params: crate::check::CheckParams<'_>) -> crate::check::Checks {
Checks {
rdap_struct: crate::check::RdapStructure::Redacted,
items: vec![],
sub_checks: vec![],
}
}
/// Get the type.
pub fn get_type(&self) -> std::any::TypeId {
TypeId::of::<Self>()
}
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use super::*;
#[test]
fn GIVEN_redaction_WHEN_set_THEN_success() {
// GIVEN
let name = Name {
description: Some("Registry Domain ID".to_string()),
type_field: None,
};
// WHEN
let redacted = Redacted::builder()
.name(name)
.reason(Reason::default())
.pre_path("$.handle".to_string())
.post_path("$.entities[?(@.roles[0]=='registrant'".to_string())
.path_lang("jsonpath".to_string())
.replacement_path(
"$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='contact-uri')]"
.to_string(),
)
.method(Method::Removal)
.build();
// THEN
assert_eq!(
redacted.name.description,
Some("Registry Domain ID".to_string())
);
assert_eq!(redacted.pre_path, Some("$.handle".to_string()));
assert_eq!(
redacted.post_path,
Some("$.entities[?(@.roles[0]=='registrant'".to_string())
);
assert_eq!(redacted.path_lang, Some("jsonpath".to_string()));
assert_eq!(
redacted.replacement_path,
Some(
"$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='contact-uri')]"
.to_string()
)
);
assert_eq!(redacted.method, Some(Method::Removal));
}
#[test]
fn GIVEN_redaction_WHEN_deserialize_THEN_success() {
// GIVEN
let expected = r#"
{
"name": {
"type": "Registry Domain ID"
},
"prePath": "$.handle",
"pathLang": "jsonpath",
"postPath": "$.entities[?(@.roles[0]=='registrant'",
"replacementPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='contact-uri')]",
"method": "removal",
"reason": {
"description": "Server policy"
}
}
"#;
// in this one we swap the two fields
let name = Name {
type_field: Some("Registry Domain ID".to_string()),
description: None,
};
let reason: Reason = Reason {
description: Some("Server policy".to_string()),
type_field: None,
};
// WHEN
// use the builder for most of the fields but not all
let mut sample_redact: Redacted = Redacted::builder()
.name(name)
.pre_path("$.handle".to_string())
.path_lang("jsonpath".to_string())
.post_path("$.entities[?(@.roles[0]=='registrant'".to_string())
.replacement_path(
"$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='contact-uri')]"
.to_string(),
)
.build();
// also make sure we can set the rest
sample_redact.method = Some(Method::Removal);
sample_redact.reason = Some(reason);
let actual: Result<Redacted, serde_json::Error> =
serde_json::from_str::<Redacted>(expected);
// THEN
let actual: Redacted = actual.unwrap();
assert_eq!(actual, sample_redact); // sanity check
assert_eq!(
actual.name.type_field,
Some("Registry Domain ID".to_string())
);
assert_eq!(actual.pre_path, Some("$.handle".to_string()));
assert_eq!(
actual.post_path,
Some("$.entities[?(@.roles[0]=='registrant'".to_string())
);
assert_eq!(actual.path_lang, Some("jsonpath".to_string()));
assert_eq!(
actual.replacement_path,
Some(
"$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='contact-uri')]"
.to_string()
)
);
assert_eq!(actual.method, Some(Method::Removal));
}
}

View file

@ -0,0 +1,109 @@
//! RDAP Search Results.
use {
crate::prelude::{Common, Extension},
serde::{Deserialize, Serialize},
};
use super::{domain::Domain, entity::Entity, nameserver::Nameserver, CommonFields, ToResponse};
/// Represents RDAP domain search results.
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug, Eq)]
pub struct DomainSearchResults {
#[serde(flatten)]
pub common: Common,
#[serde(rename = "domainSearchResults")]
pub results: Vec<Domain>,
}
#[buildstructor::buildstructor]
impl DomainSearchResults {
/// Builds a domain search result.
#[builder(visibility = "pub")]
fn new(results: Vec<Domain>, extensions: Vec<Extension>) -> Self {
Self {
common: Common::level0().extensions(extensions).build(),
results,
}
}
}
impl CommonFields for DomainSearchResults {
fn common(&self) -> &Common {
&self.common
}
}
impl ToResponse for DomainSearchResults {
fn to_response(self) -> super::RdapResponse {
super::RdapResponse::DomainSearchResults(Box::new(self))
}
}
/// Represents RDAP nameserver search results.
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug, Eq)]
pub struct NameserverSearchResults {
#[serde(flatten)]
pub common: Common,
#[serde(rename = "nameserverSearchResults")]
pub results: Vec<Nameserver>,
}
#[buildstructor::buildstructor]
impl NameserverSearchResults {
/// Builds a nameserver search result.
#[builder(visibility = "pub")]
fn new(results: Vec<Nameserver>, extensions: Vec<Extension>) -> Self {
Self {
common: Common::level0().extensions(extensions).build(),
results,
}
}
}
impl CommonFields for NameserverSearchResults {
fn common(&self) -> &Common {
&self.common
}
}
impl ToResponse for NameserverSearchResults {
fn to_response(self) -> super::RdapResponse {
super::RdapResponse::NameserverSearchResults(Box::new(self))
}
}
/// Represents RDAP entity search results.
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug, Eq)]
pub struct EntitySearchResults {
#[serde(flatten)]
pub common: Common,
#[serde(rename = "entitySearchResults")]
pub results: Vec<Entity>,
}
#[buildstructor::buildstructor]
impl EntitySearchResults {
/// Builds an entity search result.
#[builder(visibility = "pub")]
fn new(results: Vec<Entity>, extensions: Vec<Extension>) -> Self {
Self {
common: Common::level0().extensions(extensions).build(),
results,
}
}
}
impl CommonFields for EntitySearchResults {
fn common(&self) -> &Common {
&self.common
}
}
impl ToResponse for EntitySearchResults {
fn to_response(self) -> super::RdapResponse {
super::RdapResponse::EntitySearchResults(Box::new(self))
}
}

View file

@ -0,0 +1,866 @@
{
"endAutnum" : 16509,
"entities" : [
{
"entities" : [
{
"events" : [
{
"eventAction" : "last changed",
"eventDate" : "2022-09-30T16:26:55-04:00"
},
{
"eventAction" : "registration",
"eventDate" : "2021-07-22T10:42:42-04:00"
}
],
"handle" : "ARMP-ARIN",
"links" : [
{
"href" : "https://rdap.arin.net/registry/entity/ARMP-ARIN",
"rel" : "self",
"type" : "application/rdap+json",
"value" : "https://rdap.arin.net/registry/autnum/16509"
},
{
"href" : "https://whois.arin.net/rest/poc/ARMP-ARIN",
"rel" : "alternate",
"type" : "application/xml",
"value" : "https://rdap.arin.net/registry/autnum/16509"
}
],
"objectClassName" : "entity",
"port43" : "whois.arin.net",
"roles" : [
"routing"
],
"status" : [
"validated"
],
"vcardArray" : [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"adr",
{
"label" : "13200 Woodland Park Dr\nHerndon\nHerndon\nVA\n20171\nUnited States"
},
"text",
[
"",
"",
"",
"",
"",
"",
""
]
],
[
"fn",
{},
"text",
"AWS RPKI Management POC"
],
[
"org",
{},
"text",
"AWS RPKI Management POC"
],
[
"kind",
{},
"text",
"group"
],
[
"email",
{},
"text",
"aws-rpki-routing-poc@amazon.com"
],
[
"tel",
{
"type" : [
"work",
"voice"
]
},
"text",
"+1-206-555-0000"
]
]
]
},
{
"events" : [
{
"eventAction" : "last changed",
"eventDate" : "2022-09-07T22:23:27-04:00"
},
{
"eventAction" : "registration",
"eventDate" : "2019-07-24T13:17:11-04:00"
}
],
"handle" : "IPROU3-ARIN",
"links" : [
{
"href" : "https://rdap.arin.net/registry/entity/IPROU3-ARIN",
"rel" : "self",
"type" : "application/rdap+json",
"value" : "https://rdap.arin.net/registry/autnum/16509"
},
{
"href" : "https://whois.arin.net/rest/poc/IPROU3-ARIN",
"rel" : "alternate",
"type" : "application/xml",
"value" : "https://rdap.arin.net/registry/autnum/16509"
}
],
"objectClassName" : "entity",
"port43" : "whois.arin.net",
"remarks" : [
{
"description" : [
"Report abuse incidents to our Abuse POC AEA8-ARIN. ",
"",
"Thank you for your cooperation."
],
"title" : "Registration Comments"
}
],
"roles" : [
"routing"
],
"status" : [
"validated"
],
"vcardArray" : [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"adr",
{
"label" : "1918 8th Ave\nSeattle\nWA\n98109\nUnited States"
},
"text",
[
"",
"",
"",
"",
"",
"",
""
]
],
[
"fn",
{},
"text",
"IP Routing"
],
[
"org",
{},
"text",
"IP Routing"
],
[
"kind",
{},
"text",
"group"
],
[
"email",
{},
"text",
"aws-routing-poc@amazon.com"
],
[
"tel",
{
"type" : [
"work",
"voice"
]
},
"text",
"+1-206-555-0000"
]
]
]
},
{
"events" : [
{
"eventAction" : "last changed",
"eventDate" : "2022-08-22T19:08:51-04:00"
},
{
"eventAction" : "registration",
"eventDate" : "2008-03-24T14:12:07-04:00"
}
],
"handle" : "AEA8-ARIN",
"links" : [
{
"href" : "https://rdap.arin.net/registry/entity/AEA8-ARIN",
"rel" : "self",
"type" : "application/rdap+json",
"value" : "https://rdap.arin.net/registry/autnum/16509"
},
{
"href" : "https://whois.arin.net/rest/poc/AEA8-ARIN",
"rel" : "alternate",
"type" : "application/xml",
"value" : "https://rdap.arin.net/registry/autnum/16509"
}
],
"objectClassName" : "entity",
"port43" : "whois.arin.net",
"remarks" : [
{
"description" : [
"Amazon Web Services Abuse - The activity you have detected originates from a dynamic hosting environment. For fastest response, please submit abuse reports to abuse@amazonaws.com",
"All reports MUST include:",
"* src IP",
"* dest IP (your IP)",
"* dest port",
"* Accurate date/timestamp and timezone of activity",
"* Intensity/frequency (short log extracts)",
"* Your contact details (phone and email)",
"Without these we will be unable to identify the correct owner of the IP address at that point in time."
],
"title" : "Registration Comments"
}
],
"roles" : [
"abuse"
],
"status" : [
"validated"
],
"vcardArray" : [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"adr",
{
"label" : "Amazon Web Services Elastic Compute Cloud, EC2\n410 Terry Avenue North\nSeattle\nWA\n98109-5210\nUnited States"
},
"text",
[
"",
"",
"",
"",
"",
"",
""
]
],
[
"fn",
{},
"text",
"Amazon EC2 Abuse"
],
[
"org",
{},
"text",
"Amazon EC2 Abuse"
],
[
"kind",
{},
"text",
"group"
],
[
"email",
{},
"text",
"abuse@amazonaws.com"
],
[
"tel",
{
"type" : [
"work",
"voice"
]
},
"text",
"+1-206-555-0000"
]
]
]
},
{
"events" : [
{
"eventAction" : "last changed",
"eventDate" : "2022-08-24T13:17:35-04:00"
},
{
"eventAction" : "registration",
"eventDate" : "2005-09-19T06:00:05-04:00"
}
],
"handle" : "ANO24-ARIN",
"links" : [
{
"href" : "https://rdap.arin.net/registry/entity/ANO24-ARIN",
"rel" : "self",
"type" : "application/rdap+json",
"value" : "https://rdap.arin.net/registry/autnum/16509"
},
{
"href" : "https://whois.arin.net/rest/poc/ANO24-ARIN",
"rel" : "alternate",
"type" : "application/xml",
"value" : "https://rdap.arin.net/registry/autnum/16509"
}
],
"objectClassName" : "entity",
"port43" : "whois.arin.net",
"roles" : [
"technical"
],
"status" : [
"validated"
],
"vcardArray" : [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"adr",
{
"label" : "PO BOX 81226\nSeattle\nWA\n98108-1226\nUnited States"
},
"text",
[
"",
"",
"",
"",
"",
"",
""
]
],
[
"fn",
{},
"text",
"Amazon EC2 Network Operations"
],
[
"org",
{},
"text",
"Amazon EC2 Network Operations"
],
[
"kind",
{},
"text",
"group"
],
[
"email",
{},
"text",
"amzn-noc-contact@amazon.com"
],
[
"tel",
{
"type" : [
"work",
"voice"
]
},
"text",
"+1-206-555-0000"
]
]
]
},
{
"events" : [
{
"eventAction" : "last changed",
"eventDate" : "2022-08-24T13:17:49-04:00"
},
{
"eventAction" : "registration",
"eventDate" : "2010-03-04T18:38:30-05:00"
}
],
"handle" : "AANO1-ARIN",
"links" : [
{
"href" : "https://rdap.arin.net/registry/entity/AANO1-ARIN",
"rel" : "self",
"type" : "application/rdap+json",
"value" : "https://rdap.arin.net/registry/autnum/16509"
},
{
"href" : "https://whois.arin.net/rest/poc/AANO1-ARIN",
"rel" : "alternate",
"type" : "application/xml",
"value" : "https://rdap.arin.net/registry/autnum/16509"
}
],
"objectClassName" : "entity",
"port43" : "whois.arin.net",
"roles" : [
"noc"
],
"status" : [
"validated"
],
"vcardArray" : [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"adr",
{
"label" : "410 Terry Ave N\nSeattle\nWA\n98109\nUnited States"
},
"text",
[
"",
"",
"",
"",
"",
"",
""
]
],
[
"fn",
{},
"text",
"Amazon AWS Network Operations"
],
[
"org",
{},
"text",
"Amazon AWS Network Operations"
],
[
"kind",
{},
"text",
"group"
],
[
"email",
{},
"text",
"amzn-noc-contact@amazon.com"
],
[
"tel",
{
"type" : [
"work",
"voice"
]
},
"text",
"+1-206-555-0000"
]
]
]
},
{
"events" : [
{
"eventAction" : "last changed",
"eventDate" : "2022-09-23T09:50:49-04:00"
},
{
"eventAction" : "registration",
"eventDate" : "2013-11-12T22:06:06-05:00"
}
],
"handle" : "IPMAN40-ARIN",
"links" : [
{
"href" : "https://rdap.arin.net/registry/entity/IPMAN40-ARIN",
"rel" : "self",
"type" : "application/rdap+json",
"value" : "https://rdap.arin.net/registry/autnum/16509"
},
{
"href" : "https://whois.arin.net/rest/poc/IPMAN40-ARIN",
"rel" : "alternate",
"type" : "application/xml",
"value" : "https://rdap.arin.net/registry/autnum/16509"
}
],
"objectClassName" : "entity",
"port43" : "whois.arin.net",
"remarks" : [
{
"description" : [
"Report abuse incidents to our Abuse POC AEA8-ARIN. ",
"",
"Thank you for your cooperation."
],
"title" : "Registration Comments"
}
],
"roles" : [
"administrative"
],
"status" : [
"validated"
],
"vcardArray" : [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"adr",
{
"label" : "1918 8th Ave\nSeattle\nWA\n98109\nUnited States"
},
"text",
[
"",
"",
"",
"",
"",
"",
""
]
],
[
"fn",
{},
"text",
"IP Management"
],
[
"org",
{},
"text",
"IP Management"
],
[
"kind",
{},
"text",
"group"
],
[
"email",
{},
"text",
"ipmanagement@amazon.com"
],
[
"tel",
{
"type" : [
"work",
"voice"
]
},
"text",
"+1-206-555-0000"
]
]
]
}
],
"events" : [
{
"eventAction" : "last changed",
"eventDate" : "2022-09-30T16:19:20-04:00"
},
{
"eventAction" : "registration",
"eventDate" : "1995-01-23T00:00:00-05:00"
}
],
"handle" : "AMAZON-4",
"links" : [
{
"href" : "https://rdap.arin.net/registry/entity/AMAZON-4",
"rel" : "self",
"type" : "application/rdap+json",
"value" : "https://rdap.arin.net/registry/autnum/16509"
},
{
"href" : "https://whois.arin.net/rest/org/AMAZON-4",
"rel" : "alternate",
"type" : "application/xml",
"value" : "https://rdap.arin.net/registry/autnum/16509"
}
],
"objectClassName" : "entity",
"port43" : "whois.arin.net",
"roles" : [
"registrant"
],
"vcardArray" : [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"fn",
{},
"text",
"Amazon.com, Inc."
],
[
"adr",
{
"label" : "1918 8th Ave\nSEATTLE\nWA\n98101-1244\nUnited States"
},
"text",
[
"",
"",
"",
"",
"",
"",
""
]
],
[
"kind",
{},
"text",
"org"
]
]
]
},
{
"events" : [
{
"eventAction" : "last changed",
"eventDate" : "2022-01-14T12:21:19-05:00"
},
{
"eventAction" : "registration",
"eventDate" : "1999-06-11T10:53:13-04:00"
}
],
"handle" : "AC6-ORG-ARIN",
"links" : [
{
"href" : "https://rdap.arin.net/registry/entity/AC6-ORG-ARIN",
"rel" : "self",
"type" : "application/rdap+json",
"value" : "https://rdap.arin.net/registry/autnum/16509"
},
{
"href" : "https://whois.arin.net/rest/poc/AC6-ORG-ARIN",
"rel" : "alternate",
"type" : "application/xml",
"value" : "https://rdap.arin.net/registry/autnum/16509"
}
],
"objectClassName" : "entity",
"port43" : "whois.arin.net",
"roles" : [
"technical"
],
"status" : [
"validated"
],
"vcardArray" : [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"adr",
{
"label" : "PO BOX 81226\nSeattle\nWA\n98108-1226\nUnited States"
},
"text",
[
"",
"",
"",
"",
"",
"",
""
]
],
[
"fn",
{},
"text",
"Amazon-com Incorporated"
],
[
"org",
{},
"text",
"Amazon-com Incorporated"
],
[
"kind",
{},
"text",
"group"
],
[
"email",
{},
"text",
"ipmanagement@amazon.com"
],
[
"tel",
{
"type" : [
"work",
"voice"
]
},
"text",
"+1-206-266-2187"
]
]
]
}
],
"events" : [
{
"eventAction" : "last changed",
"eventDate" : "2012-03-02T08:03:18-05:00"
},
{
"eventAction" : "registration",
"eventDate" : "2000-05-04T00:00:00-04:00"
}
],
"handle" : "AS16509",
"links" : [
{
"href" : "https://rdap.arin.net/registry/autnum/16509",
"rel" : "self",
"type" : "application/rdap+json",
"value" : "https://rdap.arin.net/registry/autnum/16509"
},
{
"href" : "https://whois.arin.net/rest/asn/AS16509",
"rel" : "alternate",
"type" : "application/xml",
"value" : "https://rdap.arin.net/registry/autnum/16509"
}
],
"name" : "AMAZON-02",
"notices" : [
{
"description" : [
"By using the ARIN RDAP/Whois service, you are agreeing to the RDAP/Whois Terms of Use"
],
"links" : [
{
"href" : "https://www.arin.net/resources/registry/whois/tou/",
"rel" : "terms-of-service",
"type" : "text/html",
"value" : "https://rdap.arin.net/registry/autnum/16509"
}
],
"title" : "Terms of Service"
},
{
"description" : [
"If you see inaccuracies in the results, please visit: "
],
"links" : [
{
"href" : "https://www.arin.net/resources/registry/whois/inaccuracy_reporting/",
"rel" : "inaccuracy-report",
"type" : "text/html",
"value" : "https://rdap.arin.net/registry/autnum/16509"
}
],
"title" : "Whois Inaccuracy Reporting"
},
{
"description" : [
"Copyright 1997-2023, American Registry for Internet Numbers, Ltd."
],
"title" : "Copyright Notice"
}
],
"objectClassName" : "autnum",
"port43" : "whois.arin.net",
"rdapConformance" : [
"nro_rdap_profile_0",
"rdap_level_0",
"nro_rdap_profile_asn_flat_0"
],
"startAutnum" : 16509,
"status" : [
"active"
]
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,70 @@
{
"rdapConformance": [
"rdap_level_0",
"redacted"
],
"domainSearchResults":[
{
"objectClassName": "domain",
"ldhName": "example1.com",
"links":[
{
"value":"https://example.com/rdap/domain/example1.com",
"rel":"self",
"href":"https://example.com/rdap/domain/example1.com",
"type":"application/rdap+json"
},
{
"value":"https://example.com/rdap/domain/example1.com",
"rel":"related",
"href":"https://example.com/rdap/domain/example1.com",
"type":"application/rdap+json"
}
],
"redacted": [
{
"name": {
"type": "Registry Domain ID"
},
"prePath": "$.domainSearchResults[0].handle",
"pathLang": "jsonpath",
"method": "removal",
"reason": {
"type": "Server policy"
}
}
]
},
{
"objectClassName": "domain",
"ldhName": "example2.com",
"links":[
{
"value":"https://example.com/rdap/domain/example2.com",
"rel":"self",
"href":"https://example.com/rdap/domain/example2.com",
"type":"application/rdap+json"
},
{
"value":"https://example.com/rdap/domain/example2.com",
"rel":"related",
"href":"https://example.com/rdap/domain/example2.com",
"type":"application/rdap+json"
}
],
"redacted": [
{
"name": {
"description": "Registry Domain ID"
},
"prePath": "$.domainSearchResults[1].handle",
"pathLang": "jsonpath",
"method": "removal",
"reason": {
"description": "Server policy"
}
}
]
}
]
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,52 @@
{
"rdapConformance" : [ "nro_rdap_profile_0", "rdap_level_0" ],
"notices" : [ {
"title" : "Terms of Service",
"description" : [ "By using the ARIN RDAP/Whois service, you are agreeing to the RDAP/Whois Terms of Use" ],
"links" : [ {
"value" : "https://rdap.arin.net/registry/entity/arin-hostmaster",
"rel" : "terms-of-service",
"type" : "text/html",
"href" : "https://www.arin.net/resources/registry/whois/tou/"
} ]
}, {
"title" : "Whois Inaccuracy Reporting",
"description" : [ "If you see inaccuracies in the results, please visit: " ],
"links" : [ {
"value" : "https://rdap.arin.net/registry/entity/arin-hostmaster",
"rel" : "inaccuracy-report",
"type" : "text/html",
"href" : "https://www.arin.net/resources/registry/whois/inaccuracy_reporting/"
} ]
}, {
"title" : "Copyright Notice",
"description" : [ "Copyright 1997-2023, American Registry for Internet Numbers, Ltd." ]
} ],
"handle" : "ARIN-HOSTMASTER",
"vcardArray" : [ "vcard", [ [ "version", { }, "text", "4.0" ], [ "adr", {
"label" : "P.O. Box 232290\nCentreville\nVA\n20120\nUnited States"
}, "text", [ "", "", "", "", "", "", "" ] ], [ "fn", { }, "text", "Registration Services Department" ], [ "org", { }, "text", "Registration Services Department" ], [ "kind", { }, "text", "group" ], [ "email", { }, "text", "hostmaster@arin.net" ], [ "tel", {
"type" : [ "work", "voice" ]
}, "text", "+1-703-227-0660" ] ] ],
"links" : [ {
"value" : "https://rdap.arin.net/registry/entity/arin-hostmaster",
"rel" : "self",
"type" : "application/rdap+json",
"href" : "https://rdap.arin.net/registry/entity/ARIN-HOSTMASTER"
}, {
"value" : "https://rdap.arin.net/registry/entity/arin-hostmaster",
"rel" : "alternate",
"type" : "application/xml",
"href" : "https://whois.arin.net/rest/poc/ARIN-HOSTMASTER"
} ],
"events" : [ {
"eventAction" : "last changed",
"eventDate" : "2022-08-02T15:17:32-04:00"
}, {
"eventAction" : "registration",
"eventDate" : "2003-04-30T12:32:56-04:00"
} ],
"status" : [ "validated" ],
"port43" : "whois.arin.net",
"objectClassName" : "entity"
}

View file

@ -0,0 +1,35 @@
{
"description" : [
"Nameserver not supported"
],
"errorCode" : 501,
"links" : [
{
"href" : "http://www.ripe.net/data-tools/support/documentation/terms",
"rel" : "copyright",
"value" : "http://www.ripe.net/data-tools/support/documentation/terms"
}
],
"notices" : [
{
"description" : [
"This is the RIPE Database query service. The objects are in RDAP format."
],
"links" : [
{
"href" : "http://www.ripe.net/db/support/db-terms-conditions.pdf",
"rel" : "terms-of-service",
"type" : "application/pdf"
}
],
"title" : "Terms and Conditions"
}
],
"port43" : "whois.ripe.net",
"rdapConformance" : [
"cidr0",
"rdap_level_0",
"nro_rdap_profile_0"
],
"title" : "501 Not Implemented"
}

View file

@ -0,0 +1,52 @@
{
"notices" : [
{
"description" : [
"domain/XXXX",
"domains?name=XXXX",
"entity/XXXX",
"entities?fn=XXXX",
"nameserver/XXXX",
"nameservers?ip=XXXX",
"help"
],
"links" : [
{
"href" : "https://rdap.nic.fr/help",
"media" : "application/json",
"rel" : "self",
"value" : "https://rdap.nic.fr/help"
}
],
"title" : "RDAP queries can be made on the following types"
},
{
"description" : [
"(1) DOMAIN : Domain queries are made based only on the domain name criterion.",
" Example: domain/example.tld",
"(2) ENTITY : Registrar queries are made based only on the registrar gurid criterion (IANA ID).",
" Example: entity/9995",
" Registrar searches are made based only on the registrar name criterion.",
" Example: entities?fn=Registry%20Testing%201 for the registrar with name 'Registry Testing 1'",
"(3) NAMESERVER : Nameserver queries are made based only on the nameserver name criterion.",
" Example: nameserver/ns1.example.tld",
" Nameserver searches are made based only on the IP address criterion.",
" Example: nameservers?ip=198.51.100.0"
],
"links" : [
{
"href" : "https://rdap.nic.fr/help",
"media" : "application/json",
"rel" : "self",
"value" : "https://rdap.nic.fr/help"
}
],
"title" : "USE"
}
],
"rdapConformance" : [
"rdap_level_0",
"icann_rdap_technical_implementation_guide_0",
"icann_rdap_response_profile_0"
]
}

View file

@ -0,0 +1,393 @@
{
"rdapConformance": [
"rdap_level_0",
"redacted"
],
"objectClassName": "domain",
"ldhName": "example.com",
"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": "Registry Domain ID"
},
"prePath": "$.handle",
"pathLang": "jsonpath",
"method": "removal",
"reason": {
"description": "Server policy"
}
},
{
"name": {
"description": "Registrant Name"
},
"postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='fn')][3]",
"pathLang": "jsonpath",
"method": "emptyValue",
"reason": {
"description": "Server policy"
}
},
{
"name": {
"description": "Registrant Organization"
},
"prePath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='org')]",
"pathLang": "jsonpath",
"method": "removal",
"reason": {
"description": "Server policy"
}
},
{
"name": {
"description": "Registrant Street"
},
"postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='adr')][3][:3]",
"pathLang": "jsonpath",
"method": "emptyValue",
"reason": {
"description": "Server policy"
}
},
{
"name": {
"description": "Registrant City"
},
"postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='adr')][3][3]",
"pathLang": "jsonpath",
"method": "emptyValue",
"reason": {
"description": "Server policy"
}
},
{
"name": {
"description": "Registrant Postal Code"
},
"postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='adr')][3][5]",
"pathLang": "jsonpath",
"method": "emptyValue",
"reason": {
"description": "Server policy"
}
},
{
"name": {
"description": "Registrant Email"
},
"prePath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='email')]",
"method": "removal",
"reason": {
"description": "Server policy"
}
},
{
"name": {
"description": "Registrant Phone"
},
"prePath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[1].type=='voice')]",
"method": "removal",
"reason": {
"description": "Server policy"
}
},
{
"name": {
"description": "Technical Name"
},
"postPath": "$.entities[?(@.roles[0]=='technical')].vcardArray[1][?(@[0]=='fn')][3]",
"method": "emptyValue",
"reason": {
"description": "Server policy"
}
},
{
"name": {
"description": "Technical Email"
},
"prePath": "$.entities[?(@.roles[0]=='technical')].vcardArray[1][?(@[0]=='email')]",
"method": "removal",
"reason": {
"description": "Server policy"
}
},
{
"name": {
"description": "Technical Phone"
},
"prePath": "$.entities[?(@.roles[0]=='technical')].vcardArray[1][?(@[1].type=='voice')]",
"method": "removal",
"reason": {
"description": "Server policy"
}
},
{
"name": {
"description": "Technical Fax"
},
"prePath": "$.entities[?(@.roles[0]=='technical')].vcardArray[1][?(@[1].type=='fax')]",
"reason": {
"description": "Client request"
}
},
{
"name": {
"description": "Administrative Contact"
},
"prePath": "$.entities[?(@.roles[0]=='administrative')]",
"method": "removal",
"reason": {
"description": "Refer to the technical contact"
}
},
{
"name": {
"description": "Billing Contact"
},
"prePath": "$.entities[?(@.roles[0]=='billing')]",
"method": "removal",
"reason": {
"description": "Refer to the registrant contact"
}
}
]
}

View file

@ -0,0 +1,146 @@
{
"entities" : [
{
"events" : [
{
"eventAction" : "registration",
"eventDate" : "2014-07-29T15:35:33Z"
},
{
"eventAction" : "last changed",
"eventDate" : "2022-10-12T18:54:01.785057Z"
}
],
"handle" : "RAR939-FRNIC",
"links" : [
{
"href" : "https://rdap.nic.fr/entity/RAR939-FRNIC",
"rel" : "self",
"value" : "https://rdap.nic.fr/entity/RAR939-FRNIC"
}
],
"objectClassName" : "entity",
"port43" : "whois.nic.fr",
"publicIds" : [
{
"identifier" : "9999",
"type" : "IANA Registrar ID"
}
],
"remarks" : [
{
"description" : [
"No"
],
"type" : "registrar restricted publication"
}
],
"roles" : [
"sponsor",
"registrar"
],
"status" : [
"active"
],
"vcardArray" : [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"fn",
{},
"text",
"Registry Operations"
],
[
"tel",
{},
"text",
"+33.139308300"
],
[
"tel",
{
"type" : "fax"
},
"text",
"+33.139308301"
],
[
"email",
{},
"text",
"support@afnic.fr"
],
[
"url",
{},
"uri",
"https://www.afnic.fr"
],
[
"adr",
{},
"text",
[
"",
"",
[
"AFNIC",
"immeuble le Stephenson",
"1, rue Stephenson"
],
"Montigny-Le-Bretonneux",
"",
"78180",
"FR"
]
]
]
]
}
],
"handle" : "HOST05-FRNIC",
"ipAddresses" : {
"v4" : [
"192.134.4.1"
],
"v6" : [
"2001:67c:2218:2::4:1"
]
},
"ldhName" : "ns1.nic.fr",
"links" : [
{
"href" : "https://rdap.nic.fr/nameserver/ns1.nic.fr",
"rel" : "self",
"value" : "https://rdap.nic.fr/nameserver/ns1.nic.fr"
}
],
"objectClassName" : "nameserver",
"port43" : "whois.nic.fr",
"rdapConformance" : [
"rdap_level_0",
"icann_rdap_technical_implementation_guide_0",
"icann_rdap_response_profile_0"
],
"remarks" : [
{
"description" : [
"The list of results does not contain all results due to lack of authorization.",
"This may indicate to some clients that proper authorization will yield a longer result set."
],
"title" : "result set truncated due to authorization"
}
],
"status" : [
"server update prohibited",
"server delete prohibited",
"associated"
]
}

View file

@ -0,0 +1,384 @@
{
"arin_originas0_originautnums" : [
53301
],
"cidr0_cidrs" : [
{
"length" : 22,
"v4prefix" : "192.198.0.0"
}
],
"endAddress" : "192.198.3.255",
"entities" : [
{
"entities" : [
{
"events" : [
{
"eventAction" : "last changed",
"eventDate" : "2022-02-07T09:20:07-05:00"
},
{
"eventAction" : "registration",
"eventDate" : "2011-10-29T11:02:59-04:00"
}
],
"handle" : "PETSI-ARIN",
"links" : [
{
"href" : "https://rdap.arin.net/registry/entity/PETSI-ARIN",
"rel" : "self",
"type" : "application/rdap+json",
"value" : "https://rdap.arin.net/registry/ip/192.198.0.0"
},
{
"href" : "https://whois.arin.net/rest/poc/PETSI-ARIN",
"rel" : "alternate",
"type" : "application/xml",
"value" : "https://rdap.arin.net/registry/ip/192.198.0.0"
}
],
"objectClassName" : "entity",
"port43" : "whois.arin.net",
"roles" : [
"noc",
"technical",
"administrative",
"abuse"
],
"status" : [
"validated"
],
"vcardArray" : [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"adr",
{
"label" : "8162 Sw 81st ST\nEllendale\nMN\n56026\nUnited States"
},
"text",
[
"",
"",
"",
"",
"",
"",
""
]
],
[
"fn",
{},
"text",
"Daniel Petsinger"
],
[
"n",
{},
"text",
[
"Petsinger",
"Daniel",
"",
"",
""
]
],
[
"kind",
{},
"text",
"individual"
],
[
"email",
{},
"text",
"daniel.petsinger@radiolinkinternet.com"
],
[
"tel",
{
"type" : [
"work",
"voice"
]
},
"text",
"+1-507-417-4176"
]
]
]
}
],
"events" : [
{
"eventAction" : "last changed",
"eventDate" : "2017-01-28T08:32:29-05:00"
},
{
"eventAction" : "registration",
"eventDate" : "2011-11-02T07:36:05-04:00"
}
],
"handle" : "DP-41",
"links" : [
{
"href" : "https://rdap.arin.net/registry/entity/DP-41",
"rel" : "self",
"type" : "application/rdap+json",
"value" : "https://rdap.arin.net/registry/ip/192.198.0.0"
},
{
"href" : "https://whois.arin.net/rest/org/DP-41",
"rel" : "alternate",
"type" : "application/xml",
"value" : "https://rdap.arin.net/registry/ip/192.198.0.0"
}
],
"objectClassName" : "entity",
"port43" : "whois.arin.net",
"remarks" : [
{
"description" : [
"http://www.radiolinkinternet.com",
"Standard NOC hours are 9am to 5pm CST"
],
"title" : "Registration Comments"
}
],
"roles" : [
"registrant"
],
"vcardArray" : [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"fn",
{},
"text",
"Radio Link Internet"
],
[
"adr",
{
"label" : "8162 SW 81st St\nEllendale\nMN\n56026\nUnited States"
},
"text",
[
"",
"",
"",
"",
"",
"",
""
]
],
[
"kind",
{},
"text",
"org"
]
]
]
},
{
"events" : [
{
"eventAction" : "last changed",
"eventDate" : "2022-02-07T09:20:07-05:00"
},
{
"eventAction" : "registration",
"eventDate" : "2011-10-29T11:02:59-04:00"
}
],
"handle" : "PETSI-ARIN",
"links" : [
{
"href" : "https://rdap.arin.net/registry/entity/PETSI-ARIN",
"rel" : "self",
"type" : "application/rdap+json",
"value" : "https://rdap.arin.net/registry/ip/192.198.0.0"
},
{
"href" : "https://whois.arin.net/rest/poc/PETSI-ARIN",
"rel" : "alternate",
"type" : "application/xml",
"value" : "https://rdap.arin.net/registry/ip/192.198.0.0"
}
],
"objectClassName" : "entity",
"port43" : "whois.arin.net",
"roles" : [
"noc",
"abuse",
"technical"
],
"status" : [
"validated"
],
"vcardArray" : [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"adr",
{
"label" : "8162 Sw 81st ST\nEllendale\nMN\n56026\nUnited States"
},
"text",
[
"",
"",
"",
"",
"",
"",
""
]
],
[
"fn",
{},
"text",
"Daniel Petsinger"
],
[
"n",
{},
"text",
[
"Petsinger",
"Daniel",
"",
"",
""
]
],
[
"kind",
{},
"text",
"individual"
],
[
"email",
{},
"text",
"daniel.petsinger@radiolinkinternet.com"
],
[
"tel",
{
"type" : [
"work",
"voice"
]
},
"text",
"+1-507-417-4176"
]
]
]
}
],
"events" : [
{
"eventAction" : "last changed",
"eventDate" : "2013-03-19T09:14:03-04:00"
},
{
"eventAction" : "registration",
"eventDate" : "2013-01-31T18:11:17-05:00"
}
],
"handle" : "NET-192-198-0-0-1",
"ipVersion" : "v4",
"links" : [
{
"href" : "https://rdap.arin.net/registry/ip/192.198.0.0",
"rel" : "self",
"type" : "application/rdap+json",
"value" : "https://rdap.arin.net/registry/ip/192.198.0.0"
},
{
"href" : "https://whois.arin.net/rest/net/NET-192-198-0-0-1",
"rel" : "alternate",
"type" : "application/xml",
"value" : "https://rdap.arin.net/registry/ip/192.198.0.0"
}
],
"name" : "RADIOLINK-ARIN-1",
"notices" : [
{
"description" : [
"By using the ARIN RDAP/Whois service, you are agreeing to the RDAP/Whois Terms of Use"
],
"links" : [
{
"href" : "https://www.arin.net/resources/registry/whois/tou/",
"rel" : "terms-of-service",
"type" : "text/html",
"value" : "https://rdap.arin.net/registry/ip/192.198.0.0"
}
],
"title" : "Terms of Service"
},
{
"description" : [
"If you see inaccuracies in the results, please visit: "
],
"links" : [
{
"href" : "https://www.arin.net/resources/registry/whois/inaccuracy_reporting/",
"rel" : "inaccuracy-report",
"type" : "text/html",
"value" : "https://rdap.arin.net/registry/ip/192.198.0.0"
}
],
"title" : "Whois Inaccuracy Reporting"
},
{
"description" : [
"Copyright 1997-2023, American Registry for Internet Numbers, Ltd."
],
"title" : "Copyright Notice"
}
],
"objectClassName" : "ip network",
"parentHandle" : "NET-192-0-0-0-0",
"port43" : "whois.arin.net",
"rdapConformance" : [
"nro_rdap_profile_0",
"rdap_level_0",
"cidr0",
"arin_originas0"
],
"startAddress" : "192.198.0.0",
"status" : [
"active"
],
"type" : "DIRECT ALLOCATION"
}

File diff suppressed because it is too large Load diff