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