7.3 KiB
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
GET https://alt-firma.de/foo→ DNS löst auf Server-IP- Caddy nimmt an, ACME hat schon Cert für
alt-firma.de - Caddy
reverse_proxy localhost:3000 - server.ts
req.headers.host = "alt-firma.de" isAdminHost("alt-firma.de")→ false (kein Match auf server_ip / base_domain / localhost)resolveHost("alt-firma.de")→ Resolved-Eintrag aus In-Memory-Cache (5s TTL)- Bot-Filter (
shouldRecord) prüft Method, UA, Pfad, Browser-Signals, IP-Scan-Detector - Wenn echter Hit:
recordHit()→ Buffer (5s batch) → SQLite INSERT - Sunset aktiv? → HTML-Notice +
Cache-Control: no-store - Sonst: 302 zum Target mit
no-storeHeaders
Request-Flow für Admin
GET https://admin.example.de/dashboard→ Caddy → server.ts → isAdminHost = true → Next.jsmiddleware.tscheckt JWT-Session-Cookie(app)/layout.tsxchecktisSetupComplete()+getServerSession()(force-dynamic)- 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