github.com/gravitational/teleport/api@v0.0.0-20240507183017-3110591cbafc/utils/aws/endpoint.go (about)

     1  /*
     2  Copyright 2022 Gravitational, Inc.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package aws
    18  
    19  import (
    20  	"fmt"
    21  	"net"
    22  	"net/url"
    23  	"strconv"
    24  	"strings"
    25  
    26  	"github.com/gravitational/trace"
    27  )
    28  
    29  // IsAWSEndpoint returns true if the input URI is an AWS endpoint.
    30  func IsAWSEndpoint(uri string) bool {
    31  	// Note that AWSCNEndpointSuffix contains AWSEndpointSuffix so there is no
    32  	// need to search for AWSCNEndpointSuffix explicitly.
    33  	return strings.Contains(uri, AWSEndpointSuffix)
    34  }
    35  
    36  // IsRDSEndpoint returns true if the input URI is an RDS endpoint.
    37  //
    38  // https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Overview.Endpoints.html
    39  func IsRDSEndpoint(uri string) bool {
    40  	return isAWSServiceEndpoint(uri, RDSServiceName)
    41  }
    42  
    43  // IsRedshiftEndpoint returns true if the input URI is an Redshift endpoint.
    44  //
    45  // https://docs.aws.amazon.com/redshift/latest/mgmt/connecting-from-psql.html
    46  func IsRedshiftEndpoint(uri string) bool {
    47  	return isAWSServiceEndpoint(uri, RedshiftServiceName)
    48  }
    49  
    50  // IsRedshiftServerlessEndpoint returns true if the input URI is an Redshift
    51  // Serverless endpoint.
    52  //
    53  // https://docs.aws.amazon.com/redshift/latest/mgmt/serverless-connecting.html
    54  func IsRedshiftServerlessEndpoint(uri string) bool {
    55  	return isAWSServiceEndpoint(uri, RedshiftServerlessServiceName)
    56  }
    57  
    58  // IsElastiCacheEndpoint returns true if the input URI is an ElastiCache
    59  // endpoint.
    60  func IsElastiCacheEndpoint(uri string) bool {
    61  	return isAWSServiceEndpoint(uri, ElastiCacheServiceName)
    62  }
    63  
    64  // IsMemoryDBEndpoint returns true if the input URI is an MemoryDB
    65  // endpoint.
    66  func IsMemoryDBEndpoint(uri string) bool {
    67  	return isAWSServiceEndpoint(uri, MemoryDBSServiceName)
    68  }
    69  
    70  // IsKeyspacesEndpoint returns true if input URI is an AWS Keyspaces endpoint.
    71  // https://docs.aws.amazon.com/keyspaces/latest/devguide/programmatic.endpoints.html
    72  func IsKeyspacesEndpoint(uri string) bool {
    73  	hasCassandraPrefix := strings.HasPrefix(uri, "cassandra.") || strings.HasPrefix(uri, "cassandra-fips.")
    74  	return hasCassandraPrefix && IsAWSEndpoint(uri)
    75  }
    76  
    77  // IsOpenSearchEndpoint returns true if input URI is an OpenSearch endpoint.
    78  func IsOpenSearchEndpoint(uri string) bool {
    79  	return isAWSServiceEndpoint(uri, OpenSearchServiceName)
    80  }
    81  
    82  // RDSEndpointDetails contains information about an RDS endpoint.
    83  type RDSEndpointDetails struct {
    84  	// InstanceID is the identifier of an RDS instance.
    85  	InstanceID string
    86  	// ClusterID is the identifier of an RDS Aurora cluster.
    87  	ClusterID string
    88  	// ClusterCustomEndpointName is the identifier of an Aurora cluster custom endpoint.
    89  	ClusterCustomEndpointName string
    90  	// ProxyName is the identifier of an RDS proxy.
    91  	ProxyName string
    92  	// ProxyCustomEndpointName is the identifier of an RDS proxy custom endpoint.
    93  	ProxyCustomEndpointName string
    94  	// Region is the AWS region the database resides in.
    95  	Region string
    96  	// EndpointType specifies the type of the endpoint, if available.
    97  	//
    98  	// Note that the endpoint type of RDS Proxies are determined by their
    99  	// targets, so the endpoint type will be empty for RDS Proxies here as it
   100  	// cannot be decided by the endpoint URL itself.
   101  	EndpointType string
   102  }
   103  
   104  // IsProxy returns true if the RDS endpoint is an RDS Proxy.
   105  func (d RDSEndpointDetails) IsProxy() bool {
   106  	return d.ProxyName != "" || d.ProxyCustomEndpointName != ""
   107  }
   108  
   109  // ParseRDSEndpoint extracts the identifier and region from the provided RDS
   110  // endpoint.
   111  func ParseRDSEndpoint(endpoint string) (d *RDSEndpointDetails, err error) {
   112  	if strings.ContainsRune(endpoint, ':') {
   113  		endpoint, _, err = net.SplitHostPort(endpoint)
   114  		if err != nil {
   115  			return nil, trace.Wrap(err)
   116  		}
   117  	}
   118  
   119  	if strings.HasSuffix(endpoint, AWSCNEndpointSuffix) {
   120  		return parseRDSCNEndpoint(endpoint)
   121  	}
   122  	return parseRDSEndpoint(endpoint)
   123  }
   124  
   125  // parseRDSEndpoint extracts the identifier and region from the provided RDS
   126  // endpoint for standard regions.
   127  //
   128  // RDS/Aurora endpoints look like this:
   129  // aurora-instance-1.abcdefghijklmnop.us-west-1.rds.amazonaws.com
   130  func parseRDSEndpoint(endpoint string) (*RDSEndpointDetails, error) {
   131  	parts := strings.Split(endpoint, ".")
   132  	hasCorrectLen := len(parts) == 6 || len(parts) == 7
   133  	serviceNameIndex := len(parts) - 3
   134  	regionIndex := len(parts) - 4
   135  	suffixStart := regionIndex
   136  
   137  	if !strings.HasSuffix(endpoint, AWSEndpointSuffix) || !hasCorrectLen || parts[serviceNameIndex] != RDSServiceName {
   138  		return nil, trace.BadParameter("failed to parse %v as RDS endpoint", endpoint)
   139  	}
   140  
   141  	details, err := parseRDSWithoutSuffixes(endpoint, parts[:suffixStart], parts[regionIndex])
   142  	return details, trace.Wrap(err)
   143  }
   144  
   145  // parseRDSEndpoint extracts the identifier and region from the provided RDS
   146  // endpoint for AWS China regions.
   147  //
   148  // RDS/Aurora endpoints look like this for AWS China regions:
   149  // aurora-instance-2.abcdefghijklmnop.rds.cn-north-1.amazonaws.com.cn
   150  func parseRDSCNEndpoint(endpoint string) (*RDSEndpointDetails, error) {
   151  	parts := strings.Split(endpoint, ".")
   152  	hasCorrectLen := len(parts) == 7 || len(parts) == 8
   153  	regionIndex := len(parts) - 4
   154  	serviceNameIndex := len(parts) - 5
   155  	suffixStart := serviceNameIndex
   156  
   157  	if !strings.HasSuffix(endpoint, AWSCNEndpointSuffix) || !hasCorrectLen || parts[serviceNameIndex] != RDSServiceName {
   158  		return nil, trace.BadParameter("failed to parse %v as RDS CN endpoint", endpoint)
   159  	}
   160  
   161  	details, err := parseRDSWithoutSuffixes(endpoint, parts[:suffixStart], parts[regionIndex])
   162  	return details, trace.Wrap(err)
   163  }
   164  
   165  // parseRDSWithoutSuffixes extracts identifiers from provided parts and returns
   166  // RDSEndpointDetails. It is expected that the provided parts has either:
   167  // - two parts (e.g. aurora-instance-1.abcdefghijklmnop)
   168  // - or three parts (e.g. my-proxy-custom.endpoint.proxy-abcdefghijklmnop)
   169  // as region/service/partition suffixes are removed by the caller.
   170  func parseRDSWithoutSuffixes(endpoint string, parts []string, region string) (*RDSEndpointDetails, error) {
   171  	// RDS/Aurora instance endpoints look like this:
   172  	// aurora-instance-1.abcdefghijklmnop.<suffixes>
   173  	//
   174  	// Aurora cluster endpoints look like this:
   175  	// my-cluster.cluster-abcdefghijklmnop.<suffixes>
   176  	// my-cluster.cluster-ro-abcdefghijklmnop.<suffixes>
   177  	// my-custom.cluster-custom-abcdefghijklmnop.<suffixes>
   178  	//
   179  	// RDS Proxy "default" endpoints look like this:
   180  	// my-proxy.proxy-abcdefghijklmnop.<suffixes>
   181  	//
   182  	// RDS Proxy custom endpoints look like this:
   183  	// my-proxy-custom.endpoint.proxy-abcdefghijklmnop.<suffixes>
   184  	//
   185  	// https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Overview.Endpoints.html
   186  	// https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy-setup.html#rds-proxy-connecting
   187  	// https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy-endpoints.html
   188  	switch len(parts) {
   189  	case 2:
   190  		switch {
   191  		case strings.HasPrefix(parts[1], "cluster-custom-"):
   192  			// Note that we are not able to get the cluster ID from the cluster
   193  			// custom endpoints. The cluster ID must be provided separately in
   194  			// addition to the endpoints.
   195  			return &RDSEndpointDetails{
   196  				ClusterCustomEndpointName: parts[0],
   197  				Region:                    region,
   198  				EndpointType:              RDSEndpointTypeCustom,
   199  			}, nil
   200  
   201  		case strings.HasPrefix(parts[1], "cluster-ro-"):
   202  			return &RDSEndpointDetails{
   203  				ClusterID:    parts[0],
   204  				Region:       region,
   205  				EndpointType: RDSEndpointTypeReader,
   206  			}, nil
   207  
   208  		case strings.HasPrefix(parts[1], "cluster-"):
   209  			return &RDSEndpointDetails{
   210  				ClusterID:    parts[0],
   211  				Region:       region,
   212  				EndpointType: RDSEndpointTypePrimary,
   213  			}, nil
   214  
   215  		case strings.HasPrefix(parts[1], "proxy-"):
   216  			return &RDSEndpointDetails{
   217  				ProxyName: parts[0],
   218  				Region:    region,
   219  			}, nil
   220  
   221  		default:
   222  			return &RDSEndpointDetails{
   223  				InstanceID:   parts[0],
   224  				Region:       region,
   225  				EndpointType: RDSEndpointTypeInstance,
   226  			}, nil
   227  		}
   228  
   229  	case 3:
   230  		if strings.HasPrefix(parts[2], "proxy-") && parts[1] == "endpoint" {
   231  			return &RDSEndpointDetails{
   232  				ProxyCustomEndpointName: parts[0],
   233  				Region:                  region,
   234  			}, nil
   235  		}
   236  		return nil, trace.BadParameter("failed to parse %v as RDS Proxy custom endpoint", endpoint)
   237  
   238  	default:
   239  		return nil, trace.BadParameter("failed to parse %v as RDS endpoint", endpoint)
   240  	}
   241  }
   242  
   243  // ParseRedshiftEndpoint extracts cluster ID and region from the provided
   244  // Redshift endpoint.
   245  func ParseRedshiftEndpoint(endpoint string) (clusterID, region string, err error) {
   246  	if strings.ContainsRune(endpoint, ':') {
   247  		endpoint, _, err = net.SplitHostPort(endpoint)
   248  		if err != nil {
   249  			return "", "", trace.Wrap(err)
   250  		}
   251  	}
   252  
   253  	if strings.HasSuffix(endpoint, AWSCNEndpointSuffix) {
   254  		return parseRedshiftCNEndpoint(endpoint)
   255  	}
   256  	return parseRedshiftEndpoint(endpoint)
   257  }
   258  
   259  // parseRedshiftEndpoint extracts cluster ID and region from the provided
   260  // Redshift endpoint for standard regions.
   261  //
   262  // Redshift endpoints look like this:
   263  // redshift-cluster-1.abcdefghijklmnop.us-east-1.redshift.amazonaws.com
   264  func parseRedshiftEndpoint(endpoint string) (clusterID, region string, err error) {
   265  	parts := strings.Split(endpoint, ".")
   266  	if !strings.HasSuffix(endpoint, AWSEndpointSuffix) || len(parts) != 6 || parts[3] != RedshiftServiceName {
   267  		return "", "", trace.BadParameter("failed to parse %v as Redshift endpoint", endpoint)
   268  	}
   269  	return parts[0], parts[2], nil
   270  }
   271  
   272  // parseRedshiftCNEndpoint extracts cluster ID and region from the provided
   273  // Redshift endpoint for AWS China regions.
   274  //
   275  // Redshift endpoints look like this for AWS China regions:
   276  // redshift-cluster-2.abcdefghijklmnop.redshift.cn-north-1.amazonaws.com.cn
   277  func parseRedshiftCNEndpoint(endpoint string) (clusterID, region string, err error) {
   278  	parts := strings.Split(endpoint, ".")
   279  	if !strings.HasSuffix(endpoint, AWSCNEndpointSuffix) || len(parts) != 7 || parts[2] != RedshiftServiceName {
   280  		return "", "", trace.BadParameter("failed to parse %v as Redshift CN endpoint", endpoint)
   281  	}
   282  	return parts[0], parts[3], nil
   283  }
   284  
   285  // RedshiftServerlessEndpointDetails contains information about an Redshift
   286  // Serverless endpoint.
   287  type RedshiftServerlessEndpointDetails struct {
   288  	// WorkgroupName is the name of the workgroup.
   289  	WorkgroupName string
   290  	// EndpointName is the name of the VPC endpoint.
   291  	EndpointName string
   292  	// AccountID is the AWS Account ID.
   293  	AccountID string
   294  	// Region is the AWS region the database resides in.
   295  	Region string
   296  }
   297  
   298  // ParseRedshiftServerlessEndpoint extracts name, AWS Account ID, and region
   299  // from the provided Redshift Serverless endpoint.
   300  func ParseRedshiftServerlessEndpoint(endpoint string) (details *RedshiftServerlessEndpointDetails, err error) {
   301  	if strings.ContainsRune(endpoint, ':') {
   302  		endpoint, _, err = net.SplitHostPort(endpoint)
   303  		if err != nil {
   304  			return nil, trace.Wrap(err)
   305  		}
   306  	}
   307  
   308  	if strings.HasSuffix(endpoint, AWSCNEndpointSuffix) {
   309  		// TODO(greedy52) add AWS China support when Redshift Serverless come to those regions.
   310  		return nil, trace.NotImplemented("failed to parse %v as Redshift Serverless endpoint: AWS China regions are not supported yet", endpoint)
   311  	}
   312  	return parseRedshiftServerlessEndpoint(endpoint)
   313  }
   314  
   315  // parseRedshiftServerlessEndpoint extracts name, AWS account ID, and region
   316  // from the provided Redshift Serverless endpoint for standard regions.
   317  //
   318  // Workgroup endpoint looks like this:
   319  // <workgroup-name>.<account-id>.<region>.redshift-serverless.amazonaws.com
   320  //
   321  // VPC endpoint looks like this:
   322  // <vpc-endpoint-name>-endpoint-<some-hash>.<account-id>.<region>.redshift-serverless.amazonaws.com
   323  func parseRedshiftServerlessEndpoint(endpoint string) (*RedshiftServerlessEndpointDetails, error) {
   324  	parts := strings.Split(endpoint, ".")
   325  	if !strings.HasSuffix(endpoint, AWSEndpointSuffix) || len(parts) != 6 || parts[3] != RedshiftServerlessServiceName {
   326  		return nil, trace.BadParameter("failed to parse %v as Redshift Serverless endpoint", endpoint)
   327  	}
   328  	if endpointName, _, found := strings.Cut(parts[0], "-endpoint-"); found {
   329  		return &RedshiftServerlessEndpointDetails{
   330  			EndpointName: endpointName,
   331  			AccountID:    parts[1],
   332  			Region:       parts[2],
   333  		}, nil
   334  	}
   335  
   336  	return &RedshiftServerlessEndpointDetails{
   337  		WorkgroupName: parts[0],
   338  		AccountID:     parts[1],
   339  		Region:        parts[2],
   340  	}, nil
   341  }
   342  
   343  // RedisEndpointInfo describes details extracted from a ElastiCache or MemoryDB
   344  // Redis endpoint.
   345  type RedisEndpointInfo struct {
   346  	// ID is the identifier of the endpoint.
   347  	ID string
   348  	// Region is the AWS region for the endpoint.
   349  	Region string
   350  	// TransitEncryptionEnabled specifies if in-transit encryption (TLS) is
   351  	// enabled.
   352  	TransitEncryptionEnabled bool
   353  	// EndpointType specifies the type of the endpoint.
   354  	EndpointType string
   355  }
   356  
   357  const (
   358  	// ElastiCacheConfigurationEndpoint is the configuration endpoint that used
   359  	// for cluster mode connection.
   360  	ElastiCacheConfigurationEndpoint = "configuration"
   361  	// ElastiCachePrimaryEndpoint is the endpoint of the primary node in the
   362  	// node group.
   363  	ElastiCachePrimaryEndpoint = "primary"
   364  	// ElastiCacheReaderEndpoint is the endpoint of the replica nodes in the
   365  	// node group.
   366  	ElastiCacheReaderEndpoint = "reader"
   367  	// ElastiCacheNodeEndpoint is the endpoint that used to connect to an
   368  	// individual node.
   369  	ElastiCacheNodeEndpoint = "node"
   370  
   371  	// MemoryDBClusterEndpoint is the cluster configuration endpoint for a
   372  	// MemoryDB cluster.
   373  	MemoryDBClusterEndpoint = "cluster"
   374  	// MemoryDBNodeEndpoint is the endpoint of an individual MemoryDB node.
   375  	MemoryDBNodeEndpoint = "node"
   376  
   377  	// OpenSearchDefaultEndpoint is the default endpoint for domain.
   378  	OpenSearchDefaultEndpoint = "default"
   379  	// OpenSearchCustomEndpoint is the custom endpoint configured for domain.
   380  	OpenSearchCustomEndpoint = "custom"
   381  	// OpenSearchVPCEndpoint is the VPC endpoint for domain.
   382  	OpenSearchVPCEndpoint = "vpc"
   383  
   384  	// RDSEndpointTypePrimary is the endpoint that specifies the connection for
   385  	// the primary instance of the RDS cluster.
   386  	RDSEndpointTypePrimary = "primary"
   387  	// RDSEndpointTypeReader is the endpoint that load-balances connections
   388  	// across the Aurora Replicas that are available in an RDS cluster.
   389  	RDSEndpointTypeReader = "reader"
   390  	// RDSEndpointTypeCustom is the endpoint that specifies one of the custom
   391  	// endpoints associated with the RDS cluster.
   392  	RDSEndpointTypeCustom = "custom"
   393  	// RDSEndpointTypeInstance is the endpoint of an RDS DB instance.
   394  	RDSEndpointTypeInstance = "instance"
   395  )
   396  
   397  // ParseElastiCacheEndpoint extracts the details from the provided
   398  // ElastiCache Redis endpoint.
   399  //
   400  // https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/GettingStarted.ConnectToCacheNode.html
   401  func ParseElastiCacheEndpoint(endpoint string) (*RedisEndpointInfo, error) {
   402  	endpoint, err := removeSchemaAndPort(endpoint)
   403  	if err != nil {
   404  		return nil, trace.Wrap(err)
   405  	}
   406  
   407  	// Remove partition suffix. Note that endpoints for CN regions use the same
   408  	// format except they end with AWSCNEndpointSuffix.
   409  	endpointWithoutSuffix, _, err := removePartitionSuffix(endpoint)
   410  	if err != nil {
   411  		return nil, trace.Wrap(err)
   412  	}
   413  
   414  	// Split into parts to extract details. They look like this in general:
   415  	//   <part>.<part>.<part>.<short-region>.cache
   416  	//
   417  	// Note that ElastiCache uses short region codes like "use1".
   418  	//
   419  	// For Redis with cluster mode enabled, users can connect through either
   420  	// "configuration" endpoint or individual "node" endpoints.
   421  	// For Redis with cluster mode disabled, users can connect through either
   422  	// "primary", "reader", or individual "node" endpoints.
   423  	parts := strings.Split(endpointWithoutSuffix, ".")
   424  	if len(parts) == 5 && parts[4] == ElastiCacheServiceName {
   425  		region, ok := ShortRegionToRegion(parts[3])
   426  		if !ok {
   427  			return nil, trace.BadParameter("%v is not a valid region", parts[3])
   428  		}
   429  
   430  		// Configuration endpoint for Redis with TLS enabled looks like:
   431  		// clustercfg.my-redis-shards.xxxxxx.use1.cache.<suffix>:6379
   432  		if parts[0] == "clustercfg" {
   433  			return &RedisEndpointInfo{
   434  				ID:                       parts[1],
   435  				Region:                   region,
   436  				TransitEncryptionEnabled: true,
   437  				EndpointType:             ElastiCacheConfigurationEndpoint,
   438  			}, nil
   439  		}
   440  
   441  		// Configuration endpoint for Redis with TLS disabled looks like:
   442  		// my-redis-shards.xxxxxx.clustercfg.use1.cache.<suffix>:6379
   443  		if parts[2] == "clustercfg" {
   444  			return &RedisEndpointInfo{
   445  				ID:                       parts[0],
   446  				Region:                   region,
   447  				TransitEncryptionEnabled: false,
   448  				EndpointType:             ElastiCacheConfigurationEndpoint,
   449  			}, nil
   450  		}
   451  
   452  		// Node endpoint for Redis with TLS disabled looks like:
   453  		// my-redis-cluster-001.xxxxxx.0001.use0.cache.<suffix>:6379
   454  		// my-redis-shards-0001-001.xxxxxx.0001.use0.cache.<suffix>:6379
   455  		if isElasticCacheShardID(parts[2]) {
   456  			return &RedisEndpointInfo{
   457  				ID:                       trimElastiCacheShardAndNodeID(parts[0]),
   458  				Region:                   region,
   459  				TransitEncryptionEnabled: false,
   460  				EndpointType:             ElastiCacheNodeEndpoint,
   461  			}, nil
   462  		}
   463  
   464  		// Node, primary, reader endpoints for Redis with TLS enabled look like:
   465  		// my-redis-cluster-001.my-redis-cluster.xxxxxx.use1.cache.<suffix>:6379
   466  		// my-redis-shards-0001-001.my-redis-shards.xxxxxx.use1.cache.<suffix>:6379
   467  		// master.my-redis-cluster.xxxxxx.use1.cache.<suffix>:6379
   468  		// replica.my-redis-cluster.xxxxxx.use1.cache.<suffix>:6379
   469  		var endpointType string
   470  		switch strings.ToLower(parts[0]) {
   471  		case "master":
   472  			endpointType = ElastiCachePrimaryEndpoint
   473  		case "replica":
   474  			endpointType = ElastiCacheReaderEndpoint
   475  		default:
   476  			endpointType = ElastiCacheNodeEndpoint
   477  		}
   478  		return &RedisEndpointInfo{
   479  			ID:                       parts[1],
   480  			Region:                   region,
   481  			TransitEncryptionEnabled: true,
   482  			EndpointType:             endpointType,
   483  		}, nil
   484  	}
   485  
   486  	// Primary and reader endpoints for Redis with TLS disabled have an extra
   487  	// shard ID in the endpoints, and they look like:
   488  	// my-redis-cluster.xxxxxx.ng.0001.use1.cache.<suffix>:6379
   489  	// my-redis-cluster-ro.xxxxxx.ng.0001.use1.cache.<suffix>:6379
   490  	if len(parts) == 6 && parts[5] == ElastiCacheServiceName && isElasticCacheShardID(parts[3]) {
   491  		region, ok := ShortRegionToRegion(parts[4])
   492  		if !ok {
   493  			return nil, trace.BadParameter("%v is not a valid region", parts[4])
   494  		}
   495  
   496  		// Remove "-ro" from reader endpoint.
   497  		if strings.HasSuffix(parts[0], "-ro") {
   498  			return &RedisEndpointInfo{
   499  				ID:                       strings.TrimSuffix(parts[0], "-ro"),
   500  				Region:                   region,
   501  				TransitEncryptionEnabled: false,
   502  				EndpointType:             ElastiCacheReaderEndpoint,
   503  			}, nil
   504  		}
   505  
   506  		return &RedisEndpointInfo{
   507  			ID:                       parts[0],
   508  			Region:                   region,
   509  			TransitEncryptionEnabled: false,
   510  			EndpointType:             ElastiCachePrimaryEndpoint,
   511  		}, nil
   512  	}
   513  
   514  	return nil, trace.BadParameter("unknown ElastiCache Redis endpoint format %q", endpoint)
   515  }
   516  
   517  // isElasticCacheShardID returns true if the input part is in shard ID format.
   518  // The shard ID is a 4 character string of an integer left padded with zeros
   519  // (e.g. 0001).
   520  func isElasticCacheShardID(part string) bool {
   521  	if len(part) != 4 {
   522  		return false
   523  	}
   524  	_, err := strconv.Atoi(part)
   525  	return err == nil
   526  }
   527  
   528  // isElasticCacheNodeID returns true if the input part is in node ID format.
   529  // The node ID is a 3 character string of an integer left padded with zeros
   530  // (e.g. 001).
   531  func isElasticCacheNodeID(part string) bool {
   532  	if len(part) != 3 {
   533  		return false
   534  	}
   535  	_, err := strconv.Atoi(part)
   536  	return err == nil
   537  }
   538  
   539  // trimElastiCacheShardAndNodeID trims shard and node ID suffix from input.
   540  func trimElastiCacheShardAndNodeID(input string) string {
   541  	// input can be one of:
   542  	// <replication-group-id>
   543  	// <replication-group-id>-<node-id>
   544  	// <replication-group-id>-<shard-id>-<node-id>
   545  	parts := strings.Split(input, "-")
   546  	if len(parts) > 0 {
   547  		if isElasticCacheNodeID(parts[len(parts)-1]) {
   548  			parts = parts[:len(parts)-1]
   549  		}
   550  	}
   551  	if len(parts) > 0 {
   552  		if isElasticCacheShardID(parts[len(parts)-1]) {
   553  			parts = parts[:len(parts)-1]
   554  		}
   555  	}
   556  	return strings.Join(parts, "-")
   557  }
   558  
   559  // ParseMemoryDBEndpoint extracts the details from the provided
   560  // MemoryDB endpoint.
   561  //
   562  // https://docs.aws.amazon.com/memorydb/latest/devguide/endpoints.html
   563  func ParseMemoryDBEndpoint(endpoint string) (*RedisEndpointInfo, error) {
   564  	endpoint, err := removeSchemaAndPort(endpoint)
   565  	if err != nil {
   566  		return nil, trace.Wrap(err)
   567  	}
   568  
   569  	// Here is a sample endpoint for MemoryDB:
   570  	// clustercfg.my-memorydb.scwzlu.memorydb.ca-central-1.amazonaws.com
   571  	//
   572  	// Unlike RDS/Redshift endpoints, the service subdomain is before region.
   573  	// Unlike ElastiCache endpoints, MemoryDB uses full region name.
   574  	endpointWithoutSuffix, _, err := removePartitionSuffix(endpoint)
   575  	if err != nil {
   576  		return nil, trace.Wrap(err)
   577  	}
   578  
   579  	parts := strings.Split(endpointWithoutSuffix, ".")
   580  	if len(parts) != 5 || parts[3] != MemoryDBSServiceName {
   581  		return nil, trace.BadParameter("unknown MemoryDB endpoint format")
   582  	}
   583  
   584  	switch {
   585  	// TLS disabled cluster endpoints look like this:
   586  	// <cluster-name>.<xxxx>.clustercfg.memorydb.<region>.<suffix>
   587  	case parts[2] == "clustercfg":
   588  		return &RedisEndpointInfo{
   589  			ID:                       parts[0],
   590  			Region:                   parts[4],
   591  			TransitEncryptionEnabled: false,
   592  			EndpointType:             MemoryDBClusterEndpoint,
   593  		}, nil
   594  
   595  	// TLS enabled cluster endpoints look like this:
   596  	// clustercfg.<cluster-name>.<xxxx>.memorydb.<region>.<suffix>
   597  	case parts[0] == "clustercfg":
   598  		return &RedisEndpointInfo{
   599  			ID:                       parts[1],
   600  			Region:                   parts[4],
   601  			TransitEncryptionEnabled: true,
   602  			EndpointType:             MemoryDBClusterEndpoint,
   603  		}, nil
   604  
   605  	// TLS disabled node endpoints look like this:
   606  	// <cluster-name>-<shard-id>-<node-id>.<xxxx>.<shard-id>.memorydb.<region>.<suffix>
   607  	//
   608  	// MemoryDB and ElastiCache share same shard/node ID format.
   609  	case isElasticCacheShardID(parts[2]):
   610  		return &RedisEndpointInfo{
   611  			ID:                       trimElastiCacheShardAndNodeID(parts[0]),
   612  			Region:                   parts[4],
   613  			TransitEncryptionEnabled: false,
   614  			EndpointType:             MemoryDBNodeEndpoint,
   615  		}, nil
   616  
   617  	// TLS enabled node endpoints look like this:
   618  	// <cluster-name>-<shard-id>-<node-id>.<cluster-name>.<xxxx>.memorydb.<region>.<suffix>
   619  	default:
   620  		return &RedisEndpointInfo{
   621  			ID:                       trimElastiCacheShardAndNodeID(parts[0]),
   622  			Region:                   parts[4],
   623  			TransitEncryptionEnabled: true,
   624  			EndpointType:             MemoryDBNodeEndpoint,
   625  		}, nil
   626  	}
   627  }
   628  
   629  // isAWSServiceEndpoint returns true if uri is a valid AWS endpoint and uri
   630  // contains the provided service name as a subdomain.
   631  func isAWSServiceEndpoint(uri, serviceName string) bool {
   632  	return strings.Contains(uri, fmt.Sprintf(".%s.", serviceName)) &&
   633  		IsAWSEndpoint(uri)
   634  }
   635  
   636  func removeSchemaAndPort(endpoint string) (string, error) {
   637  	// Add a temporary schema to make a valid URL for url.Parse.
   638  	if !strings.Contains(endpoint, "://") {
   639  		endpoint = "schema://" + endpoint
   640  	}
   641  
   642  	parsedURL, err := url.Parse(endpoint)
   643  	if err != nil {
   644  		return "", trace.Wrap(err)
   645  	}
   646  
   647  	return parsedURL.Hostname(), nil
   648  }
   649  
   650  func removePartitionSuffix(endpoint string) (string, string, error) {
   651  	switch {
   652  	case strings.HasSuffix(endpoint, AWSEndpointSuffix):
   653  		return strings.TrimSuffix(endpoint, AWSEndpointSuffix), AWSEndpointSuffix, nil
   654  
   655  	case strings.HasSuffix(endpoint, AWSCNEndpointSuffix):
   656  		return strings.TrimSuffix(endpoint, AWSCNEndpointSuffix), AWSCNEndpointSuffix, nil
   657  
   658  	default:
   659  		return "", "", trace.BadParameter("%v is not a valid AWS endpoint", endpoint)
   660  	}
   661  }
   662  
   663  const (
   664  	// AWSEndpointSuffix is the endpoint suffix for AWS Standard and AWS US
   665  	// GovCloud regions.
   666  	//
   667  	// https://docs.aws.amazon.com/general/latest/gr/rande.html#regional-endpoints
   668  	// https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/using-govcloud-endpoints.html
   669  	AWSEndpointSuffix = ".amazonaws.com"
   670  
   671  	// AWSCNEndpointSuffix is the endpoint suffix for AWS China regions.
   672  	//
   673  	// https://docs.amazonaws.cn/en_us/aws/latest/userguide/endpoints-arns.html
   674  	AWSCNEndpointSuffix = ".amazonaws.com.cn"
   675  
   676  	// RDSServiceName is the service name for AWS RDS.
   677  	RDSServiceName = "rds"
   678  
   679  	// RedshiftServiceName is the service name for AWS Redshift.
   680  	RedshiftServiceName = "redshift"
   681  
   682  	// RedshiftServerlessServiceName is the service name for AWS Redshift Serverless.
   683  	RedshiftServerlessServiceName = "redshift-serverless"
   684  
   685  	// ElastiCacheServiceName is the service name for AWS ElastiCache.
   686  	ElastiCacheServiceName = "cache"
   687  
   688  	// MemoryDBSServiceName is the service name for AWS MemoryDB.
   689  	MemoryDBSServiceName = "memorydb"
   690  
   691  	// DynamoDBServiceName is the service name for AWS DynamoDB.
   692  	DynamoDBServiceName = "dynamodb"
   693  	// DynamoDBFipsServiceName is the fips variant service name for AWS DynamoDB.
   694  	DynamoDBFipsServiceName = "dynamodb-fips"
   695  	// DynamoDBStreamsServiceName is the AWS DynamoDB Streams service name.
   696  	DynamoDBStreamsServiceName = "streams.dynamodb"
   697  	// DAXServiceName is the AWS DynamoDB Accelerator service name.
   698  	DAXServiceName = "dax"
   699  
   700  	// OpenSearchServiceName is the AWS OpenSearch service name.
   701  	OpenSearchServiceName = "es"
   702  )
   703  
   704  // CassandraEndpointURLForRegion returns a Cassandra endpoint based on the provided region.
   705  // https://docs.aws.amazon.com/keyspaces/latest/devguide/programmatic.endpoints.html
   706  func CassandraEndpointURLForRegion(region string) string {
   707  	if IsCNRegion(region) {
   708  		return fmt.Sprintf("cassandra.%s%s:9142", region, AWSCNEndpointSuffix)
   709  	}
   710  	return fmt.Sprintf("cassandra.%s%s:9142", region, AWSEndpointSuffix)
   711  }
   712  
   713  // CassandraEndpointRegion returns an AWS region from cassandra endpoint:
   714  // where endpoint looks like cassandra.us-east-2.amazonaws.com
   715  // https://docs.aws.amazon.com/keyspaces/latest/devguide/programmatic.endpoints.html
   716  func CassandraEndpointRegion(endpoint string) (string, error) {
   717  	parts, _, err := extractAWSEndpointParts(endpoint)
   718  	if err != nil {
   719  		return "", trace.Wrap(err)
   720  	}
   721  	if len(parts) != 2 {
   722  		return "", trace.BadParameter("invalid Cassandra endpoint")
   723  	}
   724  	return parts[1], nil
   725  }
   726  
   727  // DynamoDBEndpointInfo describes info extracted from a DynamoDB endpoint.
   728  type DynamoDBEndpointInfo struct {
   729  	// Service is the service subdomain of the endpoint, for example "dynamodb" or "dax".
   730  	Service string
   731  	// Region is the AWS region for the endpoint, for example "us-west-1".
   732  	Region string
   733  	// Partition is the AWS partition for the endpoint, for example ".amazonaws.com"
   734  	Partition string
   735  }
   736  
   737  // ParseDynamoDBEndpoint parses and extract info from the provided DynamoDB endpoint.
   738  func ParseDynamoDBEndpoint(endpoint string) (*DynamoDBEndpointInfo, error) {
   739  	endpoint = strings.ToLower(endpoint)
   740  	parts, partition, err := extractAWSEndpointParts(endpoint)
   741  	if err != nil {
   742  		return nil, trace.Wrap(err)
   743  	}
   744  	switch len(parts) {
   745  	case 2, 3:
   746  	default:
   747  		return nil, trace.BadParameter("invalid DynamoDB endpoint %q", endpoint)
   748  	}
   749  	info := &DynamoDBEndpointInfo{
   750  		Service:   strings.Join(parts[:len(parts)-1], "."),
   751  		Region:    parts[len(parts)-1],
   752  		Partition: partition,
   753  	}
   754  
   755  	// check for recognized service name.
   756  	switch info.Service {
   757  	case DynamoDBServiceName, DynamoDBFipsServiceName,
   758  		DynamoDBStreamsServiceName, DAXServiceName:
   759  	default:
   760  		return nil, trace.BadParameter("invalid DynamoDB endpoint %q", endpoint)
   761  	}
   762  
   763  	// check that the partition is valid for the region.
   764  	if info.Region == "" || info.Partition == "" {
   765  		return nil, trace.BadParameter("invalid DynamoDB endpoint %q", endpoint)
   766  	}
   767  	switch {
   768  	case info.Partition == AWSCNEndpointSuffix && IsCNRegion(info.Region):
   769  	case info.Partition == AWSEndpointSuffix && !IsCNRegion(info.Region):
   770  	default:
   771  		return nil, trace.BadParameter("invalid AWS region %q for AWS partition %q",
   772  			info.Region, info.Partition)
   773  	}
   774  	return info, nil
   775  }
   776  
   777  // OpenSearchEndpointInfo describes info extracted from an AWS endpoint.
   778  type OpenSearchEndpointInfo struct {
   779  	// Service is the service subdomain of the endpoint. Only "es" allowed for now.
   780  	Service string
   781  	// Region is the AWS region for the endpoint, for example "us-west-1".
   782  	Region string
   783  	// Partition is the AWS partition for the endpoint, for example ".amazonaws.com"
   784  	Partition string
   785  }
   786  
   787  // ParseOpensearchEndpoint parses and extract info from the provided OpenSearch endpoint.
   788  func ParseOpensearchEndpoint(endpoint string) (*OpenSearchEndpointInfo, error) {
   789  	endpoint = strings.ToLower(endpoint)
   790  	parts, partition, err := extractAWSEndpointParts(endpoint)
   791  	if err != nil {
   792  		return nil, trace.Wrap(err)
   793  	}
   794  
   795  	if len(parts) != 3 {
   796  		return nil, trace.BadParameter("invalid OpenSearch endpoint %q, wrong number of parts %v", endpoint, len(parts))
   797  	}
   798  
   799  	info := &OpenSearchEndpointInfo{
   800  		Region:    parts[len(parts)-2],
   801  		Service:   parts[len(parts)-1],
   802  		Partition: partition,
   803  	}
   804  
   805  	// check for recognized service name.
   806  	if info.Service != OpenSearchServiceName {
   807  		return nil, trace.BadParameter("invalid OpenSearch endpoint %q, invalid service %q", endpoint, info.Service)
   808  	}
   809  
   810  	// check that the partition is valid for the region.
   811  	switch {
   812  	case info.Region == "" || info.Partition == "":
   813  		return nil, trace.BadParameter("invalid OpenSearch endpoint %q, empty partition and region", endpoint)
   814  	case info.Region == "":
   815  		return nil, trace.BadParameter("invalid OpenSearch endpoint %q, empty region", endpoint)
   816  	case info.Partition == "":
   817  		return nil, trace.BadParameter("invalid OpenSearch endpoint %q, empty partition", endpoint)
   818  	}
   819  
   820  	switch {
   821  	case info.Partition == AWSCNEndpointSuffix && IsCNRegion(info.Region):
   822  	case info.Partition == AWSEndpointSuffix && !IsCNRegion(info.Region):
   823  	default:
   824  		return nil, trace.BadParameter("invalid AWS region %q for AWS partition %q",
   825  			info.Region, info.Partition)
   826  	}
   827  	return info, nil
   828  }
   829  
   830  // DynamoDBURIForRegion constructs a DynamoDB URI based on the AWS region.
   831  // The URI uses a custom schema aws:// to differentiate an auto-generated URI from
   832  // a user-configured URI in the engine.
   833  // When the Teleport DynamoDB engine sees this custom URI schema, it will resolve
   834  // the real endpoint using the request API target.
   835  // https://docs.aws.amazon.com/general/latest/gr/ddb.html
   836  func DynamoDBURIForRegion(region string) string {
   837  	var suffix string
   838  	if IsCNRegion(region) {
   839  		suffix = AWSCNEndpointSuffix
   840  	} else {
   841  		suffix = AWSEndpointSuffix
   842  	}
   843  	return fmt.Sprintf("aws://dynamodb.%s%s", region, suffix)
   844  }
   845  
   846  // extractAWSEndpointParts strips the schema, port, and AWS suffix,
   847  // then splits the prefix by subdomain separator (".") and returns the parts and suffix.
   848  func extractAWSEndpointParts(endpoint string) ([]string, string, error) {
   849  	uri, err := removeSchemaAndPort(endpoint)
   850  	if err != nil {
   851  		return nil, "", trace.Wrap(err)
   852  	}
   853  	prefix, suffix, err := removePartitionSuffix(uri)
   854  	if err != nil {
   855  		return nil, "", trace.Wrap(err)
   856  	}
   857  	return strings.Split(prefix, "."), suffix, nil
   858  }