camera-trng/src/qrng_handlers.rs

220 lines
6.6 KiB
Rust

//! HTTP handlers for QTRNG-backed tools: dice, passwords, coin, 8 ball.
use axum::{
extract::Query,
http::{header, StatusCode},
response::{IntoResponse, Response},
};
use camera_trng::{
extract_entropy,
roll_dice,
generate_password,
dice_bytes_needed,
charset_alphanumeric,
charset_full,
charset_hex,
charset_from_flags,
filter_ambiguous,
eightball_shake,
eightball_bytes_needed,
CameraConfig,
DEFAULT_SIDES,
DEFAULT_COUNT,
MIN_SIDES,
MAX_SIDES,
PASSWORD_DEFAULT_LENGTH,
PASSWORD_MAX_LENGTH,
};
use serde::Deserialize;
fn default_dice_sides() -> u32 {
DEFAULT_SIDES
}
fn default_dice_count() -> usize {
DEFAULT_COUNT
}
#[derive(Deserialize)]
pub struct DiceQuery {
#[serde(default = "default_dice_sides")]
pub d: u32,
#[serde(default = "default_dice_count")]
pub count: usize,
}
#[derive(Deserialize)]
pub struct PasswordQuery {
#[serde(default)]
pub length: Option<usize>,
#[serde(default)]
pub charset: Option<String>,
#[serde(default)]
pub exclude_ambiguous: bool,
}
#[derive(Deserialize)]
pub struct EightBallQuery {
#[serde(default)]
pub question: Option<String>,
}
pub async fn get_dice(Query(params): Query<DiceQuery>) -> Response {
let sides = params.d;
let count = params.count;
if sides < MIN_SIDES || sides > MAX_SIDES {
return (StatusCode::BAD_REQUEST,
format!("d must be between {} and {}", MIN_SIDES, MAX_SIDES))
.into_response();
}
if count == 0 || count > camera_trng::DICE_MAX_COUNT {
return (StatusCode::BAD_REQUEST,
format!("count must be 1..{}", camera_trng::DICE_MAX_COUNT))
.into_response();
}
let need = dice_bytes_needed(sides, count);
let config = CameraConfig::from_env();
let result = tokio::task::spawn_blocking(move || {
extract_entropy(need, &config)
}).await;
let data = match result {
Ok(Ok(d)) => d,
Ok(Err(e)) => return (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
};
let rolls = match roll_dice(sides, count, &data) {
Some(r) => r,
None => return (StatusCode::INTERNAL_SERVER_ERROR, "dice roll failed").into_response(),
};
let body = serde_json::json!({
"sides": sides,
"count": count,
"rolls": rolls,
"sum": rolls.iter().sum::<u32>(),
});
Response::builder()
.header(header::CONTENT_TYPE, "application/json")
.header(header::CACHE_CONTROL, "no-store")
.body(axum::body::Body::from(body.to_string()))
.unwrap()
}
pub async fn get_password(Query(params): Query<PasswordQuery>) -> Response {
let length = params.length
.unwrap_or(PASSWORD_DEFAULT_LENGTH)
.min(PASSWORD_MAX_LENGTH);
if length == 0 {
return (StatusCode::BAD_REQUEST, "length must be >= 1").into_response();
}
let mut charset: Vec<u8> = match params.charset.as_deref() {
Some("alphanumeric") | None => charset_alphanumeric(),
Some("full") => charset_full(),
Some("hex") => charset_hex(),
Some(flags) => {
let l = flags.to_lowercase();
charset_from_flags(
l.contains('l'),
l.contains('u'),
l.contains('d'),
l.contains('s'),
)
}
};
if params.exclude_ambiguous {
charset = filter_ambiguous(&charset);
if charset.is_empty() {
return (StatusCode::BAD_REQUEST,
"charset empty after removing ambiguous chars")
.into_response();
}
}
let need = length * 4 + 64;
let config = CameraConfig::from_env();
let result = tokio::task::spawn_blocking(move || {
extract_entropy(need, &config)
}).await;
let data = match result {
Ok(Ok(d)) => d,
Ok(Err(e)) => return (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
};
let password = match generate_password(length, &charset, &data) {
Some(p) => p,
None => return (StatusCode::INTERNAL_SERVER_ERROR, "generation failed").into_response(),
};
let body = serde_json::json!({
"password": password,
"length": length,
"exclude_ambiguous": params.exclude_ambiguous,
});
Response::builder()
.header(header::CONTENT_TYPE, "application/json")
.header(header::CACHE_CONTROL, "no-store")
.body(axum::body::Body::from(body.to_string()))
.unwrap()
}
/// Quantum coin flip: one random bit from QTRNG.
pub async fn get_coin() -> Response {
let config = CameraConfig::from_env();
let result = tokio::task::spawn_blocking(move || {
extract_entropy(1, &config)
}).await;
let data = match result {
Ok(Ok(d)) if !d.is_empty() => d[0],
_ => return (StatusCode::INTERNAL_SERVER_ERROR, "entropy failed").into_response(),
};
let side = if data & 1 == 0 { "heads" } else { "tails" };
let body = serde_json::json!({ "result": side });
Response::builder()
.header(header::CONTENT_TYPE, "application/json")
.header(header::CACHE_CONTROL, "no-store")
.body(axum::body::Body::from(body.to_string()))
.unwrap()
}
/// Quantum 8 Ball: ask a question, get a quantumly-random Magic 8 Ball answer.
pub async fn get_eightball(Query(params): Query<EightBallQuery>) -> Response {
let need = eightball_bytes_needed();
let config = CameraConfig::from_env();
let result = tokio::task::spawn_blocking(move || {
extract_entropy(need, &config)
}).await;
let data = match result {
Ok(Ok(d)) => d,
Ok(Err(e)) => return (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
};
let (index, answer, sentiment) = match eightball_shake(&data) {
Some(r) => r,
None => return (StatusCode::INTERNAL_SERVER_ERROR, "8 ball shaking failed").into_response(),
};
let mut body = serde_json::json!({
"answer": answer,
"sentiment": sentiment,
"index": index,
});
if let Some(q) = params.question {
body["question"] = serde_json::Value::String(q);
}
Response::builder()
.header(header::CONTENT_TYPE, "application/json")
.header(header::CACHE_CONTROL, "no-store")
.body(axum::body::Body::from(body.to_string()))
.unwrap()
}