1
0
Fork 0
icann-rdap/icann-rdap-common/src/response/mod.rs
Daniel Baumann b06d3acde8
Adding upstream version 0.0.22.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-05-08 18:41:54 +02:00

652 lines
20 KiB
Rust

//! 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());
}
}