package management import ( "io" "net/http" "os" "path/filepath" "github.com/gin-gonic/gin" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" "gopkg.in/yaml.v3" ) func (h *Handler) GetConfig(c *gin.Context) { if h == nil || h.cfg == nil { c.JSON(200, gin.H{}) return } cfgCopy := *h.cfg cfgCopy.GlAPIKey = geminiKeyStringsFromConfig(h.cfg) c.JSON(200, &cfgCopy) } func (h *Handler) GetConfigYAML(c *gin.Context) { data, err := os.ReadFile(h.configFilePath) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "read_failed", "message": err.Error()}) return } var node yaml.Node if err = yaml.Unmarshal(data, &node); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "parse_failed", "message": err.Error()}) return } c.Header("Content-Type", "application/yaml; charset=utf-8") c.Header("Vary", "format, Accept") enc := yaml.NewEncoder(c.Writer) enc.SetIndent(2) _ = enc.Encode(&node) _ = enc.Close() } func WriteConfig(path string, data []byte) error { data = config.NormalizeCommentIndentation(data) f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { return err } if _, errWrite := f.Write(data); errWrite != nil { _ = f.Close() return errWrite } if errSync := f.Sync(); errSync != nil { _ = f.Close() return errSync } return f.Close() } func (h *Handler) PutConfigYAML(c *gin.Context) { body, err := io.ReadAll(c.Request.Body) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_yaml", "message": "cannot read request body"}) return } var cfg config.Config if err = yaml.Unmarshal(body, &cfg); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_yaml", "message": err.Error()}) return } // Validate config using LoadConfigOptional with optional=false to enforce parsing tmpDir := filepath.Dir(h.configFilePath) tmpFile, err := os.CreateTemp(tmpDir, "config-validate-*.yaml") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "write_failed", "message": err.Error()}) return } tempFile := tmpFile.Name() if _, errWrite := tmpFile.Write(body); errWrite != nil { _ = tmpFile.Close() _ = os.Remove(tempFile) c.JSON(http.StatusInternalServerError, gin.H{"error": "write_failed", "message": errWrite.Error()}) return } if errClose := tmpFile.Close(); errClose != nil { _ = os.Remove(tempFile) c.JSON(http.StatusInternalServerError, gin.H{"error": "write_failed", "message": errClose.Error()}) return } defer func() { _ = os.Remove(tempFile) }() _, err = config.LoadConfigOptional(tempFile, false) if err != nil { c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "invalid_config", "message": err.Error()}) return } h.mu.Lock() defer h.mu.Unlock() if WriteConfig(h.configFilePath, body) != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "write_failed", "message": "failed to write config"}) return } // Reload into handler to keep memory in sync newCfg, err := config.LoadConfig(h.configFilePath) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "reload_failed", "message": err.Error()}) return } h.cfg = newCfg c.JSON(http.StatusOK, gin.H{"ok": true, "changed": []string{"config"}}) } // GetConfigFile returns the raw config.yaml file bytes without re-encoding. // It preserves comments and original formatting/styles. func (h *Handler) GetConfigFile(c *gin.Context) { data, err := os.ReadFile(h.configFilePath) if err != nil { if os.IsNotExist(err) { c.JSON(http.StatusNotFound, gin.H{"error": "not_found", "message": "config file not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "read_failed", "message": err.Error()}) return } c.Header("Content-Type", "application/yaml; charset=utf-8") c.Header("Cache-Control", "no-store") c.Header("X-Content-Type-Options", "nosniff") // Write raw bytes as-is _, _ = c.Writer.Write(data) } // Debug func (h *Handler) GetDebug(c *gin.Context) { c.JSON(200, gin.H{"debug": h.cfg.Debug}) } func (h *Handler) PutDebug(c *gin.Context) { h.updateBoolField(c, func(v bool) { h.cfg.Debug = v }) } // UsageStatisticsEnabled func (h *Handler) GetUsageStatisticsEnabled(c *gin.Context) { c.JSON(200, gin.H{"usage-statistics-enabled": h.cfg.UsageStatisticsEnabled}) } func (h *Handler) PutUsageStatisticsEnabled(c *gin.Context) { h.updateBoolField(c, func(v bool) { h.cfg.UsageStatisticsEnabled = v }) } // UsageStatisticsEnabled func (h *Handler) GetLoggingToFile(c *gin.Context) { c.JSON(200, gin.H{"logging-to-file": h.cfg.LoggingToFile}) } func (h *Handler) PutLoggingToFile(c *gin.Context) { h.updateBoolField(c, func(v bool) { h.cfg.LoggingToFile = v }) } // Request log func (h *Handler) GetRequestLog(c *gin.Context) { c.JSON(200, gin.H{"request-log": h.cfg.RequestLog}) } func (h *Handler) PutRequestLog(c *gin.Context) { h.updateBoolField(c, func(v bool) { h.cfg.RequestLog = v }) } // Websocket auth func (h *Handler) GetWebsocketAuth(c *gin.Context) { c.JSON(200, gin.H{"ws-auth": h.cfg.WebsocketAuth}) } func (h *Handler) PutWebsocketAuth(c *gin.Context) { h.updateBoolField(c, func(v bool) { h.cfg.WebsocketAuth = v }) } // Request retry func (h *Handler) GetRequestRetry(c *gin.Context) { c.JSON(200, gin.H{"request-retry": h.cfg.RequestRetry}) } func (h *Handler) PutRequestRetry(c *gin.Context) { h.updateIntField(c, func(v int) { h.cfg.RequestRetry = v }) } // Max retry interval func (h *Handler) GetMaxRetryInterval(c *gin.Context) { c.JSON(200, gin.H{"max-retry-interval": h.cfg.MaxRetryInterval}) } func (h *Handler) PutMaxRetryInterval(c *gin.Context) { h.updateIntField(c, func(v int) { h.cfg.MaxRetryInterval = v }) } // Proxy URL func (h *Handler) GetProxyURL(c *gin.Context) { c.JSON(200, gin.H{"proxy-url": h.cfg.ProxyURL}) } func (h *Handler) PutProxyURL(c *gin.Context) { h.updateStringField(c, func(v string) { h.cfg.ProxyURL = v }) } func (h *Handler) DeleteProxyURL(c *gin.Context) { h.cfg.ProxyURL = "" h.persist(c) }