220 lines
6.6 KiB
Rust
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()
|
|
}
|