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::*;