| // Copyright 2023 Google LLC |
| // |
| // 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. |
| |
| // Package httptransport provides functionality for managing HTTP client |
| // connections to Google Cloud services. |
| package httptransport |
| |
| import ( |
| "crypto/tls" |
| "errors" |
| "fmt" |
| "net/http" |
| |
| "cloud.google.com/go/auth" |
| detect "cloud.google.com/go/auth/credentials" |
| "cloud.google.com/go/auth/internal" |
| "cloud.google.com/go/auth/internal/transport" |
| ) |
| |
| // ClientCertProvider is a function that returns a TLS client certificate to be |
| // used when opening TLS connections. It follows the same semantics as |
| // [crypto/tls.Config.GetClientCertificate]. |
| type ClientCertProvider = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) |
| |
| // Options used to configure a [net/http.Client] from [NewClient]. |
| type Options struct { |
| // DisableTelemetry disables default telemetry (OpenTelemetry). An example |
| // reason to do so would be to bind custom telemetry that overrides the |
| // defaults. |
| DisableTelemetry bool |
| // DisableAuthentication specifies that no authentication should be used. It |
| // is suitable only for testing and for accessing public resources, like |
| // public Google Cloud Storage buckets. |
| DisableAuthentication bool |
| // Headers are extra HTTP headers that will be appended to every outgoing |
| // request. |
| Headers http.Header |
| // BaseRoundTripper overrides the base transport used for serving requests. |
| // If specified ClientCertProvider is ignored. |
| BaseRoundTripper http.RoundTripper |
| // Endpoint overrides the default endpoint to be used for a service. |
| Endpoint string |
| // APIKey specifies an API key to be used as the basis for authentication. |
| // If set DetectOpts are ignored. |
| APIKey string |
| // Credentials used to add Authorization header to all requests. If set |
| // DetectOpts are ignored. |
| Credentials *auth.Credentials |
| // ClientCertProvider is a function that returns a TLS client certificate to |
| // be used when opening TLS connections. It follows the same semantics as |
| // crypto/tls.Config.GetClientCertificate. |
| ClientCertProvider ClientCertProvider |
| // DetectOpts configures settings for detect Application Default |
| // Credentials. |
| DetectOpts *detect.DetectOptions |
| // UniverseDomain is the default service domain for a given Cloud universe. |
| // The default value is "googleapis.com". This is the universe domain |
| // configured for the client, which will be compared to the universe domain |
| // that is separately configured for the credentials. |
| UniverseDomain string |
| |
| // InternalOptions are NOT meant to be set directly by consumers of this |
| // package, they should only be set by generated client code. |
| InternalOptions *InternalOptions |
| } |
| |
| func (o *Options) validate() error { |
| if o == nil { |
| return errors.New("httptransport: opts required to be non-nil") |
| } |
| if o.InternalOptions != nil && o.InternalOptions.SkipValidation { |
| return nil |
| } |
| hasCreds := o.APIKey != "" || |
| o.Credentials != nil || |
| (o.DetectOpts != nil && len(o.DetectOpts.CredentialsJSON) > 0) || |
| (o.DetectOpts != nil && o.DetectOpts.CredentialsFile != "") |
| if o.DisableAuthentication && hasCreds { |
| return errors.New("httptransport: DisableAuthentication is incompatible with options that set or detect credentials") |
| } |
| return nil |
| } |
| |
| // client returns the client a user set for the detect options or nil if one was |
| // not set. |
| func (o *Options) client() *http.Client { |
| if o.DetectOpts != nil && o.DetectOpts.Client != nil { |
| return o.DetectOpts.Client |
| } |
| return nil |
| } |
| |
| func (o *Options) resolveDetectOptions() *detect.DetectOptions { |
| io := o.InternalOptions |
| // soft-clone these so we are not updating a ref the user holds and may reuse |
| do := transport.CloneDetectOptions(o.DetectOpts) |
| |
| // If scoped JWTs are enabled user provided an aud, allow self-signed JWT. |
| if (io != nil && io.EnableJWTWithScope) || do.Audience != "" { |
| do.UseSelfSignedJWT = true |
| } |
| // Only default scopes if user did not also set an audience. |
| if len(do.Scopes) == 0 && do.Audience == "" && io != nil && len(io.DefaultScopes) > 0 { |
| do.Scopes = make([]string, len(io.DefaultScopes)) |
| copy(do.Scopes, io.DefaultScopes) |
| } |
| if len(do.Scopes) == 0 && do.Audience == "" && io != nil { |
| do.Audience = o.InternalOptions.DefaultAudience |
| } |
| if o.ClientCertProvider != nil { |
| tlsConfig := &tls.Config{ |
| GetClientCertificate: o.ClientCertProvider, |
| } |
| do.Client = transport.DefaultHTTPClientWithTLS(tlsConfig) |
| do.TokenURL = detect.GoogleMTLSTokenURL |
| } |
| return do |
| } |
| |
| // InternalOptions are only meant to be set by generated client code. These are |
| // not meant to be set directly by consumers of this package. Configuration in |
| // this type is considered EXPERIMENTAL and may be removed at any time in the |
| // future without warning. |
| type InternalOptions struct { |
| // EnableJWTWithScope specifies if scope can be used with self-signed JWT. |
| EnableJWTWithScope bool |
| // DefaultAudience specifies a default audience to be used as the audience |
| // field ("aud") for the JWT token authentication. |
| DefaultAudience string |
| // DefaultEndpointTemplate combined with UniverseDomain specifies the |
| // default endpoint. |
| DefaultEndpointTemplate string |
| // DefaultMTLSEndpoint specifies the default mTLS endpoint. |
| DefaultMTLSEndpoint string |
| // DefaultScopes specifies the default OAuth2 scopes to be used for a |
| // service. |
| DefaultScopes []string |
| // SkipValidation bypasses validation on Options. It should only be used |
| // internally for clients that needs more control over their transport. |
| SkipValidation bool |
| } |
| |
| // AddAuthorizationMiddleware adds a middleware to the provided client's |
| // transport that sets the Authorization header with the value produced by the |
| // provided [cloud.google.com/go/auth.Credentials]. An error is returned only |
| // if client or creds is nil. |
| func AddAuthorizationMiddleware(client *http.Client, creds *auth.Credentials) error { |
| if client == nil || creds == nil { |
| return fmt.Errorf("httptransport: client and tp must not be nil") |
| } |
| base := client.Transport |
| if base == nil { |
| if dt, ok := http.DefaultTransport.(*http.Transport); ok { |
| base = dt.Clone() |
| } else { |
| // Directly reuse the DefaultTransport if the application has |
| // replaced it with an implementation of RoundTripper other than |
| // http.Transport. |
| base = http.DefaultTransport |
| } |
| } |
| client.Transport = &authTransport{ |
| creds: creds, |
| base: base, |
| // TODO(quartzmo): Somehow set clientUniverseDomain from impersonate calls. |
| } |
| return nil |
| } |
| |
| // NewClient returns a [net/http.Client] that can be used to communicate with a |
| // Google cloud service, configured with the provided [Options]. It |
| // automatically appends Authorization headers to all outgoing requests. |
| func NewClient(opts *Options) (*http.Client, error) { |
| if err := opts.validate(); err != nil { |
| return nil, err |
| } |
| |
| tOpts := &transport.Options{ |
| Endpoint: opts.Endpoint, |
| ClientCertProvider: opts.ClientCertProvider, |
| Client: opts.client(), |
| UniverseDomain: opts.UniverseDomain, |
| } |
| if io := opts.InternalOptions; io != nil { |
| tOpts.DefaultEndpointTemplate = io.DefaultEndpointTemplate |
| tOpts.DefaultMTLSEndpoint = io.DefaultMTLSEndpoint |
| } |
| clientCertProvider, dialTLSContext, err := transport.GetHTTPTransportConfig(tOpts) |
| if err != nil { |
| return nil, err |
| } |
| baseRoundTripper := opts.BaseRoundTripper |
| if baseRoundTripper == nil { |
| baseRoundTripper = defaultBaseTransport(clientCertProvider, dialTLSContext) |
| } |
| // Ensure the token exchange transport uses the same ClientCertProvider as the API transport. |
| opts.ClientCertProvider = clientCertProvider |
| trans, err := newTransport(baseRoundTripper, opts) |
| if err != nil { |
| return nil, err |
| } |
| return &http.Client{ |
| Transport: trans, |
| }, nil |
| } |
| |
| // SetAuthHeader uses the provided token to set the Authorization header on a |
| // request. If the token.Type is empty, the type is assumed to be Bearer. |
| func SetAuthHeader(token *auth.Token, req *http.Request) { |
| typ := token.Type |
| if typ == "" { |
| typ = internal.TokenTypeBearer |
| } |
| req.Header.Set("Authorization", typ+" "+token.Value) |
| } |