使用 Rust 求解 reCAPTCHA Enterprise

作者:solvethree阅读需 1 分钟

从 Rust 调用 solvethree 的 REST API:创建任务,轮询至就绪,然后使用返回的浏览器身份信息和同一代理向目标提交 solution.token。以下示例直接使用 reqwest、Tokio 和 serde_json,而不是官方 Rust SDK。

本页内容

配置站点和请求

首先,在工作区控制台中为目标主机名创建启用的配置。发送主机名和操作不会创建配置。如果尚未配置目标,请参阅站点配置设置。

在运行 Rust 客户端的服务器上设置以下环境变量:

  • SOLVETHREE_API_KEY:你的工作区 API 密钥。请将其保存在服务器端。
  • SOLVETHREE_PROXY:用于求解和后续目标请求的代理 URL。
  • REQUEST_ID:此逻辑任务的可选稳定 ID。在发送请求前保存它,以便在连接中断后恢复。
reCAPTCHA 任务使用的字段
reCAPTCHA 任务使用的字段
字段发送内容
site目标主机名。替换示例中的 accounts.example.com。
action目标要求的操作。必要时替换 login。
proxy从 SOLVETHREE_PROXY 读取的单个代理 URL。
requestId包含 1–64 个 ASCII 字母、数字、下划线或连字符的幂等键。
recaptchaType此处省略,因为默认值为 enterprise-v3。

不要向此 reCAPTCHA 请求添加 type 或 url 字段。proxy 和 proxyCandidates 必须且只能提供其中一个;示例使用 proxy,以便向目标交接时使用同一连接。

创建 Rust 客户端

在 Cargo 二进制项目中,将依赖文件放入 Cargo.toml,将客户端放入 src/main.rs。设置环境变量并更改目标主机名和操作后,使用 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 令牌进行身份验证。它保留返回的任务 ID,并使用 GET /v1/tasks/:id 发送后续状态请求。

两项限制可防止客户端无限等待:reqwest 的超时将每个 HTTP 请求限制为 30 秒,而 Tokio 的超时将创建和轮询的总时长限制为 180 秒。这些是客户端设置,并非服务完成时间保证。通过 reqwest 的 ClientBuilder 禁用重定向。

只有 pending 和 solving 会继续轮询循环。ready 返回结果,failed 以错误停止,遇到意外状态时也会停止,而不是无限轮询。成功路径打印的是任务 ID,而不是令牌。

恢复中断的请求,避免再次求解

示例将 requestId 和返回的任务 ID 保存在内存中,并在发生 HTTP、传输、解析或截止时间错误时打印它们。进程终止后,这些信息不会持久保留。要支持重启恢复,请在创建前存储请求 ID 和原始任务输入,并在收到任务 ID 后立即存储。

  1. 1.

    如果已有任务 ID,则获取该任务

    使用 GET /v1/tasks/:id 恢复,而不是创建另一个任务。示例的常规入口始终会创建任务;重启恢复路径应使用已保存的 ID。

  2. 2.

    如果只有请求 ID,则恢复创建过程

    使用相同的 requestId 提交相同的任务请求。这会返回现有任务。复用 ID 时更改输入会导致冲突。

  3. 3.

    处理恢复后的状态

    继续轮询 pending 或 solving 状态的任务。使用 ready 状态的求解结果,或在 failed 状态时停止并检查 errorCode 和 error。仅凭本地超时无法认定远程任务失败。

HTTP 失败和任务失败需要采用不同的处理方式。客户端会在解析成功响应数据前检查是否为非成功 HTTP 响应。任务终态失败则通过任务状态 failed 返回;求解失败会退款。示例的失败消息仅包含 errorCode,因此诊断时还应检查任务响应中的 error。在使用未更改的输入重试前,请参阅 API 错误处理。

使用令牌对应的浏览器身份信息提交令牌

示例在任务就绪时停止,不会向目标发送应用请求。该请求需要 solution.token,而不是任务 ID。通过原始代理使用返回的 userAgent 和 secChUa 身份信息,并向预期目标提交,不要混用其他浏览器会话中的值。

任务就绪表示已生成求解结果,并不表示目标接受了你的应用请求。在成功路径中标记的注释处添加针对目标的提交逻辑;有关令牌交接的指导,请参阅 reCAPTCHA Enterprise 集成指南。

运行您的第一个任务。

使用 $5 工作区额度开始体验。

创建工作区
使用 Rust 求解 reCAPTCHA Enterprise · solvethree