Merge pull request #82 from router-for-me/mgmt

feat: Implement hot-reloading for management endpoints
This commit is contained in:
Luis Pater
2025-10-04 16:32:22 +08:00
committed by GitHub
2 changed files with 140 additions and 64 deletions
+58 -4
View File
@@ -13,6 +13,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"sync/atomic"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -126,6 +127,11 @@ type Server struct {
// management handler // management handler
mgmt *managementHandlers.Handler mgmt *managementHandlers.Handler
// managementRoutesRegistered tracks whether the management routes have been attached to the engine.
managementRoutesRegistered atomic.Bool
// managementRoutesEnabled controls whether management endpoints serve real handlers.
managementRoutesEnabled atomic.Bool
localPassword string localPassword string
keepAliveEnabled bool keepAliveEnabled bool
@@ -210,6 +216,12 @@ func NewServer(cfg *config.Config, authManager *auth.Manager, accessManager *sdk
optionState.routerConfigurator(engine, s.handlers, cfg) optionState.routerConfigurator(engine, s.handlers, cfg)
} }
// Register management routes only when a secret is present at startup.
s.managementRoutesEnabled.Store(cfg.RemoteManagement.SecretKey != "")
if cfg.RemoteManagement.SecretKey != "" {
s.registerManagementRoutes()
}
if optionState.keepAliveEnabled { if optionState.keepAliveEnabled {
s.enableKeepAlive(optionState.keepAliveTimeout, optionState.keepAliveOnTimeout) s.enableKeepAlive(optionState.keepAliveTimeout, optionState.keepAliveOnTimeout)
} }
@@ -308,11 +320,21 @@ func (s *Server) setupRoutes() {
c.String(http.StatusOK, "<html><body><h1>Authentication successful!</h1><p>You can close this window.</p></body></html>") c.String(http.StatusOK, "<html><body><h1>Authentication successful!</h1><p>You can close this window.</p></body></html>")
}) })
// Management API routes (delegated to management handlers) // Management routes are registered lazily by registerManagementRoutes when a secret is configured.
// New logic: if remote-management-key is empty, do not expose any management endpoint (404). }
if s.cfg.RemoteManagement.SecretKey != "" {
func (s *Server) registerManagementRoutes() {
if s == nil || s.engine == nil || s.mgmt == nil {
return
}
if !s.managementRoutesRegistered.CompareAndSwap(false, true) {
return
}
log.Info("management routes registered after secret key configuration")
mgmt := s.engine.Group("/v0/management") mgmt := s.engine.Group("/v0/management")
mgmt.Use(s.mgmt.Middleware()) mgmt.Use(s.managementAvailabilityMiddleware(), s.mgmt.Middleware())
{ {
mgmt.GET("/usage", s.mgmt.GetUsageStatistics) mgmt.GET("/usage", s.mgmt.GetUsageStatistics)
mgmt.GET("/config", s.mgmt.GetConfig) mgmt.GET("/config", s.mgmt.GetConfig)
@@ -388,6 +410,15 @@ func (s *Server) setupRoutes() {
mgmt.GET("/get-auth-status", s.mgmt.GetAuthStatus) mgmt.GET("/get-auth-status", s.mgmt.GetAuthStatus)
} }
} }
func (s *Server) managementAvailabilityMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if !s.managementRoutesEnabled.Load() {
c.AbortWithStatus(http.StatusNotFound)
return
}
c.Next()
}
} }
func (s *Server) serveManagementControlPanel(c *gin.Context) { func (s *Server) serveManagementControlPanel(c *gin.Context) {
@@ -641,6 +672,29 @@ func (s *Server) UpdateClients(cfg *config.Config) {
} }
} }
prevSecretEmpty := true
if oldCfg != nil {
prevSecretEmpty = oldCfg.RemoteManagement.SecretKey == ""
}
newSecretEmpty := cfg.RemoteManagement.SecretKey == ""
switch {
case prevSecretEmpty && !newSecretEmpty:
s.registerManagementRoutes()
if s.managementRoutesEnabled.CompareAndSwap(false, true) {
log.Info("management routes enabled after secret key update")
} else {
s.managementRoutesEnabled.Store(true)
}
case !prevSecretEmpty && newSecretEmpty:
if s.managementRoutesEnabled.CompareAndSwap(true, false) {
log.Info("management routes disabled after secret key removal")
} else {
s.managementRoutesEnabled.Store(false)
}
default:
s.managementRoutesEnabled.Store(!newSecretEmpty)
}
s.applyAccessConfig(oldCfg, cfg) s.applyAccessConfig(oldCfg, cfg)
s.cfg = cfg s.cfg = cfg
s.handlers.UpdateClients(&cfg.SDKConfig) s.handlers.UpdateClients(&cfg.SDKConfig)
+23 -1
View File
@@ -430,8 +430,15 @@ func (w *Watcher) handleEvent(event fsnotify.Event) {
} }
fmt.Printf("config file changed, reloading: %s\n", w.configPath) fmt.Printf("config file changed, reloading: %s\n", w.configPath)
if w.reloadConfig() { if w.reloadConfig() {
finalHash := newHash
if updatedData, errRead := os.ReadFile(w.configPath); errRead == nil && len(updatedData) > 0 {
sumUpdated := sha256.Sum256(updatedData)
finalHash = hex.EncodeToString(sumUpdated[:])
} else if errRead != nil {
log.WithError(errRead).Debug("failed to compute updated config hash after reload")
}
w.clientsMutex.Lock() w.clientsMutex.Lock()
w.lastConfigHash = newHash w.lastConfigHash = finalHash
w.clientsMutex.Unlock() w.clientsMutex.Unlock()
} }
return return
@@ -532,6 +539,21 @@ func (w *Watcher) reloadConfig() bool {
if oldConfig.RemoteManagement.AllowRemote != newConfig.RemoteManagement.AllowRemote { if oldConfig.RemoteManagement.AllowRemote != newConfig.RemoteManagement.AllowRemote {
log.Debugf(" remote-management.allow-remote: %t -> %t", oldConfig.RemoteManagement.AllowRemote, newConfig.RemoteManagement.AllowRemote) log.Debugf(" remote-management.allow-remote: %t -> %t", oldConfig.RemoteManagement.AllowRemote, newConfig.RemoteManagement.AllowRemote)
} }
if oldConfig.RemoteManagement.SecretKey != newConfig.RemoteManagement.SecretKey {
switch {
case oldConfig.RemoteManagement.SecretKey == "" && newConfig.RemoteManagement.SecretKey != "":
log.Debug(" remote-management.secret-key: created")
case oldConfig.RemoteManagement.SecretKey != "" && newConfig.RemoteManagement.SecretKey == "":
log.Debug(" remote-management.secret-key: deleted")
default:
log.Debug(" remote-management.secret-key: updated")
}
if newConfig.RemoteManagement.SecretKey == "" {
log.Info("management routes will be disabled after secret key removal")
} else {
log.Info("management routes will be enabled after secret key update")
}
}
if oldConfig.RemoteManagement.DisableControlPanel != newConfig.RemoteManagement.DisableControlPanel { if oldConfig.RemoteManagement.DisableControlPanel != newConfig.RemoteManagement.DisableControlPanel {
log.Debugf(" remote-management.disable-control-panel: %t -> %t", oldConfig.RemoteManagement.DisableControlPanel, newConfig.RemoteManagement.DisableControlPanel) log.Debugf(" remote-management.disable-control-panel: %t -> %t", oldConfig.RemoteManagement.DisableControlPanel, newConfig.RemoteManagement.DisableControlPanel)
} }