"""Zugangsdaten je Aufruf aus HTTP-Headern. Der Dienst speichert keine Zugangsdaten. Der Client (z. B. Bifröst) schickt sie mit jedem Aufruf als Header; das Modell sieht sie nie, weil sie nicht in den Tool-Argumenten stehen. Erlaubt sind nur freigegebene Hosts (Schutz vor SSRF). X-DB-Host, X-DB-Port, X-DB-Database, X-DB-User, X-DB-Password, X-DB-SSLMode """ import re from collections.abc import Mapping from dataclasses import dataclass, field from dbmcp.core import Invalid from dbmcp.core.config import Settings HOST = re.compile(r"^[a-z0-9.-]{1,253}$") IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_$.-]{0,127}$") LOCAL = {"localhost", "127.0.0.1", "::1"} SSL_MODES = ("require", "verify-full", "prefer", "disable") @dataclass(frozen=True) class Connection: host: str port: int database: str user: str password: str = field(repr=False) sslmode: str @property def local(self) -> bool: return self.host in LOCAL def describe(self) -> dict[str, str | int]: """Für das Audit-Log: alles außer dem Passwort.""" return {"host": self.host, "port": self.port, "database": self.database, "user": self.user} def allowed(host: str, settings: Settings) -> bool: if host in LOCAL: return settings.allow_local return any(host == entry or (entry.startswith(".") and host.endswith(entry)) for entry in settings.allowed_hosts) def from_headers(headers: Mapping[str, str] | None, settings: Settings, default_port: int) -> Connection: headers = {k.lower(): v for k, v in (headers or {}).items()} def value(name: str, required: bool = True) -> str: found = headers.get(f"x-db-{name}", "").strip() if required and not found: raise Invalid(f"Header X-DB-{name.title()} fehlt.") return found host = value("host").lower() if not HOST.match(host) and host != "::1": raise Invalid("Ungültiger Host.") if not allowed(host, settings): raise Invalid(f"Host {host} ist nicht freigegeben (DBMCP_ALLOWED_HOSTS).") try: port = int(value("port", required=False) or default_port) except ValueError: raise Invalid("Der Port muss eine Zahl sein.") from None if not 1 <= port <= 65535: raise Invalid("Ungültiger Port.") database, user = value("database"), value("user") if not IDENTIFIER.match(database): raise Invalid("Ungültiger Datenbankname.") if not IDENTIFIER.match(user): raise Invalid("Ungültiger Benutzername.") sslmode = value("sslmode", required=False) or "verify-full" if sslmode not in SSL_MODES: raise Invalid("Unbekannter SSL-Modus.") if sslmode in ("prefer", "disable") and host not in LOCAL: raise Invalid("Ohne erzwungenes TLS nur für lokale Datenbanken.") return Connection(host, port, database, user, value("password", required=False), sslmode)