refactor(api): Use middleware to control management route availability

Previously, management API routes were conditionally registered at server startup based on the presence of the `remote-management-key`. This static approach meant a server restart was required to enable or disable these endpoints.

This commit refactors the route handling by:
1.  Introducing an `atomic.Bool` flag, `managementRoutesEnabled`, to track the state.
2.  Always registering the management routes at startup.
3.  Adding a new `managementAvailabilityMiddleware` to the management route group.

This middleware checks the `managementRoutesEnabled` flag for each request, rejecting it if management is disabled. This change provides the same initial behavior but creates a more flexible architecture that will allow for dynamically enabling or disabling management routes at runtime in the future.
This commit is contained in:
hkfires
2025-10-04 13:08:08 +08:00
parent 9e2d76f3ce
commit fd795caf76
2 changed files with 93 additions and 62 deletions

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,9 @@ type Server struct {
// management handler // management handler
mgmt *managementHandlers.Handler mgmt *managementHandlers.Handler
// managementRoutesEnabled controls whether management endpoints serve real handlers.
managementRoutesEnabled atomic.Bool
localPassword string localPassword string
keepAliveEnabled bool keepAliveEnabled bool
@@ -203,6 +207,7 @@ func NewServer(cfg *config.Config, authManager *auth.Manager, accessManager *sdk
s.mgmt.SetLocalPassword(optionState.localPassword) s.mgmt.SetLocalPassword(optionState.localPassword)
} }
s.localPassword = optionState.localPassword s.localPassword = optionState.localPassword
s.managementRoutesEnabled.Store(cfg.RemoteManagement.SecretKey != "")
// Setup routes // Setup routes
s.setupRoutes() s.setupRoutes()
@@ -309,84 +314,91 @@ func (s *Server) setupRoutes() {
}) })
// Management API routes (delegated to management handlers) // Management API routes (delegated to management handlers)
// New logic: if remote-management-key is empty, do not expose any management endpoint (404). mgmt := s.engine.Group("/v0/management")
if s.cfg.RemoteManagement.SecretKey != "" { mgmt.Use(s.managementAvailabilityMiddleware(), s.mgmt.Middleware())
mgmt := s.engine.Group("/v0/management") {
mgmt.Use(s.mgmt.Middleware()) mgmt.GET("/usage", s.mgmt.GetUsageStatistics)
{ mgmt.GET("/config", s.mgmt.GetConfig)
mgmt.GET("/usage", s.mgmt.GetUsageStatistics)
mgmt.GET("/config", s.mgmt.GetConfig)
mgmt.GET("/debug", s.mgmt.GetDebug) mgmt.GET("/debug", s.mgmt.GetDebug)
mgmt.PUT("/debug", s.mgmt.PutDebug) mgmt.PUT("/debug", s.mgmt.PutDebug)
mgmt.PATCH("/debug", s.mgmt.PutDebug) mgmt.PATCH("/debug", s.mgmt.PutDebug)
mgmt.GET("/logging-to-file", s.mgmt.GetLoggingToFile) mgmt.GET("/logging-to-file", s.mgmt.GetLoggingToFile)
mgmt.PUT("/logging-to-file", s.mgmt.PutLoggingToFile) mgmt.PUT("/logging-to-file", s.mgmt.PutLoggingToFile)
mgmt.PATCH("/logging-to-file", s.mgmt.PutLoggingToFile) mgmt.PATCH("/logging-to-file", s.mgmt.PutLoggingToFile)
mgmt.GET("/usage-statistics-enabled", s.mgmt.GetUsageStatisticsEnabled) mgmt.GET("/usage-statistics-enabled", s.mgmt.GetUsageStatisticsEnabled)
mgmt.PUT("/usage-statistics-enabled", s.mgmt.PutUsageStatisticsEnabled) mgmt.PUT("/usage-statistics-enabled", s.mgmt.PutUsageStatisticsEnabled)
mgmt.PATCH("/usage-statistics-enabled", s.mgmt.PutUsageStatisticsEnabled) mgmt.PATCH("/usage-statistics-enabled", s.mgmt.PutUsageStatisticsEnabled)
mgmt.GET("/proxy-url", s.mgmt.GetProxyURL) mgmt.GET("/proxy-url", s.mgmt.GetProxyURL)
mgmt.PUT("/proxy-url", s.mgmt.PutProxyURL) mgmt.PUT("/proxy-url", s.mgmt.PutProxyURL)
mgmt.PATCH("/proxy-url", s.mgmt.PutProxyURL) mgmt.PATCH("/proxy-url", s.mgmt.PutProxyURL)
mgmt.DELETE("/proxy-url", s.mgmt.DeleteProxyURL) mgmt.DELETE("/proxy-url", s.mgmt.DeleteProxyURL)
mgmt.GET("/quota-exceeded/switch-project", s.mgmt.GetSwitchProject) mgmt.GET("/quota-exceeded/switch-project", s.mgmt.GetSwitchProject)
mgmt.PUT("/quota-exceeded/switch-project", s.mgmt.PutSwitchProject) mgmt.PUT("/quota-exceeded/switch-project", s.mgmt.PutSwitchProject)
mgmt.PATCH("/quota-exceeded/switch-project", s.mgmt.PutSwitchProject) mgmt.PATCH("/quota-exceeded/switch-project", s.mgmt.PutSwitchProject)
mgmt.GET("/quota-exceeded/switch-preview-model", s.mgmt.GetSwitchPreviewModel) mgmt.GET("/quota-exceeded/switch-preview-model", s.mgmt.GetSwitchPreviewModel)
mgmt.PUT("/quota-exceeded/switch-preview-model", s.mgmt.PutSwitchPreviewModel) mgmt.PUT("/quota-exceeded/switch-preview-model", s.mgmt.PutSwitchPreviewModel)
mgmt.PATCH("/quota-exceeded/switch-preview-model", s.mgmt.PutSwitchPreviewModel) mgmt.PATCH("/quota-exceeded/switch-preview-model", s.mgmt.PutSwitchPreviewModel)
mgmt.GET("/api-keys", s.mgmt.GetAPIKeys) mgmt.GET("/api-keys", s.mgmt.GetAPIKeys)
mgmt.PUT("/api-keys", s.mgmt.PutAPIKeys) mgmt.PUT("/api-keys", s.mgmt.PutAPIKeys)
mgmt.PATCH("/api-keys", s.mgmt.PatchAPIKeys) mgmt.PATCH("/api-keys", s.mgmt.PatchAPIKeys)
mgmt.DELETE("/api-keys", s.mgmt.DeleteAPIKeys) mgmt.DELETE("/api-keys", s.mgmt.DeleteAPIKeys)
mgmt.GET("/generative-language-api-key", s.mgmt.GetGlKeys) mgmt.GET("/generative-language-api-key", s.mgmt.GetGlKeys)
mgmt.PUT("/generative-language-api-key", s.mgmt.PutGlKeys) mgmt.PUT("/generative-language-api-key", s.mgmt.PutGlKeys)
mgmt.PATCH("/generative-language-api-key", s.mgmt.PatchGlKeys) mgmt.PATCH("/generative-language-api-key", s.mgmt.PatchGlKeys)
mgmt.DELETE("/generative-language-api-key", s.mgmt.DeleteGlKeys) mgmt.DELETE("/generative-language-api-key", s.mgmt.DeleteGlKeys)
mgmt.GET("/request-log", s.mgmt.GetRequestLog) mgmt.GET("/request-log", s.mgmt.GetRequestLog)
mgmt.PUT("/request-log", s.mgmt.PutRequestLog) mgmt.PUT("/request-log", s.mgmt.PutRequestLog)
mgmt.PATCH("/request-log", s.mgmt.PutRequestLog) mgmt.PATCH("/request-log", s.mgmt.PutRequestLog)
mgmt.GET("/request-retry", s.mgmt.GetRequestRetry) mgmt.GET("/request-retry", s.mgmt.GetRequestRetry)
mgmt.PUT("/request-retry", s.mgmt.PutRequestRetry) mgmt.PUT("/request-retry", s.mgmt.PutRequestRetry)
mgmt.PATCH("/request-retry", s.mgmt.PutRequestRetry) mgmt.PATCH("/request-retry", s.mgmt.PutRequestRetry)
mgmt.GET("/claude-api-key", s.mgmt.GetClaudeKeys) mgmt.GET("/claude-api-key", s.mgmt.GetClaudeKeys)
mgmt.PUT("/claude-api-key", s.mgmt.PutClaudeKeys) mgmt.PUT("/claude-api-key", s.mgmt.PutClaudeKeys)
mgmt.PATCH("/claude-api-key", s.mgmt.PatchClaudeKey) mgmt.PATCH("/claude-api-key", s.mgmt.PatchClaudeKey)
mgmt.DELETE("/claude-api-key", s.mgmt.DeleteClaudeKey) mgmt.DELETE("/claude-api-key", s.mgmt.DeleteClaudeKey)
mgmt.GET("/codex-api-key", s.mgmt.GetCodexKeys) mgmt.GET("/codex-api-key", s.mgmt.GetCodexKeys)
mgmt.PUT("/codex-api-key", s.mgmt.PutCodexKeys) mgmt.PUT("/codex-api-key", s.mgmt.PutCodexKeys)
mgmt.PATCH("/codex-api-key", s.mgmt.PatchCodexKey) mgmt.PATCH("/codex-api-key", s.mgmt.PatchCodexKey)
mgmt.DELETE("/codex-api-key", s.mgmt.DeleteCodexKey) mgmt.DELETE("/codex-api-key", s.mgmt.DeleteCodexKey)
mgmt.GET("/openai-compatibility", s.mgmt.GetOpenAICompat) mgmt.GET("/openai-compatibility", s.mgmt.GetOpenAICompat)
mgmt.PUT("/openai-compatibility", s.mgmt.PutOpenAICompat) mgmt.PUT("/openai-compatibility", s.mgmt.PutOpenAICompat)
mgmt.PATCH("/openai-compatibility", s.mgmt.PatchOpenAICompat) mgmt.PATCH("/openai-compatibility", s.mgmt.PatchOpenAICompat)
mgmt.DELETE("/openai-compatibility", s.mgmt.DeleteOpenAICompat) mgmt.DELETE("/openai-compatibility", s.mgmt.DeleteOpenAICompat)
mgmt.GET("/auth-files", s.mgmt.ListAuthFiles) mgmt.GET("/auth-files", s.mgmt.ListAuthFiles)
mgmt.GET("/auth-files/download", s.mgmt.DownloadAuthFile) mgmt.GET("/auth-files/download", s.mgmt.DownloadAuthFile)
mgmt.POST("/auth-files", s.mgmt.UploadAuthFile) mgmt.POST("/auth-files", s.mgmt.UploadAuthFile)
mgmt.DELETE("/auth-files", s.mgmt.DeleteAuthFile) mgmt.DELETE("/auth-files", s.mgmt.DeleteAuthFile)
mgmt.GET("/anthropic-auth-url", s.mgmt.RequestAnthropicToken) mgmt.GET("/anthropic-auth-url", s.mgmt.RequestAnthropicToken)
mgmt.GET("/codex-auth-url", s.mgmt.RequestCodexToken) mgmt.GET("/codex-auth-url", s.mgmt.RequestCodexToken)
mgmt.GET("/gemini-cli-auth-url", s.mgmt.RequestGeminiCLIToken) mgmt.GET("/gemini-cli-auth-url", s.mgmt.RequestGeminiCLIToken)
mgmt.POST("/gemini-web-token", s.mgmt.CreateGeminiWebToken) mgmt.POST("/gemini-web-token", s.mgmt.CreateGeminiWebToken)
mgmt.GET("/qwen-auth-url", s.mgmt.RequestQwenToken) mgmt.GET("/qwen-auth-url", s.mgmt.RequestQwenToken)
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()
} }
} }
@@ -641,6 +653,22 @@ func (s *Server) UpdateClients(cfg *config.Config) {
} }
} }
prevSecretEmpty := true
if oldCfg != nil {
prevSecretEmpty = oldCfg.RemoteManagement.SecretKey == ""
}
newSecretEmpty := cfg.RemoteManagement.SecretKey == ""
switch {
case prevSecretEmpty && !newSecretEmpty:
if s.managementRoutesEnabled.CompareAndSwap(false, true) {
log.Info("management routes enabled after secret key update")
}
case !prevSecretEmpty && newSecretEmpty:
if s.managementRoutesEnabled.CompareAndSwap(true, false) {
log.Info("management routes disabled after secret key removal")
}
}
s.applyAccessConfig(oldCfg, cfg) s.applyAccessConfig(oldCfg, cfg)
s.cfg = cfg s.cfg = cfg
s.handlers.UpdateClients(&cfg.SDKConfig) s.handlers.UpdateClients(&cfg.SDKConfig)

View File

@@ -532,6 +532,9 @@ 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 {
log.Debug(" remote-management.secret-key: updated (value hidden)")
}
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)
} }