package client import ( "bytes" "compress/gzip" "io" "math/rand" "net/http" "net/url" "strings" "github.com/andybalholm/brotli" http2 "github.com/bogdanfinn/fhttp" tls_client "github.com/bogdanfinn/tls-client" "github.com/bogdanfinn/tls-client/profiles" ) // TLSClient 使用 tls-client 模拟浏览器指纹的 HTTP 客户端 type TLSClient struct { client tls_client.HttpClient userAgent string chromeVer string acceptLang string } // 语言偏好池 var languagePrefs = []string{ "en-US,en;q=0.9", "en-GB,en;q=0.9,en-US;q=0.8", "de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7", } // New 创建一个新的 TLS 客户端 func New(proxyStr string) (*TLSClient, error) { jar := tls_client.NewCookieJar() chromeVer := "133" options := []tls_client.HttpClientOption{ tls_client.WithTimeoutSeconds(60), tls_client.WithClientProfile(profiles.Chrome_133), tls_client.WithRandomTLSExtensionOrder(), tls_client.WithCookieJar(jar), tls_client.WithInsecureSkipVerify(), } if proxyStr != "" { options = append(options, tls_client.WithProxyUrl(proxyStr)) } client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(), options...) if err != nil { return nil, err } acceptLang := languagePrefs[rand.Intn(len(languagePrefs))] userAgent := generateUserAgent(chromeVer) return &TLSClient{ client: client, userAgent: userAgent, chromeVer: chromeVer, acceptLang: acceptLang, }, nil } // generateUserAgent 生成随机化的 User-Agent func generateUserAgent(chromeVer string) string { winVersions := []string{ "Windows NT 10.0; Win64; x64", "Windows NT 10.0; WOW64", } winVer := winVersions[rand.Intn(len(winVersions))] return "Mozilla/5.0 (" + winVer + ") AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + chromeVer + ".0.0.0 Safari/537.36" } // getDefaultHeaders 获取默认请求头 func (c *TLSClient) getDefaultHeaders() map[string]string { secChUa := `"Chromium";v="` + c.chromeVer + `", "Not(A:Brand";v="99", "Google Chrome";v="` + c.chromeVer + `"` return map[string]string{ "User-Agent": c.userAgent, "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", "Accept-Language": c.acceptLang, "Accept-Encoding": "gzip, deflate, br, zstd", "Cache-Control": "max-age=0", "Sec-Ch-Ua": secChUa, "Sec-Ch-Ua-Mobile": "?0", "Sec-Ch-Ua-Platform": `"Windows"`, "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "none", "Sec-Fetch-User": "?1", "Upgrade-Insecure-Requests": "1", } } // 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 }