RustでreCAPTCHA Enterpriseを解く

著者:solvethree読了時間:1分

RustからsolvethreeのREST APIを使用します。タスクを作成し、準備完了までポーリングしてから、返されたブラウザー識別情報と同じプロキシを使い、solution.tokenを対象に送信します。以下の例では、公式Rust SDKではなく、reqwest、Tokio、serde_jsonを直接使用します。

このページの内容

サイトとリクエストを設定する

まず、ワークスペースのコンソールで、対象のホスト名の有効なプロファイルを作成してください。ホスト名とアクションを送信しても、プロファイルは作成されません。対象がまだ設定されていない場合は、サイトプロファイルの設定を参照してください。

Rustクライアントを実行するサーバーで、次の環境変数を設定してください。

  • SOLVETHREE_API_KEY:ワークスペースのAPIキー。サーバー側で管理してください。
  • SOLVETHREE_PROXY:解決と、その後の対象へのリクエストに使用するプロキシURL。
  • REQUEST_ID:この論理タスク用の任意の固定ID。接続が中断しても復旧できるよう、リクエストを送信する前に保存してください。
reCAPTCHAタスクで使用するフィールド
reCAPTCHAタスクで使用するフィールド
フィールド送信する内容
site対象のホスト名。例のaccounts.example.comを置き換えてください。
action対象が要求するアクション。必要に応じてloginを置き換えてください。
proxySOLVETHREE_PROXYから読み取る単一のプロキシURL。
requestIdASCIIの英字、数字、アンダースコア、ハイフンからなる1–64文字の冪等性キー。
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())
        }
    }
}

クライアントはBearerトークンで認証し、POST /v1/tasksに作成リクエストを1回送信します。返されたタスクIDを保持し、以降のステータスリクエストにはGET /v1/tasks/:idを使用します。

クライアントが無期限に待機しないよう、2つの制限を設けています。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エラー処理を参照してください。

ブラウザー識別情報とともにトークンを送信する

この例はタスクが準備完了になった時点で終了し、対象へアプリケーションのリクエストを送信しません。そのリクエストには、タスクIDではなくsolution.tokenが必要です。返されたuserAgentとsecChUaの識別情報を使用し、元のプロキシを経由して、意図した対象へ送信してください。別のブラウザーセッションの値を混在させないでください。

タスクが準備完了であることは、解決結果が生成されたことを意味し、対象がアプリケーションのリクエストを受け入れたことを意味するわけではありません。成功時の処理で目印となるコメントの位置に、対象固有の送信処理を追加してください。トークンの引き渡しについては、reCAPTCHA Enterprise統合ガイドを参照してください。

最初のタスクを実行しましょう。

$5のワークスペースクレジットで始められます。

ワークスペースを作成
RustでreCAPTCHA Enterpriseを解く · solvethree