Skip to main content

web_analyzer/
domain_validator_mobile.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize)]
4pub struct ValidationResult {
5    pub domain: String,
6    pub valid: bool,
7    pub skip_reason: Option<String>,
8    pub dns_valid: bool,
9    pub http_valid: bool,
10    pub ssl_valid: bool,
11    pub dns_info: Option<DnsValidation>,
12    pub http_info: Option<HttpValidation>,
13    pub ssl_info: Option<SslValidation>,
14    pub errors: Vec<String>,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct DnsValidation {
19    pub ip_addresses: Vec<String>,
20    pub mx_exists: bool,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct HttpValidation {
25    pub http_reachable: bool,
26    pub https_reachable: bool,
27    pub http_status: Option<u16>,
28    pub https_status: Option<u16>,
29    pub redirects_to_https: bool,
30    pub response_time_ms: u128,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct SslValidation {
35    pub ssl_available: bool,
36    pub protocol_version: String,
37    pub cipher_suite: String,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct ValidationStats {
42    pub total: usize,
43    pub valid: usize,
44    pub invalid: usize,
45    pub skipped: usize,
46    pub dns_failed: usize,
47    pub http_failed: usize,
48    pub ssl_failed: usize,
49    pub success_rate: f64,
50    pub processing_time_secs: f64,
51    pub domains_per_sec: f64,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct BulkValidationResult {
56    pub stats: ValidationStats,
57    pub valid_domains: Vec<String>,
58    pub results: Vec<ValidationResult>,
59}
60
61use hickory_resolver::config::*;
62use hickory_resolver::AsyncResolver;
63use reqwest::Client;
64use std::time::{Duration, Instant};
65use tokio::task::JoinSet;
66
67pub async fn validate_domain(domain: &str) -> ValidationResult {
68    let mut result = ValidationResult {
69        domain: domain.to_string(),
70        valid: false,
71        skip_reason: None,
72        dns_valid: false,
73        http_valid: false,
74        ssl_valid: false,
75        dns_info: None,
76        http_info: None,
77        ssl_info: None,
78        errors: vec![],
79    };
80
81    // 1. DNS Check
82    let mut dns_info = DnsValidation {
83        ip_addresses: vec![],
84        mx_exists: false,
85    };
86    
87    let resolver = AsyncResolver::tokio(ResolverConfig::cloudflare(), ResolverOpts::default());
88    
89    if let Ok(response) = resolver.ipv4_lookup(domain).await {
90        for ip in response.iter() {
91            dns_info.ip_addresses.push(ip.to_string());
92        }
93    }
94    
95    if let Ok(response) = resolver.ipv6_lookup(domain).await {
96        for ip in response.iter() {
97            dns_info.ip_addresses.push(ip.to_string());
98        }
99    }
100    
101    if let Ok(response) = resolver.mx_lookup(domain).await {
102        dns_info.mx_exists = response.iter().next().is_some();
103    }
104    
105    result.dns_valid = !dns_info.ip_addresses.is_empty();
106    result.dns_info = Some(dns_info);
107
108    if !result.dns_valid {
109        result.errors.push("DNS Resolution failed. No A/AAAA records.".into());
110        return result; // Fast omit
111    }
112
113    // 2. HTTP/HTTPS Check
114    let start_http = Instant::now();
115    let client = Client::builder()
116        .timeout(Duration::from_secs(5))
117        .danger_accept_invalid_certs(true)
118        .redirect(reqwest::redirect::Policy::limited(3))
119        .build()
120        .unwrap_or_else(|_| Client::new());
121
122    let mut http_info = HttpValidation {
123        http_reachable: false,
124        https_reachable: false,
125        http_status: None,
126        https_status: None,
127        redirects_to_https: false,
128        response_time_ms: 0,
129    };
130
131    if let Ok(resp) = client.get(&format!("http://{}", domain)).send().await {
132        http_info.http_reachable = true;
133        http_info.http_status = Some(resp.status().as_u16());
134        if resp.url().scheme() == "https" {
135            http_info.redirects_to_https = true;
136        }
137    }
138
139    if let Ok(resp) = client.get(&format!("https://{}", domain)).send().await {
140        http_info.https_reachable = true;
141        http_info.https_status = Some(resp.status().as_u16());
142    }
143
144    http_info.response_time_ms = start_http.elapsed().as_millis();
145    result.http_valid = http_info.http_reachable || http_info.https_reachable;
146    
147    if !result.http_valid {
148        result.errors.push("Host is unreachable via HTTP/HTTPS.".into());
149    }
150    
151    result.http_info = Some(http_info.clone());
152
153    // 3. SSL Check Extrapolation (via Reqwest connection success assuming certs are valid)
154    // Detailed parsing requires x509-parser over pure TCP, but basic validation is supported natively here.
155    let ssl_client = Client::builder()
156        .timeout(Duration::from_secs(5))
157        .build()
158        .unwrap_or_else(|_| Client::new());
159
160    if let Ok(_) = ssl_client.get(&format!("https://{}", domain)).send().await {
161        result.ssl_valid = true;
162        result.ssl_info = Some(SslValidation {
163            ssl_available: true,
164            protocol_version: "TLS".into(), // Generalized for mobile stub
165            cipher_suite: "Standard".into(),
166        });
167    } else if http_info.https_reachable {
168        result.errors.push("SSL Certificate is invalid or untrusted.".into());
169    }
170
171    result.valid = result.dns_valid && result.http_valid;
172    result
173}
174
175pub async fn validate_domains_bulk(domains: &[String], max_concurrency: usize) -> BulkValidationResult {
176    let start_time = Instant::now();
177    let total = domains.len();
178    
179    let mut set = JoinSet::new();
180    let semaphore = std::sync::Arc::new(tokio::sync::Semaphore::new(max_concurrency));
181
182    for d in domains {
183        let domain = d.clone();
184        let permit = semaphore.clone().acquire_owned().await.unwrap();
185        set.spawn(async move {
186            let res = validate_domain(&domain).await;
187            drop(permit);
188            res
189        });
190    }
191
192    let mut results = Vec::new();
193    let mut valid_count = 0;
194    let mut invalid_count = 0;
195    let mut skipped_count = 0;
196    let mut dns_failed = 0;
197    let mut http_failed = 0;
198    let mut ssl_failed = 0;
199    let mut valid_domains = Vec::new();
200
201    while let Some(res_ok) = set.join_next().await {
202        if let Ok(res) = res_ok {
203            if res.valid {
204                valid_count += 1;
205                valid_domains.push(res.domain.clone());
206            } else if res.skip_reason.is_some() {
207                skipped_count += 1;
208            } else {
209                invalid_count += 1;
210                if !res.dns_valid { dns_failed += 1; }
211                if !res.http_valid { http_failed += 1; }
212                if !res.ssl_valid { ssl_failed += 1; }
213            }
214            results.push(res);
215        }
216    }
217
218    let processing_time_secs = start_time.elapsed().as_secs_f64();
219    let success_rate = if total > 0 { (valid_count as f64 / total as f64) * 100.0 } else { 0.0 };
220    let domains_per_sec = if processing_time_secs > 0.0 { total as f64 / processing_time_secs } else { 0.0 };
221
222    BulkValidationResult {
223        stats: ValidationStats {
224            total,
225            valid: valid_count,
226            invalid: invalid_count,
227            skipped: skipped_count,
228            dns_failed,
229            http_failed,
230            ssl_failed,
231            success_rate,
232            processing_time_secs,
233            domains_per_sec,
234        },
235        valid_domains,
236        results,
237    }
238}