Files
ProxyPool/internal/tester/http_tester.go
2026-01-31 22:53:12 +08:00

247 lines
5.1 KiB
Go

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)
}