"""Server-Monitoring pro Konto: eigene Serverliste + Erreichbarkeitsprüfung (HTTP oder TCP-Port).""" import asyncio import re import time import httpx from fastapi import APIRouter, Body, Depends, HTTPException from .auth import current_user from .db import now_iso, transaction from .links import ValidationError, normalize_url router = APIRouter() TYPES = {"http", "tcp"} MAX_MONITORS = 50 TIMEOUT = 5 HOST_PORT = re.compile(r"(?P[A-Za-z0-9.-]+|\[[0-9A-Fa-f:.]+\]):(?P\d{1,5})") def _monitor(row) -> dict: return {"id": row["id"], "name": row["name"], "type": row["type"], "target": row["target"], "createdAt": row["created_at"]} def _validate(data: dict) -> dict: name = data.get("name").strip() if isinstance(data.get("name"), str) else "" if not name or len(name) > 60: raise ValidationError("Name fehlt oder ist zu lang (max. 60 Zeichen).") kind = data.get("type") if kind not in TYPES: raise ValidationError("Ungültiger Prüf-Typ.") target = data.get("target").strip() if isinstance(data.get("target"), str) else "" if kind == "http": target = normalize_url(target, "die Adresse") if not target: raise ValidationError("Adresse fehlt.") else: m = HOST_PORT.fullmatch(target) if not m or not 0 < int(m["port"]) < 65536: raise ValidationError("Bitte Host und Port angeben, z. B. 192.168.0.10:22") return {"name": name, "type": kind, "target": target} # ---------- Verwaltung ---------- @router.get("/api/monitors") def list_monitors(user: dict = Depends(current_user)): with transaction() as conn: rows = conn.execute("SELECT * FROM monitors WHERE user_id = ? ORDER BY id", (user["id"],)).fetchall() return [_monitor(r) for r in rows] @router.post("/api/monitors", status_code=201) def create_monitor(data: dict = Body(...), user: dict = Depends(current_user)): values = _validate(data) with transaction() as conn: count = conn.execute("SELECT COUNT(*) FROM monitors WHERE user_id = ?", (user["id"],)).fetchone()[0] if count >= MAX_MONITORS: raise ValidationError(f"Maximal {MAX_MONITORS} Server pro Konto.") cur = conn.execute( "INSERT INTO monitors (user_id, name, type, target, created_at) VALUES (?, ?, ?, ?, ?)", (user["id"], values["name"], values["type"], values["target"], now_iso()), ) return _monitor(conn.execute("SELECT * FROM monitors WHERE id = ?", (cur.lastrowid,)).fetchone()) @router.put("/api/monitors/{monitor_id}") def update_monitor(monitor_id: int, data: dict = Body(...), user: dict = Depends(current_user)): values = _validate(data) with transaction() as conn: cur = conn.execute( "UPDATE monitors SET name = ?, type = ?, target = ? WHERE id = ? AND user_id = ?", (values["name"], values["type"], values["target"], monitor_id, user["id"]), ) if not cur.rowcount: raise HTTPException(404, "Nicht gefunden.") return _monitor(conn.execute("SELECT * FROM monitors WHERE id = ?", (monitor_id,)).fetchone()) @router.delete("/api/monitors/{monitor_id}") def delete_monitor(monitor_id: int, user: dict = Depends(current_user)): with transaction() as conn: cur = conn.execute("DELETE FROM monitors WHERE id = ? AND user_id = ?", (monitor_id, user["id"])) if not cur.rowcount: raise HTTPException(404, "Nicht gefunden.") return {"ok": True} # ---------- Prüfung ---------- async def _check_http(client: httpx.AsyncClient, url: str) -> dict: start = time.perf_counter() try: res = await client.get(url) except httpx.TimeoutException: return {"ok": False, "detail": "Zeitüberschreitung"} except httpx.ConnectError: return {"ok": False, "detail": "Nicht erreichbar"} except httpx.HTTPError as e: return {"ok": False, "detail": type(e).__name__} ms = round((time.perf_counter() - start) * 1000) # 4xx heißt: Server antwortet (z. B. Anmeldung nötig) – also erreichbar return {"ok": res.status_code < 500, "ms": ms, "detail": f"HTTP {res.status_code}"} async def _check_tcp(target: str) -> dict: m = HOST_PORT.fullmatch(target) host = m["host"].strip("[]") start = time.perf_counter() try: _, writer = await asyncio.wait_for(asyncio.open_connection(host, int(m["port"])), TIMEOUT) except asyncio.TimeoutError: return {"ok": False, "detail": "Zeitüberschreitung"} except OSError as e: return {"ok": False, "detail": "Verbindung abgelehnt" if isinstance(e, ConnectionRefusedError) else "Nicht erreichbar"} ms = round((time.perf_counter() - start) * 1000) writer.close() return {"ok": True, "ms": ms, "detail": f"Port {m['port']} offen"} @router.get("/api/monitors/status") async def monitor_status(user: dict = Depends(current_user)): with transaction() as conn: rows = conn.execute("SELECT id, type, target FROM monitors WHERE user_id = ?", (user["id"],)).fetchall() # Zertifikate werden nicht geprüft: im Heimnetz sind selbstsignierte Zertifikate üblich async with httpx.AsyncClient(timeout=TIMEOUT, verify=False, follow_redirects=True) as client: results = await asyncio.gather(*( _check_http(client, r["target"]) if r["type"] == "http" else _check_tcp(r["target"]) for r in rows )) checked = now_iso() return [{"id": r["id"], "checkedAt": checked, **res} for r, res in zip(rows, results)]