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(¶ms)
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}