Skip to main content

web_analyzer/
domain_info_mobile.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct DomainInfoResult {
6    pub domain: String,
7    pub ipv4: Option<String>,
8    pub ipv6: Vec<String>,
9    pub all_ipv4: Vec<String>,
10    pub reverse_dns: Option<String>,
11    pub whois: WhoisInfo,
12    pub ssl: SslInfo,
13    pub dns: DnsInfo,
14    pub open_ports: Vec<String>,
15    pub http_status: Option<String>,
16    pub web_server: Option<String>,
17    pub response_time_ms: Option<f64>,
18    pub security: SecurityInfo,
19    pub security_score: u32,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct WhoisInfo {
24    pub registrar: String,
25    pub creation_date: String,
26    pub expiry_date: String,
27    pub last_updated: String,
28    pub domain_status: Vec<String>,
29    pub registrant: String,
30    pub privacy_protection: String,
31    #[serde(skip_serializing_if = "Vec::is_empty")]
32    pub name_servers: Vec<String>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct SslInfo {
37    pub status: String,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub issued_to: Option<String>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub issuer: Option<String>,
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub protocol_version: Option<String>,
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub expiry_date: Option<String>,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub days_until_expiry: Option<i64>,
48    #[serde(skip_serializing_if = "Vec::is_empty")]
49    pub alternative_names: Vec<String>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct DnsInfo {
54    pub nameservers: Vec<String>,
55    pub mx_records: Vec<String>,
56    pub txt_records: Vec<String>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub spf: Option<String>,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub dmarc: Option<String>,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct SecurityInfo {
65    pub https_available: bool,
66    pub https_redirect: bool,
67    pub security_headers: HashMap<String, String>,
68    pub headers_count: usize,
69}
70
71use hickory_resolver::config::*;
72use hickory_resolver::AsyncResolver;
73use reqwest::Client;
74use std::time::{Duration, Instant};
75use tokio::net::TcpStream;
76use tokio::time::timeout;
77
78pub async fn get_domain_info(
79    domain: &str,
80    progress_tx: Option<tokio::sync::mpsc::Sender<crate::ScanProgress>>,
81) -> Result<DomainInfoResult, Box<dyn std::error::Error + Send + Sync>> {
82    let start_time = Instant::now();
83
84    if let Some(t) = &progress_tx {
85        let _ = t.send(crate::ScanProgress {
86            module: "Domain Info".into(),
87            percentage: 10.0,
88            message: "Starting native mobile domain analysis".into(),
89            status: "Info".into(),
90        }).await;
91    }
92
93    let mut result = DomainInfoResult {
94        domain: domain.to_string(),
95        ipv4: None,
96        ipv6: vec![],
97        all_ipv4: vec![],
98        reverse_dns: None,
99        whois: WhoisInfo {
100            registrar: "Native Fallback".into(),
101            creation_date: "".into(),
102            expiry_date: "".into(),
103            last_updated: "".into(),
104            domain_status: vec![],
105            registrant: "".into(),
106            privacy_protection: "".into(),
107            name_servers: vec![],
108        },
109        ssl: SslInfo {
110            status: "Unknown".into(),
111            issued_to: None,
112            issuer: None,
113            protocol_version: None,
114            expiry_date: None,
115            days_until_expiry: None,
116            alternative_names: vec![],
117        },
118        dns: DnsInfo {
119            nameservers: vec![],
120            mx_records: vec![],
121            txt_records: vec![],
122            spf: None,
123            dmarc: None,
124        },
125        open_ports: vec![],
126        http_status: None,
127        web_server: None,
128        response_time_ms: None,
129        security: SecurityInfo {
130            https_available: false,
131            https_redirect: false,
132            security_headers: HashMap::new(),
133            headers_count: 0,
134        },
135        security_score: 50,
136    };
137
138    let resolver = AsyncResolver::tokio(ResolverConfig::cloudflare(), ResolverOpts::default());
139
140    // 1. DNS Records
141    if let Ok(response) = resolver.ipv4_lookup(domain).await {
142        for ip in response.iter() {
143            result.all_ipv4.push(ip.to_string());
144        }
145        result.ipv4 = result.all_ipv4.first().cloned();
146    }
147
148    if let Ok(response) = resolver.ipv6_lookup(domain).await {
149        for ip in response.iter() {
150            result.ipv6.push(ip.to_string());
151        }
152    }
153
154    if let Ok(response) = resolver.ns_lookup(domain).await {
155        for ns in response.iter() {
156            result.dns.nameservers.push(ns.to_string());
157        }
158    }
159
160    if let Ok(response) = resolver.mx_lookup(domain).await {
161        for mx in response.iter() {
162            result.dns.mx_records.push(format!("{} {}", mx.preference(), mx.exchange()));
163        }
164    }
165
166    if let Ok(response) = resolver.txt_lookup(domain).await {
167        for txt in response.iter() {
168            let record = txt.to_string();
169            if record.contains("v=spf1") {
170                result.dns.spf = Some(record.clone());
171            }
172            result.dns.txt_records.push(record);
173        }
174    }
175
176    // Attempt DMARC Lookup
177    let dmarc_domain = format!("_dmarc.{}", domain);
178    if let Ok(response) = resolver.txt_lookup(dmarc_domain.as_str()).await {
179        for txt in response.iter() {
180            let record = txt.to_string();
181            if record.contains("v=DMARC1") {
182                result.dns.dmarc = Some(record);
183                break;
184            }
185        }
186    }
187
188    if let Some(t) = &progress_tx {
189        let _ = t.send(crate::ScanProgress {
190            module: "Domain Info".into(),
191            percentage: 40.0,
192            message: "DNS Analysis Complete. Checking Ports.".into(),
193            status: "Info".into(),
194        }).await;
195    }
196
197    // 2. Open Ports
198    let target_ports = [80, 443, 21, 22, 25, 3306, 8080, 8443];
199    for port in target_ports {
200        let target = format!("{}:{}", domain, port);
201        if timeout(Duration::from_millis(500), TcpStream::connect(&target)).await.is_ok() {
202            result.open_ports.push(port.to_string());
203        }
204    }
205
206    if let Some(t) = &progress_tx {
207        let _ = t.send(crate::ScanProgress {
208            module: "Domain Info".into(),
209            percentage: 70.0,
210            message: "Checking HTTP/HTTPS footprint in native client".into(),
211            status: "Info".into(),
212        }).await;
213    }
214
215    // 3. HTTP / Security Check
216    let client = Client::builder()
217        .timeout(Duration::from_secs(5))
218        .danger_accept_invalid_certs(true)
219        .redirect(reqwest::redirect::Policy::limited(3))
220        .build()
221        .unwrap_or_else(|_| Client::new());
222
223    if let Ok(resp) = client.get(&format!("http://{}", domain)).send().await {
224        result.http_status = Some(resp.status().to_string());
225        if resp.url().scheme() == "https" {
226            result.security.https_redirect = true;
227        }
228        if let Some(srv) = resp.headers().get("server") {
229            result.web_server = Some(srv.to_str().unwrap_or("Unknown").to_string());
230        }
231    }
232
233    if let Ok(resp) = client.get(&format!("https://{}", domain)).send().await {
234        result.security.https_available = true;
235        result.ssl.status = "Valid (Native Check)".into();
236        result.security.headers_count = resp.headers().len();
237        
238        // Track typical security headers
239        for h in ["strict-transport-security", "content-security-policy", "x-frame-options"] {
240            if let Some(val) = resp.headers().get(h) {
241                result.security.security_headers.insert(h.into(), val.to_str().unwrap_or("").into());
242            }
243        }
244    } else {
245        result.ssl.status = "Invalid / Unreachable".into();
246    }
247
248    // Very basic scoring logic for fallback
249    let mut score = 50;
250    if result.security.https_available { score += 20; }
251    if result.security.https_redirect { score += 10; }
252    score += (result.security.security_headers.len() * 5) as u32;
253    result.security_score = std::cmp::min(100, score);
254    result.response_time_ms = Some(start_time.elapsed().as_millis() as f64);
255
256    if let Some(t) = &progress_tx {
257        let _ = t.send(crate::ScanProgress {
258            module: "Domain Info".into(),
259            percentage: 100.0,
260            message: "Analysis logic loop finished".into(),
261            status: "Info".into(),
262        }).await;
263    }
264
265    Ok(result)
266}