dial.go raw

   1  // Copyright 2015 Google LLC.
   2  // Use of this source code is governed by a BSD-style
   3  // license that can be found in the LICENSE file.
   4  
   5  // Package http supports network connections to HTTP servers.
   6  // This package is not intended for use by end developers. Use the
   7  // google.golang.org/api/option package to configure API clients.
   8  package http
   9  
  10  import (
  11  	"context"
  12  	"crypto/tls"
  13  	"errors"
  14  	"net"
  15  	"net/http"
  16  	"time"
  17  
  18  	"cloud.google.com/go/auth"
  19  	"cloud.google.com/go/auth/credentials"
  20  	"cloud.google.com/go/auth/httptransport"
  21  	"cloud.google.com/go/auth/oauth2adapt"
  22  	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
  23  	"golang.org/x/net/http2"
  24  	"golang.org/x/oauth2"
  25  	"google.golang.org/api/googleapi/transport"
  26  	"google.golang.org/api/internal"
  27  	"google.golang.org/api/internal/cert"
  28  	"google.golang.org/api/option"
  29  )
  30  
  31  // NewClient returns an HTTP client for use communicating with a Google cloud
  32  // service, configured with the given ClientOptions. It also returns the endpoint
  33  // for the service as specified in the options.
  34  func NewClient(ctx context.Context, opts ...option.ClientOption) (*http.Client, string, error) {
  35  	settings, err := newSettings(opts)
  36  	if err != nil {
  37  		return nil, "", err
  38  	}
  39  	clientCertSource, dialTLSContext, endpoint, err := internal.GetHTTPTransportConfigAndEndpoint(settings)
  40  	if err != nil {
  41  		return nil, "", err
  42  	}
  43  	// TODO(cbro): consider injecting the User-Agent even if an explicit HTTP client is provided?
  44  	if settings.HTTPClient != nil {
  45  		return settings.HTTPClient, endpoint, nil
  46  	}
  47  
  48  	if settings.IsNewAuthLibraryEnabled() {
  49  		client, err := newClientNewAuth(ctx, nil, settings)
  50  		if err != nil {
  51  			return nil, "", err
  52  		}
  53  		return client, endpoint, nil
  54  	}
  55  	trans, err := newTransport(ctx, defaultBaseTransport(ctx, clientCertSource, dialTLSContext), settings)
  56  	if err != nil {
  57  		return nil, "", err
  58  	}
  59  	return &http.Client{Transport: trans}, endpoint, nil
  60  }
  61  
  62  // newClientNewAuth is an adapter to call new auth library.
  63  func newClientNewAuth(ctx context.Context, base http.RoundTripper, ds *internal.DialSettings) (*http.Client, error) {
  64  	// honor options if set
  65  	var creds *auth.Credentials
  66  	if ds.InternalCredentials != nil {
  67  		creds = oauth2adapt.AuthCredentialsFromOauth2Credentials(ds.InternalCredentials)
  68  	} else if ds.Credentials != nil {
  69  		creds = oauth2adapt.AuthCredentialsFromOauth2Credentials(ds.Credentials)
  70  	} else if ds.AuthCredentials != nil {
  71  		creds = ds.AuthCredentials
  72  	} else if ds.TokenSource != nil {
  73  		credOpts := &auth.CredentialsOptions{
  74  			TokenProvider: oauth2adapt.TokenProviderFromTokenSource(ds.TokenSource),
  75  		}
  76  		if ds.QuotaProject != "" {
  77  			credOpts.QuotaProjectIDProvider = auth.CredentialsPropertyFunc(func(ctx context.Context) (string, error) {
  78  				return ds.QuotaProject, nil
  79  			})
  80  		}
  81  		creds = auth.NewCredentials(credOpts)
  82  	}
  83  
  84  	var skipValidation bool
  85  	// If our clients explicitly setup the credential skip validation as it is
  86  	// assumed correct
  87  	if ds.SkipValidation || ds.InternalCredentials != nil {
  88  		skipValidation = true
  89  	}
  90  
  91  	// Defaults for older clients that don't set this value yet
  92  	defaultEndpointTemplate := ds.DefaultEndpointTemplate
  93  	if defaultEndpointTemplate == "" {
  94  		defaultEndpointTemplate = ds.DefaultEndpoint
  95  	}
  96  
  97  	var aud string
  98  	if len(ds.Audiences) > 0 {
  99  		aud = ds.Audiences[0]
 100  	}
 101  	headers := http.Header{}
 102  	if ds.QuotaProject != "" {
 103  		headers.Set("X-goog-user-project", ds.QuotaProject)
 104  	}
 105  	if ds.RequestReason != "" {
 106  		headers.Set("X-goog-request-reason", ds.RequestReason)
 107  	}
 108  	if ds.UserAgent != "" {
 109  		headers.Set("User-Agent", ds.UserAgent)
 110  	}
 111  	credsJSON, _ := ds.GetAuthCredentialsJSON()
 112  	credsFile, _ := ds.GetAuthCredentialsFile()
 113  	client, err := httptransport.NewClient(&httptransport.Options{
 114  		DisableTelemetry:      ds.TelemetryDisabled,
 115  		DisableAuthentication: ds.NoAuth,
 116  		Headers:               headers,
 117  		Endpoint:              ds.Endpoint,
 118  		APIKey:                ds.APIKey,
 119  		Credentials:           creds,
 120  		ClientCertProvider:    ds.ClientCertSource,
 121  		BaseRoundTripper:      base,
 122  		DetectOpts: &credentials.DetectOptions{
 123  			Scopes:          ds.Scopes,
 124  			Audience:        aud,
 125  			CredentialsFile: credsFile,
 126  			CredentialsJSON: credsJSON,
 127  			Logger:          ds.Logger,
 128  		},
 129  		InternalOptions: &httptransport.InternalOptions{
 130  			EnableJWTWithScope:      ds.EnableJwtWithScope,
 131  			DefaultAudience:         ds.DefaultAudience,
 132  			DefaultEndpointTemplate: defaultEndpointTemplate,
 133  			DefaultMTLSEndpoint:     ds.DefaultMTLSEndpoint,
 134  			DefaultScopes:           ds.DefaultScopes,
 135  			SkipValidation:          skipValidation,
 136  		},
 137  		UniverseDomain: ds.UniverseDomain,
 138  		Logger:         ds.Logger,
 139  	})
 140  	if err != nil {
 141  		return nil, err
 142  	}
 143  	return client, nil
 144  }
 145  
 146  // NewTransport creates an http.RoundTripper for use communicating with a Google
 147  // cloud service, configured with the given ClientOptions. Its RoundTrip method delegates to base.
 148  func NewTransport(ctx context.Context, base http.RoundTripper, opts ...option.ClientOption) (http.RoundTripper, error) {
 149  	settings, err := newSettings(opts)
 150  	if err != nil {
 151  		return nil, err
 152  	}
 153  	if settings.HTTPClient != nil {
 154  		return nil, errors.New("transport/http: WithHTTPClient passed to NewTransport")
 155  	}
 156  	if settings.IsNewAuthLibraryEnabled() {
 157  		client, err := newClientNewAuth(ctx, base, settings)
 158  		if err != nil {
 159  			return nil, err
 160  		}
 161  		return client.Transport, nil
 162  	}
 163  	return newTransport(ctx, base, settings)
 164  }
 165  
 166  func newTransport(ctx context.Context, base http.RoundTripper, settings *internal.DialSettings) (http.RoundTripper, error) {
 167  	paramTransport := &parameterTransport{
 168  		base:          base,
 169  		userAgent:     settings.UserAgent,
 170  		requestReason: settings.RequestReason,
 171  	}
 172  	var trans http.RoundTripper = paramTransport
 173  	trans = addOpenTelemetryTransport(trans, settings)
 174  	switch {
 175  	case settings.NoAuth:
 176  		// Do nothing.
 177  	case settings.APIKey != "":
 178  		paramTransport.quotaProject = internal.GetQuotaProject(nil, settings.QuotaProject)
 179  		trans = &transport.APIKey{
 180  			Transport: trans,
 181  			Key:       settings.APIKey,
 182  		}
 183  	default:
 184  		creds, err := internal.Creds(ctx, settings)
 185  		if err != nil {
 186  			return nil, err
 187  		}
 188  		paramTransport.quotaProject = internal.GetQuotaProject(creds, settings.QuotaProject)
 189  		ts := creds.TokenSource
 190  		if settings.ImpersonationConfig == nil && settings.TokenSource != nil {
 191  			ts = settings.TokenSource
 192  		}
 193  		trans = &oauth2.Transport{
 194  			Base:   trans,
 195  			Source: ts,
 196  		}
 197  	}
 198  	return trans, nil
 199  }
 200  
 201  func newSettings(opts []option.ClientOption) (*internal.DialSettings, error) {
 202  	var o internal.DialSettings
 203  	for _, opt := range opts {
 204  		opt.Apply(&o)
 205  	}
 206  	if err := o.Validate(); err != nil {
 207  		return nil, err
 208  	}
 209  	if o.GRPCConn != nil {
 210  		return nil, errors.New("unsupported gRPC connection specified")
 211  	}
 212  	return &o, nil
 213  }
 214  
 215  type parameterTransport struct {
 216  	userAgent     string
 217  	quotaProject  string
 218  	requestReason string
 219  
 220  	base http.RoundTripper
 221  }
 222  
 223  func (t *parameterTransport) RoundTrip(req *http.Request) (*http.Response, error) {
 224  	rt := t.base
 225  	if rt == nil {
 226  		return nil, errors.New("transport: no Transport specified")
 227  	}
 228  	newReq := *req
 229  	newReq.Header = make(http.Header)
 230  	for k, vv := range req.Header {
 231  		newReq.Header[k] = vv
 232  	}
 233  	if t.userAgent != "" {
 234  		// TODO(cbro): append to existing User-Agent header?
 235  		newReq.Header.Set("User-Agent", t.userAgent)
 236  	}
 237  
 238  	// Attach system parameters into the header
 239  	if t.quotaProject != "" {
 240  		newReq.Header.Set("X-Goog-User-Project", t.quotaProject)
 241  	}
 242  	if t.requestReason != "" {
 243  		newReq.Header.Set("X-Goog-Request-Reason", t.requestReason)
 244  	}
 245  
 246  	return rt.RoundTrip(&newReq)
 247  }
 248  
 249  // defaultBaseTransport returns the base HTTP transport. It uses a default
 250  // transport, taking most defaults from http.DefaultTransport.
 251  // If TLSCertificate is available, set TLSClientConfig as well.
 252  func defaultBaseTransport(ctx context.Context, clientCertSource cert.Source, dialTLSContext func(context.Context, string, string) (net.Conn, error)) http.RoundTripper {
 253  	// Copy http.DefaultTransport except for MaxIdleConnsPerHost setting,
 254  	// which is increased due to reported performance issues under load in the
 255  	// GCS client. Transport.Clone is only available in Go 1.13 and up.
 256  	trans := clonedTransport(http.DefaultTransport)
 257  	if trans == nil {
 258  		trans = fallbackBaseTransport()
 259  	}
 260  	trans.MaxIdleConnsPerHost = 100
 261  
 262  	if clientCertSource != nil {
 263  		trans.TLSClientConfig = &tls.Config{
 264  			GetClientCertificate: clientCertSource,
 265  		}
 266  	}
 267  	if dialTLSContext != nil {
 268  		// If DialTLSContext is set, TLSClientConfig wil be ignored
 269  		trans.DialTLSContext = dialTLSContext
 270  	}
 271  
 272  	configureHTTP2(trans)
 273  
 274  	return trans
 275  }
 276  
 277  // configureHTTP2 configures the ReadIdleTimeout HTTP/2 option for the
 278  // transport. This allows broken idle connections to be pruned more quickly,
 279  // preventing the client from attempting to re-use connections that will no
 280  // longer work.
 281  func configureHTTP2(trans *http.Transport) {
 282  	http2Trans, err := http2.ConfigureTransports(trans)
 283  	if err == nil {
 284  		http2Trans.ReadIdleTimeout = time.Second * 31
 285  	}
 286  }
 287  
 288  // fallbackBaseTransport is used in <go1.13 as well as in the rare case if
 289  // http.DefaultTransport has been reassigned something that's not a
 290  // *http.Transport.
 291  func fallbackBaseTransport() *http.Transport {
 292  	return &http.Transport{
 293  		Proxy: http.ProxyFromEnvironment,
 294  		DialContext: (&net.Dialer{
 295  			Timeout:   30 * time.Second,
 296  			KeepAlive: 30 * time.Second,
 297  			DualStack: true,
 298  		}).DialContext,
 299  		MaxIdleConns:          100,
 300  		MaxIdleConnsPerHost:   100,
 301  		IdleConnTimeout:       90 * time.Second,
 302  		TLSHandshakeTimeout:   10 * time.Second,
 303  		ExpectContinueTimeout: 1 * time.Second,
 304  	}
 305  }
 306  
 307  func addOpenTelemetryTransport(trans http.RoundTripper, settings *internal.DialSettings) http.RoundTripper {
 308  	if settings.TelemetryDisabled {
 309  		return trans
 310  	}
 311  	return otelhttp.NewTransport(trans)
 312  }
 313  
 314  // clonedTransport returns the given RoundTripper as a cloned *http.Transport.
 315  // It returns nil if the RoundTripper can't be cloned or coerced to
 316  // *http.Transport.
 317  func clonedTransport(rt http.RoundTripper) *http.Transport {
 318  	t, ok := rt.(*http.Transport)
 319  	if !ok {
 320  		return nil
 321  	}
 322  	return t.Clone()
 323  }
 324