145 lines
4.1 KiB
Go
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
|
|
}
|