mirror of
https://github.com/zap-proto/fiber.git
synced 2026-10-30 15:06:23 +00:00
378 lines
11 KiB
Go
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
|
|
}
|