use crate::captcha_detect::{
AudioCaptchaDetector, CanvasCaptchaDetector, ChallengePageDetector, DetectedCaptcha, Detector,
HCaptchaDetector, ImageCaptchaDetector, MultiStepCaptchaDetector, PowCaptchaDetector,
RecaptchaDetector, ShadowDomDetector, SliderCaptchaDetector, TurnstileDetector,
};
use crate::solver::{CaptchaType, SolveMethod};
pub trait CaptchaProvider: Send + Sync {
fn name(&self) -> &'static str;
fn detected_kind(&self) -> DetectedCaptcha;
fn captcha_type(&self) -> CaptchaType;
fn recommended_solver_methods(&self) -> &'static [SolveMethod];
fn recommended_solver_names(&self) -> &'static [&'static str] {
&[]
}
fn detector(&self) -> &dyn Detector;
}
pub struct ProviderRegistry {
providers: Vec<Box<dyn CaptchaProvider>>,
}
impl ProviderRegistry {
pub fn new() -> Self {
let mut providers: Vec<Box<dyn CaptchaProvider>> = vec![
Box::new(ChallengePageDetector),
Box::new(TurnstileDetector),
Box::new(RecaptchaDetector),
Box::new(HCaptchaDetector),
Box::new(ImageCaptchaDetector),
Box::new(AudioCaptchaDetector),
Box::new(PowCaptchaDetector),
Box::new(CanvasCaptchaDetector),
Box::new(SliderCaptchaDetector),
Box::new(MultiStepCaptchaDetector),
Box::new(ShadowDomDetector),
];
providers.sort_by_key(|p| p.detector().priority());
Self { providers }
}
pub fn with_built_in_rules() -> anyhow::Result<Self> {
use crate::detect::rules::{built_in_rules, RuleDetector};
let mut reg = Self::new();
let rules = built_in_rules()?;
for rule in rules.providers {
reg.providers.push(Box::new(RuleDetector::from(rule)));
}
reg.providers.sort_by_key(|p| p.detector().priority());
Ok(reg)
}
pub fn push<P: CaptchaProvider + 'static>(&mut self, provider: P) {
self.providers.push(Box::new(provider));
self.providers.sort_by_key(|p| p.detector().priority());
}
pub fn providers(&self) -> &[Box<dyn CaptchaProvider>] {
&self.providers
}
pub fn find_by_kind(&self, kind: &DetectedCaptcha) -> Option<&dyn CaptchaProvider> {
self.providers
.iter()
.find(|p| p.detected_kind() == *kind)
.map(|b| b.as_ref())
}
}
impl Default for ProviderRegistry {
fn default() -> Self {
Self::new()
}
}
macro_rules! provider_impl {
(
$detector:ident,
kind = $kind:ident,
type = $captcha_type:expr,
methods = [$($method:ident),+ $(,)?],
) => {
impl CaptchaProvider for $detector {
fn name(&self) -> &'static str {
<Self as Detector>::name(self)
}
fn detected_kind(&self) -> DetectedCaptcha {
DetectedCaptcha::$kind
}
fn captcha_type(&self) -> CaptchaType {
$captcha_type
}
fn recommended_solver_methods(&self) -> &'static [SolveMethod] {
&[$(SolveMethod::$method),+]
}
fn detector(&self) -> &dyn Detector {
self
}
}
};
}
provider_impl!(
ChallengePageDetector,
kind = Turnstile,
type = CaptchaType::CloudflareTurnstile,
methods = [BehavioralBypass, ThirdPartyService],
);
provider_impl!(
TurnstileDetector,
kind = Turnstile,
type = CaptchaType::CloudflareTurnstile,
methods = [BehavioralBypass, ThirdPartyService],
);
provider_impl!(
RecaptchaDetector,
kind = RecaptchaV2,
type = CaptchaType::RecaptchaV2,
methods = [BehavioralBypass, AudioBypass, VisionLLM, ThirdPartyService],
);
provider_impl!(
HCaptchaDetector,
kind = HCaptcha,
type = CaptchaType::HCaptcha,
methods = [VisionLLM, AudioBypass, ThirdPartyService],
);
provider_impl!(
ImageCaptchaDetector,
kind = ImageCaptcha,
type = CaptchaType::ImageGrid,
methods = [VisionLLM, ThirdPartyService],
);
provider_impl!(
AudioCaptchaDetector,
kind = AudioCaptcha,
type = CaptchaType::AudioCaptcha,
methods = [AudioBypass, ThirdPartyService],
);
provider_impl!(
PowCaptchaDetector,
kind = PowCaptcha,
type = CaptchaType::PowCaptcha,
methods = [BehavioralBypass],
);
provider_impl!(
SliderCaptchaDetector,
kind = SliderCaptcha,
type = CaptchaType::Slider,
methods = [BehavioralBypass, VisionLLM, ThirdPartyService],
);
provider_impl!(
CanvasCaptchaDetector,
kind = CanvasCaptcha,
type = CaptchaType::CanvasCaptcha,
methods = [VisionLLM, ThirdPartyService],
);
provider_impl!(
ShadowDomDetector,
kind = ShadowDomCaptcha,
type = CaptchaType::ShadowDomCaptcha,
methods = [VisionLLM, BehavioralBypass],
);
provider_impl!(
MultiStepCaptchaDetector,
kind = MultiStepCaptcha,
type = CaptchaType::MultiStepCaptcha,
methods = [BehavioralBypass, VisionLLM],
);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn registry_contains_every_builtin_provider() {
let reg = ProviderRegistry::new();
let names: Vec<_> = reg.providers().iter().map(|p| p.name()).collect();
for expected in [
"challenge_page",
"turnstile",
"recaptcha",
"hcaptcha",
"image_captcha",
"audio_captcha",
"pow_captcha",
"canvas_captcha",
"slider_captcha",
"multi_step_captcha",
"shadow_dom",
] {
assert!(names.contains(&expected), "missing provider: {expected}");
}
}
#[test]
fn registry_is_priority_ordered() {
let reg = ProviderRegistry::new();
let prios: Vec<i32> = reg
.providers()
.iter()
.map(|p| p.detector().priority())
.collect();
let mut sorted = prios.clone();
sorted.sort_unstable();
assert_eq!(prios, sorted, "registry must be priority-ordered");
}
#[test]
fn find_by_kind_locates_known_kinds() {
let reg = ProviderRegistry::new();
let p = reg
.find_by_kind(&DetectedCaptcha::HCaptcha)
.expect("hcaptcha provider");
assert_eq!(p.name(), "hcaptcha");
assert_eq!(p.captcha_type(), CaptchaType::HCaptcha);
}
#[test]
fn find_by_kind_returns_none_for_unhandled_kind() {
let reg = ProviderRegistry::new();
assert!(reg.find_by_kind(&DetectedCaptcha::None).is_none());
}
#[test]
fn turnstile_recommends_behavioral_first() {
let reg = ProviderRegistry::new();
let p = reg.find_by_kind(&DetectedCaptcha::Turnstile).unwrap();
assert_eq!(
p.recommended_solver_methods().first(),
Some(&SolveMethod::BehavioralBypass),
"Turnstile should try BehavioralBypass first; was: {:?}",
p.recommended_solver_methods(),
);
}
#[test]
fn image_captcha_recommends_vision_llm_first() {
let reg = ProviderRegistry::new();
let p = reg.find_by_kind(&DetectedCaptcha::ImageCaptcha).unwrap();
assert_eq!(
p.recommended_solver_methods().first(),
Some(&SolveMethod::VisionLLM),
"ImageCaptcha should try VisionLLM first",
);
}
#[test]
fn with_built_in_rules_includes_built_in_providers() {
let reg = ProviderRegistry::with_built_in_rules().expect("bundled rules parse");
let names: Vec<_> = reg.providers().iter().map(|p| p.name()).collect();
for expected in ["turnstile", "hcaptcha", "recaptcha"] {
assert!(names.contains(&expected), "lost built-in: {expected}");
}
}
#[test]
fn with_built_in_rules_includes_community_rules() {
let reg = ProviderRegistry::with_built_in_rules().unwrap();
let names: Vec<_> = reg.providers().iter().map(|p| p.name()).collect();
for expected in [
"datadome",
"akamai_bot_manager",
"perimeterx_human",
"arkose_funcaptcha",
"geetest_v3",
"geetest_v4",
"aws_waf_captcha",
"friendly_captcha",
"mtcaptcha",
"wp_math_captcha",
"imperva_incapsula",
"kasada",
"f5_distributed_cloud",
"yandex_smartcaptcha",
"tencent_captcha",
"keycaptcha",
] {
assert!(
names.contains(&expected),
"missing community rule: {expected}"
);
}
}
#[test]
fn with_built_in_rules_keeps_priority_order_across_built_in_and_rules() {
let reg = ProviderRegistry::with_built_in_rules().unwrap();
let prios: Vec<i32> = reg
.providers()
.iter()
.map(|p| p.detector().priority())
.collect();
let mut sorted = prios.clone();
sorted.sort_unstable();
assert_eq!(prios, sorted, "merged registry must be priority-sorted");
}
#[test]
fn community_rules_route_to_custom_captcha_type() {
let reg = ProviderRegistry::with_built_in_rules().unwrap();
let datadome = reg
.providers()
.iter()
.find(|p| p.name() == "datadome")
.expect("datadome registered");
assert!(
matches!(datadome.detected_kind(), DetectedCaptcha::Custom(ref s) if s == "datadome"),
"datadome.detected_kind should be Custom(datadome)",
);
assert!(
matches!(datadome.captcha_type(), CaptchaType::Custom(ref s) if s == "datadome"),
"datadome.captcha_type should be Custom(datadome)",
);
}
#[test]
fn custom_provider_can_be_pushed_and_sorted() {
struct DummyDetector;
#[async_trait::async_trait]
impl Detector for DummyDetector {
fn name(&self) -> &'static str {
"dummy"
}
fn priority(&self) -> i32 {
1
}
async fn detect(
&self,
_page: &chromiumoxide::Page,
) -> anyhow::Result<Option<crate::captcha_detect::CaptchaInfo>> {
Ok(None)
}
}
impl CaptchaProvider for DummyDetector {
fn name(&self) -> &'static str {
"dummy"
}
fn detected_kind(&self) -> DetectedCaptcha {
DetectedCaptcha::None
}
fn captcha_type(&self) -> CaptchaType {
CaptchaType::Custom("dummy".into())
}
fn recommended_solver_methods(&self) -> &'static [SolveMethod] {
&[SolveMethod::BehavioralBypass]
}
fn detector(&self) -> &dyn Detector {
self
}
}
let mut reg = ProviderRegistry::new();
reg.push(DummyDetector);
assert_eq!(reg.providers().first().unwrap().name(), "dummy");
}
}