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