internal.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
 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}