UNPKG

13.6 kBJavaScriptView Raw
1var AWS = require('./core');
2var util = require('./util');
3var endpointDiscoveryEnabledEnvs = ['AWS_ENABLE_ENDPOINT_DISCOVERY', 'AWS_ENDPOINT_DISCOVERY_ENABLED'];
4
5/**
6 * Generate key (except resources and operation part) to index the endpoints in the cache
7 * If input shape has endpointdiscoveryid trait then use
8 * accessKey + operation + resources + region + service as cache key
9 * If input shape doesn't have endpointdiscoveryid trait then use
10 * accessKey + region + service as cache key
11 * @return [map<String,String>] object with keys to index endpoints.
12 * @api private
13 */
14function getCacheKey(request) {
15 var service = request.service;
16 var api = service.api || {};
17 var operations = api.operations;
18 var identifiers = {};
19 if (service.config.region) {
20 identifiers.region = service.config.region;
21 }
22 if (api.serviceId) {
23 identifiers.serviceId = api.serviceId;
24 }
25 if (service.config.credentials.accessKeyId) {
26 identifiers.accessKeyId = service.config.credentials.accessKeyId;
27 }
28 return identifiers;
29}
30
31/**
32 * Recursive helper for marshallCustomIdentifiers().
33 * Looks for required string input members that have 'endpointdiscoveryid' trait.
34 * @api private
35 */
36function marshallCustomIdentifiersHelper(result, params, shape) {
37 if (!shape || params === undefined || params === null) return;
38 if (shape.type === 'structure' && shape.required && shape.required.length > 0) {
39 util.arrayEach(shape.required, function(name) {
40 var memberShape = shape.members[name];
41 if (memberShape.endpointDiscoveryId === true) {
42 var locationName = memberShape.isLocationName ? memberShape.name : name;
43 result[locationName] = String(params[name]);
44 } else {
45 marshallCustomIdentifiersHelper(result, params[name], memberShape);
46 }
47 });
48 }
49}
50
51/**
52 * Get custom identifiers for cache key.
53 * Identifies custom identifiers by checking each shape's `endpointDiscoveryId` trait.
54 * @param [object] request object
55 * @param [object] input shape of the given operation's api
56 * @api private
57 */
58function marshallCustomIdentifiers(request, shape) {
59 var identifiers = {};
60 marshallCustomIdentifiersHelper(identifiers, request.params, shape);
61 return identifiers;
62}
63
64/**
65 * Call endpoint discovery operation when it's optional.
66 * When endpoint is available in cache then use the cached endpoints. If endpoints
67 * are unavailable then use regional endpoints and call endpoint discovery operation
68 * asynchronously. This is turned off by default.
69 * @param [object] request object
70 * @api private
71 */
72function optionalDiscoverEndpoint(request) {
73 var service = request.service;
74 var api = service.api;
75 var operationModel = api.operations ? api.operations[request.operation] : undefined;
76 var inputShape = operationModel ? operationModel.input : undefined;
77
78 var identifiers = marshallCustomIdentifiers(request, inputShape);
79 var cacheKey = getCacheKey(request);
80 if (Object.keys(identifiers).length > 0) {
81 cacheKey = util.update(cacheKey, identifiers);
82 if (operationModel) cacheKey.operation = operationModel.name;
83 }
84 var endpoints = AWS.endpointCache.get(cacheKey);
85 if (endpoints && endpoints.length === 1 && endpoints[0].Address === '') {
86 //endpoint operation is being made but response not yet received
87 //or endpoint operation just failed in 1 minute
88 return;
89 } else if (endpoints && endpoints.length > 0) {
90 //found endpoint record from cache
91 request.httpRequest.updateEndpoint(endpoints[0].Address);
92 } else {
93 //endpoint record not in cache or outdated. make discovery operation
94 var endpointRequest = service.makeRequest(api.endpointOperation, {
95 Operation: operationModel.name,
96 Identifiers: identifiers,
97 });
98 addApiVersionHeader(endpointRequest);
99 endpointRequest.removeListener('validate', AWS.EventListeners.Core.VALIDATE_PARAMETERS);
100 endpointRequest.removeListener('retry', AWS.EventListeners.Core.RETRY_CHECK);
101 //put in a placeholder for endpoints already requested, prevent
102 //too much in-flight calls
103 AWS.endpointCache.put(cacheKey, [{
104 Address: '',
105 CachePeriodInMinutes: 1
106 }]);
107 endpointRequest.send(function(err, data) {
108 if (data && data.Endpoints) {
109 AWS.endpointCache.put(cacheKey, data.Endpoints);
110 } else if (err) {
111 AWS.endpointCache.put(cacheKey, [{
112 Address: '',
113 CachePeriodInMinutes: 1 //not to make more endpoint operation in next 1 minute
114 }]);
115 }
116 });
117 }
118}
119
120var requestQueue = {};
121
122/**
123 * Call endpoint discovery operation when it's required.
124 * When endpoint is available in cache then use cached ones. If endpoints are
125 * unavailable then SDK should call endpoint operation then use returned new
126 * endpoint for the api call. SDK will automatically attempt to do endpoint
127 * discovery. This is turned off by default
128 * @param [object] request object
129 * @api private
130 */
131function requiredDiscoverEndpoint(request, done) {
132 var service = request.service;
133 var api = service.api;
134 var operationModel = api.operations ? api.operations[request.operation] : undefined;
135 var inputShape = operationModel ? operationModel.input : undefined;
136
137 var identifiers = marshallCustomIdentifiers(request, inputShape);
138 var cacheKey = getCacheKey(request);
139 if (Object.keys(identifiers).length > 0) {
140 cacheKey = util.update(cacheKey, identifiers);
141 if (operationModel) cacheKey.operation = operationModel.name;
142 }
143 var cacheKeyStr = AWS.EndpointCache.getKeyString(cacheKey);
144 var endpoints = AWS.endpointCache.get(cacheKeyStr); //endpoint cache also accepts string keys
145 if (endpoints && endpoints.length === 1 && endpoints[0].Address === '') {
146 //endpoint operation is being made but response not yet received
147 //push request object to a pending queue
148 if (!requestQueue[cacheKeyStr]) requestQueue[cacheKeyStr] = [];
149 requestQueue[cacheKeyStr].push({request: request, callback: done});
150 return;
151 } else if (endpoints && endpoints.length > 0) {
152 request.httpRequest.updateEndpoint(endpoints[0].Address);
153 done();
154 } else {
155 var endpointRequest = service.makeRequest(api.endpointOperation, {
156 Operation: operationModel.name,
157 Identifiers: identifiers,
158 });
159 endpointRequest.removeListener('validate', AWS.EventListeners.Core.VALIDATE_PARAMETERS);
160 addApiVersionHeader(endpointRequest);
161
162 //put in a placeholder for endpoints already requested, prevent
163 //too much in-flight calls
164 AWS.endpointCache.put(cacheKeyStr, [{
165 Address: '',
166 CachePeriodInMinutes: 60 //long-live cache
167 }]);
168 endpointRequest.send(function(err, data) {
169 if (err) {
170 var errorParams = {
171 code: 'EndpointDiscoveryException',
172 message: 'Request cannot be fulfilled without specifying an endpoint',
173 retryable: false
174 };
175 request.response.error = util.error(err, errorParams);
176 AWS.endpointCache.remove(cacheKey);
177
178 //fail all the pending requests in batch
179 if (requestQueue[cacheKeyStr]) {
180 var pendingRequests = requestQueue[cacheKeyStr];
181 util.arrayEach(pendingRequests, function(requestContext) {
182 requestContext.request.response.error = util.error(err, errorParams);
183 requestContext.callback();
184 });
185 delete requestQueue[cacheKeyStr];
186 }
187 } else if (data) {
188 AWS.endpointCache.put(cacheKeyStr, data.Endpoints);
189 request.httpRequest.updateEndpoint(data.Endpoints[0].Address);
190
191 //update the endpoint for all the pending requests in batch
192 if (requestQueue[cacheKeyStr]) {
193 var pendingRequests = requestQueue[cacheKeyStr];
194 util.arrayEach(pendingRequests, function(requestContext) {
195 requestContext.request.httpRequest.updateEndpoint(data.Endpoints[0].Address);
196 requestContext.callback();
197 });
198 delete requestQueue[cacheKeyStr];
199 }
200 }
201 done();
202 });
203 }
204}
205
206/**
207 * add api version header to endpoint operation
208 * @api private
209 */
210function addApiVersionHeader(endpointRequest) {
211 var api = endpointRequest.service.api;
212 var apiVersion = api.apiVersion;
213 if (apiVersion && !endpointRequest.httpRequest.headers['x-amz-api-version']) {
214 endpointRequest.httpRequest.headers['x-amz-api-version'] = apiVersion;
215 }
216}
217
218/**
219 * If api call gets invalid endpoint exception, SDK should attempt to remove the invalid
220 * endpoint from cache.
221 * @api private
222 */
223function invalidateCachedEndpoints(response) {
224 var error = response.error;
225 var httpResponse = response.httpResponse;
226 if (error &&
227 (error.code === 'InvalidEndpointException' || httpResponse.statusCode === 421)
228 ) {
229 var request = response.request;
230 var operations = request.service.api.operations || {};
231 var inputShape = operations[request.operation] ? operations[request.operation].input : undefined;
232 var identifiers = marshallCustomIdentifiers(request, inputShape);
233 var cacheKey = getCacheKey(request);
234 if (Object.keys(identifiers).length > 0) {
235 cacheKey = util.update(cacheKey, identifiers);
236 if (operations[request.operation]) cacheKey.operation = operations[request.operation].name;
237 }
238 AWS.endpointCache.remove(cacheKey);
239 }
240}
241
242/**
243 * If endpoint is explicitly configured, SDK should not do endpoint discovery in anytime.
244 * @param [object] client Service client object.
245 * @api private
246 */
247function hasCustomEndpoint(client) {
248 //if set endpoint is set for specific client, enable endpoint discovery will raise an error.
249 if (client._originalConfig && client._originalConfig.endpoint && client._originalConfig.endpointDiscoveryEnabled === true) {
250 throw util.error(new Error(), {
251 code: 'ConfigurationException',
252 message: 'Custom endpoint is supplied; endpointDiscoveryEnabled must not be true.'
253 });
254 };
255 var svcConfig = AWS.config[client.serviceIdentifier] || {};
256 return Boolean(AWS.config.endpoint || svcConfig.endpoint || (client._originalConfig && client._originalConfig.endpoint));
257}
258
259/**
260 * @api private
261 */
262function isFalsy(value) {
263 return ['false', '0'].indexOf(value) >= 0;
264}
265
266/**
267 * If endpoint discovery should perform for this request when endpoint discovery is optional.
268 * SDK performs config resolution in order like below:
269 * 1. If turned on client configuration(default to off) then turn on endpoint discovery.
270 * 2. If turned on in env AWS_ENABLE_ENDPOINT_DISCOVERY then turn on endpoint discovery.
271 * 3. If turned on in shared ini config file with key 'endpoint_discovery_enabled', then
272 * turn on endpoint discovery.
273 * @param [object] request request object.
274 * @api private
275 */
276function isEndpointDiscoveryApplicable(request) {
277 var service = request.service || {};
278 if (service.config.endpointDiscoveryEnabled === true) return true;
279
280 //shared ini file is only available in Node
281 //not to check env in browser
282 if (util.isBrowser()) return false;
283
284 for (var i = 0; i < endpointDiscoveryEnabledEnvs.length; i++) {
285 var env = endpointDiscoveryEnabledEnvs[i];
286 if (Object.prototype.hasOwnProperty.call(process.env, env)) {
287 if (process.env[env] === '' || process.env[env] === undefined) {
288 throw util.error(new Error(), {
289 code: 'ConfigurationException',
290 message: 'environmental variable ' + env + ' cannot be set to nothing'
291 });
292 }
293 if (!isFalsy(process.env[env])) return true;
294 }
295 }
296
297 var configFile = {};
298 try {
299 configFile = AWS.util.iniLoader ? AWS.util.iniLoader.loadFrom({
300 isConfig: true,
301 filename: process.env[AWS.util.sharedConfigFileEnv]
302 }) : {};
303 } catch (e) {}
304 var sharedFileConfig = configFile[
305 process.env.AWS_PROFILE || AWS.util.defaultProfile
306 ] || {};
307 if (Object.prototype.hasOwnProperty.call(sharedFileConfig, 'endpoint_discovery_enabled')) {
308 if (sharedFileConfig.endpoint_discovery_enabled === undefined) {
309 throw util.error(new Error(), {
310 code: 'ConfigurationException',
311 message: 'config file entry \'endpoint_discovery_enabled\' cannot be set to nothing'
312 });
313 }
314 if (!isFalsy(sharedFileConfig.endpoint_discovery_enabled)) return true;
315 }
316 return false;
317}
318
319/**
320 * attach endpoint discovery logic to request object
321 * @param [object] request
322 * @api private
323 */
324function discoverEndpoint(request, done) {
325 var service = request.service || {};
326 if (hasCustomEndpoint(service) || request.isPresigned()) return done();
327
328 if (!isEndpointDiscoveryApplicable(request)) return done();
329
330 request.httpRequest.appendToUserAgent('endpoint-discovery');
331
332 var operations = service.api.operations || {};
333 var operationModel = operations[request.operation];
334 var isEndpointDiscoveryRequired = operationModel ? operationModel.endpointDiscoveryRequired : 'NULL';
335 switch (isEndpointDiscoveryRequired) {
336 case 'OPTIONAL':
337 optionalDiscoverEndpoint(request);
338 request.addNamedListener('INVALIDATE_CACHED_ENDPOINTS', 'extractError', invalidateCachedEndpoints);
339 done();
340 break;
341 case 'REQUIRED':
342 request.addNamedListener('INVALIDATE_CACHED_ENDPOINTS', 'extractError', invalidateCachedEndpoints);
343 requiredDiscoverEndpoint(request, done);
344 break;
345 case 'NULL':
346 default:
347 done();
348 break;
349 }
350}
351
352module.exports = {
353 discoverEndpoint: discoverEndpoint,
354 requiredDiscoverEndpoint: requiredDiscoverEndpoint,
355 optionalDiscoverEndpoint: optionalDiscoverEndpoint,
356 marshallCustomIdentifiers: marshallCustomIdentifiers,
357 getCacheKey: getCacheKey,
358 invalidateCachedEndpoint: invalidateCachedEndpoints,
359};