From fd795caf760e9317cef31898f44da3a157ce2252 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Sat, 4 Oct 2025 13:08:08 +0800 Subject: [PATCH] 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. --- internal/api/server.go | 152 +++++++++++++++++++++--------------- internal/watcher/watcher.go | 3 + 2 files changed, 93 insertions(+), 62 deletions(-) diff --git a/internal/api/server.go b/internal/api/server.go index 3892bfcf..20f3abc4 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -13,6 +13,7 @@ import ( "os" "path/filepath" "strings" + "sync/atomic" "time" "github.com/gin-gonic/gin" @@ -126,6 +127,9 @@ type Server struct { // management handler mgmt *managementHandlers.Handler + // managementRoutesEnabled controls whether management endpoints serve real handlers. + managementRoutesEnabled atomic.Bool + localPassword string keepAliveEnabled bool @@ -203,6 +207,7 @@ func NewServer(cfg *config.Config, authManager *auth.Manager, accessManager *sdk s.mgmt.SetLocalPassword(optionState.localPassword) } s.localPassword = optionState.localPassword + s.managementRoutesEnabled.Store(cfg.RemoteManagement.SecretKey != "") // Setup routes s.setupRoutes() @@ -309,84 +314,91 @@ func (s *Server) setupRoutes() { }) // Management API routes (delegated to management handlers) - // New logic: if remote-management-key is empty, do not expose any management endpoint (404). - if s.cfg.RemoteManagement.SecretKey != "" { - mgmt := s.engine.Group("/v0/management") - mgmt.Use(s.mgmt.Middleware()) - { - mgmt.GET("/usage", s.mgmt.GetUsageStatistics) - mgmt.GET("/config", s.mgmt.GetConfig) + mgmt := s.engine.Group("/v0/management") + mgmt.Use(s.managementAvailabilityMiddleware(), s.mgmt.Middleware()) + { + mgmt.GET("/usage", s.mgmt.GetUsageStatistics) + mgmt.GET("/config", s.mgmt.GetConfig) - mgmt.GET("/debug", s.mgmt.GetDebug) - mgmt.PUT("/debug", s.mgmt.PutDebug) - mgmt.PATCH("/debug", s.mgmt.PutDebug) + mgmt.GET("/debug", s.mgmt.GetDebug) + mgmt.PUT("/debug", s.mgmt.PutDebug) + mgmt.PATCH("/debug", s.mgmt.PutDebug) - mgmt.GET("/logging-to-file", s.mgmt.GetLoggingToFile) - mgmt.PUT("/logging-to-file", s.mgmt.PutLoggingToFile) - mgmt.PATCH("/logging-to-file", s.mgmt.PutLoggingToFile) + mgmt.GET("/logging-to-file", s.mgmt.GetLoggingToFile) + mgmt.PUT("/logging-to-file", s.mgmt.PutLoggingToFile) + mgmt.PATCH("/logging-to-file", s.mgmt.PutLoggingToFile) - mgmt.GET("/usage-statistics-enabled", s.mgmt.GetUsageStatisticsEnabled) - mgmt.PUT("/usage-statistics-enabled", s.mgmt.PutUsageStatisticsEnabled) - mgmt.PATCH("/usage-statistics-enabled", s.mgmt.PutUsageStatisticsEnabled) + mgmt.GET("/usage-statistics-enabled", s.mgmt.GetUsageStatisticsEnabled) + mgmt.PUT("/usage-statistics-enabled", s.mgmt.PutUsageStatisticsEnabled) + mgmt.PATCH("/usage-statistics-enabled", s.mgmt.PutUsageStatisticsEnabled) - mgmt.GET("/proxy-url", s.mgmt.GetProxyURL) - mgmt.PUT("/proxy-url", s.mgmt.PutProxyURL) - mgmt.PATCH("/proxy-url", s.mgmt.PutProxyURL) - mgmt.DELETE("/proxy-url", s.mgmt.DeleteProxyURL) + mgmt.GET("/proxy-url", s.mgmt.GetProxyURL) + mgmt.PUT("/proxy-url", s.mgmt.PutProxyURL) + mgmt.PATCH("/proxy-url", s.mgmt.PutProxyURL) + mgmt.DELETE("/proxy-url", s.mgmt.DeleteProxyURL) - mgmt.GET("/quota-exceeded/switch-project", s.mgmt.GetSwitchProject) - mgmt.PUT("/quota-exceeded/switch-project", s.mgmt.PutSwitchProject) - mgmt.PATCH("/quota-exceeded/switch-project", s.mgmt.PutSwitchProject) + mgmt.GET("/quota-exceeded/switch-project", s.mgmt.GetSwitchProject) + mgmt.PUT("/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.PUT("/quota-exceeded/switch-preview-model", s.mgmt.PutSwitchPreviewModel) - mgmt.PATCH("/quota-exceeded/switch-preview-model", s.mgmt.PutSwitchPreviewModel) + mgmt.GET("/quota-exceeded/switch-preview-model", s.mgmt.GetSwitchPreviewModel) + mgmt.PUT("/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.PUT("/api-keys", s.mgmt.PutAPIKeys) - mgmt.PATCH("/api-keys", s.mgmt.PatchAPIKeys) - mgmt.DELETE("/api-keys", s.mgmt.DeleteAPIKeys) + mgmt.GET("/api-keys", s.mgmt.GetAPIKeys) + mgmt.PUT("/api-keys", s.mgmt.PutAPIKeys) + mgmt.PATCH("/api-keys", s.mgmt.PatchAPIKeys) + mgmt.DELETE("/api-keys", s.mgmt.DeleteAPIKeys) - mgmt.GET("/generative-language-api-key", s.mgmt.GetGlKeys) - mgmt.PUT("/generative-language-api-key", s.mgmt.PutGlKeys) - mgmt.PATCH("/generative-language-api-key", s.mgmt.PatchGlKeys) - mgmt.DELETE("/generative-language-api-key", s.mgmt.DeleteGlKeys) + mgmt.GET("/generative-language-api-key", s.mgmt.GetGlKeys) + mgmt.PUT("/generative-language-api-key", s.mgmt.PutGlKeys) + mgmt.PATCH("/generative-language-api-key", s.mgmt.PatchGlKeys) + mgmt.DELETE("/generative-language-api-key", s.mgmt.DeleteGlKeys) - mgmt.GET("/request-log", s.mgmt.GetRequestLog) - mgmt.PUT("/request-log", s.mgmt.PutRequestLog) - mgmt.PATCH("/request-log", s.mgmt.PutRequestLog) + mgmt.GET("/request-log", s.mgmt.GetRequestLog) + mgmt.PUT("/request-log", s.mgmt.PutRequestLog) + mgmt.PATCH("/request-log", s.mgmt.PutRequestLog) - mgmt.GET("/request-retry", s.mgmt.GetRequestRetry) - mgmt.PUT("/request-retry", s.mgmt.PutRequestRetry) - mgmt.PATCH("/request-retry", s.mgmt.PutRequestRetry) + mgmt.GET("/request-retry", s.mgmt.GetRequestRetry) + mgmt.PUT("/request-retry", s.mgmt.PutRequestRetry) + mgmt.PATCH("/request-retry", s.mgmt.PutRequestRetry) - mgmt.GET("/claude-api-key", s.mgmt.GetClaudeKeys) - mgmt.PUT("/claude-api-key", s.mgmt.PutClaudeKeys) - mgmt.PATCH("/claude-api-key", s.mgmt.PatchClaudeKey) - mgmt.DELETE("/claude-api-key", s.mgmt.DeleteClaudeKey) + mgmt.GET("/claude-api-key", s.mgmt.GetClaudeKeys) + mgmt.PUT("/claude-api-key", s.mgmt.PutClaudeKeys) + mgmt.PATCH("/claude-api-key", s.mgmt.PatchClaudeKey) + mgmt.DELETE("/claude-api-key", s.mgmt.DeleteClaudeKey) - mgmt.GET("/codex-api-key", s.mgmt.GetCodexKeys) - mgmt.PUT("/codex-api-key", s.mgmt.PutCodexKeys) - mgmt.PATCH("/codex-api-key", s.mgmt.PatchCodexKey) - mgmt.DELETE("/codex-api-key", s.mgmt.DeleteCodexKey) + mgmt.GET("/codex-api-key", s.mgmt.GetCodexKeys) + mgmt.PUT("/codex-api-key", s.mgmt.PutCodexKeys) + mgmt.PATCH("/codex-api-key", s.mgmt.PatchCodexKey) + mgmt.DELETE("/codex-api-key", s.mgmt.DeleteCodexKey) - mgmt.GET("/openai-compatibility", s.mgmt.GetOpenAICompat) - mgmt.PUT("/openai-compatibility", s.mgmt.PutOpenAICompat) - mgmt.PATCH("/openai-compatibility", s.mgmt.PatchOpenAICompat) - mgmt.DELETE("/openai-compatibility", s.mgmt.DeleteOpenAICompat) + mgmt.GET("/openai-compatibility", s.mgmt.GetOpenAICompat) + mgmt.PUT("/openai-compatibility", s.mgmt.PutOpenAICompat) + mgmt.PATCH("/openai-compatibility", s.mgmt.PatchOpenAICompat) + mgmt.DELETE("/openai-compatibility", s.mgmt.DeleteOpenAICompat) - mgmt.GET("/auth-files", s.mgmt.ListAuthFiles) - mgmt.GET("/auth-files/download", s.mgmt.DownloadAuthFile) - mgmt.POST("/auth-files", s.mgmt.UploadAuthFile) - mgmt.DELETE("/auth-files", s.mgmt.DeleteAuthFile) + mgmt.GET("/auth-files", s.mgmt.ListAuthFiles) + mgmt.GET("/auth-files/download", s.mgmt.DownloadAuthFile) + mgmt.POST("/auth-files", s.mgmt.UploadAuthFile) + mgmt.DELETE("/auth-files", s.mgmt.DeleteAuthFile) - mgmt.GET("/anthropic-auth-url", s.mgmt.RequestAnthropicToken) - mgmt.GET("/codex-auth-url", s.mgmt.RequestCodexToken) - mgmt.GET("/gemini-cli-auth-url", s.mgmt.RequestGeminiCLIToken) - mgmt.POST("/gemini-web-token", s.mgmt.CreateGeminiWebToken) - mgmt.GET("/qwen-auth-url", s.mgmt.RequestQwenToken) - mgmt.GET("/get-auth-status", s.mgmt.GetAuthStatus) + mgmt.GET("/anthropic-auth-url", s.mgmt.RequestAnthropicToken) + mgmt.GET("/codex-auth-url", s.mgmt.RequestCodexToken) + mgmt.GET("/gemini-cli-auth-url", s.mgmt.RequestGeminiCLIToken) + mgmt.POST("/gemini-web-token", s.mgmt.CreateGeminiWebToken) + mgmt.GET("/qwen-auth-url", s.mgmt.RequestQwenToken) + 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.cfg = cfg s.handlers.UpdateClients(&cfg.SDKConfig) diff --git a/internal/watcher/watcher.go b/internal/watcher/watcher.go index ae8c06e1..6819b9fa 100644 --- a/internal/watcher/watcher.go +++ b/internal/watcher/watcher.go @@ -532,6 +532,9 @@ func (w *Watcher) reloadConfig() bool { if 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 { log.Debugf(" remote-management.disable-control-panel: %t -> %t", oldConfig.RemoteManagement.DisableControlPanel, newConfig.RemoteManagement.DisableControlPanel) }