Résoudre reCAPTCHA Enterprise avec Rust

Par solvethree5 min de lecture

Utilisez l’API REST de solvethree depuis Rust : créez une tâche, interrogez son état jusqu’à ce qu’elle soit prête, puis envoyez solution.token à la cible avec l’identité de navigateur renvoyée et le même proxy. L’exemple ci-dessous utilise directement reqwest, Tokio et serde_json, et non un SDK Rust officiel.

Sur cette page

Configurer le site et la requête

Créez d’abord un profil actif pour le nom d’hôte cible dans la console de votre espace de travail. L’envoi d’un nom d’hôte et d’une action n’en crée pas. Consultez la configuration du profil de site si la cible n’est pas encore configurée.

Définissez ces variables d’environnement sur le serveur où le client Rust sera exécuté :

  • SOLVETHREE_API_KEY : la clé API de votre espace de travail. Conservez-la côté serveur.
  • SOLVETHREE_PROXY : l’URL du proxy pour la résolution et la requête cible qui suit.
  • REQUEST_ID : un identifiant stable facultatif pour cette tâche logique. Enregistrez-le avant d’envoyer la requête afin de pouvoir récupérer la tâche après une interruption de connexion.
Champs utilisés par la tâche reCAPTCHA
Champs utilisés par la tâche reCAPTCHA
ChampValeur à envoyer
siteLe nom d’hôte cible. Remplacez accounts.example.com dans l’exemple.
actionL’action attendue par la cible. Remplacez login si nécessaire.
proxyL’unique URL de proxy lue depuis SOLVETHREE_PROXY.
requestIdUne clé d’idempotence contenant 1–64 lettres ASCII, chiffres, traits de soulignement ou traits d’union.
recaptchaTypeOmis ici, car sa valeur par défaut est enterprise-v3.

N’ajoutez pas de champs type ou url à cette requête reCAPTCHA. Exactement un des champs proxy ou proxyCandidates est requis ; l’exemple utilise proxy pour que l’envoi à la cible puisse utiliser cette même connexion.

Créer le client Rust

Placez le fichier de dépendances dans Cargo.toml et le client dans src/main.rs d’un projet binaire Cargo. Après avoir défini les variables d’environnement et modifié le nom d’hôte cible et l’action, exécutez le projet avec Cargo. Les dépendances fixent reqwest à la version 0.13.5.

Cargo.toml · dépendances vérifiées
[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 · appels REST asynchrones limités
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())
        }
    }
}

Le client effectue une seule requête de création vers POST /v1/tasks, avec une authentification par jeton Bearer. Il conserve l’identifiant de tâche renvoyé et utilise GET /v1/tasks/:id pour les requêtes d’état suivantes.

Deux limites empêchent le client d’attendre indéfiniment : le délai d’expiration de reqwest limite chaque requête HTTP à 30 secondes, tandis que celui de Tokio limite l’ensemble de la création et de l’interrogation à 180 secondes. Ce sont des choix côté client, et non des garanties de délai d’exécution du service. Les redirections sont désactivées via ClientBuilder de reqwest.

Seuls pending et solving prolongent la boucle d’interrogation. ready renvoie le résultat, failed provoque un arrêt avec une erreur, et un statut inattendu provoque également un arrêt plutôt qu’une interrogation sans fin. En cas de réussite, le programme affiche l’identifiant de tâche, pas le jeton.

Récupérer une requête interrompue sans nouvelle résolution

L’exemple conserve requestId et l’identifiant de tâche renvoyé en mémoire et les affiche lorsqu’une erreur HTTP, de transport, d’analyse ou d’expiration survient. Il ne les conserve pas après l’arrêt du processus. Pour permettre une récupération après redémarrage, enregistrez l’identifiant de requête et les paramètres d’origine de la tâche avant la création, puis enregistrez l’identifiant de tâche dès sa réception.

  1. 1.

    Si vous avez un ID de tâche, récupérez cette tâche

    Reprenez avec GET /v1/tasks/:id plutôt que de créer une autre tâche. Le point d’entrée normal de l’exemple crée toujours une tâche ; le chemin de reprise après redémarrage doit utiliser l’identifiant enregistré.

  2. 2.

    Si vous n’avez qu’un identifiant de requête, récupérer la création

    Envoyez la même requête de tâche avec le même requestId. Cela renvoie la tâche existante. Modifier les paramètres tout en réutilisant l’identifiant provoque un conflit.

  3. 3.

    Traiter le statut récupéré

    Continuez à interroger les tâches au statut pending ou solving. Utilisez une solution au statut ready, ou arrêtez-vous au statut failed et examinez errorCode et error. Une expiration locale ne suffit pas à établir que la tâche distante a échoué.

Les échecs HTTP et les échecs de tâche nécessitent des traitements différents. Le client vérifie les réponses HTTP non réussies avant d’analyser une réponse de réussite. Un échec terminal de tâche est en revanche signalé par le statut de tâche failed ; les résolutions échouées sont remboursées. Le message d’échec de l’exemple ne contient que errorCode ; examinez donc aussi error dans la réponse de la tâche pour le diagnostic. Consultez la gestion des erreurs API avant de réessayer avec des paramètres inchangés.

Envoyer le jeton avec son identité de navigateur

L’exemple s’arrête lorsque la tâche est prête ; il n’envoie pas la requête applicative à la cible. Cette requête nécessite solution.token, pas l’identifiant de tâche. Utilisez l’identité userAgent et secChUa renvoyée via le proxy d’origine, et envoyez la requête à la cible prévue plutôt que de mélanger des valeurs provenant d’une autre session de navigateur.

Une tâche prête signifie qu’une solution a été générée, pas que la cible a accepté votre requête applicative. Ajoutez l’envoi propre à la cible à l’emplacement indiqué par le commentaire du chemin de réussite ; consultez le guide d’intégration reCAPTCHA Enterprise pour des conseils sur la transmission du jeton.

Lancez votre première tâche.

Démarrez avec $5 de crédit dans votre espace de travail.

Créer un espace de travail
Résoudre reCAPTCHA Enterprise avec Rust · solvethree