1use std::sync::Arc;
7
8use axum::{
9 Router,
10 extract::{Query, State},
11 http::StatusCode,
12 response::Json,
13 routing::get,
14};
15use serde::{Deserialize, Serialize};
16use tokio::sync::Mutex;
17
18use openentropy_core::conditioning::ConditioningMode;
19use openentropy_core::pool::EntropyPool;
20
21struct AppState {
23 pool: Mutex<EntropyPool>,
24 allow_raw: bool,
25}
26
27#[derive(Deserialize)]
28struct RandomParams {
29 length: Option<usize>,
30 #[serde(rename = "type")]
31 data_type: Option<String>,
32 raw: Option<bool>,
34 conditioning: Option<String>,
36 source: Option<String>,
38}
39
40#[derive(Serialize)]
41struct RandomResponse {
42 #[serde(rename = "type")]
43 data_type: String,
44 length: usize,
45 data: serde_json::Value,
46 success: bool,
47 conditioned: bool,
49 #[serde(skip_serializing_if = "Option::is_none")]
51 source: Option<String>,
52 #[serde(skip_serializing_if = "Option::is_none")]
54 error: Option<String>,
55}
56
57#[derive(Serialize)]
58struct HealthResponse {
59 status: String,
60 sources_healthy: usize,
61 sources_total: usize,
62 raw_bytes: u64,
63 output_bytes: u64,
64}
65
66#[derive(Serialize)]
67struct SourcesResponse {
68 sources: Vec<SourceEntry>,
69 total: usize,
70}
71
72#[derive(Serialize)]
73struct SourceEntry {
74 name: String,
75 healthy: bool,
76 bytes: u64,
77 entropy: f64,
78 time: f64,
79 failures: u64,
80}
81
82async fn handle_random(
83 State(state): State<Arc<AppState>>,
84 Query(params): Query<RandomParams>,
85) -> (StatusCode, Json<RandomResponse>) {
86 let length = params.length.unwrap_or(1024).clamp(1, 65536);
87 let data_type = params.data_type.unwrap_or_else(|| "hex16".to_string());
88
89 let mode = if let Some(ref c) = params.conditioning {
91 match c.as_str() {
92 "raw" if state.allow_raw => ConditioningMode::Raw,
93 "vonneumann" | "von_neumann" | "vn" => ConditioningMode::VonNeumann,
94 "raw" => ConditioningMode::Sha256, _ => ConditioningMode::Sha256,
96 }
97 } else if params.raw.unwrap_or(false) && state.allow_raw {
98 ConditioningMode::Raw
99 } else {
100 ConditioningMode::Sha256
101 };
102
103 let pool = state.pool.lock().await;
104 let raw = if let Some(ref source_name) = params.source {
105 match pool.get_source_bytes(source_name, length, mode) {
106 Some(bytes) => bytes,
107 None => {
108 let err_msg = format!(
109 "Unknown source: {source_name}. Use /sources to list available sources."
110 );
111 return Json(RandomResponse {
112 data_type,
113 length: 0,
114 data: serde_json::Value::Array(vec![]),
115 success: false,
116 conditioned: mode != ConditioningMode::Raw,
117 source: Some(source_name.clone()),
118 error: Some(err_msg),
119 })
120 .with_status(StatusCode::BAD_REQUEST);
121 }
122 }
123 } else {
124 pool.get_bytes(length, mode)
125 };
126 let use_raw = mode == ConditioningMode::Raw;
127
128 let data = match data_type.as_str() {
129 "hex16" => {
130 let hex_pairs: Vec<String> = raw
131 .chunks(2)
132 .filter(|c| c.len() == 2)
133 .map(|c| format!("{:02x}{:02x}", c[0], c[1]))
134 .collect();
135 serde_json::Value::Array(
136 hex_pairs
137 .into_iter()
138 .map(serde_json::Value::String)
139 .collect(),
140 )
141 }
142 "uint8" => {
143 serde_json::Value::Array(raw.iter().map(|&b| serde_json::Value::from(b)).collect())
144 }
145 "uint16" => {
146 let vals: Vec<u16> = raw
147 .chunks(2)
148 .filter(|c| c.len() == 2)
149 .map(|c| u16::from_le_bytes([c[0], c[1]]))
150 .collect();
151 serde_json::Value::Array(vals.into_iter().map(serde_json::Value::from).collect())
152 }
153 _ => serde_json::Value::String(hex::encode(&raw)),
154 };
155
156 let len = match &data {
157 serde_json::Value::Array(a) => a.len(),
158 _ => length,
159 };
160
161 (
162 StatusCode::OK,
163 Json(RandomResponse {
164 data_type,
165 length: len,
166 data,
167 success: true,
168 conditioned: !use_raw,
169 source: params.source,
170 error: None,
171 }),
172 )
173}
174
175trait JsonWithStatus<T> {
176 fn with_status(self, status: StatusCode) -> (StatusCode, Json<T>);
177}
178
179impl<T> JsonWithStatus<T> for Json<T> {
180 fn with_status(self, status: StatusCode) -> (StatusCode, Json<T>) {
181 (status, self)
182 }
183}
184
185async fn handle_health(State(state): State<Arc<AppState>>) -> Json<HealthResponse> {
186 let pool = state.pool.lock().await;
187 let report = pool.health_report();
188 Json(HealthResponse {
189 status: if report.healthy > 0 {
190 "healthy".to_string()
191 } else {
192 "degraded".to_string()
193 },
194 sources_healthy: report.healthy,
195 sources_total: report.total,
196 raw_bytes: report.raw_bytes,
197 output_bytes: report.output_bytes,
198 })
199}
200
201async fn handle_sources(State(state): State<Arc<AppState>>) -> Json<SourcesResponse> {
202 let pool = state.pool.lock().await;
203 let report = pool.health_report();
204 let sources: Vec<SourceEntry> = report
205 .sources
206 .iter()
207 .map(|s| SourceEntry {
208 name: s.name.clone(),
209 healthy: s.healthy,
210 bytes: s.bytes,
211 entropy: s.entropy,
212 time: s.time,
213 failures: s.failures,
214 })
215 .collect();
216 let total = sources.len();
217 Json(SourcesResponse { sources, total })
218}
219
220async fn handle_pool_status(State(state): State<Arc<AppState>>) -> Json<serde_json::Value> {
221 let pool = state.pool.lock().await;
222 let report = pool.health_report();
223 Json(serde_json::json!({
224 "healthy": report.healthy,
225 "total": report.total,
226 "raw_bytes": report.raw_bytes,
227 "output_bytes": report.output_bytes,
228 "buffer_size": report.buffer_size,
229 "sources": report.sources.iter().map(|s| serde_json::json!({
230 "name": s.name,
231 "healthy": s.healthy,
232 "bytes": s.bytes,
233 "entropy": s.entropy,
234 "time": s.time,
235 "failures": s.failures,
236 })).collect::<Vec<_>>(),
237 }))
238}
239
240async fn handle_index(State(state): State<Arc<AppState>>) -> Json<serde_json::Value> {
241 let pool = state.pool.lock().await;
242 let source_names = pool.source_names();
243 drop(pool);
244
245 Json(serde_json::json!({
246 "name": "OpenEntropy Server",
247 "version": openentropy_core::VERSION,
248 "sources": source_names.len(),
249 "endpoints": {
250 "/": "This API index",
251 "/api/v1/random": {
252 "method": "GET",
253 "description": "Get random entropy bytes",
254 "params": {
255 "length": "Number of bytes (1-65536, default: 1024)",
256 "type": "Output format: hex16, uint8, uint16 (default: hex16)",
257 "source": format!("Request from a specific source by name. Available: {}", source_names.join(", ")),
258 "conditioning": "Conditioning mode: sha256 (default), vonneumann, raw",
259 }
260 },
261 "/sources": "List all active entropy sources with health metrics",
262 "/pool/status": "Detailed pool status",
263 "/health": "Health check",
264 },
265 "examples": {
266 "mixed_pool": "/api/v1/random?length=32&type=uint8",
267 "single_source": format!("/api/v1/random?length=32&source={}", source_names.first().map(|s| s.as_str()).unwrap_or("clock_jitter")),
268 "raw_output": "/api/v1/random?length=32&conditioning=raw",
269 }
270 }))
271}
272
273fn build_router(pool: EntropyPool, allow_raw: bool) -> Router {
275 let state = Arc::new(AppState {
276 pool: Mutex::new(pool),
277 allow_raw,
278 });
279
280 Router::new()
281 .route("/", get(handle_index))
282 .route("/api/v1/random", get(handle_random))
283 .route("/health", get(handle_health))
284 .route("/sources", get(handle_sources))
285 .route("/pool/status", get(handle_pool_status))
286 .with_state(state)
287}
288
289pub async fn run_server(pool: EntropyPool, host: &str, port: u16, allow_raw: bool) {
291 let app = build_router(pool, allow_raw);
292 let addr = format!("{host}:{port}");
293 let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
294 axum::serve(listener, app).await.unwrap();
295}
296
297mod hex {
299 pub fn encode(data: &[u8]) -> String {
300 data.iter().map(|b| format!("{b:02x}")).collect()
301 }
302}