mirror of
https://github.com/black-ant/Ant-Browser.git
synced 2026-07-14 18:48:55 +08:00
publish: 1.0.0 snapshot (bad2ec1)
channel: master version: 1.0.0 source-ref: master published-at-utc: 2026-03-13T15:19:28Z
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ant-chrome/backend/internal/config"
|
||||
xproxy "golang.org/x/net/proxy"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// TestResult 代理测试结果
|
||||
type TestResult struct {
|
||||
ProxyId string
|
||||
Ok bool
|
||||
LatencyMs int64
|
||||
Error string
|
||||
}
|
||||
|
||||
// proxyEndpoint 从代理配置中提取 server:port,用于 TCP ping
|
||||
func proxyEndpoint(src string) (string, error) {
|
||||
src = strings.TrimSpace(src)
|
||||
l := strings.ToLower(src)
|
||||
|
||||
// 标准 URL 格式: socks5://host:port, http://host:port
|
||||
if strings.HasPrefix(l, "socks5://") || strings.HasPrefix(l, "http://") || strings.HasPrefix(l, "https://") {
|
||||
hostport := src[strings.Index(src, "//")+2:]
|
||||
hostport = strings.SplitN(hostport, "/", 2)[0]
|
||||
return hostport, nil
|
||||
}
|
||||
|
||||
// vmess:// URL (base64 encoded JSON)
|
||||
if strings.HasPrefix(l, "vmess://") {
|
||||
raw := strings.TrimPrefix(src, "vmess://")
|
||||
decoded, err := decodeBase64String(strings.TrimSpace(raw))
|
||||
if err == nil {
|
||||
var v struct {
|
||||
Add string `json:"add"`
|
||||
Port interface{} `json:"port"`
|
||||
}
|
||||
if jsonErr := json.Unmarshal(decoded, &v); jsonErr == nil && v.Add != "" {
|
||||
return fmt.Sprintf("%s:%v", v.Add, v.Port), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// vless:// URL: vless://uuid@host:port?...
|
||||
if strings.HasPrefix(l, "vless://") {
|
||||
rest := src[len("vless://"):]
|
||||
if at := strings.LastIndex(rest, "@"); at >= 0 {
|
||||
hostport := strings.SplitN(rest[at+1:], "?", 2)[0]
|
||||
hostport = strings.SplitN(hostport, "#", 2)[0]
|
||||
return hostport, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Clash YAML 格式
|
||||
var payload interface{}
|
||||
if err := yaml.Unmarshal([]byte(src), &payload); err == nil {
|
||||
node := pickClashNode(payload)
|
||||
if node != nil {
|
||||
server := getMapString(node, "server")
|
||||
port := getMapInt(node, "port")
|
||||
if server != "" && port > 0 {
|
||||
return fmt.Sprintf("%s:%d", server, port), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("无法解析代理地址")
|
||||
}
|
||||
|
||||
// TestConnectivity 通过 TCP 握手测试代理服务器的可达性和延迟
|
||||
// 直接对 server:port 建立 TCP 连接测量 RTT,无需启动外部进程
|
||||
func TestConnectivity(proxyId string, proxyConfig string, proxies []config.BrowserProxy, _ interface{}) TestResult {
|
||||
src := strings.TrimSpace(proxyConfig)
|
||||
if proxyId != "" {
|
||||
for _, item := range proxies {
|
||||
if strings.EqualFold(item.ProxyId, proxyId) {
|
||||
src = strings.TrimSpace(item.ProxyConfig)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if src == "" {
|
||||
return TestResult{ProxyId: proxyId, Ok: false, Error: "代理配置为空"}
|
||||
}
|
||||
|
||||
endpoint, err := proxyEndpoint(src)
|
||||
if err != nil {
|
||||
return TestResult{ProxyId: proxyId, Ok: false, Error: fmt.Sprintf("地址解析失败: %v", err)}
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
conn, err := net.DialTimeout("tcp", endpoint, 10*time.Second)
|
||||
latency := time.Since(start).Milliseconds()
|
||||
|
||||
if err != nil {
|
||||
return TestResult{ProxyId: proxyId, Ok: false, LatencyMs: latency, Error: err.Error()}
|
||||
}
|
||||
conn.Close()
|
||||
return TestResult{ProxyId: proxyId, Ok: true, LatencyMs: latency}
|
||||
}
|
||||
|
||||
func toStringMap(input interface{}) map[string]interface{} {
|
||||
switch v := input.(type) {
|
||||
case map[string]interface{}:
|
||||
return v
|
||||
case map[interface{}]interface{}:
|
||||
out := map[string]interface{}{}
|
||||
for k, val := range v {
|
||||
out[fmt.Sprint(k)] = val
|
||||
}
|
||||
return out
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getMapString(m map[string]interface{}, key string) string {
|
||||
v, ok := m[key]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
switch s := v.(type) {
|
||||
case string:
|
||||
return strings.TrimSpace(s)
|
||||
case int:
|
||||
return strconv.Itoa(s)
|
||||
case int64:
|
||||
return strconv.FormatInt(s, 10)
|
||||
case float64:
|
||||
return strconv.Itoa(int(s))
|
||||
case bool:
|
||||
if s {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
return strings.TrimSpace(fmt.Sprint(v))
|
||||
}
|
||||
|
||||
func getMapInt(m map[string]interface{}, key string) int {
|
||||
v, ok := m[key]
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
switch s := v.(type) {
|
||||
case int:
|
||||
return s
|
||||
case int64:
|
||||
return int(s)
|
||||
case float64:
|
||||
return int(s)
|
||||
case string:
|
||||
value, _ := strconv.Atoi(s)
|
||||
return value
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func getMapBool(m map[string]interface{}, key string) bool {
|
||||
v, ok := m[key]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
switch s := v.(type) {
|
||||
case bool:
|
||||
return s
|
||||
case string:
|
||||
return strings.ToLower(s) == "true"
|
||||
case int:
|
||||
return s != 0
|
||||
case float64:
|
||||
return int(s) != 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func decodeBase64String(raw string) ([]byte, error) {
|
||||
if raw == "" {
|
||||
return nil, fmt.Errorf("base64 内容为空")
|
||||
}
|
||||
if data, err := base64.StdEncoding.DecodeString(raw); err == nil {
|
||||
return data, nil
|
||||
}
|
||||
if data, err := base64.RawStdEncoding.DecodeString(raw); err == nil {
|
||||
return data, nil
|
||||
}
|
||||
if data, err := base64.URLEncoding.DecodeString(raw); err == nil {
|
||||
return data, nil
|
||||
}
|
||||
if data, err := base64.RawURLEncoding.DecodeString(raw); err == nil {
|
||||
return data, nil
|
||||
}
|
||||
return nil, fmt.Errorf("base64 解析失败")
|
||||
}
|
||||
|
||||
// isUnsupportedProtocol 判断是否为不支持的协议(hysteria/hysteria2)
|
||||
func isUnsupportedProtocol(src string) bool {
|
||||
l := strings.ToLower(strings.TrimSpace(src))
|
||||
return strings.HasPrefix(l, "hysteria://") || strings.HasPrefix(l, "hysteria2://")
|
||||
}
|
||||
|
||||
// TestRealConnectivity 通过代理链路发起真实 HTTP 请求测量端到端延迟。
|
||||
// - DirectProxy (http/https/socks5):直接通过该代理发送请求
|
||||
// - BridgeProxy (vmess/vless/Clash):调用 EnsureBridge 获取 socks5 地址后发送请求
|
||||
// - SingBoxProxy (hysteria2/tuic):调用 SingBoxManager.EnsureBridge 后发送请求
|
||||
func TestRealConnectivity(
|
||||
proxyId string,
|
||||
proxies []config.BrowserProxy,
|
||||
xrayMgr *XrayManager,
|
||||
) TestResult {
|
||||
return TestRealConnectivityWithSingBox(proxyId, proxies, xrayMgr, nil)
|
||||
}
|
||||
|
||||
// TestRealConnectivityWithSingBox 支持 sing-box 的真实连通性测试
|
||||
func TestRealConnectivityWithSingBox(
|
||||
proxyId string,
|
||||
proxies []config.BrowserProxy,
|
||||
xrayMgr *XrayManager,
|
||||
singboxMgr *SingBoxManager,
|
||||
) TestResult {
|
||||
src := ""
|
||||
for _, item := range proxies {
|
||||
if strings.EqualFold(item.ProxyId, proxyId) {
|
||||
src = strings.TrimSpace(item.ProxyConfig)
|
||||
break
|
||||
}
|
||||
}
|
||||
if src == "" {
|
||||
return TestResult{ProxyId: proxyId, Ok: false, Error: "代理配置为空"}
|
||||
}
|
||||
|
||||
const targetURL = "http://www.gstatic.com/generate_204"
|
||||
const timeout = 15 * time.Second
|
||||
|
||||
var client *http.Client
|
||||
|
||||
if IsSingBoxProtocol(src) {
|
||||
// hysteria2/tuic → sing-box 桥接
|
||||
if singboxMgr == nil {
|
||||
return TestResult{ProxyId: proxyId, Ok: false, Error: "sing-box 管理器未初始化,无法测试 hysteria2"}
|
||||
}
|
||||
socks5Addr, err := singboxMgr.EnsureBridge(src, proxies, proxyId)
|
||||
if err != nil {
|
||||
return TestResult{ProxyId: proxyId, Ok: false, Error: fmt.Sprintf("sing-box 桥接启动失败: %v", err)}
|
||||
}
|
||||
socks5Host := strings.TrimPrefix(socks5Addr, "socks5://")
|
||||
dialer, err := xproxy.SOCKS5("tcp", socks5Host, nil, xproxy.Direct)
|
||||
if err != nil {
|
||||
return TestResult{ProxyId: proxyId, Ok: false, Error: fmt.Sprintf("SOCKS5 dialer 创建失败: %v", err)}
|
||||
}
|
||||
contextDialer, ok := dialer.(xproxy.ContextDialer)
|
||||
if !ok {
|
||||
return TestResult{ProxyId: proxyId, Ok: false, Error: "SOCKS5 dialer 不支持 ContextDialer"}
|
||||
}
|
||||
transport := &http.Transport{DialContext: contextDialer.DialContext}
|
||||
client = &http.Client{Transport: transport, Timeout: timeout}
|
||||
} else if RequiresBridge(src, proxies, proxyId) {
|
||||
// BridgeProxy:通过 xray socks5 桥接
|
||||
if xrayMgr == nil {
|
||||
return TestResult{ProxyId: proxyId, Ok: false, Error: "xray 管理器未初始化"}
|
||||
}
|
||||
socks5Addr, err := xrayMgr.EnsureBridge(src, proxies, proxyId)
|
||||
if err != nil {
|
||||
return TestResult{ProxyId: proxyId, Ok: false, Error: fmt.Sprintf("桥接启动失败: %v", err)}
|
||||
}
|
||||
// 解析 socks5://127.0.0.1:port
|
||||
socks5Host := strings.TrimPrefix(socks5Addr, "socks5://")
|
||||
dialer, err := xproxy.SOCKS5("tcp", socks5Host, nil, xproxy.Direct)
|
||||
if err != nil {
|
||||
return TestResult{ProxyId: proxyId, Ok: false, Error: fmt.Sprintf("SOCKS5 dialer 创建失败: %v", err)}
|
||||
}
|
||||
contextDialer, ok := dialer.(xproxy.ContextDialer)
|
||||
if !ok {
|
||||
return TestResult{ProxyId: proxyId, Ok: false, Error: "SOCKS5 dialer 不支持 ContextDialer"}
|
||||
}
|
||||
transport := &http.Transport{DialContext: contextDialer.DialContext}
|
||||
client = &http.Client{Transport: transport, Timeout: timeout}
|
||||
} else {
|
||||
// DirectProxy:http/https/socks5 直接代理
|
||||
proxyURL, err := url.Parse(src)
|
||||
if err != nil {
|
||||
return TestResult{ProxyId: proxyId, Ok: false, Error: fmt.Sprintf("代理地址解析失败: %v", err)}
|
||||
}
|
||||
transport := &http.Transport{Proxy: http.ProxyURL(proxyURL)}
|
||||
client = &http.Client{Transport: transport, Timeout: timeout}
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
resp, err := client.Get(targetURL)
|
||||
latency := time.Since(start).Milliseconds()
|
||||
if err != nil {
|
||||
return TestResult{ProxyId: proxyId, Ok: false, LatencyMs: latency, Error: err.Error()}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
return TestResult{ProxyId: proxyId, Ok: false, LatencyMs: latency, Error: fmt.Sprintf("HTTP %d", resp.StatusCode)}
|
||||
}
|
||||
return TestResult{ProxyId: proxyId, Ok: true, LatencyMs: latency}
|
||||
}
|
||||
Reference in New Issue
Block a user