// 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") } }