Enterprise-Backup-, Recovery-, Verification-, Security- und Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme. Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes geht in keine Bewertung als "gut" ein. Umfang (Phasen 0-23): - Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll, Katalogaufbau allein aus den Manifesten — ohne Datenbank - Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck - Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell) - Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive - Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit - Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center, Ransomware-Heuristik (meldet, handelt nie) - Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing - Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository - Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64 Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs. Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md und docs/release-candidate.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
143 lines
4.8 KiB
Go
143 lines
4.8 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"net"
|
|
"net/http"
|
|
|
|
"github.com/syncova/syncova/packages/platform/config"
|
|
"github.com/syncova/syncova/packages/platform/logging"
|
|
)
|
|
|
|
// Server kapselt den HTTP-Server des API-Dienstes samt geordnetem Herunterfahren.
|
|
type Server struct {
|
|
// httpServer ist der zugrunde liegende Standard-Server.
|
|
httpServer *http.Server
|
|
// listener ist der gebundene Netzwerk-Listener.
|
|
listener net.Listener
|
|
// logger protokolliert Start und Stopp des Servers.
|
|
logger *slog.Logger
|
|
// shutdownTimeout ist die Frist für laufende Requests beim Herunterfahren.
|
|
shutdownTimeout config.HTTPConfig
|
|
}
|
|
|
|
// NewServer bindet die konfigurierte Adresse und bereitet den Server vor.
|
|
//
|
|
// Das Binden erfolgt bereits hier, damit ein belegter Port sofort als Fehler
|
|
// sichtbar wird und nicht erst im Hintergrund auftaucht.
|
|
func NewServer(httpConfig config.HTTPConfig, requestHandler http.Handler, baseLogger *slog.Logger) (*Server, error) {
|
|
networkListener, listenError := net.Listen("tcp", httpConfig.ListenAddress)
|
|
if listenError != nil {
|
|
return nil, fmt.Errorf("HTTP-Adresse %q konnte nicht gebunden werden: %w", httpConfig.ListenAddress, listenError)
|
|
}
|
|
|
|
httpServer := &http.Server{
|
|
Handler: requestHandler,
|
|
ReadHeaderTimeout: httpConfig.ReadHeaderTimeout,
|
|
ReadTimeout: httpConfig.ReadTimeout,
|
|
WriteTimeout: httpConfig.WriteTimeout,
|
|
IdleTimeout: httpConfig.IdleTimeout,
|
|
// Fehler des Servers laufen über das strukturierte Logging statt über
|
|
// die Standardausgabe, damit sie auswertbar bleiben.
|
|
ErrorLog: slog.NewLogLogger(logging.WithComponent(baseLogger, "http").Handler(), slog.LevelError),
|
|
}
|
|
|
|
// TLS wird eingerichtet, sobald Zertifikat und Schlüssel vorliegen.
|
|
//
|
|
// Die Mindestversion ist 1.2, und Renegotiation bleibt aus: Beides sind
|
|
// die Vorgaben von Go, hier ausdrücklich festgeschrieben, damit eine
|
|
// künftige Lockerung der Vorgaben diese Anlage nicht mitzieht.
|
|
if httpConfig.TLSCertificateFile != "" {
|
|
serverCertificate, certificateError := tls.LoadX509KeyPair(
|
|
httpConfig.TLSCertificateFile, httpConfig.TLSPrivateKeyFile)
|
|
if certificateError != nil {
|
|
_ = networkListener.Close()
|
|
|
|
return nil, fmt.Errorf("das TLS-Zertifikat konnte nicht geladen werden: %w", certificateError)
|
|
}
|
|
|
|
httpServer.TLSConfig = &tls.Config{
|
|
Certificates: []tls.Certificate{serverCertificate},
|
|
MinVersion: tls.VersionTLS12,
|
|
}
|
|
}
|
|
|
|
return &Server{
|
|
httpServer: httpServer,
|
|
listener: networkListener,
|
|
logger: logging.WithComponent(baseLogger, "http"),
|
|
shutdownTimeout: httpConfig,
|
|
}, nil
|
|
}
|
|
|
|
// serveConnections bedient Verbindungen, verschlüsselt oder im Klartext.
|
|
func (server *Server) serveConnections() error {
|
|
if server.httpServer.TLSConfig != nil {
|
|
return server.httpServer.ServeTLS(server.listener, "", "")
|
|
}
|
|
|
|
return server.httpServer.Serve(server.listener)
|
|
}
|
|
|
|
// UsesTLS meldet einen verschlüsselten Dienst.
|
|
func (server *Server) UsesTLS() bool {
|
|
return server.httpServer.TLSConfig != nil
|
|
}
|
|
|
|
// Address liefert die tatsächlich gebundene Adresse.
|
|
//
|
|
// Bei Port 0 weist das Betriebssystem einen freien Port zu; Tests brauchen
|
|
// deshalb die effektive Adresse statt der konfigurierten.
|
|
func (server *Server) Address() string {
|
|
return server.listener.Addr().String()
|
|
}
|
|
|
|
// Serve bedient Requests, bis der Context abgebrochen wird.
|
|
//
|
|
// Danach läuft ein geordnetes Herunterfahren: laufende Requests dürfen innerhalb
|
|
// der konfigurierten Frist zu Ende laufen.
|
|
func (server *Server) Serve(runContext context.Context) error {
|
|
// serveErrorChannel ist gepuffert, damit die Goroutine auch dann endet,
|
|
// wenn zuerst der Context abgebrochen wird.
|
|
serveErrorChannel := make(chan error, 1)
|
|
|
|
go func() {
|
|
server.logger.Info("http server gestartet", slog.String("address", server.Address()))
|
|
|
|
// ServeTLS verschlüsselt, sobald eine TLS-Konfiguration vorliegt. Die
|
|
// Dateinamen sind leer, weil das Zertifikat bereits geladen ist.
|
|
serveError := server.serveConnections()
|
|
// ErrServerClosed ist die erwartete Folge eines geordneten Shutdowns.
|
|
if errors.Is(serveError, http.ErrServerClosed) {
|
|
serveErrorChannel <- nil
|
|
return
|
|
}
|
|
|
|
serveErrorChannel <- serveError
|
|
}()
|
|
|
|
select {
|
|
case serveError := <-serveErrorChannel:
|
|
return serveError
|
|
|
|
case <-runContext.Done():
|
|
server.logger.Info("http server wird beendet")
|
|
|
|
// Der Shutdown-Context hängt bewusst nicht am abgebrochenen runContext,
|
|
// sonst bliebe für laufende Requests keine Zeit mehr.
|
|
shutdownContext, cancelShutdownContext := context.WithTimeout(context.Background(), server.shutdownTimeout.ShutdownTimeout)
|
|
defer cancelShutdownContext()
|
|
|
|
if shutdownError := server.httpServer.Shutdown(shutdownContext); shutdownError != nil {
|
|
return fmt.Errorf("http server konnte nicht geordnet beendet werden: %w", shutdownError)
|
|
}
|
|
|
|
server.logger.Info("http server beendet")
|
|
return nil
|
|
}
|
|
}
|