]>
Commit | Line | Data |
---|---|---|
bae9f6d2 JC |
1 | package endpoints |
2 | ||
3 | import ( | |
4 | "fmt" | |
5 | "regexp" | |
6 | "strconv" | |
7 | "strings" | |
8 | ) | |
9 | ||
10 | type partitions []partition | |
11 | ||
12 | func (ps partitions) EndpointFor(service, region string, opts ...func(*Options)) (ResolvedEndpoint, error) { | |
13 | var opt Options | |
14 | opt.Set(opts...) | |
15 | ||
16 | for i := 0; i < len(ps); i++ { | |
17 | if !ps[i].canResolveEndpoint(service, region, opt.StrictMatching) { | |
18 | continue | |
19 | } | |
20 | ||
21 | return ps[i].EndpointFor(service, region, opts...) | |
22 | } | |
23 | ||
24 | // If loose matching fallback to first partition format to use | |
25 | // when resolving the endpoint. | |
26 | if !opt.StrictMatching && len(ps) > 0 { | |
27 | return ps[0].EndpointFor(service, region, opts...) | |
28 | } | |
29 | ||
30 | return ResolvedEndpoint{}, NewUnknownEndpointError("all partitions", service, region, []string{}) | |
31 | } | |
32 | ||
33 | // Partitions satisfies the EnumPartitions interface and returns a list | |
34 | // of Partitions representing each partition represented in the SDK's | |
35 | // endpoints model. | |
36 | func (ps partitions) Partitions() []Partition { | |
37 | parts := make([]Partition, 0, len(ps)) | |
38 | for i := 0; i < len(ps); i++ { | |
39 | parts = append(parts, ps[i].Partition()) | |
40 | } | |
41 | ||
42 | return parts | |
43 | } | |
44 | ||
45 | type partition struct { | |
46 | ID string `json:"partition"` | |
47 | Name string `json:"partitionName"` | |
48 | DNSSuffix string `json:"dnsSuffix"` | |
49 | RegionRegex regionRegex `json:"regionRegex"` | |
50 | Defaults endpoint `json:"defaults"` | |
51 | Regions regions `json:"regions"` | |
52 | Services services `json:"services"` | |
53 | } | |
54 | ||
55 | func (p partition) Partition() Partition { | |
56 | return Partition{ | |
57 | id: p.ID, | |
58 | p: &p, | |
59 | } | |
60 | } | |
61 | ||
62 | func (p partition) canResolveEndpoint(service, region string, strictMatch bool) bool { | |
63 | s, hasService := p.Services[service] | |
64 | _, hasEndpoint := s.Endpoints[region] | |
65 | ||
66 | if hasEndpoint && hasService { | |
67 | return true | |
68 | } | |
69 | ||
70 | if strictMatch { | |
71 | return false | |
72 | } | |
73 | ||
74 | return p.RegionRegex.MatchString(region) | |
75 | } | |
76 | ||
77 | func (p partition) EndpointFor(service, region string, opts ...func(*Options)) (resolved ResolvedEndpoint, err error) { | |
78 | var opt Options | |
79 | opt.Set(opts...) | |
80 | ||
81 | s, hasService := p.Services[service] | |
82 | if !(hasService || opt.ResolveUnknownService) { | |
83 | // Only return error if the resolver will not fallback to creating | |
84 | // endpoint based on service endpoint ID passed in. | |
85 | return resolved, NewUnknownServiceError(p.ID, service, serviceList(p.Services)) | |
86 | } | |
87 | ||
88 | e, hasEndpoint := s.endpointForRegion(region) | |
89 | if !hasEndpoint && opt.StrictMatching { | |
90 | return resolved, NewUnknownEndpointError(p.ID, service, region, endpointList(s.Endpoints)) | |
91 | } | |
92 | ||
93 | defs := []endpoint{p.Defaults, s.Defaults} | |
94 | return e.resolve(service, region, p.DNSSuffix, defs, opt), nil | |
95 | } | |
96 | ||
97 | func serviceList(ss services) []string { | |
98 | list := make([]string, 0, len(ss)) | |
99 | for k := range ss { | |
100 | list = append(list, k) | |
101 | } | |
102 | return list | |
103 | } | |
104 | func endpointList(es endpoints) []string { | |
105 | list := make([]string, 0, len(es)) | |
106 | for k := range es { | |
107 | list = append(list, k) | |
108 | } | |
109 | return list | |
110 | } | |
111 | ||
112 | type regionRegex struct { | |
113 | *regexp.Regexp | |
114 | } | |
115 | ||
116 | func (rr *regionRegex) UnmarshalJSON(b []byte) (err error) { | |
117 | // Strip leading and trailing quotes | |
118 | regex, err := strconv.Unquote(string(b)) | |
119 | if err != nil { | |
120 | return fmt.Errorf("unable to strip quotes from regex, %v", err) | |
121 | } | |
122 | ||
123 | rr.Regexp, err = regexp.Compile(regex) | |
124 | if err != nil { | |
125 | return fmt.Errorf("unable to unmarshal region regex, %v", err) | |
126 | } | |
127 | return nil | |
128 | } | |
129 | ||
130 | type regions map[string]region | |
131 | ||
132 | type region struct { | |
133 | Description string `json:"description"` | |
134 | } | |
135 | ||
136 | type services map[string]service | |
137 | ||
138 | type service struct { | |
139 | PartitionEndpoint string `json:"partitionEndpoint"` | |
140 | IsRegionalized boxedBool `json:"isRegionalized,omitempty"` | |
141 | Defaults endpoint `json:"defaults"` | |
142 | Endpoints endpoints `json:"endpoints"` | |
143 | } | |
144 | ||
145 | func (s *service) endpointForRegion(region string) (endpoint, bool) { | |
146 | if s.IsRegionalized == boxedFalse { | |
147 | return s.Endpoints[s.PartitionEndpoint], region == s.PartitionEndpoint | |
148 | } | |
149 | ||
150 | if e, ok := s.Endpoints[region]; ok { | |
151 | return e, true | |
152 | } | |
153 | ||
154 | // Unable to find any matching endpoint, return | |
155 | // blank that will be used for generic endpoint creation. | |
156 | return endpoint{}, false | |
157 | } | |
158 | ||
159 | type endpoints map[string]endpoint | |
160 | ||
161 | type endpoint struct { | |
162 | Hostname string `json:"hostname"` | |
163 | Protocols []string `json:"protocols"` | |
164 | CredentialScope credentialScope `json:"credentialScope"` | |
165 | ||
166 | // Custom fields not modeled | |
167 | HasDualStack boxedBool `json:"-"` | |
168 | DualStackHostname string `json:"-"` | |
169 | ||
170 | // Signature Version not used | |
171 | SignatureVersions []string `json:"signatureVersions"` | |
172 | ||
173 | // SSLCommonName not used. | |
174 | SSLCommonName string `json:"sslCommonName"` | |
175 | } | |
176 | ||
177 | const ( | |
178 | defaultProtocol = "https" | |
179 | defaultSigner = "v4" | |
180 | ) | |
181 | ||
182 | var ( | |
183 | protocolPriority = []string{"https", "http"} | |
184 | signerPriority = []string{"v4", "v2"} | |
185 | ) | |
186 | ||
187 | func getByPriority(s []string, p []string, def string) string { | |
188 | if len(s) == 0 { | |
189 | return def | |
190 | } | |
191 | ||
192 | for i := 0; i < len(p); i++ { | |
193 | for j := 0; j < len(s); j++ { | |
194 | if s[j] == p[i] { | |
195 | return s[j] | |
196 | } | |
197 | } | |
198 | } | |
199 | ||
200 | return s[0] | |
201 | } | |
202 | ||
203 | func (e endpoint) resolve(service, region, dnsSuffix string, defs []endpoint, opts Options) ResolvedEndpoint { | |
204 | var merged endpoint | |
205 | for _, def := range defs { | |
206 | merged.mergeIn(def) | |
207 | } | |
208 | merged.mergeIn(e) | |
209 | e = merged | |
210 | ||
211 | hostname := e.Hostname | |
212 | ||
213 | // Offset the hostname for dualstack if enabled | |
214 | if opts.UseDualStack && e.HasDualStack == boxedTrue { | |
215 | hostname = e.DualStackHostname | |
216 | } | |
217 | ||
218 | u := strings.Replace(hostname, "{service}", service, 1) | |
219 | u = strings.Replace(u, "{region}", region, 1) | |
220 | u = strings.Replace(u, "{dnsSuffix}", dnsSuffix, 1) | |
221 | ||
222 | scheme := getEndpointScheme(e.Protocols, opts.DisableSSL) | |
223 | u = fmt.Sprintf("%s://%s", scheme, u) | |
224 | ||
225 | signingRegion := e.CredentialScope.Region | |
226 | if len(signingRegion) == 0 { | |
227 | signingRegion = region | |
228 | } | |
15c0b25d | 229 | |
bae9f6d2 | 230 | signingName := e.CredentialScope.Service |
15c0b25d | 231 | var signingNameDerived bool |
bae9f6d2 JC |
232 | if len(signingName) == 0 { |
233 | signingName = service | |
15c0b25d | 234 | signingNameDerived = true |
bae9f6d2 JC |
235 | } |
236 | ||
237 | return ResolvedEndpoint{ | |
15c0b25d AP |
238 | URL: u, |
239 | SigningRegion: signingRegion, | |
240 | SigningName: signingName, | |
241 | SigningNameDerived: signingNameDerived, | |
242 | SigningMethod: getByPriority(e.SignatureVersions, signerPriority, defaultSigner), | |
bae9f6d2 JC |
243 | } |
244 | } | |
245 | ||
246 | func getEndpointScheme(protocols []string, disableSSL bool) string { | |
247 | if disableSSL { | |
248 | return "http" | |
249 | } | |
250 | ||
251 | return getByPriority(protocols, protocolPriority, defaultProtocol) | |
252 | } | |
253 | ||
254 | func (e *endpoint) mergeIn(other endpoint) { | |
255 | if len(other.Hostname) > 0 { | |
256 | e.Hostname = other.Hostname | |
257 | } | |
258 | if len(other.Protocols) > 0 { | |
259 | e.Protocols = other.Protocols | |
260 | } | |
261 | if len(other.SignatureVersions) > 0 { | |
262 | e.SignatureVersions = other.SignatureVersions | |
263 | } | |
264 | if len(other.CredentialScope.Region) > 0 { | |
265 | e.CredentialScope.Region = other.CredentialScope.Region | |
266 | } | |
267 | if len(other.CredentialScope.Service) > 0 { | |
268 | e.CredentialScope.Service = other.CredentialScope.Service | |
269 | } | |
270 | if len(other.SSLCommonName) > 0 { | |
271 | e.SSLCommonName = other.SSLCommonName | |
272 | } | |
273 | if other.HasDualStack != boxedBoolUnset { | |
274 | e.HasDualStack = other.HasDualStack | |
275 | } | |
276 | if len(other.DualStackHostname) > 0 { | |
277 | e.DualStackHostname = other.DualStackHostname | |
278 | } | |
279 | } | |
280 | ||
281 | type credentialScope struct { | |
282 | Region string `json:"region"` | |
283 | Service string `json:"service"` | |
284 | } | |
285 | ||
286 | type boxedBool int | |
287 | ||
288 | func (b *boxedBool) UnmarshalJSON(buf []byte) error { | |
289 | v, err := strconv.ParseBool(string(buf)) | |
290 | if err != nil { | |
291 | return err | |
292 | } | |
293 | ||
294 | if v { | |
295 | *b = boxedTrue | |
296 | } else { | |
297 | *b = boxedFalse | |
298 | } | |
299 | ||
300 | return nil | |
301 | } | |
302 | ||
303 | const ( | |
304 | boxedBoolUnset boxedBool = iota | |
305 | boxedFalse | |
306 | boxedTrue | |
307 | ) |