| // Copyright 2015 Google Inc. All Rights Reserved. | |
| // | |
| // 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 certs implements a CertSource which speaks to the public Cloud SQL API endpoint. | |
| package certs | |
| import ( | |
| "crypto/rand" | |
| "crypto/rsa" | |
| "crypto/tls" | |
| "crypto/x509" | |
| "encoding/pem" | |
| "errors" | |
| "fmt" | |
| "math" | |
| mrand "math/rand" | |
| "net/http" | |
| "strings" | |
| "sync" | |
| "time" | |
| "github.com/GoogleCloudPlatform/cloudsql-proxy/logging" | |
| "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/util" | |
| "golang.org/x/oauth2" | |
| "google.golang.org/api/googleapi" | |
| sqladmin "google.golang.org/api/sqladmin/v1beta4" | |
| ) | |
| const defaultUserAgent = "custom cloud_sql_proxy version >= 1.10" | |
| // NewCertSource returns a CertSource which can be used to authenticate using | |
| // the provided client, which must not be nil. | |
| // | |
| // This function is deprecated; use NewCertSourceOpts instead. | |
| func NewCertSource(host string, c *http.Client, checkRegion bool) *RemoteCertSource { | |
| return NewCertSourceOpts(c, RemoteOpts{ | |
| APIBasePath: host, | |
| IgnoreRegion: !checkRegion, | |
| UserAgent: defaultUserAgent, | |
| }) | |
| } | |
| // RemoteOpts are a collection of options for NewCertSourceOpts. All fields are | |
| // optional. | |
| type RemoteOpts struct { | |
| // APIBasePath specifies the base path for the sqladmin API. If left blank, | |
| // the default from the autogenerated sqladmin library is used (which is | |
| // sufficient for nearly all users) | |
| APIBasePath string | |
| // IgnoreRegion specifies whether a missing or mismatched region in the | |
| // instance name should be ignored. In a future version this value will be | |
| // forced to 'false' by the RemoteCertSource. | |
| IgnoreRegion bool | |
| // A string for the RemoteCertSource to identify itself when contacting the | |
| // sqladmin API. | |
| UserAgent string | |
| // IP address type options | |
| IPAddrTypeOpts []string | |
| // Enable IAM proxy db authentication | |
| EnableIAMLogin bool | |
| // Token source for token information used in cert creation | |
| TokenSource oauth2.TokenSource | |
| // DelayKeyGenerate, if true, causes the RSA key to be generated lazily | |
| // on the first connection to a database. The default behavior is to generate | |
| // the key when the CertSource is created. | |
| DelayKeyGenerate bool | |
| } | |
| // NewCertSourceOpts returns a CertSource configured with the provided Opts. | |
| // The provided http.Client must not be nil. | |
| // | |
| // Use this function instead of NewCertSource; it has a more forward-compatible | |
| // signature. | |
| func NewCertSourceOpts(c *http.Client, opts RemoteOpts) *RemoteCertSource { | |
| serv, err := sqladmin.New(c) | |
| if err != nil { | |
| panic(err) // Only will happen if the provided client is nil. | |
| } | |
| if opts.APIBasePath != "" { | |
| serv.BasePath = opts.APIBasePath | |
| } | |
| ua := opts.UserAgent | |
| if ua == "" { | |
| ua = defaultUserAgent | |
| } | |
| serv.UserAgent = ua | |
| // Set default value to be "PUBLIC,PRIVATE" if not specified | |
| if len(opts.IPAddrTypeOpts) == 0 { | |
| opts.IPAddrTypeOpts = []string{"PUBLIC", "PRIVATE"} | |
| } | |
| // Add "PUBLIC" as an alias for "PRIMARY" | |
| for index, ipAddressType := range opts.IPAddrTypeOpts { | |
| if strings.ToUpper(ipAddressType) == "PUBLIC" { | |
| opts.IPAddrTypeOpts[index] = "PRIMARY" | |
| } | |
| } | |
| certSource := &RemoteCertSource{ | |
| serv: serv, | |
| checkRegion: !opts.IgnoreRegion, | |
| IPAddrTypes: opts.IPAddrTypeOpts, | |
| EnableIAMLogin: opts.EnableIAMLogin, | |
| TokenSource: opts.TokenSource, | |
| } | |
| if !opts.DelayKeyGenerate { | |
| // Generate the RSA key now, but don't block on it. | |
| go certSource.generateKey() | |
| } | |
| return certSource | |
| } | |
| // RemoteCertSource implements a CertSource, using Cloud SQL APIs to | |
| // return Local certificates for identifying oneself as a specific user | |
| // to the remote instance and Remote certificates for confirming the | |
| // remote database's identity. | |
| type RemoteCertSource struct { | |
| // keyOnce is used to create `key` lazily. | |
| keyOnce sync.Once | |
| // key is the private key used for certificates returned by Local. | |
| key *rsa.PrivateKey | |
| // serv is used to make authenticated API calls to Cloud SQL. | |
| serv *sqladmin.Service | |
| // If set, providing an incorrect region in their connection string will be | |
| // treated as an error. This is to provide the same functionality that will | |
| // occur when API calls require the region. | |
| checkRegion bool | |
| // a list of ip address types that users select | |
| IPAddrTypes []string | |
| // flag to enable IAM proxy db authentication | |
| EnableIAMLogin bool | |
| // token source for the token information used in cert creation | |
| TokenSource oauth2.TokenSource | |
| } | |
| // Constants for backoffAPIRetry. These cause the retry logic to scale the | |
| // backoff delay from 200ms to around 3.5s. | |
| const ( | |
| baseBackoff = float64(200 * time.Millisecond) | |
| backoffMult = 1.618 | |
| backoffRetries = 5 | |
| ) | |
| func backoffAPIRetry(desc, instance string, do func(staleRead time.Time) error) error { | |
| var ( | |
| err error | |
| t time.Time | |
| ) | |
| for i := 0; i < backoffRetries; i++ { | |
| err = do(t) | |
| gErr, ok := err.(*googleapi.Error) | |
| switch { | |
| case !ok: | |
| // 'ok' will also be false if err is nil. | |
| return err | |
| case gErr.Code == 403 && len(gErr.Errors) > 0 && gErr.Errors[0].Reason == "insufficientPermissions": | |
| // The case where the admin API has not yet been enabled. | |
| return fmt.Errorf("ensure that the Cloud SQL API is enabled for your project (https://console.cloud.google.com/flows/enableapi?apiid=sqladmin). Error during %s %s: %v", desc, instance, err) | |
| case gErr.Code == 404 || gErr.Code == 403: | |
| return fmt.Errorf("ensure that the account has access to %q (and make sure there's no typo in that name). Error during %s %s: %v", instance, desc, instance, err) | |
| case gErr.Code < 500: | |
| // Only Server-level HTTP errors are immediately retryable. | |
| return err | |
| } | |
| // sleep = baseBackoff * backoffMult^(retries + randomFactor) | |
| exp := float64(i+1) + mrand.Float64() | |
| sleep := time.Duration(baseBackoff * math.Pow(backoffMult, exp)) | |
| logging.Errorf("Error in %s %s: %v; retrying in %v", desc, instance, err, sleep) | |
| time.Sleep(sleep) | |
| // Create timestamp 30 seconds before now for stale read requests | |
| t = time.Now().UTC().Add(-30 * time.Second) | |
| } | |
| return err | |
| } | |
| func refreshToken(ts oauth2.TokenSource, tok *oauth2.Token) (*oauth2.Token, error) { | |
| expiredToken := &oauth2.Token{ | |
| AccessToken: tok.AccessToken, | |
| TokenType: tok.TokenType, | |
| RefreshToken: tok.RefreshToken, | |
| Expiry: time.Time{}.Add(1), // Expired | |
| } | |
| return oauth2.ReuseTokenSource(expiredToken, ts).Token() | |
| } | |
| // Local returns a certificate that may be used to establish a TLS | |
| // connection to the specified instance. | |
| func (s *RemoteCertSource) Local(instance string) (tls.Certificate, error) { | |
| pkix, err := x509.MarshalPKIXPublicKey(s.generateKey().Public()) | |
| if err != nil { | |
| return tls.Certificate{}, err | |
| } | |
| p, r, n := util.SplitName(instance) | |
| regionName := fmt.Sprintf("%s~%s", r, n) | |
| pubKey := string(pem.EncodeToMemory(&pem.Block{Bytes: pkix, Type: "RSA PUBLIC KEY"})) | |
| generateEphemeralCertRequest := &sqladmin.GenerateEphemeralCertRequest{ | |
| PublicKey: pubKey, | |
| } | |
| var tok *oauth2.Token | |
| // If IAM login is enabled, add the OAuth2 token into the ephemeral | |
| // certificate request. | |
| if s.EnableIAMLogin { | |
| var tokErr error | |
| tok, tokErr = s.TokenSource.Token() | |
| if tokErr != nil { | |
| return tls.Certificate{}, tokErr | |
| } | |
| // Always refresh the token to ensure its expiration is far enough in | |
| // the future. | |
| tok, tokErr = refreshToken(s.TokenSource, tok) | |
| if tokErr != nil { | |
| return tls.Certificate{}, tokErr | |
| } | |
| generateEphemeralCertRequest.AccessToken = tok.AccessToken | |
| } | |
| req := s.serv.Connect.GenerateEphemeralCert(p, regionName, generateEphemeralCertRequest) | |
| var data *sqladmin.GenerateEphemeralCertResponse | |
| err = backoffAPIRetry("generateEphemeral for", instance, func(staleRead time.Time) error { | |
| if !staleRead.IsZero() { | |
| generateEphemeralCertRequest.ReadTime = staleRead.Format(time.RFC3339) | |
| } | |
| data, err = req.Do() | |
| return err | |
| }) | |
| if err != nil { | |
| return tls.Certificate{}, err | |
| } | |
| c, err := parseCert(data.EphemeralCert.Cert) | |
| if err != nil { | |
| return tls.Certificate{}, fmt.Errorf("couldn't parse ephemeral certificate for instance %q: %v", instance, err) | |
| } | |
| if s.EnableIAMLogin { | |
| // Adjust the certificate's expiration to be the earlier of tok.Expiry or c.NotAfter | |
| if tok.Expiry.Before(c.NotAfter) { | |
| c.NotAfter = tok.Expiry | |
| } | |
| } | |
| return tls.Certificate{ | |
| Certificate: [][]byte{c.Raw}, | |
| PrivateKey: s.generateKey(), | |
| Leaf: c, | |
| }, nil | |
| } | |
| func parseCert(pemCert string) (*x509.Certificate, error) { | |
| bl, _ := pem.Decode([]byte(pemCert)) | |
| if bl == nil { | |
| return nil, errors.New("invalid PEM: " + pemCert) | |
| } | |
| return x509.ParseCertificate(bl.Bytes) | |
| } | |
| // Return the RSA private key, which is lazily initialized. | |
| func (s *RemoteCertSource) generateKey() *rsa.PrivateKey { | |
| s.keyOnce.Do(func() { | |
| start := time.Now() | |
| pkey, err := rsa.GenerateKey(rand.Reader, 2048) | |
| if err != nil { | |
| panic(err) // very unexpected. | |
| } | |
| logging.Verbosef("Generated RSA key in %v", time.Since(start)) | |
| s.key = pkey | |
| }) | |
| return s.key | |
| } | |
| // Find the first matching IP address by user input IP address types | |
| func (s *RemoteCertSource) findIPAddr(data *sqladmin.ConnectSettings, instance string) (ipAddrInUse string, err error) { | |
| for _, eachIPAddrTypeByUser := range s.IPAddrTypes { | |
| for _, eachIPAddrTypeOfInstance := range data.IpAddresses { | |
| if strings.ToUpper(eachIPAddrTypeOfInstance.Type) == strings.ToUpper(eachIPAddrTypeByUser) { | |
| ipAddrInUse = eachIPAddrTypeOfInstance.IpAddress | |
| return ipAddrInUse, nil | |
| } | |
| } | |
| } | |
| ipAddrTypesOfInstance := "" | |
| for _, eachIPAddrTypeOfInstance := range data.IpAddresses { | |
| ipAddrTypesOfInstance += fmt.Sprintf("(TYPE=%v, IP_ADDR=%v)", eachIPAddrTypeOfInstance.Type, eachIPAddrTypeOfInstance.IpAddress) | |
| } | |
| ipAddrTypeOfUser := fmt.Sprintf("%v", s.IPAddrTypes) | |
| return "", fmt.Errorf("User input IP address type %v does not match the instance %v, the instance's IP addresses are %v ", ipAddrTypeOfUser, instance, ipAddrTypesOfInstance) | |
| } | |
| // Remote returns the specified instance's CA certificate, address, and name. | |
| func (s *RemoteCertSource) Remote(instance string) (cert *x509.Certificate, addr, name, version string, err error) { | |
| p, region, n := util.SplitName(instance) | |
| regionName := fmt.Sprintf("%s~%s", region, n) | |
| req := s.serv.Connect.Get(p, regionName) | |
| var data *sqladmin.ConnectSettings | |
| err = backoffAPIRetry("get instance", instance, func(staleRead time.Time) error { | |
| if !staleRead.IsZero() { | |
| req.ReadTime(staleRead.Format(time.RFC3339)) | |
| } | |
| data, err = req.Do() | |
| return err | |
| }) | |
| if err != nil { | |
| return nil, "", "", "", err | |
| } | |
| // TODO(chowski): remove this when us-central is removed. | |
| if data.Region == "us-central" { | |
| data.Region = "us-central1" | |
| } | |
| if data.Region != region { | |
| if region == "" { | |
| err = fmt.Errorf("instance %v doesn't provide region", instance) | |
| } else { | |
| err = fmt.Errorf(`for connection string "%s": got region %q, want %q`, instance, region, data.Region) | |
| } | |
| if s.checkRegion { | |
| return nil, "", "", "", err | |
| } | |
| logging.Errorf("%v", err) | |
| logging.Errorf("WARNING: specifying the correct region in an instance string will become required in a future version!") | |
| } | |
| if len(data.IpAddresses) == 0 { | |
| return nil, "", "", "", fmt.Errorf("no IP address found for %v", instance) | |
| } | |
| if data.BackendType == "FIRST_GEN" { | |
| logging.Errorf("WARNING: proxy client does not support first generation Cloud SQL instances.") | |
| return nil, "", "", "", fmt.Errorf("%q is a first generation instance", instance) | |
| } | |
| // Find the first matching IP address by user input IP address types | |
| ipAddrInUse := "" | |
| ipAddrInUse, err = s.findIPAddr(data, instance) | |
| if err != nil { | |
| return nil, "", "", "", err | |
| } | |
| c, err := parseCert(data.ServerCaCert.Cert) | |
| return c, ipAddrInUse, p + ":" + n, data.DatabaseVersion, err | |
| } |