hcaptcha/request.rs
1// SPDX-FileCopyrightText: 2022 jerusdp
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5//! Collect the required and optional parameters for the hcaptcha api request.
6//!
7//! # Example
8//!
9//! ```
10//! use hcaptcha::Request;
11//! # #[tokio::main]
12//! # async fn main() -> Result<(), hcaptcha::Error> {
13//! let secret = get_your_secret(); // your secret key
14//! let captcha = get_captcha(); // user's response token
15//! let sitekey = get_your_sitekey(); // your site key
16//! let remoteip = get_remoteip_address(); // user's ip address
17//!
18//! let request = Request::new(&secret, captcha)?
19//! .set_sitekey(&sitekey)?
20//! .set_remoteip(&remoteip)?;
21//! # Ok(())
22//! # }
23//! # fn get_your_secret() -> String {
24//! # "0x123456789abcde0f123456789abcdef012345678".to_string()
25//! # }
26//! # use hcaptcha::Captcha;
27//! # use rand::distr::Alphanumeric;
28//! # use rand::{rng, RngExt};
29//! # fn random_response() -> String {
30//! # let mut rng = rng();
31//! # (&mut rng)
32//! # .sample_iter(Alphanumeric)
33//! # .take(100)
34//! # .map(char::from)
35//! # .collect()
36//! # }
37//! # fn get_captcha() -> Captcha {
38//! # Captcha::new(&random_response())
39//! # .unwrap()
40//! # .set_remoteip(&mockd::internet::ipv4_address())
41//! # .unwrap()
42//! # .set_sitekey(&mockd::unique::uuid_v4())
43//! # .unwrap()
44//! # }
45//! # fn get_remoteip_address() -> String {
46//! # "192.168.71.17".to_string()
47//! # }
48//! # use uuid::Uuid;
49//! # fn get_your_sitekey() -> String {
50//! # Uuid::new_v4().to_string()
51//! # }
52//!
53//! ```
54
55use crate::domain::Secret;
56use crate::Captcha;
57use crate::Error;
58
59/// Capture the required and optional data for a call to the hcaptcha API
60#[cfg_attr(docsrs, allow(rustdoc::missing_doc_code_examples))]
61#[derive(Debug, Default, serde::Serialize)]
62pub struct Request {
63 /// [Captcha] captures the response and, optionally, the remoteip
64 /// and sitekey reported by the client.
65 captcha: Captcha,
66 /// The secret_key related to the sitekey used to capture the response.
67 secret: Secret,
68}
69
70#[cfg_attr(docsrs, allow(rustdoc::missing_doc_code_examples))]
71impl Request {
72 /// Create a new Request
73 ///
74 /// # Input
75 ///
76 /// The Hcaptcha API has two mandatory parameters:
77 /// `secret`: The client's secret key for authentication
78 /// `captcha`: [Captcha] (including response token)
79 ///
80 /// # Output
81 ///
82 /// Request is returned if the input strings are valid.
83 /// [Error] is returned if the validation fails.
84 ///
85 /// # Example
86 ///
87 /// ``` no_run
88 /// use hcaptcha::Request;
89 /// # fn main() -> Result<(), hcaptcha::Error>{
90 /// let secret = get_your_secret(); // your secret key
91 /// let captcha = get_captcha(); // captcha with response token
92 ///
93 /// let request = Request::new(&secret, captcha)?;
94 /// # Ok(())
95 /// # }
96 /// # fn get_your_secret() -> String {
97 /// # "0x123456789abcde0f123456789abcdef012345678".to_string()
98 /// # }
99 /// # use hcaptcha::Captcha;
100 /// # use rand::distr::Alphanumeric;
101 /// # use rand::{rng, RngExt};
102 /// # fn random_response() -> String {
103 /// # let mut rng = rng();
104 /// # (&mut rng)
105 /// # .sample_iter(Alphanumeric)
106 /// # .take(100)
107 /// # .map(char::from)
108 /// # .collect()
109 /// # }
110 /// # fn get_captcha() -> Captcha {
111 /// # Captcha::new(&random_response())
112 /// # .unwrap()
113 /// # .set_remoteip(&mockd::internet::ipv4_address())
114 /// # .unwrap()
115 /// # .set_sitekey(&mockd::unique::uuid_v4())
116 /// # .unwrap()
117 /// # }
118 /// ```
119 /// # Logging
120 ///
121 /// If the tracing feature is enabled a debug level span is set for the
122 /// method.
123 /// The secret field will not be logged.
124 ///
125 #[allow(dead_code)]
126 #[cfg_attr(
127 feature = "trace",
128 tracing::instrument(
129 name = "Create new Request from Captcha struct.",
130 skip(secret),
131 level = "debug"
132 )
133 )]
134 pub fn new(secret: &str, captcha: Captcha) -> Result<Request, Error> {
135 Ok(Request {
136 captcha,
137 secret: Secret::parse(secret.to_owned())?,
138 })
139 }
140
141 /// Create a new Request from only the response string
142 ///
143 /// # Input
144 ///
145 /// The Hcaptcha API has two mandatory parameters:
146 /// secret: The client's secret key for authentication
147 /// response: The response code to validate
148 ///
149 /// # Output
150 ///
151 /// Request is returned if the inputs are valid.
152 /// [Error] is returned if the validation fails.
153 ///
154 /// # Example
155 ///
156 /// ``` no_run
157 /// use hcaptcha::Request;
158 /// # fn main() -> Result<(), hcaptcha::Error>{
159 /// let secret = get_your_secret(); // your secret key
160 /// let response = get_response(); // Hcaptcha client response
161 ///
162 /// let request = Request::new_from_response(&secret, &response)?;
163 /// # Ok(())
164 /// # }
165 /// # fn get_your_secret() -> String {
166 /// # "0x123456789abcde0f123456789abcdef012345678".to_string()
167 /// # }
168 /// # use rand::distr::Alphanumeric;
169 /// # use rand::{rng, RngExt};
170 /// # fn get_response() -> String {
171 /// # let mut rng = rng();
172 /// # (&mut rng)
173 /// # .sample_iter(Alphanumeric)
174 /// # .take(100)
175 /// # .map(char::from)
176 /// # .collect()
177 /// # }
178 /// ```
179 /// # Logging
180 ///
181 /// If the tracing feature is enabled a debug level span is set for the
182 /// method.
183 /// The secret field will not be logged.
184 ///
185 #[allow(dead_code)]
186 #[cfg_attr(
187 feature = "trace",
188 tracing::instrument(
189 name = "Create new Request from response string.",
190 skip(secret),
191 level = "debug"
192 )
193 )]
194 pub fn new_from_response(secret: &str, response: &str) -> Result<Request, Error> {
195 let captcha = Captcha::new(response)?;
196 Request::new(secret, captcha)
197 }
198
199 /// Specify the optional ip address value
200 ///
201 /// Update client IP address.
202 ///
203 /// # Example
204 /// ``` no_run
205 /// use hcaptcha::Request;
206 /// # #[tokio::main]
207 /// # async fn main() -> Result<(), hcaptcha::Error> {
208 /// let secret = get_your_secret(); // your secret key
209 /// let response = get_response(); // user's response token
210 /// let remoteip = get_remoteip_address(); // user's ip address
211 ///
212 /// let request = Request::new_from_response(&secret, &response)?
213 /// .set_remoteip(&remoteip)?;
214 /// # Ok(())
215 /// # }
216 /// # fn get_your_secret() -> String {
217 /// # "0x123456789abcde0f123456789abcdef012345678".to_string()
218 /// # }
219 /// # use hcaptcha::Captcha;
220 /// # use rand::distr::Alphanumeric;
221 /// # use rand::{rng, RngExt};
222 /// # fn get_response() -> String {
223 /// # let mut rng = rng();
224 /// # (&mut rng)
225 /// # .sample_iter(Alphanumeric)
226 /// # .take(100)
227 /// # .map(char::from)
228 /// # .collect()
229 /// # }
230 /// # use std::net::{IpAddr, Ipv4Addr};
231 /// # fn get_remoteip_address() -> String {
232 /// # "192.168.71.17".to_string()
233 /// # }
234 ///
235 /// ```
236 ///
237 /// #Logging
238 ///
239 /// If the `trace` feature is enabled a debug level span is set for the
240 /// method.
241 /// The secret field is not logged.
242 ///
243 #[allow(dead_code)]
244 #[cfg_attr(
245 feature = "trace",
246 tracing::instrument(
247 name = "Request verification from hcaptcha.",
248 skip(self),
249 fields(captcha = ?self.captcha),
250 level = "debug"
251 )
252 )]
253 pub fn set_remoteip(mut self, remoteip: &str) -> Result<Self, Error> {
254 self.captcha.set_remoteip(remoteip)?;
255 Ok(self)
256 }
257
258 /// Specify the optional sitekey value
259 ///
260 /// Update the sitekey.
261 ///
262 /// # Example
263 /// Create a new request and set the sitekey field in the request.
264 /// ```
265 /// use hcaptcha::Request;
266 /// # #[tokio::main]
267 /// # async fn main() -> Result<(), hcaptcha::Error> {
268 /// let secret = get_your_secret(); // your secret key
269 /// let captcha = get_captcha(); // captcha
270 /// let sitekey = get_your_sitekey(); // your site key
271 ///
272 /// let request = Request::new(&secret, captcha)?
273 /// .set_sitekey(&sitekey);
274 /// # Ok(())
275 /// # }
276 /// # fn get_your_secret() -> String {
277 /// # "0x123456789abcde0f123456789abcdef012345678".to_string()
278 /// # }
279 /// # use hcaptcha::Captcha;
280 /// # use rand::distr::Alphanumeric;
281 /// # use rand::{rng, RngExt};
282 /// # fn random_response() -> String {
283 /// # let mut rng = rng();
284 /// # (&mut rng)
285 /// # .sample_iter(Alphanumeric)
286 /// # .take(100)
287 /// # .map(char::from)
288 /// # .collect()
289 /// # }
290 /// # fn get_captcha() -> Captcha {
291 /// # Captcha::new(&random_response())
292 /// # .unwrap()
293 /// # .set_remoteip(&mockd::internet::ipv4_address())
294 /// # .unwrap()
295 /// # .set_sitekey(&mockd::unique::uuid_v4())
296 /// # .unwrap()
297 /// # }
298 /// # use uuid::Uuid;
299 /// # fn get_your_sitekey() -> String {
300 /// # Uuid::new_v4().to_string()
301 /// # }
302 ///
303 /// ```
304 ///
305 /// #Logging
306 ///
307 /// If the `trace` feature is enabled a debug level span is created for the
308 /// method.
309 /// The secret field is not logged.
310 ///
311 #[cfg_attr(
312 feature = "trace",
313 tracing::instrument(
314 name = "Request verification from hcaptcha.",
315 skip(self),
316 fields(captcha = ?self.captcha),
317 level = "debug"
318 )
319 )]
320 pub fn set_sitekey(mut self, sitekey: &str) -> Result<Self, Error> {
321 self.captcha.set_sitekey(sitekey)?;
322 Ok(self)
323 }
324
325 #[allow(dead_code)]
326 pub(crate) fn secret(&self) -> Secret {
327 self.secret.clone()
328 }
329
330 #[allow(dead_code)]
331 pub(crate) fn captcha(&self) -> Captcha {
332 self.captcha.clone()
333 }
334}
335#[cfg(test)]
336mod tests {
337 use super::*;
338 use crate::Captcha;
339 use claims::{assert_none, assert_ok};
340 use rand::distr::Alphanumeric;
341 use rand::{rng, RngExt};
342
343 fn random_hex_string(len: usize) -> String {
344 let mut rng = rng();
345
346 let chars: String = (&mut rng)
347 .sample_iter(Alphanumeric)
348 .take(len / 2)
349 .map(char::from)
350 .collect();
351
352 hex::encode(chars)
353 }
354
355 fn random_response() -> String {
356 let mut rng = rng();
357 (&mut rng)
358 .sample_iter(Alphanumeric)
359 .take(100)
360 .map(char::from)
361 .collect()
362 }
363
364 fn dummy_captcha() -> Captcha {
365 Captcha::new(&random_response())
366 .unwrap()
367 .set_remoteip(&mockd::internet::ipv4_address())
368 .unwrap()
369 .set_sitekey(&mockd::unique::uuid_v4())
370 .unwrap()
371 }
372
373 #[test]
374 fn valid_new_from_captcha_struct() {
375 let secret = format!("0x{}", random_hex_string(40));
376 let captcha = dummy_captcha();
377
378 assert_ok!(Request::new(&secret, captcha));
379 }
380
381 #[test]
382 fn valid_new_from_response() {
383 let secret = format!("0x{}", random_hex_string(40));
384 let response = random_response();
385
386 let request = Request::new_from_response(&secret, &response).unwrap();
387
388 assert_eq!(&secret, &request.secret().to_string().as_str());
389
390 let Captcha {
391 response: resp,
392 remoteip: ip,
393 sitekey: key,
394 } = request.captcha;
395
396 assert_eq!(response, resp.to_string());
397 assert_none!(ip);
398 assert_none!(key);
399 }
400}