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;
20use openentropy_core::telemetry::{
21 TelemetryWindowReport, collect_telemetry_snapshot, collect_telemetry_window,
22};
23
24struct AppState {
26 pool: Mutex<EntropyPool>,
27 allow_raw: bool,
28}
29
30#[derive(Deserialize)]
31struct RandomParams {
32 length: Option<usize>,
33 #[serde(rename = "type")]
34 data_type: Option<String>,
35 raw: Option<bool>,
37 conditioning: Option<String>,
39 source: Option<String>,
41}
42
43#[derive(Serialize)]
44struct RandomResponse {
45 #[serde(rename = "type")]
46 data_type: String,
47 length: usize,
48 data: serde_json::Value,
49 success: bool,
50 conditioned: bool,
52 #[serde(skip_serializing_if = "Option::is_none")]
54 source: Option<String>,
55 #[serde(skip_serializing_if = "Option::is_none")]
57 error: Option<String>,
58}
59
60#[derive(Serialize)]
61struct HealthResponse {
62 status: String,
63 sources_healthy: usize,
64 sources_total: usize,
65 raw_bytes: u64,
66 output_bytes: u64,
67}
68
69#[derive(Serialize)]
70struct SourcesResponse {
71 sources: Vec<SourceEntry>,
72 total: usize,
73 #[serde(skip_serializing_if = "Option::is_none")]
74 telemetry_v1: Option<TelemetryWindowReport>,
75}
76
77#[derive(Serialize)]
78struct SourceEntry {
79 name: String,
80 healthy: bool,
81 bytes: u64,
82 entropy: f64,
83 min_entropy: f64,
84 time: f64,
85 failures: u64,
86}
87
88#[derive(Deserialize, Default)]
89struct DiagnosticsParams {
90 telemetry: Option<bool>,
91}
92
93fn include_telemetry(params: &DiagnosticsParams) -> bool {
94 params.telemetry.unwrap_or(false)
95}
96
97async fn handle_random(
98 State(state): State<Arc<AppState>>,
99 Query(params): Query<RandomParams>,
100) -> (StatusCode, Json<RandomResponse>) {
101 let length = params.length.unwrap_or(1024).clamp(1, 65536);
102 let data_type = params.data_type.unwrap_or_else(|| "hex16".to_string());
103
104 let mode = if let Some(ref c) = params.conditioning {
106 match c.as_str() {
107 "raw" if state.allow_raw => ConditioningMode::Raw,
108 "vonneumann" | "von_neumann" | "vn" => ConditioningMode::VonNeumann,
109 "raw" => ConditioningMode::Sha256, _ => ConditioningMode::Sha256,
111 }
112 } else if params.raw.unwrap_or(false) && state.allow_raw {
113 ConditioningMode::Raw
114 } else {
115 ConditioningMode::Sha256
116 };
117
118 let pool = state.pool.lock().await;
119 let raw = if let Some(ref source_name) = params.source {
120 match pool.get_source_bytes(source_name, length, mode) {
121 Some(bytes) => bytes,
122 None => {
123 let err_msg = format!(
124 "Unknown source: {source_name}. Use /sources to list available sources."
125 );
126 return Json(RandomResponse {
127 data_type,
128 length: 0,
129 data: serde_json::Value::Array(vec![]),
130 success: false,
131 conditioned: mode != ConditioningMode::Raw,
132 source: Some(source_name.clone()),
133 error: Some(err_msg),
134 })
135 .with_status(StatusCode::BAD_REQUEST);
136 }
137 }
138 } else {
139 pool.get_bytes(length, mode)
140 };
141 let use_raw = mode == ConditioningMode::Raw;
142
143 let data = match data_type.as_str() {
144 "hex16" => {
145 let hex_pairs: Vec<String> = raw
146 .chunks(2)
147 .filter(|c| c.len() == 2)
148 .map(|c| format!("{:02x}{:02x}", c[0], c[1]))
149 .collect();
150 serde_json::Value::Array(
151 hex_pairs
152 .into_iter()
153 .map(serde_json::Value::String)
154 .collect(),
155 )
156 }
157 "uint8" => {
158 serde_json::Value::Array(raw.iter().map(|&b| serde_json::Value::from(b)).collect())
159 }
160 "uint16" => {
161 let vals: Vec<u16> = raw
162 .chunks(2)
163 .filter(|c| c.len() == 2)
164 .map(|c| u16::from_le_bytes([c[0], c[1]]))
165 .collect();
166 serde_json::Value::Array(vals.into_iter().map(serde_json::Value::from).collect())
167 }
168 _ => serde_json::Value::String(hex::encode(&raw)),
169 };
170
171 let len = match &data {
172 serde_json::Value::Array(a) => a.len(),
173 _ => length,
174 };
175
176 (
177 StatusCode::OK,
178 Json(RandomResponse {
179 data_type,
180 length: len,
181 data,
182 success: true,
183 conditioned: !use_raw,
184 source: params.source,
185 error: None,
186 }),
187 )
188}
189
190trait JsonWithStatus<T> {
191 fn with_status(self, status: StatusCode) -> (StatusCode, Json<T>);
192}
193
194impl<T> JsonWithStatus<T> for Json<T> {
195 fn with_status(self, status: StatusCode) -> (StatusCode, Json<T>) {
196 (status, self)
197 }
198}
199
200async fn handle_health(State(state): State<Arc<AppState>>) -> Json<HealthResponse> {
201 let pool = state.pool.lock().await;
202 let report = pool.health_report();
203 Json(HealthResponse {
204 status: if report.healthy > 0 {
205 "healthy".to_string()
206 } else {
207 "degraded".to_string()
208 },
209 sources_healthy: report.healthy,
210 sources_total: report.total,
211 raw_bytes: report.raw_bytes,
212 output_bytes: report.output_bytes,
213 })
214}
215
216async fn handle_sources(
217 State(state): State<Arc<AppState>>,
218 Query(params): Query<DiagnosticsParams>,
219) -> Json<SourcesResponse> {
220 let telemetry_start = include_telemetry(¶ms).then(collect_telemetry_snapshot);
221 let pool = state.pool.lock().await;
222 let report = pool.health_report();
223 drop(pool);
224 let telemetry_v1 = telemetry_start.map(collect_telemetry_window);
225 let sources: Vec<SourceEntry> = report
226 .sources
227 .iter()
228 .map(|s| SourceEntry {
229 name: s.name.clone(),
230 healthy: s.healthy,
231 bytes: s.bytes,
232 entropy: s.entropy,
233 min_entropy: s.min_entropy,
234 time: s.time,
235 failures: s.failures,
236 })
237 .collect();
238 let total = sources.len();
239 Json(SourcesResponse {
240 sources,
241 total,
242 telemetry_v1,
243 })
244}
245
246async fn handle_pool_status(
247 State(state): State<Arc<AppState>>,
248 Query(params): Query<DiagnosticsParams>,
249) -> Json<serde_json::Value> {
250 let telemetry_start = include_telemetry(¶ms).then(collect_telemetry_snapshot);
251 let pool = state.pool.lock().await;
252 let report = pool.health_report();
253 drop(pool);
254
255 let mut payload = serde_json::json!({
256 "healthy": report.healthy,
257 "total": report.total,
258 "raw_bytes": report.raw_bytes,
259 "output_bytes": report.output_bytes,
260 "buffer_size": report.buffer_size,
261 "sources": report.sources.iter().map(|s| serde_json::json!({
262 "name": s.name,
263 "healthy": s.healthy,
264 "bytes": s.bytes,
265 "entropy": s.entropy,
266 "min_entropy": s.min_entropy,
267 "time": s.time,
268 "failures": s.failures,
269 })).collect::<Vec<_>>(),
270 });
271 if let Some(window) = telemetry_start.map(collect_telemetry_window) {
272 payload["telemetry_v1"] = serde_json::json!(window);
273 }
274 Json(payload)
275}
276
277async fn handle_index(State(state): State<Arc<AppState>>) -> Json<serde_json::Value> {
278 let pool = state.pool.lock().await;
279 let source_names = pool.source_names();
280 drop(pool);
281
282 Json(serde_json::json!({
283 "name": "OpenEntropy Server",
284 "version": openentropy_core::VERSION,
285 "sources": source_names.len(),
286 "endpoints": {
287 "/": "This API index",
288 "/api/v1/random": {
289 "method": "GET",
290 "description": "Get random entropy bytes",
291 "params": {
292 "length": "Number of bytes (1-65536, default: 1024)",
293 "type": "Output format: hex16, uint8, uint16 (default: hex16)",
294 "source": format!("Request from a specific source by name. Available: {}", source_names.join(", ")),
295 "conditioning": "Conditioning mode: sha256 (default), vonneumann, raw",
296 }
297 },
298 "/sources": {
299 "description": "List all active entropy sources with health metrics",
300 "params": {
301 "telemetry": "Include telemetry_v1 start/end report (true/false, default false)"
302 }
303 },
304 "/pool/status": {
305 "description": "Detailed pool status",
306 "params": {
307 "telemetry": "Include telemetry_v1 start/end report (true/false, default false)"
308 }
309 },
310 "/health": "Health check",
311 },
312 "examples": {
313 "mixed_pool": "/api/v1/random?length=32&type=uint8",
314 "single_source": format!("/api/v1/random?length=32&source={}", source_names.first().map(|s| s.as_str()).unwrap_or("clock_jitter")),
315 "raw_output": "/api/v1/random?length=32&conditioning=raw",
316 "sources_with_telemetry": "/sources?telemetry=true",
317 "pool_with_telemetry": "/pool/status?telemetry=true",
318 }
319 }))
320}
321
322fn build_router(pool: EntropyPool, allow_raw: bool) -> Router {
324 let state = Arc::new(AppState {
325 pool: Mutex::new(pool),
326 allow_raw,
327 });
328
329 Router::new()
330 .route("/", get(handle_index))
331 .route("/api/v1/random", get(handle_random))
332 .route("/health", get(handle_health))
333 .route("/sources", get(handle_sources))
334 .route("/pool/status", get(handle_pool_status))
335 .with_state(state)
336}
337
338pub async fn run_server(
343 pool: EntropyPool,
344 host: &str,
345 port: u16,
346 allow_raw: bool,
347) -> std::io::Result<()> {
348 let app = build_router(pool, allow_raw);
349 let addr = format!("{host}:{port}");
350 let listener = tokio::net::TcpListener::bind(&addr).await?;
351 axum::serve(listener, app).await?;
352 Ok(())
353}
354
355mod hex {
357 pub fn encode(data: &[u8]) -> String {
358 data.iter().map(|b| format!("{b:02x}")).collect()
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use super::{DiagnosticsParams, include_telemetry};
365
366 #[test]
367 fn telemetry_flag_defaults_to_false() {
368 let default = DiagnosticsParams::default();
369 assert!(!include_telemetry(&default));
370 assert!(include_telemetry(&DiagnosticsParams {
371 telemetry: Some(true),
372 }));
373 }
374}