use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::fmt;
use crate::antibot::ChallengeVendor;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum SolverKind {
None,
TwoCaptcha,
AntiCaptcha,
Vlm,
RecaptchaInvisible,
}
impl SolverKind {
pub fn as_str(self) -> &'static str {
match self {
Self::None => "none",
Self::TwoCaptcha => "2captcha",
Self::AntiCaptcha => "anticaptcha",
Self::Vlm => "vlm",
Self::RecaptchaInvisible => "recaptcha-invisible",
}
}
}
impl std::str::FromStr for SolverKind {
type Err = SolverError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"none" | "" => Ok(Self::None),
"2captcha" | "twocaptcha" => Ok(Self::TwoCaptcha),
"anticaptcha" | "anti-captcha" => Ok(Self::AntiCaptcha),
"vlm" | "openai" | "anthropic" => Ok(Self::Vlm),
"recaptcha-invisible" | "recaptcha_invisible" | "recaptcha" => {
Ok(Self::RecaptchaInvisible)
}
other => Err(SolverError::UnknownAdapter(other.to_string())),
}
}
}
#[derive(Debug, Clone)]
pub struct ChallengePayload {
pub vendor: ChallengeVendor,
pub url: url::Url,
pub sitekey: Option<String>,
pub action: Option<String>,
pub iframe_srcs: Vec<String>,
pub screenshot_png: Option<Vec<u8>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SolveResult {
pub token: String,
pub elapsed_ms: u64,
pub adapter: &'static str,
}
#[derive(Debug, Clone)]
pub enum SolverError {
AdapterNotConfigured(&'static str),
UnknownAdapter(String),
UnsupportedVendor {
adapter: &'static str,
vendor: ChallengeVendor,
},
Upstream(String),
}
impl fmt::Display for SolverError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::AdapterNotConfigured(a) => {
write!(f, "captcha solver `{a}` selected but not configured")
}
Self::UnknownAdapter(a) => write!(f, "unknown captcha solver adapter `{a}`"),
Self::UnsupportedVendor { adapter, vendor } => {
write!(
f,
"solver `{adapter}` does not handle vendor `{}`",
vendor.as_str()
)
}
Self::Upstream(m) => write!(f, "captcha solver upstream error: {m}"),
}
}
}
impl std::error::Error for SolverError {}
#[async_trait]
pub trait CaptchaSolver: Send + Sync {
fn name(&self) -> &'static str;
fn supported_vendors(&self) -> &'static [ChallengeVendor];
async fn solve(&self, challenge: ChallengePayload) -> Result<SolveResult, SolverError>;
}
#[derive(Debug, Default)]
pub struct TwoCaptchaAdapter;
#[async_trait]
impl CaptchaSolver for TwoCaptchaAdapter {
fn name(&self) -> &'static str {
"2captcha-stub"
}
fn supported_vendors(&self) -> &'static [ChallengeVendor] {
&[
ChallengeVendor::Recaptcha,
ChallengeVendor::RecaptchaEnterprise,
ChallengeVendor::HCaptcha,
ChallengeVendor::CloudflareTurnstile,
]
}
async fn solve(&self, _c: ChallengePayload) -> Result<SolveResult, SolverError> {
Err(SolverError::AdapterNotConfigured("2captcha"))
}
}
#[derive(Debug, Default)]
pub struct AntiCaptchaAdapter;
#[async_trait]
impl CaptchaSolver for AntiCaptchaAdapter {
fn name(&self) -> &'static str {
"anticaptcha-stub"
}
fn supported_vendors(&self) -> &'static [ChallengeVendor] {
&[
ChallengeVendor::Recaptcha,
ChallengeVendor::RecaptchaEnterprise,
ChallengeVendor::HCaptcha,
]
}
async fn solve(&self, _c: ChallengePayload) -> Result<SolveResult, SolverError> {
Err(SolverError::AdapterNotConfigured("anticaptcha"))
}
}
#[derive(Debug, Default)]
pub struct VlmAdapter;
#[async_trait]
impl CaptchaSolver for VlmAdapter {
fn name(&self) -> &'static str {
"vlm-stub"
}
fn supported_vendors(&self) -> &'static [ChallengeVendor] {
&[
ChallengeVendor::Recaptcha,
ChallengeVendor::HCaptcha,
ChallengeVendor::GenericCaptcha,
]
}
async fn solve(&self, _c: ChallengePayload) -> Result<SolveResult, SolverError> {
Err(SolverError::AdapterNotConfigured("vlm"))
}
}
pub mod env {
pub const CRAWLEX_SOLVER: &str = "CRAWLEX_SOLVER";
pub const CRAWLEX_SOLVER_2CAPTCHA_KEY: &str = "CRAWLEX_SOLVER_2CAPTCHA_KEY";
pub const CRAWLEX_SOLVER_ANTICAPTCHA_KEY: &str = "CRAWLEX_SOLVER_ANTICAPTCHA_KEY";
pub const CRAWLEX_SOLVER_VLM_PROVIDER: &str = "CRAWLEX_SOLVER_VLM_PROVIDER";
pub const CRAWLEX_SOLVER_VLM_API_KEY: &str = "CRAWLEX_SOLVER_VLM_API_KEY";
pub const CRAWLEX_SOLVER_VLM_MODEL: &str = "CRAWLEX_SOLVER_VLM_MODEL";
}
pub fn build_solver(kind: SolverKind) -> Option<Box<dyn CaptchaSolver>> {
match kind {
SolverKind::None => None,
SolverKind::TwoCaptcha => Some(Box::new(TwoCaptchaAdapter)),
SolverKind::AntiCaptcha => Some(Box::new(AntiCaptchaAdapter)),
SolverKind::Vlm => Some(Box::new(VlmAdapter)),
#[cfg(feature = "cdp-backend")]
SolverKind::RecaptchaInvisible => Some(Box::new(
crate::antibot::recaptcha::RecaptchaInvisibleAdapter::new(),
)),
#[cfg(not(feature = "cdp-backend"))]
SolverKind::RecaptchaInvisible => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
fn payload(vendor: ChallengeVendor) -> ChallengePayload {
ChallengePayload {
vendor,
url: url::Url::parse("https://example.com/").unwrap(),
sitekey: Some("fake-sitekey".into()),
action: None,
iframe_srcs: vec![],
screenshot_png: None,
}
}
#[test]
fn parses_known_adapters() {
assert_eq!(
SolverKind::from_str("2captcha").unwrap(),
SolverKind::TwoCaptcha
);
assert_eq!(
SolverKind::from_str("anticaptcha").unwrap(),
SolverKind::AntiCaptcha
);
assert_eq!(SolverKind::from_str("vlm").unwrap(), SolverKind::Vlm);
assert_eq!(SolverKind::from_str("none").unwrap(), SolverKind::None);
assert!(SolverKind::from_str("bogus").is_err());
}
#[tokio::test]
async fn twocaptcha_stub_refuses() {
let a = TwoCaptchaAdapter;
let err = a
.solve(payload(ChallengeVendor::Recaptcha))
.await
.unwrap_err();
assert!(matches!(err, SolverError::AdapterNotConfigured(_)));
}
#[tokio::test]
async fn vlm_stub_refuses() {
let a = VlmAdapter;
let err = a
.solve(payload(ChallengeVendor::HCaptcha))
.await
.unwrap_err();
assert!(matches!(err, SolverError::AdapterNotConfigured(_)));
}
#[test]
fn build_solver_honours_none() {
assert!(build_solver(SolverKind::None).is_none());
assert!(build_solver(SolverKind::TwoCaptcha).is_some());
}
#[test]
fn supported_vendors_advertised() {
let a = TwoCaptchaAdapter;
assert!(a.supported_vendors().contains(&ChallengeVendor::Recaptcha));
}
}