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
15package internal
16
17import (
18 "context"
19 "crypto"
20 "crypto/x509"
21 "encoding/json"
22 "encoding/pem"
23 "errors"
24 "fmt"
25 "io"
26 "net/http"
27 "os"
28 "sync"
29 "time"
30
31 "cloud.google.com/go/compute/metadata"
32)
33
34const (
35 // TokenTypeBearer is the auth header prefix for bearer tokens.
36 TokenTypeBearer = "Bearer"
37
38 // QuotaProjectEnvVar is the environment variable for setting the quota
39 // project.
40 QuotaProjectEnvVar = "GOOGLE_CLOUD_QUOTA_PROJECT"
41 // UniverseDomainEnvVar is the environment variable for setting the default
42 // service domain for a given Cloud universe.
43 UniverseDomainEnvVar = "GOOGLE_CLOUD_UNIVERSE_DOMAIN"
44 projectEnvVar = "GOOGLE_CLOUD_PROJECT"
45 maxBodySize = 1 << 20
46
47 // DefaultUniverseDomain is the default value for universe domain.
48 // Universe domain is the default service domain for a given Cloud universe.
49 DefaultUniverseDomain = "googleapis.com"
50)
51
52type clonableTransport interface {
53 Clone() *http.Transport
54}
55
56// DefaultClient returns an [http.Client] with some defaults set. If
57// the current [http.DefaultTransport] is a [clonableTransport], as
58// is the case for an [*http.Transport], the clone will be used.
59// Otherwise the [http.DefaultTransport] is used directly.
60func DefaultClient() *http.Client {
61 if transport, ok := http.DefaultTransport.(clonableTransport); ok {
62 return &http.Client{
63 Transport: transport.Clone(),
64 Timeout: 30 * time.Second,
65 }
66 }
67
68 return &http.Client{
69 Transport: http.DefaultTransport,
70 Timeout: 30 * time.Second,
71 }
72}
73
74// ParseKey converts the binary contents of a private key file
75// to an crypto.Signer. It detects whether the private key is in a
76// PEM container or not. If so, it extracts the the private key
77// from PEM container before conversion. It only supports PEM
78// containers with no passphrase.
79func ParseKey(key []byte) (crypto.Signer, error) {
80 block, _ := pem.Decode(key)
81 if block != nil {
82 key = block.Bytes
83 }
84 var parsedKey crypto.PrivateKey
85 var err error
86 parsedKey, err = x509.ParsePKCS8PrivateKey(key)
87 if err != nil {
88 parsedKey, err = x509.ParsePKCS1PrivateKey(key)
89 if err != nil {
90 return nil, fmt.Errorf("private key should be a PEM or plain PKCS1 or PKCS8: %w", err)
91 }
92 }
93 parsed, ok := parsedKey.(crypto.Signer)
94 if !ok {
95 return nil, errors.New("private key is not a signer")
96 }
97 return parsed, nil
98}
99
100// GetQuotaProject retrieves quota project with precedence being: override,
101// environment variable, creds json file.
102func GetQuotaProject(b []byte, override string) string {
103 if override != "" {
104 return override
105 }
106 if env := os.Getenv(QuotaProjectEnvVar); env != "" {
107 return env
108 }
109 if b == nil {
110 return ""
111 }
112 var v struct {
113 QuotaProject string `json:"quota_project_id"`
114 }
115 if err := json.Unmarshal(b, &v); err != nil {
116 return ""
117 }
118 return v.QuotaProject
119}
120
121// GetProjectID retrieves project with precedence being: override,
122// environment variable, creds json file.
123func GetProjectID(b []byte, override string) string {
124 if override != "" {
125 return override
126 }
127 if env := os.Getenv(projectEnvVar); env != "" {
128 return env
129 }
130 if b == nil {
131 return ""
132 }
133 var v struct {
134 ProjectID string `json:"project_id"` // standard service account key
135 Project string `json:"project"` // gdch key
136 }
137 if err := json.Unmarshal(b, &v); err != nil {
138 return ""
139 }
140 if v.ProjectID != "" {
141 return v.ProjectID
142 }
143 return v.Project
144}
145
146// DoRequest executes the provided req with the client. It reads the response
147// body, closes it, and returns it.
148func DoRequest(client *http.Client, req *http.Request) (*http.Response, []byte, error) {
149 resp, err := client.Do(req)
150 if err != nil {
151 return nil, nil, err
152 }
153 defer resp.Body.Close()
154 body, err := ReadAll(io.LimitReader(resp.Body, maxBodySize))
155 if err != nil {
156 return nil, nil, err
157 }
158 return resp, body, nil
159}
160
161// ReadAll consumes the whole reader and safely reads the content of its body
162// with some overflow protection.
163func ReadAll(r io.Reader) ([]byte, error) {
164 return io.ReadAll(io.LimitReader(r, maxBodySize))
165}
166
167// StaticCredentialsProperty is a helper for creating static credentials
168// properties.
169func StaticCredentialsProperty(s string) StaticProperty {
170 return StaticProperty(s)
171}
172
173// StaticProperty always returns that value of the underlying string.
174type StaticProperty string
175
176// GetProperty loads the properly value provided the given context.
177func (p StaticProperty) GetProperty(context.Context) (string, error) {
178 return string(p), nil
179}
180
181// ComputeUniverseDomainProvider fetches the credentials universe domain from
182// the google cloud metadata service.
183type ComputeUniverseDomainProvider struct {
184 MetadataClient *metadata.Client
185 universeDomainOnce sync.Once
186 universeDomain string
187 universeDomainErr error
188}
189
190// GetProperty fetches the credentials universe domain from the google cloud
191// metadata service.
192func (c *ComputeUniverseDomainProvider) GetProperty(ctx context.Context) (string, error) {
193 c.universeDomainOnce.Do(func() {
194 c.universeDomain, c.universeDomainErr = getMetadataUniverseDomain(ctx, c.MetadataClient)
195 })
196 if c.universeDomainErr != nil {
197 return "", c.universeDomainErr
198 }
199 return c.universeDomain, nil
200}
201
202// httpGetMetadataUniverseDomain is a package var for unit test substitution.
203var httpGetMetadataUniverseDomain = func(ctx context.Context, client *metadata.Client) (string, error) {
204 ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
205 defer cancel()
206 return client.GetWithContext(ctx, "universe/universe-domain")
207}
208
209func getMetadataUniverseDomain(ctx context.Context, client *metadata.Client) (string, error) {
210 universeDomain, err := httpGetMetadataUniverseDomain(ctx, client)
211 if err == nil {
212 return universeDomain, nil
213 }
214 if _, ok := err.(metadata.NotDefinedError); ok {
215 // http.StatusNotFound (404)
216 return DefaultUniverseDomain, nil
217 }
218 return "", err
219}