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] 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) { var errorParams = { code: 'EndpointDiscoveryException', message: 'Request cannot be fulfilled without specifying an endpoint', retryable: false }; request.response.error = util.error(err, errorParams); 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, errorParams); 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 endpoint discovery is optional. * SDK performs config resolution in order like below: * 1. If turned on client configuration(default to off) then turn on endpoint discovery. * 2. If turned on in env AWS_ENABLE_ENDPOINT_DISCOVERY then turn on endpoint discovery. * 3. If turned on in shared ini config file with key 'endpoint_discovery_enabled', then * turn on endpoint discovery. * @param [object] request request object. * @api private */ function isEndpointDiscoveryApplicable(request) { var service = request.service || {}; if (service.config.endpointDiscoveryEnabled === true) return true; //shared ini file is only available in Node //not to check env in browser if (util.isBrowser()) return false; 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' }); } if (!isFalsy(process.env[env])) return true; } } 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' }); } if (!isFalsy(sharedFileConfig.endpoint_discovery_enabled)) return true; } return false; } /** * 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(); if (!isEndpointDiscoveryApplicable(request)) return done(); request.httpRequest.appendToUserAgent('endpoint-discovery'); var operations = service.api.operations || {}; var operationModel = operations[request.operation]; var isEndpointDiscoveryRequired = operationModel ? operationModel.endpointDiscoveryRequired : 'NULL'; switch (isEndpointDiscoveryRequired) { case 'OPTIONAL': optionalDiscoverEndpoint(request); request.addNamedListener('INVALIDATE_CACHED_ENDPOINTS', 'extractError', invalidateCachedEndpoints); done(); break; case 'REQUIRED': 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, };