mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-06-23 05:26:20 +08:00
Replace the tag-pinned release lookup with the repository latest release endpoint. Derive the plugin version from the release tag, validate it, and attach an optional token to API requests to raise the rate limit.
161 lines
4.6 KiB
Go
161 lines
4.6 KiB
Go
package pluginstore
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/httpfetch"
|
|
)
|
|
|
|
const userAgent = "CLIProxyAPI"
|
|
|
|
// HTTPDoer abstracts the HTTP client used to execute requests.
|
|
type HTTPDoer = httpfetch.Doer
|
|
|
|
type Client struct {
|
|
HTTPClient HTTPDoer
|
|
RegistryURL string
|
|
UserAgent string
|
|
}
|
|
|
|
type Release struct {
|
|
TagName string `json:"tag_name"`
|
|
Assets []ReleaseAsset `json:"assets"`
|
|
}
|
|
|
|
type ReleaseAsset struct {
|
|
Name string `json:"name"`
|
|
BrowserDownloadURL string `json:"browser_download_url"`
|
|
}
|
|
|
|
func (c Client) FetchRegistry(ctx context.Context) (Registry, error) {
|
|
registryURL := strings.TrimSpace(c.RegistryURL)
|
|
if registryURL == "" {
|
|
registryURL = DefaultRegistryURL
|
|
}
|
|
data, errDownload := c.get(ctx, registryURL, "application/json")
|
|
if errDownload != nil {
|
|
return Registry{}, errDownload
|
|
}
|
|
registry, errParse := ParseRegistry(data)
|
|
if errParse != nil {
|
|
return Registry{}, errParse
|
|
}
|
|
return registry, nil
|
|
}
|
|
|
|
// FetchLatestRelease returns the latest published release of the plugin's
|
|
// GitHub repository, mirroring the WebUI panel update check.
|
|
func (c Client) FetchLatestRelease(ctx context.Context, plugin Plugin) (Release, error) {
|
|
owner, repo, errRepository := GitHubRepositoryParts(plugin.Repository)
|
|
if errRepository != nil {
|
|
return Release{}, errRepository
|
|
}
|
|
releaseURL := fmt.Sprintf(
|
|
"https://api.github.com/repos/%s/%s/releases/latest",
|
|
url.PathEscape(owner),
|
|
url.PathEscape(repo),
|
|
)
|
|
data, errDownload := c.get(ctx, releaseURL, "application/vnd.github+json")
|
|
if errDownload != nil {
|
|
return Release{}, errDownload
|
|
}
|
|
var release Release
|
|
if errDecode := json.Unmarshal(data, &release); errDecode != nil {
|
|
return Release{}, fmt.Errorf("decode release: %w", errDecode)
|
|
}
|
|
return release, nil
|
|
}
|
|
|
|
// ReleaseVersion derives the plugin version from the release tag, stripping a
|
|
// leading "v"/"V" and validating the result.
|
|
func ReleaseVersion(release Release) (string, error) {
|
|
version := normalizeVersion(release.TagName)
|
|
if !validPluginVersion(version) {
|
|
return "", fmt.Errorf("invalid release tag %q", release.TagName)
|
|
}
|
|
return version, nil
|
|
}
|
|
|
|
func (c Client) DownloadAsset(ctx context.Context, asset ReleaseAsset) ([]byte, error) {
|
|
if strings.TrimSpace(asset.BrowserDownloadURL) == "" {
|
|
return nil, fmt.Errorf("asset %q missing browser_download_url", asset.Name)
|
|
}
|
|
return c.get(ctx, asset.BrowserDownloadURL, "application/octet-stream")
|
|
}
|
|
|
|
func (c Client) get(ctx context.Context, requestURL string, accept string) ([]byte, error) {
|
|
headers := map[string]string{
|
|
"Accept": accept,
|
|
"User-Agent": c.userAgent(),
|
|
}
|
|
if token := gitHubAPIToken(requestURL); token != "" {
|
|
headers["Authorization"] = "Bearer " + token
|
|
}
|
|
return httpfetch.GetBytes(ctx, c.httpClient(), requestURL, headers, 0)
|
|
}
|
|
|
|
// gitHubAPIToken returns the optional GitHub token for GitHub API requests to
|
|
// raise the unauthenticated rate limit, mirroring the management asset updater.
|
|
func gitHubAPIToken(requestURL string) string {
|
|
parsed, errParse := url.Parse(requestURL)
|
|
if errParse != nil || !strings.EqualFold(parsed.Host, "api.github.com") {
|
|
return ""
|
|
}
|
|
gitURL := strings.ToLower(strings.TrimSpace(os.Getenv("GITSTORE_GIT_URL")))
|
|
if !strings.Contains(gitURL, "github.com") {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(os.Getenv("GITSTORE_GIT_TOKEN"))
|
|
}
|
|
|
|
func (c Client) httpClient() HTTPDoer {
|
|
if c.HTTPClient != nil {
|
|
return c.HTTPClient
|
|
}
|
|
return http.DefaultClient
|
|
}
|
|
|
|
func (c Client) userAgent() string {
|
|
if strings.TrimSpace(c.UserAgent) != "" {
|
|
return strings.TrimSpace(c.UserAgent)
|
|
}
|
|
return userAgent
|
|
}
|
|
|
|
func SelectReleaseAssets(release Release, id, version, goos, goarch string) (ReleaseAsset, ReleaseAsset, error) {
|
|
archiveName := ArchiveName(id, version, goos, goarch)
|
|
var archiveAsset ReleaseAsset
|
|
var checksumAsset ReleaseAsset
|
|
for _, asset := range release.Assets {
|
|
switch strings.TrimSpace(asset.Name) {
|
|
case archiveName:
|
|
archiveAsset = asset
|
|
case "checksums.txt":
|
|
checksumAsset = asset
|
|
}
|
|
}
|
|
if strings.TrimSpace(archiveAsset.Name) == "" {
|
|
return ReleaseAsset{}, ReleaseAsset{}, fmt.Errorf("release asset %s not found", archiveName)
|
|
}
|
|
if strings.TrimSpace(checksumAsset.Name) == "" {
|
|
return ReleaseAsset{}, ReleaseAsset{}, fmt.Errorf("release asset checksums.txt not found")
|
|
}
|
|
return archiveAsset, checksumAsset, nil
|
|
}
|
|
|
|
func ArchiveName(id, version, goos, goarch string) string {
|
|
return fmt.Sprintf(
|
|
"%s_%s_%s_%s.zip",
|
|
strings.TrimSpace(id),
|
|
strings.TrimSpace(version),
|
|
strings.TrimSpace(goos),
|
|
strings.TrimSpace(goarch),
|
|
)
|
|
}
|