320 lines
7.8 KiB
Go
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,
|
|
},
|
|
} |