src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

client.go (8949B)


      1 package http
      2 
      3 import (
      4 	"crypto/tls"
      5 	"github.com/aws/aws-sdk-go-v2/aws"
      6 	"net"
      7 	"net/http"
      8 	"reflect"
      9 	"sync"
     10 	"time"
     11 )
     12 
     13 // Defaults for the HTTPTransportBuilder.
     14 var (
     15 	// Default connection pool options
     16 	DefaultHTTPTransportMaxIdleConns        = 100
     17 	DefaultHTTPTransportMaxIdleConnsPerHost = 10
     18 
     19 	// Default connection timeouts
     20 	DefaultHTTPTransportIdleConnTimeout       = 90 * time.Second
     21 	DefaultHTTPTransportTLSHandleshakeTimeout = 10 * time.Second
     22 	DefaultHTTPTransportExpectContinueTimeout = 1 * time.Second
     23 
     24 	// Default to TLS 1.2 for all HTTPS requests.
     25 	DefaultHTTPTransportTLSMinVersion uint16 = tls.VersionTLS12
     26 )
     27 
     28 // Timeouts for net.Dialer's network connection.
     29 var (
     30 	DefaultDialConnectTimeout   = 30 * time.Second
     31 	DefaultDialKeepAliveTimeout = 30 * time.Second
     32 )
     33 
     34 // BuildableClient provides a HTTPClient implementation with options to
     35 // create copies of the HTTPClient when additional configuration is provided.
     36 //
     37 // The client's methods will not share the http.Transport value between copies
     38 // of the BuildableClient. Only exported member values of the Transport and
     39 // optional Dialer will be copied between copies of BuildableClient.
     40 type BuildableClient struct {
     41 	transport *http.Transport
     42 	dialer    *net.Dialer
     43 
     44 	initOnce sync.Once
     45 
     46 	clientTimeout time.Duration
     47 	client        *http.Client
     48 }
     49 
     50 // NewBuildableClient returns an initialized client for invoking HTTP
     51 // requests.
     52 func NewBuildableClient() *BuildableClient {
     53 	return &BuildableClient{}
     54 }
     55 
     56 // Do implements the HTTPClient interface's Do method to invoke a HTTP request,
     57 // and receive the response. Uses the BuildableClient's current
     58 // configuration to invoke the http.Request.
     59 //
     60 // If connection pooling is enabled (aka HTTP KeepAlive) the client will only
     61 // share pooled connections with its own instance. Copies of the
     62 // BuildableClient will have their own connection pools.
     63 //
     64 // Redirect (3xx) responses will not be followed, the HTTP response received
     65 // will returned instead.
     66 func (b *BuildableClient) Do(req *http.Request) (*http.Response, error) {
     67 	b.initOnce.Do(b.build)
     68 
     69 	return b.client.Do(req)
     70 }
     71 
     72 // Freeze returns a frozen aws.HTTPClient implementation that is no longer a BuildableClient.
     73 // Use this to prevent the SDK from applying DefaultMode configuration values to a buildable client.
     74 func (b *BuildableClient) Freeze() aws.HTTPClient {
     75 	cpy := b.clone()
     76 	cpy.build()
     77 	return cpy.client
     78 }
     79 
     80 func (b *BuildableClient) build() {
     81 	b.client = wrapWithLimitedRedirect(&http.Client{
     82 		Timeout:   b.clientTimeout,
     83 		Transport: b.GetTransport(),
     84 	})
     85 }
     86 
     87 func (b *BuildableClient) clone() *BuildableClient {
     88 	cpy := NewBuildableClient()
     89 	cpy.transport = b.GetTransport()
     90 	cpy.dialer = b.GetDialer()
     91 	cpy.clientTimeout = b.clientTimeout
     92 
     93 	return cpy
     94 }
     95 
     96 // WithTransportOptions copies the BuildableClient and returns it with the
     97 // http.Transport options applied.
     98 //
     99 // If a non (*http.Transport) was set as the round tripper, the round tripper
    100 // will be replaced with a default Transport value before invoking the option
    101 // functions.
    102 func (b *BuildableClient) WithTransportOptions(opts ...func(*http.Transport)) *BuildableClient {
    103 	cpy := b.clone()
    104 
    105 	tr := cpy.GetTransport()
    106 	for _, opt := range opts {
    107 		opt(tr)
    108 	}
    109 	cpy.transport = tr
    110 
    111 	return cpy
    112 }
    113 
    114 // WithDialerOptions copies the BuildableClient and returns it with the
    115 // net.Dialer options applied. Will set the client's http.Transport DialContext
    116 // member.
    117 func (b *BuildableClient) WithDialerOptions(opts ...func(*net.Dialer)) *BuildableClient {
    118 	cpy := b.clone()
    119 
    120 	dialer := cpy.GetDialer()
    121 	for _, opt := range opts {
    122 		opt(dialer)
    123 	}
    124 	cpy.dialer = dialer
    125 
    126 	tr := cpy.GetTransport()
    127 	tr.DialContext = cpy.dialer.DialContext
    128 	cpy.transport = tr
    129 
    130 	return cpy
    131 }
    132 
    133 // WithTimeout Sets the timeout used by the client for all requests.
    134 func (b *BuildableClient) WithTimeout(timeout time.Duration) *BuildableClient {
    135 	cpy := b.clone()
    136 	cpy.clientTimeout = timeout
    137 	return cpy
    138 }
    139 
    140 // GetTransport returns a copy of the client's HTTP Transport.
    141 func (b *BuildableClient) GetTransport() *http.Transport {
    142 	var tr *http.Transport
    143 	if b.transport != nil {
    144 		tr = b.transport.Clone()
    145 	} else {
    146 		tr = defaultHTTPTransport()
    147 	}
    148 
    149 	return tr
    150 }
    151 
    152 // GetDialer returns a copy of the client's network dialer.
    153 func (b *BuildableClient) GetDialer() *net.Dialer {
    154 	var dialer *net.Dialer
    155 	if b.dialer != nil {
    156 		dialer = shallowCopyStruct(b.dialer).(*net.Dialer)
    157 	} else {
    158 		dialer = defaultDialer()
    159 	}
    160 
    161 	return dialer
    162 }
    163 
    164 // GetTimeout returns a copy of the client's timeout to cancel requests with.
    165 func (b *BuildableClient) GetTimeout() time.Duration {
    166 	return b.clientTimeout
    167 }
    168 
    169 func defaultDialer() *net.Dialer {
    170 	return &net.Dialer{
    171 		Timeout:   DefaultDialConnectTimeout,
    172 		KeepAlive: DefaultDialKeepAliveTimeout,
    173 		DualStack: true,
    174 	}
    175 }
    176 
    177 func defaultHTTPTransport() *http.Transport {
    178 	dialer := defaultDialer()
    179 
    180 	tr := &http.Transport{
    181 		Proxy:                 http.ProxyFromEnvironment,
    182 		DialContext:           dialer.DialContext,
    183 		TLSHandshakeTimeout:   DefaultHTTPTransportTLSHandleshakeTimeout,
    184 		MaxIdleConns:          DefaultHTTPTransportMaxIdleConns,
    185 		MaxIdleConnsPerHost:   DefaultHTTPTransportMaxIdleConnsPerHost,
    186 		IdleConnTimeout:       DefaultHTTPTransportIdleConnTimeout,
    187 		ExpectContinueTimeout: DefaultHTTPTransportExpectContinueTimeout,
    188 		ForceAttemptHTTP2:     true,
    189 		TLSClientConfig: &tls.Config{
    190 			MinVersion: DefaultHTTPTransportTLSMinVersion,
    191 		},
    192 	}
    193 
    194 	return tr
    195 }
    196 
    197 // shallowCopyStruct creates a shallow copy of the passed in source struct, and
    198 // returns that copy of the same struct type.
    199 func shallowCopyStruct(src interface{}) interface{} {
    200 	srcVal := reflect.ValueOf(src)
    201 	srcValType := srcVal.Type()
    202 
    203 	var returnAsPtr bool
    204 	if srcValType.Kind() == reflect.Ptr {
    205 		srcVal = srcVal.Elem()
    206 		srcValType = srcValType.Elem()
    207 		returnAsPtr = true
    208 	}
    209 	dstVal := reflect.New(srcValType).Elem()
    210 
    211 	for i := 0; i < srcValType.NumField(); i++ {
    212 		ft := srcValType.Field(i)
    213 		if len(ft.PkgPath) != 0 {
    214 			// unexported fields have a PkgPath
    215 			continue
    216 		}
    217 
    218 		dstVal.Field(i).Set(srcVal.Field(i))
    219 	}
    220 
    221 	if returnAsPtr {
    222 		dstVal = dstVal.Addr()
    223 	}
    224 
    225 	return dstVal.Interface()
    226 }
    227 
    228 // wrapWithLimitedRedirect updates the Client's Transport and CheckRedirect to
    229 // not follow any redirect other than 307 and 308. No other redirect will be
    230 // followed.
    231 //
    232 // If the client does not have a Transport defined will use a new SDK default
    233 // http.Transport configuration.
    234 func wrapWithLimitedRedirect(c *http.Client) *http.Client {
    235 	tr := c.Transport
    236 	if tr == nil {
    237 		tr = defaultHTTPTransport()
    238 	}
    239 
    240 	cc := *c
    241 	cc.CheckRedirect = limitedRedirect
    242 	cc.Transport = suppressBadHTTPRedirectTransport{
    243 		tr: tr,
    244 	}
    245 
    246 	return &cc
    247 }
    248 
    249 // limitedRedirect is a CheckRedirect that prevents the client from following
    250 // any non 307/308 HTTP status code redirects.
    251 //
    252 // The 307 and 308 redirects are allowed because the client must use the
    253 // original HTTP method for the redirected to location. Whereas 301 and 302
    254 // allow the client to switch to GET for the redirect.
    255 //
    256 // Suppresses all redirect requests with a URL of badHTTPRedirectLocation.
    257 func limitedRedirect(r *http.Request, via []*http.Request) error {
    258 	// Request.Response, in CheckRedirect is the response that is triggering
    259 	// the redirect.
    260 	resp := r.Response
    261 	if r.URL.String() == badHTTPRedirectLocation {
    262 		resp.Header.Del(badHTTPRedirectLocation)
    263 		return http.ErrUseLastResponse
    264 	}
    265 
    266 	switch resp.StatusCode {
    267 	case 307, 308:
    268 		// Only allow 307 and 308 redirects as they preserve the method.
    269 		return nil
    270 	}
    271 
    272 	return http.ErrUseLastResponse
    273 }
    274 
    275 // suppressBadHTTPRedirectTransport provides an http.RoundTripper
    276 // implementation that wraps another http.RoundTripper to prevent HTTP client
    277 // receiving 301 and 302 HTTP responses redirects without the required location
    278 // header.
    279 //
    280 // Clients using this utility must have a CheckRedirect, e.g. limitedRedirect,
    281 // that check for responses with having a URL of baseHTTPRedirectLocation, and
    282 // suppress the redirect.
    283 type suppressBadHTTPRedirectTransport struct {
    284 	tr http.RoundTripper
    285 }
    286 
    287 const badHTTPRedirectLocation = `https://amazonaws.com/badhttpredirectlocation`
    288 
    289 // RoundTrip backfills a stub location when a 301/302 response is received
    290 // without a location. This stub location is used by limitedRedirect to prevent
    291 // the HTTP client from failing attempting to use follow a redirect without a
    292 // location value.
    293 func (t suppressBadHTTPRedirectTransport) RoundTrip(r *http.Request) (*http.Response, error) {
    294 	resp, err := t.tr.RoundTrip(r)
    295 	if err != nil {
    296 		return resp, err
    297 	}
    298 
    299 	// S3 is the only known service to return 301 without location header.
    300 	// The Go standard library HTTP client will return an opaque error if it
    301 	// tries to follow a 301/302 response missing the location header.
    302 	switch resp.StatusCode {
    303 	case 301, 302:
    304 		if v := resp.Header.Get("Location"); len(v) == 0 {
    305 			resp.Header.Set("Location", badHTTPRedirectLocation)
    306 		}
    307 	}
    308 
    309 	return resp, err
    310 }