Skip to main content

openentropy_server/
lib.rs

1//! HTTP entropy server — ANU QRNG API compatible.
2//!
3//! Serves random bytes via HTTP, compatible with the ANU QRNG API format for easy integration with
4//! QRNG backend and any client expecting the ANU API format.
5
6use 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
21/// Shared server state.
22struct 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    /// If true, return raw unconditioned entropy (no SHA-256/DRBG).
33    raw: Option<bool>,
34    /// Conditioning mode: raw, vonneumann, sha256 (overrides `raw` flag).
35    conditioning: Option<String>,
36    /// Request entropy from a specific source by name.
37    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    /// Whether this output was conditioned (SHA-256) or raw.
48    conditioned: bool,
49    /// Which source was queried (null if mixed pool).
50    #[serde(skip_serializing_if = "Option::is_none")]
51    source: Option<String>,
52    /// Error message if request failed.
53    #[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    // Determine conditioning mode: ?conditioning= takes priority, then ?raw=true
90    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, // raw not allowed
95            _ => 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
273/// Build the axum router.
274fn 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
289/// Run the HTTP entropy server.
290pub 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
297// Simple hex encoding without external dep
298mod hex {
299    pub fn encode(data: &[u8]) -> String {
300        data.iter().map(|b| format!("{b:02x}")).collect()
301    }
302}