1package endpoints
2
3import (
4 "fmt"
5 "regexp"
6 "strings"
7
8 "github.com/aws/aws-sdk-go-v2/aws"
9)
10
11const (
12 defaultProtocol = "https"
13 defaultSigner = "v4"
14)
15
16var (
17 protocolPriority = []string{"https", "http"}
18 signerPriority = []string{"v4"}
19)
20
21// Options provide configuration needed to direct how endpoints are resolved.
22type Options struct {
23 // Disable usage of HTTPS (TLS / SSL)
24 DisableHTTPS bool
25}
26
27// Partitions is a slice of partition
28type Partitions []Partition
29
30// ResolveEndpoint resolves a service endpoint for the given region and options.
31func (ps Partitions) ResolveEndpoint(region string, opts Options) (aws.Endpoint, error) {
32 if len(ps) == 0 {
33 return aws.Endpoint{}, fmt.Errorf("no partitions found")
34 }
35
36 for i := 0; i < len(ps); i++ {
37 if !ps[i].canResolveEndpoint(region) {
38 continue
39 }
40
41 return ps[i].ResolveEndpoint(region, opts)
42 }
43
44 // fallback to first partition format to use when resolving the endpoint.
45 return ps[0].ResolveEndpoint(region, opts)
46}
47
48// Partition is an AWS partition description for a service and its' region endpoints.
49type Partition struct {
50 ID string
51 RegionRegex *regexp.Regexp
52 PartitionEndpoint string
53 IsRegionalized bool
54 Defaults Endpoint
55 Endpoints Endpoints
56}
57
58func (p Partition) canResolveEndpoint(region string) bool {
59 _, ok := p.Endpoints[region]
60 return ok || p.RegionRegex.MatchString(region)
61}
62
63// ResolveEndpoint resolves and service endpoint for the given region and options.
64func (p Partition) ResolveEndpoint(region string, options Options) (resolved aws.Endpoint, err error) {
65 if len(region) == 0 && len(p.PartitionEndpoint) != 0 {
66 region = p.PartitionEndpoint
67 }
68
69 e, _ := p.endpointForRegion(region)
70
71 return e.resolve(p.ID, region, p.Defaults, options), nil
72}
73
74func (p Partition) endpointForRegion(region string) (Endpoint, bool) {
75 if e, ok := p.Endpoints[region]; ok {
76 return e, true
77 }
78
79 if !p.IsRegionalized {
80 return p.Endpoints[p.PartitionEndpoint], region == p.PartitionEndpoint
81 }
82
83 // Unable to find any matching endpoint, return
84 // blank that will be used for generic endpoint creation.
85 return Endpoint{}, false
86}
87
88// Endpoints is a map of service config regions to endpoints
89type Endpoints map[string]Endpoint
90
91// CredentialScope is the credential scope of a region and service
92type CredentialScope struct {
93 Region string
94 Service string
95}
96
97// Endpoint is a service endpoint description
98type Endpoint struct {
99 // True if the endpoint cannot be resolved for this partition/region/service
100 Unresolveable aws.Ternary
101
102 Hostname string
103 Protocols []string
104
105 CredentialScope CredentialScope
106
107 SignatureVersions []string `json:"signatureVersions"`
108}
109
110func (e Endpoint) resolve(partition, region string, def Endpoint, options Options) aws.Endpoint {
111 var merged Endpoint
112 merged.mergeIn(def)
113 merged.mergeIn(e)
114 e = merged
115
116 var u string
117 if e.Unresolveable != aws.TrueTernary {
118 // Only attempt to resolve the endpoint if it can be resolved.
119 hostname := strings.Replace(e.Hostname, "{region}", region, 1)
120
121 scheme := getEndpointScheme(e.Protocols, options.DisableHTTPS)
122 u = scheme + "://" + hostname
123 }
124
125 signingRegion := e.CredentialScope.Region
126 if len(signingRegion) == 0 {
127 signingRegion = region
128 }
129 signingName := e.CredentialScope.Service
130
131 return aws.Endpoint{
132 URL: u,
133 PartitionID: partition,
134 SigningRegion: signingRegion,
135 SigningName: signingName,
136 SigningMethod: getByPriority(e.SignatureVersions, signerPriority, defaultSigner),
137 }
138}
139
140func (e *Endpoint) mergeIn(other Endpoint) {
141 if other.Unresolveable != aws.UnknownTernary {
142 e.Unresolveable = other.Unresolveable
143 }
144 if len(other.Hostname) > 0 {
145 e.Hostname = other.Hostname
146 }
147 if len(other.Protocols) > 0 {
148 e.Protocols = other.Protocols
149 }
150 if len(other.CredentialScope.Region) > 0 {
151 e.CredentialScope.Region = other.CredentialScope.Region
152 }
153 if len(other.CredentialScope.Service) > 0 {
154 e.CredentialScope.Service = other.CredentialScope.Service
155 }
156 if len(other.SignatureVersions) > 0 {
157 e.SignatureVersions = other.SignatureVersions
158 }
159}
160
161func getEndpointScheme(protocols []string, disableHTTPS bool) string {
162 if disableHTTPS {
163 return "http"
164 }
165
166 return getByPriority(protocols, protocolPriority, defaultProtocol)
167}
168
169func getByPriority(s []string, p []string, def string) string {
170 if len(s) == 0 {
171 return def
172 }
173
174 for i := 0; i < len(p); i++ {
175 for j := 0; j < len(s); j++ {
176 if s[j] == p[i] {
177 return s[j]
178 }
179 }
180 }
181
182 return s[0]
183}
184
185// MapFIPSRegion extracts the intrinsic AWS region from one that may have an
186// embedded FIPS microformat.
187func MapFIPSRegion(region string) string {
188 const fipsInfix = "-fips-"
189 const fipsPrefix = "fips-"
190 const fipsSuffix = "-fips"
191
192 if strings.Contains(region, fipsInfix) ||
193 strings.Contains(region, fipsPrefix) ||
194 strings.Contains(region, fipsSuffix) {
195 region = strings.ReplaceAll(region, fipsInfix, "-")
196 region = strings.ReplaceAll(region, fipsPrefix, "")
197 region = strings.ReplaceAll(region, fipsSuffix, "")
198 }
199
200 return region
201}