cx-nexredirect/wiki/Architecture.md

7.3 KiB

Architecture

Komponenten

┌──────────────────────────────────────────────────────────────┐
│  Internet                                                     │
└──────────────────┬───────────────────────────────────────────┘
                   │ Port 80 / 443
                   ▼
┌──────────────────────────────────────────────────────────────┐
│  Caddy (auto-HTTPS via Let's Encrypt)                         │
│                                                                │
│  /etc/caddy/Caddyfile (auto-generated)                        │
│  ┌────────────────────────────────────────────────────┐       │
│  │ admin.example.de, server-ip → reverse_proxy :3000 │       │
│  │ alt-firma.de, www.alt-firma.de → reverse_proxy    │       │
│  │   handle_errors { redir https://target.de/ 302 }  │       │
│  │ ...                                                │       │
│  └────────────────────────────────────────────────────┘       │
└──────────────────┬───────────────────────────────────────────┘
                   │ reverse_proxy localhost:3000
                   ▼
┌──────────────────────────────────────────────────────────────┐
│  server.ts (Custom Node Server, läuft via tsx)                │
│                                                                │
│  ① Host-Check                                                  │
│     ├─ Admin-Host (server-IP / base_domain) → Next.js         │
│     ├─ Aktive Redirect-Domain → Hit loggen + 302              │
│     │                          (Sunset → HTML-Notice)         │
│     └─ Unbekannter Host → 404 "Domain not configured"         │
│                                                                │
│  ② Bei Admin-Host:                                             │
│     Next.js handleRequest() — App Router                      │
│     ├─ middleware.ts (Edge): Auth-Check via JWT-Cookie        │
│     ├─ /(app)/* — eingeloggte Routes                          │
│     ├─ /(auth)/login — NextAuth Credentials                   │
│     ├─ /(setup)/setup — First-Run-Wizard                      │
│     ├─ /api/v1/* — Public API (Bearer Token)                  │
│     ├─ /api/* — Admin-API (Session-Cookie)                    │
│     └─ /r/[token] — Internal Report-Page für PDF-Export       │
└──────────────────┬───────────────────────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────────────────────┐
│  better-sqlite3                                                │
│  /var/lib/corex-nexredirect/nexredirect.db                    │
│  Tables: users, domains, domain_groups, hits, settings,       │
│          api_tokens, update_log, audit_log                    │
└──────────────────────────────────────────────────────────────┘

Request-Flow für einen Domain-Hit

  1. GET https://alt-firma.de/foo → DNS löst auf Server-IP
  2. Caddy nimmt an, ACME hat schon Cert für alt-firma.de
  3. Caddy reverse_proxy localhost:3000
  4. server.ts req.headers.host = "alt-firma.de"
  5. isAdminHost("alt-firma.de") → false (kein Match auf server_ip / base_domain / localhost)
  6. resolveHost("alt-firma.de") → Resolved-Eintrag aus In-Memory-Cache (5s TTL)
  7. Bot-Filter (shouldRecord) prüft Method, UA, Pfad, Browser-Signals, IP-Scan-Detector
  8. Wenn echter Hit: recordHit() → Buffer (5s batch) → SQLite INSERT
  9. Sunset aktiv? → HTML-Notice + Cache-Control: no-store
  10. Sonst: 302 zum Target mit no-store Headers

Request-Flow für Admin

  1. GET https://admin.example.de/dashboard → Caddy → server.ts → isAdminHost = true → Next.js
  2. middleware.ts checkt JWT-Session-Cookie
  3. (app)/layout.tsx checkt isSetupComplete() + getServerSession() (force-dynamic)
  4. Render Sidebar + Page

Datenfluss bei Domain-Add

UI POST /api/domains
  ├─ Zod-Validation
  ├─ INSERT INTO domains (status='pending')
  └─ Audit-Log

UI POST /api/domains/[id]/verify
  ├─ DNS-Lookup für domain + www-subdomain
  ├─ Vergleich mit getSetting('server_ip')
  ├─ Wenn Match: status='active', verified_at=now
  ├─ invalidateRedirectCache()
  ├─ Caddy: writeCaddyfile() + `caddy reload`
  └─ Response { ok, caddy_reloaded }

Update-Flow

UI Click "Update installieren"
  └─ POST /api/update/apply (admin-only)
       └─ exec(`sudo update.sh <tag>`)
            ├─ git checkout tag
            ├─ npm ci
            ├─ Prebuilt .next aus GitHub-Release-Asset (oder npm run build)
            ├─ ln -sf bin/nexredirect /usr/local/bin/
            └─ ( sleep 2 && systemctl restart ) & disown
                 (Detached — script exitet, API kann response zurückgeben)
  └─ UI polled /api/v1/health → window.location.reload()

In-Memory-State

  • Redirect-Cache (lib/redirect-resolver.ts): Map<host, ResolvedRedirect> mit 5s TTL. Invalidiert bei Domain/Group-Mutationen.
  • Hits-Buffer (lib/hits.ts): Array von Pending Hits, alle 5s als Transaction in DB geschrieben.
  • Geo-Reader (lib/geo.ts): MaxMind-mmdb-Reader, lazy load on first lookup.
  • IP-Scan-Tracker (lib/hits.ts): Map<ip_hash, {paths: Set, firstSeen}> mit 30s Window.

Alle in-memory — bei Restart weg, kein Problem (Cache füllt sich neu).

Sicherheit

  • NextAuth JWT mit NEXTAUTH_SECRET (zufällig generiert beim Install, 30d Session)
  • bcryptjs für Passwort-Hash (cost 12)
  • API-Tokens als sha256-Hash in DB; Klartext nur einmalig bei Anlage angezeigt
  • HMAC-signierte Tokens für PDF-Internal-Access (60s TTL)
  • DSGVO: IP wird nie im Klartext gespeichert. sha256(ip + daily_salt). Daily-Salt rotiert täglich → keine Re-Identifizierung über Tagesgrenzen.
  • CSRF: NextAuth handhabt das für Auth-Routes. Admin-Mutations brauchen Session-Cookie. API-Routes brauchen Bearer-Token.
  • Bot-Filter auch als implizite Schutzschicht gegen Scanner-Probing
  • No exposed admin paths: alle Mutations brauchen Auth. /api/v1/* Bearer.

→ Weiter mit Troubleshooting