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 := ¶meterTransport{
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