frist
This commit is contained in:
246
internal/tester/http_tester.go
Normal file
246
internal/tester/http_tester.go
Normal file
@@ -0,0 +1,246 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user