package server import ( "fmt" "net/http" "strings" "time" "git.cloonar.com/cloonar/updns/internal/config" pvd "git.cloonar.com/cloonar/updns/internal/provider" "git.cloonar.com/cloonar/updns/internal/provider/hetzner" "github.com/gin-gonic/gin" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promhttp" "go.uber.org/zap" ) var ( totalUpdates = promauto.NewCounter(prometheus.CounterOpts{ Name: "updns_total_updates", Help: "Total number of update requests", }) successUpdates = promauto.NewCounter(prometheus.CounterOpts{ Name: "updns_success_updates", Help: "Number of successful updates", }) failedUpdates = promauto.NewCounter(prometheus.CounterOpts{ Name: "updns_failed_updates", Help: "Number of failed updates", }) exactAuth = promauto.NewCounter(prometheus.CounterOpts{ Name: "updns_exact_auth", Help: "Number of updates authorized via exact match", }) wildcardAuth = promauto.NewCounter(prometheus.CounterOpts{ Name: "updns_wildcard_auth", Help: "Number of updates authorized via wildcard", }) ) type updateRequest struct { Key string `json:"key" binding:"required"` Secret string `json:"secret" binding:"required"` Host string `json:"host" binding:"required"` IP string `json:"ip"` } // NewRouter constructs the HTTP handler with routes, middleware, logging and metrics. func NewRouter(cfg *config.Config, logger *zap.Logger, prov pvd.Provider) *gin.Engine { r := gin.New() r.Use(gin.Recovery()) r.Use(func(c *gin.Context) { start := time.Now() c.Next() logger.Info("request", zap.String("method", c.Request.Method), zap.String("path", c.Request.URL.Path), zap.Int("status", c.Writer.Status()), zap.Duration("duration", time.Since(start)), ) }) r.GET("/metrics", gin.WrapH(promhttp.Handler())) r.POST("/update", func(c *gin.Context) { totalUpdates.Inc() var req updateRequest if err := c.ShouldBindJSON(&req); err != nil { failedUpdates.Inc() logger.Error("invalid request", zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{"status": "error", "message": err.Error()}) return } ip := req.IP if ip == "" { ip = c.ClientIP() } clientCfg, ok := cfg.Clients[req.Key] if !ok || req.Secret != clientCfg.Secret { failedUpdates.Inc() c.JSON(http.StatusUnauthorized, gin.H{"status": "error", "message": "invalid key or secret"}) return } authorized := false for _, h := range clientCfg.Exact { if req.Host == h { authorized = true exactAuth.Inc() break } } if !authorized { for _, base := range clientCfg.Wildcard { if req.Host == base || strings.HasSuffix(req.Host, "."+base) { authorized = true wildcardAuth.Inc() break } } } if !authorized { failedUpdates.Inc() c.JSON(http.StatusForbidden, gin.H{"status": "error", "message": "host not authorized"}) return } // Provider is now initialized at startup and passed in if err := prov.UpdateRecord(c.Request.Context(), req.Host, ip); err != nil { failedUpdates.Inc() logger.Error("update record failed", zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "message": "update failed"}) return } successUpdates.Inc() c.JSON(http.StatusOK, gin.H{"status": "ok", "message": "Record updated"}) }) return r } // StartServer initializes Provider and starts the HTTP server. func StartServer(cfg *config.Config) error { logger, _ := zap.NewProduction() // Initialize logger once // Initialize provider at startup prov, err := selectProvider(cfg) if err != nil { logger.Error("failed to initialize provider", zap.Error(err)) return fmt.Errorf("provider initialization failed: %w", err) } logger.Info("provider initialized successfully", zap.String("provider", cfg.Upstream.Provider)) router := NewRouter(cfg, logger, prov) // Pass logger and provider if cfg.Server.TLS.Enabled { logger.Info("starting TLS server", zap.String("address", cfg.Server.BindAddress)) return router.RunTLS(cfg.Server.BindAddress, cfg.Server.TLS.CertFile, cfg.Server.TLS.KeyFile) } logger.Info("starting HTTP server", zap.String("address", cfg.Server.BindAddress)) return router.Run(cfg.Server.BindAddress) } // selectProvider returns the configured Provider or an error if initialization fails. func selectProvider(cfg *config.Config) (pvd.Provider, error) { // configToProviderName logic is effectively duplicated here, safe to remove the separate function if only used here. switch cfg.Upstream.Provider { case "hetzner": prov, err := hetzner.NewProvider(cfg.Upstream.Hetzner) if err != nil { return nil, fmt.Errorf("initializing hetzner provider: %w", err) } return prov, nil default: // This case should technically be unreachable due to the check above return nil, fmt.Errorf("internal error: unsupported provider %s passed initial check", cfg.Upstream.Provider) } }