Adding upstream version 0.0.22.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
2f814b513a
commit
b06d3acde8
190 changed files with 61565 additions and 0 deletions
28
icann-rdap-common/Cargo.toml
Normal file
28
icann-rdap-common/Cargo.toml
Normal 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
177
icann-rdap-common/README.md
Normal 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 ICANN’s option, without any additional terms or conditions.
|
192
icann-rdap-common/src/check/autnum.rs
Normal file
192
icann-rdap-common/src/check/autnum.rs
Normal 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);
|
||||
}
|
||||
}
|
648
icann-rdap-common/src/check/domain.rs
Normal file
648
icann-rdap-common/src/check/domain.rs
Normal 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));
|
||||
}
|
||||
}
|
71
icann-rdap-common/src/check/entity.rs
Normal file
71
icann-rdap-common/src/check/entity.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
23
icann-rdap-common/src/check/error.rs
Normal file
23
icann-rdap-common/src/check/error.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
23
icann-rdap-common/src/check/help.rs
Normal file
23
icann-rdap-common/src/check/help.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
346
icann-rdap-common/src/check/httpdata.rs
Normal file
346
icann-rdap-common/src/check/httpdata.rs
Normal 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));
|
||||
}
|
||||
}
|
754
icann-rdap-common/src/check/mod.rs
Normal file
754
icann-rdap-common/src/check/mod.rs
Normal 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()));
|
||||
}
|
||||
}
|
184
icann-rdap-common/src/check/nameserver.rs
Normal file
184
icann-rdap-common/src/check/nameserver.rs
Normal 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));
|
||||
}
|
||||
}
|
482
icann-rdap-common/src/check/network.rs
Normal file
482
icann-rdap-common/src/check/network.rs
Normal 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));
|
||||
}
|
||||
}
|
68
icann-rdap-common/src/check/search.rs
Normal file
68
icann-rdap-common/src/check/search.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
295
icann-rdap-common/src/check/string.rs
Normal file
295
icann-rdap-common/src/check/string.rs
Normal 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);
|
||||
}
|
||||
}
|
1011
icann-rdap-common/src/check/types.rs
Normal file
1011
icann-rdap-common/src/check/types.rs
Normal file
File diff suppressed because it is too large
Load diff
846
icann-rdap-common/src/contact/from_vcard.rs
Normal file
846
icann-rdap-common/src/contact/from_vcard.rs
Normal 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");
|
||||
}
|
||||
}
|
486
icann-rdap-common/src/contact/mod.rs
Normal file
486
icann-rdap-common/src/contact/mod.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
303
icann-rdap-common/src/contact/to_vcard.rs
Normal file
303
icann-rdap-common/src/contact/to_vcard.rs
Normal 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);
|
||||
}
|
||||
}
|
320
icann-rdap-common/src/dns_types.rs
Normal file
320
icann-rdap-common/src/dns_types.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
327
icann-rdap-common/src/httpdata.rs
Normal file
327
icann-rdap-common/src/httpdata.rs
Normal 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);
|
||||
}
|
||||
}
|
746
icann-rdap-common/src/iana.rs
Normal file
746
icann-rdap-common/src/iana.rs
Normal 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/"
|
||||
);
|
||||
}
|
||||
}
|
28
icann-rdap-common/src/lib.rs
Normal file
28
icann-rdap-common/src/lib.rs
Normal 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"));
|
7
icann-rdap-common/src/media_types.rs
Normal file
7
icann-rdap-common/src/media_types.rs
Normal 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";
|
341
icann-rdap-common/src/response/autnum.rs
Normal file
341
icann-rdap-common/src/response/autnum.rs
Normal 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());
|
||||
}
|
||||
}
|
51
icann-rdap-common/src/response/common.rs
Normal file
51
icann-rdap-common/src/response/common.rs
Normal 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)
|
||||
}
|
||||
}
|
986
icann-rdap-common/src/response/domain.rs
Normal file
986
icann-rdap-common/src/response/domain.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
420
icann-rdap-common/src/response/entity.rs
Normal file
420
icann-rdap-common/src/response/entity.rs
Normal 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());
|
||||
}
|
||||
}
|
119
icann-rdap-common/src/response/error.rs
Normal file
119
icann-rdap-common/src/response/error.rs
Normal 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);
|
||||
}
|
||||
}
|
40
icann-rdap-common/src/response/help.rs
Normal file
40
icann-rdap-common/src/response/help.rs
Normal 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))
|
||||
}
|
||||
}
|
626
icann-rdap-common/src/response/lenient.rs
Normal file
626
icann-rdap-common/src/response/lenient.rs
Normal 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");
|
||||
}
|
||||
}
|
652
icann-rdap-common/src/response/mod.rs
Normal file
652
icann-rdap-common/src/response/mod.rs
Normal 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());
|
||||
}
|
||||
}
|
341
icann-rdap-common/src/response/nameserver.rs
Normal file
341
icann-rdap-common/src/response/nameserver.rs
Normal 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());
|
||||
}
|
||||
}
|
548
icann-rdap-common/src/response/network.rs
Normal file
548
icann-rdap-common/src/response/network.rs
Normal 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());
|
||||
}
|
||||
}
|
266
icann-rdap-common/src/response/obj_common.rs
Normal file
266
icann-rdap-common/src/response/obj_common.rs
Normal 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)
|
||||
}
|
||||
}
|
251
icann-rdap-common/src/response/redacted.rs
Normal file
251
icann-rdap-common/src/response/redacted.rs
Normal 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));
|
||||
}
|
||||
}
|
109
icann-rdap-common/src/response/search.rs
Normal file
109
icann-rdap-common/src/response/search.rs
Normal 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))
|
||||
}
|
||||
}
|
866
icann-rdap-common/src/response/test_files/autnum_16509.json
Normal file
866
icann-rdap-common/src/response/test_files/autnum_16509.json
Normal 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"
|
||||
]
|
||||
}
|
1068
icann-rdap-common/src/response/test_files/domain_afnic_fr.json
Normal file
1068
icann-rdap-common/src/response/test_files/domain_afnic_fr.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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
7906
icann-rdap-common/src/response/test_files/entities_fn_arin.json
Normal file
7906
icann-rdap-common/src/response/test_files/entities_fn_arin.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
52
icann-rdap-common/src/response/test_files/help_nic_fr.json
Normal file
52
icann-rdap-common/src/response/test_files/help_nic_fr.json
Normal 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"
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
1022
icann-rdap-common/src/response/types.rs
Normal file
1022
icann-rdap-common/src/response/types.rs
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue