endpoints.go raw

   1  package endpoints
   2  
   3  import (
   4  	"fmt"
   5  	"github.com/aws/smithy-go/logging"
   6  	"regexp"
   7  	"strings"
   8  
   9  	"github.com/aws/aws-sdk-go-v2/aws"
  10  )
  11  
  12  // DefaultKey is a compound map key of a variant and other values.
  13  type DefaultKey struct {
  14  	Variant        EndpointVariant
  15  	ServiceVariant ServiceVariant
  16  }
  17  
  18  // EndpointKey is a compound map key of a region and associated variant value.
  19  type EndpointKey struct {
  20  	Region         string
  21  	Variant        EndpointVariant
  22  	ServiceVariant ServiceVariant
  23  }
  24  
  25  // EndpointVariant is a bit field to describe the endpoints attributes.
  26  type EndpointVariant uint64
  27  
  28  const (
  29  	// FIPSVariant indicates that the endpoint is FIPS capable.
  30  	FIPSVariant EndpointVariant = 1 << (64 - 1 - iota)
  31  
  32  	// DualStackVariant indicates that the endpoint is DualStack capable.
  33  	DualStackVariant
  34  )
  35  
  36  // ServiceVariant is a bit field to describe the service endpoint attributes.
  37  type ServiceVariant uint64
  38  
  39  const (
  40  	defaultProtocol = "https"
  41  	defaultSigner   = "v4"
  42  )
  43  
  44  var (
  45  	protocolPriority = []string{"https", "http"}
  46  	signerPriority   = []string{"v4", "s3v4"}
  47  )
  48  
  49  // Options provide configuration needed to direct how endpoints are resolved.
  50  type Options struct {
  51  	// Logger is a logging implementation that log events should be sent to.
  52  	Logger logging.Logger
  53  
  54  	// LogDeprecated indicates that deprecated endpoints should be logged to the provided logger.
  55  	LogDeprecated bool
  56  
  57  	// ResolvedRegion is the resolved region string. If provided (non-zero length) it takes priority
  58  	// over the region name passed to the ResolveEndpoint call.
  59  	ResolvedRegion string
  60  
  61  	// Disable usage of HTTPS (TLS / SSL)
  62  	DisableHTTPS bool
  63  
  64  	// Instruct the resolver to use a service endpoint that supports dual-stack.
  65  	// If a service does not have a dual-stack endpoint an error will be returned by the resolver.
  66  	UseDualStackEndpoint aws.DualStackEndpointState
  67  
  68  	// Instruct the resolver to use a service endpoint that supports FIPS.
  69  	// If a service does not have a FIPS endpoint an error will be returned by the resolver.
  70  	UseFIPSEndpoint aws.FIPSEndpointState
  71  
  72  	// ServiceVariant is a bitfield of service specified endpoint variant data.
  73  	ServiceVariant ServiceVariant
  74  }
  75  
  76  // GetEndpointVariant returns the EndpointVariant for the variant associated options.
  77  func (o Options) GetEndpointVariant() (v EndpointVariant) {
  78  	if o.UseDualStackEndpoint == aws.DualStackEndpointStateEnabled {
  79  		v |= DualStackVariant
  80  	}
  81  	if o.UseFIPSEndpoint == aws.FIPSEndpointStateEnabled {
  82  		v |= FIPSVariant
  83  	}
  84  	return v
  85  }
  86  
  87  // Partitions is a slice of partition
  88  type Partitions []Partition
  89  
  90  // ResolveEndpoint resolves a service endpoint for the given region and options.
  91  func (ps Partitions) ResolveEndpoint(region string, opts Options) (aws.Endpoint, error) {
  92  	if len(ps) == 0 {
  93  		return aws.Endpoint{}, fmt.Errorf("no partitions found")
  94  	}
  95  
  96  	if opts.Logger == nil {
  97  		opts.Logger = logging.Nop{}
  98  	}
  99  
 100  	if len(opts.ResolvedRegion) > 0 {
 101  		region = opts.ResolvedRegion
 102  	}
 103  
 104  	for i := 0; i < len(ps); i++ {
 105  		if !ps[i].canResolveEndpoint(region, opts) {
 106  			continue
 107  		}
 108  
 109  		return ps[i].ResolveEndpoint(region, opts)
 110  	}
 111  
 112  	// fallback to first partition format to use when resolving the endpoint.
 113  	return ps[0].ResolveEndpoint(region, opts)
 114  }
 115  
 116  // Partition is an AWS partition description for a service and its' region endpoints.
 117  type Partition struct {
 118  	ID                string
 119  	RegionRegex       *regexp.Regexp
 120  	PartitionEndpoint string
 121  	IsRegionalized    bool
 122  	Defaults          map[DefaultKey]Endpoint
 123  	Endpoints         Endpoints
 124  }
 125  
 126  func (p Partition) canResolveEndpoint(region string, opts Options) bool {
 127  	_, ok := p.Endpoints[EndpointKey{
 128  		Region:  region,
 129  		Variant: opts.GetEndpointVariant(),
 130  	}]
 131  	return ok || p.RegionRegex.MatchString(region)
 132  }
 133  
 134  // ResolveEndpoint resolves and service endpoint for the given region and options.
 135  func (p Partition) ResolveEndpoint(region string, options Options) (resolved aws.Endpoint, err error) {
 136  	if len(region) == 0 && len(p.PartitionEndpoint) != 0 {
 137  		region = p.PartitionEndpoint
 138  	}
 139  
 140  	endpoints := p.Endpoints
 141  
 142  	variant := options.GetEndpointVariant()
 143  	serviceVariant := options.ServiceVariant
 144  
 145  	defaults := p.Defaults[DefaultKey{
 146  		Variant:        variant,
 147  		ServiceVariant: serviceVariant,
 148  	}]
 149  
 150  	return p.endpointForRegion(region, variant, serviceVariant, endpoints).resolve(p.ID, region, defaults, options)
 151  }
 152  
 153  func (p Partition) endpointForRegion(region string, variant EndpointVariant, serviceVariant ServiceVariant, endpoints Endpoints) Endpoint {
 154  	key := EndpointKey{
 155  		Region:  region,
 156  		Variant: variant,
 157  	}
 158  
 159  	if e, ok := endpoints[key]; ok {
 160  		return e
 161  	}
 162  
 163  	if !p.IsRegionalized {
 164  		return endpoints[EndpointKey{
 165  			Region:         p.PartitionEndpoint,
 166  			Variant:        variant,
 167  			ServiceVariant: serviceVariant,
 168  		}]
 169  	}
 170  
 171  	// Unable to find any matching endpoint, return
 172  	// blank that will be used for generic endpoint creation.
 173  	return Endpoint{}
 174  }
 175  
 176  // Endpoints is a map of service config regions to endpoints
 177  type Endpoints map[EndpointKey]Endpoint
 178  
 179  // CredentialScope is the credential scope of a region and service
 180  type CredentialScope struct {
 181  	Region  string
 182  	Service string
 183  }
 184  
 185  // Endpoint is a service endpoint description
 186  type Endpoint struct {
 187  	// True if the endpoint cannot be resolved for this partition/region/service
 188  	Unresolveable aws.Ternary
 189  
 190  	Hostname  string
 191  	Protocols []string
 192  
 193  	CredentialScope CredentialScope
 194  
 195  	SignatureVersions []string
 196  
 197  	// Indicates that this endpoint is deprecated.
 198  	Deprecated aws.Ternary
 199  }
 200  
 201  // IsZero returns whether the endpoint structure is an empty (zero) value.
 202  func (e Endpoint) IsZero() bool {
 203  	switch {
 204  	case e.Unresolveable != aws.UnknownTernary:
 205  		return false
 206  	case len(e.Hostname) != 0:
 207  		return false
 208  	case len(e.Protocols) != 0:
 209  		return false
 210  	case e.CredentialScope != (CredentialScope{}):
 211  		return false
 212  	case len(e.SignatureVersions) != 0:
 213  		return false
 214  	}
 215  	return true
 216  }
 217  
 218  func (e Endpoint) resolve(partition, region string, def Endpoint, options Options) (aws.Endpoint, error) {
 219  	var merged Endpoint
 220  	merged.mergeIn(def)
 221  	merged.mergeIn(e)
 222  	e = merged
 223  
 224  	if e.IsZero() {
 225  		return aws.Endpoint{}, fmt.Errorf("unable to resolve endpoint for region: %v", region)
 226  	}
 227  
 228  	var u string
 229  	if e.Unresolveable != aws.TrueTernary {
 230  		// Only attempt to resolve the endpoint if it can be resolved.
 231  		hostname := strings.Replace(e.Hostname, "{region}", region, 1)
 232  
 233  		scheme := getEndpointScheme(e.Protocols, options.DisableHTTPS)
 234  		u = scheme + "://" + hostname
 235  	}
 236  
 237  	signingRegion := e.CredentialScope.Region
 238  	if len(signingRegion) == 0 {
 239  		signingRegion = region
 240  	}
 241  	signingName := e.CredentialScope.Service
 242  
 243  	if e.Deprecated == aws.TrueTernary && options.LogDeprecated {
 244  		options.Logger.Logf(logging.Warn, "endpoint identifier %q, url %q marked as deprecated", region, u)
 245  	}
 246  
 247  	return aws.Endpoint{
 248  		URL:           u,
 249  		PartitionID:   partition,
 250  		SigningRegion: signingRegion,
 251  		SigningName:   signingName,
 252  		SigningMethod: getByPriority(e.SignatureVersions, signerPriority, defaultSigner),
 253  	}, nil
 254  }
 255  
 256  func (e *Endpoint) mergeIn(other Endpoint) {
 257  	if other.Unresolveable != aws.UnknownTernary {
 258  		e.Unresolveable = other.Unresolveable
 259  	}
 260  	if len(other.Hostname) > 0 {
 261  		e.Hostname = other.Hostname
 262  	}
 263  	if len(other.Protocols) > 0 {
 264  		e.Protocols = other.Protocols
 265  	}
 266  	if len(other.CredentialScope.Region) > 0 {
 267  		e.CredentialScope.Region = other.CredentialScope.Region
 268  	}
 269  	if len(other.CredentialScope.Service) > 0 {
 270  		e.CredentialScope.Service = other.CredentialScope.Service
 271  	}
 272  	if len(other.SignatureVersions) > 0 {
 273  		e.SignatureVersions = other.SignatureVersions
 274  	}
 275  	if other.Deprecated != aws.UnknownTernary {
 276  		e.Deprecated = other.Deprecated
 277  	}
 278  }
 279  
 280  func getEndpointScheme(protocols []string, disableHTTPS bool) string {
 281  	if disableHTTPS {
 282  		return "http"
 283  	}
 284  
 285  	return getByPriority(protocols, protocolPriority, defaultProtocol)
 286  }
 287  
 288  func getByPriority(s []string, p []string, def string) string {
 289  	if len(s) == 0 {
 290  		return def
 291  	}
 292  
 293  	for i := 0; i < len(p); i++ {
 294  		for j := 0; j < len(s); j++ {
 295  			if s[j] == p[i] {
 296  				return s[j]
 297  			}
 298  		}
 299  	}
 300  
 301  	return s[0]
 302  }
 303