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 }