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