Rust
Welcome to the Rust SDK documentation! This guide will teach you how to install and set up the SDK in no time.
This SDK uses reqwest, which depends on Tokio. If you want to use a different async runtime,
you'll have to disable the std feature of the crate and implement your own interactions with the
Duckling API. Note that disabling the std feature still requires a global allocator.
This crate also has full documentation available at docs.rs/duckity plus
all the type introspection that this document does not provide. It may be useful to check that out
in case you want to use the core module later on.
Quick Start
Before you can integrate Duckity into your application, you'll need to have the following:
- An application,
- At least one policy created in that application, and
- The ID of the policies to use
If you're missing either of those, head over to the Duckity Dashboard or read the Quick Start guide to learn how to set those up.
Once you got those ready, follow these steps to get things running on your client:
Install the SDK
Run the following cargo command in your terminal to add the duckity crate to your project:
cargo add duckitySolve a Challenge
To get and solve a challenge, use duckity::solve() like follows:
let solution: str = duckity::solve(POLICY_ID).await?;That's it! Once you have your solution token, you can send it over to your backend for validation.
The CPU-intensive part of solving the challenge is done in a separate thread via
tokio::task::spawn_blocking(). Your async runtime will not be blocked.
Validate a Challenge
Validation is done server-side. You can send the solution token to your server any way you want; A JSON field or an HTTP header is usually convenient.
To validate a solution token, you'll need 4 things:
- The solution token,
- The IP of the client that submitted the solution,
- The application's secret, and
- The policy's ID.
Once you have them, you can validate a solution token as follows:
let is_valid: bool = duckity::validate(
solution,
client_ip,
APPLICATION_SECRET,
POLICY_ID
)
.await?;That's it! If the solution token is valid, you can proceed to process your request. If it's not, return an error to the client and do not further process the request.
Advanced Usage
Solving a challenge on demand works well for simple setups. However, UX can be greatly improved by
planning when to solve challenges. Additionally, you can access the low-level APIs in the
duckity::core:: module to customize your processing of the challenges.
Asynchronous Challenge Solving
The challenge does not need to wait for the user to finish filling up a form or completing an action to be issued. When you can guess the user will need a solution token, it is a good idea to start computing it before the user needs it.
For example, if the user is logging in to a backend service via a CLI, you can fetch and solve a challenge while the user is filling up their username and password. For example:
use std::io::{self, Write};
const POLICY_ID: &str = "<your-own-policy-id>";
struct Credentials {
email: String,
password: String,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let solution_task = tokio::spawn(duckity::solve(POLICY_ID).into_future());
let credentials = tokio::task::spawn_blocking(|| {
let mut email = String::new();
let mut password = String::new();
print!("Enter your email: ");
io::stdout().flush()?;
io::stdin().read_line(&mut email)?;
print!("Enter your password: ");
io::stdout().flush()?;
io::stdin().read_line(&mut password)?;
anyhow::Ok(Credentials { email, password })
})
.await??;
let solution = solution_task.await??;
Ok(())
}duckity::core:: Module
This module is available even without the std feature. It contains all the internals that
duckity::solve() uses to decode, solve, and encode challenges and solutions, and is useful when
you want to read challenge metadata or customize the way challenges are solved.
The three functions provided by this module are the following:
duckity::core::decode(&str) -> Result<Challenge, duckity::core::DuckityDecodeError>duckity::core::solve(&Challenge) -> Solutionduckity::core::encode(&str, &Solution) -> Result<String, duckity::core::DuckityEncodeError>
Additionally, two struct types are provided:
ChallengeSolution
The Challenge struct contains 2 public fields and 1 method:
Challenge::id- The challenge's unique ID.Challenge::ip- The IP of the client this challenge was issued for.IpAddrunder thestdfeature flag,Stringwhen it's disabled.Challenge::hardness()- The hardness of the challenge.
The Solution struct is opaque. It can only be passed to duckity::core::encode() for encoding
into a solution token.
Using a Different Async Runtime
You can customize the asynchronous runtime by disabling the std feature flag and implementing the
fetching and validating yourself. The decoding, solving, and encoding functions are all provided
inside the duckity::core:: module and are still available without the std feature flag under
environments with alloc.
The async-compat crate may make migration to another runtime extremely
simple. For example, the following snippet shows how to solve and validate a challenge with
smol.
use async_compat::CompatExt;
const POLICY_ID: &str = "<insert-your-policy-here>";
fn main() -> anyhow::Result<()> {
smol::block_on(async {
let solution = duckity::solve(POLICY_ID).into_future().compat().await?;
Ok(())
})
}Integrations
This SDK integrates easily with multiple frameworks and libraries. The following examples show how to integrate with a few common libraries.
Axum
use std::net::SocketAddr;
use axum::extract::ConnectInfo;
use axum::response::IntoResponse;
use axum::{Json, Router, routing};
use reqwest::StatusCode;
use serde::Deserialize;
use tokio::net::TcpListener;
// In an actual application, make these two configurable. `clap` is a good tool for that.
const APPLICATION_SECRET: &str = "<your-application-secret>";
const POLICY_ID: &str = "<your-policy-id>";
#[tokio::main]
async fn main() {
let router = Router::new().route("/protected", routing::post(handler));
let listener = TcpListener::bind("0.0.0.0:8000").await.unwrap();
axum::serve(
listener,
router.into_make_service_with_connect_info::<SocketAddr>(),
)
.await
.unwrap();
}
#[derive(Deserialize)]
struct ProtectedRequestPayload {
solution: String,
}
async fn handler(
// If behind a reverse proxy, use X-Forwarded-For instead. Make sure it's not spoofable.
ConnectInfo(addr): ConnectInfo<SocketAddr>,
Json(payload): Json<ProtectedRequestPayload>,
) -> impl IntoResponse {
let is_valid = duckity::validate(
payload.solution,
addr.ip(),
APPLICATION_SECRET,
POLICY_ID,
)
.await
.unwrap();
if is_valid {
(StatusCode::OK, Json("This is protected!"))
} else {
(
StatusCode::BAD_REQUEST,
Json("The provided solution token was invalid."),
)
}
}Warp
use std::convert::Infallible;
use std::net::SocketAddr;
use serde::Deserialize;
use warp::{Filter, Reply};
// In an actual application, make these two configurable. `clap` is a good tool for that.
const APPLICATION_SECRET: &str = "<your-application-secret>";
const POLICY_ID: &str = "<your-policy-id>";
#[tokio::main]
async fn main() {
let protected = warp::path("protected")
.and(warp::post())
.and(warp::addr::remote())
.and(warp::body::json::<ProtectedRequestPayload>())
.and_then(handler);
warp::serve(protected)
.run(([0, 0, 0, 0], 8000))
.await;
}
#[derive(Deserialize)]
struct ProtectedRequestPayload {
solution: String,
}
async fn handler(
// If behind a reverse proxy, use X-Forwarded-For instead. Make sure it cannot be spoofed.
addr: Option<SocketAddr>,
payload: ProtectedRequestPayload,
) -> Result<impl Reply, Infallible> {
let addr = addr.expect("remote address unavailable");
let is_valid = duckity::validate(
payload.solution,
addr.ip(),
APPLICATION_SECRET,
POLICY_ID,
)
.await
.unwrap();
if is_valid {
Ok(warp::reply::with_status(
warp::reply::json(&"This is protected!"),
warp::http::StatusCode::OK,
))
} else {
Ok(warp::reply::with_status(
warp::reply::json(&"The provided solution token was invalid."),
warp::http::StatusCode::BAD_REQUEST,
))
}
}Actix Web
use std::net::SocketAddr;
use actix_web::web::Json;
use actix_web::{App, HttpRequest, HttpResponse, HttpServer, Responder, post};
use serde::Deserialize;
// In an actual application, make these two configurable. `clap` is a good tool for that.
const APPLICATION_SECRET: &str = "<your-application-secret>";
const POLICY_ID: &str = "<your-policy-id>";
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| App::new().service(protected))
.bind(("0.0.0.0", 8000))?
.run()
.await
}
#[derive(Deserialize)]
struct ProtectedRequestPayload {
solution: String,
}
#[post("/protected")]
async fn protected(req: HttpRequest, payload: Json<ProtectedRequestPayload>) -> impl Responder {
// If behind a reverse proxy, use X-Forwarded-For instead.
// Make sure it cannot be spoofed.
let addr: SocketAddr = req.peer_addr().expect("remote address unavailable");
let is_valid = duckity::validate(
payload.solution.clone(),
addr.ip(),
APPLICATION_SECRET,
POLICY_ID,
)
.await
.unwrap();
if is_valid {
HttpResponse::Ok().json("This is protected!")
} else {
HttpResponse::BadRequest().json("The provided solution token was invalid.")
}
}Contributing & License
All contributions are welcome to the SDK. Whether it's bug fixes, suggestions, new features, documentation updates, or fixing a typo, if you think you can make this SDK better, feel free to make a pull request in the GitHub repository.
This SDK is licensed under the permissive MIT License, and so will be all contributions to the SDK.