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    response::Json,
12    routing::get,
13};
14use serde::{Deserialize, Serialize};
15use tokio::sync::Mutex;
16
17use openentropy_core::conditioning::ConditioningMode;
18use openentropy_core::pool::EntropyPool;
19
20/// Shared server state.
21struct AppState {
22    pool: Mutex<EntropyPool>,
23    allow_raw: bool,
24}
25
26#[derive(Deserialize)]
27struct RandomParams {
28    length: Option<usize>,
29    #[serde(rename = "type")]
30    data_type: Option<String>,
31    /// If true, return raw unconditioned entropy (no SHA-256/DRBG).
32    raw: Option<bool>,
33    /// Conditioning mode: raw, vonneumann, sha256 (overrides `raw` flag).
34    conditioning: Option<String>,
35}
36
37#[derive(Serialize)]
38struct RandomResponse {
39    #[serde(rename = "type")]
40    data_type: String,
41    length: usize,
42    data: serde_json::Value,
43    success: bool,
44    /// Whether this output was conditioned (SHA-256) or raw.
45    conditioned: bool,
46}
47
48#[derive(Serialize)]
49struct HealthResponse {
50    status: String,
51    sources_healthy: usize,
52    sources_total: usize,
53    raw_bytes: u64,
54    output_bytes: u64,
55}
56
57#[derive(Serialize)]
58struct SourcesResponse {
59    sources: Vec<SourceEntry>,
60    total: usize,
61}
62
63#[derive(Serialize)]
64struct SourceEntry {
65    name: String,
66    healthy: bool,
67    bytes: u64,
68    entropy: f64,
69    time: f64,
70    failures: u64,
71}
72
73async fn handle_random(
74    State(state): State<Arc<AppState>>,
75    Query(params): Query<RandomParams>,
76) -> Json<RandomResponse> {
77    let length = params.length.unwrap_or(1024).clamp(1, 65536);
78    let data_type = params.data_type.unwrap_or_else(|| "hex16".to_string());
79
80    // Determine conditioning mode: ?conditioning= takes priority, then ?raw=true
81    let mode = if let Some(ref c) = params.conditioning {
82        match c.as_str() {
83            "raw" if state.allow_raw => ConditioningMode::Raw,
84            "vonneumann" | "von_neumann" | "vn" => ConditioningMode::VonNeumann,
85            "raw" => ConditioningMode::Sha256, // raw not allowed
86            _ => ConditioningMode::Sha256,
87        }
88    } else if params.raw.unwrap_or(false) && state.allow_raw {
89        ConditioningMode::Raw
90    } else {
91        ConditioningMode::Sha256
92    };
93
94    let pool = state.pool.lock().await;
95    let raw = pool.get_bytes(length, mode);
96    let use_raw = mode == ConditioningMode::Raw;
97
98    let data = match data_type.as_str() {
99        "hex16" => {
100            let hex_pairs: Vec<String> = raw
101                .chunks(2)
102                .filter(|c| c.len() == 2)
103                .map(|c| format!("{:02x}{:02x}", c[0], c[1]))
104                .collect();
105            serde_json::Value::Array(
106                hex_pairs
107                    .into_iter()
108                    .map(serde_json::Value::String)
109                    .collect(),
110            )
111        }
112        "uint8" => {
113            serde_json::Value::Array(raw.iter().map(|&b| serde_json::Value::from(b)).collect())
114        }
115        "uint16" => {
116            let vals: Vec<u16> = raw
117                .chunks(2)
118                .filter(|c| c.len() == 2)
119                .map(|c| u16::from_le_bytes([c[0], c[1]]))
120                .collect();
121            serde_json::Value::Array(vals.into_iter().map(serde_json::Value::from).collect())
122        }
123        _ => serde_json::Value::String(hex::encode(&raw)),
124    };
125
126    let len = match &data {
127        serde_json::Value::Array(a) => a.len(),
128        _ => length,
129    };
130
131    Json(RandomResponse {
132        data_type,
133        length: len,
134        data,
135        success: true,
136        conditioned: !use_raw,
137    })
138}
139
140async fn handle_health(State(state): State<Arc<AppState>>) -> Json<HealthResponse> {
141    let pool = state.pool.lock().await;
142    let report = pool.health_report();
143    Json(HealthResponse {
144        status: if report.healthy > 0 {
145            "healthy".to_string()
146        } else {
147            "degraded".to_string()
148        },
149        sources_healthy: report.healthy,
150        sources_total: report.total,
151        raw_bytes: report.raw_bytes,
152        output_bytes: report.output_bytes,
153    })
154}
155
156async fn handle_sources(State(state): State<Arc<AppState>>) -> Json<SourcesResponse> {
157    let pool = state.pool.lock().await;
158    let report = pool.health_report();
159    let sources: Vec<SourceEntry> = report
160        .sources
161        .iter()
162        .map(|s| SourceEntry {
163            name: s.name.clone(),
164            healthy: s.healthy,
165            bytes: s.bytes,
166            entropy: s.entropy,
167            time: s.time,
168            failures: s.failures,
169        })
170        .collect();
171    let total = sources.len();
172    Json(SourcesResponse { sources, total })
173}
174
175async fn handle_pool_status(State(state): State<Arc<AppState>>) -> Json<serde_json::Value> {
176    let pool = state.pool.lock().await;
177    let report = pool.health_report();
178    Json(serde_json::json!({
179        "healthy": report.healthy,
180        "total": report.total,
181        "raw_bytes": report.raw_bytes,
182        "output_bytes": report.output_bytes,
183        "buffer_size": report.buffer_size,
184        "sources": report.sources.iter().map(|s| serde_json::json!({
185            "name": s.name,
186            "healthy": s.healthy,
187            "bytes": s.bytes,
188            "entropy": s.entropy,
189            "time": s.time,
190            "failures": s.failures,
191        })).collect::<Vec<_>>(),
192    }))
193}
194
195/// Build the axum router.
196fn build_router(pool: EntropyPool, allow_raw: bool) -> Router {
197    let state = Arc::new(AppState {
198        pool: Mutex::new(pool),
199        allow_raw,
200    });
201
202    Router::new()
203        .route("/api/v1/random", get(handle_random))
204        .route("/health", get(handle_health))
205        .route("/sources", get(handle_sources))
206        .route("/pool/status", get(handle_pool_status))
207        .with_state(state)
208}
209
210/// Run the HTTP entropy server.
211pub async fn run_server(pool: EntropyPool, host: &str, port: u16, allow_raw: bool) {
212    let app = build_router(pool, allow_raw);
213    let addr = format!("{host}:{port}");
214    let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
215    axum::serve(listener, app).await.unwrap();
216}
217
218// Simple hex encoding without external dep
219mod hex {
220    pub fn encode(data: &[u8]) -> String {
221        data.iter().map(|b| format!("{b:02x}")).collect()
222    }
223}