247 lines
5.1 KiB
Go
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)
|
|
}
|