hcaptcha/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![warn(missing_docs)]
3#![cfg_attr(docsrs, feature(rustdoc_missing_doc_code_examples))]
4#![cfg_attr(docsrs, warn(rustdoc::invalid_codeblock_attributes))]
5// SPDX-FileCopyrightText: 2022 jerusdp
6//
7// SPDX-License-Identifier: MIT OR Apache-2.0
8
9//! Hcaptcha
10//!
11//! # Build the request and verify
12//!
13//! Initialise a client using the [`Client`] builder to submit requests to the hcaptcha service validation.
14//!
15//! For each request build the request using the [`Request`] builder.
16//!
17//! Submit the request using the [`Client`] struct's [`Client::verify_request`] method to reuse the same client instance.
18//! The [`Client::verify`] method is also available but consumes the client.
19//!
20//! A [`Response`] is returned if the validation was successful or the method fails with a set of [`Error`] [`Code`]s if the validation failed.
21//!
22//! ## Examples
23//!
24//! ### Enterprise example (requires `enterprise` feature)
25//!
26//! Token needs to be supplied by the client.
27//! This example will fail as a client-provided token is not used.
28//! ```ignore
29//!     use hcaptcha::{Client, Request};
30//! # use itertools::Itertools;
31//!
32//! # #[tokio::main]
33//! # async fn main() -> Result<(), hcaptcha::Error> {
34//! #   let secret = "0x123456789abcde0f123456789abcdef012345678".to_string();
35//! #   let captcha = Captcha::new(&random_response())?
36//! #       .set_remoteip(&mockd::internet::ipv4_address())?
37//! #       .set_sitekey(&mockd::unique::uuid_v4())?;
38//! #   let remoteip = mockd::internet::ipv4_address();
39//!
40//!     let request = Request::new(&secret, captcha)?
41//!         .set_remoteip(&remoteip)?;
42//!
43//!     let client = Client::new();
44//!
45//!     let response = client.verify_client_response(request).await?;
46//!
47//!     let score = match &response.score() {
48//!         Some(v) => *v,
49//!         None => 0.0,
50//!     };
51//!     let score_reasons = match &response.score_reason() {
52//!         Some(v) => v.iter().join(", "),
53//!         None => "".to_owned(),
54//!     };
55//!     println!("\tScore: {:?}\n\tReasons: {:?}", score, score_reasons);
56//!     # Ok(())
57//! # }
58//! # use hcaptcha::Captcha;
59//! # use rand::distr::Alphanumeric;
60//! # use rand::{rng, Rng};
61//! # use std::iter;
62//! # fn random_response() -> String {
63//! #    let mut rng = rng();
64//! #    iter::repeat(())
65//! #        .map(|()| rng.sample(Alphanumeric))
66//! #        .map(char::from)
67//! #        .take(100)
68//! #        .collect()
69//! # }
70//! ```
71//!
72//! ### Lambda backend implementation.
73//!
74//! See examples for more detail.
75//!
76//! ``` no_run
77//! # use lambda_runtime::Error;
78//! # use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer};
79//! # use tracing_log::LogTracer;
80//! # use tracing_subscriber::layer::SubscriberExt;
81//! # use tracing_subscriber::{EnvFilter, Registry};
82//! #
83//! mod handler {
84//! #     mod error {
85//! #         use thiserror::Error;
86//! #         #[derive(Error, Debug)]
87//! #         pub enum ContactError {
88//! #             #[error("{0}")]
89//! #             Hcaptcha(#[from] hcaptcha::Error),
90//! #             #[error("{0}")]
91//! #             Json(#[from] serde_json::Error),
92//! #         }
93//! #     }
94//! #
95//! #     mod param {
96//! #         use super::error::ContactError;
97//! #         use tracing::instrument;
98//! #         #[instrument(name = "get the secret key from parameter store")]
99//! #         pub async fn get_parameter(key: &str) -> Result<String, ContactError> {
100//! #             // Extract the secret key from your parameter store
101//! #             Ok("0x123456789abcedf0123456789abcedf012345678".to_owned())
102//! #         }
103//! #     }
104//! #
105//! #     mod record {
106//! #         use super::error::ContactError;
107//! #         use super::send::ContactForm;
108//! #         use tracing::instrument;
109//! #
110//! #         #[instrument(
111//! #     name = "Write record to database"
112//! #     skip(form)
113//! #     fields(email = %form.email)
114//! # )]
115//! #         pub async fn write(form: &ContactForm) -> Result<(), ContactError> {
116//! #             // Write the contact form data to dynamodb
117//! #             Ok(())
118//! #         }
119//! #     }
120//! #
121//! #     mod send {
122//! #         use super::error::ContactError;
123//! #         use serde::{Deserialize, Serialize};
124//! #         use tracing::instrument;
125//! #
126//! #         #[derive(Deserialize, Serialize, Clone, Debug, Default)]
127//! #         pub struct ContactForm {
128//! #             #[serde(default)]
129//! #             pub name: String,
130//! #             #[serde(default)]
131//! #             pub phone: String,
132//! #             #[serde(default)]
133//! #             pub email: String,
134//! #             #[serde(default)]
135//! #             pub message: String,
136//! #             #[serde(default)]
137//! #             pub page: String,
138//! #             #[serde(default)]
139//! #             pub site: String,
140//! #         }
141//! #
142//! #         #[instrument(name = "send notification to info mailbox", skip(_contact_form))]
143//! #         pub async fn notify_office(
144//! #             _contact_form: &ContactForm,
145//! #         ) -> Result<(), ContactError> {
146//! #             // Construct email and send message to the office info mailbox
147//! #
148//! #             Ok(())
149//! #         }
150//! #
151//! #         #[instrument(name = "Send notification to the contact", skip(_contact_form))]
152//! #         pub async fn notify_contact(
153//! #             _contact_form: &ContactForm,
154//! #         ) -> Result<(), ContactError> {
155//! #             // Construct and send email to the contact
156//! #
157//! #             Ok(())
158//! #         }
159//! #     }
160//!
161//! #     const HCAPTCHA_SECRET: &str = "/hcaptcha/secret";
162//! #
163//! #     use hcaptcha::{Captcha, Client, Request};
164//! #     use lambda_runtime::{Context, Error};
165//! #     use send::ContactForm;
166//! #     use serde::{Deserialize, Serialize};
167//! #     use tokio::join;
168//! #     use tracing::{debug, error};
169//! #
170//! #     #[derive(Deserialize, Serialize, Clone, Debug, Default)]
171//! #     pub struct CustomEvent {
172//! #         body: Option<String>,
173//! #     }
174//! #
175//! #     #[derive(Deserialize, Serialize, Clone, Default)]
176//! #     pub struct Recaptcha {
177//! #         #[serde(rename = "reCaptchaResponse")]
178//! #         re_captcha_response: String,
179//! #     }
180//! #
181//! #     #[derive(Serialize, Clone, Debug, PartialEq)]
182//! #     pub struct CustomOutput {
183//! #         #[serde(rename = "isBase64Encoded")]
184//! #         is_base64_encoded: bool,
185//! #         #[serde(rename = "statusCode")]
186//! #         status_code: u16,
187//! #         body: String,
188//! #     }
189//! #
190//! #     impl CustomOutput {
191//! #         fn new(status_code: u16, body: String) -> CustomOutput {
192//! #             CustomOutput {
193//! #                 is_base64_encoded: false,
194//! #                 status_code,
195//! #                 body,
196//! #             }
197//! #         }
198//! #     }
199//! #
200//! #
201//!     pub async fn my_handler(e: CustomEvent, _c: Context) -> Result<CustomOutput,  Error> {
202//!         debug!("The event logged is: {:?}", e);
203//!
204//!         let body_str = e.body.unwrap_or_else(|| "".to_owned());
205//!         let captcha: Captcha = serde_json::from_str(&body_str)?;
206//!
207//!         let hcaptcha_secret = param::get_parameter(HCAPTCHA_SECRET).await?;
208//!
209//!         let request = Request::new(&hcaptcha_secret,
210//!             captcha)?;
211//!         
212//!         let client = Client::new();
213//!         let _response = client.verify_client_response(request).await?;
214//!
215//!         let contact_form: ContactForm = serde_json::from_str(&body_str)?;
216//!
217//!         let notify_office_fut = send::notify_office(&contact_form);
218//!         let notify_contact_fut = send::notify_contact(&contact_form);
219//!         let write_fut = record::write(&contact_form);
220//!
221//!         let (notify_office, notify_contact, write) =
222//!             join!(notify_office_fut, notify_contact_fut, write_fut);
223//!
224//!         if let Err(e) = notify_contact {
225//!             error!("Notification to the contact not sent: {}", e);
226//!             return Err("Notification not sent".into());
227//!         }
228//!
229//!         if let Err(e) = notify_office {
230//!             error!("Notification to the office not sent: {}", e);
231//!             return Err("Info not sent to office".into());
232//!         }
233//!
234//!         if let Err(e) = write {
235//!             error!("Contact information not written to database: {}", e);
236//!         }
237//!
238//!         Ok(CustomOutput::new(
239//!             200,
240//!             format!("{}, thank you for your contact request.", contact_form.name),
241//!         ))
242//!     }
243//! }
244//!
245//! #[tokio::main]
246//! async fn main() -> Result<(), Error> {
247//! #    LogTracer::init()?;
248//! #
249//! #    let app_name = concat!(env!("CARGO_PKG_NAME"), "-", env!("CARGO_PKG_VERSION")).to_string();
250//! #    let (non_blocking_writer, _guard) = tracing_appender::non_blocking(std::io::stdout());
251//! #    let bunyan_formatting_layer = BunyanFormattingLayer::new(app_name, non_blocking_writer);
252//! #    let subscriber = Registry::default()
253//! #        .with(EnvFilter::new(
254//! #            std::env::var("RUST_LOG").unwrap_or_else(|_| "INFO".to_owned()),
255//! #        ))
256//! #        .with(JsonStorageLayer)
257//! #        .with(bunyan_formatting_layer);
258//! #    tracing::subscriber::set_global_default(subscriber)?;
259//!
260//!     lambda_runtime::run(lambda_runtime::handler_fn(handler::my_handler)).await?;
261//!     Ok(())
262//! }
263//!
264//! ```
265//! ## Feature Flags
266//!
267//! The default library includes extended validation for the secret field and use of rustls TLS as the TLS backend.
268//! Disable this validation by setting default-features = false and enable rustls with features=["nativetls-backend"].
269//!
270//! ```toml
271//! [dependency]
272//! hcaptcha = { version = "3.1.1", default-features = false }
273//! ```
274//!
275//! The following feature flags are available:
276//! * `enterprise` - Enable methods to access enterprise service fields in the `Response`
277//! * `ext` - Enables extended validation of secret
278//! * `trace` - Enables tracing instrumentation on all functions. Traces are logged at the debug level. The value of the secret is not logged.
279//! * `nativetls-backend` - Enables native-tls backend in reqwest
280//! * `rustls-backend` - Enables rustls backend in reqwest
281//!
282//! ## Rust Version
283//!
284//! This version of hcaptcha requires Rust v1.88 or later.
285
286mod captcha;
287mod client;
288#[doc(hidden)]
289pub(crate) mod domain;
290mod error;
291mod hcaptcha;
292mod request;
293mod response;
294
295pub use captcha::Captcha;
296pub use client::Client;
297pub use client::VERIFY_URL;
298pub use error::Code;
299pub use error::Error;
300pub use request::Request;
301pub use response::Response;
302
303pub use crate::hcaptcha::Hcaptcha;
304pub use hcaptcha_derive::*;