157 lines
4.9 KiB
Go
157 lines
4.9 KiB
Go
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)
|
|
}
|
|
}
|