package autorest // Copyright 2017 Microsoft Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import ( "bytes" "crypto/tls" "fmt" "io" "io/ioutil" "log" "net/http" "net/http/cookiejar" "strings" "time" "github.com/Azure/go-autorest/logger" "github.com/Azure/go-autorest/tracing" ) const ( // DefaultPollingDelay is a reasonable delay between polling requests. DefaultPollingDelay = 60 * time.Second // DefaultPollingDuration is a reasonable total polling duration. DefaultPollingDuration = 15 * time.Minute // DefaultRetryAttempts is number of attempts for retry status codes (5xx). DefaultRetryAttempts = 3 // DefaultRetryDuration is the duration to wait between retries. DefaultRetryDuration = 30 * time.Second ) var ( // StatusCodesForRetry are a defined group of status code for which the client will retry StatusCodesForRetry = []int{ http.StatusRequestTimeout, // 408 http.StatusTooManyRequests, // 429 http.StatusInternalServerError, // 500 http.StatusBadGateway, // 502 http.StatusServiceUnavailable, // 503 http.StatusGatewayTimeout, // 504 } ) const ( requestFormat = `HTTP Request Begin =================================================== %s ===================================================== HTTP Request End ` responseFormat = `HTTP Response Begin =================================================== %s ===================================================== HTTP Response End ` ) // Response serves as the base for all responses from generated clients. It provides access to the // last http.Response. type Response struct { *http.Response `json:"-"` } // LoggingInspector implements request and response inspectors that log the full request and // response to a supplied log. type LoggingInspector struct { Logger *log.Logger } // WithInspection returns a PrepareDecorator that emits the http.Request to the supplied logger. The // body is restored after being emitted. // // Note: Since it reads the entire Body, this decorator should not be used where body streaming is // important. It is best used to trace JSON or similar body values. func (li LoggingInspector) WithInspection() PrepareDecorator { return func(p Preparer) Preparer { return PreparerFunc(func(r *http.Request) (*http.Request, error) { var body, b bytes.Buffer defer r.Body.Close() r.Body = ioutil.NopCloser(io.TeeReader(r.Body, &body)) if err := r.Write(&b); err != nil { return nil, fmt.Errorf("Failed to write response: %v", err) } li.Logger.Printf(requestFormat, b.String()) r.Body = ioutil.NopCloser(&body) return p.Prepare(r) }) } } // ByInspecting returns a RespondDecorator that emits the http.Response to the supplied logger. The // body is restored after being emitted. // // Note: Since it reads the entire Body, this decorator should not be used where body streaming is // important. It is best used to trace JSON or similar body values. func (li LoggingInspector) ByInspecting() RespondDecorator { return func(r Responder) Responder { return ResponderFunc(func(resp *http.Response) error { var body, b bytes.Buffer defer resp.Body.Close() resp.Body = ioutil.NopCloser(io.TeeReader(resp.Body, &body)) if err := resp.Write(&b); err != nil { return fmt.Errorf("Failed to write response: %v", err) } li.Logger.Printf(responseFormat, b.String()) resp.Body = ioutil.NopCloser(&body) return r.Respond(resp) }) } } // Client is the base for autorest generated clients. It provides default, "do nothing" // implementations of an Authorizer, RequestInspector, and ResponseInspector. It also returns the // standard, undecorated http.Client as a default Sender. // // Generated clients should also use Error (see NewError and NewErrorWithError) for errors and // return responses that compose with Response. // // Most customization of generated clients is best achieved by supplying a custom Authorizer, custom // RequestInspector, and / or custom ResponseInspector. Users may log requests, implement circuit // breakers (see https://msdn.microsoft.com/en-us/library/dn589784.aspx) or otherwise influence // sending the request by providing a decorated Sender. type Client struct { Authorizer Authorizer Sender Sender RequestInspector PrepareDecorator ResponseInspector RespondDecorator // PollingDelay sets the polling frequency used in absence of a Retry-After HTTP header PollingDelay time.Duration // PollingDuration sets the maximum polling time after which an error is returned. // Setting this to zero will use the provided context to control the duration. PollingDuration time.Duration // RetryAttempts sets the default number of retry attempts for client. RetryAttempts int // RetryDuration sets the delay duration for retries. RetryDuration time.Duration // UserAgent, if not empty, will be set as the HTTP User-Agent header on all requests sent // through the Do method. UserAgent string Jar http.CookieJar // Set to true to skip attempted registration of resource providers (false by default). SkipResourceProviderRegistration bool } // NewClientWithUserAgent returns an instance of a Client with the UserAgent set to the passed // string. func NewClientWithUserAgent(ua string) Client { return newClient(ua, tls.RenegotiateNever) } // ClientOptions contains various Client configuration options. type ClientOptions struct { // UserAgent is an optional user-agent string to append to the default user agent. UserAgent string // Renegotiation is an optional setting to control client-side TLS renegotiation. Renegotiation tls.RenegotiationSupport } // NewClientWithOptions returns an instance of a Client with the specified values. func NewClientWithOptions(options ClientOptions) Client { return newClient(options.UserAgent, options.Renegotiation) } func newClient(ua string, renegotiation tls.RenegotiationSupport) Client { c := Client{ PollingDelay: DefaultPollingDelay, PollingDuration: DefaultPollingDuration, RetryAttempts: DefaultRetryAttempts, RetryDuration: DefaultRetryDuration, UserAgent: UserAgent(), } c.Sender = c.sender(renegotiation) c.AddToUserAgent(ua) return c } // AddToUserAgent adds an extension to the current user agent func (c *Client) AddToUserAgent(extension string) error { if extension != "" { c.UserAgent = fmt.Sprintf("%s %s", c.UserAgent, extension) return nil } return fmt.Errorf("Extension was empty, User Agent stayed as %s", c.UserAgent) } // Do implements the Sender interface by invoking the active Sender after applying authorization. // If Sender is not set, it uses a new instance of http.Client. In both cases it will, if UserAgent // is set, apply set the User-Agent header. func (c Client) Do(r *http.Request) (*http.Response, error) { if r.UserAgent() == "" { r, _ = Prepare(r, WithUserAgent(c.UserAgent)) } // NOTE: c.WithInspection() must be last in the list so that it can inspect all preceding operations r, err := Prepare(r, c.WithAuthorization(), c.WithInspection()) if err != nil { var resp *http.Response if detErr, ok := err.(DetailedError); ok { // if the authorization failed (e.g. invalid credentials) there will // be a response associated with the error, be sure to return it. resp = detErr.Response } return resp, NewErrorWithError(err, "autorest/Client", "Do", nil, "Preparing request failed") } logger.Instance.WriteRequest(r, logger.Filter{ Header: func(k string, v []string) (bool, []string) { // remove the auth token from the log if strings.EqualFold(k, "Authorization") || strings.EqualFold(k, "Ocp-Apim-Subscription-Key") { v = []string{"**REDACTED**"} } return true, v }, }) resp, err := SendWithSender(c.sender(tls.RenegotiateNever), r) logger.Instance.WriteResponse(resp, logger.Filter{}) Respond(resp, c.ByInspecting()) return resp, err } // sender returns the Sender to which to send requests. func (c Client) sender(renengotiation tls.RenegotiationSupport) Sender { if c.Sender == nil { // Use behaviour compatible with DefaultTransport, but require TLS minimum version. var defaultTransport = http.DefaultTransport.(*http.Transport) transport := tracing.Transport // for non-default values of TLS renegotiation create a new tracing transport. // updating tracing.Transport affects all clients which is not what we want. if renengotiation != tls.RenegotiateNever { transport = tracing.NewTransport() } transport.Base = &http.Transport{ Proxy: defaultTransport.Proxy, DialContext: defaultTransport.DialContext, MaxIdleConns: defaultTransport.MaxIdleConns, IdleConnTimeout: defaultTransport.IdleConnTimeout, TLSHandshakeTimeout: defaultTransport.TLSHandshakeTimeout, ExpectContinueTimeout: defaultTransport.ExpectContinueTimeout, TLSClientConfig: &tls.Config{ MinVersion: tls.VersionTLS12, Renegotiation: renengotiation, }, } j, _ := cookiejar.New(nil) return &http.Client{Jar: j, Transport: transport} } return c.Sender } // WithAuthorization is a convenience method that returns the WithAuthorization PrepareDecorator // from the current Authorizer. If not Authorizer is set, it uses the NullAuthorizer. func (c Client) WithAuthorization() PrepareDecorator { return c.authorizer().WithAuthorization() } // authorizer returns the Authorizer to use. func (c Client) authorizer() Authorizer { if c.Authorizer == nil { return NullAuthorizer{} } return c.Authorizer } // WithInspection is a convenience method that passes the request to the supplied RequestInspector, // if present, or returns the WithNothing PrepareDecorator otherwise. func (c Client) WithInspection() PrepareDecorator { if c.RequestInspector == nil { return WithNothing() } return c.RequestInspector } // ByInspecting is a convenience method that passes the response to the supplied ResponseInspector, // if present, or returns the ByIgnoring RespondDecorator otherwise. func (c Client) ByInspecting() RespondDecorator { if c.ResponseInspector == nil { return ByIgnoring() } return c.ResponseInspector }