package tester import ( "context" "crypto/tls" "fmt" "io" "net" "net/http" "net/url" "strings" "sync" "time" "golang.org/x/net/proxy" "proxyrotator/internal/model" ) // HTTPTester HTTP 代理测试器 type HTTPTester struct { maxBodySize int64 } // NewHTTPTester 创建测试器 func NewHTTPTester() *HTTPTester { return &HTTPTester{ maxBodySize: 1024 * 1024, // 1MB } } // TestOne 测试单个代理 func (t *HTTPTester) TestOne(ctx context.Context, p model.Proxy, spec model.TestSpec) model.TestResult { result := model.TestResult{ ProxyID: p.ID, CheckedAt: time.Now(), } start := time.Now() // 创建 HTTP 客户端 client, err := t.createClient(p, spec.Timeout) if err != nil { result.ErrorText = err.Error() result.LatencyMs = time.Since(start).Milliseconds() return result } // 创建请求 method := spec.Method if method == "" { method = "GET" } req, err := http.NewRequestWithContext(ctx, method, spec.URL, nil) if err != nil { result.ErrorText = fmt.Sprintf("create request failed: %v", err) result.LatencyMs = time.Since(start).Milliseconds() return result } req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") // 发起请求 resp, err := client.Do(req) if err != nil { result.ErrorText = err.Error() result.LatencyMs = time.Since(start).Milliseconds() return result } defer resp.Body.Close() result.LatencyMs = time.Since(start).Milliseconds() // 检查状态码 if len(spec.ExpectStatus) > 0 { found := false for _, s := range spec.ExpectStatus { if resp.StatusCode == s { found = true break } } if !found { result.ErrorText = fmt.Sprintf("unexpected status: %d", resp.StatusCode) return result } } // 检查响应体关键字 if spec.ExpectContains != "" { body, err := io.ReadAll(io.LimitReader(resp.Body, t.maxBodySize)) if err != nil { result.ErrorText = fmt.Sprintf("read body failed: %v", err) return result } if !strings.Contains(string(body), spec.ExpectContains) { result.ErrorText = "expected content not found" return result } } result.OK = true return result } // TestBatch 并发测试多个代理 func (t *HTTPTester) TestBatch(ctx context.Context, proxies []model.Proxy, spec model.TestSpec, concurrency int) []model.TestResult { if concurrency <= 0 { concurrency = 10 } jobs := make(chan model.Proxy, len(proxies)) results := make(chan model.TestResult, len(proxies)) // 启动 worker var wg sync.WaitGroup for i := 0; i < concurrency; i++ { wg.Add(1) go func() { defer wg.Done() for p := range jobs { select { case <-ctx.Done(): results <- model.TestResult{ ProxyID: p.ID, ErrorText: "context cancelled", CheckedAt: time.Now(), } default: results <- t.TestOne(ctx, p, spec) } } }() } // 发送任务 go func() { for _, p := range proxies { jobs <- p } close(jobs) }() // 等待完成 go func() { wg.Wait() close(results) }() // 收集结果 out := make([]model.TestResult, 0, len(proxies)) for r := range results { out = append(out, r) } return out } // createClient 根据代理类型创建 HTTP 客户端 func (t *HTTPTester) createClient(p model.Proxy, timeout time.Duration) (*http.Client, error) { if timeout <= 0 { timeout = 10 * time.Second } transport := &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, DisableKeepAlives: true, TLSHandshakeTimeout: timeout, ResponseHeaderTimeout: timeout, ExpectContinueTimeout: 1 * time.Second, // 代理连接设置 ProxyConnectHeader: http.Header{}, } switch p.Protocol { case model.ProtoHTTP, model.ProtoHTTPS: proxyURL := t.buildProxyURL(p) transport.Proxy = http.ProxyURL(proxyURL) case model.ProtoSOCKS5: dialer, err := t.createSOCKS5Dialer(p, timeout) if err != nil { return nil, err } transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { return dialer.Dial(network, addr) } default: return nil, fmt.Errorf("unsupported protocol: %s", p.Protocol) } return &http.Client{ Transport: transport, Timeout: timeout, // 不自动跟随重定向,让我们检查原始响应 CheckRedirect: func(req *http.Request, via []*http.Request) error { if len(via) >= 10 { return fmt.Errorf("too many redirects") } return nil }, }, nil } // buildProxyURL 构建代理 URL func (t *HTTPTester) buildProxyURL(p model.Proxy) *url.URL { scheme := "http" if p.Protocol == model.ProtoHTTPS { scheme = "https" } u := &url.URL{ Scheme: scheme, Host: fmt.Sprintf("%s:%d", p.Host, p.Port), } if p.Username != "" { u.User = url.UserPassword(p.Username, p.Password) } return u } // createSOCKS5Dialer 创建 SOCKS5 拨号器 func (t *HTTPTester) createSOCKS5Dialer(p model.Proxy, timeout time.Duration) (proxy.Dialer, error) { addr := fmt.Sprintf("%s:%d", p.Host, p.Port) var auth *proxy.Auth if p.Username != "" { auth = &proxy.Auth{ User: p.Username, Password: p.Password, } } // 创建基础拨号器带超时 baseDialer := &net.Dialer{ Timeout: timeout, } return proxy.SOCKS5("tcp", addr, auth, baseDialer) }