mirror of
https://github.com/black-ant/Ant-Browser.git
synced 2026-07-14 18:48:55 +08:00
256 lines
8.1 KiB
Go
256 lines
8.1 KiB
Go
package backend
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net"
|
||
"net/http"
|
||
"net/url"
|
||
"os"
|
||
"sort"
|
||
"strings"
|
||
"time"
|
||
|
||
xproxy "golang.org/x/net/proxy"
|
||
)
|
||
|
||
type githubRelease struct {
|
||
TagName string `json:"tag_name"`
|
||
Assets []githubReleaseAsset `json:"assets"`
|
||
}
|
||
|
||
type githubReleaseAsset struct {
|
||
Name string `json:"name"`
|
||
BrowserDownloadURL string `json:"browser_download_url"`
|
||
Size int64 `json:"size"`
|
||
}
|
||
|
||
func proxyCoreHTTPClient(timeout time.Duration, proxyConfig string) (*http.Client, string, error) {
|
||
proxyConfig = strings.TrimSpace(proxyConfig)
|
||
if proxyConfig == "" || strings.EqualFold(proxyConfig, "direct://") {
|
||
return &http.Client{Timeout: timeout, Transport: proxyCoreDirectTransport()}, "直连", nil
|
||
}
|
||
u, err := url.Parse(proxyConfig)
|
||
if err != nil {
|
||
return nil, "", fmt.Errorf("代理地址解析失败: %w", err)
|
||
}
|
||
if isBadLocalHTTPSProxy(u) {
|
||
return nil, "", fmt.Errorf("下载代理不能填 %s,127.0.0.1:443 通常不是本机代理端口;请改成真实代理端口,如 socks5://127.0.0.1:7890,或留空直连", u.Host)
|
||
}
|
||
scheme := strings.ToLower(u.Scheme)
|
||
switch scheme {
|
||
case "http", "https":
|
||
return &http.Client{Timeout: timeout, Transport: &http.Transport{Proxy: http.ProxyURL(u)}}, "指定代理", nil
|
||
case "socks5":
|
||
var auth *xproxy.Auth
|
||
if u.User != nil {
|
||
password, _ := u.User.Password()
|
||
auth = &xproxy.Auth{User: u.User.Username(), Password: password}
|
||
}
|
||
dialer, err := xproxy.SOCKS5("tcp", u.Host, auth, xproxy.Direct)
|
||
if err != nil {
|
||
return nil, "", fmt.Errorf("SOCKS5 dialer 创建失败: %w", err)
|
||
}
|
||
contextDialer, ok := dialer.(xproxy.ContextDialer)
|
||
if !ok {
|
||
return nil, "", fmt.Errorf("SOCKS5 dialer 不支持 ContextDialer")
|
||
}
|
||
return &http.Client{Timeout: timeout, Transport: &http.Transport{DialContext: contextDialer.DialContext}}, "指定代理", nil
|
||
default:
|
||
return nil, "", fmt.Errorf("仅支持 http://、https://、socks5:// 或 direct://")
|
||
}
|
||
}
|
||
|
||
func proxyCoreDirectTransport() *http.Transport {
|
||
dialer := &net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}
|
||
return &http.Transport{
|
||
DialContext: func(ctx context.Context, network string, address string) (net.Conn, error) {
|
||
host, port, err := net.SplitHostPort(address)
|
||
if err == nil && port == "443" && isLocalhostHost(host) {
|
||
return nil, fmt.Errorf("直连下载被解析到 %s:这通常是本机 hosts/DNS 污染或仍在运行旧版本。请重启应用;如果仍出现,请检查 hosts/DNS,或在下载代理中填写真实代理端口", address)
|
||
}
|
||
return dialer.DialContext(ctx, network, address)
|
||
},
|
||
}
|
||
}
|
||
|
||
func isBadLocalHTTPSProxy(u *url.URL) bool {
|
||
if u == nil {
|
||
return false
|
||
}
|
||
return isLocalhostHost(u.Hostname()) && u.Port() == "443"
|
||
}
|
||
|
||
func isLocalhostHost(host string) bool {
|
||
host = strings.Trim(strings.ToLower(strings.TrimSpace(host)), "[]")
|
||
return host == "127.0.0.1" || host == "localhost" || host == "::1"
|
||
}
|
||
|
||
func fetchGitHubRelease(ctx context.Context, client *http.Client, repo string, version string) (githubRelease, error) {
|
||
apiURL := "https://api.github.com/repos/" + repo + "/releases/latest"
|
||
if !strings.EqualFold(strings.TrimSpace(version), "latest") {
|
||
apiURL = "https://api.github.com/repos/" + repo + "/releases/tags/" + strings.TrimSpace(version)
|
||
}
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil)
|
||
if err != nil {
|
||
return githubRelease{}, err
|
||
}
|
||
req.Header.Set("Accept", "application/vnd.github+json")
|
||
req.Header.Set("User-Agent", "ant-chrome-proxy-core-downloader")
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
return githubRelease{}, err
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||
return githubRelease{}, fmt.Errorf("GitHub API HTTP %d", resp.StatusCode)
|
||
}
|
||
var release githubRelease
|
||
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
|
||
return githubRelease{}, err
|
||
}
|
||
if len(release.Assets) == 0 {
|
||
return githubRelease{}, fmt.Errorf("Release 没有可下载资产")
|
||
}
|
||
return release, nil
|
||
}
|
||
|
||
func selectProxyCoreAsset(spec proxyCoreSpec, assets []githubReleaseAsset, goos string, goarch string) (githubReleaseAsset, error) {
|
||
osTokens := map[string][]string{
|
||
"windows": {"windows", "win"},
|
||
"linux": {"linux"},
|
||
"darwin": {"darwin", "macos"},
|
||
}
|
||
archTokens := map[string][]string{
|
||
"amd64": {"amd64", "x86_64", "64"},
|
||
"arm64": {"arm64", "aarch64"},
|
||
"386": {"386", "i386", "x86"},
|
||
}
|
||
extTokens := []string{".zip", ".tar.gz", ".tgz"}
|
||
if spec.Core == "mihomo" {
|
||
extTokens = append(extTokens, ".gz")
|
||
}
|
||
if goos == "windows" {
|
||
extTokens = []string{".zip"}
|
||
}
|
||
badTokens := []string{"sha", "checksum", "dgst", ".sig", ".asc", "source", "geoip", "geosite"}
|
||
candidates := make([]githubReleaseAsset, 0)
|
||
for _, asset := range assets {
|
||
name := strings.ToLower(asset.Name)
|
||
if !hasAnySuffix(name, extTokens) || containsAny(name, badTokens) {
|
||
continue
|
||
}
|
||
if !containsAny(name, osTokens[goos]) || !matchesProxyAssetArch(name, goarch, archTokens[goarch]) {
|
||
continue
|
||
}
|
||
if spec.Core == "mihomo" && !strings.Contains(name, "compatible") {
|
||
continue
|
||
}
|
||
candidates = append(candidates, asset)
|
||
}
|
||
if len(candidates) == 0 && spec.Core == "mihomo" {
|
||
fallbackSpec := spec
|
||
fallbackSpec.Core = "mihomo-fallback"
|
||
for _, asset := range assets {
|
||
name := strings.ToLower(asset.Name)
|
||
if hasAnySuffix(name, extTokens) && !containsAny(name, badTokens) && containsAny(name, osTokens[goos]) && matchesProxyAssetArch(name, goarch, archTokens[goarch]) {
|
||
candidates = append(candidates, asset)
|
||
}
|
||
}
|
||
}
|
||
if len(candidates) == 0 {
|
||
return githubReleaseAsset{}, fmt.Errorf("官方 Release 未找到适配 %s/%s 的 %s 资产", goos, goarch, spec.DisplayName)
|
||
}
|
||
sort.SliceStable(candidates, func(i, j int) bool {
|
||
ai := assetScore(spec, candidates[i].Name)
|
||
aj := assetScore(spec, candidates[j].Name)
|
||
if ai != aj {
|
||
return ai > aj
|
||
}
|
||
return candidates[i].Name < candidates[j].Name
|
||
})
|
||
return candidates[0], nil
|
||
}
|
||
|
||
func proxyCoreBinaryName(binaryBase string, targetOS string) string {
|
||
if targetOS == "windows" {
|
||
return binaryBase + ".exe"
|
||
}
|
||
return binaryBase
|
||
}
|
||
|
||
func matchesProxyAssetArch(name string, goarch string, tokens []string) bool {
|
||
if goarch == "amd64" && strings.Contains(name, "arm64") {
|
||
return false
|
||
}
|
||
if goarch == "386" && (strings.Contains(name, "amd64") || strings.Contains(name, "arm64")) {
|
||
return false
|
||
}
|
||
return containsAny(name, tokens)
|
||
}
|
||
|
||
func assetScore(spec proxyCoreSpec, name string) int {
|
||
lower := strings.ToLower(name)
|
||
score := 0
|
||
if strings.HasSuffix(lower, ".zip") {
|
||
score += 3
|
||
}
|
||
if strings.Contains(lower, "compatible") {
|
||
score += 5
|
||
}
|
||
if strings.Contains(lower, spec.BinaryBase) || strings.Contains(lower, spec.Core) {
|
||
score += 2
|
||
}
|
||
if !strings.Contains(lower, "glibc") && !strings.Contains(lower, "musl") && !strings.Contains(lower, "softfloat") && !strings.Contains(lower, "legacy") {
|
||
score += 2
|
||
}
|
||
return score
|
||
}
|
||
|
||
func downloadProxyCoreAsset(ctx context.Context, client *http.Client, url string, file *os.File, totalSize int64, send func(string, int, string)) error {
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
req.Header.Set("User-Agent", "ant-chrome-proxy-core-downloader")
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||
return fmt.Errorf("HTTP %d", resp.StatusCode)
|
||
}
|
||
if totalSize <= 0 {
|
||
totalSize = resp.ContentLength
|
||
}
|
||
buf := make([]byte, 1024*1024)
|
||
var downloaded int64
|
||
lastTick := time.Now()
|
||
for {
|
||
n, readErr := resp.Body.Read(buf)
|
||
if n > 0 {
|
||
if _, err := file.Write(buf[:n]); err != nil {
|
||
return err
|
||
}
|
||
downloaded += int64(n)
|
||
if totalSize > 0 && time.Since(lastTick) > 500*time.Millisecond {
|
||
progress := 5 + int(float64(downloaded)/float64(totalSize)*70)
|
||
if progress > 75 {
|
||
progress = 75
|
||
}
|
||
send("downloading", progress, fmt.Sprintf("下载中 %.1f MB / %.1f MB", float64(downloaded)/1024/1024, float64(totalSize)/1024/1024))
|
||
lastTick = time.Now()
|
||
}
|
||
}
|
||
if readErr == io.EOF {
|
||
return nil
|
||
}
|
||
if readErr != nil {
|
||
return readErr
|
||
}
|
||
}
|
||
}
|