Решайте reCAPTCHA Enterprise с Rust

Автор: solvethree4 мин на чтение

Используйте REST API solvethree из Rust: создайте задачу, опрашивайте её до готовности, затем отправьте solution.token на целевой сайт с возвращёнными идентификационными данными браузера и через тот же прокси. Пример ниже использует reqwest, Tokio и serde_json напрямую, а не официальный Rust SDK.

На этой странице

Настройте сайт и запрос

Сначала создайте активный профиль для целевого имени хоста в консоли рабочего пространства. Отправка имени хоста и действия не создаёт профиль. См. настройку профиля сайта, если целевой сайт ещё не настроен.

Задайте следующие переменные окружения на сервере, где будет работать Rust-клиент:

  • SOLVETHREE_API_KEY: API-ключ вашего рабочего пространства. Храните его на стороне сервера.
  • SOLVETHREE_PROXY: URL прокси для решения и последующего запроса к целевому сайту.
  • REQUEST_ID: необязательный постоянный идентификатор этой логической задачи. Сохраните его перед отправкой запроса, чтобы можно было восстановиться после разрыва соединения.
Поля задачи reCAPTCHA
Поля задачи reCAPTCHA
ПолеЧто отправлять
siteЦелевое имя хоста. Замените accounts.example.com в примере.
actionДействие, ожидаемое целевым сайтом. При необходимости замените login.
proxyURL единственного прокси, считываемый из 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.

Cargo.toml · проверенные зависимости
[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"] }
Rust · асинхронные REST-вызовы с ограничениями
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. 1.

    Если у вас есть ID задачи, запросите эту задачу

    Возобновите работу через GET /v1/tasks/:id вместо создания новой задачи. Обычная точка входа примера всегда создаёт задачу; путь восстановления после перезапуска должен использовать сохранённый идентификатор.

  2. 2.

    Если есть только идентификатор запроса, восстановите создание

    Отправьте тот же запрос задачи с тем же requestId. Он вернёт существующую задачу. Изменение параметров при повторном использовании идентификатора вызывает конфликт.

  3. 3.

    Обработайте статус восстановленной задачи

    Продолжайте опрашивать задачи со статусом pending или solving. Используйте решение со статусом ready либо остановитесь при failed и изучите errorCode и error. Сам по себе локальный тайм-аут не доказывает, что удалённая задача завершилась ошибкой.

HTTP-ошибки и ошибки задач требуют разной обработки. Клиент проверяет наличие неуспешных HTTP-ответов до разбора данных успешного ответа. Конечная ошибка задачи, напротив, приходит в виде статуса задачи failed; средства за неудачные решения возвращаются. Сообщение об ошибке в примере содержит только errorCode, поэтому при диагностике также изучите error в ответе задачи. Прежде чем повторять запрос с неизменёнными параметрами, см. обработку ошибок API.

Отправляйте токен с его идентификационными данными браузера

Пример останавливается на готовой задаче; он не отправляет запрос приложения на целевой сайт. Для такого запроса нужен solution.token, а не идентификатор задачи. Используйте возвращённые идентификационные данные userAgent и secChUa через исходный прокси и отправляйте запрос на нужный целевой сайт, не смешивая значения из другого сеанса браузера.

Готовая задача означает, что решение сгенерировано, а не что целевой сайт принял запрос вашего приложения. Добавьте отправку с учётом требований целевого сайта в месте, отмеченном комментарием в ветке успешного выполнения; рекомендации по передаче токена см. в руководстве по интеграции reCAPTCHA Enterprise.

Запустите первую задачу.

Начните с $5 на балансе рабочего пространства.

Создать рабочее пространство
Решайте reCAPTCHA Enterprise с Rust · solvethree