httptransport.go

  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.
 17package httptransport
 18
 19import (
 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"
 29	"cloud.google.com/go/auth/internal/transport"
 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].
 36type ClientCertProvider = func(*tls.CertificateRequestInfo) (*tls.Certificate, error)
 37
 38// Options used to configure a [net/http.Client] from [NewClient].
 39type 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
 85func (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.
104func (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
111func (o *Options) logger() *slog.Logger {
112	return internallog.New(o.Logger)
113}
114
115func (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.
149type 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.
179func 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.
204func 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 header on a
240// request. If the token.Type is empty, the type is assumed to be Bearer.
241func SetAuthHeader(token *auth.Token, req *http.Request) {
242	typ := token.Type
243	if typ == "" {
244		typ = internal.TokenTypeBearer
245	}
246	req.Header.Set("Authorization", typ+" "+token.Value)
247}