343 lines · 12.6 KB · Go
RawDownload
package api import (	"context"	"net/http"	"os"	"strings" 	"github.com/gin-gonic/gin"	"github.com/router-for-me/CLIProxyAPI/v7/internal/managementasset"	log "github.com/sirupsen/logrus") 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") 	s.engine.POST("/v0/management/oauth-callback", s.managementAvailabilityMiddleware(), s.mgmt.PostOAuthCallback)	s.engine.GET("/v0/management/oauth-callback", s.managementAvailabilityMiddleware(), s.mgmt.GetOAuthCallback) 	mgmt := s.engine.Group("/v0/management")	mgmt.Use(s.managementAvailabilityMiddleware(), s.mgmt.Middleware())	{		mgmt.GET("/config", s.mgmt.GetConfig)		mgmt.GET("/config.yaml", s.mgmt.GetConfigYAML)		mgmt.PUT("/config.yaml", s.mgmt.PutConfigYAML)		mgmt.GET("/latest-version", s.mgmt.GetLatestVersion)		mgmt.GET("/plugins", s.mgmt.ListPlugins)		mgmt.GET("/plugin-store", s.mgmt.ListPluginStore)		mgmt.POST("/plugin-store/:id/install", s.mgmt.InstallPluginFromStore)		mgmt.DELETE("/plugins/:id", s.mgmt.DeletePlugin)		mgmt.PATCH("/plugins/:id/enabled", s.mgmt.PatchPluginEnabled)		mgmt.GET("/plugins/:id/config", s.mgmt.GetPluginConfig)		mgmt.PUT("/plugins/:id/config", s.mgmt.PutPluginConfig)		mgmt.PATCH("/plugins/:id/config", s.mgmt.PatchPluginConfig)		mgmt.GET("/plugins/:id/quota", s.mgmt.GetPluginQuota)		mgmt.POST("/plugins/:id/quota", s.mgmt.FetchPluginQuota)		mgmt.DELETE("/plugins/:id/quota", s.mgmt.ResetPluginQuota)		mgmt.POST("/plugins/:id/quota/reset", s.mgmt.ResetPluginQuota) 		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("/logs-max-total-size-mb", s.mgmt.GetLogsMaxTotalSizeMB)		mgmt.PUT("/logs-max-total-size-mb", s.mgmt.PutLogsMaxTotalSizeMB)		mgmt.PATCH("/logs-max-total-size-mb", s.mgmt.PutLogsMaxTotalSizeMB) 		mgmt.GET("/error-logs-max-files", s.mgmt.GetErrorLogsMaxFiles)		mgmt.PUT("/error-logs-max-files", s.mgmt.PutErrorLogsMaxFiles)		mgmt.PATCH("/error-logs-max-files", s.mgmt.PutErrorLogsMaxFiles) 		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.POST("/api-call", s.mgmt.APICall) 		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.POST("/reset-quota", s.mgmt.ResetQuota) 		mgmt.GET("/quota/providers", s.mgmt.GetQuotaProviders)		mgmt.POST("/quota/fetch", s.mgmt.FetchCredentialQuota)		mgmt.POST("/quota/reset", s.mgmt.ResetCredentialQuota) 		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-key-usage", s.mgmt.GetAPIKeyUsage)		mgmt.GET("/usage-queue", s.mgmt.GetUsageQueue) 		mgmt.GET("/gemini-api-key", s.mgmt.GetGeminiKeys)		mgmt.PUT("/gemini-api-key", s.mgmt.PutGeminiKeys)		mgmt.PATCH("/gemini-api-key", s.mgmt.PatchGeminiKey)		mgmt.DELETE("/gemini-api-key", s.mgmt.DeleteGeminiKey) 		mgmt.GET("/interactions-api-key", s.mgmt.GetInteractionsKeys)		mgmt.PUT("/interactions-api-key", s.mgmt.PutInteractionsKeys)		mgmt.PATCH("/interactions-api-key", s.mgmt.PatchInteractionsKey)		mgmt.DELETE("/interactions-api-key", s.mgmt.DeleteInteractionsKey) 		mgmt.GET("/logs", s.mgmt.GetLogs)		mgmt.DELETE("/logs", s.mgmt.DeleteLogs)		mgmt.GET("/request-error-logs", s.mgmt.GetRequestErrorLogs)		mgmt.GET("/request-error-logs/:name", s.mgmt.DownloadRequestErrorLog)		mgmt.GET("/request-log-by-id/:id", s.mgmt.GetRequestLogByID)		mgmt.GET("/request-log", s.mgmt.GetRequestLog)		mgmt.PUT("/request-log", s.mgmt.PutRequestLog)		mgmt.PATCH("/request-log", s.mgmt.PutRequestLog)		mgmt.GET("/ws-auth", s.mgmt.GetWebsocketAuth)		mgmt.PUT("/ws-auth", s.mgmt.PutWebsocketAuth)		mgmt.PATCH("/ws-auth", s.mgmt.PutWebsocketAuth) 		mgmt.GET("/request-retry", s.mgmt.GetRequestRetry)		mgmt.PUT("/request-retry", s.mgmt.PutRequestRetry)		mgmt.PATCH("/request-retry", s.mgmt.PutRequestRetry)		mgmt.GET("/max-retry-credentials", s.mgmt.GetMaxRetryCredentials)		mgmt.PUT("/max-retry-credentials", s.mgmt.PutMaxRetryCredentials)		mgmt.PATCH("/max-retry-credentials", s.mgmt.PutMaxRetryCredentials)		mgmt.GET("/max-retry-interval", s.mgmt.GetMaxRetryInterval)		mgmt.PUT("/max-retry-interval", s.mgmt.PutMaxRetryInterval)		mgmt.PATCH("/max-retry-interval", s.mgmt.PutMaxRetryInterval) 		mgmt.GET("/force-model-prefix", s.mgmt.GetForceModelPrefix)		mgmt.PUT("/force-model-prefix", s.mgmt.PutForceModelPrefix)		mgmt.PATCH("/force-model-prefix", s.mgmt.PutForceModelPrefix) 		mgmt.GET("/routing/strategy", s.mgmt.GetRoutingStrategy)		mgmt.PUT("/routing/strategy", s.mgmt.PutRoutingStrategy)		mgmt.PATCH("/routing/strategy", s.mgmt.PutRoutingStrategy) 		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("/xai-api-key", s.mgmt.GetXAIKeys)		mgmt.PUT("/xai-api-key", s.mgmt.PutXAIKeys)		mgmt.PATCH("/xai-api-key", s.mgmt.PatchXAIKey)		mgmt.DELETE("/xai-api-key", s.mgmt.DeleteXAIKey) 		mgmt.GET("/meta-api-key", s.mgmt.GetMetaKeys)		mgmt.PUT("/meta-api-key", s.mgmt.PutMetaKeys)		mgmt.PATCH("/meta-api-key", s.mgmt.PatchMetaKey)		mgmt.DELETE("/meta-api-key", s.mgmt.DeleteMetaKey) 		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("/vertex-api-key", s.mgmt.GetVertexCompatKeys)		mgmt.PUT("/vertex-api-key", s.mgmt.PutVertexCompatKeys)		mgmt.PATCH("/vertex-api-key", s.mgmt.PatchVertexCompatKey)		mgmt.DELETE("/vertex-api-key", s.mgmt.DeleteVertexCompatKey) 		mgmt.GET("/oauth-excluded-models", s.mgmt.GetOAuthExcludedModels)		mgmt.PUT("/oauth-excluded-models", s.mgmt.PutOAuthExcludedModels)		mgmt.PATCH("/oauth-excluded-models", s.mgmt.PatchOAuthExcludedModels)		mgmt.DELETE("/oauth-excluded-models", s.mgmt.DeleteOAuthExcludedModels) 		mgmt.GET("/oauth-model-alias", s.mgmt.GetOAuthModelAlias)		mgmt.PUT("/oauth-model-alias", s.mgmt.PutOAuthModelAlias)		mgmt.PATCH("/oauth-model-alias", s.mgmt.PatchOAuthModelAlias)		mgmt.DELETE("/oauth-model-alias", s.mgmt.DeleteOAuthModelAlias) 		mgmt.GET("/oauth-request-scoped-errors", s.mgmt.GetOAuthRequestScopedErrors)		mgmt.PUT("/oauth-request-scoped-errors", s.mgmt.PutOAuthRequestScopedErrors)		mgmt.PATCH("/oauth-request-scoped-errors", s.mgmt.PatchOAuthRequestScopedErrors)		mgmt.DELETE("/oauth-request-scoped-errors", s.mgmt.DeleteOAuthRequestScopedErrors) 		mgmt.GET("/auth-files", s.mgmt.ListAuthFiles)		mgmt.GET("/auth-files/models", s.mgmt.GetAuthFileModels)		mgmt.GET("/model-definitions/:channel", s.mgmt.GetStaticModelDefinitions)		mgmt.GET("/auth-files/download", s.mgmt.DownloadAuthFile)		mgmt.POST("/auth-files", s.mgmt.UploadAuthFile)		mgmt.DELETE("/auth-files", s.mgmt.DeleteAuthFile)		mgmt.PATCH("/auth-files/status", s.mgmt.PatchAuthFileStatus)		mgmt.PATCH("/auth-files/fields", s.mgmt.PatchAuthFileFields)		mgmt.POST("/auth-files/refresh", s.mgmt.RefreshAuthFiles)		mgmt.POST("/vertex/import", s.mgmt.ImportVertexCredential) 		mgmt.GET("/anthropic-auth-url", s.mgmt.RequestAnthropicToken)		mgmt.GET("/codex-auth-url", s.mgmt.RequestCodexToken)		mgmt.GET("/antigravity-auth-url", s.mgmt.RequestAntigravityToken)		mgmt.GET("/kimi-auth-url", s.mgmt.RequestKimiToken)		mgmt.GET("/xai-auth-url", s.mgmt.RequestXAIToken)		mgmt.GET("/devin-auth-url", s.mgmt.RequestDevinToken)		mgmt.GET("/meta-auth-url", s.mgmt.RequestMetaToken)		mgmt.GET("/get-auth-status", s.mgmt.GetAuthStatus)		mgmt.DELETE("/oauth-session", s.mgmt.CancelAuthSession)	}} func (s *Server) managementAvailabilityMiddleware() gin.HandlerFunc {	return func(c *gin.Context) {		if !s.managementAvailable(c) {			return		}		c.Next()	}} func (s *Server) managementAvailable(c *gin.Context) bool {	if s == nil || s.cfg == nil {		c.AbortWithStatus(http.StatusNotFound)		return false	}	if s.cfg.Home.Enabled {		c.AbortWithStatus(http.StatusNotFound)		return false	}	if !s.managementRoutesEnabled.Load() {		c.AbortWithStatus(http.StatusNotFound)		return false	}	return true} func (s *Server) refreshPluginManagementRoutes() {	if s == nil || s.pluginHost == nil || s.engine == nil {		return	}	s.pluginHost.RegisterManagementRoutes(context.Background(), s.registeredManagementRouteKeys())} // RefreshPluginManagementRoutes rebuilds plugin-owned Management API routes.func (s *Server) RefreshPluginManagementRoutes() {	s.refreshPluginManagementRoutes()} func (s *Server) registeredManagementRouteKeys() map[string]struct{} {	out := make(map[string]struct{})	if s == nil || s.engine == nil {		return out	}	for _, route := range s.engine.Routes() {		if strings.HasPrefix(route.Path, "/v0/management/") || route.Path == "/v0/management" {			out[strings.ToUpper(strings.TrimSpace(route.Method))+" "+route.Path] = struct{}{}		}	}	return out} func (s *Server) pluginManagementNoRoute(c *gin.Context) {	if s == nil || c == nil || c.Request == nil || c.Request.URL == nil {		if c != nil {			c.AbortWithStatus(http.StatusNotFound)		}		return	}	path := c.Request.URL.Path	if strings.HasPrefix(path, "/v0/resource/plugins/") {		s.pluginResourceNoRoute(c)		return	}	if path != "/v0/management" && !strings.HasPrefix(path, "/v0/management/") {		c.AbortWithStatus(http.StatusNotFound)		return	}	if s.pluginHost == nil || s.mgmt == nil {		c.AbortWithStatus(http.StatusNotFound)		return	}	if !s.managementAvailable(c) {		return	}	s.mgmt.Middleware()(c)	if c.IsAborted() {		return	}	if s.mgmt.ServePluginAuthURL(c) {		c.Abort()		return	}	if s.pluginHost.ServeManagementHTTP(c.Writer, c.Request) {		c.Abort()		return	}	c.AbortWithStatus(http.StatusNotFound)} func (s *Server) pluginResourceNoRoute(c *gin.Context) {	if s == nil || c == nil || c.Request == nil || c.Request.URL == nil {		if c != nil {			c.AbortWithStatus(http.StatusNotFound)		}		return	}	if s.cfg == nil || s.cfg.Home.Enabled || s.pluginHost == nil {		c.AbortWithStatus(http.StatusNotFound)		return	}	if s.pluginHost.ServeResourceHTTP(c.Writer, c.Request) {		c.Abort()		return	}	c.AbortWithStatus(http.StatusNotFound)} func (s *Server) serveManagementControlPanel(c *gin.Context) {	cfg := s.cfg	if cfg == nil || cfg.Home.Enabled || cfg.RemoteManagement.DisableControlPanel {		c.AbortWithStatus(http.StatusNotFound)		return	}	if c.Query("panel") != "upstream" {		c.Header("Cache-Control", "no-store")		c.Header("X-Content-Type-Options", "nosniff")		c.Header("Referrer-Policy", "no-referrer")		c.Data(http.StatusOK, "text/html; charset=utf-8", proxyDashboard)		return	}	filePath := managementasset.FilePath(s.configFilePath)	if strings.TrimSpace(filePath) == "" {		c.AbortWithStatus(http.StatusNotFound)		return	} 	if _, err := os.Stat(filePath); err != nil {		if os.IsNotExist(err) {			// Synchronously ensure management.html is available with a detached context.			// Control panel bootstrap should not be canceled by client disconnects.			if !managementasset.EnsureLatestManagementHTML(context.Background(), managementasset.StaticDir(s.configFilePath), cfg.ProxyURL, cfg.RemoteManagement.PanelGitHubRepository) {				c.AbortWithStatus(http.StatusNotFound)				return			}		} else {			log.WithError(err).Error("failed to stat management control panel asset")			c.AbortWithStatus(http.StatusInternalServerError)			return		}	} 	c.File(filePath)}