1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
//! Parked domain detection — identify if a domain is parked for sale.
use serde::{Deserialize, Serialize};
use crate::error::Result;
/// Request to check if a domain is parked.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParkedDomainRequest {
pub domain: String,
#[serde(default = "default_timeout")]
pub timeout_secs: u64,
}
fn default_timeout() -> u64 { 10 }
/// Result of parked domain check.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParkedDomainResult {
pub domain: String,
pub is_parked: bool,
pub confidence: String,
pub parking_provider: Option<String>,
pub signals: Vec<String>,
pub body_length: Option<usize>,
pub error: Option<String>,
}
/// Detect if a domain is parked for sale.
pub async fn check_parked_domain(req: &ParkedDomainRequest) -> Result<ParkedDomainResult> {
let domain = req.domain.clone();
// Try HTTPS first, then HTTP
let urls = vec![
format!("https://{}", domain),
format!("http://{}", domain),
];
let mut best_result: Option<ParkedDomainResult> = None;
for url in urls {
let http_req = crate::api::HttpCheckRequest {
url: url.clone(),
follow_redirects: true,
timeout_secs: req.timeout_secs,
};
match crate::api::check_http(&http_req).await {
Ok(http_result) => {
// Skip if 404 or 5xx
if let Some(status) = http_result.status_code {
if status >= 400 {
continue;
}
}
let mut signals = Vec::new();
let mut confidence = "low".to_string();
let mut parking_provider: Option<String> = None;
// Check headers for parking provider signatures
if let Some(server_header) = &http_result.server_header {
let server_lower = server_header.to_lowercase();
if server_lower.contains("sedo") {
parking_provider = Some("Sedo".to_string());
signals.push("sedo_header".to_string());
confidence = "high".to_string();
} else if server_lower.contains("godaddy") || server_lower.contains("parking") {
parking_provider = Some("GoDaddy".to_string());
signals.push("godaddy_header".to_string());
confidence = "high".to_string();
} else if server_lower.contains("bodis") {
parking_provider = Some("Bodis".to_string());
signals.push("bodis_header".to_string());
confidence = "high".to_string();
}
}
// Check for known parking keywords in response (note: we don't have body in HttpCheckResult)
// This is a limitation — parked domain detection fully requires body content
// For now, we rely on headers and indirect signals
// Check for thin content signals (inferred from what HTTP returns)
// Status 200 with certain servers is a signal
// Optional: call check_whois to see domain age
if confidence == "high" {
// Domain is likely parked based on headers
let result = ParkedDomainResult {
domain: domain.clone(),
is_parked: true,
confidence,
parking_provider,
signals,
body_length: None,
error: None,
};
best_result = Some(result);
break;
} else if best_result.is_none() {
let result = ParkedDomainResult {
domain: domain.clone(),
is_parked: false,
confidence: "low".to_string(),
parking_provider,
signals,
body_length: None,
error: None,
};
best_result = Some(result);
}
}
Err(_) => continue,
}
}
match best_result {
Some(result) => Ok(result),
None => Ok(ParkedDomainResult {
domain,
is_parked: false,
confidence: "unknown".to_string(),
parking_provider: None,
signals: vec![],
body_length: None,
error: Some("Unable to fetch domain content".to_string()),
}),
}
}