github.com/aavshr/aws-sdk-go@v1.41.3/aws/endpoints/v3model.go (about)

     1  package endpoints
     2  
     3  import (
     4  	"fmt"
     5  	"regexp"
     6  	"strconv"
     7  	"strings"
     8  )
     9  
    10  const (
    11  	ec2MetadataEndpointIPv6 = "http://[fd00:ec2::254]/latest"
    12  	ec2MetadataEndpointIPv4 = "http://169.254.169.254/latest"
    13  )
    14  
    15  var regionValidationRegex = regexp.MustCompile(`^[[:alnum:]]([[:alnum:]\-]*[[:alnum:]])?$`)
    16  
    17  type partitions []partition
    18  
    19  func (ps partitions) EndpointFor(service, region string, opts ...func(*Options)) (ResolvedEndpoint, error) {
    20  	var opt Options
    21  	opt.Set(opts...)
    22  
    23  	for i := 0; i < len(ps); i++ {
    24  		if !ps[i].canResolveEndpoint(service, region, opt.StrictMatching) {
    25  			continue
    26  		}
    27  
    28  		return ps[i].EndpointFor(service, region, opts...)
    29  	}
    30  
    31  	// If loose matching fallback to first partition format to use
    32  	// when resolving the endpoint.
    33  	if !opt.StrictMatching && len(ps) > 0 {
    34  		return ps[0].EndpointFor(service, region, opts...)
    35  	}
    36  
    37  	return ResolvedEndpoint{}, NewUnknownEndpointError("all partitions", service, region, []string{})
    38  }
    39  
    40  // Partitions satisfies the EnumPartitions interface and returns a list
    41  // of Partitions representing each partition represented in the SDK's
    42  // endpoints model.
    43  func (ps partitions) Partitions() []Partition {
    44  	parts := make([]Partition, 0, len(ps))
    45  	for i := 0; i < len(ps); i++ {
    46  		parts = append(parts, ps[i].Partition())
    47  	}
    48  
    49  	return parts
    50  }
    51  
    52  type partition struct {
    53  	ID          string      `json:"partition"`
    54  	Name        string      `json:"partitionName"`
    55  	DNSSuffix   string      `json:"dnsSuffix"`
    56  	RegionRegex regionRegex `json:"regionRegex"`
    57  	Defaults    endpoint    `json:"defaults"`
    58  	Regions     regions     `json:"regions"`
    59  	Services    services    `json:"services"`
    60  }
    61  
    62  func (p partition) Partition() Partition {
    63  	return Partition{
    64  		dnsSuffix: p.DNSSuffix,
    65  		id:        p.ID,
    66  		p:         &p,
    67  	}
    68  }
    69  
    70  func (p partition) canResolveEndpoint(service, region string, strictMatch bool) bool {
    71  	s, hasService := p.Services[service]
    72  	_, hasEndpoint := s.Endpoints[region]
    73  
    74  	if hasEndpoint && hasService {
    75  		return true
    76  	}
    77  
    78  	if strictMatch {
    79  		return false
    80  	}
    81  
    82  	return p.RegionRegex.MatchString(region)
    83  }
    84  
    85  func allowLegacyEmptyRegion(service string) bool {
    86  	legacy := map[string]struct{}{
    87  		"budgets":       {},
    88  		"ce":            {},
    89  		"chime":         {},
    90  		"cloudfront":    {},
    91  		"ec2metadata":   {},
    92  		"iam":           {},
    93  		"importexport":  {},
    94  		"organizations": {},
    95  		"route53":       {},
    96  		"sts":           {},
    97  		"support":       {},
    98  		"waf":           {},
    99  	}
   100  
   101  	_, allowed := legacy[service]
   102  	return allowed
   103  }
   104  
   105  func (p partition) EndpointFor(service, region string, opts ...func(*Options)) (resolved ResolvedEndpoint, err error) {
   106  	var opt Options
   107  	opt.Set(opts...)
   108  
   109  	s, hasService := p.Services[service]
   110  
   111  	if service == Ec2metadataServiceID && !hasService {
   112  		endpoint := getEC2MetadataEndpoint(p.ID, service, opt.EC2MetadataEndpointMode)
   113  		return endpoint, nil
   114  	}
   115  
   116  	if len(service) == 0 || !(hasService || opt.ResolveUnknownService) {
   117  		// Only return error if the resolver will not fallback to creating
   118  		// endpoint based on service endpoint ID passed in.
   119  		return resolved, NewUnknownServiceError(p.ID, service, serviceList(p.Services))
   120  	}
   121  
   122  	if len(region) == 0 && allowLegacyEmptyRegion(service) && len(s.PartitionEndpoint) != 0 {
   123  		region = s.PartitionEndpoint
   124  	}
   125  
   126  	if (service == "sts" && opt.STSRegionalEndpoint != RegionalSTSEndpoint) ||
   127  		(service == "s3" && opt.S3UsEast1RegionalEndpoint != RegionalS3UsEast1Endpoint) {
   128  		if _, ok := legacyGlobalRegions[service][region]; ok {
   129  			region = "aws-global"
   130  		}
   131  	}
   132  
   133  	e, hasEndpoint := s.endpointForRegion(region)
   134  	if len(region) == 0 || (!hasEndpoint && opt.StrictMatching) {
   135  		return resolved, NewUnknownEndpointError(p.ID, service, region, endpointList(s.Endpoints))
   136  	}
   137  
   138  	defs := []endpoint{p.Defaults, s.Defaults}
   139  
   140  	return e.resolve(service, p.ID, region, p.DNSSuffix, defs, opt)
   141  }
   142  
   143  func getEC2MetadataEndpoint(partitionID, service string, mode EC2IMDSEndpointModeState) ResolvedEndpoint {
   144  	switch mode {
   145  	case EC2IMDSEndpointModeStateIPv6:
   146  		return ResolvedEndpoint{
   147  			URL:                ec2MetadataEndpointIPv6,
   148  			PartitionID:        partitionID,
   149  			SigningRegion:      "aws-global",
   150  			SigningName:        service,
   151  			SigningNameDerived: true,
   152  			SigningMethod:      "v4",
   153  		}
   154  	case EC2IMDSEndpointModeStateIPv4:
   155  		fallthrough
   156  	default:
   157  		return ResolvedEndpoint{
   158  			URL:                ec2MetadataEndpointIPv4,
   159  			PartitionID:        partitionID,
   160  			SigningRegion:      "aws-global",
   161  			SigningName:        service,
   162  			SigningNameDerived: true,
   163  			SigningMethod:      "v4",
   164  		}
   165  	}
   166  }
   167  
   168  func serviceList(ss services) []string {
   169  	list := make([]string, 0, len(ss))
   170  	for k := range ss {
   171  		list = append(list, k)
   172  	}
   173  	return list
   174  }
   175  func endpointList(es endpoints) []string {
   176  	list := make([]string, 0, len(es))
   177  	for k := range es {
   178  		list = append(list, k)
   179  	}
   180  	return list
   181  }
   182  
   183  type regionRegex struct {
   184  	*regexp.Regexp
   185  }
   186  
   187  func (rr *regionRegex) UnmarshalJSON(b []byte) (err error) {
   188  	// Strip leading and trailing quotes
   189  	regex, err := strconv.Unquote(string(b))
   190  	if err != nil {
   191  		return fmt.Errorf("unable to strip quotes from regex, %v", err)
   192  	}
   193  
   194  	rr.Regexp, err = regexp.Compile(regex)
   195  	if err != nil {
   196  		return fmt.Errorf("unable to unmarshal region regex, %v", err)
   197  	}
   198  	return nil
   199  }
   200  
   201  type regions map[string]region
   202  
   203  type region struct {
   204  	Description string `json:"description"`
   205  }
   206  
   207  type services map[string]service
   208  
   209  type service struct {
   210  	PartitionEndpoint string    `json:"partitionEndpoint"`
   211  	IsRegionalized    boxedBool `json:"isRegionalized,omitempty"`
   212  	Defaults          endpoint  `json:"defaults"`
   213  	Endpoints         endpoints `json:"endpoints"`
   214  }
   215  
   216  func (s *service) endpointForRegion(region string) (endpoint, bool) {
   217  	if e, ok := s.Endpoints[region]; ok {
   218  		return e, true
   219  	}
   220  
   221  	if s.IsRegionalized == boxedFalse {
   222  		return s.Endpoints[s.PartitionEndpoint], region == s.PartitionEndpoint
   223  	}
   224  
   225  	// Unable to find any matching endpoint, return
   226  	// blank that will be used for generic endpoint creation.
   227  	return endpoint{}, false
   228  }
   229  
   230  type endpoints map[string]endpoint
   231  
   232  type endpoint struct {
   233  	Hostname        string          `json:"hostname"`
   234  	Protocols       []string        `json:"protocols"`
   235  	CredentialScope credentialScope `json:"credentialScope"`
   236  
   237  	// Custom fields not modeled
   238  	HasDualStack      boxedBool `json:"-"`
   239  	DualStackHostname string    `json:"-"`
   240  
   241  	// Signature Version not used
   242  	SignatureVersions []string `json:"signatureVersions"`
   243  
   244  	// SSLCommonName not used.
   245  	SSLCommonName string `json:"sslCommonName"`
   246  }
   247  
   248  const (
   249  	defaultProtocol = "https"
   250  	defaultSigner   = "v4"
   251  )
   252  
   253  var (
   254  	protocolPriority = []string{"https", "http"}
   255  	signerPriority   = []string{"v4", "v2"}
   256  )
   257  
   258  func getByPriority(s []string, p []string, def string) string {
   259  	if len(s) == 0 {
   260  		return def
   261  	}
   262  
   263  	for i := 0; i < len(p); i++ {
   264  		for j := 0; j < len(s); j++ {
   265  			if s[j] == p[i] {
   266  				return s[j]
   267  			}
   268  		}
   269  	}
   270  
   271  	return s[0]
   272  }
   273  
   274  func (e endpoint) resolve(service, partitionID, region, dnsSuffix string, defs []endpoint, opts Options) (ResolvedEndpoint, error) {
   275  	var merged endpoint
   276  	for _, def := range defs {
   277  		merged.mergeIn(def)
   278  	}
   279  	merged.mergeIn(e)
   280  	e = merged
   281  
   282  	signingRegion := e.CredentialScope.Region
   283  	if len(signingRegion) == 0 {
   284  		signingRegion = region
   285  	}
   286  
   287  	signingName := e.CredentialScope.Service
   288  	var signingNameDerived bool
   289  	if len(signingName) == 0 {
   290  		signingName = service
   291  		signingNameDerived = true
   292  	}
   293  
   294  	hostname := e.Hostname
   295  	// Offset the hostname for dualstack if enabled
   296  	if opts.UseDualStack && e.HasDualStack == boxedTrue {
   297  		hostname = e.DualStackHostname
   298  		region = signingRegion
   299  	}
   300  
   301  	if !validateInputRegion(region) {
   302  		return ResolvedEndpoint{}, fmt.Errorf("invalid region identifier format provided")
   303  	}
   304  
   305  	u := strings.Replace(hostname, "{service}", service, 1)
   306  	u = strings.Replace(u, "{region}", region, 1)
   307  	u = strings.Replace(u, "{dnsSuffix}", dnsSuffix, 1)
   308  
   309  	scheme := getEndpointScheme(e.Protocols, opts.DisableSSL)
   310  	u = fmt.Sprintf("%s://%s", scheme, u)
   311  
   312  	return ResolvedEndpoint{
   313  		URL:                u,
   314  		PartitionID:        partitionID,
   315  		SigningRegion:      signingRegion,
   316  		SigningName:        signingName,
   317  		SigningNameDerived: signingNameDerived,
   318  		SigningMethod:      getByPriority(e.SignatureVersions, signerPriority, defaultSigner),
   319  	}, nil
   320  }
   321  
   322  func getEndpointScheme(protocols []string, disableSSL bool) string {
   323  	if disableSSL {
   324  		return "http"
   325  	}
   326  
   327  	return getByPriority(protocols, protocolPriority, defaultProtocol)
   328  }
   329  
   330  func (e *endpoint) mergeIn(other endpoint) {
   331  	if len(other.Hostname) > 0 {
   332  		e.Hostname = other.Hostname
   333  	}
   334  	if len(other.Protocols) > 0 {
   335  		e.Protocols = other.Protocols
   336  	}
   337  	if len(other.SignatureVersions) > 0 {
   338  		e.SignatureVersions = other.SignatureVersions
   339  	}
   340  	if len(other.CredentialScope.Region) > 0 {
   341  		e.CredentialScope.Region = other.CredentialScope.Region
   342  	}
   343  	if len(other.CredentialScope.Service) > 0 {
   344  		e.CredentialScope.Service = other.CredentialScope.Service
   345  	}
   346  	if len(other.SSLCommonName) > 0 {
   347  		e.SSLCommonName = other.SSLCommonName
   348  	}
   349  	if other.HasDualStack != boxedBoolUnset {
   350  		e.HasDualStack = other.HasDualStack
   351  	}
   352  	if len(other.DualStackHostname) > 0 {
   353  		e.DualStackHostname = other.DualStackHostname
   354  	}
   355  }
   356  
   357  type credentialScope struct {
   358  	Region  string `json:"region"`
   359  	Service string `json:"service"`
   360  }
   361  
   362  type boxedBool int
   363  
   364  func (b *boxedBool) UnmarshalJSON(buf []byte) error {
   365  	v, err := strconv.ParseBool(string(buf))
   366  	if err != nil {
   367  		return err
   368  	}
   369  
   370  	if v {
   371  		*b = boxedTrue
   372  	} else {
   373  		*b = boxedFalse
   374  	}
   375  
   376  	return nil
   377  }
   378  
   379  const (
   380  	boxedBoolUnset boxedBool = iota
   381  	boxedFalse
   382  	boxedTrue
   383  )
   384  
   385  func validateInputRegion(region string) bool {
   386  	return regionValidationRegex.MatchString(region)
   387  }