"""Playwish-Web – Cloudflare Turnstile: Bot-Schutz für Admin-Login und Wünsche. Kein Domain-Umzug nötig: Der Browser holt sich bei Cloudflare ein Token, der Server prüft es per siteverify. Ohne TURNSTILE_SITE_KEY/TURNSTILE_SECRET_KEY in der .env ist der Schutz aus (z. B. in Tests). Ist Cloudflare nicht erreichbar, bleibt der Admin-Login gesperrt (sicher), Wünsche gehen dagegen durch (die Party soll nicht stillstehen) – mit Logeintrag. """ import logging from urllib.parse import urlsplit import httpx from fastapi import HTTPException, Request from .config import konfig log = logging.getLogger("playwish.turnstile") VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify" def aktiv() -> bool: k = konfig() return bool(k.turnstile_site_key and k.turnstile_secret) def _siteverify(daten: dict) -> dict: r = httpx.post(VERIFY_URL, data=daten, timeout=8) r.raise_for_status() return r.json() def pruefen(token: str | None, request: Request, aktion: str, bei_ausfall_erlauben: bool) -> None: """Wirft HTTPException, wenn das Turnstile-Token fehlt oder ungültig ist.""" if not aktiv(): return if not token: raise HTTPException(400, "Bitte warte kurz, bis die Sicherheitsprüfung abgeschlossen ist.") daten = {"secret": konfig().turnstile_secret, "response": token} if request.client: daten["remoteip"] = request.client.host try: ergebnis = _siteverify(daten) except (httpx.HTTPError, ValueError) as e: if bei_ausfall_erlauben: log.warning("Turnstile nicht erreichbar (%s) – %s ausnahmsweise ohne Prüfung erlaubt", type(e).__name__, aktion) return log.error("Turnstile nicht erreichbar (%s) – %s abgelehnt", type(e).__name__, aktion) raise HTTPException(503, "Die Sicherheitsprüfung ist gerade nicht erreichbar – bitte gleich nochmal versuchen.") erwarteter_host = urlsplit(konfig().public_url).hostname if (not ergebnis.get("success") or (erwarteter_host and ergebnis.get("hostname") not in (None, erwarteter_host)) or (ergebnis.get("action") and ergebnis["action"] != aktion)): log.info("Turnstile abgelehnt (%s): %s", aktion, ergebnis.get("error-codes")) raise HTTPException(403, "Die Sicherheitsprüfung ist fehlgeschlagen – bitte nochmal versuchen.")