httptransport.go raw

   1  // Copyright 2023 Google LLC
   2  //
   3  // Licensed under the Apache License, Version 2.0 (the "License");
   4  // you may not use this file except in compliance with the License.
   5  // You may obtain a copy of the License at
   6  //
   7  //      http://www.apache.org/licenses/LICENSE-2.0
   8  //
   9  // Unless required by applicable law or agreed to in writing, software
  10  // distributed under the License is distributed on an "AS IS" BASIS,
  11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12  // See the License for the specific language governing permissions and
  13  // limitations under the License.
  14  
  15  // Package httptransport provides functionality for managing HTTP client
  16  // connections to Google Cloud services.
  17  package httptransport
  18  
  19  import (
  20  	"crypto/tls"
  21  	"errors"
  22  	"fmt"
  23  	"log/slog"
  24  	"net/http"
  25  
  26  	"cloud.google.com/go/auth"
  27  	detect "cloud.google.com/go/auth/credentials"
  28  	"cloud.google.com/go/auth/internal/transport"
  29  	"cloud.google.com/go/auth/internal/transport/headers"
  30  	"github.com/googleapis/gax-go/v2/internallog"
  31  )
  32  
  33  // ClientCertProvider is a function that returns a TLS client certificate to be
  34  // used when opening TLS connections. It follows the same semantics as
  35  // [crypto/tls.Config.GetClientCertificate].
  36  type ClientCertProvider = func(*tls.CertificateRequestInfo) (*tls.Certificate, error)
  37  
  38  // Options used to configure a [net/http.Client] from [NewClient].
  39  type Options struct {
  40  	// DisableTelemetry disables default telemetry (OpenTelemetry). An example
  41  	// reason to do so would be to bind custom telemetry that overrides the
  42  	// defaults.
  43  	DisableTelemetry bool
  44  	// DisableAuthentication specifies that no authentication should be used. It
  45  	// is suitable only for testing and for accessing public resources, like
  46  	// public Google Cloud Storage buckets.
  47  	DisableAuthentication bool
  48  	// Headers are extra HTTP headers that will be appended to every outgoing
  49  	// request.
  50  	Headers http.Header
  51  	// BaseRoundTripper overrides the base transport used for serving requests.
  52  	// If specified ClientCertProvider is ignored.
  53  	BaseRoundTripper http.RoundTripper
  54  	// Endpoint overrides the default endpoint to be used for a service.
  55  	Endpoint string
  56  	// APIKey specifies an API key to be used as the basis for authentication.
  57  	// If set DetectOpts are ignored.
  58  	APIKey string
  59  	// Credentials used to add Authorization header to all requests. If set
  60  	// DetectOpts are ignored.
  61  	Credentials *auth.Credentials
  62  	// ClientCertProvider is a function that returns a TLS client certificate to
  63  	// be used when opening TLS connections. It follows the same semantics as
  64  	// crypto/tls.Config.GetClientCertificate.
  65  	ClientCertProvider ClientCertProvider
  66  	// DetectOpts configures settings for detect Application Default
  67  	// Credentials.
  68  	DetectOpts *detect.DetectOptions
  69  	// UniverseDomain is the default service domain for a given Cloud universe.
  70  	// The default value is "googleapis.com". This is the universe domain
  71  	// configured for the client, which will be compared to the universe domain
  72  	// that is separately configured for the credentials.
  73  	UniverseDomain string
  74  	// Logger is used for debug logging. If provided, logging will be enabled
  75  	// at the loggers configured level. By default logging is disabled unless
  76  	// enabled by setting GOOGLE_SDK_GO_LOGGING_LEVEL in which case a default
  77  	// logger will be used. Optional.
  78  	Logger *slog.Logger
  79  
  80  	// InternalOptions are NOT meant to be set directly by consumers of this
  81  	// package, they should only be set by generated client code.
  82  	InternalOptions *InternalOptions
  83  }
  84  
  85  func (o *Options) validate() error {
  86  	if o == nil {
  87  		return errors.New("httptransport: opts required to be non-nil")
  88  	}
  89  	if o.InternalOptions != nil && o.InternalOptions.SkipValidation {
  90  		return nil
  91  	}
  92  	hasCreds := o.APIKey != "" ||
  93  		o.Credentials != nil ||
  94  		(o.DetectOpts != nil && len(o.DetectOpts.CredentialsJSON) > 0) ||
  95  		(o.DetectOpts != nil && o.DetectOpts.CredentialsFile != "")
  96  	if o.DisableAuthentication && hasCreds {
  97  		return errors.New("httptransport: DisableAuthentication is incompatible with options that set or detect credentials")
  98  	}
  99  	return nil
 100  }
 101  
 102  // client returns the client a user set for the detect options or nil if one was
 103  // not set.
 104  func (o *Options) client() *http.Client {
 105  	if o.DetectOpts != nil && o.DetectOpts.Client != nil {
 106  		return o.DetectOpts.Client
 107  	}
 108  	return nil
 109  }
 110  
 111  func (o *Options) logger() *slog.Logger {
 112  	return internallog.New(o.Logger)
 113  }
 114  
 115  func (o *Options) resolveDetectOptions() *detect.DetectOptions {
 116  	io := o.InternalOptions
 117  	// soft-clone these so we are not updating a ref the user holds and may reuse
 118  	do := transport.CloneDetectOptions(o.DetectOpts)
 119  
 120  	// If scoped JWTs are enabled user provided an aud, allow self-signed JWT.
 121  	if (io != nil && io.EnableJWTWithScope) || do.Audience != "" {
 122  		do.UseSelfSignedJWT = true
 123  	}
 124  	// Only default scopes if user did not also set an audience.
 125  	if len(do.Scopes) == 0 && do.Audience == "" && io != nil && len(io.DefaultScopes) > 0 {
 126  		do.Scopes = make([]string, len(io.DefaultScopes))
 127  		copy(do.Scopes, io.DefaultScopes)
 128  	}
 129  	if len(do.Scopes) == 0 && do.Audience == "" && io != nil {
 130  		do.Audience = o.InternalOptions.DefaultAudience
 131  	}
 132  	if o.ClientCertProvider != nil {
 133  		tlsConfig := &tls.Config{
 134  			GetClientCertificate: o.ClientCertProvider,
 135  		}
 136  		do.Client = transport.DefaultHTTPClientWithTLS(tlsConfig)
 137  		do.TokenURL = detect.GoogleMTLSTokenURL
 138  	}
 139  	if do.Logger == nil {
 140  		do.Logger = o.logger()
 141  	}
 142  	return do
 143  }
 144  
 145  // InternalOptions are only meant to be set by generated client code. These are
 146  // not meant to be set directly by consumers of this package. Configuration in
 147  // this type is considered EXPERIMENTAL and may be removed at any time in the
 148  // future without warning.
 149  type InternalOptions struct {
 150  	// EnableJWTWithScope specifies if scope can be used with self-signed JWT.
 151  	EnableJWTWithScope bool
 152  	// DefaultAudience specifies a default audience to be used as the audience
 153  	// field ("aud") for the JWT token authentication.
 154  	DefaultAudience string
 155  	// DefaultEndpointTemplate combined with UniverseDomain specifies the
 156  	// default endpoint.
 157  	DefaultEndpointTemplate string
 158  	// DefaultMTLSEndpoint specifies the default mTLS endpoint.
 159  	DefaultMTLSEndpoint string
 160  	// DefaultScopes specifies the default OAuth2 scopes to be used for a
 161  	// service.
 162  	DefaultScopes []string
 163  	// SkipValidation bypasses validation on Options. It should only be used
 164  	// internally for clients that need more control over their transport.
 165  	SkipValidation bool
 166  	// SkipUniverseDomainValidation skips the verification that the universe
 167  	// domain configured for the client matches the universe domain configured
 168  	// for the credentials. It should only be used internally for clients that
 169  	// need more control over their transport. The default is false.
 170  	SkipUniverseDomainValidation bool
 171  }
 172  
 173  // AddAuthorizationMiddleware adds a middleware to the provided client's
 174  // transport that sets the Authorization header with the value produced by the
 175  // provided [cloud.google.com/go/auth.Credentials]. An error is returned only
 176  // if client or creds is nil.
 177  //
 178  // This function does not support setting a universe domain value on the client.
 179  func AddAuthorizationMiddleware(client *http.Client, creds *auth.Credentials) error {
 180  	if client == nil || creds == nil {
 181  		return fmt.Errorf("httptransport: client and tp must not be nil")
 182  	}
 183  	base := client.Transport
 184  	if base == nil {
 185  		if dt, ok := http.DefaultTransport.(*http.Transport); ok {
 186  			base = dt.Clone()
 187  		} else {
 188  			// Directly reuse the DefaultTransport if the application has
 189  			// replaced it with an implementation of RoundTripper other than
 190  			// http.Transport.
 191  			base = http.DefaultTransport
 192  		}
 193  	}
 194  	client.Transport = &authTransport{
 195  		creds: creds,
 196  		base:  base,
 197  	}
 198  	return nil
 199  }
 200  
 201  // NewClient returns a [net/http.Client] that can be used to communicate with a
 202  // Google cloud service, configured with the provided [Options]. It
 203  // automatically appends Authorization headers to all outgoing requests.
 204  func NewClient(opts *Options) (*http.Client, error) {
 205  	if err := opts.validate(); err != nil {
 206  		return nil, err
 207  	}
 208  
 209  	tOpts := &transport.Options{
 210  		Endpoint:           opts.Endpoint,
 211  		ClientCertProvider: opts.ClientCertProvider,
 212  		Client:             opts.client(),
 213  		UniverseDomain:     opts.UniverseDomain,
 214  		Logger:             opts.logger(),
 215  	}
 216  	if io := opts.InternalOptions; io != nil {
 217  		tOpts.DefaultEndpointTemplate = io.DefaultEndpointTemplate
 218  		tOpts.DefaultMTLSEndpoint = io.DefaultMTLSEndpoint
 219  	}
 220  	clientCertProvider, dialTLSContext, err := transport.GetHTTPTransportConfig(tOpts)
 221  	if err != nil {
 222  		return nil, err
 223  	}
 224  	baseRoundTripper := opts.BaseRoundTripper
 225  	if baseRoundTripper == nil {
 226  		baseRoundTripper = defaultBaseTransport(clientCertProvider, dialTLSContext)
 227  	}
 228  	// Ensure the token exchange transport uses the same ClientCertProvider as the API transport.
 229  	opts.ClientCertProvider = clientCertProvider
 230  	trans, err := newTransport(baseRoundTripper, opts)
 231  	if err != nil {
 232  		return nil, err
 233  	}
 234  	return &http.Client{
 235  		Transport: trans,
 236  	}, nil
 237  }
 238  
 239  // SetAuthHeader uses the provided token to set the Authorization and trust
 240  // boundary headers on an http.Request. If the token.Type is empty, the type is
 241  // assumed to be Bearer. This is the recommended way to set authorization
 242  // headers on a custom http.Request.
 243  func SetAuthHeader(token *auth.Token, req *http.Request) {
 244  	headers.SetAuthHeader(token, req)
 245  }
 246