1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
//! Domain availability checking.
//!
//! Determines if a domain is available for registration by interpreting
//! WHOIS/RDAP "not found" responses.
use serde::{Deserialize, Serialize};
use tracing::{debug, instrument};
use crate::error::Result;
use crate::rdap::RdapClient;
use crate::whois::WhoisClient;
/// Result of a domain availability check.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AvailabilityResult {
/// The domain that was checked.
pub domain: String,
/// Whether the domain appears to be available for registration.
pub available: bool,
/// Confidence level of the result ("high", "medium", "low").
pub confidence: String,
/// How availability was determined.
pub method: String,
/// Additional details about the check.
pub details: Option<String>,
}
/// Checks domain availability by attempting lookups and interpreting failures.
#[derive(Debug, Clone)]
pub struct AvailabilityChecker {
rdap_client: RdapClient,
whois_client: WhoisClient,
}
impl Default for AvailabilityChecker {
fn default() -> Self {
Self::new()
}
}
impl AvailabilityChecker {
pub fn new() -> Self {
Self {
rdap_client: RdapClient::new(),
whois_client: WhoisClient::new(),
}
}
/// Check if a domain is available for registration.
#[instrument(skip(self), fields(domain = %domain))]
pub async fn check(&self, domain: &str) -> Result<AvailabilityResult> {
let domain = crate::validation::normalize_domain(domain)?;
debug!(domain = %domain, "Checking domain availability");
// Try RDAP first - it gives structured error responses
match self.rdap_client.lookup_domain(&domain).await {
Ok(response) => {
// Domain exists in RDAP - check status
let statuses: Vec<String> = response.status.clone();
let is_redemption = statuses
.iter()
.any(|s| s.contains("redemption") || s.contains("pending delete"));
if is_redemption {
return Ok(AvailabilityResult {
domain,
available: false,
confidence: "medium".to_string(),
method: "rdap".to_string(),
details: Some("Domain is in redemption/pending delete period".to_string()),
});
}
Ok(AvailabilityResult {
domain,
available: false,
confidence: "high".to_string(),
method: "rdap".to_string(),
details: Some(format!(
"Domain is registered (status: {})",
statuses.join(", ")
)),
})
}
Err(rdap_err) => {
debug!(error = %rdap_err, "RDAP lookup failed, falling back to WHOIS");
match self.whois_client.lookup(&domain).await {
Ok(whois_response) => {
if whois_response.is_available() {
Ok(AvailabilityResult {
domain,
available: true,
confidence: "high".to_string(),
method: "whois".to_string(),
details: Some(
"WHOIS indicates domain is not registered".to_string(),
),
})
} else {
Ok(AvailabilityResult {
domain,
available: false,
confidence: "high".to_string(),
method: "whois".to_string(),
details: whois_response
.registrar
.map(|r| format!("Registered with {}", r)),
})
}
}
Err(whois_err) => {
// Both failed - domain might be available or queries blocked
let whois_msg = whois_err.to_string().to_lowercase();
let likely_available = whois_msg.contains("no match")
|| whois_msg.contains("not found")
|| whois_msg.contains("no data found")
|| whois_msg.contains("no entries found");
if likely_available {
Ok(AvailabilityResult {
domain,
available: true,
confidence: "medium".to_string(),
method: "whois_error".to_string(),
details: Some(
"WHOIS server indicates no matching records".to_string(),
),
})
} else {
// Both queries failed with non-"not found" errors.
// We genuinely don't know — could be registered, could be
// blocked by the registrar, or servers could be down.
// Default to available=false to avoid misleading the user
// into thinking they can register a domain that's actually taken.
let rdap_detail = rdap_err.to_string();
let whois_detail = whois_err.to_string();
Ok(AvailabilityResult {
domain,
available: false,
confidence: "none".to_string(),
method: "inconclusive".to_string(),
details: Some(format!(
"Could not determine availability. RDAP: {}. WHOIS: {}",
rdap_detail, whois_detail
)),
})
}
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_availability_result_serialization() {
let result = AvailabilityResult {
domain: "example.com".to_string(),
available: false,
confidence: "high".to_string(),
method: "rdap".to_string(),
details: Some("Domain is registered".to_string()),
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"available\":false"));
assert!(json.contains("\"confidence\":\"high\""));
}
}