#!/usr/bin/env python3 """BSN Health Check — Überwacht brettspiel-news.de + Cloudflare-Status. Ausgabe: LEER wenn alles OK. Nur bei Problemen wird ein Alarm-Text ausgegeben. Läuft als Hermes-Cronjob (no_agent=True, alle 5 Minuten). Überwachte Fehlertypen: - HTTP 5xx mit CF-Ray-Header (Cloudflare-Fehlerseiten) - HTTP 502/521/522/523/524/525/526/530 (spezifische CF-Error-Codes) - Cloudflare-Status-API: ungelöste Incidents (gefiltert auf CDN/DNS/Routing) - Komplettausfall (keine HTTP-Antwort) Testschleife (neu): 1. Wenn Seite offline/CF-Fehler → prüfe Alternativ-URL (wp.snkbnet.de) → Erreichbar = Server läuft, Cloudflare-Problem wahrscheinlich 2. Cloudflare-Status-Seite scrapen → Europa-Re-Routing erkennen """ import json import re import subprocess from datetime import datetime, timezone TARGET = "https://www.brettspiel-news.de/" SNKBNET_URL = "https://wp.snkbnet.de/login_up.php?success_redirect_url=%2Fsmb%2Fweb%2Fview" CF_STATUS_API = "https://www.cloudflarestatus.com/api/v2" CF_STATUS_PAGE = "https://www.cloudflarestatus.com/" TIMEOUT = 15 # Cloudflare-Fehlermuster (Body-Scan nach HTTP 5xx) CF_ERROR_PATTERNS = [ ("Error 502", "502 Bad Gateway — Origin-Server nicht erreichbar"), ("Error 521", "521 Web Server Down — Server antwortet nicht"), ("Error 522", "522 Connection Timeout — Timeout zum Origin"), ("Error 523", "523 Origin Unreachable — Origin nicht erreichbar"), ("Error 524", "524 A Timeout Occurred — Timeout"), ("Error 525", "525 SSL Handshake Failed — SSL-Fehler"), ("Error 526", "526 Invalid SSL Certificate — Ungültiges Zertifikat"), ("Error 530", "530 Origin DNS Error — DNS-Fehler"), ("cloudflare-nginx", "Cloudflare-nginx — CF-Fehlerseite"), ("Bad gateway", "Bad Gateway — CF-Proxy-Fehler"), ] # Cloudflare-Services die NICHTS mit Website-Erreichbarkeit zu tun haben IRRELEVANT_SERVICES = [ "workers ai", "workers kv", "durable objects", "d1", "r2", "pages", "images", "stream", "zero trust", "access", "spectrum", "magic", "warp", "analytics", "logs", "dashboard", "registrar", "billing", "support case", "ssl for saas", "custom certificates" ] def curl(url, timeout=TIMEOUT): """Führe curl aus und gib Status, Header, Body zurück.""" try: result = subprocess.run( ["curl", "-sL", "-o", "/tmp/bsn_health_body.tmp", "-w", "%{http_code}|%{time_total}|%{size_download}", "-D", "/tmp/bsn_health_headers.tmp", "--max-time", str(timeout), url], capture_output=True, text=True, timeout=timeout + 5 ) parts = result.stdout.strip().split("|") http_code = int(parts[0]) if parts and parts[0].isdigit() else 0 time_total = float(parts[1]) if len(parts) > 1 else 0 size = int(parts[2]) if len(parts) > 2 else 0 body = headers = "" try: with open("/tmp/bsn_health_body.tmp", "r", errors="replace") as f: body = f.read()[:5000] with open("/tmp/bsn_health_headers.tmp", "r", errors="replace") as f: headers = f.read()[:2000] except Exception: pass return {"http_code": http_code, "time": time_total, "size": size, "body": body, "headers": headers, "ok": True} except Exception as e: return {"http_code": 0, "time": 0, "size": 0, "body": "", "headers": "", "ok": False, "error": str(e)} def check_snkbnet(): """Testschleife: Prüfe Alternativ-URL auf demselben Server. Wenn wp.snkbnet.de erreichbar ist, läuft der Server grundsätzlich — das Problem liegt dann mit hoher Wahrscheinlichkeit bei Cloudflare. Returns: dict mit 'reachable' (bool), 'http_code', 'time', 'error' """ r = curl(SNKBNET_URL) if not r["ok"]: return {"reachable": False, "http_code": 0, "time": 0, "error": r.get("error", "Unbekannt")} # Erfolg: Alles außer 5xx gilt als erreichbar # (302/303 Redirects bei Login sind normal und bedeuten Server läuft) is_reachable = r["http_code"] < 500 and r["http_code"] > 0 return {"reachable": is_reachable, "http_code": r["http_code"], "time": r["time"], "error": None} def check_cf_europe_rerouting(): """Scrape cloudflarestatus.com auf Europa-Re-Routing. Sucht die HTML-Seite nach Hinweisen auf Rerouting in Europa. Cloudflare listet regionale Status in der Komponenten-Tabelle. Returns: str | None: Alarm-Text bei Fund, None sonst """ r = curl(CF_STATUS_PAGE) if not r["ok"] or r["http_code"] != 200: return None html = r["body"].lower() alerts = [] # Suche nach "re-routed" oder "partially re-routed" im Europa-Kontext # Pattern: Im HTML gibt's Tabellen/Divs mit Region+Status reroute_patterns = [ (r"europe.*?re-routed", "🟡 Europa: Re-Routed erkannt"), (r"europe.*?partially re-routed", "🟠 Europa: Partially Re-Routed"), (r"re-routed.*?europe", "🟡 Re-Routed in Europa"), (r"partially re-routed.*?europe", "🟠 Partially Re-Routed in Europa"), ] for pattern, desc in reroute_patterns: if re.search(pattern, html, re.DOTALL): alerts.append(f" • {desc}") # Alternativ: Prüfe Components-API auf Europa-Status try: comp_r = curl(CF_STATUS_API + "/components.json") if comp_r["ok"]: data = json.loads(comp_r["body"]) components = data.get("components", []) for comp in components: name = comp.get("name", "").lower() status = comp.get("status", "") # "partial_outage" oder "degraded_performance" im Europa-Kontext if ("europe" in name or "europa" in name) and \ status in ("partial_outage", "degraded_performance", "major_outage"): alerts.append( f" • CF-Komponente [{status}]: {comp.get('name', '?')}" ) except Exception: pass return "\n".join(alerts) if alerts else None def check_cf_status(): """Prüfe Cloudflare-Status-API auf ungelöste Incidents.""" try: r = curl(CF_STATUS_API + "/incidents/unresolved.json") if not r["ok"]: return None data = json.loads(r["body"]) incidents = data.get("incidents", []) if not incidents: return None alerts = [] for inc in incidents: name = inc.get("name", "").lower() if any(kw in name for kw in IRRELEVANT_SERVICES): continue impact = inc.get("impact", "unknown") link = inc.get("shortlink", "") alerts.append(f"🔴 CF-Incident [{impact}]: {inc.get('name', '?')}\n {link}") return "\n".join(alerts) if alerts else None except Exception: return None def main(): now = datetime.now(timezone.utc).strftime("%d.%m.%Y %H:%M UTC") alerts = [] r = curl(TARGET) has_problem = False # --- Fall 1: Komplettausfall --- if not r["ok"]: has_problem = True alerts.append(f"❌ brettspiel-news.de NICHT ERREICHBAR\n Fehler: {r.get('error', 'Unbekannt')}") # --- Fall 2: HTTP-Fehler --- elif r["http_code"] >= 500: has_problem = True has_cf = "cf-ray" in r["headers"].lower() if has_cf: cf_ray = "" for line in r["headers"].split("\n"): if line.lower().startswith("cf-ray:"): cf_ray = line.split(":", 1)[1].strip() break found = [] for pattern, desc in CF_ERROR_PATTERNS: if pattern.lower() in r["body"].lower(): found.append(f" • {desc}") alerts.append( f"❌ brettspiel-news.de: HTTP {r['http_code']} (CF-Fehler)\n" f" CF-Ray: {cf_ray}\n" + ("\n".join(found) if found else " CF-Fehlerseite erkannt") ) else: alerts.append(f"⚠️ brettspiel-news.de: HTTP {r['http_code']} (Server-Fehler)") elif r["http_code"] == 403: has_problem = True alerts.append(f"⚠️ brettspiel-news.de: HTTP 403 (möglicher CF-Block/Challenge)") elif r["http_code"] == 0 or r["http_code"] >= 600: has_problem = True alerts.append(f"❌ brettspiel-news.de: Keine gültige HTTP-Antwort (Code: {r['http_code']})") # --- Testschleife: Nur bei Problemen --- if has_problem: # 1. Prüfe Alternativ-URL (gleicher Server, andere Domain) snkb = check_snkbnet() if snkb["reachable"]: alerts.append( f"\n🧪 Testschleife:\n" f" ✅ Alternativ-URL erreichbar (HTTP {snkb['http_code']}, {snkb['time']:.2f}s)\n" f" 📡 Server läuft — Cloudflare-Problem wahrscheinlich" ) else: alerts.append( f"\n🧪 Testschleife:\n" f" ❌ Alternativ-URL NICHT erreichbar" + (f" (Fehler: {snkb['error']})" if snkb.get('error') else "") + f"\n ⚠️ Server selbst könnte betroffen sein" ) # 2. Cloudflare-Status-Seite auf Europa-Re-Routing prüfen cf_europe = check_cf_europe_rerouting() if cf_europe: alerts.append(f"\n🌍 Cloudflare-Status (Europa):\n{cf_europe}") else: alerts.append("\n🌍 Cloudflare-Status: Kein Europa-Re-Routing erkannt") # --- CF-Status-API immer mitprüfen --- cf_alert = check_cf_status() if cf_alert: alerts.append(f"\n📢 Cloudflare-Incidents:\n{cf_alert}") # --- Ausgabe --- if has_problem: print(f"🚨 BSN-Health-Alarm {now}\n" + "\n".join(alerts)) # Sonst: still (kein stdout → kein Alarm) if __name__ == "__main__": main()