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 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
22/// Shared server state.
23///
24/// `EntropyPool` uses interior mutability (`Mutex<Vec<u8>>`, `Mutex<[u8;32]>`, etc.)
25/// so all its methods take `&self`. No outer mutex needed — concurrent HTTP requests
26/// can access the pool simultaneously without serializing.
27struct 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    /// If true, return raw unconditioned entropy (no SHA-256/DRBG).
38    raw: Option<bool>,
39    /// Conditioning mode: raw, vonneumann, sha256 (overrides `raw` flag).
40    conditioning: Option<String>,
41    /// Request entropy from a specific source by name.
42    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    /// Whether this output was conditioned (SHA-256) or raw.
53    conditioned: bool,
54    /// Which source was queried (null if mixed pool).
55    #[serde(skip_serializing_if = "Option::is_none")]
56    source: Option<String>,
57    /// Error message if request failed.
58    #[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    // Determine conditioning mode: ?conditioning= takes priority, then ?raw=true
107    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(&params).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(&params).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
345/// Build the axum router.
346fn 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
358/// Run the HTTP entropy server.
359///
360/// Returns an error if the address cannot be bound or the server encounters
361/// a fatal I/O error.
362pub 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
375// Simple hex encoding without external dep
376mod 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}