use std::{any::TypeId, collections::HashSet};
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 rir_search::*;
#[doc(inline)]
pub use search::*;
#[doc(inline)]
pub use types::*;
#[doc(inline)]
pub use values::*;
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 mod jscontact; pub(crate) mod lenient;
pub(crate) mod nameserver;
pub(crate) mod network;
pub(crate) mod obj_common;
pub mod redacted; pub(crate) mod rir_search; pub(crate) mod search;
pub mod ttl; pub(crate) mod types;
pub(crate) mod values;
#[derive(Debug, Error)]
pub enum RdapResponseError {
#[error("Wrong JSON type: {0}")]
WrongJsonType(String),
#[error("Unknown RDAP response.")]
UnknownRdapResponse,
#[error(transparent)]
SerdeJson(#[from] serde_json::Error),
#[error(transparent)]
AddrParse(#[from] std::net::AddrParseError),
#[error(transparent)]
CidrParse(#[from] cidr::errors::NetworkParseError),
#[error("Network type must either be 'v4' or 'v6'.")]
InvalidNetworkType,
}
#[derive(Serialize, Deserialize, Clone, Display, PartialEq, Debug)]
#[serde(untagged, try_from = "Value")]
pub enum RdapResponse {
Entity(Box<Entity>),
Domain(Box<Domain>),
Nameserver(Box<Nameserver>),
Autnum(Box<Autnum>),
Network(Box<Network>),
DomainSearchResults(Box<DomainSearchResults>),
EntitySearchResults(Box<EntitySearchResults>),
NameserverSearchResults(Box<NameserverSearchResults>),
IpSearchResults(Box<IpSearchResults>),
AutnumSearchResults(Box<AutnumSearchResults>),
ErrorResponse(Box<Rfc9083Error>),
Help(Box<Help>),
}
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 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(),
));
}
};
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(),
));
}
}
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(),
));
}
}
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(),
));
}
}
if let Some(result) = response.get("ipSearchResults") {
if result.is_array() {
return Ok(serde_json::from_value::<IpSearchResults>(value)?.to_response());
} else {
return Err(RdapResponseError::WrongJsonType(
"'ipSearchResults' is not an array".to_string(),
));
}
}
if let Some(result) = response.get("autnumSearchResults") {
if result.is_array() {
return Ok(serde_json::from_value::<AutnumSearchResults>(value)?.to_response());
} else {
return Err(RdapResponseError::WrongJsonType(
"'autnumSearchResults' is not an array".to_string(),
));
}
}
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(),
));
}
}
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::IpSearchResults(_) => TypeId::of::<IpSearchResults>(),
Self::AutnumSearchResults(_) => TypeId::of::<AutnumSearchResults>(),
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::IpSearchResults(_)
| Self::AutnumSearchResults(_)
| 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::IpSearchResults(s) => s.common.rdap_conformance.as_ref(),
Self::AutnumSearchResults(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()
.is_some_and(|conformance| conformance.contains(&extension_id.to_extension()))
}
pub fn has_extension(&self, extension: &str) -> bool {
self.get_conformance()
.is_some_and(|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 self_link(&self) -> Option<&Link> {
self.get_links()
.and_then(|links| links.iter().find(|link| link.is_relation("self")))
}
}
pub trait ToResponse {
fn to_response(self) -> RdapResponse;
}
pub trait GetSelfLink {
fn self_link(&self) -> Option<&Link>;
}
pub trait SelfLink: GetSelfLink {
fn with_self_link(self, link: Link) -> Self;
}
pub fn get_related_links(rdap_response: &RdapResponse) -> Vec<&str> {
let related = &["related".to_string()];
get_relationship_links(related, rdap_response)
}
pub fn get_relationship_links<'b>(
relationships: &[String],
rdap_response: &'b RdapResponse,
) -> Vec<&'b 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 is_relationship(rel, relationships)
&& media_type.eq_ignore_ascii_case(RDAP_MEDIA_TYPE) =>
{
Some(href.as_str())
}
_ => None,
})
.collect();
if urls.is_empty() {
urls = links
.iter()
.filter(|l| {
if let Some(href) = l.href() {
if let Some(rel) = l.rel() {
is_relationship(rel, relationships) && has_rdap_path(href)
} else {
false
}
} else {
false
}
})
.map(|l| l.href.as_ref().unwrap().as_str())
.collect::<Vec<&str>>();
}
urls
}
fn is_relationship(rel: &str, relationships: &[String]) -> bool {
let mut splits: usize = 0;
let num_found = rel
.split_whitespace()
.filter(|r| {
splits += 1;
relationships.iter().any(|s| r.eq_ignore_ascii_case(s))
})
.count();
splits == num_found && num_found == relationships.len()
}
pub fn has_rdap_path(url: &str) -> bool {
if url.contains("/domain/")
|| url.contains("/ip/")
|| url.contains("/autnum/")
|| url.contains("/nameserver/")
|| url.contains("/entity/")
{
return true;
}
false
}
pub trait ToChild {
fn to_child(self) -> Self;
}
pub fn to_opt_vec<T>(vec: Vec<T>) -> Option<Vec<T>> {
(!vec.is_empty()).then_some(vec)
}
pub fn opt_to_vec<T>(opt: Option<Vec<T>>) -> Vec<T> {
opt.unwrap_or_default()
}
pub trait ContentExtensions {
fn content_extensions(&self) -> HashSet<ExtensionId>;
}
impl ContentExtensions for RdapResponse {
fn content_extensions(&self) -> HashSet<ExtensionId> {
match &self {
Self::Entity(e) => e.content_extensions(),
Self::Domain(d) => d.content_extensions(),
Self::Nameserver(n) => n.content_extensions(),
Self::Autnum(a) => a.content_extensions(),
Self::Network(n) => n.content_extensions(),
Self::DomainSearchResults(r) => r.content_extensions(),
Self::EntitySearchResults(r) => r.content_extensions(),
Self::NameserverSearchResults(r) => r.content_extensions(),
Self::IpSearchResults(r) => r.content_extensions(),
Self::AutnumSearchResults(r) => r.content_extensions(),
Self::ErrorResponse(e) => e.content_extensions(),
Self::Help(h) => h.content_extensions(),
}
}
}
pub fn normalize_extensions(rdap: RdapResponse) -> RdapResponse {
normalize_extensions_with(rdap, [])
}
pub fn normalize_extensions_with(
rdap: RdapResponse,
extra_extensions: impl IntoIterator<Item = ExtensionId>,
) -> RdapResponse {
let mut extensions: HashSet<ExtensionId> = rdap.content_extensions();
extensions.extend(extra_extensions);
let rdap_conformance = extensions
.iter()
.map(|e| e.to_extension())
.collect::<Vec<Extension>>();
match rdap {
RdapResponse::Entity(e) => Entity {
common: Common {
rdap_conformance: Some(rdap_conformance),
..e.common
},
..*e
}
.to_response(),
RdapResponse::Domain(d) => Domain {
common: Common {
rdap_conformance: Some(rdap_conformance),
..d.common
},
..*d
}
.to_response(),
RdapResponse::Nameserver(n) => Nameserver {
common: Common {
rdap_conformance: Some(rdap_conformance),
..n.common
},
..*n
}
.to_response(),
RdapResponse::Autnum(a) => Autnum {
common: Common {
rdap_conformance: Some(rdap_conformance),
..a.common
},
..*a
}
.to_response(),
RdapResponse::Network(n) => Network {
common: Common {
rdap_conformance: Some(rdap_conformance),
..n.common
},
..*n
}
.to_response(),
RdapResponse::DomainSearchResults(r) => DomainSearchResults {
common: Common {
rdap_conformance: Some(rdap_conformance),
..r.common
},
..*r
}
.to_response(),
RdapResponse::EntitySearchResults(r) => EntitySearchResults {
common: Common {
rdap_conformance: Some(rdap_conformance),
..r.common
},
..*r
}
.to_response(),
RdapResponse::NameserverSearchResults(r) => NameserverSearchResults {
common: Common {
rdap_conformance: Some(rdap_conformance),
..r.common
},
..*r
}
.to_response(),
RdapResponse::IpSearchResults(r) => IpSearchResults {
common: Common {
rdap_conformance: Some(rdap_conformance),
..r.common
},
..*r
}
.to_response(),
RdapResponse::AutnumSearchResults(r) => AutnumSearchResults {
common: Common {
rdap_conformance: Some(rdap_conformance),
..r.common
},
..*r
}
.to_response(),
RdapResponse::ErrorResponse(e) => Rfc9083Error {
common: Common {
rdap_conformance: Some(rdap_conformance),
..e.common
},
..*e
}
.to_response(),
RdapResponse::Help(h) => Help {
common: Common {
rdap_conformance: Some(rdap_conformance),
..h.common
},
}
.to_response(),
}
}
#[cfg(test)]
mod tests {
use serde_json::Value;
use crate::{
media_types::RDAP_MEDIA_TYPE,
prelude::{get_relationship_links, ExtensionId},
};
use super::{get_related_links, Domain, Link, RdapResponse, ToResponse};
#[test]
fn test_redaction_response_gets_object() {
let expected: Value =
serde_json::from_str(include_str!("test_files/lookup_with_redaction.json")).unwrap();
let actual = RdapResponse::try_from(expected).unwrap();
assert!(matches!(actual, RdapResponse::Domain(_)));
}
#[test]
fn test_redaction_response_has_extension() {
let expected: Value =
serde_json::from_str(include_str!("test_files/lookup_with_redaction.json")).unwrap();
let actual = RdapResponse::try_from(expected).unwrap();
assert!(actual.has_extension_id(ExtensionId::Redacted));
}
#[test]
fn test_redaction_response_domain_search() {
let expected: Value =
serde_json::from_str(include_str!("test_files/domain_search_with_redaction.json"))
.unwrap();
let actual = RdapResponse::try_from(expected).unwrap();
assert!(matches!(actual, RdapResponse::DomainSearchResults(_)));
}
#[test]
fn test_response_is_domain() {
let expected: Value =
serde_json::from_str(include_str!("test_files/domain_afnic_fr.json")).unwrap();
let actual = RdapResponse::try_from(expected).unwrap();
assert!(matches!(actual, RdapResponse::Domain(_)));
}
#[test]
fn test_response_is_entity() {
let expected: Value =
serde_json::from_str(include_str!("test_files/entity_arin_hostmaster.json")).unwrap();
let actual = RdapResponse::try_from(expected).unwrap();
assert!(matches!(actual, RdapResponse::Entity(_)));
}
#[test]
fn test_response_is_nameserver() {
let expected: Value =
serde_json::from_str(include_str!("test_files/nameserver_ns1_nic_fr.json")).unwrap();
let actual = RdapResponse::try_from(expected).unwrap();
assert!(matches!(actual, RdapResponse::Nameserver(_)));
}
#[test]
fn test_response_is_autnum() {
let expected: Value =
serde_json::from_str(include_str!("test_files/autnum_16509.json")).unwrap();
let actual = RdapResponse::try_from(expected).unwrap();
assert!(matches!(actual, RdapResponse::Autnum(_)));
}
#[test]
fn test_response_is_network() {
let expected: Value =
serde_json::from_str(include_str!("test_files/network_192_198_0_0.json")).unwrap();
let actual = RdapResponse::try_from(expected).unwrap();
assert!(matches!(actual, RdapResponse::Network(_)));
}
#[test]
fn test_response_is_domain_search_results() {
let expected: Value =
serde_json::from_str(include_str!("test_files/domains_ldhname_ns1_arin_net.json"))
.unwrap();
let actual = RdapResponse::try_from(expected).unwrap();
assert!(matches!(actual, RdapResponse::DomainSearchResults(_)));
}
#[test]
fn test_response_is_entity_search_results() {
let expected: Value =
serde_json::from_str(include_str!("test_files/entities_fn_arin.json")).unwrap();
let actual = RdapResponse::try_from(expected).unwrap();
assert!(matches!(actual, RdapResponse::EntitySearchResults(_)));
}
#[test]
fn test_response_is_ip_search_results() {
let expected = r#"
{
"rdapConformance": [
"rdap_level_0"
],
"ipSearchResults": [
{
"objectClassName": "ip network",
"handle": "NET-10-0-0-0",
"startAddress": "10.0.0.0",
"endAddress": "10.0.0.255",
"ipVersion": "v4"
}
]
}
"#;
let actual =
RdapResponse::try_from(serde_json::from_str::<Value>(expected).unwrap()).unwrap();
assert!(matches!(actual, RdapResponse::IpSearchResults(_)));
}
#[test]
fn test_response_is_autnum_search_results() {
let expected = r#"
{
"rdapConformance": [
"rdap_level_0"
],
"autnumSearchResults": [
{
"objectClassName": "autnum",
"handle": "AS65000",
"startAutnum": 65000,
"endAutnum": 65000,
"name": "TEST-AS",
"status": "active"
}
]
}
"#;
let actual =
RdapResponse::try_from(serde_json::from_str::<Value>(expected).unwrap()).unwrap();
assert!(matches!(actual, RdapResponse::AutnumSearchResults(_)));
}
#[test]
fn test_response_is_help() {
let expected: Value =
serde_json::from_str(include_str!("test_files/help_nic_fr.json")).unwrap();
let actual = RdapResponse::try_from(expected).unwrap();
assert!(matches!(actual, RdapResponse::Help(_)));
}
#[test]
fn test_response_is_error() {
let expected: Value =
serde_json::from_str(include_str!("test_files/error_ripe_net.json")).unwrap();
let actual = RdapResponse::try_from(expected).unwrap();
assert!(matches!(actual, RdapResponse::ErrorResponse(_)));
}
#[test]
fn test_string_is_entity_search_results() {
let entity: Value =
serde_json::from_str(include_str!("test_files/entities_fn_arin.json")).unwrap();
let value = RdapResponse::try_from(entity).unwrap();
let actual = value.to_string();
assert_eq!(actual, "EntitySearchResults");
}
#[test]
fn test_get_related_for_non_rel_link() {
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();
let links = get_related_links(&rdap);
assert!(links.is_empty());
}
#[test]
fn test_get_related_for_rel_with_rdap_type_link() {
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();
let links = get_related_links(&rdap);
assert!(!links.is_empty());
assert_eq!(links.first().expect("empty links"), &link.href().unwrap());
}
#[test]
fn test_get_related_for_rel_link() {
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();
let links = get_related_links(&rdap);
assert!(links.is_empty());
}
#[test]
fn test_get_related_for_rel_link_that_look_like_rdap() {
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();
let links = get_related_links(&rdap);
assert!(!links.is_empty());
assert_eq!(links.first().expect("empty links"), &link.href().unwrap());
}
#[test]
fn test_get_rdap_up_and_rdap_active_link() {
let link = Link::builder()
.rel("rdap-up rdap-active")
.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();
let links =
get_relationship_links(&["rdap-up".to_string(), "rdap-active".to_string()], &rdap);
assert!(!links.is_empty());
assert_eq!(links.first().expect("empty links"), &link.href().unwrap());
}
#[test]
fn test_get_rdap_active_and_rdap_up_link() {
let link = Link::builder()
.rel("rdap-up rdap-active")
.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();
let links =
get_relationship_links(&["rdap-active".to_string(), "rdap-up".to_string()], &rdap);
assert!(!links.is_empty());
assert_eq!(links.first().expect("empty links"), &link.href().unwrap());
}
#[test]
fn test_get_only_one_relationship_link() {
let link = Link::builder()
.rel("rdap-up rdap-active")
.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();
let links = get_relationship_links(&["rdap-up".to_string()], &rdap);
assert!(links.is_empty());
}
#[test]
fn test_get_too_many_relationship_link() {
let link = Link::builder()
.rel("rdap-up")
.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();
let links =
get_relationship_links(&["rdap-active".to_string(), "rdap-up".to_string()], &rdap);
assert!(links.is_empty());
}
}