package client import ( "context" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "regexp" "strings" "time" "douyu_go/internal/types" ) var ipPortRegex = regexp.MustCompile(`(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d{2,5})`) // ParseProxyURL 解析并规范化代理字符串为 *url.URL。 // 支持 http://, https://, socks5://;若未带协议头,默认补齐 http://。 func ParseProxyURL(proxyStr string) (*url.URL, error) { s := strings.TrimSpace(proxyStr) if s == "" { return nil, nil } if !strings.Contains(s, "://") { s = "http://" + s } u, err := url.Parse(s) if err != nil { return nil, fmt.Errorf("代理地址格式无效: %w", err) } scheme := strings.ToLower(u.Scheme) if scheme != "http" && scheme != "https" && scheme != "socks5" { return nil, fmt.Errorf("不支持的代理协议 %q,仅支持 http、https、socks5", u.Scheme) } if u.Host == "" { return nil, errors.New("代理地址缺少主机或端口") } return u, nil } // FetchProxyFromAPI 从指定的代理提供商 API 拉取一个代理 IP。 // 兼容星空代理等常见 JSON 格式,以及纯文本 IP:Port 格式。 func FetchProxyFromAPI(apiURL string) (*types.ProxyFetchResult, error) { trimmed := strings.TrimSpace(apiURL) if trimmed == "" { return nil, errors.New("代理提取 API 地址为空") } if !strings.HasPrefix(trimmed, "http://") && !strings.HasPrefix(trimmed, "https://") { return nil, errors.New("代理提取 API 地址必须以 http:// 或 https:// 开头") } req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, trimmed, nil) if err != nil { return nil, fmt.Errorf("创建提取请求失败: %w", err) } req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") client := &http.Client{ Timeout: 8 * time.Second, } resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("请求代理 API 失败: %w", err) } defer resp.Body.Close() bodyBytes, err := io.ReadAll(io.LimitReader(resp.Body, 128*1024)) if err != nil { return nil, fmt.Errorf("读取代理 API 响应失败: %w", err) } bodyStr := strings.TrimSpace(string(bodyBytes)) if bodyStr == "" { return nil, errors.New("代理 API 返回内容为空") } // 尝试优先按照 JSON 结构体解析 var jsonObj struct { Status int `json:"status"` Code *int `json:"code"` Info string `json:"info"` Msg string `json:"msg"` Data json.RawMessage `json:"data"` } if jsonErr := json.Unmarshal([]byte(bodyStr), &jsonObj); jsonErr == nil { // 判断是否有错误提示 errInfo := jsonObj.Info if errInfo == "" { errInfo = jsonObj.Msg } // 星空代理成功状态为 100,其它平台通用成功状态为 0 hasSuccessStatus := jsonObj.Status == 100 || (jsonObj.Code != nil && *jsonObj.Code == 0) hasErrorStatus := (jsonObj.Status != 0 && jsonObj.Status != 100) || (jsonObj.Code != nil && *jsonObj.Code != 0) if hasErrorStatus && errInfo != "" { return nil, fmt.Errorf("代理平台返回: %s (状态码=%d)", errInfo, jsonObj.Status) } if len(jsonObj.Data) > 0 { // 1. 尝试解析为对象数组: [{"ip":"...", "port":"...", "prov":"...", "city":"..."}] type nodeItem struct { IP string `json:"ip"` Port any `json:"port"` Prov string `json:"prov"` City string `json:"city"` } var items []nodeItem if err := json.Unmarshal(jsonObj.Data, &items); err == nil && len(items) > 0 { ip := strings.TrimSpace(items[0].IP) portStr := fmt.Sprintf("%v", items[0].Port) region := strings.TrimSpace(items[0].Prov + " " + items[0].City) if ip != "" && portStr != "" { return &types.ProxyFetchResult{ Success: true, ProxyURL: fmt.Sprintf("http://%s:%s", ip, portStr), IP: ip, Port: portStr, Region: region, }, nil } } // 2. 尝试解析为字符串数组: ["ip:port"] var strArr []string if err := json.Unmarshal(jsonObj.Data, &strArr); err == nil && len(strArr) > 0 { if m := ipPortRegex.FindStringSubmatch(strArr[0]); len(m) == 3 { return &types.ProxyFetchResult{ Success: true, ProxyURL: fmt.Sprintf("http://%s:%s", m[1], m[2]), IP: m[1], Port: m[2], }, nil } } // 3. 尝试解析为单字符串 var singleStr string if err := json.Unmarshal(jsonObj.Data, &singleStr); err == nil { if m := ipPortRegex.FindStringSubmatch(singleStr); len(m) == 3 { return &types.ProxyFetchResult{ Success: true, ProxyURL: fmt.Sprintf("http://%s:%s", m[1], m[2]), IP: m[1], Port: m[2], }, nil } } } if !hasSuccessStatus && errInfo != "" { return nil, fmt.Errorf("代理平台提示: %s", errInfo) } } // 兜底正则提取 IP:Port 文本格式 matches := ipPortRegex.FindStringSubmatch(bodyStr) if len(matches) == 3 { return &types.ProxyFetchResult{ Success: true, ProxyURL: fmt.Sprintf("http://%s:%s", matches[1], matches[2]), IP: matches[1], Port: matches[2], }, nil } return nil, fmt.Errorf("未能从 API 返回中识别出有效 IP:端口,原始返回: %s", truncateString(bodyStr, 120)) } // TestProxyConnection 测试指定代理的连通性、出口公网 IP 与往返延迟。 func TestProxyConnection(proxyStr string) types.ProxyTestResult { start := time.Now() trsp := &http.Transport{ MaxConnsPerHost: 10, MaxIdleConns: 10, MaxIdleConnsPerHost: 5, IdleConnTimeout: 10 * time.Second, } trimmed := strings.TrimSpace(proxyStr) if trimmed != "" { u, err := ParseProxyURL(trimmed) if err != nil { return types.ProxyTestResult{ Success: false, Error: err.Error(), } } trsp.Proxy = http.ProxyURL(u) } client := &http.Client{ Transport: trsp, Timeout: 6 * time.Second, } // 优先选用 myip.ipip.net req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://myip.ipip.net", nil) if err != nil { return types.ProxyTestResult{ Success: false, Error: fmt.Sprintf("创建请求失败: %v", err), } } req.Header.Set("User-Agent", "curl/8.0") resp, err := client.Do(req) latency := time.Since(start).Milliseconds() if err != nil { // 备用请求 cip.cc backupStart := time.Now() backupReq, bErr := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://cip.cc", nil) if bErr == nil { backupReq.Header.Set("User-Agent", "curl/8.0") if bResp, bDoErr := client.Do(backupReq); bDoErr == nil { defer bResp.Body.Close() latency = time.Since(backupStart).Milliseconds() bBytes, _ := io.ReadAll(io.LimitReader(bResp.Body, 8192)) bText := string(bBytes) ip, region := parseCipResponse(bText) if ip != "" { return types.ProxyTestResult{ Success: true, IP: ip, Region: region, Latency: latency, } } } } return types.ProxyTestResult{ Success: false, Latency: latency, Error: formatProxyError(err), } } defer resp.Body.Close() bodyBytes, err := io.ReadAll(io.LimitReader(resp.Body, 8192)) if err != nil { return types.ProxyTestResult{ Success: false, Latency: latency, Error: fmt.Sprintf("读取响应失败: %v", err), } } text := string(bodyBytes) ip, region := parseIpipResponse(text) if ip == "" { // 尝试正则抽取 IP m := regexp.MustCompile(`\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}`).FindString(text) ip = m } if ip == "" { return types.ProxyTestResult{ Success: false, Latency: latency, Error: fmt.Sprintf("无法识别出口 IP,返回内容: %s", truncateString(text, 80)), } } return types.ProxyTestResult{ Success: true, IP: ip, Region: region, Latency: latency, } } func parseIpipResponse(text string) (string, string) { // 示例: 当前 IP:115.230.134.174 来自于:中国 浙江 嘉兴 电信 text = strings.TrimSpace(text) ip := "" region := "" if idx := strings.Index(text, "当前 IP:"); idx != -1 { rest := text[idx+len("当前 IP:"):] parts := strings.SplitN(rest, "来自于:", 2) ip = strings.TrimSpace(parts[0]) if len(parts) > 1 { region = strings.TrimSpace(parts[1]) } } return ip, region } func parseCipResponse(text string) (string, string) { // IP : 115.230.134.174 // 地址 : 中国 浙江 嘉兴 lines := strings.Split(text, "\n") ip := "" region := "" for _, l := range lines { l = strings.TrimSpace(l) if strings.HasPrefix(l, "IP") && strings.Contains(l, ":") { parts := strings.SplitN(l, ":", 2) ip = strings.TrimSpace(parts[1]) } if strings.HasPrefix(l, "地址") && strings.Contains(l, ":") { parts := strings.SplitN(l, ":", 2) region = strings.TrimSpace(parts[1]) } } return ip, region } func formatProxyError(err error) string { msg := err.Error() if strings.Contains(msg, "407") { return "代理服务器要求身份认证(407):请检查是否已在代理后台将本机公网IP添加进白名单,或代理账密是否有误" } if strings.Contains(msg, "context deadline exceeded") || strings.Contains(msg, "timeout") { return "连接代理超时(timeout):代理节点可能已失效或网络不通" } if strings.Contains(msg, "connection refused") { return "代理连接被拒绝(connection refused):请核对代理地址与端口" } return msg } func truncateString(s string, maxLen int) string { r := []rune(s) if len(r) <= maxLen { return s } return string(r[:maxLen]) + "..." }