domain_check_lib/checker.rs
1//! Main domain checker implementation.
2//!
3//! This module provides the primary `DomainChecker` struct that orchestrates
4//! domain availability checking using RDAP, WHOIS, and bootstrap protocols.
5
6use crate::error::DomainCheckError;
7use crate::protocols::{RdapClient, WhoisClient};
8use crate::types::{CheckConfig, CheckMethod, DomainResult};
9use crate::utils::validate_domain;
10use futures::stream::{Stream, StreamExt};
11use std::pin::Pin;
12use std::sync::Arc;
13use tokio::sync::Semaphore;
14
15/// Check a single domain using the provided clients (for concurrent processing).
16///
17/// This is a helper function that implements the same logic as `check_domain`
18/// but works with cloned client instances for concurrent execution.
19async fn check_single_domain_concurrent(
20 domain: &str,
21 rdap_client: &RdapClient,
22 whois_client: &WhoisClient,
23 config: &CheckConfig,
24) -> Result<DomainResult, DomainCheckError> {
25 // Validate domain format first
26 validate_domain(domain)?;
27
28 // Try RDAP first
29 match rdap_client.check_domain(domain).await {
30 Ok(result) => {
31 // RDAP succeeded, filter info based on configuration
32 let mut filtered_result = result;
33 if !config.detailed_info {
34 filtered_result.info = None;
35 }
36 Ok(filtered_result)
37 }
38 Err(rdap_error) => {
39 // RDAP failed, try WHOIS fallback if enabled
40 if config.enable_whois_fallback {
41 match whois_client.check_domain(domain).await {
42 Ok(whois_result) => {
43 let mut filtered_result = whois_result;
44 if !config.detailed_info {
45 filtered_result.info = None;
46 }
47 Ok(filtered_result)
48 }
49 Err(whois_error) => {
50 // Both RDAP and WHOIS failed, determine best response
51
52 // Check if either error indicates the domain is available
53 if rdap_error.indicates_available() || whois_error.indicates_available() {
54 Ok(DomainResult {
55 domain: domain.to_string(),
56 available: Some(true),
57 info: None,
58 check_duration: None,
59 method_used: CheckMethod::Rdap,
60 error_message: None,
61 })
62 }
63 // Check if it's an unknown TLD or truly ambiguous case
64 else if matches!(rdap_error, DomainCheckError::BootstrapError { .. })
65 || matches!(whois_error, DomainCheckError::BootstrapError { .. })
66 || whois_error
67 .to_string()
68 .contains("Unable to determine domain status")
69 {
70 // Return unknown status for invalid TLDs or ambiguous cases
71 Ok(DomainResult {
72 domain: domain.to_string(),
73 available: None, // Unknown status
74 info: None,
75 check_duration: None,
76 method_used: CheckMethod::Unknown,
77 error_message: Some(
78 "Unknown TLD or unable to determine status".to_string(),
79 ),
80 })
81 } else {
82 // Return the RDAP error as it's usually more informative
83 Err(rdap_error)
84 }
85 }
86 }
87 } else {
88 // No fallback enabled, return RDAP error
89 Err(rdap_error)
90 }
91 }
92 }
93}
94
95/// Main domain checker that coordinates availability checking operations.
96///
97/// The `DomainChecker` handles all aspects of domain checking including:
98/// - Protocol selection (RDAP vs WHOIS)
99/// - Concurrent processing
100/// - Error handling and retries
101/// - Result formatting
102///
103/// # Example
104///
105/// ```rust,no_run
106/// use domain_check_lib::{DomainChecker, CheckConfig};
107///
108/// #[tokio::main]
109/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
110/// let checker = DomainChecker::new();
111/// let result = checker.check_domain("example.com").await?;
112/// println!("Available: {:?}", result.available);
113/// Ok(())
114/// }
115/// ```
116pub struct DomainChecker {
117 /// Configuration settings for this checker instance
118 config: CheckConfig,
119 /// RDAP client for modern domain checking
120 rdap_client: RdapClient,
121 /// WHOIS client for fallback domain checking
122 whois_client: WhoisClient,
123}
124
125impl DomainChecker {
126 /// Create a new domain checker with default configuration.
127 ///
128 /// Default settings:
129 /// - Concurrency: 10
130 /// - Timeout: 5 seconds
131 /// - WHOIS fallback: enabled
132 /// - Bootstrap: disabled
133 /// - Detailed info: disabled
134 pub fn new() -> Self {
135 let config = CheckConfig::default();
136 let rdap_client = RdapClient::with_config(config.rdap_timeout, config.enable_bootstrap)
137 .expect("Failed to create RDAP client");
138 let whois_client = WhoisClient::with_timeout(config.whois_timeout);
139
140 Self {
141 config,
142 rdap_client,
143 whois_client,
144 }
145 }
146
147 /// Create a new domain checker with custom configuration.
148 ///
149 /// # Example
150 ///
151 /// ```rust
152 /// use domain_check_lib::{DomainChecker, CheckConfig};
153 /// use std::time::Duration;
154 ///
155 /// let config = CheckConfig::default()
156 /// .with_concurrency(20)
157 /// .with_timeout(Duration::from_secs(10))
158 /// .with_detailed_info(true);
159 ///
160 /// let checker = DomainChecker::with_config(config);
161 /// ```
162 pub fn with_config(config: CheckConfig) -> Self {
163 let rdap_client = RdapClient::with_config(config.rdap_timeout, config.enable_bootstrap)
164 .expect("Failed to create RDAP client");
165 let whois_client = WhoisClient::with_timeout(config.whois_timeout);
166
167 Self {
168 config,
169 rdap_client,
170 whois_client,
171 }
172 }
173
174 /// Check availability of a single domain.
175 ///
176 /// This is the most basic operation - check one domain and return the result.
177 /// The domain should be a fully qualified domain name (e.g., "example.com").
178 ///
179 /// The checking process:
180 /// 1. Validates the domain format
181 /// 2. Attempts RDAP check first (modern protocol)
182 /// 3. Falls back to WHOIS if RDAP fails and fallback is enabled
183 /// 4. Returns comprehensive result with timing and method information
184 ///
185 /// # Arguments
186 ///
187 /// * `domain` - The domain name to check (e.g., "example.com")
188 ///
189 /// # Returns
190 ///
191 /// A `DomainResult` containing availability status and optional details.
192 ///
193 /// # Errors
194 ///
195 /// Returns `DomainCheckError` if:
196 /// - The domain name is invalid
197 /// - Network errors occur
198 /// - All checking methods fail
199 pub async fn check_domain(&self, domain: &str) -> Result<DomainResult, DomainCheckError> {
200 // Validate domain format first
201 validate_domain(domain)?;
202
203 // Try RDAP first
204 match self.rdap_client.check_domain(domain).await {
205 Ok(result) => {
206 // RDAP succeeded, filter info based on configuration
207 Ok(self.filter_result_info(result))
208 }
209 Err(rdap_error) => {
210 // RDAP failed, try WHOIS fallback if enabled
211 if self.config.enable_whois_fallback {
212 match self.whois_client.check_domain(domain).await {
213 Ok(whois_result) => Ok(self.filter_result_info(whois_result)),
214 Err(whois_error) => {
215 // Both RDAP and WHOIS failed, determine best response
216
217 // Check if either error indicates the domain is available
218 if rdap_error.indicates_available() || whois_error.indicates_available()
219 {
220 Ok(DomainResult {
221 domain: domain.to_string(),
222 available: Some(true),
223 info: None,
224 check_duration: None,
225 method_used: CheckMethod::Rdap,
226 error_message: None,
227 })
228 }
229 // Check if it's an unknown TLD or truly ambiguous case
230 else if matches!(rdap_error, DomainCheckError::BootstrapError { .. })
231 || matches!(whois_error, DomainCheckError::BootstrapError { .. })
232 || whois_error
233 .to_string()
234 .contains("Unable to determine domain status")
235 {
236 // Return unknown status for invalid TLDs or ambiguous cases
237 Ok(DomainResult {
238 domain: domain.to_string(),
239 available: None, // Unknown status
240 info: None,
241 check_duration: None,
242 method_used: CheckMethod::Unknown,
243 error_message: Some(
244 "Unknown TLD or unable to determine status".to_string(),
245 ),
246 })
247 } else {
248 // Return the most informative error
249 Err(rdap_error)
250 }
251 }
252 }
253 } else {
254 // No fallback enabled, return RDAP error
255 Err(rdap_error)
256 }
257 }
258 }
259 }
260
261 /// Filter domain result info based on configuration.
262 ///
263 /// If detailed_info is disabled, removes the info field to keep results clean.
264 fn filter_result_info(&self, mut result: DomainResult) -> DomainResult {
265 if !self.config.detailed_info {
266 result.info = None;
267 }
268 result
269 }
270
271 /// Check availability of multiple domains concurrently.
272 ///
273 /// This method processes all domains in parallel according to the
274 /// concurrency setting, then returns all results at once.
275 ///
276 /// # Arguments
277 ///
278 /// * `domains` - Slice of domain names to check
279 ///
280 /// # Returns
281 ///
282 /// Vector of `DomainResult` in the same order as input domains.
283 ///
284 /// # Example
285 ///
286 /// ```rust,no_run
287 /// use domain_check_lib::DomainChecker;
288 ///
289 /// #[tokio::main]
290 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
291 /// let checker = DomainChecker::new();
292 /// let domains = vec!["example.com".to_string(), "google.com".to_string(), "test.org".to_string()];
293 /// let results = checker.check_domains(&domains).await?;
294 ///
295 /// for result in results {
296 /// println!("{}: {:?}", result.domain, result.available);
297 /// }
298 /// Ok(())
299 /// }
300 /// ```
301 pub async fn check_domains(
302 &self,
303 domains: &[String],
304 ) -> Result<Vec<DomainResult>, DomainCheckError> {
305 if domains.is_empty() {
306 return Ok(Vec::new());
307 }
308
309 // Create semaphore to limit concurrent operations
310 let semaphore = Arc::new(Semaphore::new(self.config.concurrency));
311 let mut handles = Vec::new();
312
313 // Spawn concurrent tasks for each domain
314 for (index, domain) in domains.iter().enumerate() {
315 let domain = domain.clone();
316 let semaphore = Arc::clone(&semaphore);
317
318 // Clone the checker components we need
319 let rdap_client = self.rdap_client.clone();
320 let whois_client = self.whois_client.clone();
321 let config = self.config.clone();
322
323 let handle = tokio::spawn(async move {
324 // Acquire semaphore permit
325 let _permit = semaphore.acquire().await.unwrap();
326
327 // Check this domain
328 let result =
329 check_single_domain_concurrent(&domain, &rdap_client, &whois_client, &config)
330 .await;
331
332 // Return with original index to maintain order
333 (index, result)
334 });
335
336 handles.push(handle);
337 }
338
339 // Wait for all tasks to complete and collect results
340 let mut indexed_results = Vec::new();
341 for handle in handles {
342 match handle.await {
343 Ok((index, result)) => indexed_results.push((index, result)),
344 Err(e) => {
345 return Err(DomainCheckError::internal(format!(
346 "Concurrent task failed: {}",
347 e
348 )));
349 }
350 }
351 }
352
353 // Sort by original index to maintain input order
354 indexed_results.sort_by_key(|(index, _)| *index);
355
356 // Extract results, converting errors to DomainResult with error info
357 let results = indexed_results
358 .into_iter()
359 .map(|(_, result)| match result {
360 Ok(domain_result) => domain_result,
361 Err(e) => DomainResult {
362 domain: domains[0].clone(), // We'll fix this in the concurrent function
363 available: None,
364 info: None,
365 check_duration: None,
366 method_used: CheckMethod::Unknown,
367 error_message: Some(e.to_string()),
368 },
369 })
370 .collect();
371
372 Ok(results)
373 }
374
375 /// Check domains and return results as a stream.
376 ///
377 /// This method yields results as they become available, which is useful
378 /// for real-time updates or when processing large numbers of domains.
379 /// Results are returned in the order they complete, not input order.
380 ///
381 /// # Arguments
382 ///
383 /// * `domains` - Slice of domain names to check
384 ///
385 /// # Returns
386 ///
387 /// A stream that yields `DomainResult` items as they complete.
388 ///
389 /// # Example
390 ///
391 /// ```rust,no_run
392 /// use domain_check_lib::DomainChecker;
393 /// use futures::StreamExt;
394 ///
395 /// #[tokio::main]
396 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
397 /// let checker = DomainChecker::new();
398 /// let domains = vec!["example.com".to_string(), "google.com".to_string()];
399 ///
400 /// let mut stream = checker.check_domains_stream(&domains);
401 /// while let Some(result) = stream.next().await {
402 /// match result {
403 /// Ok(domain_result) => println!("✓ {}: {:?}", domain_result.domain, domain_result.available),
404 /// Err(e) => println!("✗ Error: {}", e),
405 /// }
406 /// }
407 /// Ok(())
408 /// }
409 /// ```
410 pub fn check_domains_stream(
411 &self,
412 domains: &[String],
413 ) -> Pin<Box<dyn Stream<Item = Result<DomainResult, DomainCheckError>> + Send + '_>> {
414 let domains = domains.to_vec();
415 let semaphore = Arc::new(Semaphore::new(self.config.concurrency));
416
417 // Create stream of futures
418 let stream = futures::stream::iter(domains)
419 .map(move |domain| {
420 let semaphore = Arc::clone(&semaphore);
421 let rdap_client = self.rdap_client.clone();
422 let whois_client = self.whois_client.clone();
423 let config = self.config.clone();
424
425 async move {
426 // Acquire semaphore permit
427 let _permit = semaphore.acquire().await.unwrap();
428
429 // Check domain
430 check_single_domain_concurrent(&domain, &rdap_client, &whois_client, &config)
431 .await
432 }
433 })
434 // Buffer unordered allows concurrent execution while maintaining the stream interface
435 .buffer_unordered(self.config.concurrency);
436
437 Box::pin(stream)
438 }
439
440 /// Read domain names from a file and check their availability.
441 ///
442 /// The file should contain one domain name per line. Empty lines and
443 /// lines starting with '#' are ignored as comments.
444 ///
445 /// # Arguments
446 ///
447 /// * `file_path` - Path to the file containing domain names
448 ///
449 /// # Returns
450 ///
451 /// Vector of `DomainResult` for all valid domains in the file.
452 ///
453 /// # Errors
454 ///
455 /// Returns `DomainCheckError` if:
456 /// - The file cannot be read
457 /// - The file contains too many domains (over limit)
458 /// - No valid domains are found in the file
459 pub async fn check_domains_from_file(
460 &self,
461 file_path: &str,
462 ) -> Result<Vec<DomainResult>, DomainCheckError> {
463 use std::fs::File;
464 use std::io::{BufRead, BufReader};
465 use std::path::Path;
466
467 // Check if file exists
468 let path = Path::new(file_path);
469 if !path.exists() {
470 return Err(DomainCheckError::file_error(file_path, "File not found"));
471 }
472
473 // Read domains from file
474 let file = File::open(path).map_err(|e| {
475 DomainCheckError::file_error(file_path, format!("Cannot open file: {}", e))
476 })?;
477
478 let reader = BufReader::new(file);
479 let mut domains = Vec::new();
480 let mut line_num = 0;
481
482 for line in reader.lines() {
483 line_num += 1;
484 match line {
485 Ok(line) => {
486 let trimmed = line.trim();
487
488 // Skip empty lines and comments
489 if trimmed.is_empty() || trimmed.starts_with('#') {
490 continue;
491 }
492
493 // Handle inline comments
494 let domain_part = trimmed.split('#').next().unwrap_or("").trim();
495 if !domain_part.is_empty() && domain_part.len() >= 2 {
496 domains.push(domain_part.to_string());
497 }
498 }
499 Err(e) => {
500 return Err(DomainCheckError::file_error(
501 file_path,
502 format!("Error reading line {}: {}", line_num, e),
503 ));
504 }
505 }
506 }
507
508 if domains.is_empty() {
509 return Err(DomainCheckError::file_error(
510 file_path,
511 "No valid domains found in file",
512 ));
513 }
514
515 // Check domains using existing concurrent logic
516 self.check_domains(&domains).await
517 }
518
519 /// Get the current configuration for this checker.
520 pub fn config(&self) -> &CheckConfig {
521 &self.config
522 }
523
524 /// Update the configuration for this checker.
525 ///
526 /// This allows modifying settings like concurrency or timeout
527 /// after the checker has been created. Note that this will recreate
528 /// the internal protocol clients with the new settings.
529 pub fn set_config(&mut self, config: CheckConfig) {
530 // Recreate clients with new configuration
531 self.rdap_client = RdapClient::with_config(config.rdap_timeout, config.enable_bootstrap)
532 .expect("Failed to recreate RDAP client");
533 self.whois_client = WhoisClient::with_timeout(config.whois_timeout);
534 self.config = config;
535 }
536}
537
538impl Default for DomainChecker {
539 fn default() -> Self {
540 Self::new()
541 }
542}