1use reqwest::Client;
2use serde::{Deserialize, Serialize};
3use std::net::IpAddr;
4use std::sync::atomic::{AtomicUsize, Ordering};
5use std::sync::Arc;
6use std::time::{Duration, Instant};
7use tokio::process::Command;
8
9const SKIP_PATTERNS: &[&str] = &[
12 "stun.l.google.com",
13 ".cloudapp.azure.com",
14 "clients6.google.com",
15 ".cdn.cloudflare.net",
16 "rr1.sn-",
17 "rr2.sn-",
18 "rr3.sn-",
19 "rr4.sn-",
20 "rr5.sn-",
21 "e-0014.e-msedge",
22 "s-part-",
23 ".t-msedge.net",
24 "perimeterx.map",
25 "i.ytimg.com",
26 "analytics-alv.google.com",
27 "signaler-pa.clients",
28 "westus-0.in.applicationinsights",
29];
30
31const INTERNAL_PATTERNS: &[&str] = &[
32 "localhost",
33 "127.0.0.1",
34 "0.0.0.0",
35 "192.168.",
36 "10.",
37 "172.16.",
38 "172.17.",
39 "172.18.",
40 "172.19.",
41 "172.20.",
42 "172.21.",
43 "172.22.",
44 "172.23.",
45 "172.24.",
46 "172.25.",
47 "172.26.",
48 "172.27.",
49 "172.28.",
50 "172.29.",
51 "172.30.",
52 "172.31.",
53];
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct ValidationResult {
59 pub domain: String,
60 pub valid: bool,
61 pub skip_reason: Option<String>,
62 pub dns_valid: bool,
63 pub http_valid: bool,
64 pub ssl_valid: bool,
65 pub dns_info: Option<DnsValidation>,
66 pub http_info: Option<HttpValidation>,
67 pub ssl_info: Option<SslValidation>,
68 pub errors: Vec<String>,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct DnsValidation {
73 pub ip_addresses: Vec<String>,
74 pub mx_exists: bool,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct HttpValidation {
79 pub http_reachable: bool,
80 pub https_reachable: bool,
81 pub http_status: Option<u16>,
82 pub https_status: Option<u16>,
83 pub redirects_to_https: bool,
84 pub response_time_ms: u128,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct SslValidation {
89 pub ssl_available: bool,
90 pub protocol_version: String,
91 pub cipher_suite: String,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct ValidationStats {
96 pub total: usize,
97 pub valid: usize,
98 pub invalid: usize,
99 pub skipped: usize,
100 pub dns_failed: usize,
101 pub http_failed: usize,
102 pub ssl_failed: usize,
103 pub success_rate: f64,
104 pub processing_time_secs: f64,
105 pub domains_per_sec: f64,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct BulkValidationResult {
110 pub stats: ValidationStats,
111 pub valid_domains: Vec<String>,
112 pub results: Vec<ValidationResult>,
113}
114
115struct AtomicStats {
118 valid: AtomicUsize,
119 invalid: AtomicUsize,
120 skipped: AtomicUsize,
121 dns_failed: AtomicUsize,
122 http_failed: AtomicUsize,
123 ssl_failed: AtomicUsize,
124}
125
126impl AtomicStats {
127 fn new() -> Self {
128 Self {
129 valid: AtomicUsize::new(0),
130 invalid: AtomicUsize::new(0),
131 skipped: AtomicUsize::new(0),
132 dns_failed: AtomicUsize::new(0),
133 http_failed: AtomicUsize::new(0),
134 ssl_failed: AtomicUsize::new(0),
135 }
136 }
137}
138
139pub async fn validate_domain(domain: &str) -> ValidationResult {
143 let client = crate::http_client_builder()
144 .timeout(Duration::from_secs(10))
145 .danger_accept_invalid_certs(true)
146 .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
147 .redirect(reqwest::redirect::Policy::limited(5))
148 .build()
149 .unwrap_or_else(|_| Client::new());
150
151 validate_single(&client, domain).await
152}
153
154pub async fn validate_domains_bulk(
156 domains: &[String],
157 max_concurrency: usize,
158 progress_tx: Option<tokio::sync::mpsc::Sender<crate::ScanProgress>>,
159) -> BulkValidationResult {
160 let start = Instant::now();
161 let total = domains.len();
162 report_progress(
163 &progress_tx,
164 5.0,
165 format!("Starting validation for {} domain(s)", total),
166 "Info",
167 );
168
169 let client = crate::http_client_builder()
170 .timeout(Duration::from_secs(10))
171 .danger_accept_invalid_certs(true)
172 .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
173 .redirect(reqwest::redirect::Policy::limited(5))
174 .pool_max_idle_per_host(max_concurrency)
175 .build()
176 .unwrap_or_else(|_| Client::new());
177
178 let stats = Arc::new(AtomicStats::new());
179 let semaphore = Arc::new(tokio::sync::Semaphore::new(max_concurrency));
180 let completed = Arc::new(AtomicUsize::new(0));
181
182 let mut handles = Vec::with_capacity(total);
183
184 for domain in domains {
185 let client = client.clone();
186 let domain = domain.clone();
187 let stats = Arc::clone(&stats);
188 let sem = Arc::clone(&semaphore);
189 let completed = Arc::clone(&completed);
190 let progress_tx = progress_tx.clone();
191
192 handles.push(tokio::spawn(async move {
193 let _permit = sem.acquire().await.unwrap();
194 let result = validate_single(&client, &domain).await;
195
196 if result.skip_reason.is_some() {
198 stats.skipped.fetch_add(1, Ordering::Relaxed);
199 } else if result.valid {
200 stats.valid.fetch_add(1, Ordering::Relaxed);
201 } else {
202 stats.invalid.fetch_add(1, Ordering::Relaxed);
203 if !result.dns_valid {
204 stats.dns_failed.fetch_add(1, Ordering::Relaxed);
205 }
206 if !result.http_valid && result.dns_valid {
207 stats.http_failed.fetch_add(1, Ordering::Relaxed);
208 }
209 if !result.ssl_valid && result.dns_valid {
210 stats.ssl_failed.fetch_add(1, Ordering::Relaxed);
211 }
212 }
213
214 let done = completed.fetch_add(1, Ordering::Relaxed) + 1;
215 let percentage = if total > 0 {
216 5.0 + (90.0 * (done as f32 / total as f32))
217 } else {
218 95.0
219 };
220 let status = if result.skip_reason.is_some() {
221 "Info"
222 } else if result.valid {
223 "Success"
224 } else {
225 "Error"
226 };
227 let message = if let Some(reason) = &result.skip_reason {
228 format!("Skipped {}: {}", domain, reason)
229 } else if result.valid {
230 format!("Validated {}", domain)
231 } else {
232 format!("Validation failed for {}", domain)
233 };
234 report_progress(&progress_tx, percentage, message, status);
235
236 result
237 }));
238 }
239
240 let mut results = Vec::with_capacity(total);
242 for handle in handles {
243 if let Ok(result) = handle.await {
244 results.push(result);
245 }
246 }
247
248 let elapsed = start.elapsed().as_secs_f64();
249 let valid_count = stats.valid.load(Ordering::Relaxed);
250
251 let valid_domains: Vec<String> = results
252 .iter()
253 .filter(|r| r.valid)
254 .map(|r| r.domain.clone())
255 .collect();
256
257 report_progress(
258 &progress_tx,
259 100.0,
260 format!(
261 "Bulk validation complete: {} valid of {}",
262 valid_count, total
263 ),
264 "Success",
265 );
266
267 BulkValidationResult {
268 stats: ValidationStats {
269 total,
270 valid: valid_count,
271 invalid: stats.invalid.load(Ordering::Relaxed),
272 skipped: stats.skipped.load(Ordering::Relaxed),
273 dns_failed: stats.dns_failed.load(Ordering::Relaxed),
274 http_failed: stats.http_failed.load(Ordering::Relaxed),
275 ssl_failed: stats.ssl_failed.load(Ordering::Relaxed),
276 success_rate: if total > 0 {
277 (valid_count as f64 / total as f64) * 100.0
278 } else {
279 0.0
280 },
281 processing_time_secs: elapsed,
282 domains_per_sec: if elapsed > 0.0 {
283 total as f64 / elapsed
284 } else {
285 0.0
286 },
287 },
288 valid_domains,
289 results,
290 }
291}
292
293fn report_progress(
294 progress_tx: &Option<tokio::sync::mpsc::Sender<crate::ScanProgress>>,
295 percentage: f32,
296 message: impl Into<String>,
297 status: &str,
298) {
299 if let Some(tx) = progress_tx {
300 let _ = tx.try_send(crate::ScanProgress {
301 module: "Domain Validator".into(),
302 percentage,
303 message: message.into(),
304 status: status.into(),
305 });
306 }
307}
308
309async fn validate_single(client: &Client, domain: &str) -> ValidationResult {
312 let mut result = ValidationResult {
313 domain: domain.to_string(),
314 valid: false,
315 skip_reason: None,
316 dns_valid: false,
317 http_valid: false,
318 ssl_valid: false,
319 dns_info: None,
320 http_info: None,
321 ssl_info: None,
322 errors: vec![],
323 };
324
325 if let Some(reason) = should_skip(domain) {
327 result.skip_reason = Some(reason);
328 return result;
329 }
330
331 match validate_dns(domain).await {
333 Ok(dns) => {
334 result.dns_valid = true;
335 result.dns_info = Some(dns);
336 }
337 Err(e) => {
338 result.errors.push(format!("DNS: {}", e));
339 return result; }
341 }
342
343 match validate_http(client, domain).await {
345 Ok(http) => {
346 result.http_valid = http.http_reachable || http.https_reachable;
347 if !result.http_valid {
348 result
349 .errors
350 .push("HTTP: No HTTP/HTTPS connectivity".into());
351 }
352 result.http_info = Some(http);
353 }
354 Err(e) => {
355 result.errors.push(format!("HTTP: {}", e));
356 }
357 }
358
359 match validate_ssl(domain).await {
361 Ok(ssl) => {
362 result.ssl_valid = ssl.ssl_available;
363 result.ssl_info = Some(ssl);
364 }
365 Err(e) => {
366 result.errors.push(format!("SSL: {}", e));
367 }
368 }
369
370 result.valid = result.dns_valid && result.http_valid;
372 result
373}
374
375fn should_skip(domain: &str) -> Option<String> {
378 let lower = domain.to_lowercase();
379
380 for &pattern in SKIP_PATTERNS {
382 if lower.contains(pattern) {
383 return Some(format!("Matches skip pattern: {}", pattern));
384 }
385 }
386
387 if domain.parse::<IpAddr>().is_ok() {
389 return Some("IP address detected".into());
390 }
391
392 for &internal in INTERNAL_PATTERNS {
394 if lower.contains(internal) {
395 return Some("Internal/localhost domain".into());
396 }
397 }
398
399 if domain.len() < 4 || domain.len() > 253 {
401 return Some("Invalid domain length".into());
402 }
403
404 if !domain.contains('.') {
406 return Some("No TLD detected".into());
407 }
408
409 None
410}
411
412async fn validate_dns(domain: &str) -> Result<DnsValidation, String> {
415 let a_output = Command::new("dig")
417 .args(["+short", "A", domain])
418 .output()
419 .await
420 .map_err(|e| format!("dig failed: {}", e))?;
421
422 let a_records: Vec<String> = String::from_utf8_lossy(&a_output.stdout)
423 .lines()
424 .map(|s| s.trim().to_string())
425 .filter(|s| !s.is_empty() && s.parse::<IpAddr>().is_ok())
426 .collect();
427
428 if a_records.is_empty() {
429 return Err("No A records found (NXDOMAIN or empty)".into());
430 }
431
432 let mx_output = Command::new("dig")
434 .args(["+short", "MX", domain])
435 .output()
436 .await
437 .unwrap_or_else(|_| std::process::Output {
438 status: std::process::ExitStatus::default(),
439 stdout: vec![],
440 stderr: vec![],
441 });
442
443 let mx_exists = !String::from_utf8_lossy(&mx_output.stdout).trim().is_empty();
444
445 Ok(DnsValidation {
446 ip_addresses: a_records,
447 mx_exists,
448 })
449}
450
451async fn validate_http(client: &Client, domain: &str) -> Result<HttpValidation, String> {
454 let mut info = HttpValidation {
455 http_reachable: false,
456 https_reachable: false,
457 http_status: None,
458 https_status: None,
459 redirects_to_https: false,
460 response_time_ms: 0,
461 };
462
463 let start = Instant::now();
464
465 if let Ok(resp) = client.head(format!("https://{}", domain)).send().await {
467 info.https_reachable = true;
468 info.https_status = Some(resp.status().as_u16());
469 info.response_time_ms = start.elapsed().as_millis();
470
471 if resp.status().as_u16() < 500 {
472 return Ok(info);
473 }
474 }
475
476 let no_redirect_client = crate::http_client_builder()
479 .timeout(Duration::from_secs(8))
480 .redirect(reqwest::redirect::Policy::none())
481 .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
482 .build()
483 .unwrap_or_else(|_| Client::new());
484
485 if let Ok(resp) = no_redirect_client
486 .head(format!("http://{}", domain))
487 .send()
488 .await
489 {
490 info.http_reachable = true;
491 info.http_status = Some(resp.status().as_u16());
492
493 let status = resp.status().as_u16();
495 if [301, 302, 307, 308].contains(&status) {
496 if let Some(location) = resp.headers().get("location") {
497 if let Ok(loc) = location.to_str() {
498 if loc.starts_with("https://") {
499 info.redirects_to_https = true;
500 }
501 }
502 }
503 }
504 }
505
506 if info.response_time_ms == 0 {
507 info.response_time_ms = start.elapsed().as_millis();
508 }
509
510 Ok(info)
511}
512
513async fn validate_ssl(domain: &str) -> Result<SslValidation, String> {
516 let output = Command::new("openssl")
517 .args([
518 "s_client",
519 "-connect",
520 &format!("{}:443", domain),
521 "-servername",
522 domain,
523 "-brief",
524 ])
525 .stdin(std::process::Stdio::null())
526 .stdout(std::process::Stdio::piped())
527 .stderr(std::process::Stdio::piped())
528 .output()
529 .await
530 .map_err(|e| format!("openssl failed: {}", e))?;
531
532 let stderr = String::from_utf8_lossy(&output.stderr);
533 let stdout = String::from_utf8_lossy(&output.stdout);
534 let combined = format!("{}\n{}", stdout, stderr);
535
536 if combined.contains("CONNECTION ESTABLISHED") || combined.contains("Protocol") {
537 let protocol = combined
539 .lines()
540 .find(|l| l.contains("Protocol version:") || l.starts_with("Protocol"))
541 .and_then(|l| l.split(':').nth(1))
542 .map(|s| s.trim().to_string())
543 .unwrap_or_else(|| "Unknown".into());
544
545 let cipher = combined
547 .lines()
548 .find(|l| l.contains("Ciphersuite:") || l.contains("Cipher"))
549 .and_then(|l| l.split(':').nth(1))
550 .map(|s| s.trim().to_string())
551 .unwrap_or_else(|| "Unknown".into());
552
553 Ok(SslValidation {
554 ssl_available: true,
555 protocol_version: protocol,
556 cipher_suite: cipher,
557 })
558 } else if output.status.success() || combined.contains("Verify") {
559 Ok(SslValidation {
561 ssl_available: true,
562 protocol_version: "TLS".into(),
563 cipher_suite: "Unknown".into(),
564 })
565 } else {
566 Err("SSL connection failed".to_string())
567 }
568}