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,28 @@
[package]
name = "icann-rdap-common"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
description = """
Common RDAP data structures.
"""
[dependencies]
chrono.workspace = true
cidr.workspace = true
const_format.workspace = true
buildstructor.workspace = true
idna.workspace = true
ipnet.workspace = true
prefix-trie.workspace = true
serde.workspace = true
serde_json.workspace = true
strum.workspace = true
strum_macros.workspace = true
thiserror.workspace = true
[dev-dependencies]
# fixture testings
rstest = "0.17.0"

177
icann-rdap-common/README.md Normal file
View file

@ -0,0 +1,177 @@
ICANN RDAP Common
=================
This is a common component library for the Registration Data Access Protocol (RDAP) written and sponsored
by the Internet Corporation for Assigned Names and Numbers [(ICANN)](https://www.icann.org).
RDAP is standard of the [IETF](https://ietf.org/), and extensions
to RDAP are a current work activity of the IETF's [REGEXT working group](https://datatracker.ietf.org/wg/regext/documents/).
More information on ICANN's role in RDAP can be found [here](https://www.icann.org/rdap).
General information on RDAP can be found [here](https://rdap.rcode3.com/).
Installation
------------
Add the library to your Cargo.toml: `cargo add icann-rdap-common`.
This library can be compiled for WASM targets.
Usage
-----
Create some RDAP objects:
```rust
// create an entity
use icann_rdap_common::response::Entity;
let holder = Entity::builder().handle("foo-BAR").build();
// create an RDAP domain
use icann_rdap_common::response::Domain;
let domain = Domain::builder().ldh_name("example.com").entity(holder.clone()).build();
// create an IP network
use icann_rdap_common::response::Network;
let net = Network::builder().cidr("10.0.0.0/16").entity(holder.clone()).build().unwrap();
// create a nameserver
use icann_rdap_common::response::Nameserver;
let ns = Nameserver::builder().ldh_name("ns1.example.com").entity(holder.clone()).build().unwrap();
// create an autnum
use icann_rdap_common::response::Autnum;
let autnum = Autnum::builder().autnum_range(700..700).entity(holder).build();
```
Parse RDAP JSON:
```rust
use icann_rdap_common::prelude::*;
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(_)));
```
RDAP uses jCard, the JSON version of vCard, to model "contact information"
(e.g. postal addresses, phone numbers, etc...). Because jCard is difficult
to use and there might be other contact models standardized by the IETF,
this library includes the [`contact::Contact`] struct. This struct can be
converted to and from jCard/vCard with the [`contact::Contact::from_vcard`]
and [`contact::Contact::to_vcard`] functions.
[`contact::Contact`] structs 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);
```
License
-------
Licensed under either of
* Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
* MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT) at your option.
Contribution
------------
Unless you explicitly state otherwise, any contribution, as defined in the Apache-2.0 license,
intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license,
shall be dual licensed pursuant to the Apache License, Version 2.0 or the MIT License referenced
as above, at ICANNs option, without any additional terms or conditions.

View file

@ -0,0 +1,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