Files
phishingclub/backend/vendor/github.com/enetx/surf/http3s.go
Ronni Skansing 07c8adaf76 update vendor deps
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
2025-11-06 23:31:08 +01:00

616 lines
16 KiB
Go

// Package surf provides HTTP/3 support with full uQUIC fingerprinting for advanced web scraping and automation.
// This file implements HTTP/3 transport with complete QUIC Initial Packet + TLS ClientHello fingerprinting,
// SOCKS5 proxy support, and automatic fallback to HTTP/2 for non-SOCKS5 proxies.
package surf
import (
"context"
"crypto/tls"
"fmt"
"math"
"math/rand"
"net"
"net/url"
"strconv"
"time"
"github.com/enetx/g"
"github.com/enetx/g/ref"
"github.com/enetx/http"
"github.com/enetx/surf/pkg/quicconn"
uquic "github.com/enetx/uquic"
"github.com/enetx/uquic/http3"
utls "github.com/enetx/utls"
"github.com/wzshiming/socks5"
)
// HTTP3Settings represents HTTP/3 settings with uQUIC fingerprinting support.
type HTTP3Settings struct {
builder *Builder
quicID *uquic.QUICID
quicSpec *uquic.QUICSpec
}
// Chrome configures HTTP/3 settings to mimic Chrome browser.
func (h *HTTP3Settings) Chrome() *HTTP3Settings {
h.quicID = &uquic.QUICChrome_115
return h
}
// Firefox configures HTTP/3 settings to mimic Firefox browser.
func (h *HTTP3Settings) Firefox() *HTTP3Settings {
h.quicID = &uquic.QUICFirefox_116
return h
}
// SetQUICID sets a custom QUIC ID for fingerprinting.
func (h *HTTP3Settings) SetQUICID(quicID uquic.QUICID) *HTTP3Settings {
h.quicID = &quicID
return h
}
// SetQUICSpec sets a custom QUIC spec for advanced fingerprinting.
func (h *HTTP3Settings) SetQUICSpec(quicSpec uquic.QUICSpec) *HTTP3Settings {
h.quicSpec = &quicSpec
return h
}
// getQUICSpec returns the QUIC spec either from custom spec or by converting QUICID.
// Returns None if neither custom spec nor QUICID is configured or conversion fails.
func (h *HTTP3Settings) getQUICSpec() g.Option[uquic.QUICSpec] {
if h.quicSpec != nil {
return g.Some(*h.quicSpec)
}
if h.quicID != nil {
if spec, err := uquic.QUICID2Spec(*h.quicID); err == nil {
return g.Some(spec)
}
}
return g.None[uquic.QUICSpec]()
}
// Set applies the accumulated HTTP/3 settings.
// It configures the uQUIC transport for the surf client.
func (h *HTTP3Settings) Set() *Builder {
if h.builder.forceHTTP1 {
return h.builder
}
return h.builder.addCliMW(func(c *Client) error {
if !h.builder.singleton {
h.builder.addRespMW(closeIdleConnectionsMW, 0)
}
quicSpec := h.getQUICSpec()
if quicSpec.IsNone() {
return nil
}
tlsConfig := c.tlsConfig.Clone()
transport := &uquicTransport{
quicSpec: ref.Of(quicSpec.Some()),
tlsConfig: tlsConfig,
dialer: c.GetDialer(),
proxy: h.builder.proxy,
fallbackTransport: c.GetTransport(),
cachedConnections: g.NewMapSafe[string, *connection](),
cachedTransports: g.NewMapSafe[string, http.RoundTripper](),
}
switch v := h.builder.proxy.(type) {
case string:
transport.staticProxy = v
transport.isDynamic = false
case g.String:
transport.staticProxy = v.Std()
transport.isDynamic = false
default:
transport.isDynamic = true
}
c.GetClient().Transport = transport
c.transport = transport
return nil
}, math.MaxInt)
}
type connection struct {
packetConn net.PacketConn
quicConn uquic.Connection
}
// uquicTransport implements http.RoundTripper using uQUIC fingerprinting for HTTP/3.
// It provides full QUIC Initial Packet + TLS ClientHello fingerprinting capabilities,
// SOCKS5 proxy compatibility, and automatic fallback to HTTP/2 for non-SOCKS5 proxies.
// The transport supports both static and dynamic proxy configurations with connection caching.
type uquicTransport struct {
quicSpec *uquic.QUICSpec // QUIC specification for fingerprinting
tlsConfig *tls.Config // TLS configuration for QUIC connections
dialer *net.Dialer // Network dialer (may contain custom DNS resolver)
proxy any // Proxy configuration (static or dynamic function)
staticProxy string // Cached static proxy URL for performance
isDynamic bool // Flag indicating if proxy is dynamic (disables caching)
cachedConnections *g.MapSafe[string, *connection]
cachedTransports *g.MapSafe[string, http.RoundTripper] // Per-address HTTP/3 transport cache
fallbackTransport http.RoundTripper // HTTP/2 transport for non-SOCKS5 proxy fallback
}
// CloseIdleConnections closes all cached HTTP/3 connections and clears the cache.
// It also attempts to close idle connections on the fallback transport if available.
func (ut *uquicTransport) CloseIdleConnections() {
for k, transport := range ut.cachedTransports.Iter() {
// Check if transport implements CloseIdleConnections
switch t := transport.(type) {
case *http3.RoundTripper:
t.CloseIdleConnections()
case *http3.URoundTripper:
t.CloseIdleConnections()
}
ut.cachedTransports.Delete(k)
}
for id, c := range ut.cachedConnections.Iter() {
if c.quicConn != nil {
_ = c.quicConn.CloseWithError(0, "idle close")
}
if c.packetConn != nil {
_ = c.packetConn.Close()
}
ut.cachedConnections.Delete(id)
}
if ut.fallbackTransport != nil {
if closer, ok := ut.fallbackTransport.(interface{ CloseIdleConnections() }); ok {
closer.CloseIdleConnections()
}
}
}
func (ut *uquicTransport) address(req *http.Request) string {
host, port, err := net.SplitHostPort(req.URL.Host)
if err == nil {
return net.JoinHostPort(host, port)
}
var defaultPort string
switch g.String(req.URL.Scheme).Lower() {
case "http":
defaultPort = defaultHTTPPort
case "https":
defaultPort = defaultHTTPSPort
default:
defaultPort = defaultHTTPSPort
}
return net.JoinHostPort(req.URL.Host, defaultPort)
}
// createH3 returns per-address cached http3.Transport with proper Dial & SNI configuration.
// Caching is disabled for dynamic proxy configurations to ensure proper proxy rotation.
func (ut *uquicTransport) createH3(req *http.Request, addr, proxy string) http.RoundTripper {
key := addr
if proxy != "" {
key = proxy + "|" + addr
}
// Skip cache for dynamic proxy providers to ensure proxy rotation works correctly
if !ut.isDynamic {
if tr := ut.cachedTransports.Get(key); tr.IsSome() {
return tr.Some()
}
}
// Create uquic/http3 RoundTripper (with or without full QUIC fingerprinting)
base := &http3.RoundTripper{
TLSClientConfig: tlsToUTLS(ut.tlsConfig),
QuicConfig: &uquic.Config{},
}
var h3 http.RoundTripper
if ut.quicSpec != nil {
h3 = http3.GetURoundTripper(base, ut.quicSpec, nil)
} else {
h3 = base
}
if (ut.dialer != nil && ut.dialer.Resolver != nil) || proxy != "" {
hostname := req.URL.Hostname()
// Create common dial function
dialFunc := func(ctx context.Context, quicAddr string, tlsCfg *utls.Config, cfg *uquic.Config) (uquic.EarlyConnection, error) {
if tlsCfg == nil {
tlsCfg = &utls.Config{}
}
if tlsCfg.ServerName == "" {
if hn := hostname; hn != "" && net.ParseIP(hn) == nil {
clone := tlsCfg.Clone()
clone.ServerName = hn
tlsCfg = clone
}
}
if proxy != "" {
return ut.dialSOCKS5(ctx, quicAddr, tlsCfg, cfg, proxy)
}
return ut.dialDNS(ctx, quicAddr, tlsCfg, cfg)
}
// Configure custom dial function for uquic/http3
switch rt := h3.(type) {
case *http3.URoundTripper:
if rt.RoundTripper != nil {
rt.RoundTripper.Dial = dialFunc
}
rt.Dial = dialFunc
case *http3.RoundTripper:
rt.Dial = dialFunc
}
}
// Only cache transport if not using dynamic proxy provider
if !ut.isDynamic {
ut.cachedTransports.Set(key, h3)
}
return h3
}
// resolve always resolves host:port to ip:port.
// Uses custom resolver when provided, otherwise the system resolver.
func (ut *uquicTransport) resolve(ctx context.Context, address string) (string, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return "", fmt.Errorf("invalid address format: %w", err)
}
// Skip resolution for IP addresses
if ip := net.ParseIP(host); ip != nil {
return address, nil
}
r := net.DefaultResolver
if ut.dialer != nil && ut.dialer.Resolver != nil {
r = ut.dialer.Resolver
}
ips, err := r.LookupIPAddr(ctx, host)
if err != nil {
return "", fmt.Errorf("lookup failed for %q: %w", host, err)
}
if len(ips) == 0 {
return "", &net.DNSError{Err: "no IP addresses found", Name: host}
}
// Prefer IPv4 addresses for better compatibility
for _, ipa := range ips {
if v4 := ipa.IP.To4(); v4 != nil {
return net.JoinHostPort(v4.String(), port), nil
}
}
// Fallback to first IPv6 address
return net.JoinHostPort(ips[0].IP.String(), port), nil
}
const (
minPort = 1
maxPort = 65535
)
// parsedAddr represents validated network address components
type parsedAddr struct {
IP net.IP
Port int
}
// parseResolvedAddress validates and parses a resolved address
func parseResolvedAddress(resolved string) (*parsedAddr, error) {
host, portStr, err := net.SplitHostPort(resolved)
if err != nil {
return nil, fmt.Errorf("split host/port: %w", err)
}
port, err := strconv.Atoi(portStr)
if err != nil {
return nil, fmt.Errorf("parse port %q: %w", portStr, err)
}
if port < minPort || port > maxPort {
return nil, fmt.Errorf("port %d out of valid range [%d-%d]", port, minPort, maxPort)
}
ip := net.ParseIP(host)
if ip == nil {
return nil, fmt.Errorf("invalid IP address: %q", host)
}
return &parsedAddr{
IP: ip,
Port: port,
}, nil
}
// createUDPListener creates a UDP listener with fallback support
func createUDPListener(preferredNetwork string) (*net.UDPConn, error) {
// Try preferred network first
conn, err := net.ListenUDP(preferredNetwork, nil)
if err == nil {
return conn, nil
}
// If specific version failed, try generic UDP
if preferredNetwork != "udp" {
conn, err = net.ListenUDP("udp", nil)
if err == nil {
return conn, nil
}
}
return nil, fmt.Errorf("failed to create UDP listener on %s: %w", preferredNetwork, err)
}
// RoundTrip implements the http.RoundTripper interface with HTTP/3 support and automatic proxy fallback.
// For non-SOCKS5 proxies, it automatically falls back to the HTTP/2 transport.
// Dynamic proxy configurations are evaluated on each request for proper rotation.
func (ut *uquicTransport) RoundTrip(req *http.Request) (*http.Response, error) {
var proxy string
if ut.isDynamic {
if p := ut.getProxy(); p.IsSome() {
proxy = p.Some()
}
} else {
proxy = ut.staticProxy
}
if proxy != "" && !isSOCKS5(proxy) && ut.fallbackTransport != nil {
return ut.fallbackTransport.RoundTrip(req)
}
if req.URL.Scheme == "" {
clone := *req.URL
clone.Scheme = "https"
req.URL = &clone
}
addr := ut.address(req)
h3 := ut.createH3(req, addr, proxy)
return h3.RoundTrip(req)
}
// getProxy extracts proxy URL from configured proxy source.
// Supports static (string, []string) and dynamic (func() g.String) configurations.
// Returns g.Option[string] - Some(proxy_url) if proxy is available, None if no proxy is configured.
func (ut *uquicTransport) getProxy() g.Option[string] {
var p string
switch v := ut.proxy.(type) {
case func() g.String:
p = v().Std()
case string:
p = v
case g.String:
p = v.Std()
case []string:
if len(v) > 0 {
p = v[rand.Intn(len(v))]
}
case g.Slice[string]:
p = v.Random()
case g.Slice[g.String]:
p = v.Random().Std()
}
if p != "" {
return g.Some(p)
}
return g.None[string]()
}
// tlsToUTLS converts standard tls.Config to utls.Config with minimal compatibility
func tlsToUTLS(tlsConf *tls.Config) *utls.Config {
if tlsConf == nil {
return &utls.Config{}
}
return &utls.Config{
ServerName: tlsConf.ServerName,
InsecureSkipVerify: tlsConf.InsecureSkipVerify,
NextProtos: tlsConf.NextProtos,
RootCAs: tlsConf.RootCAs,
MinVersion: tlsConf.MinVersion,
MaxVersion: tlsConf.MaxVersion,
CipherSuites: tlsConf.CipherSuites,
}
}
// dialSOCKS5 establishes a QUIC connection through a SOCKS5 proxy (for uquic)
func (ut *uquicTransport) dialSOCKS5(
ctx context.Context,
address string,
tlsConfig *utls.Config,
cfg *uquic.Config,
proxy string,
) (uquic.EarlyConnection, error) {
// Validate proxy URL
proxyURL, err := url.Parse(proxy)
if err != nil {
return nil, fmt.Errorf("parse proxy URL: %w", err)
}
// Create SOCKS5 dialer
dialer, err := socks5.NewDialer(proxyURL.String())
if err != nil {
return nil, fmt.Errorf("create SOCKS5 dialer: %w", err)
}
// Resolve target address
resolved, err := ut.resolve(ctx, address)
if err != nil {
return nil, fmt.Errorf("resolve address: %w", err)
}
// Establish SOCKS5 UDP associate
conn, err := dialer.DialContext(ctx, "udp", resolved)
if err != nil {
return nil, fmt.Errorf("SOCKS5 UDP associate: %w", err)
}
// Ensure cleanup on error
success := false
defer func() {
if !success {
_ = conn.Close()
}
}()
proxyUDP, err := net.ResolveUDPAddr("udp", conn.RemoteAddr().String())
if err != nil {
return nil, fmt.Errorf("socks5 get proxy UDP addr: %w", err)
}
// Create packet connection wrapper
packetConn := quicconn.New(conn, proxyUDP, quicconn.EncapRaw)
// Ensure QUIC config exists
if cfg == nil {
cfg = &uquic.Config{}
}
// Establish QUIC connection with uquic
quicConn, err := uquic.DialEarly(ctx, packetConn, proxyUDP, tlsConfig, cfg)
if err != nil {
_ = packetConn.Close()
return nil, fmt.Errorf("QUIC dial failed: %w", err)
}
success = true
// Cache connection for reuse
if ut.cachedConnections != nil {
key := quicconn.ConnKey(packetConn)
ut.cachedConnections.Set(key, &connection{
packetConn: packetConn,
quicConn: quicConn,
})
}
return quicConn, nil
}
// dialDNS establishes a QUIC connection using custom DNS resolver (for uquic)
func (ut *uquicTransport) dialDNS(
ctx context.Context,
address string,
tlsConfig *utls.Config,
cfg *uquic.Config,
) (uquic.EarlyConnection, error) {
// Resolve address using custom DNS
resolved, err := ut.resolve(ctx, address)
if err != nil {
return nil, fmt.Errorf("DNS resolution failed: %w", err)
}
// Parse and validate resolved address
addr, err := parseResolvedAddress(resolved)
if err != nil {
return nil, err
}
// Determine optimal network type
network := "udp"
if addr.IP.To4() != nil {
network = "udp4"
} else if addr.IP.To16() != nil {
network = "udp6"
}
// Create UDP listener with fallback
udpConn, err := createUDPListener(network)
if err != nil {
return nil, fmt.Errorf("create UDP listener: %w", err)
}
// Ensure cleanup on error
success := false
defer func() {
if !success {
_ = udpConn.Close()
}
}()
// Create target address
targetAddr := &net.UDPAddr{
IP: addr.IP,
Port: addr.Port,
}
// Set deadline for dial operation only
if deadline, ok := ctx.Deadline(); ok {
if err := udpConn.SetDeadline(deadline); err != nil {
return nil, fmt.Errorf("set dial deadline: %w", err)
}
defer func() {
if success {
_ = udpConn.SetDeadline(time.Time{})
}
}()
}
// Ensure QUIC config exists
if cfg == nil {
cfg = &uquic.Config{}
}
// Establish QUIC connection with uquic
quicConn, err := uquic.DialEarly(ctx, udpConn, targetAddr, tlsConfig, cfg)
if err != nil {
return nil, fmt.Errorf("QUIC dial failed: %w", err)
}
success = true
// Cache connection for reuse
if ut.cachedConnections != nil {
key := quicconn.ConnKey(udpConn)
ut.cachedConnections.Set(key, &connection{
packetConn: udpConn,
quicConn: quicConn,
})
}
return quicConn, nil
}
// isSOCKS5 checks if the given proxy URL is a SOCKS5 proxy supporting UDP.
// Only SOCKS5 proxies are compatible with QUIC/HTTP3 due to UDP requirements.
func isSOCKS5(proxyURL string) bool {
if proxyURL == "" {
return false
}
u, err := url.Parse(proxyURL)
if err != nil {
return false
}
scheme := u.Scheme
return scheme == "socks5" || scheme == "socks5h"
}