Files
codexautopool/backend/internal/client/tls.go

375 lines
11 KiB
Go

package client
import (
"bytes"
"compress/gzip"
"fmt"
"io"
"math/rand"
"net/http"
"net/url"
"strings"
"codex-pool/internal/proxyutil"
"github.com/andybalholm/brotli"
http2 "github.com/bogdanfinn/fhttp"
tls_client "github.com/bogdanfinn/tls-client"
)
// TLSClient 使用 tls-client 模拟浏览器指纹的 HTTP 客户端
type TLSClient struct {
client tls_client.HttpClient
fingerprint BrowserFingerprint
userAgent string
acceptLang string
}
// 语言偏好池
var languagePrefs = []string{
"en-US,en;q=0.9",
"en-GB,en;q=0.9,en-US;q=0.8",
"en-US,en;q=0.9,de;q=0.8",
"de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7",
"en-US,en;q=0.9,fr;q=0.8",
"en-US,en;q=0.9,es;q=0.8",
"fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7",
"es-ES,es;q=0.9,en-US;q=0.8,en;q=0.7",
}
// New 创建一个新的 TLS 客户端(使用随机指纹)
func New(proxyStr string) (*TLSClient, error) {
// 获取随机桌面端指纹
fp := GetRandomDesktopFingerprint()
return NewWithFingerprint(fp, proxyStr)
}
// NewWithFingerprint 使用指定指纹创建客户端
func NewWithFingerprint(fp BrowserFingerprint, proxyStr string) (*TLSClient, error) {
jar := tls_client.NewCookieJar()
options := []tls_client.HttpClientOption{
tls_client.WithTimeoutSeconds(90),
tls_client.WithClientProfile(fp.TLSProfile),
tls_client.WithRandomTLSExtensionOrder(),
tls_client.WithCookieJar(jar),
tls_client.WithInsecureSkipVerify(),
tls_client.WithNotFollowRedirects(),
}
if proxyStr != "" {
normalized, err := proxyutil.Normalize(proxyStr)
if err != nil {
return nil, err
}
options = append(options, tls_client.WithProxyUrl(normalized))
}
client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(), options...)
if err != nil {
return nil, err
}
acceptLang := languagePrefs[rand.Intn(len(languagePrefs))]
userAgent := generateUserAgent(fp)
return &TLSClient{
client: client,
fingerprint: fp,
userAgent: userAgent,
acceptLang: acceptLang,
}, nil
}
// generateUserAgent 根据指纹生成 User-Agent
func generateUserAgent(fp BrowserFingerprint) string {
winVersions := []string{"Windows NT 10.0; Win64; x64", "Windows NT 11.0; Win64; x64"}
macVersions := []string{"10_15_7", "11_0_0", "12_0_0", "13_0_0", "14_0", "14_5", "15_0", "15_2"}
linuxVersions := []string{"X11; Linux x86_64", "X11; Ubuntu; Linux x86_64", "X11; Fedora; Linux x86_64"}
version := fp.Version
switch fp.Browser {
case "chrome":
switch fp.Platform {
case "Windows":
return fmt.Sprintf("Mozilla/5.0 (%s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s.0.0.0 Safari/537.36",
winVersions[rand.Intn(len(winVersions))], version)
case "macOS":
return fmt.Sprintf("Mozilla/5.0 (Macintosh; Intel Mac OS X %s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s.0.0.0 Safari/537.36",
macVersions[rand.Intn(len(macVersions))], version)
case "Linux":
return fmt.Sprintf("Mozilla/5.0 (%s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s.0.0.0 Safari/537.36",
linuxVersions[rand.Intn(len(linuxVersions))], version)
case "Android":
return fmt.Sprintf("Mozilla/5.0 (Linux; Android 14; SM-S918B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s.0.0.0 Mobile Safari/537.36", version)
}
case "firefox":
switch fp.Platform {
case "Windows":
return fmt.Sprintf("Mozilla/5.0 (%s; rv:%s.0) Gecko/20100101 Firefox/%s.0",
winVersions[rand.Intn(len(winVersions))], version, version)
case "macOS":
return fmt.Sprintf("Mozilla/5.0 (Macintosh; Intel Mac OS X %s; rv:%s.0) Gecko/20100101 Firefox/%s.0",
macVersions[rand.Intn(len(macVersions))], version, version)
case "Linux":
return fmt.Sprintf("Mozilla/5.0 (%s; rv:%s.0) Gecko/20100101 Firefox/%s.0",
linuxVersions[rand.Intn(len(linuxVersions))], version, version)
}
case "safari":
if fp.Mobile {
iosVersions := map[string]string{"18.5": "18_5", "18.0": "18_0", "17.0": "17_0", "16.0": "16_0", "15.6": "15_6", "15.5": "15_5"}
iosVer := iosVersions[version]
if iosVer == "" {
iosVer = "18_0"
}
if fp.Platform == "iPadOS" {
return fmt.Sprintf("Mozilla/5.0 (iPad; CPU OS %s like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/%s Mobile/15E148 Safari/604.1", iosVer, version)
}
return fmt.Sprintf("Mozilla/5.0 (iPhone; CPU iPhone OS %s like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/%s Mobile/15E148 Safari/604.1", iosVer, version)
}
return fmt.Sprintf("Mozilla/5.0 (Macintosh; Intel Mac OS X %s) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/%s Safari/605.1.15",
macVersions[rand.Intn(len(macVersions))], version)
case "opera":
chromeVer := map[string]string{"91": "118", "90": "117", "89": "116"}[version]
if chromeVer == "" {
chromeVer = "118"
}
return fmt.Sprintf("Mozilla/5.0 (%s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s.0.0.0 Safari/537.36 OPR/%s.0.0.0",
winVersions[rand.Intn(len(winVersions))], chromeVer, version)
case "okhttp":
return "okhttp/4.12.0"
}
// 默认 Chrome
return fmt.Sprintf("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s.0.0.0 Safari/537.36", version)
}
// getDefaultHeaders 获取默认请求头(根据浏览器类型返回不同的头)
func (c *TLSClient) getDefaultHeaders() map[string]string {
headers := map[string]string{
"User-Agent": c.userAgent,
"Accept-Language": c.acceptLang,
"Accept-Encoding": "gzip, deflate, br",
}
fp := c.fingerprint
switch fp.Browser {
case "firefox":
headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"
headers["Upgrade-Insecure-Requests"] = "1"
headers["Sec-Fetch-Dest"] = "document"
headers["Sec-Fetch-Mode"] = "navigate"
headers["Sec-Fetch-Site"] = "none"
headers["Sec-Fetch-User"] = "?1"
headers["DNT"] = "1"
case "safari":
headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
case "okhttp":
headers["Accept"] = "*/*"
headers["Accept-Encoding"] = "gzip"
default: // chrome, opera
secChUa := c.generateSecChUa()
headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"
headers["Cache-Control"] = "max-age=0"
headers["Sec-Ch-Ua"] = secChUa
headers["Sec-Ch-Ua-Mobile"] = "?0"
if fp.Mobile {
headers["Sec-Ch-Ua-Mobile"] = "?1"
}
headers["Sec-Ch-Ua-Platform"] = c.getPlatformHeader()
headers["Sec-Fetch-Dest"] = "document"
headers["Sec-Fetch-Mode"] = "navigate"
headers["Sec-Fetch-Site"] = "none"
headers["Sec-Fetch-User"] = "?1"
headers["Upgrade-Insecure-Requests"] = "1"
}
return headers
}
// generateSecChUa 生成 Sec-Ch-Ua 头
func (c *TLSClient) generateSecChUa() string {
ver := c.fingerprint.Version
switch c.fingerprint.Browser {
case "opera":
return fmt.Sprintf(`"Opera";v="%s", "Chromium";v="118", "Not(A:Brand";v="99"`, ver)
default:
notABrands := []string{`"Not(A:Brand";v="99"`, `"Not A(Brand";v="99"`, `"Not/A)Brand";v="99"`}
return fmt.Sprintf(`"Chromium";v="%s", %s, "Google Chrome";v="%s"`, ver, notABrands[rand.Intn(len(notABrands))], ver)
}
}
// getPlatformHeader 获取平台头
func (c *TLSClient) getPlatformHeader() string {
switch c.fingerprint.Platform {
case "macOS":
return `"macOS"`
case "Linux":
return `"Linux"`
case "iOS", "iPadOS":
return `"iOS"`
case "Android":
return `"Android"`
default:
return `"Windows"`
}
}
// GetFingerprintInfo 获取指纹信息字符串(用于日志输出)
func (c *TLSClient) GetFingerprintInfo() string {
fp := c.fingerprint
return fmt.Sprintf("%s/%s (%s)", fp.Browser, fp.Version, fp.Platform)
}
// Do 执行 HTTP 请求
func (c *TLSClient) Do(req *http.Request) (*http.Response, error) {
fhttpReq, err := http2.NewRequest(req.Method, req.URL.String(), req.Body)
if err != nil {
return nil, err
}
for key, value := range c.getDefaultHeaders() {
if req.Header.Get(key) == "" {
fhttpReq.Header.Set(key, value)
}
}
for key, values := range req.Header {
for _, value := range values {
fhttpReq.Header.Set(key, value)
}
}
resp, err := c.client.Do(fhttpReq)
if err != nil {
return nil, err
}
finalReq := req
if resp.Request != nil && resp.Request.URL != nil {
finalReq = &http.Request{
Method: resp.Request.Method,
URL: (*url.URL)(resp.Request.URL),
Header: http.Header(resp.Request.Header),
}
}
stdResp := &http.Response{
Status: resp.Status,
StatusCode: resp.StatusCode,
Proto: resp.Proto,
ProtoMajor: resp.ProtoMajor,
ProtoMinor: resp.ProtoMinor,
Header: http.Header(resp.Header),
Body: resp.Body,
ContentLength: resp.ContentLength,
TransferEncoding: resp.TransferEncoding,
Close: resp.Close,
Uncompressed: resp.Uncompressed,
Request: finalReq,
}
return stdResp, nil
}
// Get 执行 GET 请求
func (c *TLSClient) Get(urlStr string) (*http.Response, error) {
req, err := http.NewRequest("GET", urlStr, nil)
if err != nil {
return nil, err
}
return c.Do(req)
}
// Post 执行 POST 请求
func (c *TLSClient) Post(urlStr string, contentType string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequest("POST", urlStr, body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", contentType)
return c.Do(req)
}
// PostForm 执行 POST 表单请求
func (c *TLSClient) PostForm(urlStr string, data url.Values) (*http.Response, error) {
return c.Post(urlStr, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
}
// PostJSON 执行 POST JSON 请求
func (c *TLSClient) PostJSON(urlStr string, body io.Reader) (*http.Response, error) {
return c.Post(urlStr, "application/json", body)
}
// GetCookie 获取指定 URL 的 Cookie
func (c *TLSClient) GetCookie(urlStr string, name string) string {
u, err := url.Parse(urlStr)
if err != nil {
return ""
}
cookies := c.client.GetCookies(u)
for _, cookie := range cookies {
if cookie.Name == name {
return cookie.Value
}
}
return ""
}
// SetCookie 设置 Cookie
func (c *TLSClient) SetCookie(urlStr string, cookie *http.Cookie) {
u, err := url.Parse(urlStr)
if err != nil {
return
}
c.client.SetCookies(u, []*http2.Cookie{
{
Name: cookie.Name,
Value: cookie.Value,
Path: cookie.Path,
Domain: cookie.Domain,
},
})
}
// ReadBody 读取响应体并自动处理压缩
func ReadBody(resp *http.Response) ([]byte, error) {
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
switch resp.Header.Get("Content-Encoding") {
case "gzip":
gzReader, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return data, nil
}
defer gzReader.Close()
return io.ReadAll(gzReader)
case "br":
return io.ReadAll(brotli.NewReader(bytes.NewReader(data)))
}
return data, nil
}
// ReadBodyString 读取响应体为字符串
func ReadBodyString(resp *http.Response) (string, error) {
body, err := ReadBody(resp)
if err != nil {
return "", err
}
return string(body), nil
}