Решайте reCAPTCHA Enterprise с Rust
Используйте REST API solvethree из Rust: создайте задачу, опрашивайте её до готовности, затем отправьте solution.token на целевой сайт с возвращёнными идентификационными данными браузера и через тот же прокси. Пример ниже использует reqwest, Tokio и serde_json напрямую, а не официальный Rust SDK.
На этой странице
Настройте сайт и запрос
Сначала создайте активный профиль для целевого имени хоста в консоли рабочего пространства. Отправка имени хоста и действия не создаёт профиль. См. настройку профиля сайта, если целевой сайт ещё не настроен.
Задайте следующие переменные окружения на сервере, где будет работать Rust-клиент:
SOLVETHREE_API_KEY: API-ключ вашего рабочего пространства. Храните его на стороне сервера.SOLVETHREE_PROXY: URL прокси для решения и последующего запроса к целевому сайту.REQUEST_ID: необязательный постоянный идентификатор этой логической задачи. Сохраните его перед отправкой запроса, чтобы можно было восстановиться после разрыва соединения.
| Поле | Что отправлять |
|---|---|
site | Целевое имя хоста. Замените accounts.example.com в примере. |
action | Действие, ожидаемое целевым сайтом. При необходимости замените login. |
proxy | URL единственного прокси, считываемый из SOLVETHREE_PROXY. |
requestId | Ключ идемпотентности, содержащий 1–64 ASCII-буквы, цифры, символы подчёркивания или дефисы. |
recaptchaType | Здесь не указан, поскольку по умолчанию используется enterprise-v3. |
Не добавляйте поля type или url в этот запрос reCAPTCHA. Требуется ровно одно из полей: proxy или proxyCandidates; в примере используется proxy, чтобы при передаче результата целевому сайту можно было использовать то же подключение.
Создайте Rust-клиент
Поместите файл зависимостей в Cargo.toml, а клиент — в src/main.rs исполняемого проекта Cargo. После настройки переменных окружения и изменения целевого имени хоста и действия запустите проект с помощью Cargo. В зависимостях reqwest закреплён на версии 0.13.5.
[package]
name = "solvethree-example"
version = "0.1.0"
edition = "2021"
[dependencies]
reqwest = { version = "=0.13.5", features = ["json"] }
serde_json = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }use std::{env, error::Error, time::{Duration, SystemTime, UNIX_EPOCH}};
use reqwest::{Client, redirect::Policy};
use serde_json::{json, Value};
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let api = "https://api.solvethree.com/v1";
let key = env::var("SOLVETHREE_API_KEY")?;
let request_id = env::var("REQUEST_ID").unwrap_or_else(|_| format!("rust-{}", SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos()));
let input = json!({"site":"accounts.example.com", "action":"login", "proxy":env::var("SOLVETHREE_PROXY")?, "requestId":request_id});
let client = Client::builder().redirect(Policy::none()).timeout(Duration::from_secs(30)).build()?;
let mut task_id: Option<String> = None;
let outcome = tokio::time::timeout(Duration::from_secs(180), async {
let response = client.post(format!("{api}/tasks?waitMs=0")).bearer_auth(&key).json(&input).send().await?;
if !response.status().is_success() { return Err(format!("API HTTP {}", response.status()).into()); }
let mut result: Value = response.json().await?;
let id = result["id"].as_str().ok_or("Missing task ID")?.to_owned();
if !id.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-') { return Err("Invalid task ID".into()); }
task_id = Some(id.clone());
loop {
match result["status"].as_str() {
Some("ready") => return Ok::<Value, Box<dyn Error>>(result),
Some("failed") => return Err(format!("Task failed: {}", result["errorCode"]).into()),
Some("pending") | Some("solving") => {},
_ => return Err("Unexpected task status".into()),
}
tokio::time::sleep(Duration::from_secs(1)).await;
let response = client.get(format!("{api}/tasks/{id}?waitMs=15000")).bearer_auth(&key).send().await?;
if !response.status().is_success() { return Err(format!("API HTTP {}", response.status()).into()); }
result = response.json().await?;
}
}).await;
match outcome {
Ok(Ok(result)) => {
println!("Ready task: {}", result["id"].as_str().unwrap_or("unknown"));
// Submit result["solution"]["token"] with returned browser headers
// through the original proxy; do not log the solution.
Ok(())
},
Ok(Err(error)) => {
eprintln!("Recovery requestId={request_id} taskId={task_id:?}");
Err(error)
},
Err(_) => {
eprintln!("Recovery requestId={request_id} taskId={task_id:?}");
Err("Local deadline reached; recover the existing task".into())
}
}
}Клиент выполняет один запрос создания к POST /v1/tasks, аутентифицируясь с помощью Bearer-токена. Он сохраняет возвращённый идентификатор задачи и использует GET /v1/tasks/:id для последующих запросов статуса.
Два ограничения не дают клиенту ждать бесконечно: тайм-аут reqwest ограничивает каждый HTTP-запрос 30 секундами, а тайм-аут Tokio ограничивает создание и опрос вместе 180 секундами. Это настройки клиента, а не гарантии времени выполнения со стороны сервиса. Перенаправления отключены через ClientBuilder в reqwest.
Цикл опроса продолжается только при pending и solving. При ready возвращается результат, при failed выполнение прекращается с ошибкой; неожиданный статус также останавливает выполнение, а не приводит к бесконечному опросу. При успехе выводится идентификатор задачи, а не токен.
Восстановите прерванный запрос без нового решения
Пример хранит requestId и возвращённый идентификатор задачи в памяти и выводит их при HTTP-ошибке, транспортной ошибке, ошибке разбора или истечении срока ожидания. После завершения процесса они не сохраняются. Для восстановления после перезапуска сохраните идентификатор запроса и исходные параметры задачи до создания, а затем сохраните идентификатор задачи сразу после его получения.
- 1.
Если у вас есть ID задачи, запросите эту задачу
Возобновите работу через GET /v1/tasks/:id вместо создания новой задачи. Обычная точка входа примера всегда создаёт задачу; путь восстановления после перезапуска должен использовать сохранённый идентификатор.
- 2.
Если есть только идентификатор запроса, восстановите создание
Отправьте тот же запрос задачи с тем же requestId. Он вернёт существующую задачу. Изменение параметров при повторном использовании идентификатора вызывает конфликт.
- 3.
Обработайте статус восстановленной задачи
Продолжайте опрашивать задачи со статусом pending или solving. Используйте решение со статусом ready либо остановитесь при failed и изучите errorCode и error. Сам по себе локальный тайм-аут не доказывает, что удалённая задача завершилась ошибкой.
HTTP-ошибки и ошибки задач требуют разной обработки. Клиент проверяет наличие неуспешных HTTP-ответов до разбора данных успешного ответа. Конечная ошибка задачи, напротив, приходит в виде статуса задачи failed; средства за неудачные решения возвращаются. Сообщение об ошибке в примере содержит только errorCode, поэтому при диагностике также изучите error в ответе задачи. Прежде чем повторять запрос с неизменёнными параметрами, см. обработку ошибок API.
Отправляйте токен с его идентификационными данными браузера
Пример останавливается на готовой задаче; он не отправляет запрос приложения на целевой сайт. Для такого запроса нужен solution.token, а не идентификатор задачи. Используйте возвращённые идентификационные данные userAgent и secChUa через исходный прокси и отправляйте запрос на нужный целевой сайт, не смешивая значения из другого сеанса браузера.
Готовая задача означает, что решение сгенерировано, а не что целевой сайт принял запрос вашего приложения. Добавьте отправку с учётом требований целевого сайта в месте, отмеченном комментарием в ветке успешного выполнения; рекомендации по передаче токена см. в руководстве по интеграции reCAPTCHA Enterprise.
Источники и справочник API
Запустите первую задачу.
Начните с $5 на балансе рабочего пространства.