"""FastAPI entropy service. Runs the camera entropy producer in a background thread and serves the latest generated blob over HTTP. Designed for container deployment: the producer and API share one process, so there is no FIFO to manage and no cross-container IPC. Endpoints: GET /healthz -> liveness probe GET /entropy -> { hex, bits, ts } (requires X-API-Key if configured) GET / -> service info """ import os import threading import time from datetime import datetime, timezone from fastapi import FastAPI, Header, HTTPException from pydantic import BaseModel import entropy CAMERA_URL = os.environ.get("ENTROPY_CAMERA_URL", "http://192.168.0.200/mjpg/video.mjpg") BITS = int(os.environ.get("ENTROPY_BITS", "256")) INTERVAL = float(os.environ.get("ENTROPY_INTERVAL", "1.0")) NO_HEALTH = os.environ.get("ENTROPY_NO_HEALTH", "") == "1" # Simple shared-secret auth. Empty = no auth (for the first iteration). API_KEY = os.environ.get("ENTROPY_API_KEY", "") NUM_BYTES = BITS // 8 app = FastAPI(title="Entropy-RNG Service", version="1.0") class LatestBlob: """Thread-safe holder for the most recent entropy blob.""" def __init__(self): self._lock = threading.Lock() self._blob = None self._ts = None self._ok = False def set(self, blob): with self._lock: self._blob = blob self._ts = time.time() self._ok = True def get(self): with self._lock: return self._blob, self._ts, self._ok def mark_failed(self): with self._lock: self._ok = False latest = LatestBlob() def producer_loop(): """Continuously generate entropy blobs and stash the latest one.""" while True: try: frame = entropy.fetch_frame_opencv(CAMERA_URL) raw = entropy.frame_raw_bytes(frame) if not NO_HEALTH: from health import check as health_check health_check(raw) digest = entropy.HMACDRBG(raw).generate(NUM_BYTES) latest.set(digest) except Exception as e: print(f"[producer] error: {e}", flush=True) latest.mark_failed() time.sleep(INTERVAL) @app.on_event("startup") def _start_producer(): t = threading.Thread(target=producer_loop, daemon=True) t.start() class EntropyResponse(BaseModel): hex: str bits: int ts: str @app.get("/") def root(): blob, ts, ok = latest.get() return { "service": "entropy-rng", "bits": BITS, "healthy": ok, "last_update": datetime.fromtimestamp(ts, tz=timezone.utc).isoformat() if ts else None, } @app.get("/healthz") def healthz(): blob, ts, ok = latest.get() if not ok or blob is None: raise HTTPException(status_code=503, detail="no entropy available") return {"status": "ok"} @app.get("/entropy") def get_entropy(x_api_key: str | None = Header(default=None)): if API_KEY and x_api_key != API_KEY: raise HTTPException(status_code=401, detail="invalid or missing API key") blob, ts, ok = latest.get() if blob is None: raise HTTPException(status_code=503, detail="no entropy available yet") return EntropyResponse( hex=blob.hex(), bits=BITS, ts=datetime.fromtimestamp(ts, tz=timezone.utc).isoformat(), )