use serde::{Deserialize, Serialize};
use crate::error::Result;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TyposquatRequest {
pub domain: String,
#[serde(default = "default_timeout")]
pub timeout_secs: u64,
#[serde(default = "default_max_mutations")]
pub max_mutations: u32,
}
fn default_timeout() -> u64 { 10 }
fn default_max_mutations() -> u32 { 200 }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TyposquatHit {
pub mutation: String,
pub strategy: String,
pub ips: Vec<String>,
pub live: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TyposquatResult {
pub domain: String,
pub mutations_checked: u32,
pub live_count: u32,
pub hits: Vec<TyposquatHit>,
pub error: Option<String>,
}
pub async fn check_typosquatting(req: &TyposquatRequest) -> Result<TyposquatResult> {
let domain = req.domain.clone();
let timeout_secs = req.timeout_secs;
let parts: Vec<&str> = domain.split('.').collect();
if parts.is_empty() {
return Ok(TyposquatResult {
domain,
mutations_checked: 0,
live_count: 0,
hits: vec![],
error: Some("Invalid domain format".to_string()),
});
}
let label = if parts.len() >= 2 {
parts[..parts.len() - 1].join(".")
} else {
parts[0].to_string()
};
let tld = parts[parts.len() - 1];
let mutations = generate_mutations(&label, tld, req.max_mutations);
let mutations_checked = mutations.len() as u32;
let handles: Vec<_> = mutations
.iter()
.map(|(mutation, strategy)| {
let mutation_clone = mutation.clone();
let strategy_clone = strategy.clone();
let timeout = timeout_secs;
async move {
let dns_req = crate::api::helpers::dns_request_for_record_type(
mutation_clone.clone(),
"A",
timeout,
);
match crate::api::check_dns(&dns_req).await {
Ok(results) => {
let mut ips = Vec::new();
for result in results {
for record in &result.answers {
if let crate::api::RecordData::A(ip_str) = &record.data {
ips.push(ip_str.clone());
}
}
}
let live = !ips.is_empty();
Some(TyposquatHit {
mutation: mutation_clone,
strategy: strategy_clone,
ips,
live,
})
}
Err(_) => None,
}
}
})
.collect();
let handles_spawned: Vec<_> = handles.into_iter().map(tokio::spawn).collect();
let mut results = Vec::with_capacity(handles_spawned.len());
for h in handles_spawned { results.push(h.await.unwrap_or(None)); }
let hits: Vec<TyposquatHit> = results.into_iter().filter_map(|r| r).collect();
let live_count = hits.iter().filter(|h| h.live).count() as u32;
Ok(TyposquatResult {
domain,
mutations_checked,
live_count,
hits,
error: None,
})
}
fn generate_mutations(label: &str, tld: &str, max_count: u32) -> Vec<(String, String)> {
let mut mutations = Vec::new();
let tld_swaps = [
"com", "net", "org", "io", "co", "info", "biz", "de", "uk", "ru", "cn", "jp",
];
for alt_tld in &tld_swaps {
if alt_tld != &tld && mutations.len() < max_count as usize {
mutations.push((
format!("{}.{}", label, alt_tld),
"tld_swap".to_string(),
));
}
}
for (byte_i, _) in label.char_indices() {
if mutations.len() >= max_count as usize {
break;
}
let mut mutated = label.to_string();
mutated.remove(byte_i);
if !mutated.is_empty() {
mutations.push((
format!("{}.{}", mutated, tld),
"missing_char".to_string(),
));
}
}
for i in 0..label.len().saturating_sub(1) {
if mutations.len() >= max_count as usize {
break;
}
let mut chars: Vec<char> = label.chars().collect();
chars.swap(i, i + 1);
let mutated: String = chars.iter().collect();
mutations.push((
format!("{}.{}", mutated, tld),
"transposition".to_string(),
));
}
for i in 0..label.len() {
if mutations.len() >= max_count as usize {
break;
}
let mut mutated = String::new();
for (idx, ch) in label.chars().enumerate() {
mutated.push(ch);
if idx == i {
mutated.push(ch);
}
}
mutations.push((
format!("{}.{}", mutated, tld),
"double_char".to_string(),
));
}
for (byte_i, _) in label.char_indices().skip(1) {
if mutations.len() >= max_count as usize {
break;
}
let mut mutated = label.to_string();
mutated.insert(byte_i, '-');
mutations.push((
format!("{}.{}", mutated, tld),
"hyphen".to_string(),
));
}
let homoglyphs = [('o', '0'), ('l', '1'), ('i', '1'), ('e', '3'), ('a', '4'), ('s', '5'), ('g', '9')];
for (byte_i, ch) in label.char_indices() {
if mutations.len() >= max_count as usize {
break;
}
for (from, to) in &homoglyphs {
if ch == *from {
let mut mutated = label.to_string();
let char_len = ch.len_utf8();
mutated.replace_range(byte_i..byte_i + char_len, &to.to_string());
mutations.push((
format!("{}.{}", mutated, tld),
"homoglyph".to_string(),
));
break;
}
}
}
mutations.sort();
mutations.dedup();
mutations.truncate(max_count as usize);
mutations
}