1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use tracing::{debug, warn};
4
5use crate::error::Result;
6use crate::rdap::{RdapClient, RdapResponse};
7use crate::whois::{WhoisClient, WhoisResponse};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10#[serde(tag = "source", rename_all = "lowercase")]
11pub enum LookupResult {
12 Rdap {
13 data: Box<RdapResponse>,
14 #[serde(skip_serializing_if = "Option::is_none")]
15 whois_fallback: Option<WhoisResponse>,
16 },
17 Whois {
18 data: WhoisResponse,
19 rdap_error: Option<String>,
20 },
21}
22
23impl LookupResult {
24 pub fn domain_name(&self) -> Option<String> {
25 match self {
26 LookupResult::Rdap { data, .. } => data.domain_name().map(String::from),
27 LookupResult::Whois { data, .. } => Some(data.domain.clone()),
28 }
29 }
30
31 pub fn registrar(&self) -> Option<String> {
32 match self {
33 LookupResult::Rdap { data, whois_fallback } => {
34 data.get_registrar().or_else(|| {
35 whois_fallback.as_ref().and_then(|w| w.registrar.clone())
36 })
37 }
38 LookupResult::Whois { data, .. } => data.registrar.clone(),
39 }
40 }
41
42 pub fn is_rdap(&self) -> bool {
43 matches!(self, LookupResult::Rdap { .. })
44 }
45
46 pub fn is_whois(&self) -> bool {
47 matches!(self, LookupResult::Whois { .. })
48 }
49
50 pub fn expiration_info(&self) -> (Option<DateTime<Utc>>, Option<String>) {
52 match self {
53 LookupResult::Rdap { data, whois_fallback } => {
54 let expiration_date = data
56 .events
57 .iter()
58 .find(|e| e.event_action == "expiration")
59 .and_then(|e| e.parsed_date())
60 .or_else(|| {
61 whois_fallback.as_ref().and_then(|w| w.expiration_date)
63 });
64
65 let registrar = data.get_registrar().or_else(|| {
66 whois_fallback.as_ref().and_then(|w| w.registrar.clone())
67 });
68
69 (expiration_date, registrar)
70 }
71 LookupResult::Whois { data, .. } => {
72 (data.expiration_date, data.registrar.clone())
73 }
74 }
75 }
76}
77
78#[derive(Debug, Clone)]
79pub struct SmartLookup {
80 rdap_client: RdapClient,
81 whois_client: WhoisClient,
82 prefer_rdap: bool,
83 include_fallback: bool,
84}
85
86impl Default for SmartLookup {
87 fn default() -> Self {
88 Self::new()
89 }
90}
91
92impl SmartLookup {
93 pub fn new() -> Self {
94 Self {
95 rdap_client: RdapClient::new(),
96 whois_client: WhoisClient::new(),
97 prefer_rdap: true,
98 include_fallback: false,
99 }
100 }
101
102 pub fn prefer_rdap(mut self, prefer: bool) -> Self {
104 self.prefer_rdap = prefer;
105 self
106 }
107
108 pub fn include_fallback(mut self, include: bool) -> Self {
110 self.include_fallback = include;
111 self
112 }
113
114 pub async fn lookup(&self, domain: &str) -> Result<LookupResult> {
115 if self.prefer_rdap {
116 self.lookup_rdap_first(domain).await
117 } else {
118 self.lookup_whois_first(domain).await
119 }
120 }
121
122 async fn lookup_rdap_first(&self, domain: &str) -> Result<LookupResult> {
123 debug!(domain = %domain, "Attempting RDAP lookup first");
124
125 match self.rdap_client.lookup_domain(domain).await {
126 Ok(rdap_data) => {
127 if self.is_rdap_response_useful(&rdap_data) {
129 debug!("RDAP lookup successful");
130
131 let whois_fallback = if self.include_fallback {
133 match self.whois_client.lookup(domain).await {
134 Ok(whois) => Some(whois),
135 Err(e) => {
136 debug!(error = %e, "WHOIS fallback failed");
137 None
138 }
139 }
140 } else {
141 None
142 };
143
144 Ok(LookupResult::Rdap {
145 data: Box::new(rdap_data),
146 whois_fallback,
147 })
148 } else {
149 debug!("RDAP response lacks useful data, falling back to WHOIS");
150 self.fallback_to_whois(domain, Some("RDAP response incomplete")).await
151 }
152 }
153 Err(e) => {
154 warn!(error = %e, "RDAP lookup failed, falling back to WHOIS");
155 self.fallback_to_whois(domain, Some(&e.to_string())).await
156 }
157 }
158 }
159
160 async fn lookup_whois_first(&self, domain: &str) -> Result<LookupResult> {
161 debug!(domain = %domain, "Attempting WHOIS lookup first");
162
163 match self.whois_client.lookup(domain).await {
164 Ok(whois_data) => {
165 Ok(LookupResult::Whois {
166 data: whois_data,
167 rdap_error: None,
168 })
169 }
170 Err(e) => {
171 warn!(error = %e, "WHOIS lookup failed, trying RDAP");
172 let rdap_data = self.rdap_client.lookup_domain(domain).await?;
174 Ok(LookupResult::Rdap {
175 data: Box::new(rdap_data),
176 whois_fallback: None,
177 })
178 }
179 }
180 }
181
182 async fn fallback_to_whois(&self, domain: &str, rdap_error: Option<&str>) -> Result<LookupResult> {
183 let whois_data = self.whois_client.lookup(domain).await?;
184 Ok(LookupResult::Whois {
185 data: whois_data,
186 rdap_error: rdap_error.map(String::from),
187 })
188 }
189
190 fn is_rdap_response_useful(&self, response: &RdapResponse) -> bool {
191 let has_name = response.ldh_name.is_some() || response.unicode_name.is_some();
193 let has_dates = response.events.iter().any(|e| {
194 e.event_action == "registration" || e.event_action == "expiration"
195 });
196 let has_entities = !response.entities.is_empty();
197 let has_nameservers = !response.nameservers.is_empty();
198 let has_status = !response.status.is_empty();
199
200 has_name && (has_dates || has_entities || has_nameservers || has_status)
202 }
203}