201 lines
7.4 KiB
Go
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()
|
|
}
|
|
}
|