2FA (TOTP): - backend/api/auth.go: zweistufiger Login-Flow mit Pending-Token (5min), Endpunkte /2fa/validate, /2fa/setup, /2fa/confirm, /2fa/disable - backend/db/users.go: GetUserByID, SetTOTPSecret, EnableTOTP, DisableTOTP - backend/db/postgres.go: Migration totp_secret + totp_enabled Spalten - backend/models/user.go: TOTPEnabled/TOTPSecret Felder, neue Request-Typen - backend/main.go: neue Routen registriert (/2fa/validate ohne Auth, rest geschuetzt) - backend/go.mod+sum: github.com/pquerna/otp hinzugefuegt - frontend/src/pages/Login.jsx: zweistufiger Login (Passwort -> TOTP) - frontend/src/pages/SettingsPage.jsx: TwoFactorSection mit Setup/Deaktivieren - frontend/src/stores/auth.js: validateTOTP Action - frontend/src/api/client.js: setupTOTP, confirmTOTP, disableTOTP, validateTOTP Weitere Aenderungen (unstaged -> jetzt committed): - frontend/src/components/ScriptModal.jsx: neu - frontend/src/components/AgentPanel.jsx: Erweiterungen - frontend/src/components/ProxmoxPanel.jsx: Erweiterungen - agent-linux/ws/handler.go: Erweiterungen - backend/api/scheduler.go + tasks.go: Erweiterungen - backend/db/tasks.go + models/task.go: Erweiterungen
185 lines
5.2 KiB
Go
185 lines
5.2 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"log"
|
|
"crypto/tls"
|
|
"math/big"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/cynfo/rmm-backend/api"
|
|
"github.com/cynfo/rmm-backend/config"
|
|
"github.com/cynfo/rmm-backend/db"
|
|
"github.com/cynfo/rmm-backend/monitor"
|
|
"github.com/cynfo/rmm-backend/ws"
|
|
)
|
|
|
|
func main() {
|
|
cfgPath := "config.yaml"
|
|
if len(os.Args) > 1 {
|
|
cfgPath = os.Args[1]
|
|
}
|
|
|
|
cfg, err := config.Load(cfgPath)
|
|
if err != nil {
|
|
log.Fatalf("Konfiguration laden fehlgeschlagen: %v", err)
|
|
}
|
|
|
|
// TLS-Zertifikate pruefen/generieren
|
|
if err := ensureTLS(cfg.TLSCert, cfg.TLSKey); err != nil {
|
|
log.Fatalf("TLS-Setup fehlgeschlagen: %v", err)
|
|
}
|
|
|
|
// Datenbank initialisieren
|
|
database, err := db.New(cfg.Database)
|
|
if err != nil {
|
|
log.Fatalf("Datenbank-Fehler: %v", err)
|
|
}
|
|
defer database.Close()
|
|
|
|
// WebSocket Hub starten
|
|
hub := ws.NewHub()
|
|
hub.SetEventCallback(func(agentID, eventType, message string) {
|
|
database.InsertAgentEvent(agentID, eventType, message)
|
|
})
|
|
go hub.Run()
|
|
|
|
// Inaktivitaets-Monitor starten
|
|
mon := monitor.New(database)
|
|
go mon.Run()
|
|
|
|
// Task-Scheduler starten
|
|
scheduler := api.NewTaskScheduler(database, hub)
|
|
go scheduler.Run()
|
|
|
|
// Config API-Keys in DB migrieren (einmalig)
|
|
database.MigrateConfigAPIKeys(cfg.APIKeys)
|
|
|
|
// API-Key Cache (laedt Keys aus DB, refresht alle 30s)
|
|
apiKeyCache := api.NewAPIKeyCache(database)
|
|
|
|
// Auth-Handler initialisieren
|
|
authHandler := api.NewAuthHandler(database, cfg.JWTSecret)
|
|
authHandler.EnsureDefaultAdmin()
|
|
|
|
// Router aufbauen - Auth-Routes ohne Auth-Middleware
|
|
authMux := http.NewServeMux()
|
|
authHandler.SetupAuthRoutes(authMux)
|
|
|
|
// Geschuetzte Routes
|
|
protectedMux := http.NewServeMux()
|
|
handler := api.NewHandler(database)
|
|
handler.SetScheduler(scheduler)
|
|
handler.SetupRoutes(protectedMux, hub)
|
|
// GET /api/v1/auth/me braucht JWT
|
|
protectedMux.HandleFunc("GET /api/v1/auth/me", authHandler.Me)
|
|
|
|
// Geschuetzte 2FA-Routen
|
|
protectedMux.HandleFunc("POST /api/v1/auth/2fa/setup", authHandler.TOTPSetup)
|
|
protectedMux.HandleFunc("POST /api/v1/auth/2fa/confirm", authHandler.TOTPConfirm)
|
|
protectedMux.HandleFunc("POST /api/v1/auth/2fa/disable", authHandler.TOTPDisable)
|
|
|
|
// Combined router: auth routes ungeschuetzt, rest mit DB-basierter Auth
|
|
mux := http.NewServeMux()
|
|
mux.Handle("POST /api/v1/auth/login", authMux)
|
|
mux.Handle("POST /api/v1/auth/2fa/validate", authMux) // 2FA-Schritt 2 ebenfalls ohne JWT
|
|
mux.Handle("/", api.CombinedAuthWithCache(apiKeyCache, authHandler, protectedMux))
|
|
|
|
// Middleware-Chain: CORS -> Logging -> Handler
|
|
var chain http.Handler = mux
|
|
chain = api.Logging(chain)
|
|
chain = api.CORS(chain)
|
|
|
|
log.Printf("RMM Backend startet auf %s (TLS)", cfg.ListenAddr)
|
|
log.Printf("API-Keys (Config): %d, DB-basierte Validierung aktiv", len(cfg.APIKeys))
|
|
|
|
// HTTP/2 deaktivieren: WebSocket-Upgrades benoetigen HTTP/1.1
|
|
srv := &http.Server{
|
|
Addr: cfg.ListenAddr,
|
|
Handler: chain,
|
|
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
|
|
}
|
|
if err := srv.ListenAndServeTLS(cfg.TLSCert, cfg.TLSKey); err != nil {
|
|
log.Fatalf("Server-Fehler: %v", err)
|
|
}
|
|
}
|
|
|
|
// ensureTLS generiert self-signed Zertifikate wenn keine vorhanden
|
|
func ensureTLS(certPath, keyPath string) error {
|
|
if _, err := os.Stat(certPath); err == nil {
|
|
if _, err := os.Stat(keyPath); err == nil {
|
|
log.Println("TLS-Zertifikate gefunden")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
log.Println("Generiere self-signed TLS-Zertifikat...")
|
|
|
|
// Verzeichnis erstellen
|
|
if dir := filepath.Dir(certPath); dir != "" && dir != "." {
|
|
os.MkdirAll(dir, 0755)
|
|
}
|
|
if dir := filepath.Dir(keyPath); dir != "" && dir != "." {
|
|
os.MkdirAll(dir, 0755)
|
|
}
|
|
|
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
return fmt.Errorf("Key generieren: %w", err)
|
|
}
|
|
|
|
serial, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
|
|
|
template := x509.Certificate{
|
|
SerialNumber: serial,
|
|
Subject: pkix.Name{
|
|
Organization: []string{"RMM Backend"},
|
|
CommonName: "rmm-backend",
|
|
},
|
|
NotBefore: time.Now(),
|
|
NotAfter: time.Now().Add(365 * 24 * time.Hour * 10), // 10 Jahre
|
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
BasicConstraintsValid: true,
|
|
IPAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("192.168.85.13")},
|
|
DNSNames: []string{"localhost", "rmm-backend"},
|
|
}
|
|
|
|
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
|
|
if err != nil {
|
|
return fmt.Errorf("Zertifikat erstellen: %w", err)
|
|
}
|
|
|
|
certFile, err := os.Create(certPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer certFile.Close()
|
|
pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
|
|
|
keyBytes, err := x509.MarshalECPrivateKey(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
keyFile, err := os.OpenFile(keyPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer keyFile.Close()
|
|
pem.Encode(keyFile, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes})
|
|
|
|
log.Printf("TLS-Zertifikat generiert: %s, %s", certPath, keyPath)
|
|
return nil
|
|
}
|