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