123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377 |
- var AWS = require('./core');
- var util = require('./util');
- var endpointDiscoveryEnabledEnvs = ['AWS_ENABLE_ENDPOINT_DISCOVERY', 'AWS_ENDPOINT_DISCOVERY_ENABLED'];
- /**
- * Generate key (except resources and operation part) to index the endpoints in the cache
- * If input shape has endpointdiscoveryid trait then use
- * accessKey + operation + resources + region + service as cache key
- * If input shape doesn't have endpointdiscoveryid trait then use
- * accessKey + region + service as cache key
- * @return [map<String,String>] object with keys to index endpoints.
- * @api private
- */
- function getCacheKey(request) {
- var service = request.service;
- var api = service.api || {};
- var operations = api.operations;
- var identifiers = {};
- if (service.config.region) {
- identifiers.region = service.config.region;
- }
- if (api.serviceId) {
- identifiers.serviceId = api.serviceId;
- }
- if (service.config.credentials.accessKeyId) {
- identifiers.accessKeyId = service.config.credentials.accessKeyId;
- }
- return identifiers;
- }
- /**
- * Recursive helper for marshallCustomIdentifiers().
- * Looks for required string input members that have 'endpointdiscoveryid' trait.
- * @api private
- */
- function marshallCustomIdentifiersHelper(result, params, shape) {
- if (!shape || params === undefined || params === null) return;
- if (shape.type === 'structure' && shape.required && shape.required.length > 0) {
- util.arrayEach(shape.required, function(name) {
- var memberShape = shape.members[name];
- if (memberShape.endpointDiscoveryId === true) {
- var locationName = memberShape.isLocationName ? memberShape.name : name;
- result[locationName] = String(params[name]);
- } else {
- marshallCustomIdentifiersHelper(result, params[name], memberShape);
- }
- });
- }
- }
- /**
- * Get custom identifiers for cache key.
- * Identifies custom identifiers by checking each shape's `endpointDiscoveryId` trait.
- * @param [object] request object
- * @param [object] input shape of the given operation's api
- * @api private
- */
- function marshallCustomIdentifiers(request, shape) {
- var identifiers = {};
- marshallCustomIdentifiersHelper(identifiers, request.params, shape);
- return identifiers;
- }
- /**
- * Call endpoint discovery operation when it's optional.
- * When endpoint is available in cache then use the cached endpoints. If endpoints
- * are unavailable then use regional endpoints and call endpoint discovery operation
- * asynchronously. This is turned off by default.
- * @param [object] request object
- * @api private
- */
- function optionalDiscoverEndpoint(request) {
- var service = request.service;
- var api = service.api;
- var operationModel = api.operations ? api.operations[request.operation] : undefined;
- var inputShape = operationModel ? operationModel.input : undefined;
- var identifiers = marshallCustomIdentifiers(request, inputShape);
- var cacheKey = getCacheKey(request);
- if (Object.keys(identifiers).length > 0) {
- cacheKey = util.update(cacheKey, identifiers);
- if (operationModel) cacheKey.operation = operationModel.name;
- }
- var endpoints = AWS.endpointCache.get(cacheKey);
- if (endpoints && endpoints.length === 1 && endpoints[0].Address === '') {
- //endpoint operation is being made but response not yet received
- //or endpoint operation just failed in 1 minute
- return;
- } else if (endpoints && endpoints.length > 0) {
- //found endpoint record from cache
- request.httpRequest.updateEndpoint(endpoints[0].Address);
- } else {
- //endpoint record not in cache or outdated. make discovery operation
- var endpointRequest = service.makeRequest(api.endpointOperation, {
- Operation: operationModel.name,
- Identifiers: identifiers,
- });
- addApiVersionHeader(endpointRequest);
- endpointRequest.removeListener('validate', AWS.EventListeners.Core.VALIDATE_PARAMETERS);
- endpointRequest.removeListener('retry', AWS.EventListeners.Core.RETRY_CHECK);
- //put in a placeholder for endpoints already requested, prevent
- //too much in-flight calls
- AWS.endpointCache.put(cacheKey, [{
- Address: '',
- CachePeriodInMinutes: 1
- }]);
- endpointRequest.send(function(err, data) {
- if (data && data.Endpoints) {
- AWS.endpointCache.put(cacheKey, data.Endpoints);
- } else if (err) {
- AWS.endpointCache.put(cacheKey, [{
- Address: '',
- CachePeriodInMinutes: 1 //not to make more endpoint operation in next 1 minute
- }]);
- }
- });
- }
- }
- var requestQueue = {};
- /**
- * Call endpoint discovery operation when it's required.
- * When endpoint is available in cache then use cached ones. If endpoints are
- * unavailable then SDK should call endpoint operation then use returned new
- * endpoint for the api call. SDK will automatically attempt to do endpoint
- * discovery. This is turned off by default
- * @param [object] request object
- * @api private
- */
- function requiredDiscoverEndpoint(request, done) {
- var service = request.service;
- var api = service.api;
- var operationModel = api.operations ? api.operations[request.operation] : undefined;
- var inputShape = operationModel ? operationModel.input : undefined;
- var identifiers = marshallCustomIdentifiers(request, inputShape);
- var cacheKey = getCacheKey(request);
- if (Object.keys(identifiers).length > 0) {
- cacheKey = util.update(cacheKey, identifiers);
- if (operationModel) cacheKey.operation = operationModel.name;
- }
- var cacheKeyStr = AWS.EndpointCache.getKeyString(cacheKey);
- var endpoints = AWS.endpointCache.get(cacheKeyStr); //endpoint cache also accepts string keys
- if (endpoints && endpoints.length === 1 && endpoints[0].Address === '') {
- //endpoint operation is being made but response not yet received
- //push request object to a pending queue
- if (!requestQueue[cacheKeyStr]) requestQueue[cacheKeyStr] = [];
- requestQueue[cacheKeyStr].push({request: request, callback: done});
- return;
- } else if (endpoints && endpoints.length > 0) {
- request.httpRequest.updateEndpoint(endpoints[0].Address);
- done();
- } else {
- var endpointRequest = service.makeRequest(api.endpointOperation, {
- Operation: operationModel.name,
- Identifiers: identifiers,
- });
- endpointRequest.removeListener('validate', AWS.EventListeners.Core.VALIDATE_PARAMETERS);
- addApiVersionHeader(endpointRequest);
- //put in a placeholder for endpoints already requested, prevent
- //too much in-flight calls
- AWS.endpointCache.put(cacheKeyStr, [{
- Address: '',
- CachePeriodInMinutes: 60 //long-live cache
- }]);
- endpointRequest.send(function(err, data) {
- if (err) {
- request.response.error = util.error(err, { retryable: false });
- AWS.endpointCache.remove(cacheKey);
- //fail all the pending requests in batch
- if (requestQueue[cacheKeyStr]) {
- var pendingRequests = requestQueue[cacheKeyStr];
- util.arrayEach(pendingRequests, function(requestContext) {
- requestContext.request.response.error = util.error(err, { retryable: false });
- requestContext.callback();
- });
- delete requestQueue[cacheKeyStr];
- }
- } else if (data) {
- AWS.endpointCache.put(cacheKeyStr, data.Endpoints);
- request.httpRequest.updateEndpoint(data.Endpoints[0].Address);
- //update the endpoint for all the pending requests in batch
- if (requestQueue[cacheKeyStr]) {
- var pendingRequests = requestQueue[cacheKeyStr];
- util.arrayEach(pendingRequests, function(requestContext) {
- requestContext.request.httpRequest.updateEndpoint(data.Endpoints[0].Address);
- requestContext.callback();
- });
- delete requestQueue[cacheKeyStr];
- }
- }
- done();
- });
- }
- }
- /**
- * add api version header to endpoint operation
- * @api private
- */
- function addApiVersionHeader(endpointRequest) {
- var api = endpointRequest.service.api;
- var apiVersion = api.apiVersion;
- if (apiVersion && !endpointRequest.httpRequest.headers['x-amz-api-version']) {
- endpointRequest.httpRequest.headers['x-amz-api-version'] = apiVersion;
- }
- }
- /**
- * If api call gets invalid endpoint exception, SDK should attempt to remove the invalid
- * endpoint from cache.
- * @api private
- */
- function invalidateCachedEndpoints(response) {
- var error = response.error;
- var httpResponse = response.httpResponse;
- if (error &&
- (error.code === 'InvalidEndpointException' || httpResponse.statusCode === 421)
- ) {
- var request = response.request;
- var operations = request.service.api.operations || {};
- var inputShape = operations[request.operation] ? operations[request.operation].input : undefined;
- var identifiers = marshallCustomIdentifiers(request, inputShape);
- var cacheKey = getCacheKey(request);
- if (Object.keys(identifiers).length > 0) {
- cacheKey = util.update(cacheKey, identifiers);
- if (operations[request.operation]) cacheKey.operation = operations[request.operation].name;
- }
- AWS.endpointCache.remove(cacheKey);
- }
- }
- /**
- * If endpoint is explicitly configured, SDK should not do endpoint discovery in anytime.
- * @param [object] client Service client object.
- * @api private
- */
- function hasCustomEndpoint(client) {
- //if set endpoint is set for specific client, enable endpoint discovery will raise an error.
- if (client._originalConfig && client._originalConfig.endpoint && client._originalConfig.endpointDiscoveryEnabled === true) {
- throw util.error(new Error(), {
- code: 'ConfigurationException',
- message: 'Custom endpoint is supplied; endpointDiscoveryEnabled must not be true.'
- });
- };
- var svcConfig = AWS.config[client.serviceIdentifier] || {};
- return Boolean(AWS.config.endpoint || svcConfig.endpoint || (client._originalConfig && client._originalConfig.endpoint));
- }
- /**
- * @api private
- */
- function isFalsy(value) {
- return ['false', '0'].indexOf(value) >= 0;
- }
- /**
- * If endpoint discovery should perform for this request when no operation requires endpoint
- * discovery for the given service.
- * SDK performs config resolution in order like below:
- * 1. If set in client configuration.
- * 2. If set in env AWS_ENABLE_ENDPOINT_DISCOVERY.
- * 3. If set in shared ini config file with key 'endpoint_discovery_enabled'.
- * @param [object] request request object.
- * @returns [boolean|undefined] if endpoint discovery config is not set in any source, this
- * function returns undefined
- * @api private
- */
- function resolveEndpointDiscoveryConfig(request) {
- var service = request.service || {};
- if (service.config.endpointDiscoveryEnabled !== undefined) {
- return service.config.endpointDiscoveryEnabled;
- }
- //shared ini file is only available in Node
- //not to check env in browser
- if (util.isBrowser()) return undefined;
- // If any of recognized endpoint discovery config env is set
- for (var i = 0; i < endpointDiscoveryEnabledEnvs.length; i++) {
- var env = endpointDiscoveryEnabledEnvs[i];
- if (Object.prototype.hasOwnProperty.call(process.env, env)) {
- if (process.env[env] === '' || process.env[env] === undefined) {
- throw util.error(new Error(), {
- code: 'ConfigurationException',
- message: 'environmental variable ' + env + ' cannot be set to nothing'
- });
- }
- return !isFalsy(process.env[env]);
- }
- }
- var configFile = {};
- try {
- configFile = AWS.util.iniLoader ? AWS.util.iniLoader.loadFrom({
- isConfig: true,
- filename: process.env[AWS.util.sharedConfigFileEnv]
- }) : {};
- } catch (e) {}
- var sharedFileConfig = configFile[
- process.env.AWS_PROFILE || AWS.util.defaultProfile
- ] || {};
- if (Object.prototype.hasOwnProperty.call(sharedFileConfig, 'endpoint_discovery_enabled')) {
- if (sharedFileConfig.endpoint_discovery_enabled === undefined) {
- throw util.error(new Error(), {
- code: 'ConfigurationException',
- message: 'config file entry \'endpoint_discovery_enabled\' cannot be set to nothing'
- });
- }
- return !isFalsy(sharedFileConfig.endpoint_discovery_enabled);
- }
- return undefined;
- }
- /**
- * attach endpoint discovery logic to request object
- * @param [object] request
- * @api private
- */
- function discoverEndpoint(request, done) {
- var service = request.service || {};
- if (hasCustomEndpoint(service) || request.isPresigned()) return done();
- var operations = service.api.operations || {};
- var operationModel = operations[request.operation];
- var isEndpointDiscoveryRequired = operationModel ? operationModel.endpointDiscoveryRequired : 'NULL';
- var isEnabled = resolveEndpointDiscoveryConfig(request);
- var hasRequiredEndpointDiscovery = service.api.hasRequiredEndpointDiscovery;
- if (isEnabled || hasRequiredEndpointDiscovery) {
- // Once a customer enables endpoint discovery, the SDK should start appending
- // the string endpoint-discovery to the user-agent on all requests.
- request.httpRequest.appendToUserAgent('endpoint-discovery');
- }
- switch (isEndpointDiscoveryRequired) {
- case 'OPTIONAL':
- if (isEnabled || hasRequiredEndpointDiscovery) {
- // For a given service; if at least one operation requires endpoint discovery then the SDK must enable endpoint discovery
- // by default for all operations of that service, including operations where endpoint discovery is optional.
- optionalDiscoverEndpoint(request);
- request.addNamedListener('INVALIDATE_CACHED_ENDPOINTS', 'extractError', invalidateCachedEndpoints);
- }
- done();
- break;
- case 'REQUIRED':
- if (isEnabled === false) {
- // For a given operation; if endpoint discovery is required and it has been disabled on the SDK client,
- // then the SDK must return a clear and actionable exception.
- request.response.error = util.error(new Error(), {
- code: 'ConfigurationException',
- message: 'Endpoint Discovery is disabled but ' + service.api.className + '.' + request.operation +
- '() requires it. Please check your configurations.'
- });
- done();
- break;
- }
- request.addNamedListener('INVALIDATE_CACHED_ENDPOINTS', 'extractError', invalidateCachedEndpoints);
- requiredDiscoverEndpoint(request, done);
- break;
- case 'NULL':
- default:
- done();
- break;
- }
- }
- module.exports = {
- discoverEndpoint: discoverEndpoint,
- requiredDiscoverEndpoint: requiredDiscoverEndpoint,
- optionalDiscoverEndpoint: optionalDiscoverEndpoint,
- marshallCustomIdentifiers: marshallCustomIdentifiers,
- getCacheKey: getCacheKey,
- invalidateCachedEndpoint: invalidateCachedEndpoints,
- };
|