use super::*;
impl ThirdPartyCaptchaSolver {
pub(crate) fn build_task_params(info: &CaptchaInfo) -> Option<Vec<(&'static str, String)>> {
let pageurl = info.page_url.clone();
let sitekey = info.site_key.clone();
match &info.kind {
DetectedCaptcha::Turnstile => {
let key = sitekey?;
Some(vec![
("method", "turnstile".into()),
("sitekey", key),
("pageurl", pageurl),
("json", "1".into()),
])
}
DetectedCaptcha::RecaptchaV2 => {
let key = sitekey?;
Some(vec![
("method", "userrecaptcha".into()),
("googlekey", key),
("pageurl", pageurl),
("json", "1".into()),
])
}
DetectedCaptcha::RecaptchaV3 => {
let key = sitekey?;
Some(vec![
("method", "userrecaptcha".into()),
("googlekey", key),
("pageurl", pageurl),
("version", "v3".into()),
("min_score", "0.3".into()),
("json", "1".into()),
])
}
DetectedCaptcha::HCaptcha => {
let key = sitekey?;
Some(vec![
("method", "hcaptcha".into()),
("sitekey", key),
("pageurl", pageurl),
("json", "1".into()),
])
}
DetectedCaptcha::Custom(name) => match name.as_str() {
"datadome" => {
let captcha_url = info
.container_selector
.clone()
.unwrap_or_else(|| pageurl.clone());
Some(vec![
("method", "datadome".into()),
("captcha_url", captcha_url),
("pageurl", pageurl),
("json", "1".into()),
])
}
"arkose_funcaptcha" => {
let key = sitekey?;
Some(vec![
("method", "funcaptcha".into()),
("publickey", key),
("pageurl", pageurl),
("surl", "https://client-api.arkoselabs.com".into()),
("json", "1".into()),
])
}
"geetest_v3" => {
let gt = sitekey?;
Some(vec![
("method", "geetest".into()),
("gt", gt),
(
"challenge",
info.container_selector.clone().unwrap_or_default(),
),
("pageurl", pageurl),
("json", "1".into()),
])
}
"geetest_v4" => {
let captcha_id = sitekey?;
Some(vec![
("method", "geetest_v4".into()),
("captcha_id", captcha_id),
("pageurl", pageurl),
("json", "1".into()),
])
}
"aws_waf_captcha" => {
let _key = sitekey?;
None
}
_ => None,
},
_ => None,
}
}
pub(crate) async fn submit_task(&self, params: &[(&'static str, String)]) -> Result<String> {
let key = self
.api_key
.as_deref()
.ok_or_else(|| anyhow!("no API key configured for ThirdPartyCaptchaSolver"))?;
let mut form: Vec<(&str, String)> = Vec::with_capacity(params.len() + 1);
form.push(("key", key.to_string()));
for (k, v) in params {
form.push((*k, v.clone()));
}
let url = format!("{}/in.php", self.service.base_url().trim_end_matches('/'));
retry_transient("third-party submit", || async {
let resp = self
.client
.post(&url)
.form(&form)
.send()
.await
.map_err(transient)?;
let status = resp.status();
let body = resp.text().await.map_err(transient)?;
if status.is_server_error() {
return Err(RetryError::Transient(anyhow!(
"third-party submit {status}: {body}"
)));
}
if !status.is_success() {
return Err(RetryError::Terminal(anyhow!(
"third-party submit returned {status}: {body}"
)));
}
let v: serde_json::Value = serde_json::from_str(&body).map_err(|e| {
RetryError::Terminal(anyhow!("third-party submit json parse: {e}; body={body}"))
})?;
if v["status"].as_i64().unwrap_or(0) != 1 {
let err_str = v["request"].as_str().unwrap_or("<no error>").to_string();
if is_terminal_error(&err_str) {
return Err(RetryError::Terminal(anyhow!(
"third-party submit terminal error: {err_str}"
)));
}
return Err(RetryError::Transient(anyhow!(
"third-party submit error: {err_str}"
)));
}
v["request"].as_str().map(|s| s.to_string()).ok_or_else(|| {
RetryError::Terminal(anyhow!(
"third-party submit response missing 'request' field"
))
})
})
.await
}
pub(crate) async fn poll_result(&self, task_id: &str) -> Result<String> {
let key = self
.api_key
.as_deref()
.ok_or_else(|| anyhow!("no API key configured for ThirdPartyCaptchaSolver"))?;
let url = format!("{}/res.php", self.service.base_url().trim_end_matches('/'));
for _ in 0..self.max_polls {
tokio::time::sleep(Duration::from_millis(self.poll_interval_ms)).await;
let body = retry_transient("third-party poll", || async {
let resp = self
.client
.get(&url)
.query(&[
("key", key),
("action", "get"),
("id", task_id),
("json", "1"),
])
.send()
.await
.map_err(transient)?;
let status = resp.status();
if status.is_server_error() {
return Err(RetryError::Transient(anyhow!("third-party poll {status}")));
}
if !status.is_success() {
return Err(RetryError::Terminal(anyhow!(
"third-party poll returned {status}"
)));
}
resp.text().await.map_err(transient)
})
.await?;
let v: serde_json::Value = serde_json::from_str(&body)
.map_err(|e| anyhow!("third-party poll json parse: {e}; body={body}"))?;
let status = v["status"].as_i64().unwrap_or(0);
let request = v["request"].as_str().unwrap_or("");
if status == 1 {
return Ok(request.to_string());
}
if request == "CAPCHA_NOT_READY" {
continue;
}
if is_terminal_error(request) {
return Err(anyhow!(
"third-party terminal error (won't retry): {request}"
));
}
}
Err(anyhow!(
"third-party solver timed out after {} polls",
self.max_polls
))
}
}