package api import ( "errors" "io/fs" "net/http" "path" "regexp" "strings" "github.com/gin-gonic/gin" "github.com/router-for-me/CLIProxyAPI/v7/internal/managementasset" log "github.com/sirupsen/logrus" ) var hashedManagementAssetPattern = regexp.MustCompile(`(^|[-.])[A-Za-z0-9_-]{8,}(\.[^.]+)+$`) 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.Use(s.managementAvailabilityMiddleware(), s.mgmt.Middleware()) { mgmt.GET("/auth-files", s.mgmt.ListAuthFiles) mgmt.DELETE("/auth-files", s.mgmt.DeleteAuthFile) mgmt.POST("/codex-quota", s.mgmt.CodexQuota) mgmt.GET("/codex-auth-url", s.mgmt.RequestCodexToken) mgmt.GET("/get-auth-status", s.mgmt.GetAuthStatus) mgmt.DELETE("/oauth-session", s.mgmt.CancelAuthSession) mgmt.POST("/oauth-callback", s.mgmt.PostOAuthCallback) } } 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.managementRoutesEnabled.Load() { c.AbortWithStatus(http.StatusNotFound) return false } return true } func (s *Server) RefreshPluginManagementRoutes() {} func (s *Server) pluginManagementNoRoute(c *gin.Context) { c.AbortWithStatus(http.StatusNotFound) } func (s *Server) serveManagementControlPanel(c *gin.Context) { cfg := s.cfg if cfg == nil || cfg.RemoteManagement.DisableControlPanel { c.AbortWithStatus(http.StatusNotFound) return } source, err := managementasset.Current() if err != nil { if !errors.Is(err, fs.ErrNotExist) { log.WithError(err).Error("failed to resolve management control panel assets") } c.AbortWithStatus(http.StatusNotFound) return } s.serveManagementFile(c, source, source.Entry, false) } func (s *Server) serveManagementAsset(c *gin.Context) { cfg := s.cfg if cfg == nil || cfg.RemoteManagement.DisableControlPanel { c.AbortWithStatus(http.StatusNotFound) return } assetPath := strings.TrimPrefix(c.Param("filepath"), "/") if assetPath == "" || strings.Contains(assetPath, `\`) || !fs.ValidPath(assetPath) { c.AbortWithStatus(http.StatusNotFound) return } source, err := managementasset.Current() if err != nil { if !errors.Is(err, fs.ErrNotExist) { log.WithError(err).Error("failed to resolve management control panel assets") } c.AbortWithStatus(http.StatusNotFound) return } s.serveManagementFile(c, source, assetPath, true) } func (s *Server) serveManagementFile(c *gin.Context, source *managementasset.Source, name string, cacheHashed bool) { file, err := source.FileSystem.Open(name) if err != nil { c.AbortWithStatus(http.StatusNotFound) return } defer func() { if errClose := file.Close(); errClose != nil { log.WithError(errClose).Debug("failed to close management asset") } }() info, err := file.Stat() if err != nil || info.IsDir() { c.AbortWithStatus(http.StatusNotFound) return } if !cacheHashed { c.Header("Content-Type", "text/html; charset=utf-8") } else if hashedManagementAssetPattern.MatchString(path.Base(name)) { c.Header("Cache-Control", "public, max-age=31536000, immutable") } http.ServeContent(c.Writer, c.Request, info.Name(), info.ModTime(), file) }