use std::fmt;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::domain::{DomainName, Tld};
use crate::error::{Error, Result};
pub const WHOIS_PORT: u16 = 43;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum Endpoint {
Whois(WhoisEndpoint),
Rdap(RdapEndpoint),
}
impl Endpoint {
pub fn whois(host: impl Into<String>) -> Self {
Endpoint::Whois(WhoisEndpoint::new(host, WHOIS_PORT))
}
pub fn rdap(base: impl Into<String>) -> Self {
Endpoint::Rdap(RdapEndpoint::new(base))
}
pub fn parse(spec: &str) -> Result<Self> {
let spec = spec.trim();
if spec.is_empty() {
return Err(Error::Definitions("empty endpoint".into()));
}
if let Some(rest) = spec.strip_prefix("socket://") {
return WhoisEndpoint::parse(rest).map(Endpoint::Whois);
}
if spec.starts_with("http://") || spec.starts_with("https://") {
return Ok(Endpoint::Rdap(RdapEndpoint::new(spec)));
}
if spec.contains("://") {
return Err(Error::Definitions(format!(
"unsupported endpoint scheme in {spec:?}"
)));
}
WhoisEndpoint::parse(spec).map(Endpoint::Whois)
}
pub fn is_whois(&self) -> bool {
matches!(self, Endpoint::Whois(_))
}
pub fn is_rdap(&self) -> bool {
matches!(self, Endpoint::Rdap(_))
}
pub fn address(&self) -> String {
match self {
Endpoint::Whois(endpoint) => endpoint.address(),
Endpoint::Rdap(endpoint) => endpoint.base().to_string(),
}
}
}
impl fmt::Display for Endpoint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Endpoint::Whois(endpoint) => write!(f, "whois://{}", endpoint.address()),
Endpoint::Rdap(endpoint) => f.write_str(endpoint.base()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct WhoisEndpoint {
host: String,
port: u16,
}
impl WhoisEndpoint {
pub fn new(host: impl Into<String>, port: u16) -> Self {
WhoisEndpoint {
host: host.into().trim().trim_matches('/').to_ascii_lowercase(),
port,
}
}
pub fn parse(spec: &str) -> Result<Self> {
let spec = spec.trim().trim_matches('/');
let (host, port) = match spec.rsplit_once(':') {
Some((host, port)) => {
let port = port.parse::<u16>().map_err(|_| {
Error::Definitions(format!("{spec:?} has an invalid WHOIS port"))
})?;
(host, port)
}
None => (spec, WHOIS_PORT),
};
if host.is_empty() {
return Err(Error::Definitions(format!("{spec:?} has no WHOIS host")));
}
Ok(WhoisEndpoint::new(host, port))
}
pub fn host(&self) -> &str {
&self.host
}
pub fn port(&self) -> u16 {
self.port
}
pub fn address(&self) -> String {
if self.port == WHOIS_PORT {
self.host.clone()
} else {
format!("{}:{}", self.host, self.port)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct RdapEndpoint {
base: String,
}
impl RdapEndpoint {
pub fn new(base: impl Into<String>) -> Self {
RdapEndpoint {
base: base.into().trim().to_string(),
}
}
pub fn base(&self) -> &str {
&self.base
}
pub fn query_url(&self, domain: &str) -> String {
let base = self.base.trim_end_matches('/');
if base.ends_with("/domain") {
format!("{base}/{domain}")
} else {
format!("{base}/domain/{domain}")
}
}
pub fn is_plaintext(&self) -> bool {
self.base.starts_with("http://")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum IdnForm {
#[default]
Ascii,
Unicode,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Registry {
tlds: Vec<Tld>,
endpoints: Vec<Endpoint>,
available_markers: Vec<String>,
premium_markers: Vec<String>,
thin: bool,
idn: IdnForm,
available_when_empty: bool,
note: Option<String>,
}
impl Registry {
pub fn builder(tlds: impl IntoIterator<Item = Tld>) -> RegistryBuilder {
RegistryBuilder {
registry: Registry {
tlds: tlds.into_iter().collect(),
endpoints: Vec::new(),
available_markers: Vec::new(),
premium_markers: Vec::new(),
thin: false,
idn: IdnForm::default(),
available_when_empty: false,
note: None,
},
}
}
pub fn tlds(&self) -> &[Tld] {
&self.tlds
}
pub fn endpoints(&self) -> &[Endpoint] {
&self.endpoints
}
pub fn available_markers(&self) -> &[String] {
&self.available_markers
}
pub fn premium_markers(&self) -> &[String] {
&self.premium_markers
}
pub fn is_thin(&self) -> bool {
self.thin
}
pub fn idn_form(&self) -> IdnForm {
self.idn
}
pub fn available_when_empty(&self) -> bool {
self.available_when_empty
}
pub fn note(&self) -> Option<&str> {
self.note.as_deref()
}
pub fn endpoints_matching(&self, kind: EndpointKind) -> impl Iterator<Item = &Endpoint> {
self.endpoints.iter().filter(move |endpoint| match kind {
EndpointKind::Whois => endpoint.is_whois(),
EndpointKind::Rdap => endpoint.is_rdap(),
})
}
pub fn wire_form(&self, name: &DomainName) -> String {
match self.idn {
IdnForm::Ascii => name.as_ascii().to_string(),
IdnForm::Unicode => name.as_unicode().to_string(),
}
}
pub fn overlay(&self, other: &Registry) -> Registry {
let pick = |theirs: &Vec<String>, ours: &Vec<String>| {
if theirs.is_empty() {
ours.clone()
} else {
theirs.clone()
}
};
Registry {
tlds: if other.tlds.is_empty() {
self.tlds.clone()
} else {
other.tlds.clone()
},
endpoints: if other.endpoints.is_empty() {
self.endpoints.clone()
} else {
other.endpoints.clone()
},
available_markers: pick(&other.available_markers, &self.available_markers),
premium_markers: pick(&other.premium_markers, &self.premium_markers),
thin: self.thin || other.thin,
idn: if other.idn == IdnForm::default() {
self.idn
} else {
other.idn
},
available_when_empty: self.available_when_empty || other.available_when_empty,
note: other.note.clone().or_else(|| self.note.clone()),
}
}
pub fn union(&self, other: &Registry) -> Registry {
let mut merged = self.overlay(other);
merged.endpoints = self.endpoints.clone();
for endpoint in &other.endpoints {
if !merged.endpoints.contains(endpoint) {
merged.endpoints.push(endpoint.clone());
}
}
let mut tlds = self.tlds.clone();
for tld in &other.tlds {
if !tlds.contains(tld) {
tlds.push(tld.clone());
}
}
merged.tlds = tlds;
merged
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EndpointKind {
Whois,
Rdap,
}
#[derive(Debug, Clone)]
pub struct RegistryBuilder {
registry: Registry,
}
impl RegistryBuilder {
pub fn endpoint(mut self, endpoint: Endpoint) -> Self {
self.registry.endpoints.push(endpoint);
self
}
pub fn endpoints(mut self, endpoints: impl IntoIterator<Item = Endpoint>) -> Self {
self.registry.endpoints.extend(endpoints);
self
}
pub fn available_marker(mut self, marker: impl Into<String>) -> Self {
self.registry.available_markers.push(marker.into());
self
}
pub fn available_markers(mut self, markers: impl IntoIterator<Item = String>) -> Self {
self.registry.available_markers.extend(markers);
self
}
pub fn premium_marker(mut self, marker: impl Into<String>) -> Self {
self.registry.premium_markers.push(marker.into());
self
}
pub fn premium_markers(mut self, markers: impl IntoIterator<Item = String>) -> Self {
self.registry.premium_markers.extend(markers);
self
}
pub fn thin(mut self, thin: bool) -> Self {
self.registry.thin = thin;
self
}
pub fn idn_form(mut self, idn: IdnForm) -> Self {
self.registry.idn = idn;
self
}
pub fn available_when_empty(mut self, allow: bool) -> Self {
self.registry.available_when_empty = allow;
self
}
pub fn note(mut self, note: impl Into<String>) -> Self {
self.registry.note = Some(note.into());
self
}
pub fn build(self) -> Registry {
self.registry
}
pub fn build_shared(self) -> Arc<Registry> {
Arc::new(self.registry)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_the_compact_endpoint_forms() {
assert_eq!(
Endpoint::parse("whois.nic.uk").unwrap(),
Endpoint::Whois(WhoisEndpoint::new("whois.nic.uk", 43))
);
assert_eq!(
Endpoint::parse("socket://whois.nic.uk/").unwrap(),
Endpoint::Whois(WhoisEndpoint::new("whois.nic.uk", 43))
);
assert_eq!(
Endpoint::parse("whois.example:4343").unwrap(),
Endpoint::Whois(WhoisEndpoint::new("whois.example", 4343))
);
assert!(Endpoint::parse("https://rdap.example/").unwrap().is_rdap());
assert!(Endpoint::parse("").is_err());
assert!(Endpoint::parse("ftp://whois.example").is_err());
assert!(Endpoint::parse("whois.example:not-a-port").is_err());
}
#[test]
fn whois_address_hides_the_default_port() {
assert_eq!(
WhoisEndpoint::new("whois.nic.uk", 43).address(),
"whois.nic.uk"
);
assert_eq!(
WhoisEndpoint::new("whois.nic.uk", 4343).address(),
"whois.nic.uk:4343"
);
}
#[test]
fn rdap_query_url_normalises_either_base_form() {
let expected = "https://rdap.example/v1/domain/example.com";
for base in [
"https://rdap.example/v1",
"https://rdap.example/v1/",
"https://rdap.example/v1/domain",
"https://rdap.example/v1/domain/",
] {
assert_eq!(RdapEndpoint::new(base).query_url("example.com"), expected);
}
}
#[test]
fn wire_form_follows_the_idn_setting() {
let name = DomainName::parse("münchen.de").unwrap();
let tlds = vec![Tld::parse("de").unwrap()];
let ascii = Registry::builder(tlds.clone()).build();
assert_eq!(ascii.wire_form(&name), "xn--mnchen-3ya.de");
let unicode = Registry::builder(tlds).idn_form(IdnForm::Unicode).build();
assert_eq!(unicode.wire_form(&name), "münchen.de");
}
#[test]
fn overlay_takes_the_other_side_where_it_speaks() {
let tlds = vec![Tld::parse("example").unwrap()];
let base = Registry::builder(tlds.clone())
.endpoint(Endpoint::whois("old.example"))
.available_marker("No match")
.note("bundled")
.build();
let patch = Registry::builder(tlds)
.endpoint(Endpoint::whois("new.example"))
.thin(true)
.build();
let merged = base.overlay(&patch);
assert_eq!(merged.endpoints()[0].address(), "new.example");
assert_eq!(merged.available_markers(), ["No match"]);
assert_eq!(merged.note(), Some("bundled"));
assert!(merged.is_thin());
}
}