syncova-backup/apps/api/internal/httpapi/server.go
Jerrit Fritzsche 610719c316
Some checks failed
CI / Backend (Go) (push) Failing after 3m7s
CI / Frontend (React/TypeScript) (push) Successful in 37s
CI / Sicherheitsprüfungen (push) Successful in 44s
Syncova Backups V1
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>
2026-08-17 09:10:54 +02:00

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
}
}