feat: init

This commit is contained in:
isboyjc
2026-03-29 03:31:59 +08:00
parent f2c3fbac24
commit f55209d8d3
27 changed files with 4424 additions and 664 deletions

View File

@@ -9,7 +9,7 @@ import (
"strings"
"time"
"proxy-pool/storage"
"goproxy/storage"
)
// 代理来源定义
@@ -18,27 +18,171 @@ type Source struct {
Protocol string // http 或 socks5
}
// 内置多个免费代理来源
var defaultSources = []Source{
// 快速更新源5-30分钟更新- 用于紧急和补充模式
var fastUpdateSources = []Source{
// proxifly - 每5分钟更新
{"https://cdn.jsdelivr.net/gh/proxifly/free-proxy-list@main/proxies/http/data.txt", "http"},
{"https://cdn.jsdelivr.net/gh/proxifly/free-proxy-list@main/proxies/socks4/data.txt", "socks5"},
{"https://cdn.jsdelivr.net/gh/proxifly/free-proxy-list@main/proxies/socks5/data.txt", "socks5"},
// ProxyScraper - 每30分钟更新
{"https://raw.githubusercontent.com/ProxyScraper/ProxyScraper/main/http.txt", "http"},
{"https://raw.githubusercontent.com/ProxyScraper/ProxyScraper/main/socks4.txt", "socks5"},
{"https://raw.githubusercontent.com/ProxyScraper/ProxyScraper/main/socks5.txt", "socks5"},
// monosans - 每小时更新
{"https://raw.githubusercontent.com/monosans/proxy-list/main/proxies/http.txt", "http"},
}
// 慢速更新源(每天更新)- 用于优化轮换模式
var slowUpdateSources = []Source{
// TheSpeedX - 每天更新
{"https://raw.githubusercontent.com/TheSpeedX/SOCKS-List/master/http.txt", "http"},
{"https://raw.githubusercontent.com/TheSpeedX/SOCKS-List/master/socks4.txt", "socks5"},
{"https://raw.githubusercontent.com/TheSpeedX/SOCKS-List/master/socks5.txt", "socks5"},
// monosans SOCKS
{"https://raw.githubusercontent.com/monosans/proxy-list/main/proxies/socks4.txt", "socks5"},
{"https://raw.githubusercontent.com/monosans/proxy-list/main/proxies/socks5.txt", "socks5"},
// databay-labs - 备用源
{"https://cdn.jsdelivr.net/gh/databay-labs/free-proxy-list/http.txt", "http"},
{"https://cdn.jsdelivr.net/gh/databay-labs/free-proxy-list/socks5.txt", "socks5"},
{"https://cdn.jsdelivr.net/gh/proxifly/free-proxy-list@main/proxies/all/data.txt", ""},
}
// 所有源
var allSources = append(fastUpdateSources, slowUpdateSources...)
type Fetcher struct {
sources []Source
client *http.Client
sources []Source
client *http.Client
sourceManager *SourceManager
}
func New(httpURL, socks5URL string) *Fetcher {
func New(httpURL, socks5URL string, sourceManager *SourceManager) *Fetcher {
return &Fetcher{
sources: defaultSources,
sources: allSources,
sourceManager: sourceManager,
client: &http.Client{
Timeout: 30 * time.Second,
},
}
}
// FetchSmart 智能抓取:根据模式和协议需求选择源
func (f *Fetcher) FetchSmart(mode string, preferredProtocol string) ([]storage.Proxy, error) {
var sources []Source
switch mode {
case "emergency":
// 紧急模式:使用所有可用源
sources = f.filterAvailableSources(allSources, preferredProtocol)
log.Printf("[fetch] 🚨 紧急模式: 使用 %d 个源", len(sources))
case "refill":
// 补充模式:使用快更新源
sources = f.filterAvailableSources(fastUpdateSources, preferredProtocol)
log.Printf("[fetch] 🔄 补充模式: 使用 %d 个快更新源", len(sources))
case "optimize":
// 优化模式随机选择2-3个慢更新源
sources = f.selectRandomSources(slowUpdateSources, 3, preferredProtocol)
log.Printf("[fetch] ⚡ 优化模式: 使用 %d 个源", len(sources))
default:
sources = f.filterAvailableSources(fastUpdateSources, preferredProtocol)
}
if len(sources) == 0 {
return nil, fmt.Errorf("no available sources")
}
return f.fetchFromSources(sources)
}
// filterAvailableSources 过滤可用的源(通过断路器)
func (f *Fetcher) filterAvailableSources(sources []Source, preferredProtocol string) []Source {
var available []Source
for _, src := range sources {
// 检查断路器
if f.sourceManager != nil && !f.sourceManager.CanUseSource(src.URL) {
continue
}
// 如果指定了协议偏好,优先该协议的源
if preferredProtocol != "" && src.Protocol != "" && src.Protocol != preferredProtocol {
continue
}
available = append(available, src)
}
return available
}
// selectRandomSources 随机选择N个源
func (f *Fetcher) selectRandomSources(sources []Source, count int, preferredProtocol string) []Source {
available := f.filterAvailableSources(sources, preferredProtocol)
if len(available) <= count {
return available
}
// 随机打乱
shuffled := make([]Source, len(available))
copy(shuffled, available)
for i := range shuffled {
j := i + int(time.Now().UnixNano())%(len(shuffled)-i)
shuffled[i], shuffled[j] = shuffled[j], shuffled[i]
}
return shuffled[:count]
}
// fetchFromSources 从指定源列表抓取
func (f *Fetcher) fetchFromSources(sources []Source) ([]storage.Proxy, error) {
type result struct {
proxies []storage.Proxy
source Source
err error
}
ch := make(chan result, len(sources))
for _, src := range sources {
go func(s Source) {
proxies, err := f.fetchFromURL(s.URL, s.Protocol)
ch <- result{proxies: proxies, source: s, err: err}
}(src)
}
var all []storage.Proxy
seen := make(map[string]bool)
for range sources {
r := <-ch
if r.err != nil {
log.Printf("[fetch] ❌ %s error: %v", r.source.URL, r.err)
if f.sourceManager != nil {
f.sourceManager.RecordFail(r.source.URL, 3, 5, 30)
}
continue
}
// 记录成功
if f.sourceManager != nil {
f.sourceManager.RecordSuccess(r.source.URL)
}
// 去重
var deduped []storage.Proxy
for _, p := range r.proxies {
if !seen[p.Address] {
seen[p.Address] = true
deduped = append(deduped, p)
}
}
log.Printf("[fetch] ✅ %d 个 %s 代理 from %s", len(deduped), r.source.Protocol, r.source.URL)
all = append(all, deduped...)
}
if len(all) == 0 {
return nil, fmt.Errorf("no proxies fetched")
}
log.Printf("[fetch] 总共抓取: %d 个代理(去重后)", len(all))
return all, nil
}
// Fetch 从所有来源并发抓取代理
func (f *Fetcher) Fetch() ([]storage.Proxy, error) {
type result struct {

152
fetcher/ip_query.go Normal file
View File

@@ -0,0 +1,152 @@
package fetcher
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"golang.org/x/time/rate"
)
// IPQueryLimiter 全局IP查询限流器
var IPQueryLimiter *rate.Limiter
// InitIPQueryLimiter 初始化限流器
func InitIPQueryLimiter(rps int) {
IPQueryLimiter = rate.NewLimiter(rate.Limit(rps), rps*2)
}
// GetExitIPInfo 通过代理获取出口 IP 和地理位置(多源降级)
func GetExitIPInfo(client *http.Client) (string, string) {
// 等待限流令牌
if IPQueryLimiter != nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := IPQueryLimiter.Wait(ctx); err != nil {
return "", ""
}
}
// 优先级1ip-api.com
if ip, loc := tryIPAPI(client); ip != "" {
return ip, loc
}
// 优先级2ipapi.co
if ip, loc := tryIPAPICo(client); ip != "" {
return ip, loc
}
// 优先级3ipinfo.io
if ip, loc := tryIPInfo(client); ip != "" {
return ip, loc
}
// 优先级4仅获取IP
if ip := tryHTTPBinIP(client); ip != "" {
return ip, "UNKNOWN"
}
return "", ""
}
// tryIPAPI 尝试 ip-api.com
func tryIPAPI(client *http.Client) (string, string) {
resp, err := client.Get("http://ip-api.com/json/?fields=status,country,countryCode,city,query")
if err != nil {
return "", ""
}
defer resp.Body.Close()
var result struct {
Status string `json:"status"`
Query string `json:"query"`
Country string `json:"country"`
CountryCode string `json:"countryCode"`
City string `json:"city"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil || result.Status != "success" {
return "", ""
}
location := result.CountryCode
if result.City != "" {
location = fmt.Sprintf("%s %s", result.CountryCode, result.City)
}
return result.Query, location
}
// tryIPAPICo 尝试 ipapi.co
func tryIPAPICo(client *http.Client) (string, string) {
resp, err := client.Get("https://ipapi.co/json/")
if err != nil {
return "", ""
}
defer resp.Body.Close()
var result struct {
IP string `json:"ip"`
City string `json:"city"`
CountryCode string `json:"country_code"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", ""
}
location := result.CountryCode
if result.City != "" {
location = fmt.Sprintf("%s %s", result.CountryCode, result.City)
}
return result.IP, location
}
// tryIPInfo 尝试 ipinfo.io
func tryIPInfo(client *http.Client) (string, string) {
resp, err := client.Get("https://ipinfo.io/json")
if err != nil {
return "", ""
}
defer resp.Body.Close()
var result struct {
IP string `json:"ip"`
City string `json:"city"`
Country string `json:"country"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", ""
}
location := result.Country
if result.City != "" {
location = fmt.Sprintf("%s %s", result.Country, result.City)
}
return result.IP, location
}
// tryHTTPBinIP 尝试 httpbin仅获取IP
func tryHTTPBinIP(client *http.Client) string {
resp, err := client.Get("https://httpbin.org/ip")
if err != nil {
return ""
}
defer resp.Body.Close()
var result struct {
Origin string `json:"origin"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return ""
}
return result.Origin
}

132
fetcher/source_manager.go Normal file
View File

@@ -0,0 +1,132 @@
package fetcher
import (
"database/sql"
"log"
"sync"
"time"
)
// SourceManager 代理源管理器(断路器)
type SourceManager struct {
db *sql.DB
mu sync.RWMutex
}
func NewSourceManager(db *sql.DB) *SourceManager {
return &SourceManager{db: db}
}
// CanUseSource 判断源是否可用
func (sm *SourceManager) CanUseSource(url string) bool {
sm.mu.RLock()
defer sm.mu.RUnlock()
var status string
var disabledUntil sql.NullTime
err := sm.db.QueryRow(
`SELECT status, disabled_until FROM source_status WHERE url = ?`,
url,
).Scan(&status, &disabledUntil)
// 源不存在,默认可用
if err != nil {
return true
}
// 检查是否被禁用且还在冷却期
if status == "disabled" && disabledUntil.Valid {
if time.Now().Before(disabledUntil.Time) {
return false
}
// 冷却期结束,重置状态
sm.db.Exec(`UPDATE source_status SET status = 'active', consecutive_fails = 0 WHERE url = ?`, url)
return true
}
return status != "disabled"
}
// RecordSuccess 记录源抓取成功
func (sm *SourceManager) RecordSuccess(url string) {
sm.mu.Lock()
defer sm.mu.Unlock()
sm.db.Exec(`
INSERT INTO source_status (url, success_count, consecutive_fails, last_success, status)
VALUES (?, 1, 0, CURRENT_TIMESTAMP, 'active')
ON CONFLICT(url) DO UPDATE SET
success_count = success_count + 1,
consecutive_fails = 0,
last_success = CURRENT_TIMESTAMP,
status = 'active'
`, url)
}
// RecordFail 记录源抓取失败
func (sm *SourceManager) RecordFail(url string, failThreshold, disableThreshold, cooldownMinutes int) {
sm.mu.Lock()
defer sm.mu.Unlock()
// 增加失败计数
sm.db.Exec(`
INSERT INTO source_status (url, fail_count, consecutive_fails, last_fail)
VALUES (?, 1, 1, CURRENT_TIMESTAMP)
ON CONFLICT(url) DO UPDATE SET
fail_count = fail_count + 1,
consecutive_fails = consecutive_fails + 1,
last_fail = CURRENT_TIMESTAMP
`, url)
// 检查是否需要降级或禁用
var consecutiveFails int
sm.db.QueryRow(`SELECT consecutive_fails FROM source_status WHERE url = ?`, url).Scan(&consecutiveFails)
if consecutiveFails >= disableThreshold {
// 禁用源
disabledUntil := time.Now().Add(time.Duration(cooldownMinutes) * time.Minute)
sm.db.Exec(
`UPDATE source_status SET status = 'disabled', disabled_until = ? WHERE url = ?`,
disabledUntil, url,
)
log.Printf("[source] ⛔ 禁用源(连续失败%d次: %s (冷却%d分钟)", consecutiveFails, url, cooldownMinutes)
} else if consecutiveFails >= failThreshold {
// 降级源
sm.db.Exec(`UPDATE source_status SET status = 'degraded' WHERE url = ?`, url)
log.Printf("[source] ⚠️ 降级源(连续失败%d次: %s", consecutiveFails, url)
}
}
// GetSourceStats 获取所有源的统计信息
func (sm *SourceManager) GetSourceStats() ([]map[string]interface{}, error) {
rows, err := sm.db.Query(`
SELECT url, success_count, fail_count, consecutive_fails,
last_success, last_fail, status
FROM source_status
ORDER BY success_count DESC
`)
if err != nil {
return nil, err
}
defer rows.Close()
var stats []map[string]interface{}
for rows.Next() {
var url, status string
var successCount, failCount, consecutiveFails int
var lastSuccess, lastFail sql.NullTime
rows.Scan(&url, &successCount, &failCount, &consecutiveFails, &lastSuccess, &lastFail, &status)
stats = append(stats, map[string]interface{}{
"url": url,
"success_count": successCount,
"fail_count": failCount,
"consecutive_fails": consecutiveFails,
"last_success": lastSuccess,
"last_fail": lastFail,
"status": status,
})
}
return stats, nil
}