Skip to main content

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}