Files
proxy-platform/internal/unlock/unlock.go
T

320 lines
7.8 KiB
Go

package unlock
import (
"context"
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"strings"
"sync"
"time"
"go.uber.org/zap"
)
// ServiceConfig 服务检测配置
type ServiceConfig struct {
Name string
URL string
SuccessKeywords []string
FailKeywords []string
Timeout time.Duration
}
// Result 解锁检测结果
type Result struct {
Service string `json:"service"`
Unlocked bool `json:"unlocked"`
Region string `json:"region"`
Error string `json:"error,omitempty"`
CheckedAt time.Time `json:"checked_at"`
}
// Detector 解锁检测器
type Detector struct {
proxyHost string
proxyPort int
timeout time.Duration
logger *zap.Logger
client *http.Client
}
// NewDetector 创建解锁检测器
func NewDetector(proxyHost string, proxyPort int, timeout int, logger *zap.Logger) *Detector {
// 创建通过 SOCKS5 代理的 HTTP 客户端
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
// 通过 SOCKS5 代理连接
return dialSocks5(ctx, proxyHost, proxyPort, addr, timeout)
},
},
Timeout: time.Duration(timeout) * time.Second,
}
return &Detector{
proxyHost: proxyHost,
proxyPort: proxyPort,
timeout: time.Duration(timeout) * time.Second,
logger: logger,
client: client,
}
}
// CheckService 检测单个服务
func (d *Detector) CheckService(ctx context.Context, config ServiceConfig) (*Result, error) {
result := &Result{
Service: config.Name,
CheckedAt: time.Now(),
}
// 发送请求
req, err := http.NewRequestWithContext(ctx, "GET", config.URL, nil)
if err != nil {
result.Error = err.Error()
return result, err
}
// 设置请求头
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
req.Header.Set("Accept-Language", "en-US,en;q=0.5")
resp, err := d.client.Do(req)
if err != nil {
result.Error = err.Error()
d.logger.Error("请求失败", zap.String("service", config.Name), zap.Error(err))
return result, err
}
defer resp.Body.Close()
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
result.Error = err.Error()
return result, err
}
bodyStr := string(body)
// 检查失败关键词
for _, keyword := range config.FailKeywords {
if strings.Contains(bodyStr, keyword) {
result.Unlocked = false
result.Error = fmt.Sprintf("检测到失败关键词: %s", keyword)
d.logger.Info("解锁检测失败",
zap.String("service", config.Name),
zap.String("keyword", keyword),
)
return result, nil
}
}
// 检查成功关键词
for _, keyword := range config.SuccessKeywords {
if strings.Contains(bodyStr, keyword) {
result.Unlocked = true
// 尝试提取区域信息
result.Region = extractRegion(config.Name, bodyStr)
d.logger.Info("解锁检测成功",
zap.String("service", config.Name),
zap.String("region", result.Region),
)
return result, nil
}
}
// 如果没有匹配任何关键词,根据 HTTP 状态码判断
if resp.StatusCode >= 200 && resp.StatusCode < 400 {
result.Unlocked = true
result.Region = "??"
} else {
result.Unlocked = false
result.Error = fmt.Sprintf("HTTP 状态码: %d", resp.StatusCode)
}
return result, nil
}
// CheckAll 检测所有服务
func (d *Detector) CheckAll(ctx context.Context, configs []ServiceConfig) map[string]*Result {
results := make(map[string]*Result)
var wg sync.WaitGroup
var mu sync.Mutex
for _, config := range configs {
wg.Add(1)
go func(cfg ServiceConfig) {
defer wg.Done()
result, err := d.CheckService(ctx, cfg)
if err != nil {
d.logger.Error("检测服务失败", zap.String("service", cfg.Name), zap.Error(err))
}
mu.Lock()
results[cfg.Name] = result
mu.Unlock()
}(config)
}
wg.Wait()
return results
}
// dialSocks5 通过 SOCKS5 代理连接
func dialSocks5(ctx context.Context, proxyHost string, proxyPort int, target string, timeout int) (net.Conn, error) {
// 连接到代理服务器
proxyAddr := fmt.Sprintf("%s:%d", proxyHost, proxyPort)
conn, err := net.DialTimeout("tcp", proxyAddr, time.Duration(timeout)*time.Second)
if err != nil {
return nil, fmt.Errorf("连接代理失败: %w", err)
}
// SOCKS5 握手
// 发送认证方法
_, err = conn.Write([]byte{0x05, 0x01, 0x00}) // 无认证
if err != nil {
conn.Close()
return nil, err
}
// 读取响应
buf := make([]byte, 2)
_, err = io.ReadFull(conn, buf)
if err != nil {
conn.Close()
return nil, err
}
if buf[0] != 0x05 {
conn.Close()
return nil, fmt.Errorf("不支持的 SOCKS 版本: %d", buf[0])
}
// 解析目标地址
host, port, err := net.SplitHostPort(target)
if err != nil {
conn.Close()
return nil, err
}
// 构建连接请求
req := []byte{0x05, 0x01, 0x00} // CONNECT
// 判断是 IP 还是域名
ip := net.ParseIP(host)
if ip != nil {
if ip.To4() != nil {
req = append(req, 0x01) // IPv4
req = append(req, ip.To4()...)
} else {
req = append(req, 0x04) // IPv6
req = append(req, ip.To16()...)
}
} else {
req = append(req, 0x03) // 域名
req = append(req, byte(len(host)))
req = append(req, []byte(host)...)
}
// 添加端口
portNum := 0
fmt.Sscanf(port, "%d", &portNum)
req = append(req, byte(portNum>>8), byte(portNum&0xFF))
// 发送请求
_, err = conn.Write(req)
if err != nil {
conn.Close()
return nil, err
}
// 读取响应
resp := make([]byte, 10)
_, err = io.ReadFull(conn, resp)
if err != nil {
conn.Close()
return nil, err
}
if resp[1] != 0x00 {
conn.Close()
return nil, fmt.Errorf("SOCKS5 连接失败: status %d", resp[1])
}
return conn, nil
}
// extractRegion 提取区域信息
func extractRegion(service string, body string) string {
// 根据不同服务提取区域
switch service {
case "netflix":
// Netflix 通常在页面中包含区域信息
if strings.Contains(body, "US") {
return "US"
} else if strings.Contains(body, "UK") {
return "UK"
} else if strings.Contains(body, "JP") {
return "JP"
}
case "youtube":
// YouTube 可能包含区域设置
if strings.Contains(body, "\"gl\":\"US\"") {
return "US"
}
}
// 默认返回未知
return "??"
}
// DefaultServices 默认服务配置
var DefaultServices = []ServiceConfig{
{
Name: "gpt",
URL: "https://chat.openai.com/",
SuccessKeywords: []string{"challenges", "signup", "login"},
FailKeywords: []string{"Access denied", "unavailable", "not available"},
Timeout: 10 * time.Second,
},
{
Name: "netflix",
URL: "https://www.netflix.com/title/80018499",
SuccessKeywords: []string{"netflix.com", "watch"},
FailKeywords: []string{"not available", "nflxvideo.net", "error"},
Timeout: 10 * time.Second,
},
{
Name: "disney",
URL: "https://www.disneyplus.com/",
SuccessKeywords: []string{"disneyplus.com", "disney"},
FailKeywords: []string{"unavailable", "not available"},
Timeout: 10 * time.Second,
},
{
Name: "youtube",
URL: "https://www.youtube.com/",
SuccessKeywords: []string{"youtube.com", "ytInitialPlayerResponse"},
FailKeywords: []string{},
Timeout: 10 * time.Second,
},
{
Name: "claude",
URL: "https://claude.ai/",
SuccessKeywords: []string{"claude.ai", "anthropic"},
FailKeywords: []string{"Access denied", "unavailable"},
Timeout: 10 * time.Second,
},
{
Name: "gemini",
URL: "https://gemini.google.com/",
SuccessKeywords: []string{"gemini", "google"},
FailKeywords: []string{"unavailable"},
Timeout: 10 * time.Second,
},
}