mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-09-03 06:35:00 +08:00
feat(github): resolve GitHub token for release checks and asset updates
- Add `util.ResolveGitHubToken` to resolve GitHub API tokens with priority order (`GITHUB_TOKEN`, `github_token`, and `GITSTORE_GIT_TOKEN` for GitHub repositories). - Set GitHub `Authorization` headers in management version check and asset updater requests when a token is resolved. Closes: #5189
This commit is contained in:
@@ -36,6 +36,14 @@ type releaseInfo struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func setLatestReleaseRequestHeaders(req *http.Request) {
|
||||
req.Header.Set("Accept", "application/vnd.github+json")
|
||||
req.Header.Set("User-Agent", latestReleaseUserAgent)
|
||||
if token := util.ResolveGitHubToken(); token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
}
|
||||
|
||||
// GetLatestVersion returns the latest release version from GitHub without downloading assets.
|
||||
func (h *Handler) GetLatestVersion(c *gin.Context) {
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
@@ -53,8 +61,7 @@ func (h *Handler) GetLatestVersion(c *gin.Context) {
|
||||
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)
|
||||
setLatestReleaseRequestHeaders(req)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package management
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSetLatestReleaseRequestHeaders(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
githubToken string
|
||||
wantAuthorization string
|
||||
}{
|
||||
{
|
||||
name: "sets GitHub authorization",
|
||||
githubToken: "release-token",
|
||||
wantAuthorization: "Bearer release-token",
|
||||
},
|
||||
{
|
||||
name: "omits authorization without token",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Setenv("GITHUB_TOKEN", tt.githubToken)
|
||||
t.Setenv("github_token", "")
|
||||
t.Setenv("GITSTORE_GIT_TOKEN", "")
|
||||
t.Setenv("GITSTORE_GIT_URL", "")
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, latestReleaseURL, nil)
|
||||
setLatestReleaseRequestHeaders(req)
|
||||
|
||||
if got := req.Header.Get("Authorization"); got != tt.wantAuthorization {
|
||||
t.Fatalf("Authorization = %q, want %q", got, tt.wantAuthorization)
|
||||
}
|
||||
if got := req.Header.Get("Accept"); got != "application/vnd.github+json" {
|
||||
t.Fatalf("Accept = %q, want GitHub JSON media type", got)
|
||||
}
|
||||
if got := req.Header.Get("User-Agent"); got != latestReleaseUserAgent {
|
||||
t.Fatalf("User-Agent = %q, want %q", got, latestReleaseUserAgent)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -350,9 +350,8 @@ func fetchLatestAsset(ctx context.Context, client *http.Client, releaseURL strin
|
||||
"Accept": "application/vnd.github+json",
|
||||
"User-Agent": httpUserAgent,
|
||||
}
|
||||
gitURL := strings.ToLower(strings.TrimSpace(os.Getenv("GITSTORE_GIT_URL")))
|
||||
if tok := strings.TrimSpace(os.Getenv("GITSTORE_GIT_TOKEN")); tok != "" && strings.Contains(gitURL, "github.com") {
|
||||
headers["Authorization"] = "Bearer " + tok
|
||||
if token := util.ResolveGitHubToken(); token != "" {
|
||||
headers["Authorization"] = "Bearer " + token
|
||||
}
|
||||
|
||||
data, err := httpfetch.GetBytes(ctx, client, releaseURL, headers, 0)
|
||||
|
||||
@@ -1,11 +1,71 @@
|
||||
package managementasset
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
|
||||
)
|
||||
|
||||
func TestFetchLatestAssetSetsGitHubAuthorization(t *testing.T) {
|
||||
t.Setenv("GITHUB_TOKEN", "asset-token")
|
||||
t.Setenv("github_token", "")
|
||||
t.Setenv("GITSTORE_GIT_TOKEN", "")
|
||||
t.Setenv("GITSTORE_GIT_URL", "")
|
||||
|
||||
var authorization string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
authorization = req.Header.Get("Authorization")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"assets":[{"name":"management.html","browser_download_url":"https://example.com/management.html","digest":"sha256:abc123"}]}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
asset, remoteHash, err := fetchLatestAsset(t.Context(), server.Client(), server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("fetchLatestAsset() error = %v", err)
|
||||
}
|
||||
if authorization != "Bearer asset-token" {
|
||||
t.Fatalf("Authorization = %q, want %q", authorization, "Bearer asset-token")
|
||||
}
|
||||
if asset == nil || asset.Name != managementAssetName {
|
||||
t.Fatalf("asset = %#v, want %q", asset, managementAssetName)
|
||||
}
|
||||
if remoteHash != "abc123" {
|
||||
t.Fatalf("remoteHash = %q, want %q", remoteHash, "abc123")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchLatestAssetOmitsAuthorizationWithoutToken(t *testing.T) {
|
||||
t.Setenv("GITHUB_TOKEN", "")
|
||||
t.Setenv("github_token", "")
|
||||
t.Setenv("GITSTORE_GIT_TOKEN", "")
|
||||
t.Setenv("GITSTORE_GIT_URL", "")
|
||||
|
||||
var authorization string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
authorization = req.Header.Get("Authorization")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"assets":[{"name":"management.html","browser_download_url":"https://example.com/management.html","digest":"sha256:abc123"}]}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
asset, remoteHash, err := fetchLatestAsset(t.Context(), server.Client(), server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("fetchLatestAsset() error = %v", err)
|
||||
}
|
||||
if authorization != "" {
|
||||
t.Fatalf("Authorization = %q, want empty", authorization)
|
||||
}
|
||||
if asset == nil || asset.Name != managementAssetName {
|
||||
t.Fatalf("asset = %#v, want %q", asset, managementAssetName)
|
||||
}
|
||||
if remoteHash != "abc123" {
|
||||
t.Fatalf("remoteHash = %q, want %q", remoteHash, "abc123")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoUpdateSkipReason(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
25
internal/util/github.go
Normal file
25
internal/util/github.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ResolveGitHubToken returns the configured GitHub API token in priority order:
|
||||
// 1. GITHUB_TOKEN
|
||||
// 2. github_token
|
||||
// 3. GITSTORE_GIT_TOKEN (only if GITSTORE_GIT_URL points to github.com)
|
||||
func ResolveGitHubToken() string {
|
||||
for _, name := range []string{"GITHUB_TOKEN", "github_token"} {
|
||||
if token := strings.TrimSpace(os.Getenv(name)); token != "" {
|
||||
return token
|
||||
}
|
||||
}
|
||||
|
||||
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"))
|
||||
}
|
||||
62
internal/util/github_test.go
Normal file
62
internal/util/github_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package util
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestResolveGitHubToken(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
githubToken string
|
||||
lowerToken string
|
||||
gitStoreToken string
|
||||
gitStoreURL string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "GITHUB_TOKEN has highest priority",
|
||||
githubToken: " primary-token ",
|
||||
lowerToken: "lower-token",
|
||||
gitStoreToken: "gitstore-token",
|
||||
gitStoreURL: "https://github.com/example/repo.git",
|
||||
want: "primary-token",
|
||||
},
|
||||
{
|
||||
name: "lowercase token is second priority",
|
||||
githubToken: " ",
|
||||
lowerToken: " lower-token ",
|
||||
gitStoreToken: "gitstore-token",
|
||||
gitStoreURL: "https://github.com/example/repo.git",
|
||||
want: "lower-token",
|
||||
},
|
||||
{
|
||||
name: "Git store token is used for GitHub",
|
||||
gitStoreToken: " gitstore-token ",
|
||||
gitStoreURL: "HTTPS://GITHUB.COM/example/repo.git",
|
||||
want: "gitstore-token",
|
||||
},
|
||||
{
|
||||
name: "Git store token is ignored for other hosts",
|
||||
gitStoreToken: "gitstore-token",
|
||||
gitStoreURL: "https://gitlab.com/example/repo.git",
|
||||
},
|
||||
{
|
||||
name: "Git store token is ignored without URL",
|
||||
gitStoreToken: "gitstore-token",
|
||||
},
|
||||
{
|
||||
name: "no token configured",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Setenv("GITHUB_TOKEN", tt.githubToken)
|
||||
t.Setenv("github_token", tt.lowerToken)
|
||||
t.Setenv("GITSTORE_GIT_TOKEN", tt.gitStoreToken)
|
||||
t.Setenv("GITSTORE_GIT_URL", tt.gitStoreURL)
|
||||
|
||||
if got := ResolveGitHubToken(); got != tt.want {
|
||||
t.Fatalf("ResolveGitHubToken() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user