""" FastAPI-Einstiegspunkt. Bindet Security-Header, Router und Static/Template- Auslieferung zusammen (siehe Konzept 6.6 Web-Anwendungs-Hardening). """ from __future__ import annotations import logging from contextlib import asynccontextmanager from fastapi import FastAPI, Request from fastapi.responses import HTMLResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from app.admin.routes import router as admin_router from app.auth.deps import get_current_user from app.auth.routes import router as auth_router from app.catalog.routes import router as catalog_router from app.db import close_db, init_db from app.rdp_proxy.ws_tunnel import router as rdp_ws_router from app.ssh_proxy.sftp import router as sftp_router from app.ssh_proxy.terminal_ws import router as ssh_ws_router logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s") @asynccontextmanager async def lifespan(app: FastAPI): await init_db() yield await close_db() app = FastAPI(title="Jumphost Gateway", lifespan=lifespan, docs_url=None, redoc_url=None, openapi_url=None) app.include_router(auth_router) app.include_router(admin_router) app.include_router(catalog_router) app.include_router(ssh_ws_router) app.include_router(sftp_router) app.include_router(rdp_ws_router) app.mount("/static", StaticFiles(directory="static"), name="static") templates = Jinja2Templates(directory="templates") @app.middleware("http") async def security_headers_middleware(request: Request, call_next): """Setzt die in Konzept 6.6 geforderten Security-Header auf jede Antwort. Laeuft unabhaengig davon, ob nginx vorgeschaltet ist (Defense-in-Depth -- nginx setzt in der Praxis dieselben Header zusaetzlich, siehe ansible/roles/nginx_proxy).""" response = await call_next(request) response.headers["X-Frame-Options"] = "DENY" response.headers["X-Content-Type-Options"] = "nosniff" response.headers["Referrer-Policy"] = "no-referrer" response.headers["Permissions-Policy"] = "clipboard-read=(self), clipboard-write=(self), fullscreen=(self)" response.headers["Content-Security-Policy"] = ( "default-src 'self'; " "script-src 'self'; " "style-src 'self'; " "img-src 'self' data:; " "connect-src 'self' ws: wss:; " "frame-ancestors 'none'; " "base-uri 'self'; " "form-action 'self'" ) response.headers["Strict-Transport-Security"] = "max-age=63072000; includeSubDomains" return response @app.get("/", response_class=HTMLResponse) async def index(request: Request): # Hinweis: seit Starlette >=1.x ist "TemplateResponse(request, name, ...)" # die aktuelle Aufrufkonvention (die alte "TemplateResponse(name, {...})" # wurde entfernt) -- beim Dependency-Upgrade im Rahmen der pip-audit- # Bereinigung angepasst und per Pentest-Testsuite regressionsgetestet. return templates.TemplateResponse(request, "login.html", {}) @app.get("/dashboard", response_class=HTMLResponse) async def dashboard(request: Request): # Auth-Pruefung erfolgt clientseitig ueber GET /auth/me (401 -> Redirect zu /); # serverseitig zusaetzlich abgesichert, sobald Templates dynamische Inhalte rendern. return templates.TemplateResponse(request, "dashboard.html", {}) @app.get("/terminal/{host_id}", response_class=HTMLResponse) async def terminal_page(request: Request, host_id: int): return templates.TemplateResponse(request, "terminal.html", {"host_id": host_id}) @app.get("/rdp/{host_id}", response_class=HTMLResponse) async def rdp_page(request: Request, host_id: int): return templates.TemplateResponse(request, "rdp.html", {"host_id": host_id}) @app.get("/healthz") async def healthz(): return {"status": "ok"}