Files
djk/internal/app/network_browser.go
T

201 lines
7.4 KiB
Go

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