use http::HeaderMap;
use crate::error::AntibotVendor;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ChallengeSignal {
pub vendor: AntibotVendor,
}
#[deprecated(
since = "1.0.5",
note = "use crate::fingerprint::target::sources::AntibotMarkerSource via Fingerprinter::analyze_hot; ChallengeDetector is removed in B15"
)]
#[derive(Debug, Default, Clone, Copy)]
pub struct ChallengeDetector;
const VENDOR_SIGNATURES: &[(&str, AntibotVendor)] = &[
("cf-chl-bypass", AntibotVendor::Cloudflare),
("Just a moment", AntibotVendor::Cloudflare),
("/cdn-cgi/challenge-platform/", AntibotVendor::Cloudflare),
("DataDome", AntibotVendor::DataDome),
("PerimeterX", AntibotVendor::PerimeterX),
("_Incapsula_", AntibotVendor::Imperva),
("Imperva", AntibotVendor::Imperva),
("distilnetworks", AntibotVendor::DistilNetworks),
];
impl ChallengeDetector {
pub fn new() -> Self {
Self
}
pub fn detect(&self, status: u16, headers: &HeaderMap, body: &[u8]) -> Option<ChallengeSignal> {
let is_html = headers
.get("content-type")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_ascii_lowercase().contains("text/html"))
.unwrap_or(false);
if matches!(status, 403 | 503) {
let body_lower = body_as_string(body);
for (sig, vendor) in VENDOR_SIGNATURES {
if body_lower.contains(sig) {
return Some(ChallengeSignal { vendor: *vendor });
}
}
}
if is_html && body.len() < 2048 {
let body_str = body_as_string(body);
if body_str.contains("<script") || body_str.contains("window.location") {
return Some(ChallengeSignal {
vendor: AntibotVendor::Other,
});
}
if body_str.contains("<noscript")
&& (body_str.contains("enable JavaScript")
|| body_str.contains("Please enable JavaScript"))
{
return Some(ChallengeSignal {
vendor: AntibotVendor::Other,
});
}
}
None
}
}
fn body_as_string(body: &[u8]) -> String {
let slice = &body[..body.len().min(16 * 1024)];
String::from_utf8_lossy(slice).into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
fn html_headers() -> HeaderMap {
let mut h = HeaderMap::new();
h.insert("content-type", "text/html; charset=utf-8".parse().unwrap());
h
}
fn empty_headers() -> HeaderMap {
HeaderMap::new()
}
#[test]
fn detects_cloudflare_via_chl_bypass() {
let sig = ChallengeDetector::new()
.detect(403, &empty_headers(), b"... cf-chl-bypass ...")
.unwrap();
assert_eq!(sig.vendor, AntibotVendor::Cloudflare);
}
#[test]
fn detects_cloudflare_via_just_a_moment() {
let sig = ChallengeDetector::new()
.detect(503, &empty_headers(), b"<html>Just a moment...</html>")
.unwrap();
assert_eq!(sig.vendor, AntibotVendor::Cloudflare);
}
#[test]
fn detects_datadome() {
let sig = ChallengeDetector::new()
.detect(403, &empty_headers(), b"DataDome block")
.unwrap();
assert_eq!(sig.vendor, AntibotVendor::DataDome);
}
#[test]
fn detects_perimeterx() {
let sig = ChallengeDetector::new()
.detect(403, &empty_headers(), b"PerimeterX challenge")
.unwrap();
assert_eq!(sig.vendor, AntibotVendor::PerimeterX);
}
#[test]
fn detects_imperva_incapsula() {
let sig = ChallengeDetector::new()
.detect(403, &empty_headers(), b"_Incapsula_ ...")
.unwrap();
assert_eq!(sig.vendor, AntibotVendor::Imperva);
}
#[test]
fn detects_distil() {
let sig = ChallengeDetector::new()
.detect(403, &empty_headers(), b"distilnetworks tag")
.unwrap();
assert_eq!(sig.vendor, AntibotVendor::DistilNetworks);
}
#[test]
fn detects_generic_js_stub() {
let body = b"<html><script>window.location='x'</script></html>";
let sig = ChallengeDetector::new()
.detect(200, &html_headers(), body)
.unwrap();
assert_eq!(sig.vendor, AntibotVendor::Other);
}
#[test]
fn detects_generic_noscript_challenge() {
let body = b"<html><noscript>Please enable JavaScript</noscript></html>";
let sig = ChallengeDetector::new()
.detect(200, &html_headers(), body)
.unwrap();
assert_eq!(sig.vendor, AntibotVendor::Other);
}
#[test]
fn healthy_200_returns_none() {
let body = b"<html><body><h1>real content here</h1><p>lots of text</p></body></html>";
assert!(ChallengeDetector::new()
.detect(200, &html_headers(), body)
.is_none());
}
#[test]
fn vendor_signatures_only_trigger_on_403_or_503() {
let body = b"cf-chl-bypass";
assert!(ChallengeDetector::new()
.detect(200, &empty_headers(), body)
.is_none());
}
#[test]
fn detector_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<ChallengeDetector>();
}
}