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, RngExt};
61//! # fn random_response() -> String {
62//! # let mut rng = rng();
63//! # (&mut rng)
64//! # .sample_iter(Alphanumeric)
65//! # .take(100)
66//! # .map(char::from)
67//! # .collect()
68//! # }
69//! ```
70//!
71//! ### Lambda backend implementation.
72//!
73//! See examples for more detail.
74//!
75//! ``` no_run
76//! # use lambda_runtime::Error;
77//! # use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer};
78//! # use tracing_log::LogTracer;
79//! # use tracing_subscriber::layer::SubscriberExt;
80//! # use tracing_subscriber::{EnvFilter, Registry};
81//! #
82//! mod handler {
83//! # mod error {
84//! # use thiserror::Error;
85//! # #[derive(Error, Debug)]
86//! # pub enum ContactError {
87//! # #[error("{0}")]
88//! # Hcaptcha(#[from] hcaptcha::Error),
89//! # #[error("{0}")]
90//! # Json(#[from] serde_json::Error),
91//! # }
92//! # }
93//! #
94//! # mod param {
95//! # use super::error::ContactError;
96//! # use tracing::instrument;
97//! # #[instrument(name = "get the secret key from parameter store")]
98//! # pub async fn get_parameter(key: &str) -> Result<String, ContactError> {
99//! # // Extract the secret key from your parameter store
100//! # Ok("0x123456789abcedf0123456789abcedf012345678".to_owned())
101//! # }
102//! # }
103//! #
104//! # mod record {
105//! # use super::error::ContactError;
106//! # use super::send::ContactForm;
107//! # use tracing::instrument;
108//! #
109//! # #[instrument(
110//! # name = "Write record to database"
111//! # skip(form)
112//! # fields(email = %form.email)
113//! # )]
114//! # pub async fn write(form: &ContactForm) -> Result<(), ContactError> {
115//! # // Write the contact form data to dynamodb
116//! # Ok(())
117//! # }
118//! # }
119//! #
120//! # mod send {
121//! # use super::error::ContactError;
122//! # use serde::{Deserialize, Serialize};
123//! # use tracing::instrument;
124//! #
125//! # #[derive(Deserialize, Serialize, Clone, Debug, Default)]
126//! # pub struct ContactForm {
127//! # #[serde(default)]
128//! # pub name: String,
129//! # #[serde(default)]
130//! # pub phone: String,
131//! # #[serde(default)]
132//! # pub email: String,
133//! # #[serde(default)]
134//! # pub message: String,
135//! # #[serde(default)]
136//! # pub page: String,
137//! # #[serde(default)]
138//! # pub site: String,
139//! # }
140//! #
141//! # #[instrument(name = "send notification to info mailbox", skip(_contact_form))]
142//! # pub async fn notify_office(
143//! # _contact_form: &ContactForm,
144//! # ) -> Result<(), ContactError> {
145//! # // Construct email and send message to the office info mailbox
146//! #
147//! # Ok(())
148//! # }
149//! #
150//! # #[instrument(name = "Send notification to the contact", skip(_contact_form))]
151//! # pub async fn notify_contact(
152//! # _contact_form: &ContactForm,
153//! # ) -> Result<(), ContactError> {
154//! # // Construct and send email to the contact
155//! #
156//! # Ok(())
157//! # }
158//! # }
159//!
160//! # const HCAPTCHA_SECRET: &str = "/hcaptcha/secret";
161//! #
162//! # use hcaptcha::{Captcha, Client, Request};
163//! # use lambda_runtime::{Context, Error};
164//! # use send::ContactForm;
165//! # use serde::{Deserialize, Serialize};
166//! # use tokio::join;
167//! # use tracing::{debug, error};
168//! #
169//! # #[derive(Deserialize, Serialize, Clone, Debug, Default)]
170//! # pub struct CustomEvent {
171//! # body: Option<String>,
172//! # }
173//! #
174//! # #[derive(Deserialize, Serialize, Clone, Default)]
175//! # pub struct Recaptcha {
176//! # #[serde(rename = "reCaptchaResponse")]
177//! # re_captcha_response: String,
178//! # }
179//! #
180//! # #[derive(Serialize, Clone, Debug, PartialEq)]
181//! # pub struct CustomOutput {
182//! # #[serde(rename = "isBase64Encoded")]
183//! # is_base64_encoded: bool,
184//! # #[serde(rename = "statusCode")]
185//! # status_code: u16,
186//! # body: String,
187//! # }
188//! #
189//! # impl CustomOutput {
190//! # fn new(status_code: u16, body: String) -> CustomOutput {
191//! # CustomOutput {
192//! # is_base64_encoded: false,
193//! # status_code,
194//! # body,
195//! # }
196//! # }
197//! # }
198//! #
199//! #
200//! pub async fn my_handler(e: CustomEvent, _c: Context) -> Result<CustomOutput, Error> {
201//! debug!("The event logged is: {:?}", e);
202//!
203//! let body_str = e.body.unwrap_or_else(|| "".to_owned());
204//! let captcha: Captcha = serde_json::from_str(&body_str)?;
205//!
206//! let hcaptcha_secret = param::get_parameter(HCAPTCHA_SECRET).await?;
207//!
208//! let request = Request::new(&hcaptcha_secret,
209//! captcha)?;
210//!
211//! let client = Client::new();
212//! let _response = client.verify_client_response(request).await?;
213//!
214//! let contact_form: ContactForm = serde_json::from_str(&body_str)?;
215//!
216//! let notify_office_fut = send::notify_office(&contact_form);
217//! let notify_contact_fut = send::notify_contact(&contact_form);
218//! let write_fut = record::write(&contact_form);
219//!
220//! let (notify_office, notify_contact, write) =
221//! join!(notify_office_fut, notify_contact_fut, write_fut);
222//!
223//! if let Err(e) = notify_contact {
224//! error!("Notification to the contact not sent: {}", e);
225//! return Err("Notification not sent".into());
226//! }
227//!
228//! if let Err(e) = notify_office {
229//! error!("Notification to the office not sent: {}", e);
230//! return Err("Info not sent to office".into());
231//! }
232//!
233//! if let Err(e) = write {
234//! error!("Contact information not written to database: {}", e);
235//! }
236//!
237//! Ok(CustomOutput::new(
238//! 200,
239//! format!("{}, thank you for your contact request.", contact_form.name),
240//! ))
241//! }
242//! }
243//!
244//! #[tokio::main]
245//! async fn main() -> Result<(), Error> {
246//! # LogTracer::init()?;
247//! #
248//! # let app_name = concat!(env!("CARGO_PKG_NAME"), "-", env!("CARGO_PKG_VERSION")).to_string();
249//! # let (non_blocking_writer, _guard) = tracing_appender::non_blocking(std::io::stdout());
250//! # let bunyan_formatting_layer = BunyanFormattingLayer::new(app_name, non_blocking_writer);
251//! # let subscriber = Registry::default()
252//! # .with(EnvFilter::new(
253//! # std::env::var("RUST_LOG").unwrap_or_else(|_| "INFO".to_owned()),
254//! # ))
255//! # .with(JsonStorageLayer)
256//! # .with(bunyan_formatting_layer);
257//! # tracing::subscriber::set_global_default(subscriber)?;
258//!
259//! lambda_runtime::run(lambda_runtime::handler_fn(handler::my_handler)).await?;
260//! Ok(())
261//! }
262//!
263//! ```
264//! ## Feature Flags
265//!
266//! The default library includes extended validation for the secret field and use of rustls TLS as the TLS backend.
267//! Disable this validation by setting default-features = false and enable rustls with features=["nativetls-backend"].
268//!
269//! ```toml
270//! [dependency]
271//! hcaptcha = { version = "3.2.1", default-features = false }
272//! ```
273//!
274//! The following feature flags are available:
275//! * `enterprise` - Enable methods to access enterprise service fields in the `Response`
276//! * `ext` - Enables extended validation of secret
277//! * `trace` - Enables tracing instrumentation on all functions. Traces are logged at the debug level. The value of the secret is not logged.
278//! * `nativetls-backend` - Enables native-tls backend in reqwest
279//! * `rustls-backend` - Enables rustls backend in reqwest
280//!
281//! ## Rust Version
282//!
283//! This version of hcaptcha requires Rust v1.88 or later.
284
285mod captcha;
286mod client;
287#[doc(hidden)]
288pub(crate) mod domain;
289mod error;
290mod hcaptcha;
291mod request;
292mod response;
293
294pub use captcha::Captcha;
295pub use client::Client;
296pub use client::VERIFY_URL;
297pub use error::Code;
298pub use error::Error;
299pub use request::Request;
300pub use response::Response;
301
302pub use crate::hcaptcha::Hcaptcha;
303pub use hcaptcha_derive::*;