recaptcha_verify/
v3.rs

1use serde::Deserialize;
2use std::net::IpAddr;
3
4const POST_URL: &str = "https://www.google.com/recaptcha/api/siteverify";
5
6/// Error returned when ReCaptcha verification fails
7#[derive(Debug)]
8pub enum RecaptchaError {
9    Unknown(Option<String>),
10    HttpError(reqwest::Error),
11    MissingInputSecret,
12    InvalidInputSecret,
13    MissingInputResponse,
14    InvalidInputResponse,
15    BadRequest,
16    TimeoutOrDuplicate,
17}
18
19impl TryFrom<String> for RecaptchaError {
20    type Error = RecaptchaError;
21
22    fn try_from(value: String) -> Result<Self, Self::Error> {
23        Ok(match value.as_str() {
24            "missing-input-secret" => RecaptchaError::MissingInputSecret,
25            "invalid-input-secret" => RecaptchaError::InvalidInputSecret,
26            "missing-input-response" => RecaptchaError::MissingInputResponse,
27            "invalid-input-response" => RecaptchaError::InvalidInputResponse,
28            "bad-request" => RecaptchaError::BadRequest,
29            "timeout-or-duplicate" => RecaptchaError::TimeoutOrDuplicate,
30            _ => RecaptchaError::Unknown(Some(value)),
31        })
32    }
33}
34
35#[derive(Deserialize, Debug, Clone)]
36struct RecaptchaResult {
37    success: bool,
38    // challenge_ts: Option<String>,
39    // hostname: Option<String>,
40    // apk_package_name: Option<String>,
41    #[serde(rename(deserialize = "error-codes"))]
42    error_codes: Option<Vec<String>>,
43}
44
45/// # Verify ReCaptcha V2 and V3
46///
47/// This is supposed to be a (near) drop-in replacement for recaptcha-rs but using more recent
48/// versions of tokio, reqwest and serde.
49///
50/// ## Minimalist Example
51///
52/// Basic starting point.
53///
54/// ```ignore
55/// use recaptcha_verify::{RecaptchaError, verify_v3};
56///
57/// let res:Result<(), RecaptchaError> = verify_v3("secret", "token", None).await;
58/// ```
59///
60/// ## Full Example
61///
62/// End-to-end real-life use with actix and result handling.
63///
64/// ```rust
65/// #[tokio::main]
66/// async fn main() {
67///     use std::net::IpAddr;
68///     use recaptcha_verify::{RecaptchaError, verify_v3 as recaptcha_verify};
69///
70///     let recaptcha_secret_key = "secret"; // from env or config
71///     let recaptcha_token = "token"; // from request
72///     let realip_remote_addr = Some("1.1.1.1"); // actix_web::info::ConnectionInfo
73///
74///     let ip_addr;
75///     let mut ip: Option<&IpAddr> = None;
76///
77///     if let Some(remote_addr) = realip_remote_addr {
78///         if let Ok(ip_addr_res) = remote_addr.to_string().parse::<IpAddr>() {
79///             ip_addr = ip_addr_res;
80///             ip = Some(&ip_addr);
81///         }
82///     }
83///
84///     let res = recaptcha_verify(recaptcha_secret_key, recaptcha_token, ip).await;
85///
86///     if res.is_ok() {
87///         assert!(matches!(res, Ok(())));
88///     } else {
89///         assert!(matches!(res, Err(RecaptchaError::InvalidInputResponse)));
90///     }
91/// }
92/// ```
93///
94pub async fn verify_v3(
95    secret: &str,
96    response: &str,
97    remoteip: Option<&IpAddr>,
98) -> Result<(), RecaptchaError> {
99    let mut params = vec![("secret", secret), ("response", response)];
100
101    let ip_str;
102    if let Some(ip) = remoteip {
103        ip_str = ip.to_string();
104        params.push(("remoteip", ip_str.as_str()));
105    }
106
107    let client = reqwest::Client::new();
108    let response = client
109        .post(POST_URL)
110        .form(&params)
111        .send()
112        .await
113        .map_err(RecaptchaError::HttpError)?;
114
115    if let Ok(result) = response.json::<RecaptchaResult>().await {
116        if result.success {
117            return Ok(());
118        } else if let Some(errs) = result.error_codes {
119            return Err(errs
120                .first()
121                .ok_or(RecaptchaError::Unknown(None))?
122                .to_string()
123                .try_into()?);
124        }
125    }
126
127    Err(RecaptchaError::Unknown(None))
128}
129
130/// # Verify ReCaptcha
131///
132/// This is supposed to be a (near) drop-in replacement for recaptcha-rs but using more recent
133/// versions of tokio, reqwest and serde.
134///
135/// ## Minimalist Example
136///
137/// Basic starting point.
138///
139/// ```ignore
140/// use recaptcha_verify::{RecaptchaError, verify};
141///
142/// let res:Result<(), RecaptchaError> = verify("secret", "token", None).await;
143/// ```
144///
145/// ## Full Example
146///
147/// End-to-end real-life use with actix and result handling.
148///
149/// ```rust
150/// #[tokio::main]
151/// async fn main() {
152///     #![allow(deprecated)]
153///     use std::net::IpAddr;
154///     use recaptcha_verify::{RecaptchaError, verify as recaptcha_verify};
155///
156///     let recaptcha_secret_key = "secret"; // from env or config
157///     let recaptcha_token = "token"; // from request
158///     let realip_remote_addr = Some("1.1.1.1"); // actix_web::info::ConnectionInfo
159///
160///     let ip_addr;
161///     let mut ip: Option<&IpAddr> = None;
162///
163///     if let Some(remote_addr) = realip_remote_addr {
164///         if let Ok(ip_addr_res) = remote_addr.to_string().parse::<IpAddr>() {
165///             ip_addr = ip_addr_res;
166///             ip = Some(&ip_addr);
167///         }
168///     }
169///
170///     let res = recaptcha_verify(recaptcha_secret_key, recaptcha_token, ip).await;
171///
172///     if res.is_ok() {
173///         assert!(matches!(res, Ok(())));
174///     } else {
175///         assert!(matches!(res, Err(RecaptchaError::InvalidInputResponse)));
176///     }
177/// }
178/// ```
179///
180#[deprecated(
181    since = "0.2.0",
182    note = "Use `recaptcha_verify::verify_v3` instead. Or migrate to enterprise and use `recaptcha_verify::verify_enterprise`."
183)]
184pub async fn verify(
185    secret: &str,
186    response: &str,
187    remoteip: Option<&IpAddr>,
188) -> Result<(), RecaptchaError> {
189    verify_v3(secret, response, remoteip).await
190}