// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package internal import ( "context" "crypto/rsa" "crypto/x509" "encoding/json" "encoding/pem" "errors" "fmt" "io" "net/http" "os" "sync" "time" "cloud.google.com/go/compute/metadata" ) const ( // TokenTypeBearer is the auth header prefix for bearer tokens. TokenTypeBearer = "Bearer" // QuotaProjectEnvVar is the environment variable for setting the quota // project. QuotaProjectEnvVar = "GOOGLE_CLOUD_QUOTA_PROJECT" projectEnvVar = "GOOGLE_CLOUD_PROJECT" maxBodySize = 1 << 20 // DefaultUniverseDomain is the default value for universe domain. // Universe domain is the default service domain for a given Cloud universe. DefaultUniverseDomain = "googleapis.com" ) type clonableTransport interface { Clone() *http.Transport } // DefaultClient returns an [http.Client] with some defaults set. If // the current [http.DefaultTransport] is a [clonableTransport], as // is the case for an [*http.Transport], the clone will be used. // Otherwise the [http.DefaultTransport] is used directly. func DefaultClient() *http.Client { if transport, ok := http.DefaultTransport.(clonableTransport); ok { return &http.Client{ Transport: transport.Clone(), Timeout: 30 * time.Second, } } return &http.Client{ Transport: http.DefaultTransport, Timeout: 30 * time.Second, } } // ParseKey converts the binary contents of a private key file // to an *rsa.PrivateKey. It detects whether the private key is in a // PEM container or not. If so, it extracts the the private key // from PEM container before conversion. It only supports PEM // containers with no passphrase. func ParseKey(key []byte) (*rsa.PrivateKey, error) { block, _ := pem.Decode(key) if block != nil { key = block.Bytes } parsedKey, err := x509.ParsePKCS8PrivateKey(key) if err != nil { parsedKey, err = x509.ParsePKCS1PrivateKey(key) if err != nil { return nil, fmt.Errorf("private key should be a PEM or plain PKCS1 or PKCS8: %w", err) } } parsed, ok := parsedKey.(*rsa.PrivateKey) if !ok { return nil, errors.New("private key is invalid") } return parsed, nil } // GetQuotaProject retrieves quota project with precedence being: override, // environment variable, creds json file. func GetQuotaProject(b []byte, override string) string { if override != "" { return override } if env := os.Getenv(QuotaProjectEnvVar); env != "" { return env } if b == nil { return "" } var v struct { QuotaProject string `json:"quota_project_id"` } if err := json.Unmarshal(b, &v); err != nil { return "" } return v.QuotaProject } // GetProjectID retrieves project with precedence being: override, // environment variable, creds json file. func GetProjectID(b []byte, override string) string { if override != "" { return override } if env := os.Getenv(projectEnvVar); env != "" { return env } if b == nil { return "" } var v struct { ProjectID string `json:"project_id"` // standard service account key Project string `json:"project"` // gdch key } if err := json.Unmarshal(b, &v); err != nil { return "" } if v.ProjectID != "" { return v.ProjectID } return v.Project } // DoRequest executes the provided req with the client. It reads the response // body, closes it, and returns it. func DoRequest(client *http.Client, req *http.Request) (*http.Response, []byte, error) { resp, err := client.Do(req) if err != nil { return nil, nil, err } defer resp.Body.Close() body, err := ReadAll(io.LimitReader(resp.Body, maxBodySize)) if err != nil { return nil, nil, err } return resp, body, nil } // ReadAll consumes the whole reader and safely reads the content of its body // with some overflow protection. func ReadAll(r io.Reader) ([]byte, error) { return io.ReadAll(io.LimitReader(r, maxBodySize)) } // StaticCredentialsProperty is a helper for creating static credentials // properties. func StaticCredentialsProperty(s string) StaticProperty { return StaticProperty(s) } // StaticProperty always returns that value of the underlying string. type StaticProperty string // GetProperty loads the properly value provided the given context. func (p StaticProperty) GetProperty(context.Context) (string, error) { return string(p), nil } // ComputeUniverseDomainProvider fetches the credentials universe domain from // the google cloud metadata service. type ComputeUniverseDomainProvider struct { universeDomainOnce sync.Once universeDomain string universeDomainErr error } // GetProperty fetches the credentials universe domain from the google cloud // metadata service. func (c *ComputeUniverseDomainProvider) GetProperty(ctx context.Context) (string, error) { c.universeDomainOnce.Do(func() { c.universeDomain, c.universeDomainErr = getMetadataUniverseDomain(ctx) }) if c.universeDomainErr != nil { return "", c.universeDomainErr } return c.universeDomain, nil } // httpGetMetadataUniverseDomain is a package var for unit test substitution. var httpGetMetadataUniverseDomain = func(ctx context.Context) (string, error) { ctx, cancel := context.WithTimeout(ctx, 1*time.Second) defer cancel() return metadata.GetWithContext(ctx, "universe/universe_domain") } func getMetadataUniverseDomain(ctx context.Context) (string, error) { universeDomain, err := httpGetMetadataUniverseDomain(ctx) if err == nil { return universeDomain, nil } if _, ok := err.(metadata.NotDefinedError); ok { // http.StatusNotFound (404) return DefaultUniverseDomain, nil } return "", err }