Files
hetzner-api-dyndns-records-…/cmd/dyndns-web/main.go
T
admin f02ec9977c
Container-Image veröffentlichen / container (push) Has been cancelled
feat: add resilient updates and access controls
2026-08-28 14:02:07 +02:00

145 lines
4.1 KiB
Go

package main
import (
"context"
"errors"
"log/slog"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/appupdate"
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/config"
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/hetzner"
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/ipresolve"
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/scheduler"
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/secure"
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/store"
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/web"
)
func main() {
if len(os.Args) > 1 && os.Args[1] == "healthcheck" {
os.Exit(healthcheck())
}
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
if err := run(logger); err != nil {
logger.Error("application stopped", "error", err)
os.Exit(1)
}
}
func run(logger *slog.Logger) error {
// Credentials and session data must never inherit a permissive process umask.
syscall.Umask(0o077)
cfg, err := config.Load()
if err != nil {
return err
}
cipher, err := secure.LoadOrCreateCipher(cfg.MasterKeyFile)
if err != nil {
return err
}
ctx := context.Background()
database, err := store.Open(ctx, cfg.DatabasePath)
if err != nil {
return err
}
defer database.Close()
if err := database.Prune(ctx); err != nil {
logger.Warn("prune old local data", "error", err)
}
provider := hetzner.New(cfg.HCloudBaseURL)
updater := scheduler.New(database, provider, ipresolve.New(), cipher, logger)
softwareUpdates, err := appupdate.New(database, cipher, cfg.UpdateAPIURL, cfg.UpdateAgentSock, cfg.UpdateInterval, logger)
if err != nil {
return err
}
settings, err := database.GetSettings(ctx)
if err != nil {
return err
}
if !settings.IPv6Enabled {
deleted, cleanupErr := updater.DisableIPv6(ctx)
if cleanupErr != nil {
logger.Warn("enforce disabled IPv6", "error", cleanupErr)
} else if deleted > 0 {
logger.Info("removed AAAA records for disabled IPv6", "count", deleted)
}
}
serverUI, err := web.New(cfg, database, cipher, updater, softwareUpdates, logger)
if err != nil {
return err
}
appCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
updater.Start(appCtx)
softwareUpdates.Start(appCtx)
go pruneStore(appCtx, database, logger)
httpServer := &http.Server{
Addr: cfg.Address,
Handler: serverUI.Handler(),
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 16 << 10,
}
serverErrors := make(chan error, 1)
go func() {
logger.Info("starting server", "address", cfg.Address, "mode", cfg.Mode)
serverErrors <- httpServer.ListenAndServe()
}()
select {
case err := <-serverErrors:
if !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
case <-appCtx.Done():
logger.Info("shutdown requested")
shutdownCtx, cancel := context.WithTimeout(context.Background(), cfg.ShutdownTimeout)
defer cancel()
return httpServer.Shutdown(shutdownCtx)
}
}
func pruneStore(ctx context.Context, database *store.Store, logger *slog.Logger) {
ticker := time.NewTicker(24 * time.Hour)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
pruneCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
err := database.Prune(pruneCtx)
cancel()
if err != nil && !errors.Is(err, context.Canceled) {
logger.Warn("prune old local data", "error", err)
}
}
}
}
func healthcheck() int {
url := strings.TrimSpace(os.Getenv("APP_HEALTHCHECK_URL"))
if url == "" {
url = "http://127.0.0.1:8080/healthz"
}
client := &http.Client{Timeout: 3 * time.Second}
response, err := client.Get(url)
if err != nil {
return 1
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return 1
}
return 0
}