| | 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/CLIProxyAPIPlus/releases/latest" |
| | latestReleaseUserAgent = "CLIProxyAPIPlus" |
| | ) |
| |
|
| | 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) 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) 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) |
| | } |
| |
|
| | |
| | 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) { |
| | h.updateIntField(c, func(v int) { h.cfg.LogsMaxTotalSizeMB = v }) |
| | } |
| |
|
| | |
| | func (h *Handler) GetRoutingStrategy(c *gin.Context) { |
| | c.JSON(200, gin.H{"strategy": h.cfg.Routing.Strategy}) |
| | } |
| | func (h *Handler) PutRoutingStrategy(c *gin.Context) { |
| | h.updateStringField(c, func(v string) { h.cfg.Routing.Strategy = 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 }) |
| | } |
| |
|