126 lines
7.3 KiB
Markdown
126 lines
7.3 KiB
Markdown
|
|
# 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]]
|