rocket_recaptcha_v3/
lib.rs1mod errors;
10mod fairing;
11mod models;
12mod verification;
13
14use std::{collections::HashMap, marker::PhantomData, str::FromStr};
15
16use chrono::prelude::*;
17pub use errors::ReCaptchaError;
18use fairing::ReCaptchaFairing;
19pub use models::*;
20use reqwest::{
21 header::{self, HeaderMap, HeaderValue},
22 Client,
23};
24pub use rocket_client_addr::ClientRealAddr;
25use validators::{errors::RegexError, prelude::*};
26pub use verification::ReCaptchaVerification;
27use verification::ReCaptchaVerificationInner;
28
29const API_URL: &str = "https://www.google.com/recaptcha/api/siteverify";
30
31pub trait ReCaptchaVariant: Sync + Send + 'static {
32 fn get_version_str(&self) -> &'static str;
33}
34
35#[derive(Debug, Clone, Copy)]
36pub struct V3;
37
38impl ReCaptchaVariant for V3 {
39 #[inline]
40 fn get_version_str(&self) -> &'static str {
41 "v3"
42 }
43}
44
45#[derive(Debug, Clone, Copy)]
46pub struct V2;
47
48impl ReCaptchaVariant for V2 {
49 #[inline]
50 fn get_version_str(&self) -> &'static str {
51 "v2"
52 }
53}
54
55#[derive(Debug, Clone)]
56pub struct ReCaptcha<V: ReCaptchaVariant = V3> {
57 html_key: Option<ReCaptchaKey>,
58 secret_key: ReCaptchaKey,
59 phantom: PhantomData<V>,
60}
61
62impl<V: ReCaptchaVariant> ReCaptcha<V> {
63 #[inline]
64 pub fn new(html_key: Option<ReCaptchaKey>, secret_key: ReCaptchaKey) -> ReCaptcha<V> {
66 ReCaptcha {
67 html_key,
68 secret_key,
69 phantom: PhantomData,
70 }
71 }
72
73 #[inline]
74 pub fn from_str<S1: AsRef<str>, S2: AsRef<str>>(
76 html_key: Option<S1>,
77 secret_key: S2,
78 ) -> Result<ReCaptcha<V>, RegexError> {
79 #[allow(clippy::manual_map)]
80 let html_key = match html_key {
81 Some(html_key) => Some(ReCaptchaKey::parse_str(html_key.as_ref())?),
82 None => None,
83 };
84
85 let secret_key = ReCaptchaKey::parse_str(secret_key.as_ref())?;
86
87 Ok(ReCaptcha::<V>::new(html_key, secret_key))
88 }
89
90 #[inline]
91 pub fn from_string<S1: Into<String>, S2: Into<String>>(
93 html_key: Option<S1>,
94 secret_key: S2,
95 ) -> Result<ReCaptcha<V>, RegexError> {
96 #[allow(clippy::manual_map)]
97 let html_key = match html_key {
98 Some(html_key) => Some(ReCaptchaKey::parse_string(html_key.into())?),
99 None => None,
100 };
101
102 let secret_key = ReCaptchaKey::parse_string(secret_key.into())?;
103
104 Ok(ReCaptcha::<V>::new(html_key, secret_key))
105 }
106
107 #[inline]
108 pub fn get_html_key_as_str(&self) -> Option<&str> {
109 self.html_key.as_ref().map(|k| k.as_str())
110 }
111
112 #[inline]
113 pub fn get_secret_key_as_str(&self) -> &str {
114 self.secret_key.as_str()
115 }
116}
117
118impl ReCaptcha {
119 #[inline]
120 pub fn fairing() -> ReCaptchaFairing<V3> {
122 ReCaptchaFairing::<V3>::new()
123 }
124
125 #[inline]
126 pub fn fairing_v2() -> ReCaptchaFairing<V2> {
128 ReCaptchaFairing::<V2>::new()
129 }
130}
131
132impl<V: ReCaptchaVariant> ReCaptcha<V> {
133 pub async fn verify(
134 &self,
135 recaptcha_token: &ReCaptchaToken,
136 remote_ip: Option<&ClientRealAddr>,
137 ) -> Result<ReCaptchaVerification, ReCaptchaError> {
138 let request = Client::builder()
139 .default_headers({
140 let mut map = HeaderMap::with_capacity(1);
143
144 map.insert(header::CONTENT_LENGTH, HeaderValue::from(0usize));
145
146 map
147 })
148 .build()
149 .unwrap()
150 .post(API_URL)
151 .query(&{
152 let mut map = HashMap::new();
153
154 map.insert("secret", self.get_secret_key_as_str().to_string());
155 map.insert("response", recaptcha_token.as_str().to_string());
156
157 if let Some(remote_ip) = remote_ip {
158 map.insert("remoteip", remote_ip.ip.to_string());
159 }
160
161 map
162 });
163
164 let response = request
165 .send()
166 .await
167 .map_err(|err| ReCaptchaError::InternalError(format!("{:?}", err)))?;
168
169 if response.status().is_success() {
170 let body = response
171 .bytes()
172 .await
173 .map_err(|err| ReCaptchaError::InternalError(format!("{:?}", err)))?;
174
175 let result: ReCaptchaVerificationInner = serde_json::from_slice(body.as_ref())
176 .map_err(|err| ReCaptchaError::InternalError(err.to_string()))?;
177
178 if result.success {
179 let score = result.score.unwrap_or(1.0);
180 let action = result.action;
181 let challenge_ts = result.challenge_ts.ok_or_else(|| {
182 ReCaptchaError::InternalError("There is no `challenge_ts` field.".to_string())
183 })?;
184 let hostname = result.hostname.ok_or_else(|| {
185 ReCaptchaError::InternalError("There is no `hostname` field.".to_string())
186 })?;
187
188 let challenge_ts = DateTime::from_str(&challenge_ts).map_err(|_| {
189 ReCaptchaError::InternalError(format!(
190 "The format of the timestamp `{}` is incorrect.",
191 challenge_ts
192 ))
193 })?;
194
195 Ok(ReCaptchaVerification {
196 score,
197 action,
198 challenge_ts,
199 hostname,
200 })
201 } else {
202 match result.error_codes {
203 Some(error_codes) => {
204 if error_codes.contains(&"invalid-input-secret".to_string()) {
205 Err(ReCaptchaError::InvalidInputSecret)
206 } else if error_codes.contains(&"invalid-input-response".to_string()) {
207 Err(ReCaptchaError::InvalidReCaptchaToken)
208 } else if error_codes.contains(&"timeout-or-duplicate".to_string()) {
209 Err(ReCaptchaError::TimeoutOrDuplicate)
210 } else {
211 Err(ReCaptchaError::InternalError(
212 "No expected error codes.".to_string(),
213 ))
214 }
215 },
216 None => Err(ReCaptchaError::InternalError("No error codes.".to_string())),
217 }
218 }
219 } else {
220 Err(ReCaptchaError::InternalError(format!(
221 "The response status code of the `siteverify` API is {}.",
222 response.status().as_u16()
223 )))
224 }
225 }
226}