Files
nofx/proxy/proxy_manager.go
2025-11-05 21:41:36 -05:00

347 lines
8.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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