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