package app import ( "crypto/tls" "fmt" "net/http" "net/url" "strings" "sync" "time" fhttp "github.com/bogdanfinn/fhttp" tlsclient "github.com/bogdanfinn/tls-client" "github.com/bogdanfinn/tls-client/profiles" ) const huangguoBrowserProfile = "Chrome 150" type browserHTTPClient interface { Do(*fhttp.Request) (*fhttp.Response, error) CloseIdleConnections() } type huangguoBrowserTransport struct { base http.RoundTripper router *proxyRouter host string insecure bool record func(diagnosticEvent) mu sync.Mutex clients map[string]browserHTTPClient newClient func(string) (browserHTTPClient, error) } func newHuangguoBrowserTransport(base http.RoundTripper, downloader *Downloader) *huangguoBrowserTransport { configured, _ := url.Parse(downloader.providerBaseURL(sourceHuangguoVideo)) transport := &huangguoBrowserTransport{ base: base, router: downloader.proxyRouter, host: configured.Host, insecure: downloader.cfg.InsecureTLS, record: downloader.recordDiagnostic, clients: make(map[string]browserHTTPClient), } transport.newClient = transport.createClient return transport } func (transport *huangguoBrowserTransport) matches(request *http.Request) bool { return (request.Method == http.MethodGet || request.Method == http.MethodHead) && (request.URL.Scheme == "http" || request.URL.Scheme == "https") && (strings.EqualFold(request.URL.Host, transport.host) || strings.EqualFold(request.URL.Hostname(), "huangguo.video")) } func (transport *huangguoBrowserTransport) createClient(proxy string) (browserHTTPClient, error) { idleTimeout := 90 * time.Second options := []tlsclient.HttpClientOption{ tlsclient.WithClientProfile(profiles.Chrome_150), tlsclient.WithRandomTLSExtensionOrder(), tlsclient.WithDisableHttp3(), tlsclient.WithNotFollowRedirects(), tlsclient.WithTimeoutSeconds(45), tlsclient.WithProxyUrl(proxy), tlsclient.WithCookieJar(tlsclient.NewCookieJar()), tlsclient.WithTransportOptions(&tlsclient.TransportOptions{ IdleConnTimeout: &idleTimeout, MaxIdleConns: 8, MaxIdleConnsPerHost: 4, MaxResponseHeaderBytes: 1 << 20, }), } if transport.insecure { options = append(options, tlsclient.WithInsecureSkipVerify()) } return tlsclient.NewHttpClient(tlsclient.NewNoopLogger(), options...) } func (transport *huangguoBrowserTransport) client(request *http.Request) (browserHTTPClient, error) { transport.mu.Lock() defer transport.mu.Unlock() proxy, err := transport.router.proxy(request) if err != nil { return nil, err } proxyURL := "" if proxy != nil { proxyURL = proxy.String() } key := request.URL.Scheme + "://" + strings.ToLower(request.URL.Host) + "|" + proxyURL if client := transport.clients[key]; client != nil { return client, nil } client, err := transport.newClient(proxyURL) if err != nil { return nil, err } if len(transport.clients) >= 8 { for key, old := range transport.clients { old.CloseIdleConnections() delete(transport.clients, key) break } } transport.clients[key] = client return client, nil } func huangguoBrowserHeaders(request *http.Request) fhttp.Header { headers := fhttp.Header{ "sec-ch-ua": {`"Chromium";v="150", "Google Chrome";v="150", "Not_A Brand";v="24"`}, "sec-ch-ua-mobile": {"?0"}, "sec-ch-ua-platform": {`"macOS"`}, "upgrade-insecure-requests": {"1"}, "user-agent": {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36"}, "accept": {"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8"}, "sec-fetch-site": {"same-origin"}, "sec-fetch-mode": {"navigate"}, "sec-fetch-user": {"?1"}, "sec-fetch-dest": {"document"}, "accept-language": {"zh-CN,zh;q=0.9"}, fhttp.HeaderOrderKey: {"sec-ch-ua", "sec-ch-ua-mobile", "sec-ch-ua-platform", "upgrade-insecure-requests", "user-agent", "accept", "sec-fetch-site", "sec-fetch-mode", "sec-fetch-user", "sec-fetch-dest", "referer", "accept-encoding", "accept-language", "cookie"}, } for key, values := range request.Header { lower := strings.ToLower(key) if _, fixed := headers[lower]; fixed || lower == "host" || lower == "connection" { continue } headers[lower] = append([]string(nil), values...) } if referer := request.Header.Get("Referer"); referer == "" { headers["sec-fetch-site"] = []string{"none"} } else if parsed, err := url.Parse(referer); err == nil && !strings.EqualFold(parsed.Host, request.URL.Host) { headers["sec-fetch-site"] = []string{"cross-site"} } return headers } func standardBrowserHeaders(headers fhttp.Header) http.Header { result := make(http.Header, len(headers)) for key, values := range headers { result[http.CanonicalHeaderKey(key)] = append([]string(nil), values...) } return result } func (transport *huangguoBrowserTransport) RoundTrip(request *http.Request) (*http.Response, error) { if !transport.matches(request) { return transport.base.RoundTrip(request) } client, err := transport.client(request) if err != nil { return nil, fmt.Errorf("黄果浏览器客户端初始化失败:%w", publicError(err)) } upstream, err := fhttp.NewRequestWithContext(request.Context(), request.Method, request.URL.String(), request.Body) if err != nil { return nil, err } upstream.Header = huangguoBrowserHeaders(request) upstream.Host = request.Host upstream.ContentLength = request.ContentLength upstream.GetBody = request.GetBody response, err := client.Do(upstream) if err != nil { if response != nil && response.Body != nil { response.Body.Close() } return nil, publicError(err) } result := &http.Response{ Status: response.Status, StatusCode: response.StatusCode, Proto: response.Proto, ProtoMajor: response.ProtoMajor, ProtoMinor: response.ProtoMinor, Header: standardBrowserHeaders(response.Header), Body: response.Body, ContentLength: response.ContentLength, TransferEncoding: response.TransferEncoding, Close: response.Close, Uncompressed: response.Uncompressed, Trailer: standardBrowserHeaders(response.Trailer), Request: request, } if state := response.TLS; state != nil { result.TLS = &tls.ConnectionState{Version: state.Version, HandshakeComplete: state.HandshakeComplete, DidResume: state.DidResume, CipherSuite: state.CipherSuite, NegotiatedProtocol: state.NegotiatedProtocol, ServerName: state.ServerName, PeerCertificates: state.PeerCertificates, VerifiedChains: state.VerifiedChains} } if transport.record != nil { level := "info" if result.StatusCode >= 400 || strings.EqualFold(result.Header.Get("Cf-Mitigated"), "challenge") { level = "warning" } transport.record(diagnosticEvent{Event: "network.browser_request", Level: level, Source: sourceHuangguoVideo, Host: request.URL.Hostname(), HTTPStatus: result.StatusCode, Client: huangguoBrowserProfile, Protocol: result.Proto, CFRay: truncate(result.Header.Get("Cf-Ray"), 128), ResponseType: truncate(result.Header.Get("Content-Type"), 128), Message: "黄果浏览器指纹请求完成"}) } return result, nil } func (transport *huangguoBrowserTransport) CloseIdleConnections() { transport.mu.Lock() for key, client := range transport.clients { client.CloseIdleConnections() delete(transport.clients, key) } transport.mu.Unlock() if closer, ok := transport.base.(interface{ CloseIdleConnections() }); ok { closer.CloseIdleConnections() } }