mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-08-18 16:37:18 +02:00
616 lines
16 KiB
Go
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"
|
|
}
|