client.go

  1package http
  2
  3import (
  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.
 14var (
 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.
 29var (
 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.
 40type 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.
 52func 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.
 66func (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.
 74func (b *BuildableClient) Freeze() aws.HTTPClient {
 75	cpy := b.clone()
 76	cpy.build()
 77	return cpy.client
 78}
 79
 80func (b *BuildableClient) build() {
 81	b.client = wrapWithLimitedRedirect(&http.Client{
 82		Timeout:   b.clientTimeout,
 83		Transport: b.GetTransport(),
 84	})
 85}
 86
 87func (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.
102func (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.
117func (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.
134func (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.
141func (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.
153func (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.
165func (b *BuildableClient) GetTimeout() time.Duration {
166	return b.clientTimeout
167}
168
169func defaultDialer() *net.Dialer {
170	return &net.Dialer{
171		Timeout:   DefaultDialConnectTimeout,
172		KeepAlive: DefaultDialKeepAliveTimeout,
173		DualStack: true,
174	}
175}
176
177func 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.
199func 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.
234func 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.
257func 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.
283type suppressBadHTTPRedirectTransport struct {
284	tr http.RoundTripper
285}
286
287const 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.
293func (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}