| package management |
|
|
| import ( |
| "encoding/json" |
| "fmt" |
| "io" |
| "net/http" |
| "os" |
| "path/filepath" |
| "strings" |
| "time" |
|
|
| "github.com/gin-gonic/gin" |
| "github.com/router-for-me/CLIProxyAPI/v6/internal/config" |
| "github.com/router-for-me/CLIProxyAPI/v6/internal/util" |
| sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" |
| log "github.com/sirupsen/logrus" |
| "gopkg.in/yaml.v3" |
| ) |
|
|
| const ( |
| latestReleaseURL = "https://api.github.com/repos/router-for-me/CLIProxyAPI/releases/latest" |
| latestReleaseUserAgent = "CLIProxyAPI" |
| ) |
|
|
| func (h *Handler) GetConfig(c *gin.Context) { |
| if h == nil || h.cfg == nil { |
| c.JSON(200, gin.H{}) |
| return |
| } |
| cfgCopy := *h.cfg |
| c.JSON(200, &cfgCopy) |
| } |
|
|
| type releaseInfo struct { |
| TagName string `json:"tag_name"` |
| Name string `json:"name"` |
| } |
|
|
| |
| func (h *Handler) GetLatestVersion(c *gin.Context) { |
| client := &http.Client{Timeout: 10 * time.Second} |
| proxyURL := "" |
| if h != nil && h.cfg != nil { |
| proxyURL = strings.TrimSpace(h.cfg.ProxyURL) |
| } |
| if proxyURL != "" { |
| sdkCfg := &sdkconfig.SDKConfig{ProxyURL: proxyURL} |
| util.SetProxy(sdkCfg, client) |
| } |
|
|
| req, err := http.NewRequestWithContext(c.Request.Context(), http.MethodGet, latestReleaseURL, nil) |
| if err != nil { |
| c.JSON(http.StatusInternalServerError, gin.H{"error": "request_create_failed", "message": err.Error()}) |
| return |
| } |
| req.Header.Set("Accept", "application/vnd.github+json") |
| req.Header.Set("User-Agent", latestReleaseUserAgent) |
|
|
| resp, err := client.Do(req) |
| if err != nil { |
| c.JSON(http.StatusBadGateway, gin.H{"error": "request_failed", "message": err.Error()}) |
| return |
| } |
| defer func() { |
| if errClose := resp.Body.Close(); errClose != nil { |
| log.WithError(errClose).Debug("failed to close latest version response body") |
| } |
| }() |
|
|
| if resp.StatusCode != http.StatusOK { |
| body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024)) |
| c.JSON(http.StatusBadGateway, gin.H{"error": "unexpected_status", "message": fmt.Sprintf("status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))}) |
| return |
| } |
|
|
| var info releaseInfo |
| if errDecode := json.NewDecoder(resp.Body).Decode(&info); errDecode != nil { |
| c.JSON(http.StatusBadGateway, gin.H{"error": "decode_failed", "message": errDecode.Error()}) |
| return |
| } |
|
|
| version := strings.TrimSpace(info.TagName) |
| if version == "" { |
| version = strings.TrimSpace(info.Name) |
| } |
| if version == "" { |
| c.JSON(http.StatusBadGateway, gin.H{"error": "invalid_response", "message": "missing release version"}) |
| return |
| } |
|
|
| c.JSON(http.StatusOK, gin.H{"latest-version": version}) |
| } |
|
|
| 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 |
| } |
| |
| 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 |
| } |
| |
| 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"}}) |
| } |
|
|
| |
| |
| func (h *Handler) GetConfigYAML(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") |
| |
| _, _ = c.Writer.Write(data) |
| } |
|
|
| |
| 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 }) } |
|
|
| |
| 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 }) |
| } |
|
|
| |
| 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 }) |
| } |
|
|
| |
| func (h *Handler) GetLogsMaxTotalSizeMB(c *gin.Context) { |
| c.JSON(200, gin.H{"logs-max-total-size-mb": h.cfg.LogsMaxTotalSizeMB}) |
| } |
| func (h *Handler) PutLogsMaxTotalSizeMB(c *gin.Context) { |
| var body struct { |
| Value *int `json:"value"` |
| } |
| if errBindJSON := c.ShouldBindJSON(&body); errBindJSON != nil || body.Value == nil { |
| c.JSON(http.StatusBadRequest, gin.H{"error": "invalid body"}) |
| return |
| } |
| value := *body.Value |
| if value < 0 { |
| value = 0 |
| } |
| h.cfg.LogsMaxTotalSizeMB = value |
| h.persist(c) |
| } |
|
|
| |
| 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 }) |
| } |
|
|
| |
| 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 }) |
| } |
|
|
| |
| 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 }) |
| } |
|
|
| |
| 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 }) |
| } |
|
|
| |
| func (h *Handler) GetForceModelPrefix(c *gin.Context) { |
| c.JSON(200, gin.H{"force-model-prefix": h.cfg.ForceModelPrefix}) |
| } |
| func (h *Handler) PutForceModelPrefix(c *gin.Context) { |
| h.updateBoolField(c, func(v bool) { h.cfg.ForceModelPrefix = v }) |
| } |
|
|
| func normalizeRoutingStrategy(strategy string) (string, bool) { |
| normalized := strings.ToLower(strings.TrimSpace(strategy)) |
| switch normalized { |
| case "", "round-robin", "roundrobin", "rr": |
| return "round-robin", true |
| case "fill-first", "fillfirst", "ff": |
| return "fill-first", true |
| default: |
| return "", false |
| } |
| } |
|
|
| |
| func (h *Handler) GetRoutingStrategy(c *gin.Context) { |
| strategy, ok := normalizeRoutingStrategy(h.cfg.Routing.Strategy) |
| if !ok { |
| c.JSON(200, gin.H{"strategy": strings.TrimSpace(h.cfg.Routing.Strategy)}) |
| return |
| } |
| c.JSON(200, gin.H{"strategy": strategy}) |
| } |
| func (h *Handler) PutRoutingStrategy(c *gin.Context) { |
| var body struct { |
| Value *string `json:"value"` |
| } |
| if errBindJSON := c.ShouldBindJSON(&body); errBindJSON != nil || body.Value == nil { |
| c.JSON(http.StatusBadRequest, gin.H{"error": "invalid body"}) |
| return |
| } |
| normalized, ok := normalizeRoutingStrategy(*body.Value) |
| if !ok { |
| c.JSON(http.StatusBadRequest, gin.H{"error": "invalid strategy"}) |
| return |
| } |
| h.cfg.Routing.Strategy = normalized |
| h.persist(c) |
| } |
|
|
| |
| 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) |
| } |
|
|