mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-10-04 14:36:50 +02:00
370 lines
14 KiB
Go
370 lines
14 KiB
Go
package surf
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/enetx/g"
|
|
"github.com/enetx/http"
|
|
"github.com/enetx/surf/profiles"
|
|
)
|
|
|
|
// Builder provides a fluent interface for configuring HTTP clients with various advanced features
|
|
// including proxy settings, TLS fingerprinting, HTTP/2 and HTTP/3 support, retry logic,
|
|
// redirect handling, and browser impersonation capabilities.
|
|
type Builder struct {
|
|
retryCodes g.Slice[int] // HTTP status codes that trigger retries
|
|
proxy g.String // Proxy URL for client connections
|
|
cli *Client // The client being configured
|
|
checkRedirect func(*http.Request, []*http.Request) error // Custom redirect policy function
|
|
http2settings *HTTP2Settings // HTTP/2 specific settings
|
|
http3settings *HTTP3Settings // HTTP/3 specific settings
|
|
cliMWs *middleware[*Client] // Priority-ordered client middlewares
|
|
headersApplier profiles.HeadersApplier // Profile-specific request header pipeline (set by Impersonate)
|
|
retryWait time.Duration // Wait duration between retry attempts
|
|
retryMax int // Maximum number of retry attempts
|
|
maxRedirects int // Maximum number of redirects to follow
|
|
forceHTTP1 bool // Force HTTP/1.1 protocol usage
|
|
forceHTTP2 bool // Force HTTP/2 protocol usage
|
|
forceHTTP3 bool // Force HTTP/3 protocol usage
|
|
cacheBody bool // Enable response body caching
|
|
followOnlyHostRedirects bool // Only follow redirects within same host
|
|
forwardHeadersOnRedirect bool // Preserve headers during redirects
|
|
ja bool // Enable JA3 TLS fingerprinting
|
|
disableCompression bool // Disable automatic response body decompression
|
|
}
|
|
|
|
// Build applies all configured settings and returns the client.
|
|
// Returns g.Result with error if any middleware fails.
|
|
func (b *Builder) Build() g.Result[*Client] {
|
|
if err := b.cliMWs.run(b.cli); err != nil {
|
|
return g.Err[*Client](err)
|
|
}
|
|
|
|
return g.Ok(b.cli)
|
|
}
|
|
|
|
// With registers middleware into the client builder with optional priority.
|
|
//
|
|
// It accepts one of the following middleware function types:
|
|
// - func(*surf.Client) error — client middleware, modifies or initializes the client
|
|
// - func(*surf.Request) error — request middleware, intercepts or transforms outgoing requests
|
|
// - func(*surf.Response) error — response middleware, intercepts or transforms incoming responses
|
|
//
|
|
// Parameters:
|
|
// - middleware: A function matching one of the supported middleware types.
|
|
// - priority (optional): Integer priority level. Lower values run earlier. Defaults to 0.
|
|
//
|
|
// Middleware with the same priority are executed in order of insertion (FIFO).
|
|
// If the middleware type is not recognized, With panics with an informative error.
|
|
//
|
|
// Example:
|
|
//
|
|
// // Adding client middleware to modify client settings.
|
|
// .With(func(client *surf.Client) error {
|
|
// // Custom logic to modify the client settings.
|
|
// return nil
|
|
// })
|
|
//
|
|
// // Adding request middleware to intercept outgoing requests.
|
|
// .With(func(req *surf.Request) error {
|
|
// // Custom logic to modify outgoing requests.
|
|
// return nil
|
|
// })
|
|
//
|
|
// // Adding response middleware to intercept incoming responses.
|
|
// .With(func(resp *surf.Response) error {
|
|
// // Custom logic to handle incoming responses.
|
|
// return nil
|
|
// })
|
|
//
|
|
// Note: Ensure that middleware functions adhere to the specified function signatures to work correctly with the With method.
|
|
func (b *Builder) With(middleware any, priority ...int) *Builder {
|
|
p := g.Int(g.Slice[int](priority).Get(0).UnwrapOrDefault())
|
|
|
|
switch v := middleware.(type) {
|
|
case func(*Client) error:
|
|
b.addCliMW(v, p)
|
|
case func(*Request) error:
|
|
b.addReqMW(v, p)
|
|
case func(*Response) error:
|
|
b.addRespMW(v, p)
|
|
default:
|
|
panic(fmt.Sprintf("invalid middleware type: %T", v))
|
|
}
|
|
|
|
return b
|
|
}
|
|
|
|
// addCliMW adds a client middleware to the ClientBuilder.
|
|
func (b *Builder) addCliMW(m func(*Client) error, priority g.Int) *Builder {
|
|
b.cliMWs.add(priority, m)
|
|
return b
|
|
}
|
|
|
|
// addReqMW adds a request middleware to the ClientBuilder.
|
|
func (b *Builder) addReqMW(m func(*Request) error, priority g.Int) *Builder {
|
|
b.cli.reqMWs.add(priority, m)
|
|
return b
|
|
}
|
|
|
|
// addRespMW adds a response middleware to the ClientBuilder.
|
|
func (b *Builder) addRespMW(m func(*Response) error, priority g.Int) *Builder {
|
|
b.cli.respMWs.add(priority, m)
|
|
return b
|
|
}
|
|
|
|
func (b *Builder) Boundary(boundary func() g.String) *Builder {
|
|
return b.addCliMW(func(client *Client) error { return boundaryMW(client, boundary) }, 999)
|
|
}
|
|
|
|
// SecureTLS enables TLS certificate verification.
|
|
// By default surf skips certificate verification (InsecureSkipVerify=true).
|
|
// Call this for production use where certificate validation is required.
|
|
func (b *Builder) SecureTLS() *Builder {
|
|
b.cli.tlsConfig.InsecureSkipVerify = false
|
|
return b
|
|
}
|
|
|
|
// WebSocketGuard enables middleware that blocks WebSocket upgrade responses (HTTP 101).
|
|
// Without this, surf allows 101 Switching Protocols to pass through — compatible with
|
|
// websocket.Dial and other WebSocket libraries that use surf.Std() as HTTPClient.
|
|
// Enable this only if you want to explicitly reject unexpected WebSocket upgrades.
|
|
func (b *Builder) WebSocketGuard() *Builder {
|
|
b.addReqMW(got101ResponseMW, 0)
|
|
b.addRespMW(webSocketUpgradeErrorMW, 0)
|
|
|
|
return b
|
|
}
|
|
|
|
// H2C configures the client to handle HTTP/2 Cleartext (h2c).
|
|
func (b *Builder) H2C() *Builder { return b.addCliMW(h2cMW, 999) }
|
|
|
|
// HTTP2Settings configures settings related to HTTP/2 and returns an http2s struct.
|
|
func (b *Builder) HTTP2Settings() *HTTP2Settings {
|
|
h2 := &HTTP2Settings{builder: b}
|
|
b.http2settings = h2
|
|
|
|
return h2
|
|
}
|
|
|
|
// HTTP3Settings configures settings related to HTTP/3 and returns an http3s struct.
|
|
func (b *Builder) HTTP3Settings() *HTTP3Settings {
|
|
h3 := &HTTP3Settings{builder: b}
|
|
b.http3settings = h3
|
|
|
|
return h3
|
|
}
|
|
|
|
// ForceHTTP3 configures the client to use HTTP/3 forcefully.
|
|
func (b *Builder) ForceHTTP3() *Builder {
|
|
b.forceHTTP3 = true
|
|
return b
|
|
}
|
|
|
|
// Impersonate configures something related to impersonation and returns an impersonate struct.
|
|
func (b *Builder) Impersonate() *Impersonate { return &Impersonate{builder: b} }
|
|
|
|
// JA configures the client to use a specific TLS fingerprint.
|
|
func (b *Builder) JA() *JA {
|
|
b.ja = true
|
|
return &JA{builder: b}
|
|
}
|
|
|
|
// UnixSocket sets the path for a Unix domain socket.
|
|
// This allows the HTTP client to connect to the server using a Unix domain
|
|
// socket instead of a traditional TCP/IP connection.
|
|
func (b *Builder) UnixSocket(address g.String) *Builder {
|
|
return b.addCliMW(func(client *Client) error { return unixSocketMW(client, address) }, 0)
|
|
}
|
|
|
|
// DNS sets the custom DNS resolver address.
|
|
func (b *Builder) DNS(dns g.String) *Builder {
|
|
return b.addCliMW(func(client *Client) error { return dnsMW(client, dns) }, 0)
|
|
}
|
|
|
|
// DNSOverTLS configures the client to use DNS over TLS.
|
|
func (b *Builder) DNSOverTLS() *DNSOverTLS { return &DNSOverTLS{builder: b} }
|
|
|
|
// Timeout sets the timeout duration for the client.
|
|
func (b *Builder) Timeout(timeout time.Duration) *Builder {
|
|
return b.addCliMW(func(client *Client) error { return timeoutMW(client, timeout) }, 0)
|
|
}
|
|
|
|
// TLSConfig sets a custom TLS configuration for the client.
|
|
func (b *Builder) TLSConfig(config *tls.Config) *Builder {
|
|
return b.addCliMW(func(client *Client) error { return tlsConfigMW(client, config) }, 0)
|
|
}
|
|
|
|
// InterfaceAddr sets the local network interface for outbound connections.
|
|
// Accepts either an IP address (e.g., "192.168.1.100", "::1") or an interface name (e.g., "eth0", "en0").
|
|
func (b *Builder) InterfaceAddr(address g.String) *Builder {
|
|
return b.addCliMW(func(client *Client) error { return interfaceAddrMW(client, address) }, 0)
|
|
}
|
|
|
|
// Proxy sets the proxy URL for the client.
|
|
func (b *Builder) Proxy(proxy g.String) *Builder {
|
|
b.proxy = proxy
|
|
return b.addCliMW(func(client *Client) error { return proxyMW(client, proxy) }, 0)
|
|
}
|
|
|
|
// BasicAuth sets the basic authentication credentials for the client.
|
|
func (b *Builder) BasicAuth(authentication g.String) *Builder {
|
|
return b.addReqMW(func(req *Request) error { return basicAuthMW(req, authentication) }, 900)
|
|
}
|
|
|
|
// BearerAuth sets the bearer token for the client.
|
|
func (b *Builder) BearerAuth(authentication g.String) *Builder {
|
|
return b.addReqMW(func(req *Request) error { return bearerAuthMW(req, authentication) }, 901)
|
|
}
|
|
|
|
// UserAgent sets the user agent for the client.
|
|
func (b *Builder) UserAgent(userAgent any) *Builder {
|
|
return b.addReqMW(func(req *Request) error { return userAgentMW(req, userAgent) }, 0)
|
|
}
|
|
|
|
// SetHeaders sets headers for the request, replacing existing ones with the same name.
|
|
func (b *Builder) SetHeaders(headers ...any) *Builder {
|
|
return b.addReqMW(func(r *Request) error {
|
|
r.SetHeaders(headers...)
|
|
return nil
|
|
}, 0)
|
|
}
|
|
|
|
// AddHeaders adds headers to the request, appending to any existing headers with the same name.
|
|
func (b *Builder) AddHeaders(headers ...any) *Builder {
|
|
return b.addReqMW(func(r *Request) error {
|
|
r.AddHeaders(headers...)
|
|
return nil
|
|
}, 0)
|
|
}
|
|
|
|
// AddCookies adds cookies to the request.
|
|
func (b *Builder) AddCookies(cookies ...*http.Cookie) *Builder {
|
|
return b.addReqMW(func(r *Request) error {
|
|
r.AddCookies(cookies...)
|
|
return nil
|
|
}, 0)
|
|
}
|
|
|
|
// WithContext associates the provided context with the request.
|
|
func (b *Builder) WithContext(ctx context.Context) *Builder {
|
|
return b.addReqMW(func(r *Request) error {
|
|
r.WithContext(ctx)
|
|
return nil
|
|
}, 0)
|
|
}
|
|
|
|
// ContentType sets the content type for the client.
|
|
func (b *Builder) ContentType(contentType g.String) *Builder {
|
|
return b.addReqMW(func(req *Request) error { return contentTypeMW(req, contentType) }, 0)
|
|
}
|
|
|
|
// CacheBody configures whether the client should cache the body of the response.
|
|
func (b *Builder) CacheBody() *Builder {
|
|
b.cacheBody = true
|
|
return b
|
|
}
|
|
|
|
// GetRemoteAddress configures whether the client should get the remote address.
|
|
func (b *Builder) GetRemoteAddress() *Builder { return b.addReqMW(remoteAddrMW, 0) }
|
|
|
|
// DisableKeepAlive disable keep-alive connections.
|
|
func (b *Builder) DisableKeepAlive() *Builder { return b.addCliMW(disableKeepAliveMW, 0) }
|
|
|
|
// DisableCompression disables automatic response body decompression.
|
|
func (b *Builder) DisableCompression() *Builder {
|
|
b.disableCompression = true
|
|
return b
|
|
}
|
|
|
|
// Retry configures the retry behavior of the client.
|
|
//
|
|
// Parameters:
|
|
//
|
|
// retryMax: Maximum number of retry attempts. If zero or negative the
|
|
// retry loop is disabled.
|
|
// retryWait: Minimum wait between retries. If the server responds with a
|
|
// Retry-After header on a retryable status, the actual pause
|
|
// becomes max(retryWait, Retry-After). Both legal forms are
|
|
// honoured: integer delay-seconds and HTTP-date (IMF-fixdate,
|
|
// RFC 850, ANSI C asctime). Absent or malformed values fall
|
|
// back to retryWait.
|
|
// codes: Optional list of HTTP status codes that trigger retries.
|
|
// If no codes are provided, the defaults are used
|
|
// (500 Internal Server Error, 429 Too Many Requests,
|
|
// 503 Service Unavailable).
|
|
//
|
|
// Upper bound for the entire retry cycle, including a long Retry-After
|
|
// sleep, is the request context deadline — set it with
|
|
// Request.WithContext(ctxWithDeadline). Builder.Timeout only bounds a
|
|
// single cli.Do invocation (connect + transmission + body read); it does
|
|
// not interrupt the sleep between retries.
|
|
func (b *Builder) Retry(retryMax int, retryWait time.Duration, codes ...int) *Builder {
|
|
b.retryMax = retryMax
|
|
b.retryWait = retryWait
|
|
|
|
if len(codes) == 0 {
|
|
b.retryCodes = g.SliceOf(
|
|
http.StatusInternalServerError,
|
|
http.StatusTooManyRequests,
|
|
http.StatusServiceUnavailable,
|
|
)
|
|
} else {
|
|
b.retryCodes = g.SliceOf(codes...)
|
|
}
|
|
|
|
return b
|
|
}
|
|
|
|
// ForceHTTP1 configures the client to use HTTP/1.1 forcefully.
|
|
func (b *Builder) ForceHTTP1() *Builder {
|
|
b.forceHTTP1 = true
|
|
return b.addCliMW(forceHTTP1MW, 0)
|
|
}
|
|
|
|
// ForceHTTP2 configures the client to use HTTP/2 forcefully.
|
|
func (b *Builder) ForceHTTP2() *Builder {
|
|
b.forceHTTP2 = true
|
|
return b.addCliMW(forceHTTP2MW, 0)
|
|
}
|
|
|
|
// Session configures whether the client should maintain a session.
|
|
func (b *Builder) Session() *Builder { return b.addCliMW(sessionMW, 0) }
|
|
|
|
// MaxRedirects sets the maximum number of redirects the client should follow.
|
|
func (b *Builder) MaxRedirects(maxRedirects int) *Builder {
|
|
b.maxRedirects = maxRedirects
|
|
return b.addCliMW(redirectPolicyMW, 0)
|
|
}
|
|
|
|
// NotFollowRedirects disables following redirects for the client.
|
|
func (b *Builder) NotFollowRedirects() *Builder {
|
|
return b.RedirectPolicy(func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse })
|
|
}
|
|
|
|
// FollowOnlyHostRedirects configures whether the client should only follow redirects within the
|
|
// same host.
|
|
func (b *Builder) FollowOnlyHostRedirects() *Builder {
|
|
b.followOnlyHostRedirects = true
|
|
return b.addCliMW(redirectPolicyMW, 0)
|
|
}
|
|
|
|
// ForwardHeadersOnRedirect adds a middleware to the ClientBuilder object that ensures HTTP headers are
|
|
// forwarded during a redirect.
|
|
func (b *Builder) ForwardHeadersOnRedirect() *Builder {
|
|
b.forwardHeadersOnRedirect = true
|
|
return b.addCliMW(redirectPolicyMW, 0)
|
|
}
|
|
|
|
// RedirectPolicy sets a custom redirect policy for the client.
|
|
func (b *Builder) RedirectPolicy(fn func(*http.Request, []*http.Request) error) *Builder {
|
|
b.checkRedirect = fn
|
|
return b.addCliMW(redirectPolicyMW, 0)
|
|
}
|
|
|
|
// String generate a string representation of the ClientBuilder instance.
|
|
func (b Builder) String() string { return fmt.Sprintf("%#v", b) }
|