You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
139 lines
4.5 KiB
139 lines
4.5 KiB
// Package ehttp 提供 e-hentai 专用的 HTTP 客户端:基于 utls 伪造 Chrome 的
|
|
// TLS ClientHello(HelloChrome_Auto),绕过 e-hentai / Cloudflare edge 的
|
|
// fingerprint 检测(否则会直接 SSL_ERROR_SYSCALL RST 掉)。
|
|
//
|
|
// 与 stdlib net/http 的差异:
|
|
// - DialTLSContext 接管 TLS 握手,用 utls.UClient 替换 crypto/tls;
|
|
// - ALPN 同时声明 h2 + http/1.1,与真实 Chrome 一致;
|
|
// - UA / Accept / Accept-Language 字段需要调用方在请求里加(本 helper
|
|
// 不主动设置通用头,保持与 stdlib 一样的「不主动设默认」行为)。
|
|
//
|
|
// 用法:
|
|
//
|
|
// hc := ehttp.NewClient(30 * time.Second)
|
|
// resp, err := hc.Get(ctx, "https://e-hentai.org/...",
|
|
// ehttp.WithReferer("https://e-hentai.org/g/.../"),
|
|
// ehttp.WithChromeHeaders(), // UA / Sec-CH-UA / Sec-Fetch-* 全套
|
|
// )
|
|
package ehttp
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"net/http"
|
|
"time"
|
|
|
|
tls "github.com/refraction-networking/utls"
|
|
)
|
|
|
|
// Options 给 Get / Do 的额外参数。
|
|
type Options struct {
|
|
Referer string
|
|
// ChromeHeaders 为 true 时,设置 Chrome 134 在 macOS 上的真实请求头集合:
|
|
// User-Agent、Accept、Accept-Language、Sec-CH-UA*、Sec-Fetch-*、
|
|
// Upgrade-Insecure-Requests。
|
|
ChromeHeaders bool
|
|
// Accept 覆盖默认 Accept。ChromeHeaders=true 时也会被它覆盖。
|
|
Accept string
|
|
}
|
|
|
|
type Option func(*Options)
|
|
|
|
func WithReferer(r string) Option { return func(o *Options) { o.Referer = r } }
|
|
func WithChromeHeaders() Option { return func(o *Options) { o.ChromeHeaders = true } }
|
|
func WithAccept(a string) Option { return func(o *Options) { o.Accept = a } }
|
|
|
|
// Client 是一个带 utls fingerprint 的 http.Client。
|
|
type Client struct {
|
|
hc *http.Client
|
|
timeout time.Duration
|
|
}
|
|
|
|
// NewClient 返回一个带 utls 的 HTTP 客户端。
|
|
// timeout 是整个请求的超时;内部 TLS 握手独立超时由 stdlib net.Dialer 控制。
|
|
func NewClient(timeout time.Duration) *Client {
|
|
stdDialer := &net.Dialer{Timeout: 30 * time.Second}
|
|
dialTLS := func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
host, _, err := net.SplitHostPort(addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rawConn, err := stdDialer.DialContext(ctx, network, addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cfg := &tls.Config{
|
|
ServerName: host,
|
|
NextProtos: []string{"h2", "http/1.1"},
|
|
}
|
|
tlsConn := tls.UClient(rawConn, cfg, tls.HelloChrome_Auto)
|
|
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
|
_ = rawConn.Close()
|
|
return nil, err
|
|
}
|
|
return tlsConn, nil
|
|
}
|
|
transport := &http.Transport{
|
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
return stdDialer.DialContext(ctx, network, addr)
|
|
},
|
|
DialTLSContext: dialTLS,
|
|
MaxIdleConns: 16,
|
|
MaxIdleConnsPerHost: 8,
|
|
IdleConnTimeout: 30 * time.Second,
|
|
}
|
|
return &Client{
|
|
hc: &http.Client{
|
|
Transport: transport,
|
|
Timeout: timeout,
|
|
},
|
|
timeout: timeout,
|
|
}
|
|
}
|
|
|
|
// Do 直接转发到内部的 http.Client,可选地应用 Options。
|
|
func (c *Client) Do(req *http.Request, opts ...Option) (*http.Response, error) {
|
|
applyOptions(req, opts)
|
|
return c.hc.Do(req)
|
|
}
|
|
|
|
// Get 是 GET 的便捷封装。
|
|
func (c *Client) Get(ctx context.Context, url string, opts ...Option) (*http.Response, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return c.Do(req, opts...)
|
|
}
|
|
|
|
// applyOptions 把 Options 里的字段塞进 req 的 Header。
|
|
func applyOptions(req *http.Request, opts []Option) {
|
|
if len(opts) == 0 {
|
|
return
|
|
}
|
|
o := Options{}
|
|
for _, fn := range opts {
|
|
fn(&o)
|
|
}
|
|
if o.Referer != "" {
|
|
req.Header.Set("Referer", o.Referer)
|
|
}
|
|
if o.ChromeHeaders {
|
|
req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36")
|
|
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
|
|
req.Header.Set("Sec-CH-UA", `"Not:A-Brand";v="24", "Chromium";v="134"`)
|
|
req.Header.Set("Sec-CH-UA-Mobile", "?0")
|
|
req.Header.Set("Sec-CH-UA-Platform", `"macOS"`)
|
|
req.Header.Set("Sec-Fetch-Dest", "document")
|
|
req.Header.Set("Sec-Fetch-Mode", "navigate")
|
|
req.Header.Set("Sec-Fetch-Site", "none")
|
|
req.Header.Set("Sec-Fetch-User", "?1")
|
|
req.Header.Set("Upgrade-Insecure-Requests", "1")
|
|
}
|
|
if o.Accept != "" {
|
|
req.Header.Set("Accept", o.Accept)
|
|
} else if o.ChromeHeaders {
|
|
// Chrome 默认 HTML Accept。
|
|
req.Header.Set("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")
|
|
}
|
|
}
|
|
|