38d875b06f
针对国内服务器访问 GitHub 困难的问题,为在线更新和定价数据获取功能添加代理支持。
主要变更:
- 新增 update.proxy_url 配置项,支持 http/https/socks5/socks5h 协议
- 修改 GitHubReleaseClient 和 PricingRemoteClient 支持代理配置
- 更新 Wire 依赖注入,通过 Provider 函数传递配置
- 更新 Docker 配置文件,支持通过 UPDATE_PROXY_URL 环境变量设置代理
配置示例:
update:
proxy_url: "http://127.0.0.1:7890"
Docker 环境变量:
UPDATE_PROXY_URL=http://host.docker.internal:7890
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
137 lines
3.5 KiB
Go
137 lines
3.5 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/httpclient"
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
)
|
|
|
|
type githubReleaseClient struct {
|
|
httpClient *http.Client
|
|
downloadHTTPClient *http.Client
|
|
}
|
|
|
|
// NewGitHubReleaseClient 创建 GitHub Release 客户端
|
|
// proxyURL 为空时直连 GitHub,支持 http/https/socks5/socks5h 协议
|
|
func NewGitHubReleaseClient(proxyURL string) service.GitHubReleaseClient {
|
|
sharedClient, err := httpclient.GetClient(httpclient.Options{
|
|
Timeout: 30 * time.Second,
|
|
ProxyURL: proxyURL,
|
|
})
|
|
if err != nil {
|
|
sharedClient = &http.Client{Timeout: 30 * time.Second}
|
|
}
|
|
|
|
// 下载客户端需要更长的超时时间
|
|
downloadClient, err := httpclient.GetClient(httpclient.Options{
|
|
Timeout: 10 * time.Minute,
|
|
ProxyURL: proxyURL,
|
|
})
|
|
if err != nil {
|
|
downloadClient = &http.Client{Timeout: 10 * time.Minute}
|
|
}
|
|
|
|
return &githubReleaseClient{
|
|
httpClient: sharedClient,
|
|
downloadHTTPClient: downloadClient,
|
|
}
|
|
}
|
|
|
|
func (c *githubReleaseClient) FetchLatestRelease(ctx context.Context, repo string) (*service.GitHubRelease, error) {
|
|
url := fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", repo)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Accept", "application/vnd.github.v3+json")
|
|
req.Header.Set("User-Agent", "Sub2API-Updater")
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("GitHub API returned %d", resp.StatusCode)
|
|
}
|
|
|
|
var release service.GitHubRelease
|
|
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &release, nil
|
|
}
|
|
|
|
func (c *githubReleaseClient) DownloadFile(ctx context.Context, url, dest string, maxSize int64) error {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 使用预配置的下载客户端(已包含代理配置)
|
|
resp, err := c.downloadHTTPClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("download returned %d", resp.StatusCode)
|
|
}
|
|
|
|
// SECURITY: Check Content-Length if available
|
|
if resp.ContentLength > maxSize {
|
|
return fmt.Errorf("file too large: %d bytes (max %d)", resp.ContentLength, maxSize)
|
|
}
|
|
|
|
out, err := os.Create(dest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = out.Close() }()
|
|
|
|
// SECURITY: Use LimitReader to enforce max download size even if Content-Length is missing/wrong
|
|
limited := io.LimitReader(resp.Body, maxSize+1)
|
|
written, err := io.Copy(out, limited)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check if we hit the limit (downloaded more than maxSize)
|
|
if written > maxSize {
|
|
_ = os.Remove(dest) // Clean up partial file (best-effort)
|
|
return fmt.Errorf("download exceeded maximum size of %d bytes", maxSize)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *githubReleaseClient) FetchChecksumFile(ctx context.Context, url string) ([]byte, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
|
|
}
|
|
|
|
return io.ReadAll(resp.Body)
|
|
}
|