Services

Captcha checks

Server-side checks of Cloudflare Turnstile, hCaptcha and reCAPTCHA v2 and v3 answers with one call and one result shape, the hostname, action and score checked, and the widget's HTML for the form.

Version 1.0.0 · 3 functions · 1 crate

#forms#spam#security#sign-up

Functions

What use captcha_verify.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use captcha_verify, captcha_verify_rules from captcha_verify (Quillo 0.6).

captcha_verify(provider: str, secret: str, token: str, remote_ip: str, verify_url: str) → anyor an error

Asks provider ("turnstile", "hcaptcha" or "recaptcha") whether token (the form's cf-turnstile-response, h-captcha-response or g-recaptcha-response) is a solved challenge, with the site's secret and the visitor's remote_ip (or ""). verify_url sends the check elsewhere (a test); "" for CAPTCHA_VERIFY_URL, else the provider's. Gives { success, provider, hostname, action, score, challenge_ts, error_codes }; score (reCAPTCHA v3, hCaptcha Enterprise) runs from 0, a bot, to 1. A missing token fails without a request.

captcha_verify_rules(result: any, rules: map) → boolor an error

Whether a result from captcha_verify is good enough: successful, and when rules names them, from hostname (your site, so a token solved elsewhere is refused), for action ("signup"), with at least min_score. Gives true, or an error value saying which rule failed.

captcha_widget(provider: str, site_key: str, action: str) → stror an error

The HTML to put in a form for provider's widget with your site_key: the script and the element it fills. action names the form for the checks (Turnstile and reCAPTCHA), or "". Show it with raw(...).

Get it

$ quillo install captcha_verify               # captcha_verify.qwer into this folder$ quillo install captcha_verify --example     # and its example, captcha_verify.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/captcha_verify.qwer$ curl -O https://quillolang.org/plugins/files/captcha_verify.qwe

Then, in your program, all of it or only what you call:

main.qwe
use captcha_verify.qweruse captcha_verify, captcha_verify_rules from captcha_verify      // Quillo 0.6: only these

It uses this crate, which cargo fetches the first time the plugin is built:

[dependencies]
ureq = "2"

Example

captcha_verify.qwe
// A sign-up form behind Cloudflare Turnstile: the widget in the form, the answer checked on the// server with the hostname and action, before the account is made.////   TURNSTILE_SITE_KEY=... TURNSTILE_SECRET=... quillo run captcha_verify.qwe      then open http://localhost:8080/signup//// Without keys it uses Cloudflare's test keys, which always pass. CAPTCHA_VERIFY_URL (SINK_URL in// tests) sends the check to another server.use "captcha_verify.qwer"let SITE_KEY = env("TURNSTILE_SITE_KEY", "1x00000000000000000000AA")let SECRET = env("TURNSTILE_SECRET", "1x0000000000000000000000000000000AA")let VERIFY_URL = env("CAPTCHA_VERIFY_URL", null) ?? env("SINK_URL", "")route GET /signup = respond(200, `<!doctype html><title>Sign up</title><form method="post" action="/signup">  <input name="email" type="email" required>  ${captcha_widget("turnstile", SITE_KEY, "signup")?}  <button>Create account</button></form>`, { "content-type": "text/html; charset=utf-8" })route POST /signup { form: { email: str } } {    let result = captcha_verify("turnstile", SECRET, request.form["cf-turnstile-response"] ?? "", request.ip, VERIFY_URL)?    let ok = captcha_verify_rules(result, { hostname: "shop.example.com", action: "signup" })    if is_err(ok) { reply 403 ok.error }    `Welcome, ${email}`}// The checks without a browser: the stand-in answers without "success", and a reCAPTCHA v3// answer is judged by its score.route GET /check {    let r = captcha_verify("turnstile", SECRET, "XXXX.DUMMY.TOKEN.XXXX", "203.0.113.7", VERIFY_URL)?    {        asked: r.provider,        passed: r.success,        missing_token: captcha_verify("hcaptcha", "0x0000000000000000000000000000000000000000", "", "", VERIFY_URL)?.error_codes,        low_score: captcha_verify_rules({ success: true, hostname: "shop.example.com", action: "login", score: 0.3 }, { action: "login", min_score: 0.5 }).error,        other_site: captcha_verify_rules({ success: true, hostname: "evil.example" }, { hostname: "shop.example.com" }).error,    }}
$ quillo run captcha_verify.qwe$ curl 'localhost:8080/check'$ curl 'localhost:8080/signup'

It talks to the Redis that REDIS_URL names, else Redis on this machine. The site's test runs it against a stand-in.

Source

captcha_verify.qwer
//! Server-side checks of Cloudflare Turnstile, hCaptcha and Google reCAPTCHA (v2 and v3) answers, with one call and one result shape; the hostname, action and score checked; and the widget's HTML for the form.//! version: 1.0.0//!//! [dependencies]//! ureq = "2"use serde_json::{json, Value};fn endpoint(provider: &str) -> Result<&'static str, String> {    Ok(match provider {        "turnstile" => "https://challenges.cloudflare.com/turnstile/v0/siteverify",        "hcaptcha" => "https://api.hcaptcha.com/siteverify",        "recaptcha" => "https://www.google.com/recaptcha/api/siteverify",        other => return Err(format!("unknown provider `{}`: turnstile, hcaptcha or recaptcha", other)),    })}fn enc(s: &str) -> String {    s.bytes().map(|b| if b.is_ascii_alphanumeric() || b"-_.~".contains(&b) { (b as char).to_string() } else { format!("%{:02X}", b) }).collect()}/// Asks `provider` ("turnstile", "hcaptcha" or "recaptcha") whether `token` (the form's `cf-turnstile-response`,/// `h-captcha-response` or `g-recaptcha-response`) is a solved challenge, with the site's `secret` and the visitor's `remote_ip`/// (or ""). `verify_url` sends the check elsewhere (a test); "" for CAPTCHA_VERIFY_URL, else the provider's. Gives `{ success,/// provider, hostname, action, score, challenge_ts, error_codes }`; `score` (reCAPTCHA v3, hCaptcha Enterprise) runs from 0, a/// bot, to 1. A missing token fails without a request.pub fn captcha_verify(provider: &str, secret: &str, token: &str, remote_ip: &str, verify_url: &str) -> Result<Value, String> {    let p = provider.to_lowercase();    let mut url = endpoint(&p)?.to_string();    let other = if verify_url.is_empty() { std::env::var("CAPTCHA_VERIFY_URL").unwrap_or_default() } else { verify_url.to_string() };    if !other.is_empty() {        url = other;    }    if token.trim().is_empty() {        return Ok(json!({ "success": false, "provider": p, "hostname": null, "action": null, "score": null, "challenge_ts": null, "error_codes": ["missing-input-response"] }));    }    if token.len() > 4096 {        return Ok(json!({ "success": false, "provider": p, "hostname": null, "action": null, "score": null, "challenge_ts": null, "error_codes": ["invalid-input-response"] }));    }    let mut body = format!("secret={}&response={}", enc(secret), enc(token.trim()));    if !remote_ip.is_empty() {        body.push_str(&format!("&remoteip={}", enc(remote_ip)));    }    let res = ureq::post(&url).timeout(std::time::Duration::from_secs(10)).set("Content-Type", "application/x-www-form-urlencoded").send_string(&body);    let text = match res {        Ok(r) => r.into_string().unwrap_or_default(),        Err(ureq::Error::Status(c, r)) => return Err(format!("{} answered {}: {}", p, c, r.into_string().unwrap_or_default().chars().take(200).collect::<String>())),        Err(e) => return Err(format!("{}: {}", p, e)),    };    let v: Value = serde_json::from_str(&text).map_err(|_| format!("{} did not answer JSON", p))?;    let codes = v.get("error-codes").cloned().unwrap_or(json!([]));    Ok(json!({        "success": v["success"].as_bool().unwrap_or(false),        "provider": p,        "hostname": v["hostname"],        "action": v["action"],        "score": v["score"],        "challenge_ts": v["challenge_ts"],        "error_codes": if v.get("success").is_none() && codes.as_array().is_some_and(|a| a.is_empty()) { json!(["unexpected-answer"]) } else { codes },    }))}/// Whether a result from `captcha_verify` is good enough: successful, and when `rules` names them, from `hostname` (your/// site, so a token solved elsewhere is refused), for `action` ("signup"), with at least `min_score`. Gives true, or an error value/// saying which rule failed.pub fn captcha_verify_rules(result: serde_json::Value, rules: std::collections::HashMap<String, serde_json::Value>) -> Result<bool, String> {    if result["success"] != json!(true) {        let codes: Vec<String> = result["error_codes"].as_array().into_iter().flatten().filter_map(|c| c.as_str().map(str::to_string)).collect();        return Err(format!("the challenge was not passed ({})", if codes.is_empty() { "no reason given".into() } else { codes.join(", ") }));    }    if let Some(h) = rules.get("hostname").and_then(|v| v.as_str()) {        if result["hostname"].as_str() != Some(h) {            return Err(format!("solved on {}, not {}", result["hostname"].as_str().unwrap_or("an unknown host"), h));        }    }    if let Some(a) = rules.get("action").and_then(|v| v.as_str()) {        if result["action"].as_str() != Some(a) {            return Err(format!("solved for action {}, not {}", result["action"].as_str().unwrap_or("none"), a));        }    }    if let Some(m) = rules.get("min_score").and_then(|v| v.as_f64()) {        match result["score"].as_f64() {            Some(s) if s >= m => {}            Some(s) => return Err(format!("score {} is below {}", s, m)),            None => return Err("no score in the answer (only reCAPTCHA v3 and hCaptcha Enterprise give one)".into()),        }    }    Ok(true)}/// The HTML to put in a form for `provider`'s widget with your `site_key`: the script and the element it fills. `action` names/// the form for the checks (Turnstile and reCAPTCHA), or "". Show it with `raw(...)`.pub fn captcha_widget(provider: &str, site_key: &str, action: &str) -> Result<String, String> {    let esc = |s: &str| s.replace('&', "&amp;").replace('"', "&quot;").replace('<', "&lt;");    let act = if action.is_empty() { String::new() } else { format!(" data-action=\"{}\"", esc(action)) };    Ok(match provider.to_lowercase().as_str() {        "turnstile" => format!("<script src=\"https://challenges.cloudflare.com/turnstile/v0/api.js\" async defer></script>\n<div class=\"cf-turnstile\" data-sitekey=\"{}\"{}></div>", esc(site_key), act),        "hcaptcha" => format!("<script src=\"https://js.hcaptcha.com/1/api.js\" async defer></script>\n<div class=\"h-captcha\" data-sitekey=\"{}\"></div>", esc(site_key)),        "recaptcha" => format!("<script src=\"https://www.google.com/recaptcha/api.js\" async defer></script>\n<div class=\"g-recaptcha\" data-sitekey=\"{}\"{}></div>", esc(site_key), act),        other => return Err(format!("unknown provider `{}`: turnstile, hcaptcha or recaptcha", other)),    })}
Thread
67Useful? Like it, or add to its thread below.

Thread 0 posts

On Captcha checks: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.