Files

378 lines
11 KiB
Go

// Transport adapters unify fasthttp clients behind a shared interface so the
// Fiber client can coordinate behavior like redirects, TLS overrides, and
// dial customizations regardless of the underlying transport type.
package client
import (
"bytes"
"crypto/tls"
"time"
"github.com/valyala/fasthttp"
)
// defaultRedirectLimit mirrors fasthttp's default when callers supply a negative redirect cap.
const defaultRedirectLimit = 16
var (
// Pre-allocated byte slice for http/https scheme comparison
httpScheme = []byte("http")
httpsScheme = []byte("https")
)
// httpClientTransport unifies the operations exposed by the Fiber client across
// the fasthttp.Client, fasthttp.HostClient, and fasthttp.LBClient adapters so
// helper logic can treat the concrete transports uniformly.
type httpClientTransport interface {
Do(req *fasthttp.Request, resp *fasthttp.Response) error
DoTimeout(req *fasthttp.Request, resp *fasthttp.Response, timeout time.Duration) error
DoDeadline(req *fasthttp.Request, resp *fasthttp.Response, deadline time.Time) error
DoRedirects(req *fasthttp.Request, resp *fasthttp.Response, maxRedirects int) error
CloseIdleConnections()
TLSConfig() *tls.Config
SetTLSConfig(config *tls.Config)
SetDial(dial fasthttp.DialFunc)
Client() any
StreamResponseBody() bool
SetStreamResponseBody(enable bool)
}
// standardClientTransport adapts fasthttp.Client to the httpClientTransport
// interface used by Fiber's client helpers.
type standardClientTransport struct {
client *fasthttp.Client
}
func newStandardClientTransport(client *fasthttp.Client) *standardClientTransport {
return &standardClientTransport{client: client}
}
func (s *standardClientTransport) Do(req *fasthttp.Request, resp *fasthttp.Response) error {
return s.client.Do(req, resp)
}
func (s *standardClientTransport) DoTimeout(req *fasthttp.Request, resp *fasthttp.Response, timeout time.Duration) error {
return s.client.DoTimeout(req, resp, timeout)
}
func (s *standardClientTransport) DoDeadline(req *fasthttp.Request, resp *fasthttp.Response, deadline time.Time) error {
return s.client.DoDeadline(req, resp, deadline)
}
func (s *standardClientTransport) DoRedirects(req *fasthttp.Request, resp *fasthttp.Response, maxRedirects int) error {
return s.client.DoRedirects(req, resp, maxRedirects)
}
func (s *standardClientTransport) CloseIdleConnections() {
s.client.CloseIdleConnections()
}
func (s *standardClientTransport) TLSConfig() *tls.Config {
return s.client.TLSConfig
}
func (s *standardClientTransport) SetTLSConfig(config *tls.Config) {
s.client.TLSConfig = config
}
func (s *standardClientTransport) SetDial(dial fasthttp.DialFunc) {
s.client.Dial = dial
}
func (s *standardClientTransport) Client() any {
return s.client
}
func (s *standardClientTransport) StreamResponseBody() bool {
return s.client.StreamResponseBody
}
func (s *standardClientTransport) SetStreamResponseBody(enable bool) {
s.client.StreamResponseBody = enable
}
// hostClientTransport adapts fasthttp.HostClient to the httpClientTransport
// interface used by Fiber's client helpers.
type hostClientTransport struct {
client *fasthttp.HostClient
}
func newHostClientTransport(client *fasthttp.HostClient) *hostClientTransport {
return &hostClientTransport{client: client}
}
func (h *hostClientTransport) Do(req *fasthttp.Request, resp *fasthttp.Response) error {
return h.client.Do(req, resp)
}
func (h *hostClientTransport) DoTimeout(req *fasthttp.Request, resp *fasthttp.Response, timeout time.Duration) error {
return h.client.DoTimeout(req, resp, timeout)
}
func (h *hostClientTransport) DoDeadline(req *fasthttp.Request, resp *fasthttp.Response, deadline time.Time) error {
return h.client.DoDeadline(req, resp, deadline)
}
func (h *hostClientTransport) DoRedirects(req *fasthttp.Request, resp *fasthttp.Response, maxRedirects int) error {
return h.client.DoRedirects(req, resp, maxRedirects)
}
func (h *hostClientTransport) CloseIdleConnections() {
h.client.CloseIdleConnections()
}
func (h *hostClientTransport) TLSConfig() *tls.Config {
return h.client.TLSConfig
}
func (h *hostClientTransport) SetTLSConfig(config *tls.Config) {
h.client.TLSConfig = config
}
func (h *hostClientTransport) SetDial(dial fasthttp.DialFunc) {
h.client.Dial = dial
}
func (h *hostClientTransport) Client() any {
return h.client
}
func (h *hostClientTransport) StreamResponseBody() bool {
return h.client.StreamResponseBody
}
func (h *hostClientTransport) SetStreamResponseBody(enable bool) {
h.client.StreamResponseBody = enable
}
// lbClientTransport adapts fasthttp.LBClient to the httpClientTransport
// interface used by Fiber's client helpers.
type lbClientTransport struct {
client *fasthttp.LBClient
}
func newLBClientTransport(client *fasthttp.LBClient) *lbClientTransport {
return &lbClientTransport{client: client}
}
func (l *lbClientTransport) Do(req *fasthttp.Request, resp *fasthttp.Response) error {
return l.client.Do(req, resp)
}
func (l *lbClientTransport) DoTimeout(req *fasthttp.Request, resp *fasthttp.Response, timeout time.Duration) error {
return l.client.DoTimeout(req, resp, timeout)
}
func (l *lbClientTransport) DoDeadline(req *fasthttp.Request, resp *fasthttp.Response, deadline time.Time) error {
return l.client.DoDeadline(req, resp, deadline)
}
// DoRedirects proxies redirect handling through doRedirectsWithClient so the
// load-balanced transport mirrors fasthttp.Client semantics despite
// fasthttp.LBClient not exposing DoRedirects directly.
func (l *lbClientTransport) DoRedirects(req *fasthttp.Request, resp *fasthttp.Response, maxRedirects int) error {
return doRedirectsWithClient(req, resp, maxRedirects, l.client)
}
func (l *lbClientTransport) CloseIdleConnections() {
forEachHostClient(l.client, func(hc *fasthttp.HostClient) {
hc.CloseIdleConnections()
})
}
func (l *lbClientTransport) TLSConfig() *tls.Config {
if len(l.client.Clients) == 0 {
return nil
}
return extractTLSConfig(l.client.Clients)
}
func (l *lbClientTransport) SetTLSConfig(config *tls.Config) {
forEachHostClient(l.client, func(hc *fasthttp.HostClient) {
hc.TLSConfig = config
})
}
func (l *lbClientTransport) SetDial(dial fasthttp.DialFunc) {
forEachHostClient(l.client, func(hc *fasthttp.HostClient) {
hc.Dial = dial
})
}
func (l *lbClientTransport) Client() any {
return l.client
}
func (l *lbClientTransport) StreamResponseBody() bool {
if len(l.client.Clients) == 0 {
return false
}
// Return the StreamResponseBody setting from the first HostClient
var streamEnabled bool
for _, c := range l.client.Clients {
if walkBalancingClientWithBreak(c, func(hc *fasthttp.HostClient) bool {
streamEnabled = hc.StreamResponseBody
return true
}) {
break
}
}
return streamEnabled
}
func (l *lbClientTransport) SetStreamResponseBody(enable bool) {
forEachHostClient(l.client, func(hc *fasthttp.HostClient) {
hc.StreamResponseBody = enable
})
}
// forEachHostClient applies fn to every host client reachable from the provided
// load balancer by recursively following nested balancers and wrapper types.
func forEachHostClient(lb *fasthttp.LBClient, fn func(*fasthttp.HostClient)) {
for _, c := range lb.Clients {
walkBalancingClient(c, fn)
}
}
// walkBalancingClient traverses balancing clients recursively, invoking fn for
// every host client discovered beneath the current node.
func walkBalancingClient(client any, fn func(*fasthttp.HostClient)) {
walkBalancingClientWithBreak(client, func(hc *fasthttp.HostClient) bool {
fn(hc)
return false
})
}
// extractTLSConfig returns the first TLS configuration discovered while walking
// the provided balancing clients so cached settings flow through nested load
// balancers without redundant traversal.
func extractTLSConfig(clients []fasthttp.BalancingClient) *tls.Config {
var cfg *tls.Config
for _, c := range clients {
if walkBalancingClientWithBreak(c, func(hc *fasthttp.HostClient) bool {
if hc.TLSConfig != nil {
cfg = hc.TLSConfig
return true
}
return false
}) {
break
}
}
return cfg
}
// walkBalancingClientWithBreak traverses balancing clients recursively and
// invokes fn for each host client until fn signals success, enabling early
// termination once a match is found.
func walkBalancingClientWithBreak(client any, fn func(*fasthttp.HostClient) bool) bool {
switch c := client.(type) {
case *fasthttp.HostClient:
return fn(c)
case *fasthttp.LBClient:
for _, nestedClient := range c.Clients {
if walkBalancingClientWithBreak(nestedClient, fn) {
return true
}
}
case interface{ LBClient() *fasthttp.LBClient }:
if nested := c.LBClient(); nested != nil {
if walkBalancingClientWithBreak(nested, fn) {
return true
}
}
}
return false
}
// redirectClient describes the minimal Do-capable surface needed by
// doRedirectsWithClient so transports that do not expose DoRedirects (such as
// fasthttp.LBClient) can participate in redirect handling.
type redirectClient interface {
Do(req *fasthttp.Request, resp *fasthttp.Response) error
}
// doRedirectsWithClient mirrors fasthttp's redirect loop for transports that do
// not expose DoRedirects (e.g. fasthttp.LBClient). The helper always issues the
// initial request, respects zero redirect limits, falls back to the default cap
// for negative values, and validates redirect targets before following them.
func doRedirectsWithClient(req *fasthttp.Request, resp *fasthttp.Response, maxRedirects int, client redirectClient) error {
currentURL := req.URI().String()
redirects := 0
singleRequestOnly := maxRedirects <= 0
if maxRedirects < 0 {
maxRedirects = defaultRedirectLimit
singleRequestOnly = false
}
for {
req.SetRequestURI(currentURL)
if err := client.Do(req, resp); err != nil {
return err
}
statusCode := resp.Header.StatusCode()
if !fasthttp.StatusCodeIsRedirect(statusCode) {
return nil
}
if singleRequestOnly {
return nil
}
redirects++
if redirects > maxRedirects {
return fasthttp.ErrTooManyRedirects
}
location := resp.Header.Peek("Location")
if len(location) == 0 {
return fasthttp.ErrMissingLocation
}
nextURL, err := composeRedirectURL(currentURL, location, req.DisableRedirectPathNormalizing)
if err != nil {
return err
}
currentURL = nextURL
if req.Header.IsPost() && (statusCode == fasthttp.StatusMovedPermanently || statusCode == fasthttp.StatusFound || statusCode == fasthttp.StatusSeeOther) {
req.Header.SetMethod(fasthttp.MethodGet)
req.SetBody(nil)
req.Header.Del(fasthttp.HeaderContentType)
req.Header.Del(fasthttp.HeaderContentLength)
}
}
}
// composeRedirectURL resolves a redirect target relative to the current request
// URL while rejecting suspicious payloads (e.g. control characters) and
// restricting schemes to HTTP/S so caller-provided Location headers cannot
// trigger arbitrary transports.
func composeRedirectURL(base string, location []byte, disablePathNormalizing bool) (string, error) {
for _, b := range location {
if b < 0x20 || b == 0x7f {
return "", fasthttp.ErrorInvalidURI
}
}
uri := fasthttp.AcquireURI()
defer fasthttp.ReleaseURI(uri)
uri.Update(base)
uri.UpdateBytes(location)
uri.DisablePathNormalizing = disablePathNormalizing
if scheme := uri.Scheme(); len(scheme) > 0 && !bytes.EqualFold(scheme, httpScheme) && !bytes.EqualFold(scheme, httpsScheme) {
return "", fasthttp.ErrorInvalidURI
}
if len(uri.Scheme()) > 0 && len(uri.Host()) == 0 {
return "", fasthttp.ErrorInvalidURI
}
return uri.String(), nil
}