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
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 errorAsks 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 errorWhether 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 errorThe 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.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/captcha_verify.qwer$ curl -O https://quillolang.org/plugins/files/captcha_verify.qweThen, in your program, all of it or only what you call:
use captcha_verify.qweruse captcha_verify, captcha_verify_rules from captcha_verify // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
ureq = "2"Example
// 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
//! 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('&', "&").replace('"', """).replace('<', "<"); 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)), })}Related
captcha.qwer · 1.0.0Picture captchas made on your own server: distorted letters as a PNG data URL and the answer to keep in the session.7 views · 6 downloadsFile type file_type.qwer · 1.0.0What a file really is from its first bytes, not its name: MIME type, extension and kind, for upload checks.7 views · 9 downloadsPDF forms pdf_forms.qwer · 1.0.0PDF forms: the fields listed with their types, values and choices, filled from a map with their appearances drawn, and flattened so they cannot be changed.8 views · 6 downloadsCSRF tokens csrf_tokens.qwer · 1.0.0CSRF protection for forms and fetch calls: tokens signed for one session that expire, the signed double-submit cookie check, and the Origin, Referer and Sec-Fetch-Site checks.7 views · 7 downloadsPassword strength password_strength.qwer · 1.0.0How guessable a password is (zxcvbn): a score from 0 to 4, a warning and suggestions, and how long it would take to crack.6 views · 8 downloadsSpam filter spam_filter.qwer · 1.0.0A spam filter that learns from your own messages: naive Bayes trained on what users mark, kept in a file, scoring comments and forms from 0 to 1.7 views · 7 downloadsThread 0 posts
On Captcha checks: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.