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