From 770f96e53f048d326c9466180a73880d148270bd Mon Sep 17 00:00:00 2001 From: hzb1115 Date: Wed, 5 Nov 2025 21:41:36 -0500 Subject: [PATCH] feat(proxy): add proxy module --- config.json.example | 17 + config/config.go | 17 + proxy/README.md | 685 +++++++++++++++++++++++++++++++++++ proxy/brightdata_provider.go | 105 ++++++ proxy/fixed_provider.go | 42 +++ proxy/provider.go | 10 + proxy/proxy_client.go | 47 +++ proxy/proxy_manager.go | 346 ++++++++++++++++++ proxy/single_provider.go | 19 + proxy/types.go | 40 ++ 10 files changed, 1328 insertions(+) create mode 100644 proxy/README.md create mode 100644 proxy/brightdata_provider.go create mode 100644 proxy/fixed_provider.go create mode 100644 proxy/provider.go create mode 100644 proxy/proxy_client.go create mode 100644 proxy/proxy_manager.go create mode 100644 proxy/single_provider.go create mode 100644 proxy/types.go diff --git a/config.json.example b/config.json.example index ac9d5ac6..f495b077 100644 --- a/config.json.example +++ b/config.json.example @@ -20,4 +20,21 @@ "max_drawdown": 20.0, "stop_trading_minutes": 60, "jwt_secret": "Qk0kAa+d0iIEzXVHXbNbm+UaN3RNabmWtH8rDWZ5OPf+4GX8pBflAHodfpbipVMyrw1fsDanHsNBjhgbDeK9Jg==" + + + "proxy": { + "enabled": false, + "mode": "single", + "timeout": 30, + "proxy_url": "http://127.0.0.1:7890", + "proxy_list": [], + "brightdata_endpoint": "", + "brightdata_token": "", + "brightdata_zone": "", + "proxy_host": "", + "proxy_user": "", + "proxy_password": "", + "refresh_interval": 0, + "blacklist_ttl": 5 + } } \ No newline at end of file diff --git a/config/config.go b/config/config.go index 37a537db..95a897b2 100644 --- a/config/config.go +++ b/config/config.go @@ -60,8 +60,25 @@ type Config struct { MaxDrawdown float64 `json:"max_drawdown"` StopTradingMinutes int `json:"stop_trading_minutes"` Leverage LeverageConfig `json:"leverage"` // 杠杆配置 + Proxy *ProxyConfig `json:"proxy"` // HTTP 代理配置(可选) } +// ProxyConfig HTTP 代理配置 +type ProxyConfig struct { + Enabled bool `json:"enabled"` // 是否启用代理 + Mode string `json:"mode"` // 模式: "single", "pool", "brightdata" + Timeout int `json:"timeout"` // 超时时间(秒) + ProxyURL string `json:"proxy_url"` // 单个代理地址 + ProxyList []string `json:"proxy_list"` // 代理列表 + BrightDataEndpoint string `json:"brightdata_endpoint"` // Bright Data接口地址 + BrightDataToken string `json:"brightdata_token"` // Bright Data访问令牌 + BrightDataZone string `json:"brightdata_zone"` // Bright Data区域 + ProxyHost string `json:"proxy_host"` // 代理主机 + ProxyUser string `json:"proxy_user"` // 代理用户名模板 + ProxyPassword string `json:"proxy_password"` // 代理密码 + RefreshInterval int `json:"refresh_interval"` // 刷新间隔(秒) + BlacklistTTL int `json:"blacklist_ttl"` // 黑名单TTL +} // LoadConfig 从文件加载配置 func LoadConfig(filename string) (*Config, error) { data, err := os.ReadFile(filename) diff --git a/proxy/README.md b/proxy/README.md new file mode 100644 index 00000000..f48a35d4 --- /dev/null +++ b/proxy/README.md @@ -0,0 +1,685 @@ +# HTTP 代理模块 + +## 概述 + +这是一个高度解耦的HTTP代理管理模块,专为解决高频API请求被限流/封禁问题而设计。支持单代理、代理池和动态IP获取三种模式,提供线程安全的IP轮换和智能黑名单管理机制。 + +## 功能特性 + +- ✅ **三种工作模式**:单代理、固定代理池、Bright Data API动态获取 +- ✅ **线程安全**:所有操作使用读写锁保护,支持并发访问 +- ✅ **智能黑名单**:失败的代理IP手动加入黑名单,TTL机制自动恢复 +- ✅ **自动刷新**:支持定时刷新代理IP列表(默认30分钟) +- ✅ **随机轮换**:从可用IP池中随机选择,避免单点压力 +- ✅ **防越界保护**:多层数组边界检查,确保运行时安全 +- ✅ **可选启用**:未配置或禁用时自动使用直连,不影响独立客户 + +## 架构设计 + +``` +proxy/ +├── README.md # 本文档 +├── types.go # 核心数据结构定义 +├── provider.go # IP提供者接口定义 +├── single_provider.go # 单代理实现 +├── fixed_provider.go # 固定代理池实现 +├── brightdata_provider.go # Bright Data API实现 +└── proxy_manager.go # 代理管理器(核心逻辑) +``` + +### 设计原则 + +1. **接口抽象**:通过 `IPProvider` 接口实现不同代理源的统一管理 +2. **策略模式**:三种Provider实现可灵活切换 +3. **单例模式**:全局ProxyManager确保资源统一管理 +4. **防御性编程**:多层边界检查,优雅处理异常情况 + +## 配置说明 + +在 `config.json` 中添加 `proxy` 配置段: + +```json +{ + "proxy": { + "enabled": true, + "mode": "single", + "timeout": 30, + "proxy_url": "http://127.0.0.1:7890", + "proxy_list": [], + "brightdata_endpoint": "", + "brightdata_token": "", + "brightdata_zone": "", + "proxy_host": "", + "proxy_user": "", + "proxy_password": "", + "refresh_interval": 1800, + "blacklist_ttl": 5 + } +} +``` + +### 配置字段详解 + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `enabled` | bool | 是 | 是否启用代理(false时使用直连) | +| `mode` | string | 是 | 代理模式:`single`/`pool`/`brightdata` | +| `timeout` | int | 否 | HTTP请求超时时间(秒),默认30 | +| `proxy_url` | string | single模式必填 | 单个代理地址,如 `http://127.0.0.1:7890` | +| `proxy_list` | []string | pool模式必填 | 代理列表,支持 `http://`、`https://`、`socks5://` | +| `brightdata_endpoint` | string | brightdata模式必填 | Bright Data API端点 | +| `brightdata_token` | string | brightdata模式可选 | Bright Data访问令牌 | +| `brightdata_zone` | string | brightdata模式可选 | Bright Data区域参数 | +| `proxy_host` | string | 否 | 代理主机(用于认证代理) | +| `proxy_user` | string | 否 | 代理用户名模板,支持 `%s` 占位符替换IP | +| `proxy_password` | string | 否 | 代理密码 | +| `refresh_interval` | int | 否 | IP列表刷新间隔(秒),brightdata模式默认1800(30分钟) | +| `blacklist_ttl` | int | 否 | 黑名单IP的TTL(刷新次数),默认5 | + +## 使用方法 + +### 1. 初始化代理管理器 + +在 `main.go` 或初始化代码中: + +```go +import ( + "nofx/proxy" + "time" +) + +// 方式1:使用配置结构体初始化 +proxyConfig := &proxy.Config{ + Enabled: true, + Mode: "single", + Timeout: 30 * time.Second, + ProxyURL: "http://127.0.0.1:7890", + BlacklistTTL: 5, +} + +err := proxy.InitGlobalProxyManager(proxyConfig) +if err != nil { + log.Fatalf("初始化代理管理器失败: %v", err) +} +``` + +### 2. 获取代理HTTP客户端 + +在需要发送HTTP请求的地方: + +```go +// 获取代理客户端(包含ProxyID用于黑名单管理) +proxyClient, err := proxy.GetProxyHTTPClient() +if err != nil { + log.Printf("获取代理客户端失败: %v", err) + return +} + +// 使用代理客户端发送请求 +resp, err := proxyClient.Client.Get("https://api.example.com/data") +if err != nil { + // 请求失败,将此代理加入黑名单 + proxy.AddBlacklist(proxyClient.ProxyID) + log.Printf("请求失败,代理IP %s 已加入黑名单", proxyClient.IP) + return +} +defer resp.Body.Close() + +// 处理响应... +``` + +### 3. 黑名单管理 + +```go +// 添加失败的代理到黑名单 +proxy.AddBlacklist(proxyClient.ProxyID) + +// 获取黑名单状态 +total, blacklisted, available := proxy.GetGlobalProxyManager().GetBlacklistStatus() +log.Printf("代理状态: 总计%d个,黑名单%d个,可用%d个", total, blacklisted, available) +``` + +### 4. 手动刷新IP列表 + +```go +err := proxy.RefreshIPList() +if err != nil { + log.Printf("刷新IP列表失败: %v", err) +} +``` + +### 5. 检查代理是否启用 + +```go +if proxy.IsEnabled() { + log.Println("代理已启用") +} else { + log.Println("代理未启用,使用直连") +} +``` + +## 三种模式详解 + +### Mode 1: Single(单代理模式) + +适用场景:本地代理工具(如Clash、V2Ray)或单个固定代理服务器 + +```json +{ + "proxy": { + "enabled": true, + "mode": "single", + "proxy_url": "http://127.0.0.1:7890" + } +} +``` + +特点: +- 简单直接,适合本地开发和测试 +- 所有请求通过同一个代理 +- 不需要刷新和轮换 + +### Mode 2: Pool(代理池模式) + +适用场景:拥有多个固定代理服务器,需要轮换使用 + +```json +{ + "proxy": { + "enabled": true, + "mode": "pool", + "proxy_list": [ + "http://proxy1.example.com:8080", + "http://user:pass@proxy2.example.com:8080", + "socks5://proxy3.example.com:1080" + ], + "blacklist_ttl": 5 + } +} +``` + +特点: +- 支持多协议:HTTP、HTTPS、SOCKS5 +- 随机选择代理,分散请求压力 +- 失败的代理自动加入黑名单 +- 黑名单IP经过TTL次刷新后自动恢复 + +### Mode 3: BrightData(动态IP模式) + +适用场景:使用Bright Data等提供API的动态代理服务 + +```json +{ + "proxy": { + "enabled": true, + "mode": "brightdata", + "brightdata_endpoint": "https://api.brightdata.com/zones/get_ips", + "brightdata_token": "your_api_token", + "brightdata_zone": "residential", + "proxy_host": "brd.superproxy.io:22225", + "proxy_user": "brd-customer-xxx-zone-residential-ip-%s", + "proxy_password": "your_password", + "refresh_interval": 1800, + "blacklist_ttl": 5 + } +} +``` + +特点: +- 从API动态获取可用IP列表 +- 自动定时刷新(默认30分钟) +- 支持用户名模板(`%s` 替换为IP地址) +- 黑名单TTL机制避免频繁切换 + +**用户名模板说明**: +``` +proxy_user: "brd-customer-xxx-zone-residential-ip-%s" + ↑ + 自动替换为IP地址 +``` + +## 核心API + +### 全局函数 + +```go +// 初始化全局代理管理器(只执行一次) +func InitGlobalProxyManager(config *Config) error + +// 获取全局代理管理器实例 +func GetGlobalProxyManager() *ProxyManager + +// 获取代理HTTP客户端(包含ProxyID和IP信息) +func GetProxyHTTPClient() (*ProxyClient, error) + +// 将代理IP添加到黑名单 +func AddBlacklist(proxyID int) + +// 刷新IP列表 +func RefreshIPList() error + +// 检查代理是否启用 +func IsEnabled() bool +``` + +### ProxyManager 方法 + +```go +// 获取代理客户端 +func (m *ProxyManager) GetProxyClient() (*ProxyClient, error) + +// 刷新IP列表 +func (m *ProxyManager) RefreshIPList() error + +// 添加到黑名单 +func (m *ProxyManager) AddBlacklist(proxyID int) + +// 获取黑名单状态 +func (m *ProxyManager) GetBlacklistStatus() (total, blacklisted, available int) + +// 启动自动刷新 +func (m *ProxyManager) StartAutoRefresh() + +// 停止自动刷新 +func (m *ProxyManager) StopAutoRefresh() +``` + +## 黑名单机制 + +### 工作原理 + +1. **添加黑名单**:当代理请求失败时,调用 `AddBlacklist(proxyID)` 将该IP加入黑名单 +2. **TTL倒计时**:每次刷新IP列表时,黑名单中的IP的TTL减1 +3. **自动恢复**:当TTL归零时,IP自动从黑名单移除,重新可用 + +### 线程安全保证 + +```go +// 添加黑名单使用写锁 +func (m *ProxyManager) AddBlacklist(proxyID int) { + m.mutex.Lock() + defer m.mutex.Unlock() + + // 防越界检查 + if proxyID < 0 || proxyID >= len(m.ipList) { + log.Printf("⚠️ 无效的 ProxyID: %d", proxyID) + return + } + + ip := m.ipList[proxyID].IP + m.blacklist[proxyID] = ip + m.ipBlacklist[ip] = m.config.BlacklistTTL +} + +// 获取代理使用读锁(支持并发) +func (m *ProxyManager) getRandomProxy() (int, *ProxyIP, error) { + m.mutex.RLock() + defer m.mutex.RUnlock() + // ... 读取操作 +} +``` + +### 示例流程 + +``` +初始状态:5个代理IP,TTL=3 +IP列表: [IP1, IP2, IP3, IP4, IP5] +黑名单: {} + +第1次失败:IP2请求失败 +IP列表: [IP1, IP2, IP3, IP4, IP5] +黑名单: {IP2: TTL=3} + +第1次刷新:TTL-1 +黑名单: {IP2: TTL=2} + +第2次刷新:TTL-1 +黑名单: {IP2: TTL=1} + +第3次刷新:TTL-1 +黑名单: {IP2: TTL=0} → 从黑名单移除 + +第3次刷新后: +IP列表: [IP1, IP2, IP3, IP4, IP5] +黑名单: {} ← IP2已恢复可用 +``` + +## 完整使用示例 + +### 示例1:币安API请求(单代理模式) + +```go +package main + +import ( + "log" + "nofx/proxy" + "time" +) + +func main() { + // 初始化代理 + err := proxy.InitGlobalProxyManager(&proxy.Config{ + Enabled: true, + Mode: "single", + ProxyURL: "http://127.0.0.1:7890", + Timeout: 30 * time.Second, + }) + if err != nil { + log.Fatalf("初始化代理失败: %v", err) + } + + // 获取币安数据 + proxyClient, err := proxy.GetProxyHTTPClient() + if err != nil { + log.Fatalf("获取代理客户端失败: %v", err) + } + + resp, err := proxyClient.Client.Get("https://fapi.binance.com/fapi/v1/ticker/24hr") + if err != nil { + log.Printf("请求失败: %v", err) + return + } + defer resp.Body.Close() + + log.Printf("请求成功,使用代理: %s", proxyClient.IP) +} +``` + +### 示例2:OI数据获取(代理池模式 + 黑名单) + +```go +package main + +import ( + "fmt" + "io" + "log" + "nofx/proxy" + "time" +) + +func fetchOIData(symbol string) error { + proxyClient, err := proxy.GetProxyHTTPClient() + if err != nil { + return fmt.Errorf("获取代理失败: %w", err) + } + + url := fmt.Sprintf("https://fapi.binance.com/futures/data/openInterestHist?symbol=%s&period=5m&limit=1", symbol) + resp, err := proxyClient.Client.Get(url) + if err != nil { + // 请求失败,加入黑名单 + proxy.AddBlacklist(proxyClient.ProxyID) + return fmt.Errorf("请求失败 (代理: %s): %w", proxyClient.IP, err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + // 状态码异常,加入黑名单 + proxy.AddBlacklist(proxyClient.ProxyID) + return fmt.Errorf("状态码异常: %d (代理: %s)", resp.StatusCode, proxyClient.IP) + } + + body, _ := io.ReadAll(resp.Body) + log.Printf("✓ 获取 %s OI数据成功 (代理: %s): %s", symbol, proxyClient.IP, string(body)) + return nil +} + +func main() { + // 初始化代理池 + err := proxy.InitGlobalProxyManager(&proxy.Config{ + Enabled: true, + Mode: "pool", + ProxyList: []string{ + "http://proxy1.example.com:8080", + "http://proxy2.example.com:8080", + "http://proxy3.example.com:8080", + }, + Timeout: 30 * time.Second, + BlacklistTTL: 5, + }) + if err != nil { + log.Fatalf("初始化代理失败: %v", err) + } + + // 循环获取数据 + symbols := []string{"BTCUSDT", "ETHUSDT", "SOLUSDT"} + for { + for _, symbol := range symbols { + if err := fetchOIData(symbol); err != nil { + log.Printf("⚠️ %v", err) + } + time.Sleep(1 * time.Second) + } + time.Sleep(10 * time.Second) + } +} +``` + +### 示例3:Bright Data动态IP + +```go +package main + +import ( + "log" + "nofx/proxy" + "time" +) + +func main() { + // 初始化Bright Data代理 + err := proxy.InitGlobalProxyManager(&proxy.Config{ + Enabled: true, + Mode: "brightdata", + BrightDataEndpoint: "https://api.brightdata.com/zones/get_ips", + BrightDataToken: "your_token", + BrightDataZone: "residential", + ProxyHost: "brd.superproxy.io:22225", + ProxyUser: "brd-customer-xxx-zone-residential-ip-%s", + ProxyPassword: "your_password", + RefreshInterval: 30 * time.Minute, + Timeout: 30 * time.Second, + BlacklistTTL: 5, + }) + if err != nil { + log.Fatalf("初始化代理失败: %v", err) + } + + // 代理会自动每30分钟刷新IP列表 + log.Println("✓ Bright Data代理已启动,自动刷新已开启") + + // 获取并使用代理 + for i := 0; i < 10; i++ { + proxyClient, err := proxy.GetProxyHTTPClient() + if err != nil { + log.Printf("获取代理失败: %v", err) + continue + } + + resp, err := proxyClient.Client.Get("https://api.ipify.org?format=json") + if err != nil { + proxy.AddBlacklist(proxyClient.ProxyID) + log.Printf("请求失败,代理已加入黑名单: %s", proxyClient.IP) + continue + } + resp.Body.Close() + + log.Printf("✓ 请求成功 (代理ID: %d, IP: %s)", proxyClient.ProxyID, proxyClient.IP) + time.Sleep(2 * time.Second) + } +} +``` + +## 注意事项 + +### 1. 模块解耦性 + +- ✅ 代理模块完全独立,不依赖其他业务模块 +- ✅ 禁用代理时自动使用直连,对业务代码透明 +- ✅ 适合多租户/多客户环境,可按需启用 + +### 2. 线程安全 + +- ✅ 所有公开方法都是线程安全的 +- ✅ 支持高并发场景下的代理获取和黑名单操作 +- ✅ 读写锁优化性能:读操作可并发,写操作独占 + +### 3. 错误处理 + +```go +proxyClient, err := proxy.GetProxyHTTPClient() +if err != nil { + // 可能的错误: + // - 代理IP列表为空 + // - 所有代理都在黑名单中 + // - 代理URL解析失败 + log.Printf("获取代理失败: %v", err) + + // 建议:降级为直连或重试 + return +} +``` + +### 4. 性能优化建议 + +- 对于高频请求,复用 `http.Client` 而不是每次创建新的 +- 合理设置 `refresh_interval` 避免频繁刷新 +- `blacklist_ttl` 建议设置为 3-10,平衡恢复速度和稳定性 + +### 5. 安全建议 + +- 生产环境中代理密钥应使用环境变量或密钥管理服务 +- 避免在日志中打印完整的代理URL(包含密码) +- TLS验证默认开启,如需跳过请谨慎评估风险 + +### 6. 调试技巧 + +```go +// 获取当前代理状态 +total, blacklisted, available := proxy.GetGlobalProxyManager().GetBlacklistStatus() +log.Printf("代理池状态: 总计=%d, 黑名单=%d, 可用=%d", total, blacklisted, available) + +// 检查是否启用 +if !proxy.IsEnabled() { + log.Println("代理未启用,请检查配置") +} +``` + +## 故障排查 + +### 问题1:获取代理失败 - "代理IP列表为空" + +**原因**: +- `single` 模式:未配置 `proxy_url` +- `pool` 模式:`proxy_list` 为空 +- `brightdata` 模式:API返回空列表或请求失败 + +**解决方案**: +```bash +# 检查配置文件 +cat config.json | grep -A 15 "proxy" + +# 检查日志,查看初始化信息 +# 应该看到类似:🌐 HTTP 代理已启用 (xxx模式) +``` + +### 问题2:所有代理都在黑名单中 + +**原因**:请求持续失败,所有IP被加入黑名单 + +**解决方案**: +```go +// 方案1:手动刷新IP列表(会触发TTL倒计时) +proxy.RefreshIPList() + +// 方案2:降低blacklist_ttl,加快恢复速度 +// config.json: "blacklist_ttl": 2 (默认5) + +// 方案3:检查代理本身是否可用 +// 使用curl测试代理: +// curl -x http://proxy_url https://api.binance.com/api/v3/ping +``` + +### 问题3:Bright Data模式无法获取IP + +**原因**: +- API端点配置错误 +- Token无效或过期 +- Zone参数不正确 + +**解决方案**: +```bash +# 手动测试API +curl -H "Authorization: Bearer YOUR_TOKEN" \ + "https://api.brightdata.com/zones/get_ips?zone=residential" + +# 检查返回格式是否符合: +# {"ips": [{"ip": "1.2.3.4", ...}, ...]} +``` + +### 问题4:代理连接超时 + +**原因**:代理服务器响应慢或网络不稳定 + +**解决方案**: +```json +{ + "proxy": { + "timeout": 60 // 增加超时时间(秒) + } +} +``` + +## 扩展开发 + +### 添加新的Provider + +实现 `IPProvider` 接口即可: + +```go +// custom_provider.go +package proxy + +type CustomProvider struct { + // 自定义字段 +} + +func NewCustomProvider(config string) *CustomProvider { + return &CustomProvider{} +} + +func (p *CustomProvider) GetIPList() ([]ProxyIP, error) { + // 实现获取IP列表的逻辑 + return []ProxyIP{}, nil +} + +func (p *CustomProvider) RefreshIPList() ([]ProxyIP, error) { + // 实现刷新IP列表的逻辑 + return p.GetIPList() +} +``` + +然后在 `proxy_manager.go` 的 `NewProxyManager` 中添加新模式: + +```go +case "custom": + m.provider = NewCustomProvider(config.CustomEndpoint) + log.Printf("🌐 HTTP 代理已启用 (自定义模式)") +``` + +## 更新日志 + +### v1.0.0 (当前版本) +- ✅ 支持三种代理模式:single、pool、brightdata +- ✅ 线程安全的IP轮换和黑名单管理 +- ✅ 自动刷新机制(30分钟默认) +- ✅ TTL黑名单自动恢复 +- ✅ 防越界保护 +- ✅ ProxyID追踪机制 + + +## 技术支持 + +如有问题或建议,请联系项目维护者 @hzb1115 +。 diff --git a/proxy/brightdata_provider.go b/proxy/brightdata_provider.go new file mode 100644 index 00000000..e8febd55 --- /dev/null +++ b/proxy/brightdata_provider.go @@ -0,0 +1,105 @@ +package proxy + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "time" +) + +// BrightDataProvider Bright Data动态获取IP提供者 +type BrightDataProvider struct { + endpoint string + token string + zone string + client *http.Client +} + +// NewBrightDataProvider 创建Bright Data IP提供者 +func NewBrightDataProvider(endpoint, token, zone string) *BrightDataProvider { + return &BrightDataProvider{ + endpoint: endpoint, + token: token, + zone: zone, + client: &http.Client{ + Timeout: 30 * time.Second, + }, + } +} + +// BrightDataIPList Bright Data API返回的IP列表结构 +type BrightDataIPList struct { + IPs []struct { + IP string `json:"ip"` + Maxmind string `json:"maxmind"` + Ext map[string]interface{} `json:"ext"` + } `json:"ips"` +} + +func (p *BrightDataProvider) GetIPList() ([]ProxyIP, error) { + return p.fetchIPList() +} + +func (p *BrightDataProvider) RefreshIPList() ([]ProxyIP, error) { + return p.fetchIPList() +} + +func (p *BrightDataProvider) fetchIPList() ([]ProxyIP, error) { + // 构建请求URL + url := p.endpoint + if p.zone != "" { + url = fmt.Sprintf("%s?zone=%s", p.endpoint, p.zone) + } + + // 创建HTTP请求 + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, fmt.Errorf("创建HTTP请求失败: %w", err) + } + + // 设置授权头 + if p.token != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", p.token)) + } + + // 发送请求 + resp, err := p.client.Do(req) + if err != nil { + return nil, fmt.Errorf("发送HTTP请求失败: %w", err) + } + defer resp.Body.Close() + + // 读取响应体 + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("读取HTTP响应失败: %w", err) + } + + // 检查状态码 + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("API返回错误状态码 %d: %s", resp.StatusCode, string(body)) + } + + // 解析JSON数据(支持Bright Data格式) + var ipList BrightDataIPList + if err := json.Unmarshal(body, &ipList); err != nil { + return nil, fmt.Errorf("解析JSON数据失败: %w", err) + } + + // 转换为ProxyIP列表 + result := make([]ProxyIP, 0, len(ipList.IPs)) + for _, ip := range ipList.IPs { + result = append(result, ProxyIP{ + IP: ip.IP, + Protocol: "http", + Ext: ip.Ext, + }) + } + + if len(result) == 0 { + return nil, fmt.Errorf("API返回的IP列表为空") + } + + return result, nil +} diff --git a/proxy/fixed_provider.go b/proxy/fixed_provider.go new file mode 100644 index 00000000..267b047e --- /dev/null +++ b/proxy/fixed_provider.go @@ -0,0 +1,42 @@ +package proxy + +import "strings" + +// FixedIPProvider 固定IP列表提供者 +type FixedIPProvider struct { + ips []ProxyIP +} + +// NewFixedIPProvider 创建固定IP列表提供者 +func NewFixedIPProvider(proxyURLs []string) *FixedIPProvider { + ips := make([]ProxyIP, 0, len(proxyURLs)) + for _, proxyURL := range proxyURLs { + // 简单解析代理URL + // 格式: http://ip:port 或 socks5://user:pass@ip:port + protocol := "http" + if strings.HasPrefix(proxyURL, "socks5://") { + protocol = "socks5" + proxyURL = strings.TrimPrefix(proxyURL, "socks5://") + } else if strings.HasPrefix(proxyURL, "http://") { + proxyURL = strings.TrimPrefix(proxyURL, "http://") + } else if strings.HasPrefix(proxyURL, "https://") { + protocol = "https" + proxyURL = strings.TrimPrefix(proxyURL, "https://") + } + + ips = append(ips, ProxyIP{ + IP: proxyURL, + Protocol: protocol, + }) + } + + return &FixedIPProvider{ips: ips} +} + +func (p *FixedIPProvider) GetIPList() ([]ProxyIP, error) { + return p.ips, nil +} + +func (p *FixedIPProvider) RefreshIPList() ([]ProxyIP, error) { + return p.ips, nil +} diff --git a/proxy/provider.go b/proxy/provider.go new file mode 100644 index 00000000..b4d6e06d --- /dev/null +++ b/proxy/provider.go @@ -0,0 +1,10 @@ +package proxy + +// IPProvider IP提供者接口 +type IPProvider interface { + // GetIPList 获取IP列表 + GetIPList() ([]ProxyIP, error) + + // RefreshIPList 刷新IP列表(可选实现) + RefreshIPList() ([]ProxyIP, error) +} diff --git a/proxy/proxy_client.go b/proxy/proxy_client.go new file mode 100644 index 00000000..cda50b00 --- /dev/null +++ b/proxy/proxy_client.go @@ -0,0 +1,47 @@ +package proxy + +import ( + "log" + "net/http" + "time" +) + +// --- 便捷函数(直接使用全局管理器) --- + +// GetProxyHTTPClient 获取代理 HTTP 客户端(返回 ProxyClient,包含 ProxyID) +func GetProxyHTTPClient() (*ProxyClient, error) { + return GetGlobalProxyManager().GetProxyClient() +} + +// NewHTTPClient 创建一个新的HTTP客户端(使用全局代理配置) +// 注意:不返回 ProxyID,如需 ProxyID 请使用 GetProxyHTTPClient() +func NewHTTPClient() *http.Client { + client, err := GetGlobalProxyManager().GetProxyClient() + if err != nil { + log.Printf("⚠️ 获取代理客户端失败,使用直连: %v", err) + return &http.Client{Timeout: 30 * time.Second} + } + return client.Client +} + +// NewHTTPClientWithTimeout 创建一个新的HTTP客户端并指定超时时间 +// 注意:不返回 ProxyID,如需 ProxyID 请使用 GetProxyHTTPClient() +func NewHTTPClientWithTimeout(timeout time.Duration) *http.Client { + client, err := GetGlobalProxyManager().GetProxyClient() + if err != nil { + log.Printf("⚠️ 获取代理客户端失败,使用直连: %v", err) + return &http.Client{Timeout: timeout} + } + client.Client.Timeout = timeout + return client.Client +} + +// GetTransport 获取HTTP Transport +func GetTransport() *http.Transport { + client, err := GetGlobalProxyManager().GetProxyClient() + if err != nil { + log.Printf("⚠️ 获取代理客户端失败,使用直连: %v", err) + return &http.Transport{} + } + return client.Client.Transport.(*http.Transport) +} \ No newline at end of file diff --git a/proxy/proxy_manager.go b/proxy/proxy_manager.go new file mode 100644 index 00000000..aaca00e4 --- /dev/null +++ b/proxy/proxy_manager.go @@ -0,0 +1,346 @@ +package proxy + +import ( + "crypto/tls" + "fmt" + "log" + "math/rand" + "net/http" + "net/url" + "sync" + "time" +) + +// ProxyManager 代理管理器 +type ProxyManager struct { + config *Config + provider IPProvider + + // IP池管理 + ipList []ProxyIP + blacklist map[int]string // ProxyID -> IP + ipBlacklist map[string]int // IP -> 剩余TTL + mutex sync.RWMutex // 读写锁,保证线程安全 + + // 刷新控制 + stopRefresh chan struct{} +} + +var ( + globalProxyManager *ProxyManager + once sync.Once +) + +// InitGlobalProxyManager 初始化全局代理管理器 +func InitGlobalProxyManager(config *Config) error { + var err error + once.Do(func() { + globalProxyManager, err = NewProxyManager(config) + if err == nil && config.Enabled && config.RefreshInterval > 0 { + globalProxyManager.StartAutoRefresh() + } + }) + return err +} + +// GetGlobalProxyManager 获取全局代理管理器 +func GetGlobalProxyManager() *ProxyManager { + if globalProxyManager == nil { + // 如果未初始化,使用默认配置(禁用代理) + _ = InitGlobalProxyManager(&Config{Enabled: false}) + } + return globalProxyManager +} + +// NewProxyManager 创建代理管理器 +func NewProxyManager(config *Config) (*ProxyManager, error) { + if config == nil { + config = &Config{Enabled: false} + } + + // 设置默认值 + if config.Timeout == 0 { + config.Timeout = 30 * time.Second + } + if config.BlacklistTTL == 0 { + config.BlacklistTTL = 5 // 默认 TTL 为 5 次刷新 + } + if config.RefreshInterval == 0 && config.Mode == "brightdata" { + config.RefreshInterval = 30 * time.Minute // 默认 30 分钟刷新一次 + } + + m := &ProxyManager{ + config: config, + blacklist: make(map[int]string), + ipBlacklist: make(map[string]int), + stopRefresh: make(chan struct{}), + } + + // 如果未启用代理,直接返回 + if !config.Enabled { + log.Printf("🌐 HTTP 代理未启用,使用直连") + return m, nil + } + + // 根据模式选择IP提供者 + switch config.Mode { + case "single": + // 单个代理模式 + if config.ProxyURL == "" { + return nil, fmt.Errorf("single模式下必须配置proxy_url") + } + m.provider = NewSingleProxyProvider(config.ProxyURL) + log.Printf("🌐 HTTP 代理已启用 (单代理模式): %s", config.ProxyURL) + + case "pool": + // 代理池模式(固定列表) + if len(config.ProxyList) == 0 { + return nil, fmt.Errorf("pool模式下必须配置proxy_list") + } + m.provider = NewFixedIPProvider(config.ProxyList) + log.Printf("🌐 HTTP 代理已启用 (代理池模式): %d个代理", len(config.ProxyList)) + + case "brightdata": + // Bright Data动态获取模式 + if config.BrightDataEndpoint == "" { + return nil, fmt.Errorf("brightdata模式下必须配置brightdata_endpoint") + } + m.provider = NewBrightDataProvider(config.BrightDataEndpoint, config.BrightDataToken, config.BrightDataZone) + log.Printf("🌐 HTTP 代理已启用 (Bright Data模式): %s", config.BrightDataEndpoint) + + default: + // 默认使用single模式 + if config.ProxyURL == "" { + return nil, fmt.Errorf("未知的proxy模式: %s", config.Mode) + } + m.provider = NewSingleProxyProvider(config.ProxyURL) + log.Printf("🌐 HTTP 代理已启用 (默认模式): %s", config.ProxyURL) + } + + // 初始化IP列表 + if err := m.RefreshIPList(); err != nil { + return nil, fmt.Errorf("初始化IP列表失败: %w", err) + } + + return m, nil +} + +// RefreshIPList 刷新IP列表(线程安全) +func (m *ProxyManager) RefreshIPList() error { + if m.provider == nil { + return nil + } + + ips, err := m.provider.RefreshIPList() + if err != nil { + return err + } + + m.mutex.Lock() + defer m.mutex.Unlock() + + // 清理黑名单,TTL倒计时 + validIPs := make([]ProxyIP, 0, len(ips)) + newBlacklist := make(map[int]string) + + for _, ip := range ips { + if ttl, inBlacklist := m.ipBlacklist[ip.IP]; inBlacklist { + // TTL 倒计时 + m.ipBlacklist[ip.IP] = ttl - 1 + if ttl > 0 { + // 仍在黑名单中,跳过 + continue + } + // TTL 归零,从黑名单移除 + delete(m.ipBlacklist, ip.IP) + log.Printf("✓ 代理IP已从黑名单恢复: %s", ip.IP) + } + validIPs = append(validIPs, ip) + } + + m.ipList = validIPs + m.blacklist = newBlacklist + + log.Printf("✓ 刷新代理IP列表: 总计%d个,黑名单%d个,可用%d个", + len(ips), len(m.ipBlacklist), len(validIPs)) + + return nil +} + +// StartAutoRefresh 启动自动刷新 +func (m *ProxyManager) StartAutoRefresh() { + if m.config.RefreshInterval <= 0 { + return + } + + go func() { + ticker := time.NewTicker(m.config.RefreshInterval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + if err := m.RefreshIPList(); err != nil { + log.Printf("⚠️ 自动刷新IP列表失败: %v", err) + } + case <-m.stopRefresh: + return + } + } + }() + + log.Printf("✓ 已启动代理IP自动刷新 (间隔: %v)", m.config.RefreshInterval) +} + +// StopAutoRefresh 停止自动刷新 +func (m *ProxyManager) StopAutoRefresh() { + close(m.stopRefresh) +} + +// getRandomProxy 随机获取一个可用代理(线程安全 - 读锁,确保不越界) +func (m *ProxyManager) getRandomProxy() (int, *ProxyIP, error) { + m.mutex.RLock() + defer m.mutex.RUnlock() + + if len(m.ipList) == 0 { + return -1, nil, fmt.Errorf("代理IP列表为空") + } + + // 找到所有未被黑名单的索引 + availableIndices := make([]int, 0, len(m.ipList)) + for i := range m.ipList { + if _, inBlacklist := m.blacklist[i]; !inBlacklist { + availableIndices = append(availableIndices, i) + } + } + + if len(availableIndices) == 0 { + return -1, nil, fmt.Errorf("所有代理IP都在黑名单中") + } + + // 随机选择一个(确保不越界) + randomIdx := availableIndices[rand.Intn(len(availableIndices))] + + // 二次检查,确保索引有效(防御性编程) + if randomIdx < 0 || randomIdx >= len(m.ipList) { + return -1, nil, fmt.Errorf("代理索引越界: %d (总数: %d)", randomIdx, len(m.ipList)) + } + + return randomIdx, &m.ipList[randomIdx], nil +} + +// buildProxyURL 构建代理URL +func (m *ProxyManager) buildProxyURL(ip *ProxyIP) string { + if m.config.ProxyHost != "" && m.config.ProxyUser != "" { + // 使用配置的代理主机和认证信息 + user := m.config.ProxyUser + if m.config.ProxyUser != "" && ip.IP != "" { + // 支持%s占位符替换IP + user = fmt.Sprintf(m.config.ProxyUser, ip.IP) + } + + protocol := ip.Protocol + if protocol == "" { + protocol = "http" + } + + if m.config.ProxyPassword != "" { + return fmt.Sprintf("%s://%s:%s@%s", protocol, user, m.config.ProxyPassword, m.config.ProxyHost) + } + return fmt.Sprintf("%s://%s@%s", protocol, user, m.config.ProxyHost) + } + + // 直接使用IP信息 + return ip.IP +} + +// GetProxyClient 获取代理客户端(线程安全) +func (m *ProxyManager) GetProxyClient() (*ProxyClient, error) { + if !m.config.Enabled { + // 未启用代理,返回普通HTTP客户端 + return &ProxyClient{ + ProxyID: -1, // -1 表示未使用代理 + IP: "direct", + Client: &http.Client{ + Timeout: m.config.Timeout, + }, + }, nil + } + + // 获取随机代理(使用读锁,确保不越界) + proxyID, proxyIP, err := m.getRandomProxy() + if err != nil { + return nil, err + } + + // 构建代理URL + proxyURLStr := m.buildProxyURL(proxyIP) + proxyURL, err := url.Parse(proxyURLStr) + if err != nil { + return nil, fmt.Errorf("解析代理URL失败: %w", err) + } + + // 创建Transport + transport := &http.Transport{ + Proxy: http.ProxyURL(proxyURL), + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: false, + }, + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 90 * time.Second, + } + + return &ProxyClient{ + ProxyID: proxyID, + IP: proxyIP.IP, + Client: &http.Client{ + Transport: transport, + Timeout: m.config.Timeout, + }, + }, nil +} + +// AddBlacklist 将代理IP添加到黑名单(线程安全 - 写锁) +func (m *ProxyManager) AddBlacklist(proxyID int) { + m.mutex.Lock() + defer m.mutex.Unlock() + + // 检查 proxyID 有效性,防止越界 + if proxyID < 0 || proxyID >= len(m.ipList) { + log.Printf("⚠️ 无效的 ProxyID: %d (有效范围: 0-%d)", proxyID, len(m.ipList)-1) + return + } + + ip := m.ipList[proxyID].IP + m.blacklist[proxyID] = ip + m.ipBlacklist[ip] = m.config.BlacklistTTL + + log.Printf("⚠️ 代理IP已加入黑名单: %s (ProxyID: %d, TTL: %d)", ip, proxyID, m.config.BlacklistTTL) +} + +// GetBlacklistStatus 获取黑名单状态(线程安全 - 读锁) +func (m *ProxyManager) GetBlacklistStatus() (total int, blacklisted int, available int) { + m.mutex.RLock() + defer m.mutex.RUnlock() + + total = len(m.ipList) + blacklisted = len(m.ipBlacklist) + available = total - len(m.blacklist) + return +} + +// IsEnabled 检查代理是否启用 +func IsEnabled() bool { + return GetGlobalProxyManager().config.Enabled +} + +// RefreshIPList 刷新全局代理IP列表 +func RefreshIPList() error { + return GetGlobalProxyManager().RefreshIPList() +} + +// AddBlacklist 将代理IP添加到全局黑名单 +func AddBlacklist(proxyID int) { + GetGlobalProxyManager().AddBlacklist(proxyID) +} diff --git a/proxy/single_provider.go b/proxy/single_provider.go new file mode 100644 index 00000000..bbea9fce --- /dev/null +++ b/proxy/single_provider.go @@ -0,0 +1,19 @@ +package proxy + +// SingleProxyProvider 单个代理提供者(不使用IP池) +type SingleProxyProvider struct { + proxyURL string +} + +// NewSingleProxyProvider 创建单个代理提供者 +func NewSingleProxyProvider(proxyURL string) *SingleProxyProvider { + return &SingleProxyProvider{proxyURL: proxyURL} +} + +func (p *SingleProxyProvider) GetIPList() ([]ProxyIP, error) { + return []ProxyIP{{IP: p.proxyURL}}, nil +} + +func (p *SingleProxyProvider) RefreshIPList() ([]ProxyIP, error) { + return p.GetIPList() +} diff --git a/proxy/types.go b/proxy/types.go new file mode 100644 index 00000000..89678c86 --- /dev/null +++ b/proxy/types.go @@ -0,0 +1,40 @@ +package proxy + +import ( + "net/http" + "time" +) + +// ProxyIP 代理IP信息 +type ProxyIP struct { + IP string `json:"ip"` // IP地址 + Port string `json:"port"` // 端口(可选) + Username string `json:"username"` // 用户名(可选) + Password string `json:"password"` // 密码(可选) + Protocol string `json:"protocol"` // 协议: http, https, socks5 + Ext map[string]interface{} `json:"ext"` // 扩展信息 +} + +// ProxyClient 代理客户端 +type ProxyClient struct { + ProxyID int // IP池中的代理ID(索引) + IP string // 使用的IP地址 + *http.Client // HTTP客户端 +} + +// Config 代理配置 +type Config struct { + Enabled bool // 是否启用代理 + Mode string // 模式: "single", "pool", "brightdata" + Timeout time.Duration // 超时时间 + ProxyURL string // 单个代理地址 (single模式) + ProxyList []string // 代理列表 (pool模式) + BrightDataEndpoint string // Bright Data接口地址 (brightdata模式) + BrightDataToken string // Bright Data访问令牌 (brightdata模式) + BrightDataZone string // Bright Data区域 (brightdata模式) + ProxyHost string // 代理主机 + ProxyUser string // 代理用户名模板(支持%s占位符) + ProxyPassword string // 代理密码 + RefreshInterval time.Duration // IP列表刷新间隔 + BlacklistTTL int // 黑名单IP的TTL(刷新次数) +}